Files
inp2p/pkg/nat/detect.go
openclaw 91e3d4da2a 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
2026-03-02 15:13:22 +08:00

261 lines
5.9 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
}
// 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()
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)
}
}