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/hub/hub b/cmd/hub/hub deleted file mode 100755 index 70a1361..0000000 Binary files a/cmd/hub/hub and /dev/null differ 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..ad3add6 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,15 @@ module vppn -go 1.24.1 +go 1.25.1 require ( - git.crumpington.com/lib/go v0.9.0 - golang.org/x/crypto v0.36.0 - golang.org/x/sys v0.31.0 + git.crumpington.com/lib/go v0.9.1 + golang.org/x/crypto v0.42.0 + golang.org/x/sys v0.36.0 ) require ( - github.com/mattn/go-sqlite3 v1.14.24 // indirect - golang.org/x/net v0.37.0 // indirect - golang.org/x/text v0.23.0 // indirect + github.com/mattn/go-sqlite3 v1.14.32 // indirect + golang.org/x/net v0.44.0 // indirect + golang.org/x/text v0.29.0 // indirect ) diff --git a/go.sum b/go.sum index a173169..0f444e0 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,12 @@ -git.crumpington.com/lib/go v0.9.0 h1:QXoMhsycSgEUWNiiPZWl0jgBls+NI9TNR5Z6nNXslCM= -git.crumpington.com/lib/go v0.9.0/go.mod h1:i3DXiPDo/pgPMHAxUTpyo1Xj2spcvXwXcBef3aSYlnQ= -github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= -github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +git.crumpington.com/lib/go v0.9.1 h1:xLBzcgiZRB6Ky3Ce9hKE+Ko0YbkA4USF4eJk5i5RJF4= +git.crumpington.com/lib/go v0.9.1/go.mod h1:5nnfjdnUnj/FHhakaliKQKsKeSkUb0GEUKF3PqRgUXg= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= diff --git a/peer/cipher-data.go b/peer/cipher-data.go index 5c407bd..5ce8555 100644 --- a/peer/cipher-data.go +++ b/peer/cipher-data.go @@ -1,7 +1,6 @@ package peer import ( - "bytes" "crypto/aes" "crypto/cipher" "crypto/rand" @@ -39,10 +38,6 @@ func (sc *dataCipher) Key() [32]byte { return sc.key } -func (sc *dataCipher) HasKey(k [32]byte) bool { - return bytes.Equal(k[:], sc.key[:]) -} - func (sc *dataCipher) Encrypt(h Header, data, out []byte) []byte { const s = dataHeaderSize out = out[:s+dataCipherOverhead+len(data)] 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/main.go b/peer/main.go index bd20de6..fb72acb 100644 --- a/peer/main.go +++ b/peer/main.go @@ -1,23 +1,207 @@ package peer import ( - "flag" + "encoding/json" + "fmt" + "log" + "net" + "net/http" + "net/netip" "os" + "time" ) -func Main() { - args := mainArgs{} +// Usage: +// +// vppn netName run +// vppn netName status +func Main2() { + printUsage := func() { + fmt.Fprintf(os.Stderr, `%s COMMAND [ARGUMENTS...] - flag.StringVar(&args.NetName, "name", "", "[REQUIRED] The network name.") - flag.StringVar(&args.HubAddress, "hub-address", "", "[REQUIRED] The hub address.") - flag.StringVar(&args.APIKey, "api-key", "", "[REQUIRED] The node's API key.") - flag.Parse() - - if args.NetName == "" || args.HubAddress == "" || args.APIKey == "" { - flag.Usage() +Available commands: + run + status + hosts +`, os.Args[0]) os.Exit(1) } - peer := newPeerMain(args) - peer.Run() + if len(os.Args) < 2 { + printUsage() + } + + command := os.Args[1] + + switch command { + case "run": + main_run() + case "status": + main_status() + case "hosts": + main_hosts() + 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] + report := fetchStatusReport(netName) + + fmt.Printf("\n%s Status\n\n", netName) + + if len(report.Network) != 4 { + fmt.Printf("Network: %v\n\n", report.Network) + } else { + nw := report.Network + fmt.Printf("%-8s %d.%d.%d.%d/24\n", "Network", nw[0], nw[1], nw[2], nw[3]) + } + + if report.RelayPeerIP != 0 { + fmt.Printf("%-8s %d\n\n", "Relay", report.RelayPeerIP) + } else { + fmt.Printf("%-8s -\n\n", "Relay") + } + + for _, status := range report.Remotes { + fmt.Printf("%3d %s\n", status.PeerIP, status.Name) + fmt.Printf(" %-11s %v\n", "Up", status.Up) + + pubIP, ok := netip.AddrFromSlice(status.PublicIP) + if ok { + fmt.Printf(" %-11s %v\n", "Public IP", pubIP) + } else { + fmt.Printf(" %-11s\n", "Public IP") + } + fmt.Printf(" %-11s %d\n", "Port", status.Port) + fmt.Printf(" %-11s %v\n", "Relay", status.Relay) + fmt.Printf(" %-11s %v\n", "Server", status.Server) + fmt.Printf(" %-11s %v\n", "Direct", status.Direct) + if status.DirectAddr.IsValid() { + fmt.Printf(" %-11s %v\n", "Address", status.DirectAddr) + } + fmt.Println("") + } +} + +// ---------------------------------------------------------------------------- + +func main_hosts() { + printUsage := func() { + fmt.Fprintf(os.Stderr, `Usage: %s hosts 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] + state, err := loadNetworkState(netName) + if err != nil { + log.Fatalf("Failed to load network state: %v", err) + } + + config, err := loadPeerConfig(netName) + if err != nil { + log.Fatalf("Failed to load config: %v", err) + } + + nw := config.Network + for _, peer := range state.Peers { + if peer == nil { + continue + } + fmt.Printf("%d.%d.%d.%d %s\n", + nw[0], nw[1], nw[2], peer.PeerIP, peer.Name) + } + fmt.Println("") +} + +// ---------------------------------------------------------------------------- + +func fetchStatusReport(netName string) StatusReport { + client := http.Client{ + Transport: &http.Transport{ + Dial: func(_, _ string) (net.Conn, error) { + return net.Dial("unix", statusSocketPath(netName)) + }, + }, + Timeout: 8 * time.Second, + } + + 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) + } + + return report } 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..9f6a442 100644 --- a/peer/remotefsm.go +++ b/peer/remotefsm.go @@ -154,6 +154,8 @@ func (r *remoteFSM) stateServer_onInit(msg controlMsg[packetInit]) { Version: version, } + // Reset traceID to force state update on SYN. + r.traceID = 0 r.sendControl(conf, init.Marshal(r.buf)) } @@ -161,23 +163,19 @@ func (r *remoteFSM) stateServer_onSyn(msg controlMsg[packetSyn]) { r.lastSeen = time.Now() p := msg.Packet - // Before we can respond to this packet, we need to make sure the - // route is setup properly. conf := r.conf() - logSyn := !conf.Up || conf.Direct != p.Direct - conf.Up = true - conf.Direct = p.Direct - conf.DirectAddr = msg.SrcAddr + // New trace ID => Update the route configuration. + if p.TraceID != r.traceID { + r.traceID = p.TraceID + + conf.Up = true + conf.Direct = p.Direct + conf.DirectAddr = msg.SrcAddr - // Update data cipher if the key has changed. - if !conf.DataCipher.HasKey(p.SharedKey) { conf.DataCipher = newDataCipherFromKey(p.SharedKey) - } - r.updateConf(conf) - - if logSyn { + r.updateConf(conf) r.logf("Got SYN.") } @@ -191,13 +189,14 @@ func (r *remoteFSM) stateServer_onSyn(msg controlMsg[packetSyn]) { return } - // Send probes if not a direct connection. + // Send probes if not a direct connection. The server sends probes without + // trace IDs unless responding to a client probe. for _, addr := range msg.Packet.PossibleAddrs { if !addr.IsValid() { break } r.logf("Probing %v...", addr) - r.sendControlToAddr(packetProbe{TraceID: r.NewTraceID()}.Marshal(r.buf), addr) + r.sendControlToAddr(packetProbe{}.Marshal(r.buf), addr) } } @@ -213,6 +212,8 @@ func (r *remoteFSM) stateServer_onProbe(msg controlMsg[packetProbe]) { func (r *remoteFSM) stateServer_onPingTimer() { conf := r.conf() if time.Since(r.lastSeen) > timeoutInterval && conf.Up { + // Reset trace ID to ensure connection goes up on next SYN. + r.traceID = 0 conf.Up = false r.updateConf(conf) r.logf("Timeout.") @@ -310,7 +311,7 @@ func (r *remoteFSM) stateClientInit_onPing() stateFunc { func (r *remoteFSM) enterClient() stateFunc { conf := r.conf() - r.probes = make(map[uint64]sentProbe, 8) + clear(r.probes) r.traceID = r.NewTraceID() r.stateClient_sendSyn(conf) diff --git a/peer/statusserver.go b/peer/statusserver.go new file mode 100644 index 0000000..12d8428 --- /dev/null +++ b/peer/statusserver.go @@ -0,0 +1,69 @@ +package peer + +import ( + "encoding/json" + "log" + "net" + "net/http" + "net/netip" + "os" +) + +type StatusReport struct { + Network []byte + RelayPeerIP byte + 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{ + Network: g.Network, + Remotes: make([]RemoteStatus, 0, 255), + } + + relay := g.RelayHandler.Load() + if relay != nil { + if relayStatus, ok := relay.Status(); ok { + report.RelayPeerIP = relayStatus.PeerIP + } + } + + 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) + } +}