1
0

hostmap_test.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. package nebula
  2. import (
  3. "net/netip"
  4. "slices"
  5. "testing"
  6. "github.com/slackhq/nebula/config"
  7. "github.com/slackhq/nebula/test"
  8. "github.com/stretchr/testify/assert"
  9. "github.com/stretchr/testify/require"
  10. )
  11. // chainIds returns the localIndexIds of the hostinfos holding addr, primary (index 0) first. It
  12. // also validates the Hosts/moreHosts sync contract on every call so a mutation that broke it
  13. // fails fast.
  14. func chainIds(t *testing.T, hm *HostMap, addr netip.Addr) []uint32 {
  15. t.Helper()
  16. assertHostMapInvariants(t, hm)
  17. list := hm.unlockedGetHostList(addr)
  18. ids := make([]uint32, len(list))
  19. for i, h := range list {
  20. ids[i] = h.localIndexId
  21. }
  22. return ids
  23. }
  24. // assertHostMapInvariants checks the Hosts/moreHosts contract: moreHosts only holds addresses
  25. // with 2 or more hostinfos, its first entry is always the primary in Hosts, lists never hold
  26. // duplicates, every hostinfo in a list owns the address and is registered in Indexes, and every
  27. // indexed hostinfo is reachable through each of its addresses.
  28. func assertHostMapInvariants(t *testing.T, hm *HostMap) {
  29. t.Helper()
  30. for addr, list := range hm.moreHosts {
  31. require.GreaterOrEqualf(t, len(list), 2, "moreHosts[%s] must hold at least 2 hostinfos", addr)
  32. require.Samef(t, hm.Hosts[addr], list[0], "moreHosts[%s][0] must match the primary in Hosts", addr)
  33. seen := map[*HostInfo]bool{}
  34. for _, h := range list {
  35. require.NotNilf(t, h, "moreHosts[%s] must never hold a nil hostinfo", addr)
  36. require.Falsef(t, seen[h], "moreHosts[%s] holds hostinfo %d twice", addr, h.localIndexId)
  37. seen[h] = true
  38. require.Samef(t, hm.Indexes[h.localIndexId], h, "moreHosts[%s] member %d is not registered in Indexes", addr, h.localIndexId)
  39. require.Truef(t, slices.Contains(h.vpnAddrs, addr), "moreHosts[%s] member %d does not own the address", addr, h.localIndexId)
  40. }
  41. }
  42. for addr, h := range hm.Hosts {
  43. require.NotNilf(t, h, "Hosts[%s] must never be nil", addr)
  44. require.Samef(t, hm.Indexes[h.localIndexId], h, "Hosts[%s] primary %d is not registered in Indexes", addr, h.localIndexId)
  45. require.Truef(t, slices.Contains(h.vpnAddrs, addr), "Hosts[%s] primary (index %d) does not own the address", addr, h.localIndexId)
  46. }
  47. for idx, h := range hm.Indexes {
  48. require.Equalf(t, idx, h.localIndexId, "Indexes[%d] holds hostinfo with localIndexId %d", idx, h.localIndexId)
  49. for _, va := range h.vpnAddrs {
  50. require.Truef(t, slices.Contains(hm.unlockedGetHostList(va), h), "indexed hostinfo %d is missing from the list for %s", idx, va)
  51. }
  52. }
  53. }
  54. func TestHostMap_MakePrimary(t *testing.T) {
  55. l := test.NewLogger()
  56. hm := newHostMap(l)
  57. f := &Interface{}
  58. a := netip.MustParseAddr("0.0.0.1")
  59. h1 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 1}
  60. h2 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 2}
  61. h3 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 3}
  62. h4 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 4}
  63. hm.unlockedAddHostInfo(h4, f)
  64. hm.unlockedAddHostInfo(h3, f)
  65. hm.unlockedAddHostInfo(h2, f)
  66. hm.unlockedAddHostInfo(h1, f)
  67. // Most-recently-added is primary: h1, h2, h3, h4
  68. assert.Equal(t, []uint32{1, 2, 3, 4}, chainIds(t, hm, a))
  69. assert.Equal(t, h1, hm.QueryVpnAddr(a))
  70. // Swap the middle to primary: h3, h1, h2, h4
  71. hm.MakePrimary(h3)
  72. assert.Equal(t, []uint32{3, 1, 2, 4}, chainIds(t, hm, a))
  73. assert.Equal(t, h3, hm.QueryVpnAddr(a))
  74. // Swap the tail to primary: h4, h3, h1, h2
  75. hm.MakePrimary(h4)
  76. assert.Equal(t, []uint32{4, 3, 1, 2}, chainIds(t, hm, a))
  77. // Swapping the current primary again is a no-op
  78. hm.MakePrimary(h4)
  79. assert.Equal(t, []uint32{4, 3, 1, 2}, chainIds(t, hm, a))
  80. }
  81. func TestHostMap_DeleteHostInfo(t *testing.T) {
  82. l := test.NewLogger()
  83. hm := newHostMap(l)
  84. f := &Interface{}
  85. a := netip.MustParseAddr("0.0.0.1")
  86. h1 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 1}
  87. h2 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 2}
  88. h3 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 3}
  89. h4 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 4}
  90. h5 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 5}
  91. h6 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 6}
  92. hm.unlockedAddHostInfo(h6, f)
  93. hm.unlockedAddHostInfo(h5, f)
  94. hm.unlockedAddHostInfo(h4, f)
  95. hm.unlockedAddHostInfo(h3, f)
  96. hm.unlockedAddHostInfo(h2, f)
  97. hm.unlockedAddHostInfo(h1, f)
  98. // h6 is evicted by the MaxHostInfosPerVpnIp cap; the rest are newest-first.
  99. assert.Nil(t, hm.QueryIndex(h6.localIndexId))
  100. assert.Equal(t, []uint32{1, 2, 3, 4, 5}, chainIds(t, hm, a))
  101. // Delete primary; not final since siblings remain.
  102. assert.False(t, hm.DeleteHostInfo(h1))
  103. assert.Equal(t, []uint32{2, 3, 4, 5}, chainIds(t, hm, a))
  104. // Deleting the same hostinfo again must not report final while siblings remain and must not
  105. // disturb the list. The old chain code got this wrong: the first delete nil'd next/prev, so a
  106. // second delete looked final and wiped lighthouse state out from under the live sibling.
  107. assert.False(t, hm.DeleteHostInfo(h1))
  108. assert.Equal(t, []uint32{2, 3, 4, 5}, chainIds(t, hm, a))
  109. // Delete a middle node.
  110. assert.False(t, hm.DeleteHostInfo(h3))
  111. assert.Equal(t, []uint32{2, 4, 5}, chainIds(t, hm, a))
  112. // Delete the tail.
  113. assert.False(t, hm.DeleteHostInfo(h5))
  114. assert.Equal(t, []uint32{2, 4}, chainIds(t, hm, a))
  115. // Delete the head; h4 remains and becomes primary.
  116. assert.False(t, hm.DeleteHostInfo(h2))
  117. assert.Equal(t, []uint32{4}, chainIds(t, hm, a))
  118. assert.Equal(t, h4, hm.QueryVpnAddr(a))
  119. // Delete the only remaining item; final is true and the address is gone.
  120. assert.True(t, hm.DeleteHostInfo(h4))
  121. assert.Empty(t, chainIds(t, hm, a))
  122. assert.Nil(t, hm.QueryVpnAddr(a))
  123. // Deleting an already-gone hostinfo is still final; nothing holds the address anymore.
  124. assert.True(t, hm.DeleteHostInfo(h4))
  125. assert.Empty(t, chainIds(t, hm, a))
  126. }
  127. // TestHostMap_MakePrimary_DeletedHostInfo covers promoting a hostinfo that lost a race with
  128. // tunnel teardown: swapPrimary and AddRelay decide to promote while holding a stale pointer and
  129. // only take the write lock after a delete fully unlinked the hostinfo. MakePrimary must be a
  130. // no-op, not a resurrection that installs an unmanaged primary.
  131. func TestHostMap_MakePrimary_DeletedHostInfo(t *testing.T) {
  132. l := test.NewLogger()
  133. hm := newHostMap(l)
  134. f := &Interface{}
  135. a := netip.MustParseAddr("0.0.0.1")
  136. h1 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 1}
  137. h2 := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 2}
  138. hm.unlockedAddHostInfo(h1, f)
  139. hm.unlockedAddHostInfo(h2, f)
  140. // h1 is fully deleted while another goroutine still holds a pointer to it.
  141. assert.False(t, hm.DeleteHostInfo(h1))
  142. assert.Equal(t, []uint32{2}, chainIds(t, hm, a))
  143. // The stale promote must not bring it back.
  144. hm.MakePrimary(h1)
  145. assert.Equal(t, []uint32{2}, chainIds(t, hm, a))
  146. assert.Equal(t, h2, hm.QueryVpnAddr(a))
  147. assert.Nil(t, hm.QueryIndex(h1.localIndexId))
  148. }
  149. // TestHostMap_QueryVpnAddrsRelayFor_NonPrimary makes sure a relay established on an older
  150. // hostinfo is still found after a newer tunnel without relay state takes primary for the same
  151. // address. The lookup checks the primary first and falls back to the rest of the list.
  152. func TestHostMap_QueryVpnAddrsRelayFor_NonPrimary(t *testing.T) {
  153. l := test.NewLogger()
  154. hm := newHostMap(l)
  155. f := &Interface{}
  156. relayAddr := netip.MustParseAddr("0.0.0.9")
  157. target := netip.MustParseAddr("0.0.0.1")
  158. older := &HostInfo{
  159. vpnAddrs: []netip.Addr{relayAddr},
  160. localIndexId: 1,
  161. relayState: RelayState{
  162. relayForByAddr: map[netip.Addr]*Relay{},
  163. relayForByIdx: map[uint32]*Relay{},
  164. },
  165. }
  166. older.relayState.InsertRelay(target, 100, &Relay{Type: ForwardingType, State: Established, LocalIndex: 100, PeerAddr: target})
  167. hm.unlockedAddHostInfo(older, f)
  168. // The relay is found on the primary.
  169. h, r, err := hm.QueryVpnAddrsRelayFor([]netip.Addr{target}, relayAddr)
  170. require.NoError(t, err)
  171. assert.Equal(t, older, h)
  172. assert.Equal(t, uint32(100), r.LocalIndex)
  173. // A re-handshake with no relay state takes primary; the established relay on the older
  174. // hostinfo must still be found through the fallback.
  175. newer := &HostInfo{vpnAddrs: []netip.Addr{relayAddr}, localIndexId: 2}
  176. hm.unlockedAddHostInfo(newer, f)
  177. assert.Equal(t, []uint32{2, 1}, chainIds(t, hm, relayAddr))
  178. h, r, err = hm.QueryVpnAddrsRelayFor([]netip.Addr{target}, relayAddr)
  179. require.NoError(t, err)
  180. assert.Equal(t, older, h)
  181. assert.Equal(t, uint32(100), r.LocalIndex)
  182. // No hostinfo at all is a plain miss.
  183. _, _, err = hm.QueryVpnAddrsRelayFor([]netip.Addr{target}, netip.MustParseAddr("0.0.0.42"))
  184. require.Error(t, err)
  185. }
  186. // TestHostMap_DeleteHostInfo_MultipleVpnAddrs exercises the case where a hostinfo carries more than one
  187. // vpnAddr and shares its next/prev chain with a live sibling. Deleting the head must not corrupt the
  188. // sibling: every address the sibling owns has to keep pointing at it. The pre-fix code unlinked the shared
  189. // chain once per vpnAddr, so on the first address it nil'd next/prev, and on the second address the node
  190. // looked already-detached: it dropped the map entry instead of promoting the sibling (and tripped the
  191. // isLastHostinfo relay teardown). See unlockedDeleteHostInfo.
  192. func TestHostMap_DeleteHostInfo_MultipleVpnAddrs(t *testing.T) {
  193. l := test.NewLogger()
  194. hm := newHostMap(l)
  195. f := &Interface{}
  196. a := netip.MustParseAddr("0.0.0.1")
  197. b := netip.MustParseAddr("0.0.0.2")
  198. // Two tunnels for the same peer, each reachable at both a and b.
  199. other := &HostInfo{vpnAddrs: []netip.Addr{a, b}, localIndexId: 1}
  200. head := &HostInfo{vpnAddrs: []netip.Addr{a, b}, localIndexId: 2}
  201. hm.unlockedAddHostInfo(other, f)
  202. hm.unlockedAddHostInfo(head, f)
  203. // head is primary for both addresses, other is next in each address's list.
  204. assert.Equal(t, head, hm.QueryVpnAddr(a))
  205. assert.Equal(t, head, hm.QueryVpnAddr(b))
  206. assert.Equal(t, []uint32{2, 1}, chainIds(t, hm, a))
  207. assert.Equal(t, []uint32{2, 1}, chainIds(t, hm, b))
  208. // Delete the head. other is still live, so it must become primary for BOTH addresses.
  209. assert.False(t, hm.DeleteHostInfo(head))
  210. assert.Equal(t, other, hm.QueryVpnAddr(a))
  211. assert.Equal(t, other, hm.QueryVpnAddr(b))
  212. assert.Equal(t, []uint32{1}, chainIds(t, hm, a))
  213. assert.Equal(t, []uint32{1}, chainIds(t, hm, b))
  214. // head is fully removed from the index map.
  215. assert.Nil(t, hm.QueryIndex(head.localIndexId))
  216. }
  217. // TestHostMap_DeleteHostInfo_DivergentVpnAddrs covers chained hostinfos for the same peer whose
  218. // vpnAddrs sets differ (a re-handshake cert added a second address). Deleting the superset node
  219. // must not promote a sibling to an address it does not own.
  220. func TestHostMap_DeleteHostInfo_DivergentVpnAddrs(t *testing.T) {
  221. l := test.NewLogger()
  222. hm := newHostMap(l)
  223. f := &Interface{}
  224. a := netip.MustParseAddr("0.0.0.1")
  225. b := netip.MustParseAddr("0.0.0.2")
  226. // sub owns only a; super (a newer handshake) owns a and b.
  227. sub := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 1}
  228. super := &HostInfo{vpnAddrs: []netip.Addr{a, b}, localIndexId: 2}
  229. hm.unlockedAddHostInfo(sub, f)
  230. hm.unlockedAddHostInfo(super, f)
  231. assert.Equal(t, []uint32{2, 1}, chainIds(t, hm, a))
  232. assert.Equal(t, []uint32{2}, chainIds(t, hm, b))
  233. // Delete super: a promotes to sub (which owns it); b has no remaining owner and must be
  234. // removed, not dangled at sub (which does not own b).
  235. assert.False(t, hm.DeleteHostInfo(super))
  236. assert.Equal(t, []uint32{1}, chainIds(t, hm, a))
  237. assert.Empty(t, chainIds(t, hm, b))
  238. assert.Equal(t, sub, hm.QueryVpnAddr(a))
  239. assert.Nil(t, hm.QueryVpnAddr(b))
  240. assert.Nil(t, hm.QueryIndex(super.localIndexId))
  241. // Deleting sub cleans up fully.
  242. assert.True(t, hm.DeleteHostInfo(sub))
  243. assert.Nil(t, hm.QueryVpnAddr(a))
  244. assertHostMapInvariants(t, hm)
  245. }
  246. // TestHostMap_AddDivergentOverlap covers a new hostinfo claiming addresses currently owned by two
  247. // DIFFERENT hostinfos. The old single shared next/prev chain overwrote a pointer and orphaned one
  248. // of them (in Indexes but unreachable via its address); independent per-address lists cannot.
  249. func TestHostMap_AddDivergentOverlap(t *testing.T) {
  250. l := test.NewLogger()
  251. hm := newHostMap(l)
  252. f := &Interface{}
  253. a := netip.MustParseAddr("0.0.0.1")
  254. b := netip.MustParseAddr("0.0.0.2")
  255. hiA := &HostInfo{vpnAddrs: []netip.Addr{a}, localIndexId: 1}
  256. hiP := &HostInfo{vpnAddrs: []netip.Addr{b}, localIndexId: 2}
  257. hm.unlockedAddHostInfo(hiA, f)
  258. hm.unlockedAddHostInfo(hiP, f)
  259. hiB := &HostInfo{vpnAddrs: []netip.Addr{a, b}, localIndexId: 3}
  260. hm.unlockedAddHostInfo(hiB, f)
  261. assert.Equal(t, []uint32{3, 1}, chainIds(t, hm, a))
  262. assert.Equal(t, []uint32{3, 2}, chainIds(t, hm, b))
  263. // hiA is still reachable via its address (not orphaned) and still indexed.
  264. assert.Contains(t, chainIds(t, hm, a), hiA.localIndexId)
  265. assert.NotNil(t, hm.QueryIndex(hiA.localIndexId))
  266. }
  267. // TestHostMap_MaxHostInfosPerVpnIp_MultipleVpnAddrs verifies the MaxHostInfosPerVpnIp overflow prune
  268. // (unlockedInnerAddHostInfo calls unlockedDeleteHostInfo on the oldest node once the chain is too long)
  269. // still behaves when hostinfos carry more than one vpnAddr. The pruned node is always the tail, so it is
  270. // primary for none of the addresses, and both address chains must stay consistent afterwards.
  271. func TestHostMap_MaxHostInfosPerVpnIp_MultipleVpnAddrs(t *testing.T) {
  272. l := test.NewLogger()
  273. hm := newHostMap(l)
  274. f := &Interface{}
  275. a := netip.MustParseAddr("0.0.0.1")
  276. b := netip.MustParseAddr("0.0.0.2")
  277. // Add one more than the cap, newest last so it becomes head. Every hostinfo owns both a and b.
  278. hostinfos := make([]*HostInfo, 0, MaxHostInfosPerVpnIp+1)
  279. for i := 0; i <= MaxHostInfosPerVpnIp; i++ {
  280. hostinfos = append(hostinfos, &HostInfo{vpnAddrs: []netip.Addr{a, b}, localIndexId: uint32(i + 1)})
  281. }
  282. // Add oldest first (highest index in our slice) so the very first one added is the overflow victim.
  283. for i := len(hostinfos) - 1; i >= 0; i-- {
  284. hm.unlockedAddHostInfo(hostinfos[i], f)
  285. }
  286. oldest := hostinfos[len(hostinfos)-1]
  287. // The oldest hostinfo was pruned from both lists and the index map.
  288. assert.Nil(t, hm.QueryIndex(oldest.localIndexId))
  289. // Both addresses hold exactly MaxHostInfosPerVpnIp survivors in the same order; oldest is absent.
  290. require.Len(t, chainIds(t, hm, a), MaxHostInfosPerVpnIp)
  291. assert.Equal(t, chainIds(t, hm, a), chainIds(t, hm, b), "both addresses must list the same survivors in the same order")
  292. assert.NotContains(t, chainIds(t, hm, a), oldest.localIndexId)
  293. assert.Equal(t, hm.QueryVpnAddr(a), hm.QueryVpnAddr(b))
  294. }
  295. func TestHostMap_reload(t *testing.T) {
  296. l := test.NewLogger()
  297. c := config.NewC(test.NewLogger())
  298. hm := NewHostMapFromConfig(l, c)
  299. toS := func(ipn []netip.Prefix) []string {
  300. var s []string
  301. for _, n := range ipn {
  302. s = append(s, n.String())
  303. }
  304. return s
  305. }
  306. assert.Empty(t, hm.GetPreferredRanges())
  307. c.ReloadConfigString("preferred_ranges: [1.1.1.0/24, 10.1.1.0/24]")
  308. assert.Equal(t, []string{"1.1.1.0/24", "10.1.1.0/24"}, toS(hm.GetPreferredRanges()))
  309. c.ReloadConfigString("preferred_ranges: [1.1.1.1/32]")
  310. assert.Equal(t, []string{"1.1.1.1/32"}, toS(hm.GetPreferredRanges()))
  311. }
  312. func TestHostMap_RelayState(t *testing.T) {
  313. h1 := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("0.0.0.1")}, localIndexId: 1}
  314. a1 := netip.MustParseAddr("::1")
  315. a2 := netip.MustParseAddr("2001::1")
  316. h1.relayState.InsertRelayTo(a1)
  317. assert.Equal(t, []netip.Addr{a1}, h1.relayState.relays)
  318. h1.relayState.InsertRelayTo(a2)
  319. assert.Equal(t, []netip.Addr{a1, a2}, h1.relayState.relays)
  320. // Ensure that the first relay added is the first one returned in the copy
  321. currentRelays := h1.relayState.CopyRelayIps()
  322. require.Len(t, currentRelays, 2)
  323. assert.Equal(t, a1, currentRelays[0])
  324. // Deleting the last one in the list works ok
  325. h1.relayState.DeleteRelay(a2)
  326. assert.Equal(t, []netip.Addr{a1}, h1.relayState.relays)
  327. // Deleting an element not in the list works ok
  328. h1.relayState.DeleteRelay(a2)
  329. assert.Equal(t, []netip.Addr{a1}, h1.relayState.relays)
  330. // Deleting the only element in the list works ok
  331. h1.relayState.DeleteRelay(a1)
  332. assert.Equal(t, []netip.Addr{}, h1.relayState.relays)
  333. }
  334. // sentSinceCheck reports whether anything has been sent since the connection manager last looked. Test only:
  335. // production reads the out bit through takeTraffic on the connection manager tick.
  336. func (i *HostInfo) sentSinceCheck() bool {
  337. return i.state.Load()&stateOut != 0
  338. }
  339. func TestHostInfo_markOut(t *testing.T) {
  340. h := &HostInfo{}
  341. h.markOut(5) // stamped when the tunnel was added
  342. // A tunnel already on the current epoch has nothing to report, which is what keeps a fresh tunnel from
  343. // requerying on its first packet
  344. assert.False(t, h.markOut(5), "an unchanged epoch should not report a move")
  345. assert.True(t, h.sentSinceCheck(), "the send is still recorded as traffic")
  346. // A rebind is observed exactly once, so we requery once per rebind
  347. assert.True(t, h.markOut(6), "a bumped epoch should report a move")
  348. assert.False(t, h.markOut(6), "the epoch move should only be reported once")
  349. // Traffic and pendingDeletion live in the same word and must survive an epoch change
  350. h.setPendingDeletion(true)
  351. h.markIn()
  352. assert.True(t, h.markOut(7))
  353. assert.True(t, h.isPendingDeletion(), "pendingDeletion must survive an epoch change")
  354. in, out := h.takeTraffic()
  355. assert.True(t, in, "inbound traffic must survive an epoch change")
  356. assert.True(t, out)
  357. // Clearing the traffic bits leaves the epoch alone, otherwise an idle tunnel would requery forever
  358. assert.False(t, h.markOut(7), "takeTraffic must not disturb the epoch")
  359. }
  360. // A relayed send records traffic but must leave the rebind epoch for the direct path to consume, otherwise
  361. // relaying to a host swallows the requery that gets the far side punching at our new address.
  362. func TestHostInfo_markOutOnly(t *testing.T) {
  363. h := &HostInfo{}
  364. h.markOut(5)
  365. h.markOutOnly()
  366. assert.True(t, h.sentSinceCheck(), "a relayed send is still outbound traffic")
  367. assert.False(t, h.markOut(5), "a relayed send must not disturb the epoch")
  368. assert.True(t, h.markOut(6), "a relayed send must not consume the epoch edge")
  369. assert.False(t, h.markOut(6))
  370. }