| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061 |
- package nebula
- import (
- "log/slog"
- "net/netip"
- "testing"
- "time"
- "github.com/gaissmai/bart"
- "github.com/slackhq/nebula/cert"
- "github.com/slackhq/nebula/config"
- "github.com/slackhq/nebula/firewall"
- "github.com/slackhq/nebula/handshake"
- "github.com/slackhq/nebula/header"
- "github.com/slackhq/nebula/overlay/overlaytest"
- "github.com/slackhq/nebula/overlay/tio"
- "github.com/slackhq/nebula/test"
- "github.com/slackhq/nebula/udp"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- )
- var (
- testMyAddr = netip.MustParseAddr("10.0.0.1")
- testPeerAddr = netip.MustParseAddr("10.0.0.2")
- )
- // newTestLaneSet builds a lane set from a real handshake result so the sessions
- // it derives hold usable keys.
- // testLaneEpoch stands in for the base session's epoch. It is deliberately not a
- // value sessionEpoch would hand out early in a test run, so a lane session that
- // went back to drawing its own cannot match it by accident.
- const testLaneEpoch = uint64(0xe90c)
- func newTestLaneSet(t *testing.T, r *handshake.Result, myLanes int, peerPorts, peerBase, peerTxLanes uint32) *laneSet {
- t.Helper()
- r.PeerPortCount = peerPorts
- r.PeerBasePort = peerBase
- r.PeerTxLanes = peerTxLanes
- return newLaneSet(r, myLanes, testLaneEpoch, testMyAddr, testPeerAddr)
- }
- // laneSessionFor derives lane s's session and installs it, standing in for the
- // data-plane call that would normally be the first to need it.
- func laneSessionFor(t *testing.T, ls *laneSet, s int) *ConnectionState {
- t.Helper()
- cs, err := ls.session(s)
- require.NoError(t, err)
- require.NotNil(t, cs)
- return cs
- }
- func newTestLaneHostInfo(t *testing.T, r *handshake.Result, ls *laneSet) *HostInfo {
- t.Helper()
- cs, err := newConnectionStateFromResult(r)
- require.NoError(t, err)
- hi := &HostInfo{
- vpnAddrs: []netip.Addr{testPeerAddr},
- localIndexId: 100,
- remoteIndexId: 200,
- remotes: NewRemoteList([]netip.Addr{testPeerAddr}, nil),
- HandshakePacket: map[uint8][]byte{},
- ConnectionState: cs,
- lanes: ls,
- }
- hi.SetRemote(netip.MustParseAddrPort("192.0.2.1:4242"))
- return hi
- }
- func TestLanePortOffset(t *testing.T) {
- a := netip.MustParseAddr("10.0.0.1")
- b := netip.MustParseAddr("10.0.0.2")
- // Deterministic and in range.
- for _, count := range []uint16{1, 2, 3, 4, 16, 256} {
- o := lanePortOffset(a, b, count)
- assert.Equal(t, o, lanePortOffset(a, b, count), "count %d not deterministic", count)
- assert.Less(t, o, count, "count %d out of range", count)
- }
- assert.Equal(t, uint16(0), lanePortOffset(a, b, 0), "zero port count")
- // The two sides' rotations cancel when port counts match, preserving the
- // lane-i-reverses-lane-j conntrack pairing.
- for _, count := range []uint16{2, 3, 4, 7, 16} {
- for i := range 32 {
- peer := netip.AddrFrom4([4]byte{192, 0, 2, byte(i)})
- oA := lanePortOffset(a, peer, count)
- oB := lanePortOffset(peer, a, count)
- assert.Equal(t, uint16(0), (oA+oB)%count,
- "offsets don't cancel for peer %s count %d", peer, count)
- }
- }
- // Distinct small peers land on distinct rotations of a big peer's range,
- // not all on the same first ports.
- const bigPeerPorts = 16
- distinct := map[uint16]struct{}{}
- for i := range 64 {
- client := netip.AddrFrom4([4]byte{192, 0, 2, byte(i)})
- distinct[lanePortOffset(client, a, bigPeerPorts)] = struct{}{}
- }
- assert.GreaterOrEqual(t, len(distinct), 8, "64 clients only produced %d distinct offsets", len(distinct))
- }
- func TestLaneTargetPort(t *testing.T) {
- ls := &laneSet{peerBasePort: 4242, peerPortCount: 4}
- // No rotation: lane i targets base+i, wrapping past the peer's range.
- for i, want := range map[int]uint16{1: 4243, 2: 4244, 3: 4245, 5: 4243} {
- assert.Equal(t, want, ls.laneTargetPortLocked(i), "lane %d", i)
- }
- // Rotation shifts the whole mapping; the wrapped lane lands on the base
- // port itself, which is a valid distinct 4-tuple (our source port differs).
- ls.portOffset = 3
- for i, want := range map[int]uint16{1: 4242, 2: 4243, 3: 4244} {
- assert.Equal(t, want, ls.laneTargetPortLocked(i), "rotated lane %d", i)
- }
- // Fewer peer ports than local lanes: rotation still spreads across all of
- // the peer's ports.
- ls = &laneSet{peerBasePort: 4242, peerPortCount: 2, portOffset: 1}
- assert.Equal(t, uint16(4242), ls.laneTargetPortLocked(1))
- assert.Equal(t, uint16(4243), ls.laneTargetPortLocked(2))
- }
- // A lane's keys are derived, not negotiated, so the whole design rests on the
- // two sides landing on the same pair without exchanging anything.
- func TestLaneKeyDerivationSymmetry(t *testing.T) {
- initR, respR := runTestHandshake(t)
- initLS := newTestLaneSet(t, initR, 4, 4, 4242, 4)
- respLS := newTestLaneSet(t, respR, 4, 4, 4242, 4)
- require.Len(t, initLS.sessions, 4)
- assert.Nil(t, initLS.sessions[0].Load(), "lane 0 is the base session, not a derived one")
- nb := make([]byte, 12)
- for s := 1; s < 4; s++ {
- out := header.EncodeLane(make([]byte, 0, mtu), header.Version, header.Message, 0, 200, 1, uint8(s))
- ct, err := laneSessionFor(t, initLS, s).eKey.EncryptDanger(out, out, []byte("lane payload"), 1, nb)
- require.NoError(t, err)
- pt, err := laneSessionFor(t, respLS, s).Decrypt(test.NewLogger(), 1, ct, nb)
- require.NoError(t, err, "lane %d keys did not match", s)
- assert.Equal(t, []byte("lane payload"), pt)
- }
- // Distinct lanes get distinct keys: lane 2's session must not open lane 1's
- // ciphertext, or the header's lane index would be forgeable in effect.
- out := header.EncodeLane(make([]byte, 0, mtu), header.Version, header.Message, 0, 200, 7, 1)
- ct, err := laneSessionFor(t, initLS, 1).eKey.EncryptDanger(out, out, []byte("lane payload"), 7, nb)
- require.NoError(t, err)
- _, err = laneSessionFor(t, respLS, 2).Decrypt(test.NewLogger(), 7, ct, nb)
- assert.Error(t, err)
- }
- func TestNewLaneSetSizing(t *testing.T) {
- initR, _ := runTestHandshake(t)
- // A peer with no multiport advert gets no lanes at all.
- assert.Nil(t, newLaneSet(&handshake.Result{}, 4, testLaneEpoch, testMyAddr, testPeerAddr))
- // One lane means only the base tunnel, which is not a lane set.
- assert.Nil(t, newLaneSet(&handshake.Result{PeerPortCount: 4, PeerTxLanes: 1}, 1, testLaneEpoch, testMyAddr, testPeerAddr))
- // Sessions cover both directions: enough for everything the peer may send,
- // even though we may only send on a few.
- ls := newTestLaneSet(t, initR, 2, 8, 4242, 6)
- assert.Len(t, ls.sessions, 6, "sessions must cover the peer's tx lanes")
- assert.Equal(t, 2, ls.txLanes, "we may only send on our own lanes")
- // The peer's advert sizes the session table but nothing else. Its lane count
- // is its own choice, so it must not be able to make us allocate per-lane tx
- // state we will never use.
- assert.Len(t, ls.txAddr, 2)
- assert.Len(t, ls.demand, 2)
- assert.Len(t, ls.probe, 2)
- // Nothing is derived up front, for the same reason.
- for s := range ls.sessions {
- assert.Nil(t, ls.sessions[s].Load(), "lane %d derived before anything needed it", s)
- }
- // Every lane starts demanded so the first traffic tick probes them all: a
- // lane that only comes up after its flows do is a lane the flows never move
- // onto, because the kernel has pinned them to the queue they started on.
- ls = newTestLaneSet(t, initR, 4, 4, 4242, 4)
- for s := 1; s < ls.txLanes; s++ {
- assert.True(t, ls.demand[s].Load(), "lane %d not demanded at creation", s)
- }
- // Our tx lanes are clamped to the ports the peer actually bound: a lane
- // aimed past the peer's range would land on some unrelated socket.
- ls = newTestLaneSet(t, initR, 8, 3, 4242, 8)
- assert.Equal(t, 3, ls.txLanes)
- assert.Len(t, ls.sessions, 8)
- }
- // The handshake log lines carry the negotiated lanes, so an operator can tell a
- // peer that got none from one that was never asked.
- func TestLaneLogAttr(t *testing.T) {
- initR, _ := runTestHandshake(t)
- // Not running multiport: an empty attr, which slog drops entirely.
- assert.Equal(t, slog.Attr{}, laneLogAttr(0, nil))
- // Running multiport against a peer that isn't: zeros, not silence.
- assert.Equal(t, m{"tx": 0, "sessions": 0}, laneLogAttr(4, nil).Value.Any())
- ls := newTestLaneSet(t, initR, 2, 8, 4242, 6)
- attr := laneLogAttr(2, ls)
- assert.Equal(t, "lanes", attr.Key)
- assert.Equal(t, m{
- "tx": 2,
- "sessions": 6,
- "peerBasePort": uint16(4242),
- "peerPorts": uint16(8),
- "portOffset": ls.portOffset,
- }, attr.Value.Any())
- }
- // The RX path must not cache a session for a lane until a packet on it has
- // actually decrypted, or a spoofer naming lanes at random could make us hold a
- // replay window and two cipher states per lane without authenticating anything.
- func TestLaneSessionRxDerivation(t *testing.T) {
- initR, respR := runTestHandshake(t)
- respLS := newTestLaneSet(t, respR, 4, 4, 4242, 4)
- hi := newTestLaneHostInfo(t, respR, respLS)
- ci, cached, err := hi.laneSession(2)
- require.NoError(t, err)
- require.NotNil(t, ci)
- assert.False(t, cached, "the first packet on a lane derives, it does not hit")
- assert.Nil(t, respLS.sessions[2].Load(), "an unauthenticated packet must not install a session")
- // The lane the peer really is using decrypts, and that is what installs it.
- nb := make([]byte, 12)
- out := header.EncodeLane(make([]byte, 0, mtu), header.Version, header.Message, 0, 200, 1, 2)
- ct, err := laneSessionFor(t, newTestLaneSet(t, initR, 4, 4, 4242, 4), 2).
- eKey.EncryptDanger(out, out, []byte("real lane traffic"), 1, nb)
- require.NoError(t, err)
- pt, err := ci.Decrypt(test.NewLogger(), 1, ct, nb)
- require.NoError(t, err)
- assert.Equal(t, []byte("real lane traffic"), pt)
- assert.Same(t, ci, respLS.installSession(test.NewLogger(), 2, ci, 1))
- assert.Same(t, ci, respLS.sessions[2].Load())
- // Now it is a hit, and the replay window the decrypt above advanced is the
- // one the next packet sees.
- got, cached, err := hi.laneSession(2)
- require.NoError(t, err)
- assert.True(t, cached)
- assert.Same(t, ci, got)
- // A racing install loses rather than swapping the session out, which would
- // throw away the replay window the live one has been accumulating. The loser's
- // packet still has to be marked seen on the winner, or dropping its session
- // would make that one counter replayable.
- other, err := newLaneConnectionState(&respLS.material, 2)
- require.NoError(t, err)
- require.True(t, ci.window.Check(test.NewLogger(), 7), "counter 7 seen before the race")
- assert.Same(t, ci, respLS.installSession(test.NewLogger(), 2, other, 7),
- "the loser must be handed the installed session, never left holding its own")
- assert.Same(t, ci, respLS.sessions[2].Load())
- assert.False(t, ci.window.Check(test.NewLogger(), 7),
- "the loser's counter was not carried to the surviving window")
- }
- // Every session on a tunnel reports the tunnel's epoch. The RX staging sort keys
- // off (epoch, counter) and lanes derive lazily, so a lane drawing its own ordinal
- // would be ordered by when its first packet landed: during a rehandshake cutover
- // a late-derived lane of the *old* tunnel would outrank the new tunnel and sort
- // its packets last, the exact inversion the epoch exists to prevent.
- func TestLaneSessionsShareTheTunnelEpoch(t *testing.T) {
- initR, _ := runTestHandshake(t)
- ls := newTestLaneSet(t, initR, 4, 4, 4242, 4)
- for s := 1; s < 4; s++ {
- // Move the global on between derivations, so a session that drew its own
- // would land on a different value for every lane.
- sessionEpoch.Add(1)
- cs, err := ls.session(s)
- require.NoError(t, err)
- assert.Equal(t, testLaneEpoch, cs.epoch, "lane %d did not inherit the tunnel's epoch", s)
- }
- }
- func TestLaneTxGate(t *testing.T) {
- initR, _ := runTestHandshake(t)
- ls := newTestLaneSet(t, initR, 4, 4, 4242, 4)
- for s := range ls.demand {
- // Creation seeds demand on every lane; start from cold so this test can
- // tell what the TX path itself raises.
- ls.demand[s].Store(false)
- }
- // A down lane hands back nothing and raises demand, which is what gets it
- // re-probed after a demotion.
- ci, addr := ls.txLane(1)
- assert.Nil(t, ci)
- assert.False(t, addr.IsValid())
- assert.True(t, ls.demand[1].Load())
- assert.False(t, ls.demand[2].Load(), "demand raised on an untouched lane")
- // An up lane with no session derived yet cannot happen — the probe that
- // promoted it derived one — but it must fall back rather than send in the
- // clear if it ever does.
- want := netip.MustParseAddrPort("192.0.2.1:4243")
- ls.txAddr[1].Store(&want)
- ci, _ = ls.txLane(1)
- assert.Nil(t, ci)
- // Promotion publishes the session and destination together.
- sess := laneSessionFor(t, ls, 1)
- ls.demand[1].Store(false)
- ci, addr = ls.txLane(1)
- assert.Same(t, sess, ci)
- assert.Equal(t, want, addr)
- assert.False(t, ls.demand[1].Load(), "a hit must not raise demand")
- // Lane 0 is the base tunnel and lanes at or above txLanes are receive-only.
- ci, _ = ls.txLane(0)
- assert.Nil(t, ci)
- ci, _ = ls.txLane(4)
- assert.Nil(t, ci)
- }
- func TestLaneSessionLookup(t *testing.T) {
- initR, _ := runTestHandshake(t)
- ls := newTestLaneSet(t, initR, 3, 4, 4242, 3)
- hi := newTestLaneHostInfo(t, initR, ls)
- assertNoLane := func(s uint8, msg string) {
- t.Helper()
- ci, cached, err := hi.laneSession(s)
- require.NoError(t, err)
- assert.Nil(t, ci, msg)
- assert.False(t, cached, msg)
- }
- assertNoLane(0, "lane 0 is the base session")
- assertNoLane(3, "a lane beyond what this tunnel covers")
- assertNoLane(255, "a lane beyond what this tunnel covers")
- // An already-derived lane is returned as a hit.
- sess := laneSessionFor(t, ls, 2)
- ci, cached, err := hi.laneSession(2)
- require.NoError(t, err)
- assert.Same(t, sess, ci)
- assert.True(t, cached)
- // A peer without lanes answers nil for every lane rather than panicking.
- bare := &HostInfo{}
- ci, _, err = bare.laneSession(1)
- require.NoError(t, err)
- assert.Nil(t, ci)
- }
- func TestMaxMessageCounter(t *testing.T) {
- initR, _ := runTestHandshake(t)
- ls := newTestLaneSet(t, initR, 3, 4, 4242, 3)
- hi := newTestLaneHostInfo(t, initR, ls)
- hi.ConnectionState.messageCounter.Store(5)
- assert.Equal(t, uint64(5), hi.maxMessageCounter())
- // A lane past the base is what the rehandshake threshold has to notice: the
- // base counter would sit still while the lane burns through its nonces.
- laneSessionFor(t, ls, 2).messageCounter.Store(9000)
- assert.Equal(t, uint64(9000), hi.maxMessageCounter())
- assert.Equal(t, uint64(0), (&HostInfo{}).maxMessageCounter())
- }
- func TestLaneRetryDelay(t *testing.T) {
- assert.Equal(t, laneRetryBase, laneRetryDelay(0))
- assert.Equal(t, 2*laneRetryBase, laneRetryDelay(1))
- assert.Equal(t, laneRetryMax, laneRetryDelay(laneMaxFails), "backoff must saturate")
- }
- func newLaneTestInterface(hostMap *HostMap) *Interface {
- l := test.NewLogger()
- lh := newTestLighthouse()
- cs := &CertState{
- initiatingVersion: cert.Version1,
- privateKey: []byte{},
- v1Cert: &dummyCert{version: cert.Version1},
- v1Credential: nil,
- }
- ifce := &Interface{
- hostMap: hostMap,
- inside: &overlaytest.NoopTun{},
- outside: &udp.NoopConn{},
- firewall: &Firewall{},
- lightHouse: lh,
- pki: &PKI{},
- handshakeManager: NewHandshakeManager(l, hostMap, lh, &udp.NoopConn{}, defaultHandshakeConfig),
- myVpnNetworksTable: new(bart.Lite),
- messageMetrics: newMessageMetricsOnlyRecvError(),
- writers: []udp.Conn{&udp.NoopConn{}, &udp.NoopConn{}, &udp.NoopConn{}, &udp.NoopConn{}},
- // One socket per port keeps writers indexed by lane, which is what most of
- // these tests want; the tests that care about the group layout set their own.
- routinesPerPort: 1,
- l: l,
- }
- ifce.pki.cs.Store(cs)
- conf := config.NewC(l)
- punchy := NewPunchyFromConfig(l, conf, nil)
- cm := newConnectionManagerFromConfig(l, conf, hostMap, punchy)
- cm.intf = ifce
- ifce.connectionManager = cm
- ifce.handshakeManager.f = ifce
- return ifce
- }
- // The full TX lifecycle of a lane: demand -> probe -> ack -> up, then an
- // unanswered keepalive -> demoted.
- func TestLaneProbeLifecycle(t *testing.T) {
- hostMap := newHostMap(test.NewLogger())
- ifce := newLaneTestInterface(hostMap)
- initR, _ := runTestHandshake(t)
- ls := newTestLaneSet(t, initR, 4, 4, 5353, 4)
- hi := newTestLaneHostInfo(t, initR, ls)
- nb := make([]byte, 12)
- out := make([]byte, mtu)
- now := time.Now()
- // No demand: nothing is probed, so a peer we barely talk to costs nothing
- // beyond its base tunnel. (Creation seeds demand on every lane, so clear it
- // to get at the no-demand case.)
- for s := range ls.demand {
- ls.demand[s].Store(false)
- }
- ifce.probeLanes(hi, now, nb, out)
- ls.mu.Lock()
- for s := 1; s < 4; s++ {
- assert.True(t, ls.probe[s].sentAt.IsZero(), "lane %d probed without demand", s)
- }
- ls.mu.Unlock()
- // Demand on lane 1 alone probes lane 1 alone, aimed at the peer's port for
- // that lane.
- ls.demand[1].Store(true)
- ifce.probeLanes(hi, now, nb, out)
- ls.mu.Lock()
- require.False(t, ls.probe[1].sentAt.IsZero(), "demand did not produce a probe")
- assert.True(t, ls.probe[2].sentAt.IsZero())
- wantPort := ls.laneTargetPortLocked(1)
- assert.Equal(t, netip.AddrPortFrom(netip.MustParseAddr("192.0.2.1"), wantPort), ls.probe[1].target)
- gen := ls.probe[1].gen
- assert.False(t, ls.demand[1].Load(), "the probe did not consume the demand")
- ls.mu.Unlock()
- // The lane stays down until the ack lands, and a stale generation cannot
- // bring it up.
- assert.Nil(t, ls.txAddr[1].Load())
- _, promoted := ls.noteAck(1, gen+1, now)
- assert.False(t, promoted, "an ack for a superseded probe promoted the lane")
- assert.Nil(t, ls.txAddr[1].Load())
- // The matching ack promotes it, and the destination is the probed target.
- ackTarget, promoted := ls.noteAck(1, gen, now)
- assert.True(t, promoted)
- assert.Equal(t, netip.AddrPortFrom(netip.MustParseAddr("192.0.2.1"), wantPort), ackTarget)
- addr := ls.txAddr[1].Load()
- require.NotNil(t, addr)
- assert.Equal(t, netip.AddrPortFrom(netip.MustParseAddr("192.0.2.1"), wantPort), *addr)
- // A second ack is a keepalive, not a promotion.
- ls.mu.Lock()
- ls.probe[1].sentAt = now
- ls.mu.Unlock()
- _, promoted = ls.noteAck(1, gen, now)
- assert.False(t, promoted)
- assert.NotNil(t, ls.txAddr[1].Load())
- // An up lane is left alone until the keepalive comes due.
- ifce.probeLanes(hi, now, nb, out)
- ls.mu.Lock()
- assert.True(t, ls.probe[1].sentAt.IsZero(), "an up lane was re-probed early")
- ls.mu.Unlock()
- // Past the keepalive it re-proves its path...
- now = now.Add(laneKeepalive + time.Second)
- ifce.probeLanes(hi, now, nb, out)
- ls.mu.Lock()
- require.False(t, ls.probe[1].sentAt.IsZero(), "keepalive did not probe")
- ls.mu.Unlock()
- assert.NotNil(t, ls.txAddr[1].Load(), "lane demoted before its probe aged out")
- // ...and an unanswered keepalive demotes it with backoff, so the routine
- // falls back to the base tunnel.
- now = now.Add(laneProbeTimeout + time.Second)
- ifce.probeLanes(hi, now, nb, out)
- assert.Nil(t, ls.txAddr[1].Load(), "unanswered keepalive did not demote the lane")
- ls.mu.Lock()
- assert.Equal(t, uint8(1), ls.probe[1].fails)
- assert.True(t, ls.probe[1].retryAt.After(now))
- ls.mu.Unlock()
- // The backoff holds even with fresh demand.
- ls.demand[1].Store(true)
- ifce.probeLanes(hi, now, nb, out)
- ls.mu.Lock()
- assert.True(t, ls.probe[1].sentAt.IsZero(), "backoff was ignored")
- ls.mu.Unlock()
- }
- // A lane aims at its own peer port and nothing else. There is no fallback to the
- // peer's base port: sharing that destination would cost the peer the receive
- // spread lanes exist to create, so an unreachable lane port just means this lane
- // stays down and its flows ride the base tunnel.
- func TestLaneProbeAlwaysTargetsItsOwnPort(t *testing.T) {
- hostMap := newHostMap(test.NewLogger())
- ifce := newLaneTestInterface(hostMap)
- initR, _ := runTestHandshake(t)
- ls := newTestLaneSet(t, initR, 4, 4, 5353, 4)
- hi := newTestLaneHostInfo(t, initR, ls)
- nb := make([]byte, 12)
- out := make([]byte, mtu)
- now := time.Now()
- ls.mu.Lock()
- lanePort := ls.laneTargetPortLocked(1)
- ls.mu.Unlock()
- require.NotEqual(t, ls.peerBasePort, lanePort, "this test needs a lane that isn't aimed at the base port")
- // Repeated failures never move the target off the lane's own port.
- for i := 0; i < 4; i++ {
- ls.demand[1].Store(true)
- ifce.probeLanes(hi, now, nb, out)
- ls.mu.Lock()
- require.False(t, ls.probe[1].sentAt.IsZero(), "the lane was not probed")
- assert.Equal(t, lanePort, ls.probe[1].target.Port(), "probe %d left the lane port", i)
- gen := ls.probe[1].gen
- ls.mu.Unlock()
- // Age the probe out, then wait out the backoff for the next attempt.
- now = now.Add(laneProbeTimeout + time.Second)
- ifce.probeLanes(hi, now, nb, out)
- require.Nil(t, ls.txAddr[1].Load())
- assert.False(t, promotedByAck(ls, 1, gen, now), "an aged-out probe was still promotable")
- now = now.Add(laneRetryMax + time.Second)
- }
- // It comes up on that port, and the keepalive re-proves the same one.
- ls.demand[1].Store(true)
- ifce.probeLanes(hi, now, nb, out)
- ls.mu.Lock()
- gen := ls.probe[1].gen
- ls.mu.Unlock()
- target, promoted := ls.noteAck(1, gen, now)
- require.True(t, promoted)
- assert.Equal(t, lanePort, target.Port())
- now = now.Add(laneKeepalive + time.Second)
- ifce.probeLanes(hi, now, nb, out)
- ls.mu.Lock()
- require.False(t, ls.probe[1].sentAt.IsZero(), "the keepalive did not probe")
- assert.Equal(t, lanePort, ls.probe[1].target.Port(), "the keepalive left the lane port")
- ls.mu.Unlock()
- // And a demotion returns to probing that same port.
- now = now.Add(laneProbeTimeout + time.Second)
- ifce.probeLanes(hi, now, nb, out)
- require.Nil(t, ls.txAddr[1].Load(), "the unanswered keepalive did not demote the lane")
- now = now.Add(laneRetryMax + time.Second)
- ls.demand[1].Store(true)
- ifce.probeLanes(hi, now, nb, out)
- ls.mu.Lock()
- require.False(t, ls.probe[1].sentAt.IsZero(), "the lane was not retried")
- assert.Equal(t, lanePort, ls.probe[1].target.Port())
- ls.mu.Unlock()
- }
- // promotedByAck is noteAck's boolean alone, for assertions that only care whether
- // an ack could bring the lane up.
- func promotedByAck(ls *laneSet, s int, gen uint8, now time.Time) bool {
- _, promoted := ls.noteAck(s, gen, now)
- return promoted
- }
- // A roam is a new path with no derivable relationship to the old lane ports, so
- // every lane has to be rebuilt rather than moved.
- func TestLaneProbeRoamResets(t *testing.T) {
- hostMap := newHostMap(test.NewLogger())
- ifce := newLaneTestInterface(hostMap)
- initR, _ := runTestHandshake(t)
- ls := newTestLaneSet(t, initR, 4, 4, 5353, 4)
- hi := newTestLaneHostInfo(t, initR, ls)
- now := time.Now()
- nb := make([]byte, 12)
- out := make([]byte, mtu)
- ls.demand[1].Store(true)
- ifce.probeLanes(hi, now, nb, out)
- ls.mu.Lock()
- gen := ls.probe[1].gen
- ls.mu.Unlock()
- _, promoted := ls.noteAck(1, gen, now)
- require.True(t, promoted)
- require.NotNil(t, ls.txAddr[1].Load())
- // New remote address: the lane is taken down, not retargeted.
- hi.SetRemote(netip.MustParseAddrPort("198.51.100.7:4242"))
- ifce.probeLanes(hi, now, nb, out)
- assert.Nil(t, ls.txAddr[1].Load(), "lane survived a roam")
- ls.mu.Lock()
- assert.Zero(t, ls.probe[1].fails, "a roam is not a lane failure")
- assert.Equal(t, netip.MustParseAddr("198.51.100.7"), ls.peerAddr)
- ls.mu.Unlock()
- // Relayed (no direct remote) means no lanes at all.
- ls.demand[1].Store(true)
- ifce.probeLanes(hi, now, nb, out)
- ls.mu.Lock()
- gen = ls.probe[1].gen
- ls.mu.Unlock()
- _, promoted = ls.noteAck(1, gen, now)
- require.True(t, promoted)
- hi.SetRemote(netip.AddrPort{})
- ifce.probeLanes(hi, now, nb, out)
- assert.Nil(t, ls.txAddr[1].Load(), "lane survived losing the direct path")
- }
- // A lane probe is answered on the base tunnel, echoing the header's lane, so a
- // peer cannot get us to vouch for a lane it never probed.
- func TestHandleLaneProbe(t *testing.T) {
- hostMap := newHostMap(test.NewLogger())
- ifce := newLaneTestInterface(hostMap)
- initR, respR := runTestHandshake(t)
- ls := newTestLaneSet(t, respR, 4, 4, 4242, 4)
- hi := newTestLaneHostInfo(t, respR, ls)
- rxc := &rxContext{nb: make([]byte, 12), scratch: make([]byte, mtu)}
- sent := &recordingUdpConn{}
- ifce.writers = []udp.Conn{sent, &udp.NoopConn{}, &udp.NoopConn{}, &udp.NoopConn{}}
- // The payload's lane is ignored in favour of the header's.
- ifce.handleLaneProbe(hi, 2, []byte{3, 42}, rxc)
- require.Len(t, sent.bufs, 1, "the ack must ride the base tunnel's socket")
- h := &header.H{}
- require.NoError(t, h.Parse(sent.bufs[0]))
- assert.Equal(t, header.Test, h.Type)
- assert.Equal(t, header.LaneProbeAck, h.Subtype)
- assert.Equal(t, uint8(0), h.Lane(), "the ack is base-tunnel traffic")
- pt, err := laneSessionFor(t, newTestLaneSet(t, initR, 4, 4, 4242, 4), 1).dKey.DecryptDanger(
- nil, sent.bufs[0][:header.Len], sent.bufs[0][header.Len:], h.MessageCounter, make([]byte, 12))
- _ = pt
- assert.Error(t, err, "the ack must not be readable with a lane key")
- // A probe claiming lane 0 or with a truncated payload is answered with
- // nothing at all.
- sent.bufs = nil
- ifce.handleLaneProbe(hi, 0, []byte{1, 2}, rxc)
- ifce.handleLaneProbe(hi, 1, []byte{1}, rxc)
- assert.Empty(t, sent.bufs)
- }
- func TestHandleLaneProbeAck(t *testing.T) {
- hostMap := newHostMap(test.NewLogger())
- ifce := newLaneTestInterface(hostMap)
- initR, _ := runTestHandshake(t)
- ls := newTestLaneSet(t, initR, 4, 4, 5353, 4)
- hi := newTestLaneHostInfo(t, initR, ls)
- now := time.Now()
- ls.demand[1].Store(true)
- ifce.probeLanes(hi, now, make([]byte, 12), make([]byte, mtu))
- ls.mu.Lock()
- gen := ls.probe[1].gen
- ls.mu.Unlock()
- // A short or out-of-range ack is ignored, and a peer without lanes does not
- // panic the handler.
- ifce.handleLaneProbeAck(hi, []byte{1})
- ifce.handleLaneProbeAck(hi, []byte{99, gen})
- ifce.handleLaneProbeAck(&HostInfo{}, []byte{1, gen})
- assert.Nil(t, ls.txAddr[1].Load())
- ifce.handleLaneProbeAck(hi, []byte{1, gen})
- assert.NotNil(t, ls.txAddr[1].Load())
- }
- // recordingUdpConn records what was written to it, one datagram per write.
- type recordingUdpConn struct {
- udp.NoopConn
- bufs [][]byte
- dsts []netip.AddrPort
- }
- func (c *recordingUdpConn) WriteTo(b []byte, addr netip.AddrPort) error {
- c.bufs = append(c.bufs, append([]byte(nil), b...))
- c.dsts = append(c.dsts, addr)
- return nil
- }
- // recordingBatchConn is a udp.Conn that records the batches flushed to it, so a
- // test can see which socket a packet left on.
- type recordingBatchConn struct {
- udp.NoopConn
- bufs [][]byte
- dsts []netip.AddrPort
- }
- func (c *recordingBatchConn) WriteBatch(bufs [][]byte, addrs []netip.AddrPort) (int, error) {
- for i := range bufs {
- c.bufs = append(c.bufs, append([]byte(nil), bufs[i]...))
- c.dsts = append(c.dsts, addrs[i])
- }
- return len(bufs), nil
- }
- // laneOfFlow is what the TX path computes to pick a lane, without needing a
- // promoted lane to observe it.
- func laneOfFlow(ls *laneSet, p *firewall.Packet) int {
- return int((laneFlowHash(p) + uint32(ls.laneBias)) % uint32(ls.txLanes))
- }
- func testFlow(port uint16) *firewall.Packet {
- return &firewall.Packet{
- LocalAddr: testMyAddr,
- RemoteAddr: testPeerAddr,
- LocalPort: port,
- RemotePort: 443,
- Protocol: 6,
- }
- }
- // flowForLane finds a flow that hashes onto lane s, so a test can aim traffic at
- // a specific lane.
- func flowForLane(t *testing.T, ls *laneSet, s int) *firewall.Packet {
- t.Helper()
- for port := uint16(1024); port < 4096; port++ {
- p := testFlow(port)
- if laneOfFlow(ls, p) == s {
- return p
- }
- }
- t.Fatalf("no flow hashes onto lane %d", s)
- return nil
- }
- func TestSendInsideMessageLaneSwap(t *testing.T) {
- hostMap := newHostMap(test.NewLogger())
- ifce := newLaneTestInterface(hostMap)
- ifce.multiport = true
- ifce.laneCount = 4
- conns := []*recordingBatchConn{{}, {}, {}, {}}
- ifce.writers = []udp.Conn{conns[0], conns[1], conns[2], conns[3]}
- initR, respR := runTestHandshake(t)
- ls := newTestLaneSet(t, initR, 4, 4, 5353, 4)
- hi := newTestLaneHostInfo(t, initR, ls)
- peerLS := newTestLaneSet(t, respR, 4, 4, 4242, 4)
- tx := ifce.newTxQueue(1)
- pkt := tio.Packet{Bytes: []byte{0x45, 0, 0, 4, 1, 2, 3, 4}}
- nb := make([]byte, 12)
- flow1 := flowForLane(t, ls, 1)
- for s := range ls.demand {
- ls.demand[s].Store(false)
- }
- // A down lane rides the base tunnel and asks for a probe.
- ifce.sendInsideMessage(hi, pkt, flow1, nb, tx)
- tx.flush(ifce)
- require.Len(t, conns[0].bufs, 1)
- assert.Empty(t, conns[1].bufs)
- assert.True(t, ls.demand[1].Load(), "a miss did not raise demand")
- assert.False(t, ls.demand[2].Load(), "demand raised on a lane nothing hashed onto")
- h := &header.H{}
- require.NoError(t, h.Parse(conns[0].bufs[0]))
- assert.Equal(t, uint8(0), h.Lane())
- // Once the lane is up, a flow hashing onto it rides the lane session, the
- // lane socket and the lane's destination, tagged with the lane index.
- // Promotion normally happens on the ack of a probe, which is also what
- // derived the session, so stand both up here.
- laneRemote := netip.MustParseAddrPort("192.0.2.1:5354")
- laneSessionFor(t, ls, 1)
- ls.txAddr[1].Store(&laneRemote)
- ls.demand[1].Store(false)
- ifce.sendInsideMessage(hi, pkt, flow1, nb, tx)
- tx.flush(ifce)
- require.Len(t, conns[1].bufs, 1)
- assert.Equal(t, laneRemote, conns[1].dsts[0])
- assert.False(t, ls.demand[1].Load(), "a hit raised demand")
- require.NoError(t, h.Parse(conns[1].bufs[0]))
- assert.Equal(t, uint8(1), h.Lane())
- assert.Equal(t, hi.remoteIndexId, h.RemoteIndex)
- // And the peer's derived lane-1 session is what opens it.
- pt, err := laneSessionFor(t, peerLS, 1).Decrypt(test.NewLogger(), h.MessageCounter, conns[1].bufs[0], nb)
- require.NoError(t, err)
- assert.Equal(t, pkt.Bytes, pt)
- // Another routine sending the same flow rides the same lane socket and
- // session: the lane follows the flow, not the routine.
- tx2 := ifce.newTxQueue(2)
- ifce.sendInsideMessage(hi, pkt, flow1, nb, tx2)
- tx2.flush(ifce)
- require.Len(t, conns[1].bufs, 2)
- require.NoError(t, h.Parse(conns[1].bufs[1]))
- assert.Equal(t, uint8(1), h.Lane())
- // A flow that hashes onto lane 0 stays on the base tunnel even with lane 1
- // up: a full share of flows belongs there.
- ifce.sendInsideMessage(hi, pkt, flowForLane(t, ls, 0), nb, tx)
- tx.flush(ifce)
- require.Len(t, conns[0].bufs, 2)
- assert.Equal(t, hi.GetRemote(), conns[0].dsts[1])
- require.NoError(t, h.Parse(conns[0].bufs[1]))
- assert.Equal(t, uint8(0), h.Lane())
- // Demotion falls back to the base tunnel on the very next packet.
- ls.txAddr[1].Store(nil)
- ifce.sendInsideMessage(hi, pkt, flow1, nb, tx)
- tx.flush(ifce)
- require.Len(t, conns[0].bufs, 3)
- require.Len(t, conns[1].bufs, 2)
- }
- // Without multiport nothing about the send path changes: every socket shares one
- // port under SO_REUSEPORT, so a routine keeps writing to its own, and there are
- // no lane batches at all. A peer that negotiated no lanes is the same story on a
- // node that does run multiport, except that base traffic owes its 4-tuple to
- // socket 0.
- func TestTxQueueWithoutLanes(t *testing.T) {
- hostMap := newHostMap(test.NewLogger())
- ifce := newLaneTestInterface(hostMap)
- conns := []*recordingBatchConn{{}, {}, {}, {}}
- ifce.writers = []udp.Conn{conns[0], conns[1], conns[2], conns[3]}
- initR, _ := runTestHandshake(t)
- hi := newTestLaneHostInfo(t, initR, nil)
- pkt := tio.Packet{Bytes: []byte{0x45, 0, 0, 4, 1, 2, 3, 4}}
- nb := make([]byte, 12)
- flow := testFlow(1234)
- tx := ifce.newTxQueue(2)
- assert.Empty(t, tx.lane, "a queue with no lanes must not hold lane batches")
- require.Len(t, tx.live, 1)
- ifce.sendInsideMessage(hi, pkt, flow, nb, tx)
- tx.flush(ifce)
- assert.Len(t, conns[2].bufs, 1, "routine 2 did not send on its own socket")
- assert.Empty(t, conns[0].bufs)
- // Multiport on, but this peer has no lanes: socket 0, lane 0, one batch.
- ifce.multiport = true
- ifce.laneCount = 4
- tx = ifce.newTxQueue(2)
- require.Len(t, tx.live, 1)
- ifce.sendInsideMessage(hi, pkt, flow, nb, tx)
- tx.flush(ifce)
- require.Len(t, conns[0].bufs, 1, "base traffic left a socket other than 0")
- assert.Len(t, conns[2].bufs, 1)
- h := &header.H{}
- require.NoError(t, h.Parse(conns[0].bufs[0]))
- assert.Equal(t, uint8(0), h.Lane())
- assert.Len(t, tx.live, 1, "a lane batch was built for a peer with no lanes")
- }
- // `routines` is per port, so a port's sockets are a group sharing it through
- // SO_REUSEPORT and every routine has a sibling of its own on every port. Pin the
- // arithmetic that turns (queue, lane) into one of them.
- func TestLaneSockGroupLayout(t *testing.T) {
- hostMap := newHostMap(test.NewLogger())
- ifce := newLaneTestInterface(hostMap)
- // Two routines per port over three ports: writers[s*2+r] is the r'th socket
- // on port listen.port+s.
- ifce.routinesPerPort = 2
- ifce.routines = 6
- ifce.laneCount = 3
- // Off, every socket is on the one port, so a routine keeps writing to its own
- // and any of them can carry any lane.
- for q := 0; q < ifce.routines; q++ {
- assert.Equal(t, q, ifce.egressSock(q))
- for s := 0; s < ifce.laneCount; s++ {
- assert.Equal(t, q, ifce.laneSock(q, s))
- }
- }
- ifce.multiport = true
- for q := 0; q < ifce.routines; q++ {
- // Base traffic is lane 0's port, and each routine has its own socket
- // there rather than sharing one with the other five.
- assert.Equal(t, q%2, ifce.egressSock(q), "queue %d", q)
- for s := 0; s < ifce.laneCount; s++ {
- assert.Equal(t, s*2+q%2, ifce.laneSock(q, s), "queue %d lane %d", q, s)
- }
- }
- // Sibling routines land on different sockets of the same port, so a lane's
- // port is served by its whole group and not by one socket.
- assert.NotEqual(t, ifce.laneSock(0, 2), ifce.laneSock(1, 2))
- assert.Equal(t, ifce.laneSock(0, 2), ifce.laneSock(2, 2), "queues 0 and 2 share a group position")
- }
- // The lane batches a routine builds must flush to that routine's own sockets, not
- // to whatever socket happens to be indexed by the lane.
- func TestTxQueueLaneBatchesFollowTheGroup(t *testing.T) {
- hostMap := newHostMap(test.NewLogger())
- ifce := newLaneTestInterface(hostMap)
- ifce.multiport = true
- ifce.routinesPerPort = 2
- ifce.routines = 4
- ifce.laneCount = 2
- conns := []*recordingBatchConn{{}, {}, {}, {}}
- ifce.writers = []udp.Conn{conns[0], conns[1], conns[2], conns[3]}
- initR, _ := runTestHandshake(t)
- ls := newTestLaneSet(t, initR, 2, 2, 5353, 2)
- hi := newTestLaneHostInfo(t, initR, ls)
- laneSessionFor(t, ls, 1)
- laneRemote := netip.MustParseAddrPort("192.0.2.1:5354")
- ls.txAddr[1].Store(&laneRemote)
- pkt := tio.Packet{Bytes: []byte{0x45, 0, 0, 4, 1, 2, 3, 4}}
- nb := make([]byte, 12)
- flow1 := flowForLane(t, ls, 1)
- // Routine 1 is the second socket in each group, so its lane 1 traffic leaves
- // writers[1*2+1].
- tx := ifce.newTxQueue(1)
- ifce.sendInsideMessage(hi, pkt, flow1, nb, tx)
- tx.flush(ifce)
- require.Len(t, conns[3].bufs, 1, "lane 1 did not leave routine 1's socket on the lane port")
- assert.Equal(t, laneRemote, conns[3].dsts[0])
- assert.Empty(t, conns[2].bufs)
- // Routine 0 sends the same flow out the other socket on that same port.
- tx0 := ifce.newTxQueue(0)
- ifce.sendInsideMessage(hi, pkt, flow1, nb, tx0)
- tx0.flush(ifce)
- require.Len(t, conns[2].bufs, 1, "sibling routine shared a socket instead of its own")
- assert.Len(t, conns[3].bufs, 1)
- }
- // One routine has to be able to reach every lane, since the kernel decides which
- // routine sees a flow and it may well hand one routine everything.
- func TestTxLaneForFlowSpread(t *testing.T) {
- initR, _ := runTestHandshake(t)
- ls := newTestLaneSet(t, initR, 4, 4, 5353, 4)
- seen := map[int]int{}
- for port := uint16(1024); port < 1224; port++ {
- s := laneOfFlow(ls, testFlow(port))
- require.Less(t, s, ls.txLanes)
- seen[s]++
- }
- assert.Len(t, seen, 4, "200 flows did not reach all four lanes: %v", seen)
- for s, n := range seen {
- assert.Greater(t, n, 10, "lane %d got a negligible share of flows", s)
- }
- // A flow's lane is a function of its 5-tuple alone, so it never moves
- // mid-flow.
- p := testFlow(1234)
- assert.Equal(t, laneOfFlow(ls, p), laneOfFlow(ls, p))
- // A tunnel with nothing but the base lane always answers lane 0.
- one := newTestLaneSet(t, initR, 1, 1, 5353, 4)
- s, ci, addr := one.txLaneForFlow(p)
- assert.Zero(t, s)
- assert.Nil(t, ci)
- assert.False(t, addr.IsValid())
- s, ci, _ = (*laneSet)(nil).txLaneForFlow(p)
- assert.Zero(t, s)
- assert.Nil(t, ci)
- }
- // The two ends of a flow have to pick partner lanes, or the flow's two
- // directions use unrelated 4-tuples instead of exact reverses and neither side's
- // traffic arrives through the conntrack entry the other's probe opened.
- func TestLaneFlowSymmetry(t *testing.T) {
- initR, respR := runTestHandshake(t)
- // Our side is the low address; the peer builds the same pair reversed.
- const ourBase, peerBase = 4242, 5353
- ourLS := newTestLaneSet(t, initR, 4, 4, peerBase, 4)
- respR.PeerPortCount, respR.PeerBasePort, respR.PeerTxLanes = 4, ourBase, 4
- peerLS := newLaneSet(respR, 4, testLaneEpoch, testPeerAddr, testMyAddr)
- require.NotNil(t, peerLS)
- require.Equal(t, uint16(0), ourLS.laneBias, "the low address hashes straight")
- partnered := 0
- for port := uint16(1024); port < 1124; port++ {
- p := testFlow(port)
- reverse := &firewall.Packet{
- LocalAddr: p.RemoteAddr, RemoteAddr: p.LocalAddr,
- LocalPort: p.RemotePort, RemotePort: p.LocalPort,
- Protocol: p.Protocol,
- }
- ours := laneOfFlow(ourLS, p)
- theirs := laneOfFlow(peerLS, reverse)
- require.Equal(t, (ours+int(ourLS.portOffset))%4, theirs,
- "flow on port %d: our lane %d is not partnered with their lane %d", port, ours, theirs)
- if ours == 0 || theirs == 0 {
- // The lane that lands on the other side's base port has no partner
- // lane; that flow rides one side's base tunnel. See lanePortOffset.
- continue
- }
- partnered++
- // Our source and destination ports are the reverse of theirs.
- ourLS.mu.Lock()
- peerLS.mu.Lock()
- assert.Equal(t, uint16(peerBase+theirs), ourLS.laneTargetPortLocked(ours), "flow on port %d", port)
- assert.Equal(t, uint16(ourBase+ours), peerLS.laneTargetPortLocked(theirs), "flow on port %d", port)
- peerLS.mu.Unlock()
- ourLS.mu.Unlock()
- }
- assert.Greater(t, partnered, 0, "no flow landed on a partnered lane")
- }
- // Regression: deleting a hostinfo whose pending entry is NOT the one recorded
- // in vpnIps must not evict a concurrently pending handshake for that address.
- func TestHandshakeManagerVpnIpsIdentityDelete(t *testing.T) {
- l := test.NewLogger()
- hostMap := newHostMap(l)
- lh := newTestLighthouse()
- hm := NewHandshakeManager(l, hostMap, lh, &udp.NoopConn{}, defaultHandshakeConfig)
- vpnIp := netip.MustParseAddr("172.1.1.4")
- pendingBase := hm.StartHandshake(vpnIp, nil)
- require.NotNil(t, pendingBase)
- other := &HostInfo{vpnAddrs: []netip.Addr{vpnIp}, localIndexId: 999}
- hm.DeleteHostInfo(other)
- // The pending base handshake must still be tracked.
- assert.Equal(t, pendingBase, hm.QueryVpnAddr(vpnIp))
- // And deleting the actual owner still works.
- hm.DeleteHostInfo(pendingBase)
- assert.Nil(t, hm.QueryVpnAddr(vpnIp))
- }
|