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
265 lines
6.1 KiB
Go
265 lines
6.1 KiB
Go
// Package nat provides NAT type detection via UDP and TCP STUN.
|
|
package nat
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"time"
|
|
|
|
"github.com/openp2p-cn/inp2p/pkg/protocol"
|
|
)
|
|
|
|
const (
|
|
detectTimeout = 5 * time.Second
|
|
)
|
|
|
|
// DetectResult holds the NAT detection outcome.
|
|
type DetectResult struct {
|
|
Type protocol.NATType
|
|
PublicIP string
|
|
Port1 int // external port seen on STUN server port 1
|
|
Port2 int // external port seen on STUN server port 2
|
|
LocalPort int // local UDP port used for detection (for punch bind)
|
|
}
|
|
|
|
// stunReq is sent to the STUN endpoint.
|
|
type stunReq struct {
|
|
ID int `json:"id"`
|
|
}
|
|
|
|
// stunRsp is received from the STUN endpoint.
|
|
type stunRsp struct {
|
|
IP string `json:"ip"`
|
|
Port int `json:"port"`
|
|
ID int `json:"id"`
|
|
}
|
|
|
|
// DetectUDP sends probes from the same local port to two different server
|
|
// UDP ports. If both return the same external port → Cone; different → Symmetric.
|
|
func DetectUDP(serverIP string, port1, port2 int) DetectResult {
|
|
result := DetectResult{Type: protocol.NATUnknown}
|
|
|
|
// Bind a single local UDP port
|
|
conn, err := net.ListenPacket("udp", ":0")
|
|
if err != nil {
|
|
return result
|
|
}
|
|
defer conn.Close()
|
|
if ua, ok := conn.LocalAddr().(*net.UDPAddr); ok {
|
|
result.LocalPort = ua.Port
|
|
}
|
|
|
|
r1, err1 := probeUDP(conn, serverIP, port1, 1)
|
|
r2, err2 := probeUDP(conn, serverIP, port2, 2)
|
|
|
|
if err1 != nil || err2 != nil {
|
|
return result // timeout → NATUnknown
|
|
}
|
|
|
|
result.PublicIP = r1.IP
|
|
result.Port1 = r1.Port
|
|
result.Port2 = r2.Port
|
|
|
|
if r1.Port == r2.Port {
|
|
result.Type = protocol.NATCone
|
|
} else {
|
|
result.Type = protocol.NATSymmetric
|
|
}
|
|
|
|
// Check if public IP equals local IP → no NAT
|
|
localIP := conn.LocalAddr().(*net.UDPAddr).IP.String()
|
|
if localIP == r1.IP || r1.IP == "" {
|
|
// might be public
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func probeUDP(conn net.PacketConn, serverIP string, port, id int) (stunRsp, error) {
|
|
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", serverIP, port))
|
|
if err != nil {
|
|
return stunRsp{}, err
|
|
}
|
|
|
|
frame, _ := protocol.Encode(protocol.MsgNAT, protocol.SubNATDetectReq, stunReq{ID: id})
|
|
|
|
conn.SetWriteDeadline(time.Now().Add(detectTimeout))
|
|
if _, err := conn.WriteTo(frame, addr); err != nil {
|
|
return stunRsp{}, err
|
|
}
|
|
|
|
buf := make([]byte, 1024)
|
|
conn.SetReadDeadline(time.Now().Add(detectTimeout))
|
|
n, _, err := conn.ReadFrom(buf)
|
|
if err != nil {
|
|
return stunRsp{}, err
|
|
}
|
|
|
|
var rsp stunRsp
|
|
if n > protocol.HeaderSize {
|
|
json.Unmarshal(buf[protocol.HeaderSize:n], &rsp)
|
|
}
|
|
return rsp, nil
|
|
}
|
|
|
|
// DetectTCP connects to two different TCP ports on the server and compares
|
|
// the observed external port. This is the fallback when UDP is blocked.
|
|
func DetectTCP(serverIP string, port1, port2 int) DetectResult {
|
|
result := DetectResult{Type: protocol.NATUnknown}
|
|
|
|
r1, err1 := probeTCP(serverIP, port1, 1)
|
|
r2, err2 := probeTCP(serverIP, port2, 2)
|
|
|
|
if err1 != nil || err2 != nil {
|
|
return result
|
|
}
|
|
|
|
result.PublicIP = r1.IP
|
|
result.Port1 = r1.Port
|
|
result.Port2 = r2.Port
|
|
|
|
if r1.Port == r2.Port {
|
|
result.Type = protocol.NATCone
|
|
} else {
|
|
result.Type = protocol.NATSymmetric
|
|
}
|
|
return result
|
|
}
|
|
|
|
func probeTCP(serverIP string, port, id int) (stunRsp, error) {
|
|
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", serverIP, port), detectTimeout)
|
|
if err != nil {
|
|
return stunRsp{}, err
|
|
}
|
|
defer conn.Close()
|
|
|
|
frame, _ := protocol.Encode(protocol.MsgNAT, protocol.SubNATDetectReq, stunReq{ID: id})
|
|
conn.SetWriteDeadline(time.Now().Add(detectTimeout))
|
|
if _, err := conn.Write(frame); err != nil {
|
|
return stunRsp{}, err
|
|
}
|
|
|
|
buf := make([]byte, 1024)
|
|
conn.SetReadDeadline(time.Now().Add(detectTimeout))
|
|
n, err := conn.Read(buf)
|
|
if err != nil {
|
|
return stunRsp{}, err
|
|
}
|
|
|
|
var rsp stunRsp
|
|
if n > protocol.HeaderSize {
|
|
json.Unmarshal(buf[protocol.HeaderSize:n], &rsp)
|
|
}
|
|
return rsp, nil
|
|
}
|
|
|
|
// Detect runs UDP detection first, falls back to TCP if UDP is blocked.
|
|
func Detect(serverIP string, udpPort1, udpPort2, tcpPort1, tcpPort2 int) DetectResult {
|
|
result := DetectUDP(serverIP, udpPort1, udpPort2)
|
|
if result.Type != protocol.NATUnknown {
|
|
return result
|
|
}
|
|
// UDP blocked, fallback to TCP
|
|
return DetectTCP(serverIP, tcpPort1, tcpPort2)
|
|
}
|
|
|
|
// ─── Server-side STUN handler ───
|
|
|
|
// ServeUDPSTUN listens on a UDP port and echoes back the sender's observed IP:port.
|
|
func ServeUDPSTUN(port int, quit <-chan struct{}) error {
|
|
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf(":%d", port))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
conn, err := net.ListenUDP("udp", addr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
|
|
go func() {
|
|
<-quit
|
|
conn.Close()
|
|
}()
|
|
|
|
buf := make([]byte, 1024)
|
|
for {
|
|
n, remoteAddr, err := conn.ReadFromUDP(buf)
|
|
if err != nil {
|
|
select {
|
|
case <-quit:
|
|
return nil
|
|
default:
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Parse request
|
|
var req stunReq
|
|
if n > protocol.HeaderSize {
|
|
json.Unmarshal(buf[protocol.HeaderSize:n], &req)
|
|
}
|
|
|
|
// Reply with observed address
|
|
rsp := stunRsp{
|
|
IP: remoteAddr.IP.String(),
|
|
Port: remoteAddr.Port,
|
|
ID: req.ID,
|
|
}
|
|
frame, _ := protocol.Encode(protocol.MsgNAT, protocol.SubNATDetectRsp, rsp)
|
|
conn.WriteToUDP(frame, remoteAddr)
|
|
}
|
|
}
|
|
|
|
// ServeTCPSTUN listens on a TCP port. Each connection: read one req, write one rsp with observed addr.
|
|
func ServeTCPSTUN(port int, quit <-chan struct{}) error {
|
|
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer ln.Close()
|
|
|
|
go func() {
|
|
<-quit
|
|
ln.Close()
|
|
}()
|
|
|
|
for {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
select {
|
|
case <-quit:
|
|
return nil
|
|
default:
|
|
continue
|
|
}
|
|
}
|
|
go func(c net.Conn) {
|
|
defer c.Close()
|
|
remoteAddr := c.RemoteAddr().(*net.TCPAddr)
|
|
|
|
buf := make([]byte, 1024)
|
|
c.SetReadDeadline(time.Now().Add(10 * time.Second))
|
|
n, err := c.Read(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
var req stunReq
|
|
if n > protocol.HeaderSize {
|
|
json.Unmarshal(buf[protocol.HeaderSize:n], &req)
|
|
}
|
|
|
|
rsp := stunRsp{
|
|
IP: remoteAddr.IP.String(),
|
|
Port: remoteAddr.Port,
|
|
ID: req.ID,
|
|
}
|
|
frame, _ := protocol.Encode(protocol.MsgNAT, protocol.SubNATDetectRsp, rsp)
|
|
c.SetWriteDeadline(time.Now().Add(5 * time.Second))
|
|
c.Write(frame)
|
|
}(conn)
|
|
}
|
|
}
|