// zonat 被控端入口:跑在内网机器上,按隧道定义登录各自节点并注册。 // 每条隧道自带节点地址与登录令牌(JWT),可分布在多个不同节点。 // 隧道通过 -tunnel 参数下发(后续由 capricorn cloud 经 WS 下发,见分析文档 P1): // // -tunnel id=web1,target=127.0.0.1:3000,node=10.0.0.2:5212,token=,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 }