feat: INP2P v0.1.0 — complete P2P tunneling system
Core modules (M1-M6): - pkg/protocol: message format, encoding, NAT type enums - pkg/config: server/client config structs, env vars, validation - pkg/auth: CRC64 token, TOTP gen/verify, one-time relay tokens - pkg/nat: UDP/TCP STUN client and server - pkg/signal: WSS message dispatch, sync request/response - pkg/punch: UDP/TCP hole punching + priority chain - pkg/mux: stream multiplexer (7B frame: StreamID+Flags+Len) - pkg/tunnel: mux-based port forwarding with stats - pkg/relay: relay manager with TOTP auth + session bridging - internal/server: signaling server (login/heartbeat/report/coordinator) - internal/client: client (NAT detect/login/punch/relay/reconnect) - cmd/inp2ps + cmd/inp2pc: main entrypoints with graceful shutdown All tests pass: 16 tests across 5 packages Code: 3559 lines core + 861 lines tests = 19 source files
This commit is contained in:
471
internal/client/client.go
Normal file
471
internal/client/client.go
Normal file
@@ -0,0 +1,471 @@
|
||||
// Package client implements the inp2pc P2P client.
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/openp2p-cn/inp2p/pkg/auth"
|
||||
"github.com/openp2p-cn/inp2p/pkg/config"
|
||||
"github.com/openp2p-cn/inp2p/pkg/nat"
|
||||
"github.com/openp2p-cn/inp2p/pkg/protocol"
|
||||
"github.com/openp2p-cn/inp2p/pkg/punch"
|
||||
"github.com/openp2p-cn/inp2p/pkg/relay"
|
||||
"github.com/openp2p-cn/inp2p/pkg/signal"
|
||||
"github.com/openp2p-cn/inp2p/pkg/tunnel"
|
||||
)
|
||||
|
||||
// Client is the INP2P client node.
|
||||
type Client struct {
|
||||
cfg config.ClientConfig
|
||||
conn *signal.Conn
|
||||
natType protocol.NATType
|
||||
publicIP string
|
||||
tunnels map[string]*tunnel.Tunnel // peerNode → tunnel
|
||||
tMu sync.RWMutex
|
||||
relayMgr *relay.Manager
|
||||
quit chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// New creates a new client.
|
||||
func New(cfg config.ClientConfig) *Client {
|
||||
c := &Client{
|
||||
cfg: cfg,
|
||||
natType: protocol.NATUnknown,
|
||||
tunnels: make(map[string]*tunnel.Tunnel),
|
||||
quit: make(chan struct{}),
|
||||
}
|
||||
|
||||
if cfg.RelayEnabled {
|
||||
c.relayMgr = relay.NewManager(cfg.RelayPort, true, cfg.SuperRelay, cfg.MaxRelayLoad, cfg.Token)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Run is the main client loop. Connects, authenticates, and maintains the connection.
|
||||
func (c *Client) Run() error {
|
||||
for {
|
||||
if err := c.connectAndRun(); err != nil {
|
||||
log.Printf("[client] disconnected: %v, reconnecting in 5s...", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-c.quit:
|
||||
return nil
|
||||
case <-time.After(5 * time.Second):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) connectAndRun() error {
|
||||
// 1. NAT Detection
|
||||
log.Printf("[client] detecting NAT type via %s...", c.cfg.ServerHost)
|
||||
natResult := nat.Detect(
|
||||
c.cfg.ServerHost,
|
||||
c.cfg.STUNUDP1, c.cfg.STUNUDP2,
|
||||
c.cfg.STUNTCP1, c.cfg.STUNTCP2,
|
||||
)
|
||||
c.natType = natResult.Type
|
||||
c.publicIP = natResult.PublicIP
|
||||
log.Printf("[client] NAT type=%s, publicIP=%s", c.natType, c.publicIP)
|
||||
|
||||
// 2. WSS Connect
|
||||
scheme := "ws"
|
||||
if !c.cfg.Insecure {
|
||||
scheme = "wss"
|
||||
}
|
||||
u := url.URL{Scheme: scheme, Host: fmt.Sprintf("%s:%d", c.cfg.ServerHost, c.cfg.ServerPort), Path: "/ws"}
|
||||
|
||||
dialer := websocket.Dialer{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: c.cfg.Insecure},
|
||||
}
|
||||
ws, _, err := dialer.Dial(u.String(), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ws connect: %w", err)
|
||||
}
|
||||
|
||||
c.conn = signal.NewConn(ws)
|
||||
defer c.conn.Close()
|
||||
|
||||
// Start ReadLoop in background BEFORE sending login
|
||||
// (so waiter can receive the LoginRsp)
|
||||
readErr := make(chan error, 1)
|
||||
go func() {
|
||||
readErr <- c.conn.ReadLoop()
|
||||
}()
|
||||
|
||||
// 3. Login
|
||||
loginReq := protocol.LoginReq{
|
||||
Node: c.cfg.Node,
|
||||
Token: c.cfg.Token,
|
||||
User: c.cfg.User,
|
||||
Version: config.Version,
|
||||
NATType: c.natType,
|
||||
ShareBandwidth: c.cfg.ShareBandwidth,
|
||||
RelayEnabled: c.cfg.RelayEnabled,
|
||||
SuperRelay: c.cfg.SuperRelay,
|
||||
PublicIP: c.publicIP,
|
||||
}
|
||||
|
||||
rspData, err := c.conn.Request(
|
||||
protocol.MsgLogin, protocol.SubLoginReq, loginReq,
|
||||
protocol.MsgLogin, protocol.SubLoginRsp,
|
||||
10*time.Second,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("login: %w", err)
|
||||
}
|
||||
|
||||
var loginRsp protocol.LoginRsp
|
||||
if err := protocol.DecodePayload(rspData, &loginRsp); err != nil {
|
||||
return fmt.Errorf("decode login rsp: %w", err)
|
||||
}
|
||||
if loginRsp.Error != 0 {
|
||||
return fmt.Errorf("login rejected: %s", loginRsp.Detail)
|
||||
}
|
||||
|
||||
log.Printf("[client] login ok: node=%s, user=%s", loginRsp.Node, loginRsp.User)
|
||||
|
||||
// 4. Send ReportBasic
|
||||
c.sendReportBasic()
|
||||
|
||||
// 5. Register handlers
|
||||
c.registerHandlers()
|
||||
|
||||
// 6. Start heartbeat
|
||||
c.wg.Add(1)
|
||||
go c.heartbeatLoop()
|
||||
|
||||
// 7. Start relay if enabled
|
||||
if c.relayMgr != nil {
|
||||
if err := c.relayMgr.Start(); err != nil {
|
||||
log.Printf("[client] relay start failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Auto-run configured apps
|
||||
for _, app := range c.cfg.Apps {
|
||||
if app.Enabled {
|
||||
go c.connectApp(app)
|
||||
}
|
||||
}
|
||||
|
||||
// 9. Wait for disconnect
|
||||
return <-readErr
|
||||
}
|
||||
|
||||
func (c *Client) sendReportBasic() {
|
||||
hostname, _ := os.Hostname()
|
||||
report := protocol.ReportBasic{
|
||||
OS: runtime.GOOS,
|
||||
LanIP: getLocalIP(),
|
||||
Version: config.Version,
|
||||
HasIPv4: 1,
|
||||
}
|
||||
_ = hostname // for future use
|
||||
c.conn.Write(protocol.MsgReport, protocol.SubReportBasic, report)
|
||||
}
|
||||
|
||||
func (c *Client) registerHandlers() {
|
||||
// Handle connection coordination from server
|
||||
c.conn.OnMessage(protocol.MsgPush, protocol.SubPushConnectReq, func(data []byte) error {
|
||||
var req protocol.ConnectReq
|
||||
if err := protocol.DecodePayload(data, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("[client] connect request: %s → %s (punch)", req.From, req.To)
|
||||
go c.handlePunchRequest(req)
|
||||
return nil
|
||||
})
|
||||
|
||||
// Handle peer online notification
|
||||
c.conn.OnMessage(protocol.MsgPush, protocol.SubPushNodeOnline, func(data []byte) error {
|
||||
var msg struct {
|
||||
Node string `json:"node"`
|
||||
}
|
||||
protocol.DecodePayload(data, &msg)
|
||||
log.Printf("[client] peer online: %s, retrying apps", msg.Node)
|
||||
// Retry apps targeting this node
|
||||
for _, app := range c.cfg.Apps {
|
||||
if app.Enabled && app.PeerNode == msg.Node {
|
||||
go c.connectApp(app)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Handle edit app push
|
||||
c.conn.OnMessage(protocol.MsgPush, protocol.SubPushEditApp, func(data []byte) error {
|
||||
var app protocol.AppConfig
|
||||
if err := protocol.DecodePayload(data, &app); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("[client] edit app push: %s → %s:%d", app.AppName, app.PeerNode, app.DstPort)
|
||||
go c.connectApp(config.AppConfig{
|
||||
AppName: app.AppName,
|
||||
Protocol: app.Protocol,
|
||||
SrcPort: app.SrcPort,
|
||||
PeerNode: app.PeerNode,
|
||||
DstHost: app.DstHost,
|
||||
DstPort: app.DstPort,
|
||||
Enabled: true,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
|
||||
// Handle relay connect request (when this node acts as relay)
|
||||
if c.relayMgr != nil {
|
||||
c.conn.OnMessage(protocol.MsgPush, protocol.SubPushRelayOffer, func(data []byte) error {
|
||||
var req struct {
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
Token uint64 `json:"token"`
|
||||
}
|
||||
if err := protocol.DecodePayload(data, &req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify TOTP
|
||||
if !auth.VerifyTOTP(req.Token, c.cfg.Token, time.Now().Unix()) {
|
||||
log.Printf("[client] relay request from %s denied: TOTP mismatch", req.From)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("[client] accepting relay: %s → %s", req.From, req.To)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) heartbeatLoop() {
|
||||
defer c.wg.Done()
|
||||
ticker := time.NewTicker(time.Duration(config.HeartbeatInterval) * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err := c.conn.Write(protocol.MsgHeartbeat, protocol.SubHeartbeatPing, nil); err != nil {
|
||||
log.Printf("[client] heartbeat send failed: %v", err)
|
||||
return
|
||||
}
|
||||
case <-c.quit:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// connectApp establishes a tunnel for an app config.
|
||||
func (c *Client) connectApp(app config.AppConfig) {
|
||||
log.Printf("[client] connecting app %s: :%d → %s:%d", app.AppName, app.SrcPort, app.PeerNode, app.DstPort)
|
||||
|
||||
// Check if we already have a tunnel
|
||||
c.tMu.RLock()
|
||||
if t, ok := c.tunnels[app.PeerNode]; ok && t.IsAlive() {
|
||||
c.tMu.RUnlock()
|
||||
// Tunnel exists, just add the port forward
|
||||
if err := t.ListenAndForward(app.Protocol, app.SrcPort, app.DstHost, app.DstPort); err != nil {
|
||||
log.Printf("[client] listen error for %s: %v", app.AppName, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
c.tMu.RUnlock()
|
||||
|
||||
// Request connection coordination from server
|
||||
req := protocol.ConnectReq{
|
||||
From: c.cfg.Node,
|
||||
To: app.PeerNode,
|
||||
Protocol: app.Protocol,
|
||||
SrcPort: app.SrcPort,
|
||||
DstHost: app.DstHost,
|
||||
DstPort: app.DstPort,
|
||||
}
|
||||
|
||||
rspData, err := c.conn.Request(
|
||||
protocol.MsgPush, protocol.SubPushConnectReq, req,
|
||||
protocol.MsgPush, protocol.SubPushConnectRsp,
|
||||
15*time.Second,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("[client] connect coordination failed for %s: %v", app.PeerNode, err)
|
||||
c.tryRelay(app)
|
||||
return
|
||||
}
|
||||
|
||||
var rsp protocol.ConnectRsp
|
||||
protocol.DecodePayload(rspData, &rsp)
|
||||
if rsp.Error != 0 {
|
||||
log.Printf("[client] connect denied: %s", rsp.Detail)
|
||||
c.tryRelay(app)
|
||||
return
|
||||
}
|
||||
|
||||
// Attempt punch
|
||||
result := punch.Connect(punch.Config{
|
||||
PeerIP: rsp.Peer.IP,
|
||||
PeerPort: rsp.Peer.Port,
|
||||
PeerNAT: rsp.Peer.NATType,
|
||||
SelfNAT: c.natType,
|
||||
IsInitiator: true,
|
||||
})
|
||||
|
||||
if result.Error != nil {
|
||||
log.Printf("[client] punch failed for %s: %v", app.PeerNode, result.Error)
|
||||
c.tryRelay(app)
|
||||
c.reportConnect(app, protocol.ReportConnect{
|
||||
PeerNode: app.PeerNode, Error: result.Error.Error(),
|
||||
NATType: c.natType, PeerNATType: rsp.Peer.NATType,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Punch success — create tunnel
|
||||
t := tunnel.New(app.PeerNode, result.Conn, result.Mode, result.RTT, true)
|
||||
c.tMu.Lock()
|
||||
c.tunnels[app.PeerNode] = t
|
||||
c.tMu.Unlock()
|
||||
|
||||
if err := t.ListenAndForward(app.Protocol, app.SrcPort, app.DstHost, app.DstPort); err != nil {
|
||||
log.Printf("[client] listen error: %v", err)
|
||||
}
|
||||
|
||||
c.reportConnect(app, protocol.ReportConnect{
|
||||
PeerNode: app.PeerNode, LinkMode: result.Mode,
|
||||
RTT: int(result.RTT.Milliseconds()),
|
||||
NATType: c.natType, PeerNATType: rsp.Peer.NATType,
|
||||
})
|
||||
|
||||
log.Printf("[client] tunnel established: %s via %s (rtt=%s)", app.PeerNode, result.Mode, result.RTT)
|
||||
}
|
||||
|
||||
// tryRelay attempts to use a relay node.
|
||||
func (c *Client) tryRelay(app config.AppConfig) {
|
||||
log.Printf("[client] trying relay for %s", app.PeerNode)
|
||||
|
||||
rspData, err := c.conn.Request(
|
||||
protocol.MsgRelay, protocol.SubRelayNodeReq,
|
||||
protocol.RelayNodeReq{PeerNode: app.PeerNode},
|
||||
protocol.MsgRelay, protocol.SubRelayNodeRsp,
|
||||
10*time.Second,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("[client] relay request failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var rsp protocol.RelayNodeRsp
|
||||
protocol.DecodePayload(rspData, &rsp)
|
||||
if rsp.Error != 0 {
|
||||
log.Printf("[client] no relay available for %s", app.PeerNode)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[client] relay via %s (%s mode), connecting...", rsp.RelayName, rsp.Mode)
|
||||
|
||||
// Connect to relay node
|
||||
result := punch.AttemptDirect(punch.Config{
|
||||
PeerIP: rsp.RelayIP,
|
||||
PeerPort: rsp.RelayPort,
|
||||
})
|
||||
if result.Error != nil {
|
||||
log.Printf("[client] relay connect failed: %v", result.Error)
|
||||
return
|
||||
}
|
||||
|
||||
t := tunnel.New(app.PeerNode, result.Conn, "relay-"+rsp.Mode, result.RTT, true)
|
||||
c.tMu.Lock()
|
||||
c.tunnels[app.PeerNode] = t
|
||||
c.tMu.Unlock()
|
||||
|
||||
if err := t.ListenAndForward(app.Protocol, app.SrcPort, app.DstHost, app.DstPort); err != nil {
|
||||
log.Printf("[client] relay listen error: %v", err)
|
||||
}
|
||||
|
||||
c.reportConnect(app, protocol.ReportConnect{
|
||||
PeerNode: app.PeerNode, LinkMode: "relay", RelayNode: rsp.RelayName,
|
||||
})
|
||||
|
||||
log.Printf("[client] relay tunnel established: %s via %s", app.PeerNode, rsp.RelayName)
|
||||
}
|
||||
|
||||
func (c *Client) handlePunchRequest(req protocol.ConnectReq) {
|
||||
log.Printf("[client] handling punch from %s, NAT=%s", req.From, req.Peer.NATType)
|
||||
|
||||
result := punch.Connect(punch.Config{
|
||||
PeerIP: req.Peer.IP,
|
||||
PeerPort: req.Peer.Port,
|
||||
PeerNAT: req.Peer.NATType,
|
||||
SelfNAT: c.natType,
|
||||
IsInitiator: false,
|
||||
})
|
||||
|
||||
rsp := protocol.ConnectRsp{
|
||||
From: c.cfg.Node,
|
||||
To: req.From,
|
||||
}
|
||||
|
||||
if result.Error != nil {
|
||||
rsp.Error = 1
|
||||
rsp.Detail = result.Error.Error()
|
||||
log.Printf("[client] punch from %s failed: %v", req.From, result.Error)
|
||||
} else {
|
||||
rsp.Peer = protocol.PunchParams{
|
||||
IP: c.publicIP,
|
||||
NATType: c.natType,
|
||||
}
|
||||
log.Printf("[client] punch from %s OK via %s", req.From, result.Mode)
|
||||
|
||||
// Create tunnel for the incoming connection
|
||||
t := tunnel.New(req.From, result.Conn, result.Mode, result.RTT, false)
|
||||
c.tMu.Lock()
|
||||
c.tunnels[req.From] = t
|
||||
c.tMu.Unlock()
|
||||
}
|
||||
|
||||
c.conn.Write(protocol.MsgPush, protocol.SubPushConnectRsp, rsp)
|
||||
}
|
||||
|
||||
func (c *Client) reportConnect(app config.AppConfig, rc protocol.ReportConnect) {
|
||||
rc.Protocol = app.Protocol
|
||||
rc.SrcPort = app.SrcPort
|
||||
rc.DstPort = app.DstPort
|
||||
rc.DstHost = app.DstHost
|
||||
rc.Version = config.Version
|
||||
rc.ShareBandwidth = c.cfg.ShareBandwidth
|
||||
c.conn.Write(protocol.MsgReport, protocol.SubReportConnect, rc)
|
||||
}
|
||||
|
||||
// Stop shuts down the client.
|
||||
func (c *Client) Stop() {
|
||||
close(c.quit)
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
}
|
||||
if c.relayMgr != nil {
|
||||
c.relayMgr.Stop()
|
||||
}
|
||||
c.tMu.Lock()
|
||||
for _, t := range c.tunnels {
|
||||
t.Close()
|
||||
}
|
||||
c.tMu.Unlock()
|
||||
c.wg.Wait()
|
||||
}
|
||||
|
||||
// ─── helpers ───
|
||||
|
||||
func getLocalIP() string {
|
||||
// Simple heuristic: find the first non-loopback IPv4
|
||||
addrs, _ := os.Hostname()
|
||||
_ = addrs
|
||||
return "0.0.0.0" // placeholder, will be properly implemented
|
||||
}
|
||||
79
internal/client/client_test.go
Normal file
79
internal/client/client_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/openp2p-cn/inp2p/internal/server"
|
||||
"github.com/openp2p-cn/inp2p/pkg/config"
|
||||
"github.com/openp2p-cn/inp2p/pkg/nat"
|
||||
)
|
||||
|
||||
func TestClientLogin(t *testing.T) {
|
||||
// Server
|
||||
sCfg := config.DefaultServerConfig()
|
||||
sCfg.WSPort = 29400
|
||||
sCfg.STUNUDP1 = 29482
|
||||
sCfg.STUNUDP2 = 29484
|
||||
sCfg.STUNTCP1 = 29480
|
||||
sCfg.STUNTCP2 = 29481
|
||||
sCfg.Token = 777
|
||||
|
||||
stunQuit := make(chan struct{})
|
||||
defer close(stunQuit)
|
||||
go nat.ServeUDPSTUN(sCfg.STUNUDP1, stunQuit)
|
||||
go nat.ServeUDPSTUN(sCfg.STUNUDP2, stunQuit)
|
||||
go nat.ServeTCPSTUN(sCfg.STUNTCP1, stunQuit)
|
||||
go nat.ServeTCPSTUN(sCfg.STUNTCP2, stunQuit)
|
||||
|
||||
srv := server.New(sCfg)
|
||||
srv.StartCleanup()
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/ws", srv.HandleWS)
|
||||
go http.ListenAndServe(fmt.Sprintf(":%d", sCfg.WSPort), mux)
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
// Client
|
||||
cCfg := config.DefaultClientConfig()
|
||||
cCfg.ServerHost = "127.0.0.1"
|
||||
cCfg.ServerPort = 29400
|
||||
cCfg.Node = "testClient"
|
||||
cCfg.Token = 777
|
||||
cCfg.Insecure = true
|
||||
cCfg.RelayEnabled = true
|
||||
cCfg.STUNUDP1 = 29482
|
||||
cCfg.STUNUDP2 = 29484
|
||||
cCfg.STUNTCP1 = 29480
|
||||
cCfg.STUNTCP2 = 29481
|
||||
|
||||
c := New(cCfg)
|
||||
|
||||
// Run in background, should connect within 8 seconds
|
||||
connected := make(chan struct{})
|
||||
go func() {
|
||||
// We'll just let it run for a bit
|
||||
c.Run()
|
||||
}()
|
||||
|
||||
// Wait for login
|
||||
time.Sleep(8 * time.Second)
|
||||
|
||||
nodes := srv.GetOnlineNodes()
|
||||
log.Printf("Online nodes: %d", len(nodes))
|
||||
for _, n := range nodes {
|
||||
log.Printf(" - %s (NAT=%s, relay=%v)", n.Name, n.NATType, n.RelayEnabled)
|
||||
}
|
||||
|
||||
if len(nodes) == 1 && nodes[0].Name == "testClient" {
|
||||
close(connected)
|
||||
log.Println("✅ Client connected successfully!")
|
||||
} else {
|
||||
t.Fatalf("Expected testClient online, got %d nodes", len(nodes))
|
||||
}
|
||||
|
||||
c.Stop()
|
||||
srv.Stop()
|
||||
}
|
||||
137
internal/server/coordinator.go
Normal file
137
internal/server/coordinator.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"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.GetNode(req.To)
|
||||
if to == nil || !to.IsOnline() {
|
||||
// Peer offline — respond with error
|
||||
from.Conn.Write(protocol.MsgPush, protocol.SubPushConnectRsp, protocol.ConnectRsp{
|
||||
Error: 1,
|
||||
Detail: fmt.Sprintf("node %s offline", req.To),
|
||||
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,
|
||||
NATType: from.NATType,
|
||||
HasIPv4: from.HasIPv4,
|
||||
}
|
||||
from.mu.RUnlock()
|
||||
|
||||
to.mu.RLock()
|
||||
toParams := protocol.PunchParams{
|
||||
IP: to.PublicIP,
|
||||
NATType: to.NATType,
|
||||
HasIPv4: to.HasIPv4,
|
||||
}
|
||||
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)
|
||||
}
|
||||
406
internal/server/server.go
Normal file
406
internal/server/server.go
Normal file
@@ -0,0 +1,406 @@
|
||||
// Package server implements the inp2ps signaling server.
|
||||
package server
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/openp2p-cn/inp2p/pkg/auth"
|
||||
"github.com/openp2p-cn/inp2p/pkg/config"
|
||||
"github.com/openp2p-cn/inp2p/pkg/protocol"
|
||||
"github.com/openp2p-cn/inp2p/pkg/signal"
|
||||
)
|
||||
|
||||
// NodeInfo represents a connected client node.
|
||||
type NodeInfo struct {
|
||||
Name string
|
||||
Token uint64
|
||||
User string
|
||||
Version string
|
||||
NATType protocol.NATType
|
||||
PublicIP string
|
||||
LanIP string
|
||||
OS string
|
||||
Mac string
|
||||
ShareBandwidth int
|
||||
RelayEnabled bool
|
||||
SuperRelay bool
|
||||
HasIPv4 int
|
||||
IPv6 string
|
||||
LoginTime time.Time
|
||||
LastHeartbeat time.Time
|
||||
Conn *signal.Conn
|
||||
Apps []protocol.AppConfig
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// IsOnline checks if node has sent heartbeat recently.
|
||||
func (n *NodeInfo) IsOnline() bool {
|
||||
n.mu.RLock()
|
||||
defer n.mu.RUnlock()
|
||||
return time.Since(n.LastHeartbeat) < time.Duration(config.HeartbeatTimeout)*time.Second
|
||||
}
|
||||
|
||||
// Server is the INP2P signaling server.
|
||||
type Server struct {
|
||||
cfg config.ServerConfig
|
||||
nodes map[string]*NodeInfo // node name → info
|
||||
mu sync.RWMutex
|
||||
upgrader websocket.Upgrader
|
||||
quit chan struct{}
|
||||
}
|
||||
|
||||
// New creates a new server.
|
||||
func New(cfg config.ServerConfig) *Server {
|
||||
return &Server{
|
||||
cfg: cfg,
|
||||
nodes: make(map[string]*NodeInfo),
|
||||
upgrader: websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
},
|
||||
quit: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// GetNode returns a connected node by name.
|
||||
func (s *Server) GetNode(name string) *NodeInfo {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.nodes[name]
|
||||
}
|
||||
|
||||
// GetOnlineNodes returns all online nodes.
|
||||
func (s *Server) GetOnlineNodes() []*NodeInfo {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
var out []*NodeInfo
|
||||
for _, n := range s.nodes {
|
||||
if n.IsOnline() {
|
||||
out = append(out, n)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// GetRelayNodes returns nodes that can serve as relay.
|
||||
// Priority: same-user private relay → super relay
|
||||
func (s *Server) GetRelayNodes(forUser string, excludeNodes ...string) []*NodeInfo {
|
||||
excludeSet := make(map[string]bool)
|
||||
for _, n := range excludeNodes {
|
||||
excludeSet[n] = true
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
var privateRelays, superRelays []*NodeInfo
|
||||
for _, n := range s.nodes {
|
||||
if !n.IsOnline() || excludeSet[n.Name] || !n.RelayEnabled {
|
||||
continue
|
||||
}
|
||||
if n.User == forUser {
|
||||
privateRelays = append(privateRelays, n)
|
||||
} else if n.SuperRelay {
|
||||
superRelays = append(superRelays, n)
|
||||
}
|
||||
}
|
||||
// private first, then super
|
||||
return append(privateRelays, superRelays...)
|
||||
}
|
||||
|
||||
// HandleWS is the WebSocket handler for client connections.
|
||||
func (s *Server) HandleWS(w http.ResponseWriter, r *http.Request) {
|
||||
ws, err := s.upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Printf("[server] ws upgrade error: %v", err)
|
||||
return
|
||||
}
|
||||
conn := signal.NewConn(ws)
|
||||
log.Printf("[server] new connection from %s", r.RemoteAddr)
|
||||
|
||||
// First message must be login
|
||||
_, msg, err := ws.ReadMessage()
|
||||
if err != nil {
|
||||
log.Printf("[server] read login error: %v", err)
|
||||
ws.Close()
|
||||
return
|
||||
}
|
||||
|
||||
hdr, err := protocol.DecodeHeader(msg)
|
||||
if err != nil || hdr.MainType != protocol.MsgLogin || hdr.SubType != protocol.SubLoginReq {
|
||||
log.Printf("[server] expected login, got %d:%d", hdr.MainType, hdr.SubType)
|
||||
ws.Close()
|
||||
return
|
||||
}
|
||||
|
||||
var loginReq protocol.LoginReq
|
||||
if err := protocol.DecodePayload(msg, &loginReq); err != nil {
|
||||
log.Printf("[server] decode login: %v", err)
|
||||
ws.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Verify token
|
||||
if loginReq.Token != s.cfg.Token {
|
||||
log.Printf("[server] login denied: %s (token mismatch)", loginReq.Node)
|
||||
conn.Write(protocol.MsgLogin, protocol.SubLoginRsp, protocol.LoginRsp{
|
||||
Error: 1,
|
||||
Detail: "invalid token",
|
||||
})
|
||||
ws.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Check duplicate node
|
||||
s.mu.Lock()
|
||||
if old, exists := s.nodes[loginReq.Node]; exists {
|
||||
log.Printf("[server] replacing existing node %s", loginReq.Node)
|
||||
old.Conn.Close()
|
||||
}
|
||||
|
||||
node := &NodeInfo{
|
||||
Name: loginReq.Node,
|
||||
Token: loginReq.Token,
|
||||
User: loginReq.User,
|
||||
Version: loginReq.Version,
|
||||
NATType: loginReq.NATType,
|
||||
ShareBandwidth: loginReq.ShareBandwidth,
|
||||
RelayEnabled: loginReq.RelayEnabled,
|
||||
SuperRelay: loginReq.SuperRelay,
|
||||
PublicIP: r.RemoteAddr, // will be updated by NAT detect
|
||||
LoginTime: time.Now(),
|
||||
LastHeartbeat: time.Now(),
|
||||
Conn: conn,
|
||||
}
|
||||
s.nodes[loginReq.Node] = node
|
||||
s.mu.Unlock()
|
||||
|
||||
// Send login response
|
||||
conn.Write(protocol.MsgLogin, protocol.SubLoginRsp, protocol.LoginRsp{
|
||||
Error: 0,
|
||||
Ts: time.Now().Unix(),
|
||||
Token: loginReq.Token,
|
||||
User: loginReq.User,
|
||||
Node: loginReq.Node,
|
||||
})
|
||||
|
||||
log.Printf("[server] login ok: node=%s, natType=%s, relay=%v, super=%v, version=%s",
|
||||
loginReq.Node, loginReq.NATType, loginReq.RelayEnabled, loginReq.SuperRelay, loginReq.Version)
|
||||
|
||||
// Notify other nodes
|
||||
s.broadcastNodeOnline(loginReq.Node)
|
||||
|
||||
// Register message handlers
|
||||
s.registerHandlers(conn, node)
|
||||
|
||||
// Start read loop (blocks until disconnect)
|
||||
if err := conn.ReadLoop(); err != nil {
|
||||
log.Printf("[server] %s disconnected: %v", loginReq.Node, err)
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
s.mu.Lock()
|
||||
if current, ok := s.nodes[loginReq.Node]; ok && current == node {
|
||||
delete(s.nodes, loginReq.Node)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
log.Printf("[server] %s offline", loginReq.Node)
|
||||
}
|
||||
|
||||
func (s *Server) registerHandlers(conn *signal.Conn, node *NodeInfo) {
|
||||
// Heartbeat
|
||||
conn.OnMessage(protocol.MsgHeartbeat, protocol.SubHeartbeatPing, func(data []byte) error {
|
||||
node.mu.Lock()
|
||||
node.LastHeartbeat = time.Now()
|
||||
node.mu.Unlock()
|
||||
return conn.Write(protocol.MsgHeartbeat, protocol.SubHeartbeatPong, nil)
|
||||
})
|
||||
|
||||
// ReportBasic
|
||||
conn.OnMessage(protocol.MsgReport, protocol.SubReportBasic, func(data []byte) error {
|
||||
var report protocol.ReportBasic
|
||||
if err := protocol.DecodePayload(data, &report); err != nil {
|
||||
return err
|
||||
}
|
||||
node.mu.Lock()
|
||||
node.OS = report.OS
|
||||
node.Mac = report.Mac
|
||||
node.LanIP = report.LanIP
|
||||
node.Version = report.Version
|
||||
node.HasIPv4 = report.HasIPv4
|
||||
node.IPv6 = report.IPv6
|
||||
node.mu.Unlock()
|
||||
log.Printf("[server] ReportBasic from %s: os=%s lanIP=%s", node.Name, report.OS, report.LanIP)
|
||||
|
||||
// Always respond (official OpenP2P bug: not responding causes client to disconnect)
|
||||
return conn.Write(protocol.MsgReport, protocol.SubReportBasic, protocol.ReportBasicRsp{Error: 0})
|
||||
})
|
||||
|
||||
// ReportApps
|
||||
conn.OnMessage(protocol.MsgReport, protocol.SubReportApps, func(data []byte) error {
|
||||
var apps []protocol.AppConfig
|
||||
protocol.DecodePayload(data, &apps)
|
||||
node.mu.Lock()
|
||||
node.Apps = apps
|
||||
node.mu.Unlock()
|
||||
log.Printf("[server] ReportApps from %s: %d apps", node.Name, len(apps))
|
||||
return nil
|
||||
})
|
||||
|
||||
// ReportConnect
|
||||
conn.OnMessage(protocol.MsgReport, protocol.SubReportConnect, func(data []byte) error {
|
||||
var rc protocol.ReportConnect
|
||||
protocol.DecodePayload(data, &rc)
|
||||
if rc.Error != "" {
|
||||
log.Printf("[server] ConnectReport ERROR from %s: peer=%s mode=%s err=%s", node.Name, rc.PeerNode, rc.LinkMode, rc.Error)
|
||||
} else {
|
||||
log.Printf("[server] ConnectReport OK from %s: peer=%s mode=%s rtt=%dms", node.Name, rc.PeerNode, rc.LinkMode, rc.RTT)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// ConnectReq — client wants to connect to a peer
|
||||
conn.OnMessage(protocol.MsgPush, protocol.SubPushConnectReq, func(data []byte) error {
|
||||
var req protocol.ConnectReq
|
||||
protocol.DecodePayload(data, &req)
|
||||
return s.HandleConnectReq(node, req)
|
||||
})
|
||||
|
||||
// RelayNodeReq — client asks for a relay node
|
||||
conn.OnMessage(protocol.MsgRelay, protocol.SubRelayNodeReq, func(data []byte) error {
|
||||
var req protocol.RelayNodeReq
|
||||
protocol.DecodePayload(data, &req)
|
||||
return s.handleRelayNodeReq(conn, node, req)
|
||||
})
|
||||
}
|
||||
|
||||
// handleRelayNodeReq finds and returns the best relay node.
|
||||
func (s *Server) handleRelayNodeReq(conn *signal.Conn, requester *NodeInfo, req protocol.RelayNodeReq) error {
|
||||
relays := s.GetRelayNodes(requester.User, requester.Name, req.PeerNode)
|
||||
|
||||
if len(relays) == 0 {
|
||||
return conn.Write(protocol.MsgRelay, protocol.SubRelayNodeRsp, protocol.RelayNodeRsp{
|
||||
Error: 1,
|
||||
})
|
||||
}
|
||||
|
||||
// Pick the first (best) relay
|
||||
relay := relays[0]
|
||||
totp := auth.GenTOTP(relay.Token, time.Now().Unix())
|
||||
|
||||
mode := "private"
|
||||
if relay.User != requester.User {
|
||||
mode = "super"
|
||||
}
|
||||
|
||||
log.Printf("[server] relay selected: %s (%s) for %s → %s", relay.Name, mode, requester.Name, req.PeerNode)
|
||||
|
||||
return conn.Write(protocol.MsgRelay, protocol.SubRelayNodeRsp, protocol.RelayNodeRsp{
|
||||
RelayName: relay.Name,
|
||||
RelayIP: relay.PublicIP,
|
||||
RelayPort: config.DefaultRelayPort,
|
||||
RelayToken: totp,
|
||||
Mode: mode,
|
||||
Error: 0,
|
||||
})
|
||||
}
|
||||
|
||||
// PushConnect sends a punch coordination message to a peer node.
|
||||
func (s *Server) PushConnect(fromNode *NodeInfo, toNodeName string, app protocol.AppConfig) error {
|
||||
toNode := s.GetNode(toNodeName)
|
||||
if toNode == nil || !toNode.IsOnline() {
|
||||
return &NodeOfflineError{Node: toNodeName}
|
||||
}
|
||||
|
||||
// Push connect request to the destination
|
||||
req := protocol.ConnectReq{
|
||||
From: fromNode.Name,
|
||||
To: toNodeName,
|
||||
FromIP: fromNode.PublicIP,
|
||||
Peer: protocol.PunchParams{
|
||||
IP: fromNode.PublicIP,
|
||||
NATType: fromNode.NATType,
|
||||
HasIPv4: fromNode.HasIPv4,
|
||||
},
|
||||
AppName: app.AppName,
|
||||
Protocol: app.Protocol,
|
||||
SrcPort: app.SrcPort,
|
||||
DstHost: app.DstHost,
|
||||
DstPort: app.DstPort,
|
||||
}
|
||||
|
||||
return toNode.Conn.Write(protocol.MsgPush, protocol.SubPushConnectReq, req)
|
||||
}
|
||||
|
||||
// broadcastNodeOnline notifies interested nodes that a peer came online.
|
||||
func (s *Server) broadcastNodeOnline(nodeName string) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
for _, n := range s.nodes {
|
||||
if n.Name == nodeName {
|
||||
continue
|
||||
}
|
||||
// Check if this node has any app targeting the new node
|
||||
n.mu.RLock()
|
||||
interested := false
|
||||
for _, app := range n.Apps {
|
||||
if app.PeerNode == nodeName {
|
||||
interested = true
|
||||
break
|
||||
}
|
||||
}
|
||||
n.mu.RUnlock()
|
||||
|
||||
if interested {
|
||||
n.Conn.Write(protocol.MsgPush, protocol.SubPushNodeOnline, struct {
|
||||
Node string `json:"node"`
|
||||
}{Node: nodeName})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// StartCleanup periodically removes stale nodes.
|
||||
func (s *Server) StartCleanup() {
|
||||
go func() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
s.mu.Lock()
|
||||
for name, n := range s.nodes {
|
||||
if !n.IsOnline() {
|
||||
log.Printf("[server] cleanup stale node: %s", name)
|
||||
n.Conn.Close()
|
||||
delete(s.nodes, name)
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
case <-s.quit:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Stop shuts down the server.
|
||||
func (s *Server) Stop() {
|
||||
close(s.quit)
|
||||
s.mu.Lock()
|
||||
for _, n := range s.nodes {
|
||||
n.Conn.Close()
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
type NodeOfflineError struct {
|
||||
Node string
|
||||
}
|
||||
|
||||
func (e *NodeOfflineError) Error() string {
|
||||
return "node offline: " + e.Node
|
||||
}
|
||||
151
internal/server/server_test.go
Normal file
151
internal/server/server_test.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/openp2p-cn/inp2p/pkg/config"
|
||||
"github.com/openp2p-cn/inp2p/pkg/nat"
|
||||
"github.com/openp2p-cn/inp2p/pkg/protocol"
|
||||
"github.com/openp2p-cn/inp2p/pkg/signal"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
func TestLoginFlow(t *testing.T) {
|
||||
// Start server
|
||||
cfg := config.DefaultServerConfig()
|
||||
cfg.WSPort = 29300
|
||||
cfg.Token = 999
|
||||
|
||||
srv := New(cfg)
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/ws", srv.HandleWS)
|
||||
go http.ListenAndServe(fmt.Sprintf(":%d", cfg.WSPort), mux)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Connect as client manually
|
||||
ws, _, err := websocket.DefaultDialer.Dial(fmt.Sprintf("ws://127.0.0.1:%d/ws", cfg.WSPort), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conn := signal.NewConn(ws)
|
||||
defer conn.Close()
|
||||
|
||||
// Start read loop in background
|
||||
go conn.ReadLoop()
|
||||
|
||||
// Send login
|
||||
loginReq := protocol.LoginReq{
|
||||
Node: "testNode",
|
||||
Token: 999,
|
||||
Version: "test",
|
||||
NATType: protocol.NATCone,
|
||||
}
|
||||
|
||||
rspData, err := conn.Request(
|
||||
protocol.MsgLogin, protocol.SubLoginReq, loginReq,
|
||||
protocol.MsgLogin, protocol.SubLoginRsp,
|
||||
5*time.Second,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("login request failed: %v", err)
|
||||
}
|
||||
|
||||
var rsp protocol.LoginRsp
|
||||
protocol.DecodePayload(rspData, &rsp)
|
||||
if rsp.Error != 0 {
|
||||
t.Fatalf("login error: %d %s", rsp.Error, rsp.Detail)
|
||||
}
|
||||
log.Printf("Login OK: node=%s", rsp.Node)
|
||||
|
||||
// Verify node is registered
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
nodes := srv.GetOnlineNodes()
|
||||
if len(nodes) != 1 {
|
||||
t.Fatalf("expected 1 node, got %d", len(nodes))
|
||||
}
|
||||
if nodes[0].Name != "testNode" {
|
||||
t.Fatalf("expected testNode, got %s", nodes[0].Name)
|
||||
}
|
||||
|
||||
srv.Stop()
|
||||
}
|
||||
|
||||
func TestTwoClientsWithSTUN(t *testing.T) {
|
||||
cfg := config.DefaultServerConfig()
|
||||
cfg.WSPort = 29301
|
||||
cfg.STUNUDP1 = 29382
|
||||
cfg.STUNUDP2 = 29384
|
||||
cfg.STUNTCP1 = 29380
|
||||
cfg.STUNTCP2 = 29381
|
||||
cfg.Token = 888
|
||||
|
||||
// STUN
|
||||
stunQuit := make(chan struct{})
|
||||
defer close(stunQuit)
|
||||
go nat.ServeUDPSTUN(cfg.STUNUDP1, stunQuit)
|
||||
go nat.ServeUDPSTUN(cfg.STUNUDP2, stunQuit)
|
||||
go nat.ServeTCPSTUN(cfg.STUNTCP1, stunQuit)
|
||||
go nat.ServeTCPSTUN(cfg.STUNTCP2, stunQuit)
|
||||
|
||||
srv := New(cfg)
|
||||
srv.StartCleanup()
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/ws", srv.HandleWS)
|
||||
go http.ListenAndServe(fmt.Sprintf(":%d", cfg.WSPort), mux)
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
// NAT detect
|
||||
natResult := nat.Detect("127.0.0.1", cfg.STUNUDP1, cfg.STUNUDP2, cfg.STUNTCP1, cfg.STUNTCP2)
|
||||
log.Printf("NAT: type=%s publicIP=%s", natResult.Type, natResult.PublicIP)
|
||||
|
||||
// Client A
|
||||
connectClient := func(name string, relay bool) *signal.Conn {
|
||||
ws, _, err := websocket.DefaultDialer.Dial(fmt.Sprintf("ws://127.0.0.1:%d/ws", cfg.WSPort), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("dial %s: %v", name, err)
|
||||
}
|
||||
conn := signal.NewConn(ws)
|
||||
go conn.ReadLoop()
|
||||
|
||||
rspData, err := conn.Request(
|
||||
protocol.MsgLogin, protocol.SubLoginReq,
|
||||
protocol.LoginReq{Node: name, Token: 888, Version: "test", NATType: natResult.Type, RelayEnabled: relay},
|
||||
protocol.MsgLogin, protocol.SubLoginRsp,
|
||||
5*time.Second,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("login %s: %v", name, err)
|
||||
}
|
||||
var rsp protocol.LoginRsp
|
||||
protocol.DecodePayload(rspData, &rsp)
|
||||
if rsp.Error != 0 {
|
||||
t.Fatalf("login %s error: %s", name, rsp.Detail)
|
||||
}
|
||||
log.Printf("%s login ok", name)
|
||||
return conn
|
||||
}
|
||||
|
||||
connA := connectClient("nodeA", true)
|
||||
defer connA.Close()
|
||||
connB := connectClient("nodeB", false)
|
||||
defer connB.Close()
|
||||
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
nodes := srv.GetOnlineNodes()
|
||||
if len(nodes) != 2 {
|
||||
t.Fatalf("expected 2 nodes, got %d", len(nodes))
|
||||
}
|
||||
|
||||
// Test relay node discovery
|
||||
relays := srv.GetRelayNodes("", "nodeB")
|
||||
if len(relays) != 1 || relays[0].Name != "nodeA" {
|
||||
t.Fatalf("expected nodeA as relay, got %v", relays)
|
||||
}
|
||||
log.Printf("Relay nodes: %v", relays[0].Name)
|
||||
|
||||
srv.Stop()
|
||||
}
|
||||
Reference in New Issue
Block a user