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
261 lines
5.9 KiB
Go
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)
|
|
}
|
|
}
|