init: 自 zomaintain/backend/zonat 平移独立成库; module path 改为 git.zeroonesoft.cn/golib/zonat; 内部 import 同步重写
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
// zonat 被控端入口:跑在内网机器上,按隧道定义登录各自节点并注册。
|
||||
// 每条隧道自带节点地址与登录令牌(JWT),可分布在多个不同节点。
|
||||
// 隧道通过 -tunnel 参数下发(后续由 capricorn cloud 经 WS 下发,见分析文档 P1):
|
||||
//
|
||||
// -tunnel id=web1,target=127.0.0.1:3000,node=10.0.0.2:5212,token=<jwt>,port=0,ttl=0
|
||||
// -tunnel id=vnc1,target=127.0.0.1:5900,ttl=300 // node/token 取 -node/-token 缺省值
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.zeroonesoft.cn/golib/zonat/agent"
|
||||
)
|
||||
|
||||
// slogLogger 把 slog 适配为 agent.Logger 接口(独立二进制用;
|
||||
// 宿主项目若是 logrus 可直接注入 logrus.StandardLogger())
|
||||
type slogLogger struct{ l *slog.Logger }
|
||||
|
||||
func (s slogLogger) Debugf(format string, args ...any) { s.l.Debug(fmt.Sprintf(format, args...)) }
|
||||
func (s slogLogger) Infof(format string, args ...any) { s.l.Info(fmt.Sprintf(format, args...)) }
|
||||
func (s slogLogger) Warnf(format string, args ...any) { s.l.Warn(fmt.Sprintf(format, args...)) }
|
||||
func (s slogLogger) Errorf(format string, args ...any) { s.l.Error(fmt.Sprintf(format, args...)) }
|
||||
|
||||
func main() {
|
||||
nodeAddr := flag.String("node", "127.0.0.1:5212", "缺省节点桥接地址(隧道未带 node= 时使用)")
|
||||
token := flag.String("token", "", "缺省登录令牌/JWT(隧道未带 token= 时使用)")
|
||||
id := flag.String("id", "", "agent 唯一标识(默认取主机名)")
|
||||
backoff := flag.Duration("backoff", 5*time.Second, "永久隧道重连退避")
|
||||
ping := flag.Duration("ping", 30*time.Second, "控制流心跳间隔")
|
||||
var tunnels sliceFlag
|
||||
flag.Var(&tunnels, "tunnel", "隧道定义 id=..,target=ip:port[,node=addr][,token=jwt][,port=0][,ttl=0],可重复")
|
||||
flag.Parse()
|
||||
|
||||
if *id == "" {
|
||||
host, err := os.Hostname()
|
||||
if err != nil {
|
||||
slog.Error("获取主机名失败", "err", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
*id = host
|
||||
}
|
||||
|
||||
a := agent.New(*id)
|
||||
a.Logger = slogLogger{slog.Default()}
|
||||
a.Backoff = *backoff
|
||||
a.PingEvery = *ping
|
||||
a.OnTunnelPort = func(tunnelId string, port int) {
|
||||
if port > 0 {
|
||||
slog.Info("隧道端口", "id", tunnelId, "listen", port)
|
||||
}
|
||||
}
|
||||
for _, spec := range tunnels {
|
||||
t, err := parseTunnel(spec, *nodeAddr, *token)
|
||||
if err != nil {
|
||||
slog.Error("隧道参数错误", "spec", spec, "err", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
if _, err := a.RegisterTunnel(t); err != nil {
|
||||
slog.Warn("注册隧道失败(永久隧道会在后台持续重试)", "id", t.Id, "err", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if err := a.Run(); err != nil {
|
||||
slog.Error("agent 退出", "err", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// parseTunnel 解析一条隧道定义,node/token 未带时回落到全局缺省值
|
||||
func parseTunnel(spec, defNode, defToken string) (agent.Tunnel, error) {
|
||||
t := agent.Tunnel{NodeAddr: defNode, Token: defToken}
|
||||
for _, kv := range strings.Split(spec, ",") {
|
||||
parts := strings.SplitN(kv, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return t, errBad("键值对缺少=:" + kv)
|
||||
}
|
||||
switch parts[0] {
|
||||
case "id":
|
||||
t.Id = parts[1]
|
||||
case "node":
|
||||
t.NodeAddr = parts[1]
|
||||
case "token":
|
||||
t.Token = parts[1]
|
||||
case "target":
|
||||
host, portStr, err := net.SplitHostPort(parts[1])
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
t.TargetIp, t.TargetPort = host, port
|
||||
case "port":
|
||||
v, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
t.ListenPort = v
|
||||
case "ttl":
|
||||
v, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
t.TTLSec = v
|
||||
default:
|
||||
return t, errBad("未知键:" + parts[0])
|
||||
}
|
||||
}
|
||||
if t.Id == "" || t.TargetIp == "" || t.TargetPort == 0 {
|
||||
return t, errBad("缺少 id 或 target")
|
||||
}
|
||||
if t.NodeAddr == "" || t.Token == "" {
|
||||
return t, errBad("缺少 node 或 token(登录令牌/JWT)")
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func errBad(msg string) error { return &badSpec{msg} }
|
||||
|
||||
type badSpec struct{ msg string }
|
||||
|
||||
func (e *badSpec) Error() string { return e.msg }
|
||||
|
||||
type sliceFlag []string
|
||||
|
||||
func (f *sliceFlag) String() string { return strings.Join(*f, " ") }
|
||||
|
||||
func (f *sliceFlag) Set(v string) error {
|
||||
*f = append(*f, v)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// 测试目标:HTTP 服务,验证纯 TCP 隧道承载 HTTP(webshell/文件服务场景)。
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func main() {
|
||||
addr := flag.String("addr", "127.0.0.1:9001", "监听地址")
|
||||
flag.Parse()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"hello":"git.zeroonesoft.cn/golib/zonat","via":"tcp-tunnel"}`))
|
||||
})
|
||||
mux.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
|
||||
io.Copy(w, r.Body)
|
||||
})
|
||||
log.Println("http-demo 监听", *addr)
|
||||
log.Fatal(http.ListenAndServe(*addr, mux))
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// zonat 节点入口:TCP 桥接监听 + 隧道注册表 + 纯 TCP 隧道监听。
|
||||
// 无管理 API、无数据库;隧道由 agent 经桥接控制流注册。
|
||||
// agent 登录用 JWT 校验(HS256);token 正式部署由 cloud 签发,
|
||||
// 手工/测试场景可用 -print-token 现场签发。
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.zeroonesoft.cn/golib/zonat/internal/node"
|
||||
)
|
||||
|
||||
func main() {
|
||||
addr := flag.String("addr", ":5212", "桥接监听地址")
|
||||
tunnelBind := flag.String("tunnel-bind", "0.0.0.0", "隧道监听绑定地址")
|
||||
jwtSecret := flag.String("jwt-secret", "", "agent 登录 JWT 校验密钥(HS256)")
|
||||
sweep := flag.Duration("sweep", 30*time.Second, "TTL 清扫间隔")
|
||||
// 便捷签发:node -jwt-secret s3cret -print-token -agent a1 -ttl 24h
|
||||
printToken := flag.Bool("print-token", false, "用 -jwt-secret 签发一张 agent 登录 token 后退出")
|
||||
tokenAgent := flag.String("agent", "", "print-token 时写入 agentId claim")
|
||||
tokenTTL := flag.Duration("ttl", 24*time.Hour, "print-token 签发的有效期")
|
||||
flag.Parse()
|
||||
|
||||
if *printToken {
|
||||
if *jwtSecret == "" || *tokenTTL <= 0 {
|
||||
slog.Error("print-token 需要 -jwt-secret 与正的 -ttl")
|
||||
os.Exit(1)
|
||||
}
|
||||
tok, err := node.SignToken(*jwtSecret, *tokenAgent, *tokenTTL)
|
||||
if err != nil {
|
||||
slog.Error("签发失败", "err", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(tok)
|
||||
return
|
||||
}
|
||||
|
||||
if *jwtSecret == "" {
|
||||
slog.Error("缺少 -jwt-secret(agent 登录 JWT 校验密钥)")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
n := node.New()
|
||||
n.JwtSecret = *jwtSecret
|
||||
n.BindTunnel = *tunnelBind
|
||||
n.SweepInterval = *sweep
|
||||
if err := n.Start(*addr); err != nil {
|
||||
slog.Error("节点启动失败", "err", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
||||
<-quit
|
||||
n.Stop()
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// 测试目标:TCP 回显服务,模拟 VNC/RDP/3000 等内网 TCP 目标。
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
)
|
||||
|
||||
func main() {
|
||||
addr := flag.String("addr", "127.0.0.1:9000", "监听地址")
|
||||
flag.Parse()
|
||||
|
||||
l, err := net.Listen("tcp", *addr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("tcp-echo 监听", l.Addr().String())
|
||||
for {
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
go func(c net.Conn) {
|
||||
defer c.Close()
|
||||
io.Copy(c, c)
|
||||
}(conn)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user