Skip to content

Commit 384610f

Browse files
authored
hostmap: replace the shared next/prev hostinfo chain with independent per-address lists so divergent or overlapping vpnAddr sets cannot corrupt the map (#1790)
1 parent c1eea11 commit 384610f

3 files changed

Lines changed: 397 additions & 292 deletions

File tree

handshake_manager.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -430,14 +430,11 @@ func (hm *HandshakeManager) CheckAndComplete(hostinfo *HostInfo, handshakePacket
430430
// Check if we already have a tunnel with this vpn ip
431431
existingHostInfo, found := hm.mainHostMap.Hosts[hostinfo.vpnAddrs[0]]
432432
if found && existingHostInfo != nil {
433-
testHostInfo := existingHostInfo
434-
for testHostInfo != nil {
435-
// Is it just a delayed handshake packet?
433+
// Is it just a delayed handshake packet? Check every hostinfo we hold for this address.
434+
for _, testHostInfo := range hm.mainHostMap.unlockedGetHostList(hostinfo.vpnAddrs[0]) {
436435
if bytes.Equal(hostinfo.HandshakePacket[handshakePacket], testHostInfo.HandshakePacket[handshakePacket]) {
437436
return testHostInfo, ErrAlreadySeen
438437
}
439-
440-
testHostInfo = testHostInfo.next
441438
}
442439

443440
// Is this a newer handshake?

hostmap.go

Lines changed: 145 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,20 @@ type Relay struct {
5656
}
5757

5858
type 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
386436
func (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

403451
func (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

502544
func (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

561614
func (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

625672
func (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

Comments
 (0)