From ab2837817d4dd4479f104f5b4b264dddb38236cf Mon Sep 17 00:00:00 2001 From: jdl Date: Thu, 11 Jun 2026 17:03:57 +0200 Subject: [PATCH] WIP --- hub/api/api.go | 19 ++++++++++++++----- hub/handler.go | 2 +- hub/handlers.go | 2 +- peer/device.go | 2 +- peer/hosts.go | 15 +++++++++++---- peer/hosts_test.go | 42 ++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 68 insertions(+), 14 deletions(-) diff --git a/hub/api/api.go b/hub/api/api.go index 6aaffce..a0fa94e 100644 --- a/hub/api/api.go +++ b/hub/api/api.go @@ -80,7 +80,11 @@ func (a *API) Session_Delete(sessionID string) error { return nil } -func (a *API) Session_Get(sessionID string) (*Session, error) { +// Session_Get returns a snapshot copy of the session for sessionID (creating a +// fresh one if absent/expired). Returning a value rather than the stored +// pointer prevents callers from racing on the shared struct; mutations go +// through Session_SignIn / Session_Delete under the lock. +func (a *API) Session_Get(sessionID string) (Session, error) { a.sessionsMu.Lock() defer a.sessionsMu.Unlock() @@ -91,13 +95,13 @@ func (a *API) Session_Get(sessionID string) (*Session, error) { if timeSince(s.LastSeenAt) > 86400*7 { s.LastSeenAt = time.Now().Unix() } - return s, nil + return *s, nil } delete(a.sessions, sessionID) } } - return a.session_Create(), nil + return *a.session_Create(), nil } // caller must hold sessionsMu @@ -111,7 +115,7 @@ func (a *API) session_Create() *Session { return s } -func (a *API) Session_SignIn(s *Session, pwd string) error { +func (a *API) Session_SignIn(sessionID, pwd string) error { conf, err := a.Config_Get() if err != nil { return err @@ -120,8 +124,13 @@ func (a *API) Session_SignIn(s *Session, pwd string) error { return ErrNotAuthorized } a.sessionsMu.Lock() + defer a.sessionsMu.Unlock() + s, ok := a.sessions[sessionID] + if !ok { + // Session expired or was evicted between fetch and sign-in. + return ErrNotAuthorized + } s.SignedIn = true - a.sessionsMu.Unlock() return nil } diff --git a/hub/handler.go b/hub/handler.go index 8b4f0f2..1581a73 100644 --- a/hub/handler.go +++ b/hub/handler.go @@ -30,7 +30,7 @@ func (app *App) handlePub(pattern string, fn handlerFunc) { r.ParseForm() } - if err := fn(s, w, r); err != nil { + if err := fn(&s, w, r); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } diff --git a/hub/handlers.go b/hub/handlers.go index 1400f47..3130111 100644 --- a/hub/handlers.go +++ b/hub/handlers.go @@ -33,7 +33,7 @@ func (a *App) _signinSubmit(s *api.Session, w http.ResponseWriter, r *http.Reque return err } - if err := a.api.Session_SignIn(s, pwd); err != nil { + if err := a.api.Session_SignIn(s.SessionID, pwd); err != nil { return err } diff --git a/peer/device.go b/peer/device.go index 71bfaaf..3a3ba1c 100644 --- a/peer/device.go +++ b/peer/device.go @@ -53,7 +53,7 @@ func (a *App) devAddDirect(p *Peer, endpoint netip.AddrPort) { func (a *App) devSetRelay(p *Peer, endpoint netip.AddrPort) { log.Printf("RELAY: %s - %s @ %s", p.Name, p.VPNIP.String(), endpoint.String()) devRetry(p.VPNIP, "SetRelay", func() error { return a.dev.SetRelay(p.PubKey(), endpoint, a.vpnNet) }) - p.State = StateDirect + p.State = StateDirect // Dirrect connection. The app marks peer as relay. } func (a *App) devPromote(p *Peer) { diff --git a/peer/hosts.go b/peer/hosts.go index 3dca3b1..fc47d20 100644 --- a/peer/hosts.go +++ b/peer/hosts.go @@ -14,10 +14,18 @@ import ( const ( hostsFile = "/etc/hosts" - hostsBegin = "# BEGIN vppn " - hostsEnd = "# END vppn " + hostsBegin = "# BEGIN vppn" + hostsEnd = "# END vppn" ) +// hostMarkers returns the begin/end marker lines that delimit the managed +// section for localDomain. The domain is wrapped in parentheses so one domain's +// marker can never be a prefix of another's (e.g. "net" vs "net2") when +// multiple vppn instances share /etc/hosts. +func hostMarkers(localDomain string) (begin, end string) { + return hostsBegin + "(" + localDomain + ")", hostsEnd + "(" + localDomain + ")" +} + // updateHosts rewrites the managed vppn section in /etc/hosts using the // current peersByIP map. Peers without a Name are skipped. func (a *App) updateHosts() { @@ -36,8 +44,7 @@ func updateHosts(hostsPath, localDomain string, peers map[netip.Addr]*Peer) erro } defer lockFile.Close() - begin := hostsBegin + localDomain - end := hostsEnd + localDomain + begin, end := hostMarkers(localDomain) info, err := os.Stat(hostsPath) if err != nil { diff --git a/peer/hosts_test.go b/peer/hosts_test.go index c7fc4e4..6b957c3 100644 --- a/peer/hosts_test.go +++ b/peer/hosts_test.go @@ -28,8 +28,7 @@ func readManagedSection(t *testing.T, path, localDomain string) (inside, outside if err != nil { t.Fatal(err) } - begin := hostsBegin + localDomain - end := hostsEnd + localDomain + begin, end := hostMarkers(localDomain) inSection := false for _, line := range strings.Split(string(raw), "\n") { @@ -157,6 +156,45 @@ func TestUpdateHosts_Idempotent(t *testing.T) { } } +// TestUpdateHosts_PrefixDomainsCoexist guards finding 4.4: two domains where +// one label is a prefix of the other ("net" vs "net2") must each manage their +// own section without clobbering the other's, even sharing one hosts file. +func TestUpdateHosts_PrefixDomainsCoexist(t *testing.T) { + path := writeTempHosts(t, "127.0.0.1 localhost\n") + + if err := updateHosts(path, "net2.local", map[netip.Addr]*Peer{ + netip.MustParseAddr("10.0.2.1"): peer("a"), + }); err != nil { + t.Fatal(err) + } + if err := updateHosts(path, "net.local", map[netip.Addr]*Peer{ + netip.MustParseAddr("10.0.1.1"): peer("b"), + }); err != nil { + t.Fatal(err) + } + + // Both sections coexist after writing the prefix domain. + if in, _ := readManagedSection(t, path, "net2.local"); len(in) != 1 || in[0] != "10.0.2.1 a.net2.local" { + t.Errorf("net2 section clobbered: %v", in) + } + if in, _ := readManagedSection(t, path, "net.local"); len(in) != 1 || in[0] != "10.0.1.1 b.net.local" { + t.Errorf("net section wrong: %v", in) + } + + // Re-updating net2 must not disturb the net section. + if err := updateHosts(path, "net2.local", map[netip.Addr]*Peer{ + netip.MustParseAddr("10.0.2.2"): peer("c"), + }); err != nil { + t.Fatal(err) + } + if in, _ := readManagedSection(t, path, "net.local"); len(in) != 1 || in[0] != "10.0.1.1 b.net.local" { + t.Errorf("net section disturbed by net2 update: %v", in) + } + if in, _ := readManagedSection(t, path, "net2.local"); len(in) != 1 || in[0] != "10.0.2.2 c.net2.local" { + t.Errorf("net2 section not updated: %v", in) + } +} + func contains(ss []string, s string) bool { for _, x := range ss { if x == s {