1
0

multi_coalesce_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. package batch
  2. import (
  3. "bytes"
  4. "encoding/binary"
  5. "io"
  6. "testing"
  7. "github.com/slackhq/nebula/firewall"
  8. "github.com/slackhq/nebula/test"
  9. )
  10. // keySeq hands out SortKeys with ascending counters in a fixed epoch, for
  11. // tests where commit order IS transmission order.
  12. type keySeq struct {
  13. epoch, counter uint64
  14. }
  15. func (k *keySeq) next() SortKey {
  16. k.counter++
  17. return SortKey{Epoch: k.epoch, Counter: k.counter}
  18. }
  19. // newTestMultiCoalescer builds a batcher over w.
  20. func newTestMultiCoalescer(tb testing.TB, w io.Writer) *MultiCoalescer {
  21. tb.Helper()
  22. return NewMultiCoalescer(w, test.NewLogger())
  23. }
  24. // TestMultiCoalescerRoutesByProto confirms TCP/UDP/other land in the right
  25. // lane: TCP and UDP get coalesced when their lanes are enabled, anything
  26. // else (ICMP here) falls through to plain Write.
  27. func TestMultiCoalescerRoutesByProto(t *testing.T) {
  28. w := &fakeTunWriter{gsoEnabled: true}
  29. m := newTestMultiCoalescer(t, w)
  30. k := &keySeq{epoch: 1}
  31. tcpPay := make([]byte, 1200)
  32. udpPay := make([]byte, 1200)
  33. icmp := make([]byte, 28)
  34. icmp[0] = 0x45
  35. icmp[2] = 0
  36. icmp[3] = 28
  37. icmp[9] = 1
  38. if err := m.Commit(buildTCPv4(1000, tcpAck, tcpPay), k.next(), testPP(buildTCPv4(1000, tcpAck, tcpPay))); err != nil {
  39. t.Fatal(err)
  40. }
  41. if err := m.Commit(buildTCPv4(2200, tcpAck, tcpPay), k.next(), testPP(buildTCPv4(2200, tcpAck, tcpPay))); err != nil {
  42. t.Fatal(err)
  43. }
  44. if err := m.Commit(buildUDPv4(2000, 53, udpPay), k.next(), testPP(buildUDPv4(2000, 53, udpPay))); err != nil {
  45. t.Fatal(err)
  46. }
  47. if err := m.Commit(buildUDPv4(2000, 53, udpPay), k.next(), testPP(buildUDPv4(2000, 53, udpPay))); err != nil {
  48. t.Fatal(err)
  49. }
  50. if err := m.Commit(icmp, k.next(), testPP(icmp)); err != nil {
  51. t.Fatal(err)
  52. }
  53. if err := m.Flush(); err != nil {
  54. t.Fatal(err)
  55. }
  56. // 1 TCP super (2 segments) + 1 UDP super (2 segments) = 2 gso writes.
  57. if len(w.gsoWrites) != 2 {
  58. t.Fatalf("want 2 gso writes (one TCP + one UDP), got %d", len(w.gsoWrites))
  59. }
  60. if len(w.writes) != 1 {
  61. t.Fatalf("want 1 plain write (ICMP), got %d", len(w.writes))
  62. }
  63. }
  64. // TestMultiCoalescerRestoresTransmissionOrder is the core staging-sort
  65. // property: packets committed out of counter order (wire reorder inside one
  66. // flush batch) are replayed into the lanes in transmission order, so the
  67. // reorder never fragments the coalesce chain — one superpacket, in seq
  68. // order, exactly as if the wire had never reordered. The retransmit shape
  69. // falls out of the same key: a retransmit carries a lower seq but a HIGHER
  70. // counter (it was encrypted later), so it emits after the data it trails.
  71. func TestMultiCoalescerRestoresTransmissionOrder(t *testing.T) {
  72. w := &fakeTunWriter{gsoEnabled: true}
  73. m := newTestMultiCoalescer(t, w)
  74. pay := make([]byte, 1200)
  75. // Transmission order: seq 1000 (c1), 2200 (c2), 3400 (c3).
  76. // Arrival order: 3400, 1000, 2200.
  77. if err := m.Commit(buildTCPv4(3400, tcpAck, pay), SortKey{Epoch: 1, Counter: 3}, testPP(buildTCPv4(3400, tcpAck, pay))); err != nil {
  78. t.Fatal(err)
  79. }
  80. if err := m.Commit(buildTCPv4(1000, tcpAck, pay), SortKey{Epoch: 1, Counter: 1}, testPP(buildTCPv4(1000, tcpAck, pay))); err != nil {
  81. t.Fatal(err)
  82. }
  83. if err := m.Commit(buildTCPv4(2200, tcpAck, pay), SortKey{Epoch: 1, Counter: 2}, testPP(buildTCPv4(2200, tcpAck, pay))); err != nil {
  84. t.Fatal(err)
  85. }
  86. if err := m.Flush(); err != nil {
  87. t.Fatal(err)
  88. }
  89. if len(w.gsoWrites) != 1 || len(w.writes) != 0 {
  90. t.Fatalf("want 1 gso write (unfragmented chain), got gso=%d plain=%d", len(w.gsoWrites), len(w.writes))
  91. }
  92. g := w.gsoWrites[0]
  93. if len(g.pays) != 3 {
  94. t.Fatalf("segs=%d want 3", len(g.pays))
  95. }
  96. const ipHdrLen = 20
  97. if seedSeq := binary.BigEndian.Uint32(g.hdr[ipHdrLen+4 : ipHdrLen+8]); seedSeq != 1000 {
  98. t.Errorf("seed seq=%d want 1000", seedSeq)
  99. }
  100. // Retransmit: seq 1000 again but counter 4 — sorts after seq 4600 (c3).
  101. w.writes, w.gsoWrites, w.order = nil, nil, nil
  102. if err := m.Commit(buildTCPv4(1000, tcpAck, pay), SortKey{Epoch: 1, Counter: 4}, testPP(buildTCPv4(1000, tcpAck, pay))); err != nil {
  103. t.Fatal(err)
  104. }
  105. if err := m.Commit(buildTCPv4(4600, tcpAck, pay), SortKey{Epoch: 1, Counter: 3}, testPP(buildTCPv4(4600, tcpAck, pay))); err != nil {
  106. t.Fatal(err)
  107. }
  108. if err := m.Flush(); err != nil {
  109. t.Fatal(err)
  110. }
  111. if len(w.writes) != 2 {
  112. t.Fatalf("want 2 plain writes, got %d (gso=%d)", len(w.writes), len(w.gsoWrites))
  113. }
  114. first := binary.BigEndian.Uint32(w.writes[0][24:28])
  115. second := binary.BigEndian.Uint32(w.writes[1][24:28])
  116. if first != 4600 || second != 1000 {
  117. t.Fatalf("emission (%d, %d), want (4600, 1000): retransmit must not overtake in-flight data", first, second)
  118. }
  119. }
  120. // TestMultiCoalescerRestoresOrderAcrossFlows scrambles two interleaved flows;
  121. // the staging sort must repair each flow into one superpacket without any
  122. // cross-flow contamination.
  123. func TestMultiCoalescerRestoresOrderAcrossFlows(t *testing.T) {
  124. w := &fakeTunWriter{gsoEnabled: true}
  125. m := newTestMultiCoalescer(t, w)
  126. pay := make([]byte, 1200)
  127. // Transmission: A.100 (c1), B.500 (c2), A.1300 (c3), B.1700 (c4).
  128. // Arrival: A.1300, B.1700, A.100, B.500.
  129. if err := m.Commit(buildTCPv4Ports(1000, 2000, 1300, tcpAck, pay), SortKey{Epoch: 1, Counter: 3}, testPP(buildTCPv4Ports(1000, 2000, 1300, tcpAck, pay))); err != nil {
  130. t.Fatal(err)
  131. }
  132. if err := m.Commit(buildTCPv4Ports(3000, 2000, 1700, tcpAck, pay), SortKey{Epoch: 1, Counter: 4}, testPP(buildTCPv4Ports(3000, 2000, 1700, tcpAck, pay))); err != nil {
  133. t.Fatal(err)
  134. }
  135. if err := m.Commit(buildTCPv4Ports(1000, 2000, 100, tcpAck, pay), SortKey{Epoch: 1, Counter: 1}, testPP(buildTCPv4Ports(1000, 2000, 100, tcpAck, pay))); err != nil {
  136. t.Fatal(err)
  137. }
  138. if err := m.Commit(buildTCPv4Ports(3000, 2000, 500, tcpAck, pay), SortKey{Epoch: 1, Counter: 2}, testPP(buildTCPv4Ports(3000, 2000, 500, tcpAck, pay))); err != nil {
  139. t.Fatal(err)
  140. }
  141. if err := m.Flush(); err != nil {
  142. t.Fatal(err)
  143. }
  144. if len(w.gsoWrites) != 2 {
  145. t.Fatalf("want 2 gso writes (one per flow), got %d (plain=%d)", len(w.gsoWrites), len(w.writes))
  146. }
  147. for i, g := range w.gsoWrites {
  148. if len(g.pays) != 2 {
  149. t.Errorf("gso[%d] segs=%d want 2", i, len(g.pays))
  150. }
  151. const ipHdrLen = 20
  152. seedSeq := binary.BigEndian.Uint32(g.hdr[ipHdrLen+4 : ipHdrLen+8])
  153. sport := binary.BigEndian.Uint16(g.hdr[ipHdrLen : ipHdrLen+2])
  154. switch sport {
  155. case 1000:
  156. if seedSeq != 100 {
  157. t.Errorf("flow A seed seq=%d want 100", seedSeq)
  158. }
  159. case 3000:
  160. if seedSeq != 500 {
  161. t.Errorf("flow B seed seq=%d want 500", seedSeq)
  162. }
  163. default:
  164. t.Errorf("unexpected sport %d", sport)
  165. }
  166. }
  167. }
  168. // TestMultiCoalescerEpochOrdersAcrossRehandshake: a re-handshake replaces
  169. // the tunnel, and the replacement's counter space starts near zero — raw
  170. // counter order would emit the new tunnel's packets first while the old
  171. // tunnel's backlog is still arriving. The epoch key must dominate:
  172. // everything from the old tunnel emits before anything from the new one.
  173. func TestMultiCoalescerEpochOrdersAcrossRehandshake(t *testing.T) {
  174. w := &fakeTunWriter{gsoEnabled: true}
  175. m := newTestMultiCoalescer(t, w)
  176. pay := make([]byte, 1200)
  177. // New session's first data arrives before the old session's last data.
  178. if err := m.Commit(buildTCPv4(2200, tcpAck, pay), SortKey{Epoch: 8, Counter: 1}, testPP(buildTCPv4(2200, tcpAck, pay))); err != nil {
  179. t.Fatal(err)
  180. }
  181. if err := m.Commit(buildTCPv4(1000, tcpAck, pay), SortKey{Epoch: 7, Counter: 9_000_000}, testPP(buildTCPv4(1000, tcpAck, pay))); err != nil {
  182. t.Fatal(err)
  183. }
  184. if err := m.Flush(); err != nil {
  185. t.Fatal(err)
  186. }
  187. // Same flow, contiguous seq, identical headers: after the epoch sort the
  188. // two segments append into one superpacket seeded by the OLD session's
  189. // packet.
  190. if len(w.gsoWrites) != 1 {
  191. t.Fatalf("want 1 gso write, got %d (plain=%d)", len(w.gsoWrites), len(w.writes))
  192. }
  193. const ipHdrLen = 20
  194. if seedSeq := binary.BigEndian.Uint32(w.gsoWrites[0].hdr[ipHdrLen+4 : ipHdrLen+8]); seedSeq != 1000 {
  195. t.Errorf("seed seq=%d want 1000 (old session first)", seedSeq)
  196. }
  197. }
  198. // TestMultiCoalescerNoUSOFallsThrough verifies that on a queue without USO
  199. // (older kernel: TSO but no GSO_UDP_L4) the UDP lane never comes up and UDP
  200. // packets still reach the kernel via verbatim rather than being lost.
  201. func TestMultiCoalescerNoUSOFallsThrough(t *testing.T) {
  202. w := &fakeTunWriter{gsoEnabled: true, noUSO: true}
  203. m := newTestMultiCoalescer(t, w)
  204. k := &keySeq{epoch: 1}
  205. if m.udp != nil {
  206. t.Fatal("UDP lane must not come up without USO")
  207. }
  208. if err := m.Commit(buildUDPv4(1000, 53, make([]byte, 800)), k.next(), testPP(buildUDPv4(1000, 53, make([]byte, 800)))); err != nil {
  209. t.Fatal(err)
  210. }
  211. if err := m.Commit(buildUDPv4(1000, 53, make([]byte, 800)), k.next(), testPP(buildUDPv4(1000, 53, make([]byte, 800)))); err != nil {
  212. t.Fatal(err)
  213. }
  214. if err := m.Flush(); err != nil {
  215. t.Fatal(err)
  216. }
  217. if len(w.gsoWrites) != 0 {
  218. t.Errorf("UDP must NOT be coalesced when USO disabled, got %d gso writes", len(w.gsoWrites))
  219. }
  220. if len(w.writes) != 2 {
  221. t.Errorf("UDP must pass through as 2 plain writes, got %d", len(w.writes))
  222. }
  223. }
  224. // TestMultiCoalescerNoOffloadsStillSorts covers a queue that can't offload
  225. // anything. Both lane constructors refuse, so every packet rides the
  226. // verbatim lane — but the staging sort still applies, so emission follows
  227. // transmission order even without GSO.
  228. func TestMultiCoalescerNoOffloadsStillSorts(t *testing.T) {
  229. w := &fakeTunWriter{gsoEnabled: false}
  230. m := newTestMultiCoalescer(t, w)
  231. if m.tcp != nil || m.udp != nil {
  232. t.Fatal("no lane may come up without offloads")
  233. }
  234. pkts := [][]byte{
  235. buildTCPv4(1000, tcpAck, make([]byte, 1200)),
  236. buildUDPv4(1000, 53, make([]byte, 800)),
  237. buildTCPv4(2200, tcpAck, make([]byte, 1200)),
  238. }
  239. // Committed in reverse transmission order; keys carry the truth.
  240. for i := len(pkts) - 1; i >= 0; i-- {
  241. if err := m.Commit(pkts[i], SortKey{Epoch: 1, Counter: uint64(i + 1)}, testPP(pkts[i])); err != nil {
  242. t.Fatal(err)
  243. }
  244. }
  245. if err := m.Flush(); err != nil {
  246. t.Fatal(err)
  247. }
  248. if len(w.gsoWrites) != 0 {
  249. t.Errorf("no GSO writes possible, got %d", len(w.gsoWrites))
  250. }
  251. if len(w.writes) != len(pkts) {
  252. t.Fatalf("want %d plain writes, got %d", len(pkts), len(w.writes))
  253. }
  254. // One lane for everything means the sorted order survives end to end.
  255. for i, want := range pkts {
  256. if !bytes.Equal(w.writes[i], want) {
  257. t.Errorf("write %d out of order or corrupt", i)
  258. }
  259. }
  260. }
  261. // buildUDPv6Fragment builds an IPv6 packet whose extension chain is a
  262. // single fragment header (NH=44) naming UDP as the terminal protocol —
  263. // a first fragment (offset 0, MF set) carrying the UDP header and a
  264. // partial payload.
  265. func buildUDPv6Fragment(sport, dport uint16, payload []byte) []byte {
  266. const ipHdrLen = 40
  267. const fragHdrLen = 8
  268. const udpHdrLen = 8
  269. total := ipHdrLen + fragHdrLen + udpHdrLen + len(payload)
  270. pkt := make([]byte, total)
  271. pkt[0] = 0x60
  272. binary.BigEndian.PutUint16(pkt[4:6], uint16(total-ipHdrLen))
  273. pkt[6] = 44 // fragment extension header
  274. pkt[7] = 64
  275. pkt[8] = 0xfe
  276. pkt[9] = 0x80
  277. pkt[23] = 1
  278. pkt[24] = 0xfe
  279. pkt[25] = 0x80
  280. pkt[39] = 2
  281. pkt[40] = ipProtoUDP // fragment's next header
  282. binary.BigEndian.PutUint16(pkt[42:44], 0x0001) // offset 0, MF set
  283. binary.BigEndian.PutUint32(pkt[44:48], 0x1badf00) // identification
  284. binary.BigEndian.PutUint16(pkt[48:50], sport)
  285. binary.BigEndian.PutUint16(pkt[50:52], dport)
  286. binary.BigEndian.PutUint16(pkt[52:54], uint16(udpHdrLen+len(payload)))
  287. copy(pkt[56:], payload)
  288. return pkt
  289. }
  290. // TestMultiCoalescerIPv6FragmentStaysInLane locks in extension-header
  291. // routing: a fragment whose chain terminates in UDP must ride the UDP lane
  292. // as an in-lane verbatim — emitted ahead of later same-flow datagrams —
  293. // not the verbatim lane, which flushes after every coalescer lane and
  294. // would reorder it behind data that arrived after it.
  295. func TestMultiCoalescerIPv6FragmentStaysInLane(t *testing.T) {
  296. w := &fakeTunWriter{gsoEnabled: true}
  297. m := newTestMultiCoalescer(t, w)
  298. k := &keySeq{epoch: 1}
  299. if err := m.Commit(buildUDPv6Fragment(2000, 53, make([]byte, 512)), k.next(), testPP(buildUDPv6Fragment(2000, 53, make([]byte, 512)))); err != nil {
  300. t.Fatal(err)
  301. }
  302. if err := m.Commit(buildUDPv6(2000, 53, make([]byte, 800)), k.next(), testPP(buildUDPv6(2000, 53, make([]byte, 800)))); err != nil {
  303. t.Fatal(err)
  304. }
  305. if err := m.Commit(buildUDPv6(2000, 53, make([]byte, 800)), k.next(), testPP(buildUDPv6(2000, 53, make([]byte, 800)))); err != nil {
  306. t.Fatal(err)
  307. }
  308. if err := m.Flush(); err != nil {
  309. t.Fatal(err)
  310. }
  311. if len(w.writes) != 1 {
  312. t.Fatalf("want the fragment as 1 plain write, got %d", len(w.writes))
  313. }
  314. if len(w.gsoWrites) != 1 {
  315. t.Fatalf("want the two whole datagrams coalesced into 1 gso write, got %d", len(w.gsoWrites))
  316. }
  317. // Transmission order was fragment-then-data; same-lane routing must keep it.
  318. if w.order[0] != "write" {
  319. t.Fatalf("fragment must be emitted before later data (in-lane verbatim), order=%v", w.order)
  320. }
  321. }
  322. // TestMultiCoalescerFragmentSealsUDPChains: an unparseable datagram
  323. // (fragment) seals every open UDP chain, so datagrams from before and after
  324. // it land in separate superpackets and the fragment holds its transmission-
  325. // order position between them.
  326. func TestMultiCoalescerFragmentSealsUDPChains(t *testing.T) {
  327. w := &fakeTunWriter{gsoEnabled: true}
  328. m := newTestMultiCoalescer(t, w)
  329. k := &keySeq{epoch: 1}
  330. if err := m.Commit(buildUDPv6(2000, 53, make([]byte, 800)), k.next(), testPP(buildUDPv6(2000, 53, make([]byte, 800)))); err != nil {
  331. t.Fatal(err)
  332. }
  333. if err := m.Commit(buildUDPv6(2000, 53, make([]byte, 800)), k.next(), testPP(buildUDPv6(2000, 53, make([]byte, 800)))); err != nil {
  334. t.Fatal(err)
  335. }
  336. if err := m.Commit(buildUDPv6Fragment(2000, 53, make([]byte, 512)), k.next(), testPP(buildUDPv6Fragment(2000, 53, make([]byte, 512)))); err != nil {
  337. t.Fatal(err)
  338. }
  339. if err := m.Commit(buildUDPv6(2000, 53, make([]byte, 800)), k.next(), testPP(buildUDPv6(2000, 53, make([]byte, 800)))); err != nil {
  340. t.Fatal(err)
  341. }
  342. if err := m.Commit(buildUDPv6(2000, 53, make([]byte, 800)), k.next(), testPP(buildUDPv6(2000, 53, make([]byte, 800)))); err != nil {
  343. t.Fatal(err)
  344. }
  345. if err := m.Flush(); err != nil {
  346. t.Fatal(err)
  347. }
  348. if len(w.gsoWrites) != 2 {
  349. t.Fatalf("want 2 gso writes (chains sealed around the fragment), got %d", len(w.gsoWrites))
  350. }
  351. if len(w.writes) != 1 {
  352. t.Fatalf("want the fragment as 1 plain write, got %d", len(w.writes))
  353. }
  354. want := []string{"gso", "write", "gso"}
  355. if len(w.order) != 3 || w.order[0] != want[0] || w.order[1] != want[1] || w.order[2] != want[2] {
  356. t.Fatalf("emission order = %v, want %v", w.order, want)
  357. }
  358. }
  359. // TestMultiCoalescerNoTSOFallsThrough mirrors the no-TSO case.
  360. func TestMultiCoalescerNoTSOFallsThrough(t *testing.T) {
  361. w := &fakeTunWriter{gsoEnabled: true, noTSO: true}
  362. m := newTestMultiCoalescer(t, w)
  363. k := &keySeq{epoch: 1}
  364. if m.tcp != nil {
  365. t.Fatal("TCP lane must not come up without TSO")
  366. }
  367. pay := make([]byte, 1200)
  368. if err := m.Commit(buildTCPv4(1000, tcpAck, pay), k.next(), testPP(buildTCPv4(1000, tcpAck, pay))); err != nil {
  369. t.Fatal(err)
  370. }
  371. if err := m.Commit(buildTCPv4(2200, tcpAck, pay), k.next(), testPP(buildTCPv4(2200, tcpAck, pay))); err != nil {
  372. t.Fatal(err)
  373. }
  374. if err := m.Flush(); err != nil {
  375. t.Fatal(err)
  376. }
  377. if len(w.gsoWrites) != 0 {
  378. t.Errorf("TCP must NOT be coalesced when TSO disabled, got %d gso writes", len(w.gsoWrites))
  379. }
  380. if len(w.writes) != 2 {
  381. t.Errorf("TCP must pass through as 2 plain writes, got %d", len(w.writes))
  382. }
  383. }
  384. // testPP derives the ParsedPacket newPacket would produce for the packet
  385. // shapes the tests build: plain v4/v6, v4 with options or fragment bits set,
  386. // and the single-fragment-header v6 shape from buildUDPv6Fragment. Anything
  387. // unrecognizable stays zero (proto 0 routes to the passthrough lane).
  388. func testPP(pkt []byte) *firewall.ParsedPacket {
  389. pp := &firewall.ParsedPacket{}
  390. if len(pkt) < 20 {
  391. return pp
  392. }
  393. switch pkt[0] >> 4 {
  394. case 4:
  395. pp.Protocol = pkt[9]
  396. pp.IPHdrLen = int(pkt[0]&0x0f) * 4
  397. pp.FragAny = binary.BigEndian.Uint16(pkt[6:8])&0x3fff != 0
  398. case 6:
  399. pp.Protocol = pkt[6]
  400. pp.IPHdrLen = 40
  401. if pp.Protocol == 44 { // fragment extension header
  402. pp.Protocol = pkt[40]
  403. pp.IPHdrLen = 48
  404. pp.FragAny = true
  405. }
  406. }
  407. return pp
  408. }