From e9dffee2de5d1fd0d0ac017d6e6e2d89fd39c1c9 Mon Sep 17 00:00:00 2001 From: jdl Date: Sun, 7 Jun 2026 19:42:04 +0200 Subject: [PATCH] Cleanup - WIP --- cmd/vppn/main.go | 46 +++++++- hub/api/db/generated.go | 19 ++- hub/api/db/sanitize-validate.go | 12 +- hub/api/db/tables.defs | 3 +- hub/api/migrations/2024-11-30-init.sql | 3 +- hub/handlers.go | 13 +-- m/models.go | 7 +- peer/hub_poller.go | 4 +- peer/hubpoller.go | 4 +- peer/init.go | 156 +++++++++++++++++++++++++ peer/new.go | 85 ++++++++++++++ 11 files changed, 310 insertions(+), 42 deletions(-) create mode 100644 peer/init.go create mode 100644 peer/new.go diff --git a/cmd/vppn/main.go b/cmd/vppn/main.go index 6a76e24..ca28299 100644 --- a/cmd/vppn/main.go +++ b/cmd/vppn/main.go @@ -1,8 +1,48 @@ package main -import "log" +import ( + "flag" + "log" + "os" + "path/filepath" + + "vppn/peer" +) func main() { - // TODO: construct peer.App and call Run (audit C1). - log.Fatal("not yet implemented") + log.SetFlags(0) + + name := flag.String("name", "", "network name (required)") + hub := flag.String("hub", "", "hub base URL (required)") + apiKey := flag.String("api-key", "", "API key (required)") + flag.Parse() + + if *name == "" || *hub == "" || *apiKey == "" { + flag.Usage() + os.Exit(1) + } + + statePath := networkStatePath(*name) + + state, err := peer.LoadOrInit(statePath, *hub, *apiKey) + if err != nil { + log.Fatalf("init: %v", err) + } + + app, err := peer.New(state, *hub, *apiKey, *name) + if err != nil { + log.Fatalf("start: %v", err) + } + + if err := app.Run(); err != nil { + log.Fatalf("run: %v", err) + } +} + +func networkStatePath(name string) string { + home, err := os.UserHomeDir() + if err != nil { + return filepath.Join(".vppn", name, "state.json") + } + return filepath.Join(home, ".vppn", name, "state.json") } diff --git a/hub/api/db/generated.go b/hub/api/db/generated.go index c02646d..76d5e96 100644 --- a/hub/api/db/generated.go +++ b/hub/api/db/generated.go @@ -345,14 +345,13 @@ type Peer struct { APIKey string Name string Addr4 []byte - Port4 uint16 Addr6 []byte - Port6 uint16 + Port uint16 Relay bool WGPubKey []byte } -const Peer_SelectQuery = "SELECT NetworkID,PeerIP,Version,APIKey,Name,Addr4,Port4,Addr6,Port6,Relay,WGPubKey FROM peers" +const Peer_SelectQuery = "SELECT NetworkID,PeerIP,Version,APIKey,Name,Addr4,Addr6,Port,Relay,WGPubKey FROM peers" func Peer_Insert( tx TX, @@ -363,7 +362,7 @@ func Peer_Insert( return err } - _, err = tx.Exec("INSERT INTO peers(NetworkID,PeerIP,Version,APIKey,Name,Addr4,Port4,Addr6,Port6,Relay,WGPubKey) VALUES(?,?,?,?,?,?,?,?,?,?,?)", row.NetworkID, row.PeerIP, row.Version, row.APIKey, row.Name, row.Addr4, row.Port4, row.Addr6, row.Port6, row.Relay, row.WGPubKey) + _, err = tx.Exec("INSERT INTO peers(NetworkID,PeerIP,Version,APIKey,Name,Addr4,Addr6,Port,Relay,WGPubKey) VALUES(?,?,?,?,?,?,?,?,?,?)", row.NetworkID, row.PeerIP, row.Version, row.APIKey, row.Name, row.Addr4, row.Addr6, row.Port, row.Relay, row.WGPubKey) return err } @@ -376,7 +375,7 @@ func Peer_Update( return err } - result, err := tx.Exec("UPDATE peers SET Version=?,Name=?,Addr4=?,Port4=?,Addr6=?,Port6=?,Relay=? WHERE NetworkID=? AND PeerIP=?", row.Version, row.Name, row.Addr4, row.Port4, row.Addr6, row.Port6, row.Relay, row.NetworkID, row.PeerIP) + result, err := tx.Exec("UPDATE peers SET Version=?,Name=?,Addr4=?,Addr6=?,Port=?,Relay=? WHERE NetworkID=? AND PeerIP=?", row.Version, row.Name, row.Addr4, row.Addr6, row.Port, row.Relay, row.NetworkID, row.PeerIP) if err != nil { return err } @@ -404,7 +403,7 @@ func Peer_UpdateFull( return err } - result, err := tx.Exec("UPDATE peers SET Version=?,APIKey=?,Name=?,Addr4=?,Port4=?,Addr6=?,Port6=?,Relay=?,WGPubKey=? WHERE NetworkID=? AND PeerIP=?", row.Version, row.APIKey, row.Name, row.Addr4, row.Port4, row.Addr6, row.Port6, row.Relay, row.WGPubKey, row.NetworkID, row.PeerIP) + result, err := tx.Exec("UPDATE peers SET Version=?,APIKey=?,Name=?,Addr4=?,Addr6=?,Port=?,Relay=?,WGPubKey=? WHERE NetworkID=? AND PeerIP=?", row.Version, row.APIKey, row.Name, row.Addr4, row.Addr6, row.Port, row.Relay, row.WGPubKey, row.NetworkID, row.PeerIP) if err != nil { return err } @@ -456,8 +455,8 @@ func Peer_Get( err error, ) { row = &Peer{} - r := tx.QueryRow("SELECT NetworkID,PeerIP,Version,APIKey,Name,Addr4,Port4,Addr6,Port6,Relay,WGPubKey FROM peers WHERE NetworkID=? AND PeerIP=?", NetworkID, PeerIP) - if err = r.Scan(&row.NetworkID, &row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.Addr4, &row.Port4, &row.Addr6, &row.Port6, &row.Relay, &row.WGPubKey); err != nil { + r := tx.QueryRow("SELECT NetworkID,PeerIP,Version,APIKey,Name,Addr4,Addr6,Port,Relay,WGPubKey FROM peers WHERE NetworkID=? AND PeerIP=?", NetworkID, PeerIP) + if err = r.Scan(&row.NetworkID, &row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.Addr4, &row.Addr6, &row.Port, &row.Relay, &row.WGPubKey); err != nil { row = nil } return @@ -473,7 +472,7 @@ func Peer_GetWhere( ) { row = &Peer{} r := tx.QueryRow(query, args...) - if err = r.Scan(&row.NetworkID, &row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.Addr4, &row.Port4, &row.Addr6, &row.Port6, &row.Relay, &row.WGPubKey); err != nil { + if err = r.Scan(&row.NetworkID, &row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.Addr4, &row.Addr6, &row.Port, &row.Relay, &row.WGPubKey); err != nil { row = nil } return @@ -495,7 +494,7 @@ func Peer_Iterate( defer rows.Close() for rows.Next() { row := &Peer{} - err := rows.Scan(&row.NetworkID, &row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.Addr4, &row.Port4, &row.Addr6, &row.Port6, &row.Relay, &row.WGPubKey) + err := rows.Scan(&row.NetworkID, &row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.Addr4, &row.Addr6, &row.Port, &row.Relay, &row.WGPubKey) if !yield(row, err) { return } diff --git a/hub/api/db/sanitize-validate.go b/hub/api/db/sanitize-validate.go index f42dcfa..c08f316 100644 --- a/hub/api/db/sanitize-validate.go +++ b/hub/api/db/sanitize-validate.go @@ -69,11 +69,8 @@ func Peer_Sanitize(p *Peer) { p.Addr6 = addr.AsSlice() } } - if p.Port4 == 0 { - p.Port4 = 456 - } - if len(p.Addr6) != 0 && p.Port6 == 0 { - p.Port6 = 456 + if p.Port == 0 { + p.Port = 51820 } } @@ -87,11 +84,8 @@ func Peer_Validate(p *Peer) error { if _, ok := netip.AddrFromSlice(p.Addr6); !ok { return ErrInvalidIP } - if p.Port6 == 0 { - return ErrInvalidPort - } } - if p.Port4 == 0 { + if p.Port == 0 { return ErrInvalidPort } diff --git a/hub/api/db/tables.defs b/hub/api/db/tables.defs index 2ec6527..15e134a 100644 --- a/hub/api/db/tables.defs +++ b/hub/api/db/tables.defs @@ -16,9 +16,8 @@ TABLE peers OF Peer ( APIKey string NoUpdate, Name string, Addr4 []byte, - Port4 uint16, Addr6 []byte, - Port6 uint16, + Port uint16, Relay bool, WGPubKey []byte NoUpdate ); diff --git a/hub/api/migrations/2024-11-30-init.sql b/hub/api/migrations/2024-11-30-init.sql index b5a337f..9a0705b 100644 --- a/hub/api/migrations/2024-11-30-init.sql +++ b/hub/api/migrations/2024-11-30-init.sql @@ -16,9 +16,8 @@ CREATE TABLE peers ( APIKey TEXT NOT NULL UNIQUE, -- Peer's secret API key. Name TEXT NOT NULL UNIQUE, -- For humans. Addr4 BLOB NOT NULL, - Port4 INTEGER NOT NULL, Addr6 BLOB NOT NULL, - Port6 INTEGER NOT NULL, + Port INTEGER NOT NULL, Relay INTEGER NOT NULL DEFAULT 0, -- Boolean if peer will forward packets. WGPubKey BLOB NOT NULL, PRIMARY KEY(NetworkID, PeerIP) diff --git a/hub/handlers.go b/hub/handlers.go index bb7fcb1..f189412 100644 --- a/hub/handlers.go +++ b/hub/handlers.go @@ -150,9 +150,8 @@ func (a *App) _adminPeerCreateSubmit(s *api.Session, w http.ResponseWriter, r *h Scan("IP", &p.PeerIP). Scan("Name", &p.Name). Scan("Addr4", &addr4Str). - Scan("Port4", &p.Port4). Scan("Addr6", &addr6Str). - Scan("Port6", &p.Port6). + Scan("Port", &p.Port). Scan("Relay", &p.Relay). Error() if err != nil { @@ -209,9 +208,8 @@ func (a *App) _adminPeerEditSubmit(s *api.Session, w http.ResponseWriter, r *htt err = webutil.NewFormScanner(r.Form). Scan("Name", &peer.Name). Scan("Addr4", &addr4Str). - Scan("Port4", &peer.Port4). Scan("Addr6", &addr6Str). - Scan("Port6", &peer.Port6). + Scan("Port", &peer.Port). Scan("Relay", &peer.Relay). Error() if err != nil { @@ -358,10 +356,9 @@ func (a *App) peersArray(networkID int64) (peers [256]*m.Peer, err error) { PeerIP: p.PeerIP, Version: p.Version, Name: p.Name, - Addr4: p.Addr4, - Port4: p.Port4, - Addr6: p.Addr6, - Port6: p.Port6, + Addr4: p.Addr4, + Addr6: p.Addr6, + Port: p.Port, Relay: p.Relay, WGPubKey: p.WGPubKey, } diff --git a/m/models.go b/m/models.go index 2a79d10..36c6ff5 100644 --- a/m/models.go +++ b/m/models.go @@ -15,10 +15,9 @@ type Peer struct { PeerIP byte Version int64 Name string - Addr4 []byte - Port4 uint16 - Addr6 []byte - Port6 uint16 + Addr4 []byte + Addr6 []byte + Port uint16 Relay bool WGPubKey []byte } diff --git a/peer/hub_poller.go b/peer/hub_poller.go index 3524930..c978889 100644 --- a/peer/hub_poller.go +++ b/peer/hub_poller.go @@ -130,12 +130,12 @@ func hubPeerFrom(pubKey wgtypes.Key, vpnIP netip.Addr, p *m.Peer) HubPeer { var ep4, ep6 netip.AddrPort if len(p.Addr4) > 0 { if addr, ok := netip.AddrFromSlice(p.Addr4); ok { - ep4 = netip.AddrPortFrom(addr.Unmap(), p.Port4) + ep4 = netip.AddrPortFrom(addr.Unmap(), p.Port) } } if len(p.Addr6) > 0 { if addr, ok := netip.AddrFromSlice(p.Addr6); ok { - ep6 = netip.AddrPortFrom(addr, p.Port6) + ep6 = netip.AddrPortFrom(addr, p.Port) } } return HubPeer{ diff --git a/peer/hubpoller.go b/peer/hubpoller.go index b5bcc07..08aeb46 100644 --- a/peer/hubpoller.go +++ b/peer/hubpoller.go @@ -116,7 +116,7 @@ func (hp *HubPoller) applyPeerConfig(peer *m.Peer) { if peer == nil || len(peer.WGPubKey) != wgtypes.KeyLen { return } - if len(peer.Addr4) == 0 || peer.Port4 == 0 { + if len(peer.Addr4) == 0 || peer.Port == 0 { return } @@ -131,7 +131,7 @@ func (hp *HubPoller) applyPeerConfig(peer *m.Peer) { hp.logf("Invalid public IP for peer %d", peer.PeerIP) return } - endpoint := netip.AddrPortFrom(ip.Unmap(), peer.Port4) + endpoint := netip.AddrPortFrom(ip.Unmap(), peer.Port) if peer.Relay { if err := applyBaseConfig(hp.WGClient, hp.WGDevName, pubKey, endpoint, hp.Network); err != nil { diff --git a/peer/init.go b/peer/init.go new file mode 100644 index 0000000..8e8010c --- /dev/null +++ b/peer/init.go @@ -0,0 +1,156 @@ +package peer + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "net/netip" + "os" + "path/filepath" + + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + + "vppn/m" +) + +// LocalState is the persisted identity for this peer, written on first run and +// loaded on every subsequent run. +type LocalState struct { + PrivKey wgtypes.Key + VPNIP netip.Addr + VPNNet netip.Prefix + WGPort uint16 + IsRelay bool + IsPublic bool +} + +// localStateJSON is the on-disk representation. +type localStateJSON struct { + PrivKey string `json:"priv_key"` // standard base64 + VPNIP netip.Addr `json:"vpn_ip"` + VPNNet netip.Prefix `json:"vpn_net"` + WGPort uint16 `json:"wg_port"` + IsRelay bool `json:"is_relay"` + IsPublic bool `json:"is_public"` +} + +// LoadOrInit loads LocalState from path, or registers with the hub and creates +// the file if it doesn't exist. +func LoadOrInit(statePath, hubURL, apiKey string) (LocalState, error) { + if data, err := os.ReadFile(statePath); err == nil { + return parseLocalState(data) + } + + privKey, err := wgtypes.GeneratePrivateKey() + if err != nil { + return LocalState{}, fmt.Errorf("generate key: %w", err) + } + + state, err := initFromHub(hubURL, apiKey, privKey) + if err != nil { + return LocalState{}, err + } + + if err := saveLocalState(statePath, state); err != nil { + return LocalState{}, fmt.Errorf("save state: %w", err) + } + return state, nil +} + +func initFromHub(hubURL, apiKey string, privKey wgtypes.Key) (LocalState, error) { + pubKey := privKey.PublicKey() + body, _ := json.Marshal(m.PeerInitArgs{WGPubKey: pubKey[:]}) + + req, err := http.NewRequest(http.MethodPost, hubURL+"/peer/init/", bytes.NewReader(body)) + if err != nil { + return LocalState{}, err + } + req.SetBasicAuth("", apiKey) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return LocalState{}, fmt.Errorf("hub init: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return LocalState{}, fmt.Errorf("hub init: HTTP %d", resp.StatusCode) + } + + var r m.PeerInitResp + if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { + return LocalState{}, fmt.Errorf("hub init decode: %w", err) + } + + if len(r.Network) != 4 { + return LocalState{}, fmt.Errorf("hub init: invalid network %v", r.Network) + } + + netAddr := netip.AddrFrom4([4]byte(r.Network)) + octets := netAddr.As4() + octets[3] = r.PeerIP + vpnIP := netip.AddrFrom4(octets) + vpnNet := netip.PrefixFrom(netAddr, 24) + + var isRelay, isPublic bool + var wgPort uint16 + if self := r.NetworkState.Peers[r.PeerIP]; self != nil { + isRelay = self.Relay + isPublic = len(self.Addr4) > 0 || len(self.Addr6) > 0 + wgPort = self.Port + } + + return LocalState{ + PrivKey: privKey, + VPNIP: vpnIP, + VPNNet: vpnNet, + WGPort: wgPort, + IsRelay: isRelay, + IsPublic: isPublic, + }, nil +} + +func parseLocalState(data []byte) (LocalState, error) { + var j localStateJSON + if err := json.Unmarshal(data, &j); err != nil { + return LocalState{}, fmt.Errorf("parse state: %w", err) + } + keyBytes, err := base64.StdEncoding.DecodeString(j.PrivKey) + if err != nil { + return LocalState{}, fmt.Errorf("decode key: %w", err) + } + key, err := wgtypes.NewKey(keyBytes) + if err != nil { + return LocalState{}, fmt.Errorf("invalid key: %w", err) + } + return LocalState{ + PrivKey: key, + VPNIP: j.VPNIP, + VPNNet: j.VPNNet, + WGPort: j.WGPort, + IsRelay: j.IsRelay, + IsPublic: j.IsPublic, + }, nil +} + +func saveLocalState(path string, s LocalState) error { + j := localStateJSON{ + PrivKey: base64.StdEncoding.EncodeToString(s.PrivKey[:]), + VPNIP: s.VPNIP, + VPNNet: s.VPNNet, + WGPort: s.WGPort, + IsRelay: s.IsRelay, + IsPublic: s.IsPublic, + } + data, err := json.MarshalIndent(j, "", " ") + if err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + return err + } + return os.WriteFile(path, data, 0600) +} diff --git a/peer/new.go b/peer/new.go new file mode 100644 index 0000000..144ec94 --- /dev/null +++ b/peer/new.go @@ -0,0 +1,85 @@ +package peer + +import ( + "fmt" + "net/netip" + + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + + "vppn/peer/wginterface" +) + +// New constructs an App, creates the WireGuard interface, and starts the +// background goroutines (hub poller, multicast, control conn reader). +// The caller should invoke Run() to start the event loop. +func New( + state LocalState, + hubURL, apiKey string, + ifaceName string, +) (*App, error) { + a4 := state.VPNIP.As4() + if err := wginterface.Create(ifaceName, a4[:], 24); err != nil { + return nil, fmt.Errorf("create WG interface: %w", err) + } + + dev, err := wginterface.Open(ifaceName) + if err != nil { + return nil, fmt.Errorf("open WG device: %w", err) + } + + if err := dev.Configure(state.PrivKey, int(state.WGPort)); err != nil { + return nil, fmt.Errorf("configure WG device: %w", err) + } + + if state.IsRelay { + if err := dev.EnableForwarding(); err != nil { + return nil, fmt.Errorf("enable forwarding: %w", err) + } + } + + cc, err := newUDPControlConn(state.VPNIP, ControlPort) + if err != nil { + return nil, fmt.Errorf("control conn: %w", err) + } + + pingCh := make(chan PingEvent) + hubAddCh := make(chan HubPeer) + hubRemoveCh := make(chan wgtypes.Key) + multicastCh := make(chan MulticastEvent) + + poller, err := NewHubPoller( + state.VPNIP, state.VPNNet, + hubURL, apiKey, + hubAddCh, hubRemoveCh, + ) + if err != nil { + return nil, fmt.Errorf("hub poller: %w", err) + } + + go cc.run(pingCh) + go poller.Run() + go RunMCWriter(state.VPNIP, state.PrivKey.PublicKey(), state.WGPort) + if !state.IsPublic { + go RunMCReader(state.VPNNet, state.VPNIP, multicastCh) + } + + return &App{ + vpnIP: state.VPNIP, + vpnNet: state.VPNNet, + privKey: state.PrivKey, + pubKey: state.PrivKey.PublicKey(), + isRelay: state.IsRelay, + isPublic: state.IsPublic, + + dev: dev, + controlConn: cc, + + peersByKey: make(map[wgtypes.Key]*Peer), + peersByIP: make(map[netip.Addr]*Peer), + + hubAddCh: hubAddCh, + hubRemoveCh: hubRemoveCh, + pingCh: pingCh, + multicastCh: multicastCh, + }, nil +}