handshake_manager_pq_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. package nebula
  2. import (
  3. "math/rand/v2"
  4. "net/netip"
  5. "sync"
  6. "testing"
  7. "time"
  8. "github.com/slackhq/nebula/config"
  9. "github.com/slackhq/nebula/handshake/fragment"
  10. "github.com/slackhq/nebula/header"
  11. "github.com/slackhq/nebula/test"
  12. "github.com/slackhq/nebula/udp"
  13. "github.com/stretchr/testify/assert"
  14. "github.com/stretchr/testify/require"
  15. )
  16. // recordingConn captures what is written to it.
  17. type recordingConn struct {
  18. udp.NoopConn
  19. sync.Mutex
  20. sent [][]byte
  21. }
  22. func (c *recordingConn) WriteTo(b []byte, _ netip.AddrPort) error {
  23. c.Lock()
  24. defer c.Unlock()
  25. c.sent = append(c.sent, append([]byte(nil), b...))
  26. return nil
  27. }
  28. func (c *recordingConn) take() [][]byte {
  29. c.Lock()
  30. defer c.Unlock()
  31. s := c.sent
  32. c.sent = nil
  33. return s
  34. }
  35. func newPQTestManager(t *testing.T) (*HandshakeManager, *recordingConn) {
  36. l := test.NewLogger()
  37. conn := &recordingConn{}
  38. cfg := defaultHandshakeConfig
  39. cfg.pq = pqHandshakeConfig{initiate: true, accept: true}
  40. hm := NewHandshakeManager(l, newHostMap(l), newTestLighthouse(), conn, cfg)
  41. hm.f = &Interface{
  42. handshakeManager: hm,
  43. outside: conn,
  44. messageMetrics: newMessageMetricsOnlyRecvError(),
  45. l: l,
  46. }
  47. return hm, conn
  48. }
  49. // fragmentIndexes checks every packet is a fragment of the expected response and
  50. // returns their symbol indexes.
  51. func fragmentIndexes(t *testing.T, pkts [][]byte, fragID, remoteIndex uint32) []int {
  52. t.Helper()
  53. var out []int
  54. for _, p := range pkts {
  55. require.LessOrEqual(t, len(p), fragment.MaxPacketLen)
  56. var h header.H
  57. require.NoError(t, h.Parse(p))
  58. assert.Equal(t, pqSubtype, h.Subtype)
  59. assert.Equal(t, remoteIndex, h.RemoteIndex)
  60. assert.Equal(t, uint64(2), h.MessageCounter)
  61. fh, _, err := fragment.Parse(p[header.Len:])
  62. require.NoError(t, err)
  63. assert.Equal(t, fragID, fh.FragID)
  64. out = append(out, int(fh.Index))
  65. }
  66. return out
  67. }
  68. func TestSendFragmentedResponse(t *testing.T) {
  69. hm, conn := newPQTestManager(t)
  70. via := ViaSender{UdpAddr: netip.MustParseAddrPort("192.0.2.1:4242")}
  71. hostinfo := &HostInfo{localIndexId: 77}
  72. // A 2000 byte message is 2 symbols of 1000, so packets of 1024 bytes.
  73. msg := make([]byte, header.Len, header.Len+2000)
  74. header.Encode(msg, header.Version, header.Handshake, pqSubtype, 5, 2)
  75. r := rand.New(rand.NewPCG(1, 1))
  76. for range 2000 {
  77. msg = append(msg, byte(r.Uint32()))
  78. }
  79. const pktLen = header.Len + fragment.HeaderLen + 1000
  80. t.Run("first send", func(t *testing.T) {
  81. require.NoError(t, hm.sendFragmentedResponse(via, msg, hostinfo, false, 0))
  82. assert.Equal(t, []int{0, 1, 2, 3}, fragmentIndexes(t, conn.take(), 77, 5))
  83. })
  84. t.Run("resends are rate limited", func(t *testing.T) {
  85. require.Error(t, hm.sendFragmentedResponse(via, msg, hostinfo, true, 10*pktLen))
  86. assert.Empty(t, conn.take())
  87. })
  88. allowResend := func() {
  89. hostinfo.handshakeResendAt.Store(time.Now().Add(-time.Second).UnixNano())
  90. }
  91. t.Run("resend offers fresh symbols", func(t *testing.T) {
  92. allowResend()
  93. require.NoError(t, hm.sendFragmentedResponse(via, msg, hostinfo, true, 10*pktLen))
  94. assert.Equal(t, []int{4, 5, 6, 7}, fragmentIndexes(t, conn.take(), 77, 5))
  95. })
  96. t.Run("amplification budget trims repair", func(t *testing.T) {
  97. allowResend()
  98. // 3x of a little over one packet allows 3 packets: both data and 1 repair.
  99. require.NoError(t, hm.sendFragmentedResponse(via, msg, hostinfo, true, pktLen+10))
  100. assert.Equal(t, []int{8, 9, 10}, fragmentIndexes(t, conn.take(), 77, 5))
  101. })
  102. t.Run("amplification budget below the data refuses", func(t *testing.T) {
  103. allowResend()
  104. require.Error(t, hm.sendFragmentedResponse(via, msg, hostinfo, true, pktLen/2))
  105. assert.Empty(t, conn.take())
  106. })
  107. }
  108. func TestPQAnswered(t *testing.T) {
  109. now := time.Unix(1000, 0)
  110. a := newPQAnswered(5 * time.Second)
  111. hi := &HostInfo{}
  112. assert.Nil(t, a.lookup([]byte("msg1"), now))
  113. a.add([]byte("msg1"), hi, now)
  114. assert.Same(t, hi, a.lookup([]byte("msg1"), now.Add(time.Second)))
  115. assert.Nil(t, a.lookup([]byte("other"), now))
  116. assert.Nil(t, a.lookup([]byte("msg1"), now.Add(6*time.Second)), "stale entries are not answered")
  117. a.expire(now.Add(6 * time.Second))
  118. assert.Empty(t, a.entries)
  119. for i := range pqAnsweredMax + 10 {
  120. a.add([]byte{byte(i), byte(i >> 8)}, hi, now)
  121. }
  122. assert.Len(t, a.entries, pqAnsweredMax)
  123. }
  124. func TestPQFragKeyFor(t *testing.T) {
  125. k1, s1 := pqFragKeyFor(ViaSender{UdpAddr: netip.MustParseAddrPort("192.0.2.1:4242")}, 9)
  126. k2, s2 := pqFragKeyFor(ViaSender{UdpAddr: netip.MustParseAddrPort("192.0.2.1:4243")}, 9)
  127. assert.NotEqual(t, k1, k2, "different ports are different messages")
  128. assert.Equal(t, s1, s2, "but share an IPv4 /32 quota")
  129. _, s3 := pqFragKeyFor(ViaSender{UdpAddr: netip.MustParseAddrPort("[2001:db8::1]:1")}, 9)
  130. _, s4 := pqFragKeyFor(ViaSender{UdpAddr: netip.MustParseAddrPort("[2001:db8::ffff]:1")}, 9)
  131. assert.Equal(t, s3, s4, "an IPv6 /64 shares a quota")
  132. _, s5 := pqFragKeyFor(ViaSender{UdpAddr: netip.MustParseAddrPort("[::ffff:192.0.2.1]:1")}, 9)
  133. assert.Equal(t, s1, s5, "v4-mapped addresses count as IPv4")
  134. }
  135. func TestPQCookies(t *testing.T) {
  136. now := time.Unix(1000, 0)
  137. c := newPQCookies(now)
  138. key := pqFragKey{addr: netip.MustParseAddrPort("192.0.2.1:4242"), fragID: 9}
  139. cookie := c.make(key, now)
  140. assert.True(t, c.valid(key, cookie, now))
  141. for _, other := range []pqFragKey{
  142. {addr: netip.MustParseAddrPort("192.0.2.2:4242"), fragID: 9},
  143. {addr: netip.MustParseAddrPort("192.0.2.1:4243"), fragID: 9},
  144. {addr: netip.MustParseAddrPort("192.0.2.1:4242"), fragID: 10},
  145. {addr: netip.MustParseAddrPort("192.0.2.1:4242"), relay: 1, fragID: 9},
  146. } {
  147. assert.False(t, c.valid(other, cookie, now), "%+v", other)
  148. }
  149. now = now.Add(pqCookieRotate)
  150. assert.True(t, c.valid(key, cookie, now), "the previous secret is still accepted")
  151. assert.NotEqual(t, cookie, c.make(key, now), "new cookies come from the new secret")
  152. now = now.Add(pqCookieRotate)
  153. assert.False(t, c.valid(key, cookie, now), "two rotations retire a cookie")
  154. }
  155. // pqMsg1Fragment builds the first fragment of a msg1, optionally with a cookie.
  156. func pqMsg1Fragment(t *testing.T, fragID uint32, cookie *fragment.Cookie) ([]byte, *header.H) {
  157. t.Helper()
  158. s, err := fragment.NewSender(make([]byte, 2000), pqSubtype, 0, 1, fragID)
  159. require.NoError(t, err)
  160. if cookie != nil {
  161. s.SetCookie(*cookie)
  162. }
  163. pkts, _, err := s.Attempt(0)
  164. require.NoError(t, err)
  165. var h header.H
  166. require.NoError(t, h.Parse(pkts[0]))
  167. return pkts[0], &h
  168. }
  169. // fillPQTable puts the responder's msg1 table at the cookie threshold.
  170. func fillPQTable(t *testing.T, hm *HandshakeManager) {
  171. t.Helper()
  172. now := time.Now()
  173. for i := 0; hm.pqTable.Load() < pqCookieLoad; i++ {
  174. p, _ := pqMsg1Fragment(t, uint32(i+1), nil)
  175. fh, sym, err := fragment.Parse(p[header.Len:])
  176. require.NoError(t, err)
  177. addr := netip.AddrPortFrom(netip.AddrFrom4([4]byte{198, 18, byte(i >> 8), byte(i)}), 4242)
  178. key, src := pqFragKeyFor(ViaSender{UdpAddr: addr}, fh.FragID)
  179. _, err = hm.pqTable.Add(key, src, now, fh, sym)
  180. require.NoError(t, err)
  181. }
  182. }
  183. func TestPQCookieUnderLoad(t *testing.T) {
  184. hm, conn := newPQTestManager(t)
  185. via := ViaSender{UdpAddr: netip.MustParseAddrPort("192.0.2.1:4242")}
  186. t.Run("no cookie needed below the threshold", func(t *testing.T) {
  187. p, h := pqMsg1Fragment(t, 5000, nil)
  188. before := hm.pqTable.Len()
  189. hm.handleFragment(via, p, h)
  190. assert.Empty(t, conn.take())
  191. assert.Equal(t, before+1, hm.pqTable.Len())
  192. })
  193. fillPQTable(t, hm)
  194. var cookie fragment.Cookie
  195. t.Run("a fragment without a cookie gets one and leaves no state", func(t *testing.T) {
  196. p, h := pqMsg1Fragment(t, 7, nil)
  197. before := hm.pqTable.Len()
  198. hm.handleFragment(via, p, h)
  199. assert.Equal(t, before, hm.pqTable.Len())
  200. sent := conn.take()
  201. require.Len(t, sent, 1)
  202. reply := sent[0]
  203. require.Len(t, reply, pqCookieReplyLen)
  204. var rh header.H
  205. require.NoError(t, rh.Parse(reply))
  206. assert.Equal(t, header.Handshake, rh.Type)
  207. assert.Equal(t, pqSubtype, rh.Subtype)
  208. assert.Equal(t, uint32(7), rh.RemoteIndex, "the reply names the initiator's FragID")
  209. assert.Equal(t, uint64(pqCookieCounter), rh.MessageCounter)
  210. copy(cookie[:], reply[header.Len:])
  211. })
  212. t.Run("a forged cookie gets a fresh one", func(t *testing.T) {
  213. bad := cookie
  214. bad[0] ^= 1
  215. p, h := pqMsg1Fragment(t, 7, &bad)
  216. before := hm.pqTable.Len()
  217. hm.handleFragment(via, p, h)
  218. assert.Equal(t, before, hm.pqTable.Len())
  219. assert.Len(t, conn.take(), 1)
  220. })
  221. t.Run("the cookie gets the fragment in", func(t *testing.T) {
  222. p, h := pqMsg1Fragment(t, 7, &cookie)
  223. before := hm.pqTable.Len()
  224. hm.handleFragment(via, p, h)
  225. assert.Equal(t, before+1, hm.pqTable.Len())
  226. assert.Empty(t, conn.take())
  227. })
  228. t.Run("a cookie is bound to its address", func(t *testing.T) {
  229. p, h := pqMsg1Fragment(t, 7, &cookie)
  230. hm.handleFragment(ViaSender{UdpAddr: netip.MustParseAddrPort("192.0.2.99:4242")}, p, h)
  231. assert.Len(t, conn.take(), 1, "another address gets its own cookie instead")
  232. })
  233. t.Run("a reply never outgrows what prompted it", func(t *testing.T) {
  234. p, h := pqMsg1Fragment(t, 8, nil)
  235. hm.sendCookie(via, fragment.Header{FragID: 8}, pqFragKey{}, pqCookieReplyLen-1, time.Now())
  236. assert.Empty(t, conn.take())
  237. hm.handleFragment(via, p, h)
  238. assert.Len(t, conn.take(), 1)
  239. })
  240. }
  241. func TestPQHandleCookie(t *testing.T) {
  242. hm, conn := newPQTestManager(t)
  243. via := ViaSender{UdpAddr: netip.MustParseAddrPort("192.0.2.1:4242")}
  244. s, err := fragment.NewSender(make([]byte, 2000), pqSubtype, 0, 1, 99)
  245. require.NoError(t, err)
  246. _, _, err = s.Attempt(2)
  247. require.NoError(t, err)
  248. hh := &HandshakeHostInfo{hostinfo: &HostInfo{}, pq: &pqAttempt{index: 99, sender: s}}
  249. hm.indexes[99] = hh
  250. reply := make([]byte, header.Len, pqCookieReplyLen)
  251. header.Encode(reply, header.Version, header.Handshake, pqSubtype, 99, pqCookieCounter)
  252. reply = append(reply, bytes16(0xab)...)
  253. var h header.H
  254. require.NoError(t, h.Parse(reply))
  255. hm.handleFragment(via, reply, &h)
  256. assert.True(t, hh.pq.heard, "a cookie reply shows the peer speaks PQ")
  257. sent := conn.take()
  258. require.Len(t, sent, 4, "an immediate resend of fresh symbols")
  259. for i, p := range sent {
  260. fh, _, err := fragment.Parse(p[header.Len:])
  261. require.NoError(t, err)
  262. assert.True(t, fh.HasCookie())
  263. assert.Equal(t, fragment.Cookie(bytes16(0xab)), fh.Cookie)
  264. assert.Equal(t, uint8(4+i), fh.Index)
  265. }
  266. hm.handleFragment(via, reply, &h)
  267. assert.Empty(t, conn.take(), "immediate resends are rate limited")
  268. t.Run("replies for other indexes are ignored", func(t *testing.T) {
  269. other := append([]byte(nil), reply...)
  270. header.Encode(other, header.Version, header.Handshake, pqSubtype, 98, pqCookieCounter)
  271. var oh header.H
  272. require.NoError(t, oh.Parse(other))
  273. hh.pq.cookieResendAt = time.Time{}
  274. hm.handleFragment(via, other, &oh)
  275. assert.Empty(t, conn.take())
  276. })
  277. }
  278. func bytes16(b byte) []byte {
  279. out := make([]byte, 16)
  280. for i := range out {
  281. out[i] = b
  282. }
  283. return out
  284. }
  285. func TestReloadPQ(t *testing.T) {
  286. l := test.NewLogger()
  287. c := config.NewC(l)
  288. require.NoError(t, c.LoadString(`handshakes: {pq: {initiate: false}}`))
  289. cfg := defaultHandshakeConfig
  290. cfg.pq = pqHandshakeConfigFrom(c)
  291. hm := NewHandshakeManager(l, newHostMap(l), newTestLighthouse(), &udp.NoopConn{}, cfg)
  292. c.RegisterReloadCallback(hm.reloadPQ)
  293. assert.Equal(t, pqHandshakeConfig{repairDelay: DefaultPQRepairDelay}, *hm.pqConfig())
  294. require.NoError(t, c.ReloadConfigString(`handshakes: {pq: {initiate: true, accept: true, sticky: true, repair_delay: 25ms}}`))
  295. assert.Equal(t, pqHandshakeConfig{initiate: true, accept: true, sticky: true, repairDelay: 25 * time.Millisecond}, *hm.pqConfig())
  296. before := hm.pqConfig()
  297. require.NoError(t, c.ReloadConfigString(`handshakes: {try_interval: 1s, pq: {initiate: true, accept: true, sticky: true, repair_delay: 25ms}}`))
  298. assert.Same(t, before, hm.pqConfig(), "an unrelated change leaves the settings alone")
  299. require.NoError(t, c.ReloadConfigString(`handshakes: {}`))
  300. assert.Equal(t, pqHandshakeConfig{repairDelay: DefaultPQRepairDelay}, *hm.pqConfig(), "removing the section restores the defaults")
  301. require.NoError(t, c.ReloadConfigString(`handshakes: {pq: {require: true}}`))
  302. assert.Equal(t, pqHandshakeConfig{initiate: true, accept: true, require: true, repairDelay: DefaultPQRepairDelay}, *hm.pqConfig(), "require implies initiate and accept")
  303. }
  304. func TestUsePQ(t *testing.T) {
  305. hm, _ := newPQTestManager(t)
  306. peer := netip.MustParseAddr("10.1.0.1")
  307. set := func(cfg pqHandshakeConfig) { hm.pq.Store(&cfg) }
  308. // attempts plays a run of attempts, given whether each was triggered early,
  309. // and returns which were PQ.
  310. attempts := func(triggered ...bool) []bool {
  311. hh := &HandshakeHostInfo{}
  312. var out []bool
  313. for _, tr := range triggered {
  314. hh.counter++
  315. hh.lastPQ = hm.usePQ(hh, peer, tr)
  316. out = append(out, hh.lastPQ)
  317. }
  318. return out
  319. }
  320. set(pqHandshakeConfig{})
  321. assert.Equal(t, []bool{false, false, false}, attempts(false, false, false), "off")
  322. set(pqHandshakeConfig{initiate: true})
  323. assert.Equal(t, []bool{true, false, true, false}, attempts(false, false, false, false), "timer retries alternate, PQ first")
  324. assert.Equal(t, []bool{true, true, false, true, false}, attempts(false, true, false, true, false), "an early trigger is a new path, which gets PQ first")
  325. assert.Equal(t, []bool{true, true, false}, attempts(true, true, false), "a triggered first attempt is PQ")
  326. t.Run("hearing back stops the classical turns", func(t *testing.T) {
  327. hh := &HandshakeHostInfo{pq: &pqAttempt{}}
  328. hh.counter, hh.lastPQ = 1, true
  329. hh.counter++
  330. assert.False(t, hm.usePQ(hh, peer, false), "unheard, the classical attempt gets its turn")
  331. hh.pq.heard = true
  332. assert.True(t, hm.usePQ(hh, peer, false))
  333. })
  334. set(pqHandshakeConfig{initiate: true, accept: true, require: true})
  335. assert.Equal(t, []bool{true, true, true, true}, attempts(false, false, true, false), "require never offers classical")
  336. set(pqHandshakeConfig{initiate: true, sticky: true})
  337. assert.Equal(t, []bool{true, false}, attempts(false, false), "sticky alternates until the peer has done PQ")
  338. hm.notePQPeer(pqSubtype, []netip.Addr{peer})
  339. assert.Equal(t, []bool{true, true, true}, attempts(false, false, false), "then only PQ")
  340. }