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:
2026-03-02 15:13:22 +08:00
commit 91e3d4da2a
23 changed files with 4681 additions and 0 deletions

118
cmd/inp2pc/main.go Normal file
View File

@@ -0,0 +1,118 @@
// inp2pc — INP2P P2P Client
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"github.com/openp2p-cn/inp2p/internal/client"
"github.com/openp2p-cn/inp2p/pkg/auth"
"github.com/openp2p-cn/inp2p/pkg/config"
)
func main() {
cfg := config.DefaultClientConfig()
flag.StringVar(&cfg.ServerHost, "serverhost", "", "Server hostname or IP (required)")
flag.IntVar(&cfg.ServerPort, "serverport", cfg.ServerPort, "Server WSS port")
flag.StringVar(&cfg.Node, "node", "", "Node name (default: hostname)")
token := flag.Uint64("token", 0, "Authentication token (uint64)")
user := flag.String("user", "", "Username for token generation")
pass := flag.String("password", "", "Password for token generation")
flag.BoolVar(&cfg.Insecure, "insecure", false, "Skip TLS verification")
flag.BoolVar(&cfg.RelayEnabled, "relay", false, "Enable relay capability")
flag.BoolVar(&cfg.SuperRelay, "super", false, "Register as super relay node (implies -relay)")
flag.IntVar(&cfg.RelayPort, "relay-port", cfg.RelayPort, "Relay listen port")
flag.IntVar(&cfg.MaxRelayLoad, "relay-max", cfg.MaxRelayLoad, "Max concurrent relay sessions")
flag.IntVar(&cfg.ShareBandwidth, "bw", cfg.ShareBandwidth, "Share bandwidth (Mbps)")
flag.IntVar(&cfg.STUNUDP1, "stun-udp1", cfg.STUNUDP1, "UDP STUN port 1")
flag.IntVar(&cfg.STUNUDP2, "stun-udp2", cfg.STUNUDP2, "UDP STUN port 2")
flag.IntVar(&cfg.STUNTCP1, "stun-tcp1", cfg.STUNTCP1, "TCP STUN port 1")
flag.IntVar(&cfg.STUNTCP2, "stun-tcp2", cfg.STUNTCP2, "TCP STUN port 2")
flag.IntVar(&cfg.LogLevel, "log-level", cfg.LogLevel, "Log level")
configFile := flag.String("config", "config.json", "Config file path")
newConfig := flag.Bool("newconfig", false, "Ignore existing config, use command line args only")
version := flag.Bool("version", false, "Print version and exit")
flag.Parse()
if *version {
fmt.Printf("inp2pc version %s\n", config.Version)
os.Exit(0)
}
// Load config file first (unless -newconfig)
if !*newConfig {
if data, err := os.ReadFile(*configFile); err == nil {
var fileCfg config.ClientConfig
if err := json.Unmarshal(data, &fileCfg); err == nil {
cfg = fileCfg
log.Printf("[main] loaded config from %s", *configFile)
}
}
}
// Command line flags override config file
flag.Visit(func(f *flag.Flag) {
switch f.Name {
case "serverhost":
cfg.ServerHost = f.Value.String()
case "serverport":
fmt.Sscanf(f.Value.String(), "%d", &cfg.ServerPort)
case "node":
cfg.Node = f.Value.String()
case "insecure":
cfg.Insecure = true
case "relay":
cfg.RelayEnabled = true
case "super":
cfg.SuperRelay = true
cfg.RelayEnabled = true // super implies relay
case "bw":
fmt.Sscanf(f.Value.String(), "%d", &cfg.ShareBandwidth)
}
})
// Token from flag or credentials
if *token > 0 {
cfg.Token = *token
} else if *user != "" && *pass != "" {
cfg.Token = auth.MakeToken(*user, *pass)
log.Printf("[main] token: %d", cfg.Token)
}
if err := cfg.Validate(); err != nil {
log.Fatalf("[main] config error: %v", err)
}
log.Printf("[main] inp2pc v%s starting", config.Version)
log.Printf("[main] node=%s server=%s:%d relay=%v super=%v",
cfg.Node, cfg.ServerHost, cfg.ServerPort, cfg.RelayEnabled, cfg.SuperRelay)
// Save config
if data, err := json.MarshalIndent(cfg, "", " "); err == nil {
os.WriteFile(*configFile, data, 0644)
}
// Create and run client
c := client.New(cfg)
// Handle shutdown
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigCh
log.Println("[main] shutting down...")
c.Stop()
}()
if err := c.Run(); err != nil {
log.Fatalf("[main] client error: %v", err)
}
log.Println("[main] goodbye")
}

119
cmd/inp2ps/main.go Normal file
View File

@@ -0,0 +1,119 @@
// inp2ps — INP2P Signaling Server
package main
import (
"context"
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/openp2p-cn/inp2p/internal/server"
"github.com/openp2p-cn/inp2p/pkg/auth"
"github.com/openp2p-cn/inp2p/pkg/config"
"github.com/openp2p-cn/inp2p/pkg/nat"
)
func main() {
cfg := config.DefaultServerConfig()
flag.IntVar(&cfg.WSPort, "ws-port", cfg.WSPort, "WebSocket signaling port")
flag.IntVar(&cfg.WebPort, "web-port", cfg.WebPort, "Web console port")
flag.IntVar(&cfg.STUNUDP1, "stun-udp1", cfg.STUNUDP1, "UDP STUN port 1")
flag.IntVar(&cfg.STUNUDP2, "stun-udp2", cfg.STUNUDP2, "UDP STUN port 2")
flag.IntVar(&cfg.STUNTCP1, "stun-tcp1", cfg.STUNTCP1, "TCP STUN port 1")
flag.IntVar(&cfg.STUNTCP2, "stun-tcp2", cfg.STUNTCP2, "TCP STUN port 2")
flag.StringVar(&cfg.DBPath, "db", cfg.DBPath, "SQLite database path")
flag.StringVar(&cfg.CertFile, "cert", "", "TLS certificate file")
flag.StringVar(&cfg.KeyFile, "key", "", "TLS key file")
flag.IntVar(&cfg.LogLevel, "log-level", cfg.LogLevel, "Log level (0=debug 1=info 2=warn 3=error)")
token := flag.Uint64("token", 0, "Master authentication token (uint64)")
user := flag.String("user", "", "Username for token generation (requires -password)")
pass := flag.String("password", "", "Password for token generation")
version := flag.Bool("version", false, "Print version and exit")
flag.Parse()
if *version {
fmt.Printf("inp2ps version %s\n", config.Version)
os.Exit(0)
}
// Token: either direct value or generated from user+password
if *token > 0 {
cfg.Token = *token
} else if *user != "" && *pass != "" {
cfg.Token = auth.MakeToken(*user, *pass)
log.Printf("[main] token generated from credentials: %d", cfg.Token)
}
cfg.FillFromEnv()
if err := cfg.Validate(); err != nil {
log.Fatalf("[main] config error: %v", err)
}
log.Printf("[main] inp2ps v%s starting", config.Version)
log.Printf("[main] WSS :%d | STUN UDP :%d,%d | STUN TCP :%d,%d",
cfg.WSPort, cfg.STUNUDP1, cfg.STUNUDP2, cfg.STUNTCP1, cfg.STUNTCP2)
// ─── STUN Servers ───
stunQuit := make(chan struct{})
startSTUN := func(proto string, port int, fn func(int, <-chan struct{}) error) {
go func() {
log.Printf("[main] %s STUN listening on :%d", proto, port)
if err := fn(port, stunQuit); err != nil {
log.Printf("[main] %s STUN :%d error: %v", proto, port, err)
}
}()
}
startSTUN("UDP", cfg.STUNUDP1, nat.ServeUDPSTUN)
if cfg.STUNUDP2 != cfg.STUNUDP1 {
startSTUN("UDP", cfg.STUNUDP2, nat.ServeUDPSTUN)
}
startSTUN("TCP", cfg.STUNTCP1, nat.ServeTCPSTUN)
if cfg.STUNTCP2 != cfg.STUNTCP1 {
startSTUN("TCP", cfg.STUNTCP2, nat.ServeTCPSTUN)
}
// ─── Signaling Server ───
srv := server.New(cfg)
srv.StartCleanup()
mux := http.NewServeMux()
mux.HandleFunc("/ws", srv.HandleWS)
mux.HandleFunc("/api/v1/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"status":"ok","version":"%s","nodes":%d}`, config.Version, len(srv.GetOnlineNodes()))
})
// ─── HTTP Listener ───
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", cfg.WSPort))
if err != nil {
log.Fatalf("[main] listen :%d: %v", cfg.WSPort, err)
}
log.Printf("[main] signaling server on :%d (no TLS — use reverse proxy for production)", cfg.WSPort)
httpSrv := &http.Server{Handler: mux}
go func() {
if err := httpSrv.Serve(ln); err != http.ErrServerClosed {
log.Fatalf("[main] serve: %v", err)
}
}()
// ─── Graceful Shutdown ───
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
log.Println("[main] shutting down...")
close(stunQuit)
srv.Stop()
httpSrv.Shutdown(context.Background())
log.Println("[main] goodbye")
}