Audit changes.

This commit is contained in:
jdl
2026-06-11 16:46:38 +02:00
parent cd2ec23ccb
commit 82a6d47bc9
17 changed files with 133 additions and 71 deletions

View File

@@ -65,12 +65,8 @@ func (a *API) ensurePassword() error {
return db.Config_Insert(a.db, conf) return db.Config_Insert(a.db, conf)
} }
func (a *API) Config_Get() *Config { func (a *API) Config_Get() (*Config, error) {
conf, err := db.Config_Get(a.db, 1) return db.Config_Get(a.db, 1)
if err != nil {
panic(err)
}
return conf
} }
func (a *API) Config_Update(conf *Config) error { func (a *API) Config_Update(conf *Config) error {
@@ -116,7 +112,10 @@ func (a *API) session_Create() *Session {
} }
func (a *API) Session_SignIn(s *Session, pwd string) error { func (a *API) Session_SignIn(s *Session, pwd string) error {
conf := a.Config_Get() conf, err := a.Config_Get()
if err != nil {
return err
}
if err := bcrypt.CompareHashAndPassword(conf.Password, []byte(pwd)); err != nil { if err := bcrypt.CompareHashAndPassword(conf.Password, []byte(pwd)); err != nil {
return ErrNotAuthorized return ErrNotAuthorized
} }

View File

@@ -8,6 +8,7 @@ import (
var ( var (
ErrInvalidIP = errors.New("invalid IP") ErrInvalidIP = errors.New("invalid IP")
ErrInvalidPeerIP = errors.New("invalid peer IP")
ErrNonPrivateIP = errors.New("non-private IP") ErrNonPrivateIP = errors.New("non-private IP")
ErrInvalidPort = errors.New("invalid port") ErrInvalidPort = errors.New("invalid port")
ErrInvalidNetName = errors.New("invalid network name") ErrInvalidNetName = errors.New("invalid network name")
@@ -67,7 +68,8 @@ func Peer_Sanitize(p *Peer) {
p.Name = strings.TrimSpace(p.Name) p.Name = strings.TrimSpace(p.Name)
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 {
p.Addr4 = addr.AsSlice() // Unmap so an IPv4-mapped form is stored canonically as 4 bytes.
p.Addr4 = addr.Unmap().AsSlice()
} }
} }
if len(p.Addr6) != 0 { if len(p.Addr6) != 0 {
@@ -81,13 +83,18 @@ func Peer_Sanitize(p *Peer) {
} }
func Peer_Validate(p *Peer) error { func Peer_Validate(p *Peer) error {
if p.PeerIP < 1 || p.PeerIP > 254 {
return ErrInvalidPeerIP
}
if len(p.Addr4) > 0 { if len(p.Addr4) > 0 {
if _, ok := netip.AddrFromSlice(p.Addr4); !ok { // Must be a genuine IPv4 address (reject an IPv6 in the v4 field).
if addr, ok := netip.AddrFromSlice(p.Addr4); !ok || !addr.Is4() {
return ErrInvalidIP return ErrInvalidIP
} }
} }
if len(p.Addr6) > 0 { if len(p.Addr6) > 0 {
if _, ok := netip.AddrFromSlice(p.Addr6); !ok { // Must be a genuine IPv6 address (reject IPv4 / IPv4-mapped in the v6 field).
if addr, ok := netip.AddrFromSlice(p.Addr6); !ok || !addr.Is6() || addr.Is4In6() {
return ErrInvalidIP return ErrInvalidIP
} }
} }

View File

@@ -7,7 +7,6 @@ import (
var ( var (
ErrNotAuthorized = errors.New("not authorized") ErrNotAuthorized = errors.New("not authorized")
ErrNoIPAvailable = errors.New("no IP address available")
ErrInvalidIP = db.ErrInvalidIP ErrInvalidIP = db.ErrInvalidIP
ErrInvalidPort = db.ErrInvalidPort ErrInvalidPort = db.ErrInvalidPort
) )

View File

@@ -14,12 +14,13 @@ CREATE TABLE peers (
PeerIP INTEGER NOT NULL, -- Final byte of IP. PeerIP INTEGER NOT NULL, -- Final byte of IP.
Version INTEGER NOT NULL, -- Changes when updated. Version INTEGER NOT NULL, -- Changes when updated.
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, -- For humans.
Addr4 BLOB NOT NULL, Addr4 BLOB NOT NULL,
Addr6 BLOB NOT NULL, Addr6 BLOB NOT NULL,
Port 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,
SignPubKey BLOB NOT NULL, SignPubKey BLOB NOT NULL,
UNIQUE(NetworkID, Name),
PRIMARY KEY(NetworkID, PeerIP) PRIMARY KEY(NetworkID, PeerIP)
) WITHOUT ROWID; ) WITHOUT ROWID;

View File

@@ -2,7 +2,6 @@ package hub
import ( import (
"net/http" "net/http"
"time"
) )
func (a *App) getCookie(r *http.Request, name string) string { func (a *App) getCookie(r *http.Request, name string) string {
@@ -26,9 +25,12 @@ func (a *App) setCookie(w http.ResponseWriter, name, value string) {
func (a *App) deleteCookie(w http.ResponseWriter, name string) { func (a *App) deleteCookie(w http.ResponseWriter, name string) {
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
Name: name, Name: name,
Value: "", Value: "",
Path: "/", Path: "/",
Expires: time.Unix(0, 0), Secure: !a.insecure,
SameSite: http.SameSiteStrictMode,
HttpOnly: true,
MaxAge: -1, // delete now
}) })
} }

View File

@@ -260,13 +260,17 @@ func (a *App) _adminPasswordEdit(s *api.Session, w http.ResponseWriter, r *http.
func (a *App) _adminPasswordSubmit(s *api.Session, w http.ResponseWriter, r *http.Request) error { func (a *App) _adminPasswordSubmit(s *api.Session, w http.ResponseWriter, r *http.Request) error {
var ( var (
conf = a.api.Config_Get()
curPwd string curPwd string
newPwd string newPwd string
newPwd2 string newPwd2 string
) )
err := webutil.NewFormScanner(r.Form). conf, err := a.api.Config_Get()
if err != nil {
return err
}
err = webutil.NewFormScanner(r.Form).
Scan("CurrentPassword", &curPwd). Scan("CurrentPassword", &curPwd).
Scan("NewPassword", &newPwd). Scan("NewPassword", &newPwd).
Scan("NewPassword2", &newPwd2). Scan("NewPassword2", &newPwd2).
@@ -337,7 +341,7 @@ func (a *App) _peerInit(peer *api.Peer, w http.ResponseWriter, r *http.Request)
LocalDomain: net.LocalDomain, LocalDomain: net.LocalDomain,
} }
resp.NetworkState.Peers, err = a.peersArray(net.NetworkID) resp.NetworkState.Peers, err = a.peersList(net.NetworkID)
if err != nil { if err != nil {
return err return err
} }
@@ -346,22 +350,24 @@ func (a *App) _peerInit(peer *api.Peer, w http.ResponseWriter, r *http.Request)
} }
func (a *App) _peerFetchState(peer *api.Peer, w http.ResponseWriter, r *http.Request) error { func (a *App) _peerFetchState(peer *api.Peer, w http.ResponseWriter, r *http.Request) error {
peers, err := a.peersArray(peer.NetworkID) peers, err := a.peersList(peer.NetworkID)
if err != nil { if err != nil {
return err return err
} }
return a.sendJSON(w, m.NetworkState{Peers: peers}) return a.sendJSON(w, m.NetworkState{Peers: peers})
} }
func (a *App) peersArray(networkID int64) (peers [256]*m.Peer, err error) { func (a *App) peersList(networkID int64) (peers []m.Peer, err error) {
l, err := a.api.Peer_List(networkID) l, err := a.api.Peer_List(networkID)
if err != nil { if err != nil {
return peers, err return nil, err
} }
peers = make([]m.Peer, 0, len(l))
for _, p := range l { for _, p := range l {
if len(p.WGPubKey) != 0 { if len(p.WGPubKey) != 0 {
peers[p.PeerIP] = &m.Peer{ peers = append(peers, m.Peer{
PeerIP: p.PeerIP, PeerIP: p.PeerIP,
Version: p.Version, Version: p.Version,
Name: p.Name, Name: p.Name,
@@ -371,9 +377,9 @@ func (a *App) peersArray(networkID int64) (peers [256]*m.Peer, err error) {
Relay: p.Relay, Relay: p.Relay,
WGPubKey: p.WGPubKey, WGPubKey: p.WGPubKey,
SignPubKey: p.SignPubKey, SignPubKey: p.SignPubKey,
} })
} }
} }
return return peers, nil
} }

View File

@@ -26,5 +26,5 @@ type Peer struct {
} }
type NetworkState struct { type NetworkState struct {
Peers [256]*Peer Peers []Peer
} }

View File

@@ -109,6 +109,5 @@ func (a *App) Run() error {
} }
func (a *App) onShutdown() error { func (a *App) onShutdown() error {
// TODO: removeHosts() ?
return wginterface.Delete(a.dev.Name()) return wginterface.Delete(a.dev.Name())
} }

View File

@@ -59,3 +59,7 @@ func (c *udpControlConn) run(ch chan<- PingEvent) {
ch <- PingEvent{srcVPNIP: srcIP.Unmap(), ping: ping} ch <- PingEvent{srcVPNIP: srcIP.Unmap(), ping: ping}
} }
} }
func (c *udpControlConn) Close() error {
return c.conn.Close()
}

View File

@@ -11,7 +11,8 @@ import (
) )
// devRetry calls fn up to 6 times with exponential backoff, retrying on EBUSY // devRetry calls fn up to 6 times with exponential backoff, retrying on EBUSY
// (transient netlink contention during WireGuard handshake/rekey). Fatal on any other error. // (transient netlink contention during WireGuard handshake/rekey). Fatal on
// any other error.
func devRetry(vpnIP netip.Addr, op string, fn func() error) { func devRetry(vpnIP netip.Addr, op string, fn func() error) {
const attempts = 6 const attempts = 6
timeout := 10 * time.Millisecond timeout := 10 * time.Millisecond

View File

@@ -55,9 +55,9 @@ func updateHosts(hostsPath, localDomain string, peers map[netip.Addr]*Peer) erro
after := "" after := ""
if idxBegin := strings.Index(data, begin); idxBegin != -1 { if idxBegin := strings.Index(data, begin); idxBegin != -1 {
idxEnd := strings.Index(data, end) idxEnd := strings.Index(data[idxBegin:], end)
if idxEnd != -1 { if idxEnd != -1 {
after = strings.TrimSpace(data[idxEnd+len(end):]) after = strings.TrimSpace(data[idxBegin+idxEnd+len(end):])
} }
before = strings.TrimSpace(data[:idxBegin]) before = strings.TrimSpace(data[:idxBegin])
} }

View File

@@ -119,7 +119,7 @@ func (hp *HubPoller) apply(state m.NetworkState) (changed bool) {
netAddr := hp.vpnNet.Addr().As4() netAddr := hp.vpnNet.Addr().As4()
for _, p := range state.Peers { for _, p := range state.Peers {
if p == nil || len(p.WGPubKey) != wgtypes.KeyLen || len(p.SignPubKey) != 32 { if len(p.WGPubKey) != wgtypes.KeyLen || len(p.SignPubKey) != 32 {
continue continue
} }
@@ -156,16 +156,17 @@ func (hp *HubPoller) apply(state m.NetworkState) (changed bool) {
return changed return changed
} }
func hubPeerFrom(pubKey wgtypes.Key, vpnIP netip.Addr, p *m.Peer) HubPeer { 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.Port) 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.Port) ep6 = netip.AddrPortFrom(addr.Unmap(), p.Port)
} }
} }
var signPubKey [32]byte var signPubKey [32]byte

View File

@@ -24,14 +24,12 @@ func testPoller(t *testing.T) (*HubPoller, chan HubPeer, chan wgtypes.Key) {
} }
func stateWith(key wgtypes.Key, peerIP byte, version int64) m.NetworkState { func stateWith(key wgtypes.Key, peerIP byte, version int64) m.NetworkState {
var s m.NetworkState return m.NetworkState{Peers: []m.Peer{{
s.Peers[peerIP] = &m.Peer{
PeerIP: peerIP, PeerIP: peerIP,
Version: version, Version: version,
WGPubKey: key[:], WGPubKey: key[:],
SignPubKey: make([]byte, 32), SignPubKey: make([]byte, 32),
} }}}
return s
} }
func TestApply_EmitsAddsAndReportsChange(t *testing.T) { func TestApply_EmitsAddsAndReportsChange(t *testing.T) {

View File

@@ -115,13 +115,34 @@ func initFromHub(hubURL, apiKey string, privKey wgtypes.Key) (LocalState, error)
vpnIP := netip.AddrFrom4(octets) vpnIP := netip.AddrFrom4(octets)
vpnNet := netip.PrefixFrom(netAddr, 24) vpnNet := netip.PrefixFrom(netAddr, 24)
var isRelay, isPublic bool var self *m.Peer
var wgPort uint16 for i := range r.NetworkState.Peers {
if self := r.NetworkState.Peers[r.PeerIP]; self != nil { if r.NetworkState.Peers[i].PeerIP == r.PeerIP {
isRelay = self.Relay self = &r.NetworkState.Peers[i]
isPublic = len(self.Addr4) > 0 || len(self.Addr6) > 0 break
wgPort = self.Port }
} }
if self == nil {
return LocalState{}, fmt.Errorf("hub init: no peer for own IP: %d", r.PeerIP)
}
var isRelay, public bool
var wgPort uint16
var ep4, ep6 netip.AddrPort
if len(self.Addr4) > 0 {
if addr, ok := netip.AddrFromSlice(self.Addr4); ok {
ep4 = netip.AddrPortFrom(addr.Unmap(), self.Port)
}
}
if len(self.Addr6) > 0 {
if addr, ok := netip.AddrFromSlice(self.Addr6); ok {
ep6 = netip.AddrPortFrom(addr.Unmap(), self.Port)
}
}
public = ep4.IsValid() || ep6.IsValid()
isRelay = self.Relay && public
wgPort = self.Port
return LocalState{ return LocalState{
PrivKey: privKey, PrivKey: privKey,
@@ -130,7 +151,7 @@ func initFromHub(hubURL, apiKey string, privKey wgtypes.Key) (LocalState, error)
VPNNet: vpnNet, VPNNet: vpnNet,
WGPort: wgPort, WGPort: wgPort,
IsRelay: isRelay, IsRelay: isRelay,
IsPublic: isPublic, IsPublic: public,
LocalDomain: r.LocalDomain, LocalDomain: r.LocalDomain,
}, nil }, nil
} }

View File

@@ -26,11 +26,19 @@ var mcAddr = net.UDPAddrFromAddrPort(netip.AddrPortFrom(
// RunMCWriter broadcasts a signed beacon on the local multicast group every // RunMCWriter broadcasts a signed beacon on the local multicast group every
// mcBroadcastInterval so that LAN peers can discover our WireGuard endpoint. // mcBroadcastInterval so that LAN peers can discover our WireGuard endpoint.
func RunMCWriter(selfVPNIP netip.Addr, pubKey wgtypes.Key, wgPort uint16, signKey *[64]byte) { func RunMCWriter(selfVPNIP netip.Addr, pubKey wgtypes.Key, wgPort uint16, signKey *[64]byte) {
for {
runMCWriterInner(selfVPNIP, pubKey, wgPort, signKey)
time.Sleep(mcErrorRetryInterval)
}
}
func runMCWriterInner(selfVPNIP netip.Addr, pubKey wgtypes.Key, wgPort uint16, signKey *[64]byte) {
conn, err := net.ListenMulticastUDP("udp", nil, mcAddr) conn, err := net.ListenMulticastUDP("udp", nil, mcAddr)
if err != nil { if err != nil {
log.Printf("[MCWriter] bind: %v", err) log.Printf("[MCWriter] bind: %v", err)
return return
} }
defer conn.Close()
payload := buildBeacon(selfVPNIP, pubKey, wgPort) payload := buildBeacon(selfVPNIP, pubKey, wgPort)
signed := sign.Sign(nil, payload, signKey) signed := sign.Sign(nil, payload, signKey)
@@ -38,9 +46,11 @@ func RunMCWriter(selfVPNIP netip.Addr, pubKey wgtypes.Key, wgPort uint16, signKe
if _, err := conn.WriteToUDP(signed, mcAddr); err != nil { if _, err := conn.WriteToUDP(signed, mcAddr); err != nil {
log.Printf("[MCWriter] write: %v", err) log.Printf("[MCWriter] write: %v", err)
} }
for range time.Tick(mcBroadcastInterval) { for range time.Tick(mcBroadcastInterval) {
if _, err := conn.WriteToUDP(signed, mcAddr); err != nil { if _, err := conn.WriteToUDP(signed, mcAddr); err != nil {
log.Printf("[MCWriter] write: %v", err) log.Printf("[MCWriter] write: %v", err)
return
} }
} }
} }

View File

@@ -11,26 +11,27 @@ import (
func TestNetworkState_RoundTrip(t *testing.T) { func TestNetworkState_RoundTrip(t *testing.T) {
path := filepath.Join(t.TempDir(), "network.json") path := filepath.Join(t.TempDir(), "network.json")
var state m.NetworkState state := m.NetworkState{Peers: []m.Peer{
state.Peers[1] = &m.Peer{ {
PeerIP: 1, PeerIP: 1,
Version: 7, Version: 7,
Name: "hub", Name: "hub",
Addr4: []byte{10, 11, 12, 1}, Addr4: []byte{10, 11, 12, 1},
Port: 51820, Port: 51820,
Relay: true, Relay: true,
WGPubKey: make([]byte, 32), WGPubKey: make([]byte, 32),
SignPubKey: make([]byte, 32), SignPubKey: make([]byte, 32),
} },
state.Peers[10] = &m.Peer{ {
PeerIP: 10, PeerIP: 10,
Version: 3, Version: 3,
Name: "laptop", Name: "laptop",
Addr4: []byte{10, 11, 12, 10}, Addr4: []byte{10, 11, 12, 10},
Port: 51820, Port: 51820,
WGPubKey: []byte("0123456789abcdef0123456789abcdef"), WGPubKey: []byte("0123456789abcdef0123456789abcdef"),
SignPubKey: []byte("fedcba9876543210fedcba9876543210"), SignPubKey: []byte("fedcba9876543210fedcba9876543210"),
} },
}}
if err := saveNetworkState(path, state); err != nil { if err := saveNetworkState(path, state); err != nil {
t.Fatal(err) t.Fatal(err)

View File

@@ -27,24 +27,35 @@ func New(
dev, err := wginterface.Open(ifaceName) dev, err := wginterface.Open(ifaceName)
if err != nil { if err != nil {
_ = wginterface.Delete(ifaceName)
return nil, fmt.Errorf("open WG device: %w", err) return nil, fmt.Errorf("open WG device: %w", err)
} }
cc, err := newUDPControlConn(state.VPNIP, ControlPort)
if err != nil {
_ = dev.Close()
_ = wginterface.Delete(ifaceName)
return nil, fmt.Errorf("control conn: %w", err)
}
cleanup := func() {
_ = cc.Close()
_ = dev.Close()
_ = wginterface.Delete(ifaceName)
}
if err := dev.Configure(state.PrivKey, int(state.WGPort)); err != nil { if err := dev.Configure(state.PrivKey, int(state.WGPort)); err != nil {
cleanup()
return nil, fmt.Errorf("configure WG device: %w", err) return nil, fmt.Errorf("configure WG device: %w", err)
} }
if state.IsRelay { if state.IsRelay {
if err := dev.EnableForwarding(); err != nil { if err := dev.EnableForwarding(); err != nil {
cleanup()
return nil, fmt.Errorf("enable forwarding: %w", err) 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) pingCh := make(chan PingEvent)
hubAddCh := make(chan HubPeer) hubAddCh := make(chan HubPeer)
hubRemoveCh := make(chan wgtypes.Key) hubRemoveCh := make(chan wgtypes.Key)
@@ -59,13 +70,15 @@ func New(
hubAddCh, hubAddCh,
hubRemoveCh) hubRemoveCh)
if err != nil { if err != nil {
cleanup()
return nil, fmt.Errorf("hub poller: %w", err) return nil, fmt.Errorf("hub poller: %w", err)
} }
go cc.run(pingCh) go cc.run(pingCh)
go poller.Run() go poller.Run()
go RunMCWriter(state.VPNIP, state.PrivKey.PublicKey(), state.WGPort, &state.SignKey)
if !state.IsPublic { if !state.IsPublic {
go RunMCWriter(state.VPNIP, state.PrivKey.PublicKey(), state.WGPort, &state.SignKey)
go RunMCReader(state.VPNNet, state.VPNIP, multicastCh) go RunMCReader(state.VPNNet, state.VPNIP, multicastCh)
} }