// tunnel.go 一条隧道的节点连接 worker:每条隧道独立完成 // 连接→登录(携带自身 Token)→smux→控制流注册自身→数据流服务, // 因此天然支持不同隧道连接不同节点。 // 永久隧道断线按 Backoff 重连重注册;临时隧道一次性退出。 package agent import ( "errors" "fmt" "net" "sync" "time" "github.com/xtaci/smux" ) const ( loginTimeout = 10 * time.Second // 登录握手超时(含拨号) registerWait = 30 * time.Second // RegisterTunnel 等待首次注册完成的上限 ) // errTunnelClosed 节点推送 CmdTunnelClosed:隧道已被节点删除(TTL 过期等) var errTunnelClosed = errors.New("隧道已被节点关闭") // tunnelConn 一条隧道的全部运行态:期望配置 + 到该隧道节点的桥接连接 type tunnelConn struct { ag *Agent t Tunnel stopCh chan struct{} stopOnce sync.Once firstOnce sync.Once first chan struct{} // 首次"连接+登录+注册"尝试结束后关闭 firstPort int firstErr error everRegistered bool // 本次注册成功过至少一次(run 协程独占读写) mu sync.Mutex conn net.Conn session *smux.Session control net.Conn ctrlMu sync.Mutex // 控制流"写请求+读应答"串行化 } func newTunnelConn(ag *Agent, t Tunnel) *tunnelConn { return &tunnelConn{ ag: ag, t: t, stopCh: make(chan struct{}), first: make(chan struct{}), } } // markFirst 记录首次注册尝试的结果;channel close 保证读取方看到字段写入 func (w *tunnelConn) markFirst(port int, err error) { w.firstOnce.Do(func() { w.firstPort, w.firstErr = port, err close(w.first) }) } // run 重连主循环,阻塞直到停止/一次性结束 func (w *tunnelConn) run() { for { port, err := w.connect() w.markFirst(port, err) if err == nil { err = w.serve() } w.teardown() if w.isStopped() || w.ag.isClosed() { return } if errors.Is(err, errTunnelClosed) { w.ag.removeTunnelState(w.t.Id) return } if w.t.TTLSec > 0 { // 临时隧道一次性:连接断开即结束(节点侧随会话移除) if w.everRegistered { w.ag.removeTunnelState(w.t.Id) } return } w.ag.logger().Warnf("agent 隧道连接断开 id=%s node=%s err=%s backoff=%s", w.t.Id, w.t.NodeAddr, errText(err), w.ag.Backoff) select { case <-time.After(w.ag.Backoff): case <-w.stopCh: return } } } // connect 连接并登录该隧道的节点,经控制流注册自身,返回节点分配端口 func (w *tunnelConn) connect() (int, error) { conn, err := net.DialTimeout("tcp", w.t.NodeAddr, loginTimeout) if err != nil { return 0, err } // 登录(同 zonat:CmdLogin 帧 + Ret 应答) lf := NewFrame(FrameVersion, CmdLogin, 0) if err := lf.Marshal(Login{AgentId: w.ag.AgentId, Token: w.t.Token, Version: Version}); err != nil { conn.Close() return 0, err } if err := WriteFrame(conn, lf); err != nil { conn.Close() return 0, err } conn.SetReadDeadline(time.Now().Add(loginTimeout)) rf, err := ReadFrame(conn) if err != nil { conn.Close() return 0, err } ret := Ret{} if err := rf.Unmarshal(&ret); err != nil { conn.Close() return 0, err } if ret.Code != 0 { conn.Close() return 0, fmt.Errorf("登录失败:%s", ret.Msg) } conn.SetReadDeadline(time.Time{}) sess, err := smux.Client(conn, SmuxConfig()) if err != nil { conn.Close() return 0, err } control, err := sess.OpenStream() if err != nil { sess.Close() return 0, err } w.mu.Lock() w.conn, w.session, w.control = conn, sess, control w.mu.Unlock() // 注册自身;凭据只用于登录,不随注册/数据帧回发 wire := w.t wire.NodeAddr, wire.Token = "", "" f, err := w.callControl(func(control net.Conn) error { rf := NewFrame(FrameVersion, CmdRegisterTunnel, 0) if err := rf.Marshal(wire); err != nil { return err } return WriteFrame(control, rf) }, CmdRegisterTunnel) if err != nil { return 0, err } rret := Ret{} if err := f.Unmarshal(&rret); err != nil { return 0, err } if rret.Code != 0 { return 0, errors.New(rret.Msg) } w.everRegistered = true w.ag.notifyPort(w.t.Id, rret.Port) w.ag.logger().Infof("agent 隧道已连接节点 id=%s node=%s listen=%d", w.t.Id, w.t.NodeAddr, rret.Port) if w.ag.PingEvery > 0 { go w.pingLoop() } return rret.Port, nil } // serve 数据流循环(阻塞;出错即断线,由 run 重连) func (w *tunnelConn) serve() error { w.mu.Lock() sess := w.session w.mu.Unlock() if sess == nil { return errors.New("未连接节点") } for { stream, err := sess.AcceptStream() if err != nil { return err } go w.ag.handleData(stream) } } func (w *tunnelConn) pingLoop() { ticker := time.NewTicker(w.ag.PingEvery) defer ticker.Stop() for { select { case <-w.stopCh: return case <-ticker.C: } _, err := w.callControl(func(control net.Conn) error { return WriteFrame(control, NewFrame(FrameVersion, CmdPing, 0)) }, CmdPing) if err != nil { if errors.Is(err, errTunnelClosed) { // 节点已删除本隧道:杀掉会话让 serve 退出,run 收尾 w.teardown() } return } } } // callControl 串行化控制流交互。等待应答期间收到本隧道的关闭推送即返回 errTunnelClosed。 func (w *tunnelConn) callControl(write func(control net.Conn) error, wantCmd byte) (Frame, error) { w.ctrlMu.Lock() defer w.ctrlMu.Unlock() w.mu.Lock() control := w.control w.mu.Unlock() if control == nil { return Frame{}, errors.New("未连接节点") } if err := write(control); err != nil { return Frame{}, err } for { f, err := ReadFrame(control) if err != nil { return Frame{}, err } if f.Cmd() == CmdTunnelClosed { t := Tunnel{} _ = f.Unmarshal(&t) if t.Id == w.t.Id { return Frame{}, errTunnelClosed } continue } if f.Cmd() == wantCmd { return f, nil } // 其它命令忽略 } } // unregister 经控制流注销本隧道。传输失败视为成功: // 连接已断时节点会随会话移除隧道,无需再通知。 func (w *tunnelConn) unregister() error { w.mu.Lock() connected := w.control != nil w.mu.Unlock() if !connected { return nil } f, err := w.callControl(func(control net.Conn) error { rf := NewFrame(FrameVersion, CmdUnregisterTunnel, 0) if err := rf.Marshal(Tunnel{Id: w.t.Id}); err != nil { return err } return WriteFrame(control, rf) }, CmdUnregisterTunnel) if err != nil { return nil } ret := Ret{} if err := f.Unmarshal(&ret); err != nil { return nil } if ret.Code != 0 { return errors.New(ret.Msg) } return nil } func (w *tunnelConn) stop() { w.stopOnce.Do(func() { close(w.stopCh) }) w.teardown() } func (w *tunnelConn) isStopped() bool { select { case <-w.stopCh: return true default: return false } } func (w *tunnelConn) teardown() { w.mu.Lock() sess, conn := w.session, w.conn w.session, w.conn, w.control = nil, nil, nil w.mu.Unlock() if sess != nil { sess.Close() } if conn != nil { conn.Close() } }