package server import ( "fmt" "log" "time" "github.com/openp2p-cn/inp2p/pkg/auth" "github.com/openp2p-cn/inp2p/pkg/protocol" ) // ConnectCoordinator handles the complete punch coordination flow: // 1. Client A sends ConnectReq to server // 2. Server looks up Client B // 3. Server pushes PunchStart to BOTH A and B simultaneously // 4. Both sides call punch.Connect() at the same time // 5. Success/failure reported back via PunchResult // HandleConnectReq processes a connection request from node A to node B. func (s *Server) HandleConnectReq(from *NodeInfo, req protocol.ConnectReq) error { to := s.GetNodeForUser(req.To, from.Token) if to == nil || !to.IsOnline() { // Peer offline or not visible — respond with generic not found from.Conn.Write(protocol.MsgPush, protocol.SubPushConnectRsp, protocol.ConnectRsp{ Error: 1, Detail: "node not found", From: req.To, To: req.From, }) return &NodeOfflineError{Node: req.To} } log.Printf("[coord] %s → %s: coordinating punch", from.Name, to.Name) // Build punch parameters for both sides from.mu.RLock() fromParams := protocol.PunchParams{ IP: from.PublicIP, Port: from.PublicPort, NATType: from.NATType, HasIPv4: from.HasIPv4, Token: auth.GenTOTP(from.Token, time.Now().Unix()), } from.mu.RUnlock() to.mu.RLock() toParams := protocol.PunchParams{ IP: to.PublicIP, Port: to.PublicPort, NATType: to.NATType, HasIPv4: to.HasIPv4, Token: auth.GenTOTP(to.Token, time.Now().Unix()), } to.mu.RUnlock() // Check if punch is possible if !protocol.CanPunch(fromParams.NATType, toParams.NATType) { log.Printf("[coord] %s(%s) ↔ %s(%s): punch impossible, suggesting relay", from.Name, fromParams.NATType, to.Name, toParams.NATType) // Respond to A with B's info but mark that punch is unlikely from.Conn.Write(protocol.MsgPush, protocol.SubPushConnectRsp, protocol.ConnectRsp{ Error: 0, From: to.Name, To: from.Name, Peer: toParams, Detail: "punch-unlikely", }) return nil } // Push PunchStart to BOTH sides simultaneously punchID := fmt.Sprintf("%s-%s-%d", from.Name, to.Name, time.Now().UnixMilli()) // Tell B about A (so B starts punching toward A) punchToB := protocol.ConnectReq{ From: from.Name, To: to.Name, FromIP: from.PublicIP, Peer: fromParams, AppName: req.AppName, Protocol: req.Protocol, SrcPort: req.SrcPort, DstHost: req.DstHost, DstPort: req.DstPort, } if err := to.Conn.Write(protocol.MsgPush, protocol.SubPushConnectReq, punchToB); err != nil { log.Printf("[coord] push to %s failed: %v", to.Name, err) } // Tell A about B (so A starts punching toward B) rspToA := protocol.ConnectRsp{ Error: 0, From: to.Name, To: from.Name, Peer: toParams, } if err := from.Conn.Write(protocol.MsgPush, protocol.SubPushConnectRsp, rspToA); err != nil { log.Printf("[coord] rsp to %s failed: %v", from.Name, err) } log.Printf("[coord] punch started: %s(%s:%s) ↔ %s(%s:%s) id=%s", from.Name, fromParams.IP, fromParams.NATType, to.Name, toParams.IP, toParams.NATType, punchID) return nil } // HandleEditApp pushes an app configuration to a node, triggering tunnel creation. func (s *Server) HandleEditApp(nodeName string, app protocol.AppConfig) error { node := s.GetNode(nodeName) if node == nil || !node.IsOnline() { return &NodeOfflineError{Node: nodeName} } log.Printf("[coord] push EditApp to %s: %s (:%d → %s:%d)", nodeName, app.AppName, app.SrcPort, app.PeerNode, app.DstPort) return node.Conn.Write(protocol.MsgPush, protocol.SubPushEditApp, app) } // HandleDeleteApp pushes app deletion to a node. func (s *Server) HandleDeleteApp(nodeName string, appName string) error { node := s.GetNode(nodeName) if node == nil || !node.IsOnline() { return &NodeOfflineError{Node: nodeName} } return node.Conn.Write(protocol.MsgPush, protocol.SubPushDeleteApp, struct { AppName string `json:"appName"` }{AppName: appName}) } // HandleReportApps pushes a report-apps request to a node. func (s *Server) HandleReportApps(nodeName string) error { node := s.GetNode(nodeName) if node == nil || !node.IsOnline() { return &NodeOfflineError{Node: nodeName} } return node.Conn.Write(protocol.MsgPush, protocol.SubPushReportApps, nil) }