feat: SDWAN data plane + UDP punch port fix + TUN reader

SDWAN:
- protocol: add SDWANConfig/SDWANPeer/SDWANPacket structs, MsgTunnel type
- server: sdwan.go (JSON file store), sdwan_api.go (Get/Set/broadcast/route)
- server: push SDWAN config on login, announce peer online/offline events
- server: RouteSDWANPacket routes TUN packets between nodes via signaling
- client: TUN device setup (optun), tunReadLoop reads IP packets
- client: handle SDWANConfig/SDWANPeer/SDWANDel push messages
- client: apply routes (per-node /32 + broad CIDR fallback)

UDP punch fix:
- nat/detect: capture LocalPort from STUN UDP socket for punch binding
- client: pass publicPort + localPort through login and punch config
- coordinator: include PublicPort in PunchParams for both sides
- protocol: add PublicPort to LoginReq and ReportBasic

Other:
- server: use client-reported PublicIP instead of raw r.RemoteAddr
- server: update PublicIP/Port from ReportBasic if provided
- client: config file loading with zero-value defaults backfill
- .gitignore: exclude run/, *.pid, *.log, sdwan.json
- go.mod: add golang.org/x/sys for TUN ioctl
This commit is contained in:
2026-03-02 17:48:05 +08:00
parent 673e354fe5
commit 5568ea67d9
12 changed files with 680 additions and 37 deletions

View File

@@ -35,6 +35,7 @@ func (s *Server) HandleConnectReq(from *NodeInfo, req protocol.ConnectReq) error
from.mu.RLock()
fromParams := protocol.PunchParams{
IP: from.PublicIP,
Port: from.PublicPort,
NATType: from.NATType,
HasIPv4: from.HasIPv4,
}
@@ -43,6 +44,7 @@ func (s *Server) HandleConnectReq(from *NodeInfo, req protocol.ConnectReq) error
to.mu.RLock()
toParams := protocol.PunchParams{
IP: to.PublicIP,
Port: to.PublicPort,
NATType: to.NATType,
HasIPv4: to.HasIPv4,
}

87
internal/server/sdwan.go Normal file
View File

@@ -0,0 +1,87 @@
package server
import (
"encoding/json"
"errors"
"os"
"sort"
"sync"
"time"
"github.com/openp2p-cn/inp2p/pkg/protocol"
)
type sdwanStore struct {
mu sync.RWMutex
path string
cfg protocol.SDWANConfig
}
func newSDWANStore(path string) *sdwanStore {
s := &sdwanStore{path: path}
_ = s.load()
return s
}
func (s *sdwanStore) load() error {
s.mu.Lock()
defer s.mu.Unlock()
b, err := os.ReadFile(s.path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil
}
return err
}
var c protocol.SDWANConfig
if err := json.Unmarshal(b, &c); err != nil {
return err
}
s.cfg = normalizeSDWAN(c)
return nil
}
func (s *sdwanStore) save(cfg protocol.SDWANConfig) error {
s.mu.Lock()
defer s.mu.Unlock()
cfg = normalizeSDWAN(cfg)
cfg.UpdatedAt = time.Now().Unix()
b, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}
if err := os.WriteFile(s.path, b, 0644); err != nil {
return err
}
s.cfg = cfg
return nil
}
func (s *sdwanStore) get() protocol.SDWANConfig {
s.mu.RLock()
defer s.mu.RUnlock()
return s.cfg
}
func normalizeSDWAN(c protocol.SDWANConfig) protocol.SDWANConfig {
if c.Mode == "" {
c.Mode = "hub"
}
if !c.Enabled {
c.Enabled = true
}
// de-dup nodes by node name, keep last and sort for stable output
m := make(map[string]string)
for _, n := range c.Nodes {
if n.Node == "" {
continue
}
m[n.Node] = n.IP
}
c.Nodes = c.Nodes[:0]
for node, ip := range m {
c.Nodes = append(c.Nodes, protocol.SDWANNode{Node: node, IP: ip})
}
sort.Slice(c.Nodes, func(i, j int) bool { return c.Nodes[i].Node < c.Nodes[j].Node })
return c
}

View File

@@ -0,0 +1,147 @@
package server
import (
"net/netip"
"github.com/openp2p-cn/inp2p/pkg/protocol"
)
func (s *Server) GetSDWAN() protocol.SDWANConfig {
return s.sdwan.get()
}
func (s *Server) SetSDWAN(cfg protocol.SDWANConfig) error {
if err := s.sdwan.save(cfg); err != nil {
return err
}
s.broadcastSDWAN(s.sdwan.get())
return nil
}
func (s *Server) broadcastSDWAN(cfg protocol.SDWANConfig) {
if !cfg.Enabled || cfg.GatewayCIDR == "" {
return
}
s.mu.RLock()
defer s.mu.RUnlock()
for _, n := range s.nodes {
if !n.IsOnline() {
continue
}
_ = n.Conn.Write(protocol.MsgPush, protocol.SubPushSDWANConfig, cfg)
}
}
func (s *Server) pushSDWANPeer(to *NodeInfo, peer protocol.SDWANPeer) {
if to == nil || !to.IsOnline() {
return
}
_ = to.Conn.Write(protocol.MsgPush, protocol.SubPushSDWANPeer, peer)
}
func (s *Server) pushSDWANDel(to *NodeInfo, peer protocol.SDWANPeer) {
if to == nil || !to.IsOnline() {
return
}
_ = to.Conn.Write(protocol.MsgPush, protocol.SubPushSDWANDel, peer)
}
func (s *Server) announceSDWANNodeOnline(nodeName string) {
cfg := s.sdwan.get()
if cfg.GatewayCIDR == "" {
return
}
selfIP := ""
for _, n := range cfg.Nodes {
if n.Node == nodeName {
selfIP = n.IP
break
}
}
if selfIP == "" {
return
}
s.mu.RLock()
newNode := s.nodes[nodeName]
if newNode == nil || !newNode.IsOnline() {
s.mu.RUnlock()
return
}
for _, n := range cfg.Nodes {
if n.Node == nodeName {
continue
}
other := s.nodes[n.Node]
if other == nil || !other.IsOnline() {
continue
}
// existing -> new
s.pushSDWANPeer(newNode, protocol.SDWANPeer{Node: n.Node, IP: n.IP, Online: true})
// new -> existing
s.pushSDWANPeer(other, protocol.SDWANPeer{Node: nodeName, IP: selfIP, Online: true})
}
s.mu.RUnlock()
}
func (s *Server) announceSDWANNodeOffline(nodeName string) {
cfg := s.sdwan.get()
if cfg.GatewayCIDR == "" {
return
}
selfIP := ""
for _, n := range cfg.Nodes {
if n.Node == nodeName {
selfIP = n.IP
break
}
}
s.mu.RLock()
defer s.mu.RUnlock()
for _, n := range s.nodes {
if n.Name == nodeName || !n.IsOnline() {
continue
}
s.pushSDWANDel(n, protocol.SDWANPeer{Node: nodeName, IP: selfIP, Online: false})
}
}
func (s *Server) RouteSDWANPacket(from *NodeInfo, pkt protocol.SDWANPacket) {
if from == nil {
return
}
cfg := s.sdwan.get()
if cfg.GatewayCIDR == "" || pkt.DstIP == "" || len(pkt.Payload) == 0 {
return
}
dst, err := netip.ParseAddr(pkt.DstIP)
if err != nil {
return
}
toNode := ""
for _, n := range cfg.Nodes {
if n.IP == pkt.DstIP {
toNode = n.Node
break
}
if p, err := netip.ParseAddr(n.IP); err == nil && p == dst {
toNode = n.Node
break
}
}
if toNode == "" || toNode == from.Name {
return
}
s.mu.RLock()
to := s.nodes[toNode]
s.mu.RUnlock()
if to == nil || !to.IsOnline() {
return
}
pkt.FromNode = from.Name
pkt.ToNode = toNode
_ = to.Conn.Write(protocol.MsgTunnel, protocol.SubTunnelSDWANData, pkt)
}

View File

@@ -3,6 +3,7 @@ package server
import (
"log"
"net"
"net/http"
"sync"
"time"
@@ -22,6 +23,7 @@ type NodeInfo struct {
Version string
NATType protocol.NATType
PublicIP string
PublicPort int
LanIP string
OS string
Mac string
@@ -46,18 +48,26 @@ func (n *NodeInfo) IsOnline() bool {
// 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{}
cfg config.ServerConfig
nodes map[string]*NodeInfo // node name → info
mu sync.RWMutex
upgrader websocket.Upgrader
quit chan struct{}
sdwanPath string
sdwan *sdwanStore
}
// New creates a new server.
func New(cfg config.ServerConfig) *Server {
sdwanPath := "sdwan.json"
if cfg.DBPath != "" {
sdwanPath = cfg.DBPath + ".sdwan.json"
}
return &Server{
cfg: cfg,
nodes: make(map[string]*NodeInfo),
cfg: cfg,
nodes: make(map[string]*NodeInfo),
sdwanPath: sdwanPath,
sdwan: newSDWANStore(sdwanPath),
upgrader: websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
},
@@ -170,7 +180,8 @@ func (s *Server) HandleWS(w http.ResponseWriter, r *http.Request) {
ShareBandwidth: loginReq.ShareBandwidth,
RelayEnabled: loginReq.RelayEnabled,
SuperRelay: loginReq.SuperRelay,
PublicIP: r.RemoteAddr, // will be updated by NAT detect
PublicIP: loginReq.PublicIP,
PublicPort: loginReq.PublicPort,
LoginTime: time.Now(),
LastHeartbeat: time.Now(),
Conn: conn,
@@ -178,6 +189,12 @@ func (s *Server) HandleWS(w http.ResponseWriter, r *http.Request) {
s.nodes[loginReq.Node] = node
s.mu.Unlock()
if node.PublicIP == "" {
// fallback to TCP remote addr if client didn't provide
host, _, _ := net.SplitHostPort(r.RemoteAddr)
node.PublicIP = host
}
// Send login response
conn.Write(protocol.MsgLogin, protocol.SubLoginRsp, protocol.LoginRsp{
Error: 0,
@@ -187,12 +204,19 @@ func (s *Server) HandleWS(w http.ResponseWriter, r *http.Request) {
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)
log.Printf("[server] login ok: node=%s, natType=%s, relay=%v, super=%v, version=%s, public=%s:%d",
loginReq.Node, loginReq.NATType, loginReq.RelayEnabled, loginReq.SuperRelay, loginReq.Version, node.PublicIP, node.PublicPort)
// Notify other nodes
s.broadcastNodeOnline(loginReq.Node)
// Push current SDWAN config right after login (if exists and enabled)
if cfg := s.sdwan.get(); cfg.Enabled && cfg.GatewayCIDR != "" {
_ = conn.Write(protocol.MsgPush, protocol.SubPushSDWANConfig, cfg)
}
// Event-driven SDWAN peer notification
s.announceSDWANNodeOnline(loginReq.Node)
// Register message handlers
s.registerHandlers(conn, node)
@@ -207,6 +231,7 @@ func (s *Server) HandleWS(w http.ResponseWriter, r *http.Request) {
delete(s.nodes, loginReq.Node)
}
s.mu.Unlock()
s.announceSDWANNodeOffline(loginReq.Node)
log.Printf("[server] %s offline", loginReq.Node)
}
@@ -235,6 +260,14 @@ func (s *Server) registerHandlers(conn *signal.Conn, node *NodeInfo) {
node.mu.Unlock()
log.Printf("[server] ReportBasic from %s: os=%s lanIP=%s", node.Name, report.OS, report.LanIP)
// Update public IP/port from NAT report (if provided)
if report.PublicIP != "" {
node.mu.Lock()
node.PublicIP = report.PublicIP
node.PublicPort = report.PublicPort
node.mu.Unlock()
}
// Always respond (official OpenP2P bug: not responding causes client to disconnect)
return conn.Write(protocol.MsgReport, protocol.SubReportBasic, protocol.ReportBasicRsp{Error: 0})
})
@@ -275,6 +308,16 @@ func (s *Server) registerHandlers(conn *signal.Conn, node *NodeInfo) {
protocol.DecodePayload(data, &req)
return s.handleRelayNodeReq(conn, node, req)
})
// SDWAN data plane packet relay (server as control-plane router)
conn.OnMessage(protocol.MsgTunnel, protocol.SubTunnelSDWANData, func(data []byte) error {
var pkt protocol.SDWANPacket
if err := protocol.DecodePayload(data, &pkt); err != nil {
return err
}
s.RouteSDWANPacket(node, pkt)
return nil
})
}
// handleRelayNodeReq finds and returns the best relay node.
@@ -317,9 +360,9 @@ func (s *Server) PushConnect(fromNode *NodeInfo, toNodeName string, app protocol
// Push connect request to the destination
req := protocol.ConnectReq{
From: fromNode.Name,
To: toNodeName,
FromIP: fromNode.PublicIP,
From: fromNode.Name,
To: toNodeName,
FromIP: fromNode.PublicIP,
Peer: protocol.PunchParams{
IP: fromNode.PublicIP,
NATType: fromNode.NATType,