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:
204
pkg/punch/punch.go
Normal file
204
pkg/punch/punch.go
Normal file
@@ -0,0 +1,204 @@
|
||||
// Package punch implements UDP and TCP hole-punching.
|
||||
package punch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/openp2p-cn/inp2p/pkg/protocol"
|
||||
)
|
||||
|
||||
const (
|
||||
punchTimeout = 5 * time.Second
|
||||
punchRetries = 5
|
||||
handshakeMagic = "INP2P-PUNCH"
|
||||
handshakeAck = "INP2P-PUNCH-ACK"
|
||||
)
|
||||
|
||||
// Result holds the outcome of a punch attempt.
|
||||
type Result struct {
|
||||
Conn net.Conn
|
||||
Mode string // "udp" or "tcp"
|
||||
RTT time.Duration
|
||||
PeerAddr string
|
||||
Error error
|
||||
}
|
||||
|
||||
// Config for a punch attempt.
|
||||
type Config struct {
|
||||
PeerIP string
|
||||
PeerPort int
|
||||
PeerNAT protocol.NATType
|
||||
SelfNAT protocol.NATType
|
||||
SelfPort int // local port to bind (0 = auto)
|
||||
IsInitiator bool
|
||||
}
|
||||
|
||||
// AttemptUDP tries to establish a UDP connection via hole-punching.
|
||||
// Both sides must call this simultaneously (coordinated by server).
|
||||
func AttemptUDP(cfg Config) Result {
|
||||
if !protocol.CanPunch(cfg.SelfNAT, cfg.PeerNAT) {
|
||||
return Result{Error: fmt.Errorf("cannot UDP punch: self=%s peer=%s", cfg.SelfNAT, cfg.PeerNAT)}
|
||||
}
|
||||
|
||||
localAddr := &net.UDPAddr{Port: cfg.SelfPort}
|
||||
conn, err := net.ListenUDP("udp", localAddr)
|
||||
if err != nil {
|
||||
return Result{Error: fmt.Errorf("listen UDP: %w", err)}
|
||||
}
|
||||
|
||||
peerAddr := &net.UDPAddr{
|
||||
IP: net.ParseIP(cfg.PeerIP),
|
||||
Port: cfg.PeerPort,
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
// Send punch packets
|
||||
for i := 0; i < punchRetries; i++ {
|
||||
conn.SetWriteDeadline(time.Now().Add(time.Second))
|
||||
conn.WriteTo([]byte(handshakeMagic), peerAddr)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
|
||||
// Listen for response
|
||||
buf := make([]byte, 256)
|
||||
conn.SetReadDeadline(time.Now().Add(punchTimeout))
|
||||
n, from, err := conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return Result{Error: fmt.Errorf("UDP punch timeout: %w", err)}
|
||||
}
|
||||
|
||||
// Verify handshake
|
||||
msg := string(buf[:n])
|
||||
if msg != handshakeMagic && msg != handshakeAck {
|
||||
conn.Close()
|
||||
return Result{Error: fmt.Errorf("unexpected punch data: %q", msg)}
|
||||
}
|
||||
|
||||
// Send ack
|
||||
conn.WriteTo([]byte(handshakeAck), from)
|
||||
|
||||
rtt := time.Since(start)
|
||||
log.Printf("[punch] UDP punch ok: peer=%s rtt=%s", from, rtt)
|
||||
|
||||
return Result{
|
||||
Conn: conn,
|
||||
Mode: "udp",
|
||||
RTT: rtt,
|
||||
PeerAddr: from.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// AttemptTCP tries TCP hole-punching using simultaneous SYN.
|
||||
// This works by having both sides dial each other at the same time.
|
||||
func AttemptTCP(cfg Config) Result {
|
||||
if !protocol.CanPunch(cfg.SelfNAT, cfg.PeerNAT) {
|
||||
return Result{Error: fmt.Errorf("cannot TCP punch: self=%s peer=%s", cfg.SelfNAT, cfg.PeerNAT)}
|
||||
}
|
||||
|
||||
peerAddr := fmt.Sprintf("%s:%d", cfg.PeerIP, cfg.PeerPort)
|
||||
start := time.Now()
|
||||
|
||||
// TCP simultaneous open: keep trying to dial the peer
|
||||
var conn net.Conn
|
||||
var err error
|
||||
for i := 0; i < punchRetries*2; i++ {
|
||||
d := net.Dialer{Timeout: time.Second, LocalAddr: &net.TCPAddr{Port: cfg.SelfPort}}
|
||||
conn, err = d.Dial("tcp", peerAddr)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return Result{Error: fmt.Errorf("TCP punch failed: %w", err)}
|
||||
}
|
||||
|
||||
// TCP handshake for INP2P
|
||||
conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
|
||||
conn.Write([]byte(handshakeMagic))
|
||||
|
||||
buf := make([]byte, 256)
|
||||
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return Result{Error: fmt.Errorf("TCP handshake read: %w", err)}
|
||||
}
|
||||
|
||||
msg := string(buf[:n])
|
||||
if msg != handshakeMagic && msg != handshakeAck {
|
||||
conn.Close()
|
||||
return Result{Error: fmt.Errorf("TCP unexpected handshake: %q", msg)}
|
||||
}
|
||||
|
||||
conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
|
||||
conn.Write([]byte(handshakeAck))
|
||||
|
||||
rtt := time.Since(start)
|
||||
log.Printf("[punch] TCP punch ok: peer=%s rtt=%s", conn.RemoteAddr(), rtt)
|
||||
|
||||
return Result{
|
||||
Conn: conn,
|
||||
Mode: "tcp",
|
||||
RTT: rtt,
|
||||
PeerAddr: conn.RemoteAddr().String(),
|
||||
}
|
||||
}
|
||||
|
||||
// AttemptDirect tries to directly connect when one side has a public IP.
|
||||
func AttemptDirect(cfg Config) Result {
|
||||
addr := fmt.Sprintf("%s:%d", cfg.PeerIP, cfg.PeerPort)
|
||||
start := time.Now()
|
||||
|
||||
conn, err := net.DialTimeout("tcp", addr, punchTimeout)
|
||||
if err != nil {
|
||||
return Result{Error: fmt.Errorf("direct connect failed: %w", err)}
|
||||
}
|
||||
|
||||
rtt := time.Since(start)
|
||||
log.Printf("[punch] direct connect ok: peer=%s rtt=%s", addr, rtt)
|
||||
|
||||
return Result{
|
||||
Conn: conn,
|
||||
Mode: "tcp-direct",
|
||||
RTT: rtt,
|
||||
PeerAddr: addr,
|
||||
}
|
||||
}
|
||||
|
||||
// Connect tries all punch methods in priority order and returns the first success.
|
||||
func Connect(cfg Config) Result {
|
||||
methods := []struct {
|
||||
name string
|
||||
fn func(Config) Result
|
||||
}{
|
||||
{"UDP-punch", AttemptUDP},
|
||||
{"TCP-punch", AttemptTCP},
|
||||
}
|
||||
|
||||
// If peer has public IP, try direct first
|
||||
if cfg.PeerNAT == protocol.NATNone {
|
||||
r := AttemptDirect(cfg)
|
||||
if r.Error == nil {
|
||||
return r
|
||||
}
|
||||
log.Printf("[punch] direct failed: %v", r.Error)
|
||||
}
|
||||
|
||||
for _, m := range methods {
|
||||
log.Printf("[punch] trying %s to %s:%d", m.name, cfg.PeerIP, cfg.PeerPort)
|
||||
r := m.fn(cfg)
|
||||
if r.Error == nil {
|
||||
return r
|
||||
}
|
||||
log.Printf("[punch] %s failed: %v", m.name, r.Error)
|
||||
}
|
||||
|
||||
return Result{Error: fmt.Errorf("all punch methods exhausted")}
|
||||
}
|
||||
Reference in New Issue
Block a user