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:
260
pkg/nat/detect.go
Normal file
260
pkg/nat/detect.go
Normal file
@@ -0,0 +1,260 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user