From e458e43d831f2977f59ad0b86f72c3617348a918 Mon Sep 17 00:00:00 2001 From: jdl Date: Mon, 15 Sep 2025 04:07:56 +0200 Subject: [PATCH] WIP --- README.md | 3 +- cmd/vppn/main.go | 2 +- go.mod | 2 +- peer/files.go | 8 +++ peer/globals.go | 2 +- peer/main2.go | 153 +++++++++++++++++++++++++++++++++++++++++++ peer/mcwriter.go | 6 +- peer/peer.go | 21 ++++-- peer/remote.go | 19 ++++++ peer/remotefsm.go | 2 +- peer/statusserver.go | 57 ++++++++++++++++ 11 files changed, 261 insertions(+), 14 deletions(-) create mode 100644 peer/main2.go create mode 100644 peer/statusserver.go diff --git a/README.md b/README.md index 7908323..06f5594 100644 --- a/README.md +++ b/README.md @@ -65,9 +65,10 @@ AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_ADMIN Type=simple User=user WorkingDirectory=/home/user/ -ExecStart=/home/user/vppn -name mynetwork -hub-address https://my.hub -api-key 1234567890 +ExecStart=/home/user/vppn run my_net_name https://my.hub my_api_key Restart=always RestartSec=8 +TimeoutStopSec=24 [Install] WantedBy=multi-user.target diff --git a/cmd/vppn/main.go b/cmd/vppn/main.go index 5daa907..dada4cf 100644 --- a/cmd/vppn/main.go +++ b/cmd/vppn/main.go @@ -7,5 +7,5 @@ import ( func main() { log.SetFlags(0) - peer.Main() + peer.Main2() } diff --git a/go.mod b/go.mod index e55e1f6..1cf4140 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module vppn -go 1.24.1 +go 1.25.1 require ( git.crumpington.com/lib/go v0.9.0 diff --git a/peer/files.go b/peer/files.go index f4ee973..6e6afe5 100644 --- a/peer/files.go +++ b/peer/files.go @@ -29,6 +29,10 @@ func configDir(netName string) string { return filepath.Join(d, ".vppn", netName) } +func lockFilePath(netName string) string { + return filepath.Join(configDir(netName), "__lock__") +} + func peerConfigPath(netName string) string { return filepath.Join(configDir(netName), "config.json") } @@ -41,6 +45,10 @@ func startupCountPath(netName string) string { return filepath.Join(configDir(netName), "startup_count.json") } +func statusSocketPath(netName string) string { + return filepath.Join(configDir(netName), "status.sock") +} + func storeJson(x any, outPath string) error { outDir := filepath.Dir(outPath) _ = os.MkdirAll(outDir, 0700) diff --git a/peer/globals.go b/peer/globals.go index 6a5bb0b..861a319 100644 --- a/peer/globals.go +++ b/peer/globals.go @@ -19,7 +19,7 @@ const ( controlCipherOverhead = 16 dataCipherOverhead = 16 - signOverhead = 64 + signingOverhead = 64 pingInterval = 8 * time.Second timeoutInterval = 30 * time.Second diff --git a/peer/main2.go b/peer/main2.go new file mode 100644 index 0000000..52bcf21 --- /dev/null +++ b/peer/main2.go @@ -0,0 +1,153 @@ +package peer + +import ( + "encoding/json" + "fmt" + "log" + "net" + "net/http" + "os" + "strings" +) + +// Usage: +// +// vppn netName run +// vppn netName status +func Main2() { + printUsage := func() { + fmt.Fprintf(os.Stderr, `%s COMMAND [ARGUMENTS...] + +Available commands: + run + status +`, os.Args[0]) + os.Exit(1) + } + + if len(os.Args) < 2 { + printUsage() + } + + command := os.Args[1] + + switch command { + case "run": + main_run() + case "status": + main_status() + default: + printUsage() + } +} + +// ---------------------------------------------------------------------------- + +type mainArgs struct { + NetName string + HubAddress string + APIKey string +} + +func main_run() { + printUsage := func() { + fmt.Fprintf(os.Stderr, `Usage: %s run NETWORK_NAME HUB_ADDRESS API_KEY + + NETWORK_NAME + Unique name of the network interface created. The network name + shouldn't change between invocations of the application. + + HUB_ADDRESS + The address of the hub server. This should also contain the scheme, for + example https://hub.domain.com/. + + API_KEY + The API key assigned to this peer by the hub. + +`, os.Args[0]) + os.Exit(1) + } + + if len(os.Args) != 5 { + printUsage() + } + + args := mainArgs{ + NetName: os.Args[2], + HubAddress: os.Args[3], + APIKey: os.Args[4], + } + + newPeerMain(args).Run() +} + +// ---------------------------------------------------------------------------- + +func main_status() { + printUsage := func() { + fmt.Fprintf(os.Stderr, `Usage: %s status NETWORK_NAME + + NETWORK_NAME + Unique name of the network interface created. + +`, os.Args[0]) + os.Exit(1) + } + + if len(os.Args) != 3 { + printUsage() + } + + netName := os.Args[2] + + client := http.Client{ + Transport: &http.Transport{ + Dial: func(_, _ string) (net.Conn, error) { + return net.Dial("unix", statusSocketPath(netName)) + }, + }, + } + + getURL := "http://unix" + statusSocketPath(netName) + resp, err := client.Get(getURL) + if err != nil { + log.Fatalf("Failed to get response: %v", err) + } + + report := StatusReport{} + if err := json.NewDecoder(resp.Body).Decode(&report); err != nil { + log.Fatalf("Failed to decode status report: %v", err) + } + + b := strings.Builder{} + + for _, status := range report.Remotes { + b.WriteString(fmt.Sprintf("%3d ", status.PeerIP)) + if status.Up { + b.WriteString("UP ") + } else { + b.WriteString("DOWN ") + } + if status.Relay && status.Direct { + b.WriteString("RELAY ") + } else if status.Server { + b.WriteString("SERVER ") + } else { + b.WriteString("CLIENT ") + } + + if status.Direct { + b.WriteString("DIRECT ") + } else { + b.WriteString("RELAYED ") + } + + b.WriteString(fmt.Sprintf("%45s ", status.DirectAddr)) + + b.WriteString(status.Name) + + b.WriteString("\n") + } + + fmt.Print(b.String()) +} diff --git a/peer/mcwriter.go b/peer/mcwriter.go index 0b520d1..5430aac 100644 --- a/peer/mcwriter.go +++ b/peer/mcwriter.go @@ -15,16 +15,16 @@ func createLocalDiscoveryPacket(localIP byte, signingKey []byte) []byte { } buf := make([]byte, headerSize) h.Marshal(buf) - out := make([]byte, headerSize+signOverhead) + out := make([]byte, headerSize+signingOverhead) return sign.Sign(out[:0], buf, (*[64]byte)(signingKey)) } func headerFromLocalDiscoveryPacket(pkt []byte) (h Header, ok bool) { - if len(pkt) != headerSize+signOverhead { + if len(pkt) != headerSize+signingOverhead { return } - h.Parse(pkt[signOverhead:]) + h.Parse(pkt[signingOverhead:]) ok = true return } diff --git a/peer/peer.go b/peer/peer.go index 69adbaf..a98069d 100644 --- a/peer/peer.go +++ b/peer/peer.go @@ -13,6 +13,8 @@ import ( "net/url" "os" "vppn/m" + + "git.crumpington.com/lib/go/flock" ) type peerMain struct { @@ -20,12 +22,7 @@ type peerMain struct { ifReader *IFReader connReader *ConnReader hubPoller *HubPoller -} - -type mainArgs struct { - NetName string - HubAddress string - APIKey string + lockFile *os.File } func newPeerMain(args mainArgs) *peerMain { @@ -33,6 +30,14 @@ func newPeerMain(args mainArgs) *peerMain { log.Printf("[Main] "+s, args...) } + lockFile, err := flock.TryLock(lockFilePath(args.NetName)) + if err != nil { + log.Fatalf("Failed to open lock file: %v", err) + } + if lockFile == nil { + log.Fatalf("Failed to obtain file lock.") + } + config, err := loadPeerConfig(args.NetName) if err != nil { logf("Failed to load configuration: %v", err) @@ -100,11 +105,15 @@ func newPeerMain(args mainArgs) *peerMain { log.Fatalf("Failed to create hub poller: %v", err) } + // Start status server. + go runStatusServer(g, statusSocketPath(args.NetName)) + return &peerMain{ Globals: g, ifReader: NewIFReader(g), connReader: NewConnReader(g, conn), hubPoller: hubPoller, + lockFile: lockFile, } } diff --git a/peer/remote.go b/peer/remote.go index 84b5c16..b468469 100644 --- a/peer/remote.go +++ b/peer/remote.go @@ -106,6 +106,25 @@ func (r *Remote) encryptControl(conf remoteConfig, packet []byte) []byte { return conf.ControlCipher.Encrypt(h, packet, packet[len(packet):cap(packet)]) } +func (r *Remote) Status() (RemoteStatus, bool) { + conf := r.conf() + if conf.Peer == nil { + return RemoteStatus{}, false + } + + return RemoteStatus{ + PeerIP: conf.Peer.PeerIP, + Up: conf.Up, + Name: conf.Peer.Name, + PublicIP: conf.Peer.PublicIP, + Port: conf.Peer.Port, + Relay: conf.Peer.Relay, + Server: conf.Server, + Direct: conf.Direct, + DirectAddr: conf.DirectAddr, + }, true +} + // ---------------------------------------------------------------------------- // SendDataTo sends a data packet to the remote, called by the IFReader. diff --git a/peer/remotefsm.go b/peer/remotefsm.go index fef32a3..b2158bb 100644 --- a/peer/remotefsm.go +++ b/peer/remotefsm.go @@ -171,7 +171,7 @@ func (r *remoteFSM) stateServer_onSyn(msg controlMsg[packetSyn]) { conf.DirectAddr = msg.SrcAddr // Update data cipher if the key has changed. - if !conf.DataCipher.HasKey(p.SharedKey) { + if conf.DataCipher == nil || !conf.DataCipher.HasKey(p.SharedKey) { conf.DataCipher = newDataCipherFromKey(p.SharedKey) } diff --git a/peer/statusserver.go b/peer/statusserver.go new file mode 100644 index 0000000..adbf23e --- /dev/null +++ b/peer/statusserver.go @@ -0,0 +1,57 @@ +package peer + +import ( + "encoding/json" + "log" + "net" + "net/http" + "net/netip" + "os" +) + +type StatusReport struct { + Remotes []RemoteStatus +} + +type RemoteStatus struct { + PeerIP byte + Up bool + Name string + PublicIP []byte + Port uint16 + Relay bool + Server bool + Direct bool + DirectAddr netip.AddrPort +} + +func runStatusServer(g Globals, socketPath string) { + _ = os.RemoveAll(socketPath) + + handler := func(w http.ResponseWriter, r *http.Request) { + report := StatusReport{ + Remotes: make([]RemoteStatus, 0, 255), + } + for i := range g.RemotePeers { + remote := g.RemotePeers[i].Load() + status, ok := remote.Status() + if !ok { + continue + } + report.Remotes = append(report.Remotes, status) + } + json.NewEncoder(w).Encode(report) + } + + server := http.Server{ + Handler: http.HandlerFunc(handler), + } + + unixListener, err := net.Listen("unix", socketPath) + if err != nil { + log.Fatalf("Failed to bind to unix socket: %v", err) + } + if err := server.Serve(unixListener); err != nil { + log.Fatalf("Failed to serve on unix socket: %v", err) + } +}