Cleanup - WIP

This commit is contained in:
jdl
2026-06-07 19:42:04 +02:00
parent b8344a20b9
commit e9dffee2de
11 changed files with 310 additions and 42 deletions

View File

@@ -1,8 +1,48 @@
package main package main
import "log" import (
"flag"
"log"
"os"
"path/filepath"
"vppn/peer"
)
func main() { func main() {
// TODO: construct peer.App and call Run (audit C1). log.SetFlags(0)
log.Fatal("not yet implemented")
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")
} }

View File

@@ -345,14 +345,13 @@ type Peer struct {
APIKey string APIKey string
Name string Name string
Addr4 []byte Addr4 []byte
Port4 uint16
Addr6 []byte Addr6 []byte
Port6 uint16 Port uint16
Relay bool Relay bool
WGPubKey []byte 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( func Peer_Insert(
tx TX, tx TX,
@@ -363,7 +362,7 @@ func Peer_Insert(
return err 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 return err
} }
@@ -376,7 +375,7 @@ func Peer_Update(
return err 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 { if err != nil {
return err return err
} }
@@ -404,7 +403,7 @@ func Peer_UpdateFull(
return err 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 { if err != nil {
return err return err
} }
@@ -456,8 +455,8 @@ func Peer_Get(
err error, err error,
) { ) {
row = &Peer{} 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) 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.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 row = nil
} }
return return
@@ -473,7 +472,7 @@ func Peer_GetWhere(
) { ) {
row = &Peer{} row = &Peer{}
r := tx.QueryRow(query, args...) 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 row = nil
} }
return return
@@ -495,7 +494,7 @@ func Peer_Iterate(
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
row := &Peer{} 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) { if !yield(row, err) {
return return
} }

View File

@@ -69,11 +69,8 @@ func Peer_Sanitize(p *Peer) {
p.Addr6 = addr.AsSlice() p.Addr6 = addr.AsSlice()
} }
} }
if p.Port4 == 0 { if p.Port == 0 {
p.Port4 = 456 p.Port = 51820
}
if len(p.Addr6) != 0 && p.Port6 == 0 {
p.Port6 = 456
} }
} }
@@ -87,11 +84,8 @@ func Peer_Validate(p *Peer) error {
if _, ok := netip.AddrFromSlice(p.Addr6); !ok { if _, ok := netip.AddrFromSlice(p.Addr6); !ok {
return ErrInvalidIP return ErrInvalidIP
} }
if p.Port6 == 0 {
return ErrInvalidPort
}
} }
if p.Port4 == 0 { if p.Port == 0 {
return ErrInvalidPort return ErrInvalidPort
} }

View File

@@ -16,9 +16,8 @@ TABLE peers OF Peer (
APIKey string NoUpdate, APIKey string NoUpdate,
Name string, Name string,
Addr4 []byte, Addr4 []byte,
Port4 uint16,
Addr6 []byte, Addr6 []byte,
Port6 uint16, Port uint16,
Relay bool, Relay bool,
WGPubKey []byte NoUpdate WGPubKey []byte NoUpdate
); );

View File

@@ -16,9 +16,8 @@ CREATE TABLE peers (
APIKey TEXT NOT NULL UNIQUE, -- Peer's secret API key. APIKey TEXT NOT NULL UNIQUE, -- Peer's secret API key.
Name TEXT NOT NULL UNIQUE, -- For humans. Name TEXT NOT NULL UNIQUE, -- For humans.
Addr4 BLOB NOT NULL, Addr4 BLOB NOT NULL,
Port4 INTEGER NOT NULL,
Addr6 BLOB 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. Relay INTEGER NOT NULL DEFAULT 0, -- Boolean if peer will forward packets.
WGPubKey BLOB NOT NULL, WGPubKey BLOB NOT NULL,
PRIMARY KEY(NetworkID, PeerIP) PRIMARY KEY(NetworkID, PeerIP)

View File

@@ -150,9 +150,8 @@ func (a *App) _adminPeerCreateSubmit(s *api.Session, w http.ResponseWriter, r *h
Scan("IP", &p.PeerIP). Scan("IP", &p.PeerIP).
Scan("Name", &p.Name). Scan("Name", &p.Name).
Scan("Addr4", &addr4Str). Scan("Addr4", &addr4Str).
Scan("Port4", &p.Port4).
Scan("Addr6", &addr6Str). Scan("Addr6", &addr6Str).
Scan("Port6", &p.Port6). Scan("Port", &p.Port).
Scan("Relay", &p.Relay). Scan("Relay", &p.Relay).
Error() Error()
if err != nil { if err != nil {
@@ -209,9 +208,8 @@ func (a *App) _adminPeerEditSubmit(s *api.Session, w http.ResponseWriter, r *htt
err = webutil.NewFormScanner(r.Form). err = webutil.NewFormScanner(r.Form).
Scan("Name", &peer.Name). Scan("Name", &peer.Name).
Scan("Addr4", &addr4Str). Scan("Addr4", &addr4Str).
Scan("Port4", &peer.Port4).
Scan("Addr6", &addr6Str). Scan("Addr6", &addr6Str).
Scan("Port6", &peer.Port6). Scan("Port", &peer.Port).
Scan("Relay", &peer.Relay). Scan("Relay", &peer.Relay).
Error() Error()
if err != nil { if err != nil {
@@ -358,10 +356,9 @@ func (a *App) peersArray(networkID int64) (peers [256]*m.Peer, err error) {
PeerIP: p.PeerIP, PeerIP: p.PeerIP,
Version: p.Version, Version: p.Version,
Name: p.Name, Name: p.Name,
Addr4: p.Addr4, Addr4: p.Addr4,
Port4: p.Port4, Addr6: p.Addr6,
Addr6: p.Addr6, Port: p.Port,
Port6: p.Port6,
Relay: p.Relay, Relay: p.Relay,
WGPubKey: p.WGPubKey, WGPubKey: p.WGPubKey,
} }

View File

@@ -15,10 +15,9 @@ type Peer struct {
PeerIP byte PeerIP byte
Version int64 Version int64
Name string Name string
Addr4 []byte Addr4 []byte
Port4 uint16 Addr6 []byte
Addr6 []byte Port uint16
Port6 uint16
Relay bool Relay bool
WGPubKey []byte WGPubKey []byte
} }

View File

@@ -130,12 +130,12 @@ func hubPeerFrom(pubKey wgtypes.Key, vpnIP netip.Addr, p *m.Peer) HubPeer {
var ep4, ep6 netip.AddrPort var ep4, ep6 netip.AddrPort
if len(p.Addr4) > 0 { if len(p.Addr4) > 0 {
if addr, ok := netip.AddrFromSlice(p.Addr4); ok { 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 len(p.Addr6) > 0 {
if addr, ok := netip.AddrFromSlice(p.Addr6); ok { if addr, ok := netip.AddrFromSlice(p.Addr6); ok {
ep6 = netip.AddrPortFrom(addr, p.Port6) ep6 = netip.AddrPortFrom(addr, p.Port)
} }
} }
return HubPeer{ return HubPeer{

View File

@@ -116,7 +116,7 @@ func (hp *HubPoller) applyPeerConfig(peer *m.Peer) {
if peer == nil || len(peer.WGPubKey) != wgtypes.KeyLen { if peer == nil || len(peer.WGPubKey) != wgtypes.KeyLen {
return return
} }
if len(peer.Addr4) == 0 || peer.Port4 == 0 { if len(peer.Addr4) == 0 || peer.Port == 0 {
return return
} }
@@ -131,7 +131,7 @@ func (hp *HubPoller) applyPeerConfig(peer *m.Peer) {
hp.logf("Invalid public IP for peer %d", peer.PeerIP) hp.logf("Invalid public IP for peer %d", peer.PeerIP)
return return
} }
endpoint := netip.AddrPortFrom(ip.Unmap(), peer.Port4) endpoint := netip.AddrPortFrom(ip.Unmap(), peer.Port)
if peer.Relay { if peer.Relay {
if err := applyBaseConfig(hp.WGClient, hp.WGDevName, pubKey, endpoint, hp.Network); err != nil { if err := applyBaseConfig(hp.WGClient, hp.WGDevName, pubKey, endpoint, hp.Network); err != nil {

156
peer/init.go Normal file
View File

@@ -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)
}

85
peer/new.go Normal file
View File

@@ -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
}