Removed peer editing, cleanup

This commit is contained in:
jdl
2026-06-12 14:49:40 +02:00
parent 4d2aa5c8c7
commit 3e7d34a92a
17 changed files with 99 additions and 203 deletions

View File

@@ -40,7 +40,13 @@ func New(dbPath string) (*API, error) {
sessions: make(map[string]*Session), sessions: make(map[string]*Session),
} }
return a, a.ensurePassword() if err := a.ensurePassword(); err != nil {
return nil, err
}
go a.sweepSessions()
return a, nil
} }
func (a *API) ensurePassword() error { func (a *API) ensurePassword() error {
@@ -80,58 +86,72 @@ func (a *API) Session_Delete(sessionID string) error {
return nil return nil
} }
// Session_Get returns a snapshot copy of the session for sessionID (creating a const (
// fresh one if absent/expired). Returning a value rather than the stored sessionTTLSecs = 86400 * 21 // sessions expire 21 days after last use
// pointer prevents callers from racing on the shared struct; mutations go sessionSweepEvery = time.Hour // cadence of expired-session eviction
// through Session_SignIn / Session_Delete under the lock. )
// Session_Get returns a snapshot copy of the signed-in session for sessionID,
// or the zero Session if the cookie is missing/unknown/expired. It never
// creates a session, so anonymous requests cost no memory — a session is minted
// only by Session_SignIn. Returning a value (not the stored pointer) keeps
// callers from racing on the shared struct.
func (a *API) Session_Get(sessionID string) (Session, error) { func (a *API) Session_Get(sessionID string) (Session, error) {
a.sessionsMu.Lock() a.sessionsMu.Lock()
defer a.sessionsMu.Unlock() defer a.sessionsMu.Unlock()
if sessionID != "" {
s, ok := a.sessions[sessionID] s, ok := a.sessions[sessionID]
if ok {
if timeSince(s.LastSeenAt) <= 86400*21 { if sessionID == "" || !ok {
if timeSince(s.LastSeenAt) > 86400*7 { return Session{}, nil
s.LastSeenAt = time.Now().Unix()
} }
if timeSince(s.LastSeenAt) > sessionTTLSecs {
delete(a.sessions, sessionID)
return Session{}, nil
}
s.LastSeenAt = time.Now().Unix()
return *s, nil return *s, nil
} }
delete(a.sessions, sessionID)
// Session_SignIn verifies pwd and, on success, mints a fresh signed-in session,
// returning it so the caller can set the cookie. A new ID per sign-in rotates
// the session at the privilege boundary (session-fixation resistance).
func (a *API) Session_SignIn(pwd string) (Session, error) {
conf, err := a.Config_Get()
if err != nil {
return Session{}, err
} }
if err := bcrypt.CompareHashAndPassword(conf.Password, []byte(pwd)); err != nil {
return Session{}, ErrNotAuthorized
} }
return *a.session_Create(), nil a.sessionsMu.Lock()
} defer a.sessionsMu.Unlock()
// caller must hold sessionsMu
func (a *API) session_Create() *Session {
s := &Session{ s := &Session{
SessionID: idgen.NewToken(), SessionID: idgen.NewToken(),
SignedIn: true,
CreatedAt: time.Now().Unix(), CreatedAt: time.Now().Unix(),
LastSeenAt: time.Now().Unix(), LastSeenAt: time.Now().Unix(),
} }
a.sessions[s.SessionID] = s a.sessions[s.SessionID] = s
return s return *s, nil
} }
func (a *API) Session_SignIn(sessionID, pwd string) error { // sweepSessions periodically evicts sessions past their TTL. Without it, a
conf, err := a.Config_Get() // signed-in session whose ID is never presented again would linger forever
if err != nil { // (Session_Get only evicts on a lookup of that same ID).
return err func (a *API) sweepSessions() {
} for range time.Tick(sessionSweepEvery) {
if err := bcrypt.CompareHashAndPassword(conf.Password, []byte(pwd)); err != nil {
return ErrNotAuthorized
}
a.sessionsMu.Lock() a.sessionsMu.Lock()
defer a.sessionsMu.Unlock() for id, s := range a.sessions {
s, ok := a.sessions[sessionID] if timeSince(s.LastSeenAt) > sessionTTLSecs {
if !ok { delete(a.sessions, id)
// Session expired or was evicted between fetch and sign-in. }
return ErrNotAuthorized }
a.sessionsMu.Unlock()
} }
s.SignedIn = true
return nil
} }
func (a *API) Network_Create(n *Network) error { func (a *API) Network_Create(n *Network) error {
@@ -153,7 +173,6 @@ func (a *API) Network_List() ([]*Network, error) {
} }
func (a *API) Peer_CreateNew(p *Peer) error { func (a *API) Peer_CreateNew(p *Peer) error {
p.Version = idgen.NextID(0)
p.WGPubKey = []byte{} p.WGPubKey = []byte{}
p.SignPubKey = []byte{} p.SignPubKey = []byte{}
p.APIKey = idgen.NewToken() p.APIKey = idgen.NewToken()
@@ -175,21 +194,12 @@ func (a *API) Peer_Init(peer *Peer, args m.PeerInitArgs) error {
return errors.New("peer already initialized") return errors.New("peer already initialized")
} }
peer.Version = idgen.NextID(0)
peer.WGPubKey = args.WGPubKey peer.WGPubKey = args.WGPubKey
peer.SignPubKey = args.SignPubKey peer.SignPubKey = args.SignPubKey
return db.Peer_UpdateFull(a.db, peer) return db.Peer_UpdateFull(a.db, peer)
} }
func (a *API) Peer_Update(p *Peer) error {
a.lock.Lock()
defer a.lock.Unlock()
p.Version = idgen.NextID(0)
return db.Peer_Update(a.db, p)
}
func (a *API) Peer_Delete(networkID int64, peerIP byte) error { func (a *API) Peer_Delete(networkID int64, peerIP byte) error {
return db.Peer_Delete(a.db, networkID, peerIP) return db.Peer_Delete(a.db, networkID, peerIP)
} }

View File

@@ -341,7 +341,6 @@ func Network_List(
type Peer struct { type Peer struct {
NetworkID int64 NetworkID int64
PeerIP byte PeerIP byte
Version int64
APIKey string APIKey string
Name string Name string
Addr4 []byte Addr4 []byte
@@ -352,7 +351,7 @@ type Peer struct {
SignPubKey []byte SignPubKey []byte
} }
const Peer_SelectQuery = "SELECT NetworkID,PeerIP,Version,APIKey,Name,Addr4,Addr6,Port,Relay,WGPubKey,SignPubKey FROM peers" const Peer_SelectQuery = "SELECT NetworkID,PeerIP,APIKey,Name,Addr4,Addr6,Port,Relay,WGPubKey,SignPubKey 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,Addr6,Port,Relay,WGPubKey,SignPubKey) VALUES(?,?,?,?,?,?,?,?,?,?,?)", row.NetworkID, row.PeerIP, row.Version, row.APIKey, row.Name, row.Addr4, row.Addr6, row.Port, row.Relay, row.WGPubKey, row.SignPubKey) _, err = tx.Exec("INSERT INTO peers(NetworkID,PeerIP,APIKey,Name,Addr4,Addr6,Port,Relay,WGPubKey,SignPubKey) VALUES(?,?,?,?,?,?,?,?,?,?)", row.NetworkID, row.PeerIP, row.APIKey, row.Name, row.Addr4, row.Addr6, row.Port, row.Relay, row.WGPubKey, row.SignPubKey)
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=?,Addr6=?,Port=?,Relay=? WHERE NetworkID=? AND PeerIP=?", row.Version, row.Name, row.Addr4, row.Addr6, row.Port, row.Relay, row.NetworkID, row.PeerIP) result, err := tx.Exec("UPDATE peers SET Name=?,Addr4=?,Addr6=?,Port=?,Relay=? WHERE NetworkID=? AND PeerIP=?", 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=?,Addr6=?,Port=?,Relay=?,WGPubKey=?,SignPubKey=? WHERE NetworkID=? AND PeerIP=?", row.Version, row.APIKey, row.Name, row.Addr4, row.Addr6, row.Port, row.Relay, row.WGPubKey, row.SignPubKey, row.NetworkID, row.PeerIP) result, err := tx.Exec("UPDATE peers SET APIKey=?,Name=?,Addr4=?,Addr6=?,Port=?,Relay=?,WGPubKey=?,SignPubKey=? WHERE NetworkID=? AND PeerIP=?", row.APIKey, row.Name, row.Addr4, row.Addr6, row.Port, row.Relay, row.WGPubKey, row.SignPubKey, 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,Addr6,Port,Relay,WGPubKey,SignPubKey FROM peers WHERE NetworkID=? AND PeerIP=?", NetworkID, PeerIP) r := tx.QueryRow("SELECT NetworkID,PeerIP,APIKey,Name,Addr4,Addr6,Port,Relay,WGPubKey,SignPubKey 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, &row.SignPubKey); err != nil { if err = r.Scan(&row.NetworkID, &row.PeerIP, &row.APIKey, &row.Name, &row.Addr4, &row.Addr6, &row.Port, &row.Relay, &row.WGPubKey, &row.SignPubKey); 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.Addr6, &row.Port, &row.Relay, &row.WGPubKey, &row.SignPubKey); err != nil { if err = r.Scan(&row.NetworkID, &row.PeerIP, &row.APIKey, &row.Name, &row.Addr4, &row.Addr6, &row.Port, &row.Relay, &row.WGPubKey, &row.SignPubKey); 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.Addr6, &row.Port, &row.Relay, &row.WGPubKey, &row.SignPubKey) err := rows.Scan(&row.NetworkID, &row.PeerIP, &row.APIKey, &row.Name, &row.Addr4, &row.Addr6, &row.Port, &row.Relay, &row.WGPubKey, &row.SignPubKey)
if !yield(row, err) { if !yield(row, err) {
return return
} }

View File

@@ -12,13 +12,12 @@ TABLE networks OF Network (
TABLE peers OF Peer ( TABLE peers OF Peer (
NetworkID int64 PK, NetworkID int64 PK,
PeerIP byte PK, PeerIP byte PK,
Version int64,
APIKey string NoUpdate, APIKey string NoUpdate,
Name string, Name string NoUpdate,
Addr4 []byte, Addr4 []byte NoUpdate,
Addr6 []byte, Addr6 []byte NoUpdate,
Port uint16, Port uint16 NoUpdate,
Relay bool, Relay bool NoUpdate,
WGPubKey []byte NoUpdate, WGPubKey []byte NoUpdate,
SignPubKey []byte NoUpdate SignPubKey []byte NoUpdate
); );

View File

@@ -12,7 +12,6 @@ CREATE TABLE networks (
CREATE TABLE peers ( CREATE TABLE peers (
NetworkID INTEGER NOT NULL, NetworkID INTEGER NOT NULL,
PeerIP INTEGER NOT NULL, -- Final byte of IP. PeerIP INTEGER NOT NULL, -- Final byte of IP.
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, -- For humans. Name TEXT NOT NULL, -- For humans.
Addr4 BLOB NOT NULL, Addr4 BLOB NOT NULL,

View File

@@ -20,10 +20,6 @@ func (app *App) handlePub(pattern string, fn handlerFunc) {
return return
} }
if s.SessionID != sessionID {
app.setCookie(w, SESSION_ID_COOKIE_NAME, s.SessionID)
}
if r.Method == http.MethodPost { if r.Method == http.MethodPost {
r.ParseMultipartForm(64 * 1024) r.ParseMultipartForm(64 * 1024)
} else { } else {

View File

@@ -34,9 +34,11 @@ func (a *App) _signinSubmit(s *api.Session, w http.ResponseWriter, r *http.Reque
return err return err
} }
if err := a.api.Session_SignIn(s.SessionID, pwd); err != nil { sess, err := a.api.Session_SignIn(pwd)
if err != nil {
return err return err
} }
a.setCookie(w, SESSION_ID_COOKIE_NAME, sess.SessionID)
return a.redirect(w, r, "/") return a.redirect(w, r, "/")
} }
@@ -185,52 +187,6 @@ func (a *App) _adminPeerView(s *api.Session, w http.ResponseWriter, r *http.Requ
}{s, net, peer}) }{s, net, peer})
} }
func (a *App) _adminPeerEdit(s *api.Session, w http.ResponseWriter, r *http.Request) error {
net, peer, err := a.formGetPeer(r.Form)
if err != nil {
return err
}
return a.render("/network/peer-edit.html", w, struct {
Session *api.Session
Network *api.Network
Peer *api.Peer
}{s, net, peer})
}
func (a *App) _adminPeerEditSubmit(s *api.Session, w http.ResponseWriter, r *http.Request) error {
_, peer, err := a.formGetPeer(r.Form)
if err != nil {
return err
}
var addr4Str, addr6Str string
err = webutil.NewFormScanner(r.Form).
Scan("Name", &peer.Name).
Scan("Addr4", &addr4Str).
Scan("Addr6", &addr6Str).
Scan("Port", &peer.Port).
Scan("Relay", &peer.Relay).
Error()
if err != nil {
return err
}
if peer.Addr4, err = stringToIP(addr4Str); err != nil {
return err
}
if peer.Addr6, err = stringToIP(addr6Str); err != nil {
return err
}
if err = a.api.Peer_Update(peer); err != nil {
return err
}
return a.redirect(w, r, "/admin/peer/view/?NetworkID=%d&PeerIP=%d", peer.NetworkID, peer.PeerIP)
}
func (a *App) _adminPeerDelete(s *api.Session, w http.ResponseWriter, r *http.Request) error { func (a *App) _adminPeerDelete(s *api.Session, w http.ResponseWriter, r *http.Request) error {
n, peer, err := a.formGetPeer(r.Form) n, peer, err := a.formGetPeer(r.Form)
if err != nil { if err != nil {
@@ -378,7 +334,6 @@ func (a *App) peersList(networkID int64) (peers []m.Peer, err error) {
copy(signKey[:], p.SignPubKey) copy(signKey[:], p.SignPubKey)
peers = append(peers, m.Peer{ peers = append(peers, m.Peer{
PeerIP: p.PeerIP, PeerIP: p.PeerIP,
Version: p.Version,
Name: p.Name, Name: p.Name,
Addr4: addrFromBytes(p.Addr4), Addr4: addrFromBytes(p.Addr4),
Addr6: addrFromBytes(p.Addr6), Addr6: addrFromBytes(p.Addr6),

View File

@@ -22,8 +22,6 @@ func (a *App) registerRoutes() {
a.handleSignedIn("GET /admin/peer/create/", a._adminPeerCreate) a.handleSignedIn("GET /admin/peer/create/", a._adminPeerCreate)
a.handleSignedIn("POST /admin/peer/create/", a._adminPeerCreateSubmit) a.handleSignedIn("POST /admin/peer/create/", a._adminPeerCreateSubmit)
a.handleSignedIn("GET /admin/peer/view/", a._adminPeerView) a.handleSignedIn("GET /admin/peer/view/", a._adminPeerView)
a.handleSignedIn("GET /admin/peer/edit/", a._adminPeerEdit)
a.handleSignedIn("POST /admin/peer/edit/", a._adminPeerEditSubmit)
a.handleSignedIn("GET /admin/peer/delete/", a._adminPeerDelete) a.handleSignedIn("GET /admin/peer/delete/", a._adminPeerDelete)
a.handleSignedIn("POST /admin/peer/delete/", a._adminPeerDeleteSubmit) a.handleSignedIn("POST /admin/peer/delete/", a._adminPeerDeleteSubmit)

View File

@@ -4,7 +4,7 @@
{{with .Peer -}} {{with .Peer -}}
<form method="POST"> <form method="POST">
<input type="hidden" name="NetworkID" value="{{.NetworkID}}"> <input type="hidden" name="NetworkID" value="{{.NetworkID}}">
<input type="hidden" name="NetworkID" value="{{.PeerIP}}"> <input type="hidden" name="PeerIP" value="{{.PeerIP}}">
<p> <p>
<button type="submit">Delete</button> <button type="submit">Delete</button>
<a href="/admin/peer/view/?PeerIP={{.PeerIP}}&NetworkID={{.NetworkID}}">Cancel</a> <a href="/admin/peer/view/?PeerIP={{.PeerIP}}&NetworkID={{.NetworkID}}">Cancel</a>

View File

@@ -1,38 +0,0 @@
{{define "body" -}}
<h2>Edit Peer</h2>
{{with .Peer -}}
<form method="POST">
<p>
<label>Peer IP</label><br>
<input type="text" value="{{.PeerIP}}" disabled>
</p>
<p>
<label>Name</label><br>
<input type="text" name="Name" value="{{.Name}}">
</p>
<p>
<label>IPv4 Address (optional)</label><br>
<input type="text" name="Addr4" value="{{ipToString .Addr4}}">
</p>
<p>
<label>IPv6 Address (optional)</label><br>
<input type="text" name="Addr6" value="{{ipToString .Addr6}}">
</p>
<p>
<label>WireGuard Port</label><br>
<input type="number" name="Port" value="{{.Port}}">
</p>
<p>
<label>
<input type="checkbox" name="Relay" {{if .Relay}}checked{{end}}>
Relay
</label>
</p>
<p>
<button type="submit">Save</button>
<a href="/admin/peer/view/?NetworkID={{$.Network.NetworkID}}&PeerIP={{.PeerIP}}">Cancel</a>
</p>
</form>
{{- end}}
{{- end}}

View File

@@ -1,7 +1,6 @@
{{define "body" -}} {{define "body" -}}
<h3>{{.Peer.Name}}</h3> <h3>{{.Peer.Name}}</h3>
<p> <p>
<a href="/admin/peer/edit/?NetworkID={{.Network.NetworkID}}&PeerIP={{.Peer.PeerIP}}">Edit</a> /
<a href="/admin/peer/delete/?NetworkID={{.Network.NetworkID}}&PeerIP={{.Peer.PeerIP}}">Delete</a> <a href="/admin/peer/delete/?NetworkID={{.Network.NetworkID}}&PeerIP={{.Peer.PeerIP}}">Delete</a>
</p> </p>

View File

@@ -28,7 +28,6 @@ type PeerInitResp struct {
// non-public peer (it is the peer's own bind/beacon port). // non-public peer (it is the peer's own bind/beacon port).
type Peer struct { type Peer struct {
PeerIP byte PeerIP byte
Version int64
Name string Name string
Addr4 netip.Addr // zero if none Addr4 netip.Addr // zero if none
Addr6 netip.Addr // zero if none Addr6 netip.Addr // zero if none
@@ -74,7 +73,6 @@ func (p Peer) PreferredEndpoint() netip.AddrPort {
// encoding/json would emit them as arrays of numbers). // encoding/json would emit them as arrays of numbers).
type peerJSON struct { type peerJSON struct {
PeerIP byte PeerIP byte
Version int64
Name string Name string
Addr4 netip.Addr Addr4 netip.Addr
Addr6 netip.Addr Addr6 netip.Addr
@@ -87,7 +85,6 @@ type peerJSON struct {
func (p Peer) MarshalJSON() ([]byte, error) { func (p Peer) MarshalJSON() ([]byte, error) {
return json.Marshal(peerJSON{ return json.Marshal(peerJSON{
PeerIP: p.PeerIP, PeerIP: p.PeerIP,
Version: p.Version,
Name: p.Name, Name: p.Name,
Addr4: p.Addr4, Addr4: p.Addr4,
Addr6: p.Addr6, Addr6: p.Addr6,
@@ -120,7 +117,6 @@ func (p *Peer) UnmarshalJSON(data []byte) error {
} }
*p = Peer{ *p = Peer{
PeerIP: j.PeerIP, PeerIP: j.PeerIP,
Version: j.Version,
Name: j.Name, Name: j.Name,
Addr4: j.Addr4, Addr4: j.Addr4,
Addr6: j.Addr6, Addr6: j.Addr6,

View File

@@ -24,7 +24,7 @@ type HubPoller struct {
statePath string // where the network state cache is persisted statePath string // where the network state cache is persisted
addCh chan<- m.Peer addCh chan<- m.Peer
removeCh chan<- wgtypes.Key removeCh chan<- wgtypes.Key
known map[wgtypes.Key]int64 // pubKey → last seen version known map[wgtypes.Key]struct{} // pubKeys currently configured
} }
func NewHubPoller( func NewHubPoller(
@@ -49,15 +49,15 @@ func NewHubPoller(
statePath: statePath, statePath: statePath,
addCh: addCh, addCh: addCh,
removeCh: removeCh, removeCh: removeCh,
known: make(map[wgtypes.Key]int64), known: make(map[wgtypes.Key]struct{}),
}, nil }, nil
} }
func (hp *HubPoller) Run() { func (hp *HubPoller) Run() {
// Prime from the on-disk cache before reaching the hub, so the peer // Prime from the on-disk cache before reaching the hub, so the peer
// configures WireGuard from its last known state even if the hub is down. // configures WireGuard from its last known state even if the hub is down.
// known starts empty, so this emits every cached peer as an add and seeds // known starts empty, so this emits every cached peer as an add; the first
// the version map; the first real poll then emits only deltas. // real poll then emits only deltas (adds for new peers, removes for gone).
if state, err := loadNetworkState(hp.statePath); err == nil { if state, err := loadNetworkState(hp.statePath); err == nil {
hp.apply(state) hp.apply(state)
} }
@@ -110,9 +110,10 @@ func (hp *HubPoller) poll() {
} }
} }
// apply diffs state against the known versions, emitting add events for new or // apply diffs state against the set of known peers, emitting an add for each
// changed peers and remove events for peers that disappeared. It returns true // newly-seen peer and a remove for each that disappeared. It returns true if
// if anything changed. // anything changed. A peer's config is immutable under a stable WG key (the hub
// has no peer-edit path), so a key already in known needs no re-emit.
func (hp *HubPoller) apply(state m.NetworkState) (changed bool) { func (hp *HubPoller) apply(state m.NetworkState) (changed bool) {
seen := make(map[wgtypes.Key]struct{}, len(hp.known)) seen := make(map[wgtypes.Key]struct{}, len(hp.known))
@@ -132,10 +133,10 @@ func (hp *HubPoller) apply(state m.NetworkState) (changed bool) {
seen[p.WGPubKey] = struct{}{} seen[p.WGPubKey] = struct{}{}
if v, ok := hp.known[p.WGPubKey]; ok && v == p.Version { if _, ok := hp.known[p.WGPubKey]; ok {
continue continue
} }
hp.known[p.WGPubKey] = p.Version hp.known[p.WGPubKey] = struct{}{}
hp.addCh <- p hp.addCh <- p
changed = true changed = true
} }

View File

@@ -18,15 +18,14 @@ func testPoller(t *testing.T) (*HubPoller, chan m.Peer, chan wgtypes.Key) {
vpnNet: netip.MustParsePrefix("10.0.0.0/24"), vpnNet: netip.MustParsePrefix("10.0.0.0/24"),
addCh: addCh, addCh: addCh,
removeCh: removeCh, removeCh: removeCh,
known: make(map[wgtypes.Key]int64), known: make(map[wgtypes.Key]struct{}),
} }
return hp, addCh, removeCh return hp, addCh, removeCh
} }
func stateWith(key wgtypes.Key, peerIP byte, version int64) m.NetworkState { func stateWith(key wgtypes.Key, peerIP byte) m.NetworkState {
return m.NetworkState{Peers: []m.Peer{{ return m.NetworkState{Peers: []m.Peer{{
PeerIP: peerIP, PeerIP: peerIP,
Version: version,
WGPubKey: key, WGPubKey: key,
}}} }}}
} }
@@ -35,7 +34,7 @@ func TestApply_EmitsAddsAndReportsChange(t *testing.T) {
hp, addCh, _ := testPoller(t) hp, addCh, _ := testPoller(t)
key := mustKey(t) key := mustKey(t)
if changed := hp.apply(stateWith(key, 2, 1)); !changed { if changed := hp.apply(stateWith(key, 2)); !changed {
t.Fatal("expected changed=true on first apply") t.Fatal("expected changed=true on first apply")
} }
if len(addCh) != 1 { if len(addCh) != 1 {
@@ -46,41 +45,26 @@ func TestApply_EmitsAddsAndReportsChange(t *testing.T) {
} }
} }
func TestApply_NoChangeWhenVersionSame(t *testing.T) { func TestApply_NoChangeWhenKnown(t *testing.T) {
hp, addCh, _ := testPoller(t) hp, addCh, _ := testPoller(t)
key := mustKey(t) key := mustKey(t)
hp.apply(stateWith(key, 2, 1)) hp.apply(stateWith(key, 2))
<-addCh // drain initial add <-addCh // drain initial add
if changed := hp.apply(stateWith(key, 2, 1)); changed { if changed := hp.apply(stateWith(key, 2)); changed {
t.Fatal("expected changed=false when version unchanged") t.Fatal("expected changed=false when peer already known")
} }
if len(addCh) != 0 { if len(addCh) != 0 {
t.Fatalf("expected no re-emit, got %d adds", len(addCh)) t.Fatalf("expected no re-emit, got %d adds", len(addCh))
} }
} }
func TestApply_ReEmitsOnVersionBump(t *testing.T) {
hp, addCh, _ := testPoller(t)
key := mustKey(t)
hp.apply(stateWith(key, 2, 1))
<-addCh
if changed := hp.apply(stateWith(key, 2, 2)); !changed {
t.Fatal("expected changed=true on version bump")
}
if len(addCh) != 1 {
t.Fatalf("expected 1 re-emit, got %d", len(addCh))
}
}
func TestApply_RemovesVanishedPeer(t *testing.T) { func TestApply_RemovesVanishedPeer(t *testing.T) {
hp, addCh, removeCh := testPoller(t) hp, addCh, removeCh := testPoller(t)
key := mustKey(t) key := mustKey(t)
hp.apply(stateWith(key, 2, 1)) hp.apply(stateWith(key, 2))
<-addCh <-addCh
// Empty state: the peer is gone. // Empty state: the peer is gone.

View File

@@ -18,7 +18,6 @@ func TestNetworkState_RoundTrip(t *testing.T) {
state := m.NetworkState{Peers: []m.Peer{ state := m.NetworkState{Peers: []m.Peer{
{ {
PeerIP: 1, PeerIP: 1,
Version: 7,
Name: "hub", Name: "hub",
Addr4: netip.MustParseAddr("10.11.12.1"), Addr4: netip.MustParseAddr("10.11.12.1"),
Port: 51820, Port: 51820,
@@ -28,7 +27,6 @@ func TestNetworkState_RoundTrip(t *testing.T) {
}, },
{ {
PeerIP: 10, PeerIP: 10,
Version: 3,
Name: "laptop", Name: "laptop",
Addr4: netip.MustParseAddr("10.11.12.10"), Addr4: netip.MustParseAddr("10.11.12.10"),
Port: 51820, Port: 51820,

View File

@@ -25,8 +25,8 @@ func (a *App) onAddPeer(p m.Peer) {
Name: p.Name, Name: p.Name,
IsRelay: p.Relay, IsRelay: p.Relay,
IsPublic: p.IsPublic(), IsPublic: p.IsPublic(),
Endpoint4: p.Endpoint4(), EndpointV4: p.Endpoint4(),
Endpoint6: p.Endpoint6(), EndpointV6: p.Endpoint6(),
RTT: time.Duration(math.MaxInt64) * time.Nanosecond, RTT: time.Duration(math.MaxInt64) * time.Nanosecond,
Role: roleFor(a.isPublic, a.vpnIP, p.IsPublic(), vpnIP), Role: roleFor(a.isPublic, a.vpnIP, p.IsPublic(), vpnIP),
SignPubKey: p.SignPubKey, SignPubKey: p.SignPubKey,

View File

@@ -280,7 +280,7 @@ func TestSwitchActiveRelay(t *testing.T) {
t.Errorf("call[0]: got %v, want AddDirect with ep1", dev.Calls[0]) t.Errorf("call[0]: got %v, want AddDirect with ep1", dev.Calls[0])
} }
dev.AssertSetRelay(t, 1, dev.Calls[1].PubKey, ep2, a.vpnNet) dev.AssertSetRelay(t, 1, dev.Calls[1].PubKey, ep2, a.vpnNet)
if a.relay == nil || a.relay.Endpoint4 != ep2 { if a.relay == nil || a.relay.EndpointV4 != ep2 {
t.Error("relay should be the backup peer") t.Error("relay should be the backup peer")
} }
}, },

View File

@@ -24,8 +24,8 @@ type Peer struct {
Name string // Human-readable DNS label. Name string // Human-readable DNS label.
IsRelay bool // Peer is a relay. IsRelay bool // Peer is a relay.
IsPublic bool // Peer has a public IP. IsPublic bool // Peer has a public IP.
Endpoint4 netip.AddrPort // Reported IPv4 endpoint. EndpointV4 netip.AddrPort // Reported IPv4 endpoint.
Endpoint6 netip.AddrPort // Reported IPv6 endpoint. EndpointV6 netip.AddrPort // Reported IPv6 endpoint.
RTT time.Duration // Round-trip time. RTT time.Duration // Round-trip time.
State PeerState // Current routing state; updated on each devXxx call. State PeerState // Current routing state; updated on each devXxx call.
Role control.Role // Client initiates pings; server responds. Role control.Role // Client initiates pings; server responds.
@@ -62,14 +62,14 @@ func (p *Peer) CanRelay() bool {
} }
func (p *Peer) PreferredEndpoint() netip.AddrPort { func (p *Peer) PreferredEndpoint() netip.AddrPort {
return preferredEndpoint(p.Endpoint4, p.Endpoint6) return preferredEndpoint(p.EndpointV4, p.EndpointV6)
} }
func (p *Peer) UpdateEndpoints(v4, v6 netip.AddrPort) { func (p *Peer) UpdateEndpoints(v4, v6 netip.AddrPort) {
if v4.IsValid() { if v4.IsValid() {
p.Endpoint4 = v4 p.EndpointV4 = v4
} }
if v6.IsValid() { if v6.IsValid() {
p.Endpoint6 = v6 p.EndpointV6 = v6
} }
} }