@@ -56,11 +56,20 @@ type Relay struct {
5656}
5757
5858type HostMap struct {
59- sync.RWMutex //Because we concurrently read and write to our maps
60- Indexes map [uint32 ]* HostInfo
61- Relays map [uint32 ]* HostInfo // Maps a Relay IDX to a Relay HostInfo object
62- RemoteIndexes map [uint32 ]* HostInfo
59+ sync.RWMutex //Because we concurrently read and write to our maps
60+ Indexes map [uint32 ]* HostInfo
61+ Relays map [uint32 ]* HostInfo // Maps a Relay IDX to a Relay HostInfo object
62+ RemoteIndexes map [uint32 ]* HostInfo
63+ // Hosts maps a vpn address to its primary hostinfo, one entry per address we hold a tunnel
64+ // for. moreHosts only has an entry while an address is held by 2 or more hostinfos and stores
65+ // the full most-recent-first list; moreHosts[a][0] is always the same hostinfo as Hosts[a].
66+ // Each address gets its own independent list, so a hostinfo owning multiple addresses can
67+ // never corrupt another address's ordering the way the old shared next/prev chain could.
68+ // Entries in moreHosts are only ever written by unlockedSetHostsForAddr; Hosts is written
69+ // directly only in the single-hostinfo fast paths where moreHosts is known to have no entry,
70+ // and unlockedDeleteHostInfo swaps either map for a fresh one when it fully drains.
6371 Hosts map [netip.Addr ]* HostInfo
72+ moreHosts map [netip.Addr ][]* HostInfo
6473 preferredRanges atomic.Pointer [[]netip.Prefix ]
6574 l * slog.Logger
6675}
@@ -266,10 +275,6 @@ type HostInfo struct {
266275 lastRoam time.Time
267276 lastRoamRemote netip.AddrPort
268277
269- // Used to track other hostinfos for this vpn ip since only 1 can be primary
270- // Synchronised via hostmap lock and not the hostinfo lock.
271- next , prev * HostInfo
272-
273278 //TODO: in, out, and others might benefit from being an atomic.Int32. We could collapse connectionManager pendingDeletion, relayUsed, and in/out into this 1 thing
274279 in , out , pendingDeletion atomic.Bool
275280
@@ -334,6 +339,7 @@ func newHostMap(l *slog.Logger) *HostMap {
334339 Relays : map [uint32 ]* HostInfo {},
335340 RemoteIndexes : map [uint32 ]* HostInfo {},
336341 Hosts : map [netip.Addr ]* HostInfo {},
342+ moreHosts : map [netip.Addr ][]* HostInfo {},
337343 l : l ,
338344 }
339345}
@@ -382,13 +388,55 @@ func (hm *HostMap) EmitStats() {
382388 metrics .GetOrRegisterGauge ("hostmap.main.relayIndexes" , nil ).Update (int64 (relaysLen ))
383389}
384390
385- // DeleteHostInfo will fully unlink the hostinfo and return true if it was the final hostinfo for this vpn ip
391+ // unlockedSetHostsForAddr stores the per-address hostinfo list (list[0] is the primary). An empty
392+ // list removes the address. This is the one place Hosts and moreHosts are written together, keep
393+ // it that way. Callers must hold the write lock.
394+ func (hm * HostMap ) unlockedSetHostsForAddr (addr netip.Addr , list []* HostInfo ) {
395+ if len (list ) == 0 {
396+ delete (hm .Hosts , addr )
397+ delete (hm .moreHosts , addr )
398+ return
399+ }
400+ hm .Hosts [addr ] = list [0 ]
401+ if len (list ) > 1 {
402+ hm .moreHosts [addr ] = list
403+ } else {
404+ delete (hm .moreHosts , addr )
405+ }
406+ }
407+
408+ // unlockedGetHostList returns every hostinfo holding addr, primary first, or nil if we have no
409+ // tunnel for addr. The common single-hostinfo case builds a fresh one element list, so keep this
410+ // off the packet hot path; the primary is a direct Hosts read. Callers must hold the lock (read
411+ // or write).
412+ func (hm * HostMap ) unlockedGetHostList (addr netip.Addr ) []* HostInfo {
413+ if list , ok := hm .moreHosts [addr ]; ok {
414+ return list
415+ }
416+ if h , ok := hm .Hosts [addr ]; ok {
417+ return []* HostInfo {h }
418+ }
419+ return nil
420+ }
421+
422+ // removeHostInfo returns list with hi removed (order preserved), or list unchanged if hi is
423+ // absent. It deletes in place: every mutator holds the hostmap write lock and no reader ever
424+ // retains a slice across a mutation (readers iterate under RLock), so there is no snapshot to
425+ // invalidate.
426+ func removeHostInfo (list []* HostInfo , hi * HostInfo ) []* HostInfo {
427+ idx := slices .Index (list , hi )
428+ if idx < 0 {
429+ return list
430+ }
431+ return slices .Delete (list , idx , idx + 1 )
432+ }
433+
434+ // DeleteHostInfo will fully unlink the hostinfo and return true if no other hostinfo still holds
435+ // any of its vpn addrs, meaning we no longer have a tunnel to the peer
386436func (hm * HostMap ) DeleteHostInfo (hostinfo * HostInfo ) bool {
387437 // Delete the host itself, ensuring it's not modified anymore
388438 hm .Lock ()
389- // If we have a previous or next hostinfo then we are not the last one for this vpn ip
390- final := (hostinfo .next == nil && hostinfo .prev == nil )
391- hm .unlockedDeleteHostInfo (hostinfo )
439+ final := hm .unlockedDeleteHostInfo (hostinfo )
392440 hm .Unlock ()
393441
394442 return final
@@ -401,71 +449,63 @@ func (hm *HostMap) MakePrimary(hostinfo *HostInfo) {
401449}
402450
403451func (hm * HostMap ) unlockedMakePrimary (hostinfo * HostInfo ) {
404- // Get the current primary, if it exists
405- oldHostinfo := hm .Hosts [hostinfo .vpnAddrs [0 ]]
406-
407- // Every address in the hostinfo gets elevated to primary
408- for _ , vpnAddr := range hostinfo .vpnAddrs {
409- //NOTE: It is possible that we leave a dangling hostinfo here but connection manager works on
410- // indexes so it should be fine.
411- hm .Hosts [vpnAddr ] = hostinfo
412- }
413-
414- // If we are already primary then we won't bother re-linking
415- if oldHostinfo == hostinfo {
452+ // A hostinfo that is no longer in the hostmap must not be re-inserted here. Callers can race
453+ // tunnel teardown, deciding to promote under the read lock and only taking the write lock
454+ // after a delete fully unlinked the hostinfo (connection manager swapPrimary, AddRelay). Every
455+ // live hostinfo is registered in Indexes by unlockedAddHostInfo, so this is a membership test.
456+ if hm .Indexes [hostinfo .localIndexId ] != hostinfo {
416457 return
417458 }
418459
419- // Unlink this hostinfo
420- if hostinfo .prev != nil {
421- hostinfo .prev .next = hostinfo .next
422- }
423- if hostinfo .next != nil {
424- hostinfo .next .prev = hostinfo .prev
425- }
426-
427- // If there wasn't a previous primary then clear out any links
428- if oldHostinfo == nil {
429- hostinfo .next = nil
430- hostinfo .prev = nil
431- return
460+ // Move hostinfo to the front (primary) of each of its address lists. The lists are
461+ // independent per address, so this can never leave a dangling entry the way promoting
462+ // against a single shared chain could.
463+ for _ , addr := range hostinfo .vpnAddrs {
464+ if hm .Hosts [addr ] == hostinfo {
465+ // Already primary for this address, the list is already in the right order
466+ continue
467+ }
468+ list := removeHostInfo (hm .unlockedGetHostList (addr ), hostinfo )
469+ list = append ([]* HostInfo {hostinfo }, list ... )
470+ hm .unlockedSetHostsForAddr (addr , list )
432471 }
433-
434- // Relink the hostinfo as primary
435- hostinfo .next = oldHostinfo
436- oldHostinfo .prev = hostinfo
437- hostinfo .prev = nil
438472}
439473
440- func (hm * HostMap ) unlockedDeleteHostInfo (hostinfo * HostInfo ) {
441- isLastHostinfo := hostinfo .next == nil && hostinfo .prev == nil
442-
474+ // unlockedDeleteHostInfo removes hostinfo from every one of its address lists and from the index
475+ // maps. It returns true if this was the last hostinfo for all of its addresses (we no longer have
476+ // any tunnel to the peer), which the caller uses to decide whether to clear learned lighthouse
477+ // state and disestablish relays.
478+ func (hm * HostMap ) unlockedDeleteHostInfo (hostinfo * HostInfo ) bool {
479+ // Remove this hostinfo from each of its address lists. The lists are independent, so a
480+ // sibling is never promoted to an address it does not own and no other list is touched.
481+ final := true
443482 for _ , addr := range hostinfo .vpnAddrs {
444- if hm .Hosts [addr ] != hostinfo {
445- continue
446- }
447- if hostinfo .next != nil {
448- // Promote the next hostinfo in the shared chain to primary for this address
449- hm .Hosts [addr ] = hostinfo .next
450- } else {
451- delete (hm .Hosts , addr )
483+ if list , ok := hm .moreHosts [addr ]; ok {
484+ list = removeHostInfo (list , hostinfo )
485+ hm .unlockedSetHostsForAddr (addr , list )
486+ if len (list ) > 0 {
487+ final = false
488+ }
489+ } else if existing , ok := hm .Hosts [addr ]; ok {
490+ if existing == hostinfo {
491+ // Common case, the only hostinfo for this address. moreHosts has no entry to clean up.
492+ delete (hm .Hosts , addr )
493+ } else {
494+ // We don't hold this address but another hostinfo does, we still have a tunnel to the peer
495+ final = false
496+ }
452497 }
453498 }
499+
500+ // Go maps never shrink their buckets, replace fully drained maps so a node that churned
501+ // through a large peer count gives the memory back. Same idiom as the index maps below.
454502 if len (hm .Hosts ) == 0 {
455503 hm .Hosts = map [netip.Addr ]* HostInfo {}
456504 }
457-
458- // Splice this hostinfo out of the shared chain exactly once
459- if hostinfo .prev != nil {
460- hostinfo .prev .next = hostinfo .next
461- }
462- if hostinfo .next != nil {
463- hostinfo .next .prev = hostinfo .prev
505+ if len (hm .moreHosts ) == 0 {
506+ hm .moreHosts = map [netip.Addr ][]* HostInfo {}
464507 }
465508
466- hostinfo .next = nil
467- hostinfo .prev = nil
468-
469509 // The remote index uses index ids outside our control so lets make sure we are only removing
470510 // the remote index pointer here if it points to the hostinfo we are deleting
471511 hostinfo2 , ok := hm .RemoteIndexes [hostinfo .remoteIndexId ]
@@ -488,7 +528,7 @@ func (hm *HostMap) unlockedDeleteHostInfo(hostinfo *HostInfo) {
488528 )
489529 }
490530
491- if isLastHostinfo {
531+ if final {
492532 // I have lost connectivity to my peers. My relay tunnel is likely broken. Mark the next
493533 // hops as 'Requested' so that new relay tunnels are created in the future.
494534 hm .unlockedDisestablishVpnAddrRelayFor (hostinfo )
@@ -497,6 +537,8 @@ func (hm *HostMap) unlockedDeleteHostInfo(hostinfo *HostInfo) {
497537 for _ , localRelayIdx := range hostinfo .relayState .CopyRelayForIdxs () {
498538 delete (hm .Relays , localRelayIdx )
499539 }
540+
541+ return final
500542}
501543
502544func (hm * HostMap ) QueryIndex (index uint32 ) * HostInfo {
@@ -540,40 +582,45 @@ func (hm *HostMap) QueryVpnAddrsRelayFor(targetIps []netip.Addr, relayHostIp net
540582 hm .RLock ()
541583 defer hm .RUnlock ()
542584
585+ // This runs per relayed packet, so check the primary with a single map probe and only consult
586+ // moreHosts when the primary can't relay for us.
543587 h , ok := hm .Hosts [relayHostIp ]
544588 if ! ok {
545589 return nil , nil , errors .New ("unable to find host" )
546590 }
547591
548- for h != nil {
549- for _ , targetIp := range targetIps {
550- r , ok := h .relayState .QueryRelayForByIp (targetIp )
551- if ok && r .State == Established {
552- return h , r , nil
592+ for _ , targetIp := range targetIps {
593+ r , ok := h .relayState .QueryRelayForByIp (targetIp )
594+ if ok && r .State == Established {
595+ return h , r , nil
596+ }
597+ }
598+
599+ if list , ok := hm .moreHosts [relayHostIp ]; ok {
600+ // list[0] is the primary we already checked
601+ for _ , h := range list [1 :] {
602+ for _ , targetIp := range targetIps {
603+ r , ok := h .relayState .QueryRelayForByIp (targetIp )
604+ if ok && r .State == Established {
605+ return h , r , nil
606+ }
553607 }
554608 }
555- h = h .next
556609 }
557610
558611 return nil , nil , errors .New ("unable to find host with relay" )
559612}
560613
561614func (hm * HostMap ) unlockedDisestablishVpnAddrRelayFor (hi * HostInfo ) {
562615 for _ , relayHostIp := range hi .relayState .CopyRelayIps () {
563- if h , ok := hm .Hosts [relayHostIp ]; ok {
564- for h != nil {
565- h .relayState .UpdateRelayForByIpState (hi .vpnAddrs [0 ], Disestablished )
566- h = h .next
567- }
616+ for _ , h := range hm .unlockedGetHostList (relayHostIp ) {
617+ h .relayState .UpdateRelayForByIpState (hi .vpnAddrs [0 ], Disestablished )
568618 }
569619 }
570620 for _ , rs := range hi .relayState .CopyAllRelayFor () {
571621 if rs .Type == ForwardingType {
572- if h , ok := hm .Hosts [rs .PeerAddr ]; ok {
573- for h != nil {
574- h .relayState .UpdateRelayForByIpState (hi .vpnAddrs [0 ], Disestablished )
575- h = h .next
576- }
622+ for _ , h := range hm .unlockedGetHostList (rs .PeerAddr ) {
623+ h .relayState .UpdateRelayForByIpState (hi .vpnAddrs [0 ], Disestablished )
577624 }
578625 }
579626 }
@@ -623,22 +670,27 @@ func (hm *HostMap) unlockedAddHostInfo(hostinfo *HostInfo, f *Interface) {
623670}
624671
625672func (hm * HostMap ) unlockedInnerAddHostInfo (vpnAddr netip.Addr , hostinfo * HostInfo , f * Interface ) {
626- existing := hm .Hosts [vpnAddr ]
627- hm .Hosts [vpnAddr ] = hostinfo
673+ existing , ok := hm .Hosts [vpnAddr ]
674+ if ! ok {
675+ // Common case, the first hostinfo for this address. moreHosts stays empty.
676+ hm .Hosts [vpnAddr ] = hostinfo
677+ return
678+ }
628679
629- if existing != nil && existing != hostinfo {
630- hostinfo .next = existing
631- existing .prev = hostinfo
680+ // The new hostinfo becomes the primary for this address. Remove any stale copy of it first so
681+ // we never hold a duplicate, then prepend.
682+ list , ok := hm .moreHosts [vpnAddr ]
683+ if ! ok {
684+ list = []* HostInfo {existing }
632685 }
686+ list = removeHostInfo (list , hostinfo )
687+ list = append ([]* HostInfo {hostinfo }, list ... )
688+ hm .unlockedSetHostsForAddr (vpnAddr , list )
633689
634- i := 1
635- check := hostinfo
636- for check != nil {
637- if i > MaxHostInfosPerVpnIp {
638- hm .unlockedDeleteHostInfo (check )
639- }
640- check = check .next
641- i ++
690+ // Enforce the per-address cap by fully retiring the oldest hostinfo once we exceed it.
691+ // Deleting it removes it from all of its addresses and the index maps, matching prior behavior.
692+ if len (list ) > MaxHostInfosPerVpnIp {
693+ hm .unlockedDeleteHostInfo (list [len (list )- 1 ])
642694 }
643695}
644696
0 commit comments