1
0

udp_coalesce_test.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. package batch
  2. import (
  3. "bytes"
  4. "encoding/binary"
  5. "io"
  6. "testing"
  7. )
  8. // buildUDPv4 builds a minimal IPv4+UDP packet with the given payload and ports.
  9. func buildUDPv4(sport, dport uint16, payload []byte) []byte {
  10. const ipHdrLen = 20
  11. const udpHdrLen = 8
  12. total := ipHdrLen + udpHdrLen + len(payload)
  13. pkt := make([]byte, total)
  14. pkt[0] = 0x45
  15. pkt[1] = 0x00
  16. binary.BigEndian.PutUint16(pkt[2:4], uint16(total))
  17. binary.BigEndian.PutUint16(pkt[4:6], 0)
  18. binary.BigEndian.PutUint16(pkt[6:8], 0x4000)
  19. pkt[8] = 64
  20. pkt[9] = ipProtoUDP
  21. copy(pkt[12:16], []byte{10, 0, 0, 1})
  22. copy(pkt[16:20], []byte{10, 0, 0, 2})
  23. binary.BigEndian.PutUint16(pkt[20:22], sport)
  24. binary.BigEndian.PutUint16(pkt[22:24], dport)
  25. binary.BigEndian.PutUint16(pkt[24:26], uint16(udpHdrLen+len(payload)))
  26. binary.BigEndian.PutUint16(pkt[26:28], 0)
  27. copy(pkt[28:], payload)
  28. return pkt
  29. }
  30. // buildUDPv6 builds a minimal IPv6+UDP packet.
  31. func buildUDPv6(sport, dport uint16, payload []byte) []byte {
  32. const ipHdrLen = 40
  33. const udpHdrLen = 8
  34. total := ipHdrLen + udpHdrLen + len(payload)
  35. pkt := make([]byte, total)
  36. pkt[0] = 0x60
  37. binary.BigEndian.PutUint16(pkt[4:6], uint16(udpHdrLen+len(payload)))
  38. pkt[6] = ipProtoUDP
  39. pkt[7] = 64
  40. pkt[8] = 0xfe
  41. pkt[9] = 0x80
  42. pkt[23] = 1
  43. pkt[24] = 0xfe
  44. pkt[25] = 0x80
  45. pkt[39] = 2
  46. binary.BigEndian.PutUint16(pkt[40:42], sport)
  47. binary.BigEndian.PutUint16(pkt[42:44], dport)
  48. binary.BigEndian.PutUint16(pkt[44:46], uint16(udpHdrLen+len(payload)))
  49. binary.BigEndian.PutUint16(pkt[46:48], 0)
  50. copy(pkt[48:], payload)
  51. return pkt
  52. }
  53. // newTestUDPCoalescer builds a coalescer over w and fails the test if w can't
  54. // do USO. See newTestTCPCoalescer.
  55. func newTestUDPCoalescer(tb testing.TB, w io.Writer) *UDPCoalescer {
  56. tb.Helper()
  57. c := NewUDPCoalescer(w)
  58. if c == nil {
  59. tb.Fatal("NewUDPCoalescer: writer does not support USO")
  60. }
  61. return c
  62. }
  63. // TestNewUDPCoalescerRefusesWhenGSOUnavailable mirrors the TCP precondition:
  64. // no USO, no coalescer.
  65. func TestNewUDPCoalescerRefusesWhenGSOUnavailable(t *testing.T) {
  66. if c := NewUDPCoalescer(&fakeTunWriter{gsoEnabled: false}); c != nil {
  67. t.Fatalf("want nil for a non-USO writer, got %v", c)
  68. }
  69. if c := NewUDPCoalescer(&plainOnlyWriter{}); c != nil {
  70. t.Fatalf("want nil for a plain writer, got %v", c)
  71. }
  72. }
  73. func TestUDPCoalescerNonUDPPassthrough(t *testing.T) {
  74. w := &fakeTunWriter{gsoEnabled: true}
  75. c := newTestUDPCoalescer(t, w)
  76. // ICMP packet
  77. pkt := make([]byte, 28)
  78. pkt[0] = 0x45
  79. binary.BigEndian.PutUint16(pkt[2:4], 28)
  80. pkt[9] = 1
  81. copy(pkt[12:16], []byte{10, 0, 0, 1})
  82. copy(pkt[16:20], []byte{10, 0, 0, 2})
  83. if err := c.Commit(pkt); err != nil {
  84. t.Fatal(err)
  85. }
  86. if err := c.Flush(); err != nil {
  87. t.Fatal(err)
  88. }
  89. if len(w.writes) != 1 || len(w.gsoWrites) != 0 {
  90. t.Fatalf("ICMP must pass through unchanged: writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
  91. }
  92. }
  93. func TestUDPCoalescerSeedThenFlushAlone(t *testing.T) {
  94. w := &fakeTunWriter{gsoEnabled: true}
  95. c := newTestUDPCoalescer(t, w)
  96. pkt := buildUDPv4(1000, 53, make([]byte, 800))
  97. if err := c.Commit(pkt); err != nil {
  98. t.Fatal(err)
  99. }
  100. if err := c.Flush(); err != nil {
  101. t.Fatal(err)
  102. }
  103. // A slot that never grew past one datagram flushes as a plain Write of
  104. // the original packet bytes: the original (already valid) checksum
  105. // ships via the DATA_VALID path, so the kernel does no csum work.
  106. // WriteGSO is reserved for slots that actually coalesced (>=2 segs).
  107. if len(w.writes) != 1 || len(w.gsoWrites) != 0 {
  108. t.Fatalf("single-seg flush: writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
  109. }
  110. if !bytes.Equal(w.writes[0], pkt) {
  111. t.Errorf("plain write not byte-identical to committed packet: got %d bytes want %d", len(w.writes[0]), len(pkt))
  112. }
  113. }
  114. func TestUDPCoalescerCoalescesEqualSized(t *testing.T) {
  115. w := &fakeTunWriter{gsoEnabled: true}
  116. c := newTestUDPCoalescer(t, w)
  117. pay := make([]byte, 1200)
  118. for range 3 {
  119. if err := c.Commit(buildUDPv4(1000, 53, pay)); err != nil {
  120. t.Fatal(err)
  121. }
  122. }
  123. if err := c.Flush(); err != nil {
  124. t.Fatal(err)
  125. }
  126. if len(w.gsoWrites) != 1 {
  127. t.Fatalf("want 1 gso write, got %d (plain=%d)", len(w.gsoWrites), len(w.writes))
  128. }
  129. g := w.gsoWrites[0]
  130. if g.gsoSize != 1200 {
  131. t.Errorf("gsoSize=%d want 1200", g.gsoSize)
  132. }
  133. if len(g.pays) != 3 {
  134. t.Errorf("pay count=%d want 3", len(g.pays))
  135. }
  136. if g.csumStart != 20 {
  137. t.Errorf("csumStart=%d want 20", g.csumStart)
  138. }
  139. // IP totalLen and UDP length must be the TOTAL across all segments —
  140. // the kernel's ip_rcv_core trims skbs to iph->tot_len, so a per-segment
  141. // value would silently drop everything but the first segment. Total =
  142. // IP(20) + UDP(8) + 3*1200 = 3628.
  143. gotTotalLen := binary.BigEndian.Uint16(g.hdr[2:4])
  144. if gotTotalLen != 3628 {
  145. t.Errorf("ipv4 total_len=%d want 3628 (must be total across segments)", gotTotalLen)
  146. }
  147. gotUDPLen := binary.BigEndian.Uint16(g.hdr[20+4 : 20+6])
  148. if gotUDPLen != 8+3*1200 {
  149. t.Errorf("udp len=%d want %d", gotUDPLen, 8+3*1200)
  150. }
  151. }
  152. // Last segment may be shorter, sealing the chain.
  153. func TestUDPCoalescerShortLastSegmentSeals(t *testing.T) {
  154. w := &fakeTunWriter{gsoEnabled: true}
  155. c := newTestUDPCoalescer(t, w)
  156. full := make([]byte, 1200)
  157. tail := make([]byte, 600)
  158. if err := c.Commit(buildUDPv4(1000, 53, full)); err != nil {
  159. t.Fatal(err)
  160. }
  161. if err := c.Commit(buildUDPv4(1000, 53, full)); err != nil {
  162. t.Fatal(err)
  163. }
  164. if err := c.Commit(buildUDPv4(1000, 53, tail)); err != nil {
  165. t.Fatal(err)
  166. }
  167. // A 4th packet, even same-sized, must NOT join — chain is sealed.
  168. if err := c.Commit(buildUDPv4(1000, 53, full)); err != nil {
  169. t.Fatal(err)
  170. }
  171. if err := c.Flush(); err != nil {
  172. t.Fatal(err)
  173. }
  174. // The sealed 3-datagram chain is a real superpacket; the re-seed stays
  175. // single-segment and flushes as a plain write of the original packet.
  176. if len(w.gsoWrites) != 1 || len(w.writes) != 1 {
  177. t.Fatalf("want 1 gso (sealed) + 1 plain (new seed), got gso=%d plain=%d", len(w.gsoWrites), len(w.writes))
  178. }
  179. if len(w.gsoWrites[0].pays) != 3 {
  180. t.Errorf("super: want 3 pays, got %d", len(w.gsoWrites[0].pays))
  181. }
  182. if got, want := len(w.writes[0]), 20+8+1200; got != want {
  183. t.Errorf("re-seed plain write len=%d want %d", got, want)
  184. }
  185. }
  186. // A larger-than-gsoSize packet cannot extend the slot — it reseeds.
  187. func TestUDPCoalescerLargerThanSeedReseeds(t *testing.T) {
  188. w := &fakeTunWriter{gsoEnabled: true}
  189. c := newTestUDPCoalescer(t, w)
  190. if err := c.Commit(buildUDPv4(1000, 53, make([]byte, 800))); err != nil {
  191. t.Fatal(err)
  192. }
  193. if err := c.Commit(buildUDPv4(1000, 53, make([]byte, 1200))); err != nil {
  194. t.Fatal(err)
  195. }
  196. if err := c.Flush(); err != nil {
  197. t.Fatal(err)
  198. }
  199. // Both seeds stay single-segment → two plain writes in arrival order.
  200. if len(w.writes) != 2 || len(w.gsoWrites) != 0 {
  201. t.Fatalf("want 2 separate plain writes, got writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
  202. }
  203. for i, want := range []int{20 + 8 + 800, 20 + 8 + 1200} {
  204. if len(w.writes[i]) != want {
  205. t.Errorf("write %d len=%d want %d", i, len(w.writes[i]), want)
  206. }
  207. }
  208. }
  209. // Different 5-tuples must not coalesce.
  210. func TestUDPCoalescerDifferentFlowsKeepSeparate(t *testing.T) {
  211. w := &fakeTunWriter{gsoEnabled: true}
  212. c := newTestUDPCoalescer(t, w)
  213. pay := make([]byte, 800)
  214. if err := c.Commit(buildUDPv4(1000, 53, pay)); err != nil {
  215. t.Fatal(err)
  216. }
  217. if err := c.Commit(buildUDPv4(2000, 53, pay)); err != nil {
  218. t.Fatal(err)
  219. }
  220. if err := c.Commit(buildUDPv4(1000, 53, pay)); err != nil {
  221. t.Fatal(err)
  222. }
  223. if err := c.Commit(buildUDPv4(2000, 53, pay)); err != nil {
  224. t.Fatal(err)
  225. }
  226. if err := c.Flush(); err != nil {
  227. t.Fatal(err)
  228. }
  229. // Two flows × 2 datagrams each = 2 superpackets of 2 segments.
  230. if len(w.gsoWrites) != 2 {
  231. t.Fatalf("want 2 gso writes (one per flow), got %d", len(w.gsoWrites))
  232. }
  233. for i, g := range w.gsoWrites {
  234. if len(g.pays) != 2 {
  235. t.Errorf("super %d: want 2 pays, got %d", i, len(g.pays))
  236. }
  237. }
  238. }
  239. // Caps at udpCoalesceMaxSegs.
  240. func TestUDPCoalescerCapsAtMaxSegs(t *testing.T) {
  241. w := &fakeTunWriter{gsoEnabled: true}
  242. c := newTestUDPCoalescer(t, w)
  243. pay := make([]byte, 100)
  244. for range udpCoalesceMaxSegs + 5 {
  245. if err := c.Commit(buildUDPv4(1000, 53, pay)); err != nil {
  246. t.Fatal(err)
  247. }
  248. }
  249. if err := c.Flush(); err != nil {
  250. t.Fatal(err)
  251. }
  252. // First superpacket holds udpCoalesceMaxSegs segments; the spillover
  253. // reseeds a new one.
  254. if len(w.gsoWrites) != 2 {
  255. t.Fatalf("want 2 gso writes (cap then reseed), got %d", len(w.gsoWrites))
  256. }
  257. if len(w.gsoWrites[0].pays) != udpCoalesceMaxSegs {
  258. t.Errorf("first super: pays=%d want %d", len(w.gsoWrites[0].pays), udpCoalesceMaxSegs)
  259. }
  260. if len(w.gsoWrites[1].pays) != 5 {
  261. t.Errorf("second super: pays=%d want 5", len(w.gsoWrites[1].pays))
  262. }
  263. }
  264. // Differing IP ECN codepoints must not coalesce: udpHeadersMatch compares
  265. // the full ToS byte (matching kernel GRO). A CE-marked datagram mid-run
  266. // seals the Not-ECT chain and reseeds; the trailing Not-ECT datagram
  267. // reseeds again. All three stay single-segment, so each ships as a plain
  268. // write of its original bytes, keeping its own codepoint.
  269. func TestUDPCoalescerDifferingECNReseeds(t *testing.T) {
  270. w := &fakeTunWriter{gsoEnabled: true}
  271. c := newTestUDPCoalescer(t, w)
  272. pay := make([]byte, 800)
  273. pkt0 := buildUDPv4(1000, 53, pay) // ECN=00 (Not-ECT)
  274. pkt1 := buildUDPv4(1000, 53, pay)
  275. pkt1[1] = 0x03 // CE
  276. pkt2 := buildUDPv4(1000, 53, pay) // ECN=00 again
  277. for _, p := range [][]byte{pkt0, pkt1, pkt2} {
  278. if err := c.Commit(p); err != nil {
  279. t.Fatal(err)
  280. }
  281. }
  282. if err := c.Flush(); err != nil {
  283. t.Fatal(err)
  284. }
  285. if len(w.writes) != 3 || len(w.gsoWrites) != 0 {
  286. t.Fatalf("want 3 separate plain writes (differing ECN), got writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
  287. }
  288. wantECN := []byte{0x00, 0x03, 0x00}
  289. for i, p := range w.writes {
  290. if got := p[1] & 0x03; got != wantECN[i] {
  291. t.Errorf("write %d ECN=%#x want %#x", i, got, wantECN[i])
  292. }
  293. }
  294. }
  295. // IPv6 path: same flow, equal-sized → coalesced.
  296. func TestUDPCoalescerIPv6Coalesces(t *testing.T) {
  297. w := &fakeTunWriter{gsoEnabled: true}
  298. c := newTestUDPCoalescer(t, w)
  299. pay := make([]byte, 1200)
  300. for range 3 {
  301. if err := c.Commit(buildUDPv6(1000, 53, pay)); err != nil {
  302. t.Fatal(err)
  303. }
  304. }
  305. if err := c.Flush(); err != nil {
  306. t.Fatal(err)
  307. }
  308. if len(w.gsoWrites) != 1 {
  309. t.Fatalf("want 1 gso write, got %d", len(w.gsoWrites))
  310. }
  311. g := w.gsoWrites[0]
  312. if !g.isV6 {
  313. t.Errorf("expected v6 write")
  314. }
  315. if g.csumStart != 40 {
  316. t.Errorf("csumStart=%d want 40", g.csumStart)
  317. }
  318. // IPv6 payload_len and UDP length must be TOTAL — kernel's
  319. // ip6_rcv_core trims to payload_len + ipv6 hdr size. Total UDP = 8 +
  320. // 3*1200 = 3608.
  321. gotPlen := binary.BigEndian.Uint16(g.hdr[4:6])
  322. if gotPlen != 8+3*1200 {
  323. t.Errorf("ipv6 payload_len=%d want %d (must be total)", gotPlen, 8+3*1200)
  324. }
  325. gotUDPLen := binary.BigEndian.Uint16(g.hdr[40+4 : 40+6])
  326. if gotUDPLen != 8+3*1200 {
  327. t.Errorf("udp len=%d want %d", gotUDPLen, 8+3*1200)
  328. }
  329. }
  330. // DSCP differences must reseed: udpHeadersMatch compares the full ToS byte.
  331. func TestUDPCoalescerDSCPMismatchReseeds(t *testing.T) {
  332. w := &fakeTunWriter{gsoEnabled: true}
  333. c := newTestUDPCoalescer(t, w)
  334. pay := make([]byte, 800)
  335. pkt0 := buildUDPv4(1000, 53, pay)
  336. pkt1 := buildUDPv4(1000, 53, pay)
  337. pkt1[1] = 0xb8 // EF DSCP, ECN=0
  338. if err := c.Commit(pkt0); err != nil {
  339. t.Fatal(err)
  340. }
  341. if err := c.Commit(pkt1); err != nil {
  342. t.Fatal(err)
  343. }
  344. if err := c.Flush(); err != nil {
  345. t.Fatal(err)
  346. }
  347. // Both seeds stay single-segment → two plain writes, no gso.
  348. if len(w.writes) != 2 || len(w.gsoWrites) != 0 {
  349. t.Fatalf("want 2 separate plain writes (different DSCP), got writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
  350. }
  351. }
  352. // Fragmented IPv4 must not be coalesced.
  353. func TestUDPCoalescerFragmentedIPv4PassesThrough(t *testing.T) {
  354. w := &fakeTunWriter{gsoEnabled: true}
  355. c := newTestUDPCoalescer(t, w)
  356. pkt := buildUDPv4(1000, 53, make([]byte, 200))
  357. binary.BigEndian.PutUint16(pkt[6:8], 0x2000) // MF=1
  358. if err := c.Commit(pkt); err != nil {
  359. t.Fatal(err)
  360. }
  361. if err := c.Flush(); err != nil {
  362. t.Fatal(err)
  363. }
  364. if len(w.writes) != 1 || len(w.gsoWrites) != 0 {
  365. t.Fatalf("frag must pass through plain, got writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
  366. }
  367. }
  368. // A zero-length UDP datagram (UDP length == 8, no payload) is legal and
  369. // must be delivered as a plain single datagram — never coalesced. Seeding
  370. // it into a GSO slot stores an empty payload iovec that panics WriteGSO
  371. // (index-out-of-range on &pay[0]); this is a remote DoS if we ever let it
  372. // reach the GSO path. Regression: must not panic and must be written.
  373. func TestUDPCoalescerZeroLengthPayloadPassesThrough(t *testing.T) {
  374. w := &fakeTunWriter{gsoEnabled: true}
  375. c := newTestUDPCoalescer(t, w)
  376. pkt := buildUDPv4(1000, 53, nil) // UDP length 8, zero payload
  377. if err := c.Commit(pkt); err != nil {
  378. t.Fatal(err)
  379. }
  380. if err := c.Flush(); err != nil {
  381. t.Fatal(err)
  382. }
  383. if len(w.writes) != 1 || len(w.gsoWrites) != 0 {
  384. t.Fatalf("zero-length UDP must pass through plain, got writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
  385. }
  386. if len(w.writes[0]) != len(pkt) {
  387. t.Errorf("delivered %d bytes, want the whole %d-byte datagram", len(w.writes[0]), len(pkt))
  388. }
  389. }
  390. // IPv6 zero-length UDP datagram: same verbatim contract as v4.
  391. func TestUDPCoalescerZeroLengthPayloadIPv6PassesThrough(t *testing.T) {
  392. w := &fakeTunWriter{gsoEnabled: true}
  393. c := newTestUDPCoalescer(t, w)
  394. pkt := buildUDPv6(1000, 53, nil) // UDP length 8, zero payload
  395. if err := c.Commit(pkt); err != nil {
  396. t.Fatal(err)
  397. }
  398. if err := c.Flush(); err != nil {
  399. t.Fatal(err)
  400. }
  401. if len(w.writes) != 1 || len(w.gsoWrites) != 0 {
  402. t.Fatalf("zero-length IPv6 UDP must pass through plain, got writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
  403. }
  404. if len(w.writes[0]) != len(pkt) {
  405. t.Errorf("delivered %d bytes, want the whole %d-byte datagram", len(w.writes[0]), len(pkt))
  406. }
  407. }
  408. // A zero-length datagram arriving mid-flow must seal the open chain so the
  409. // datagram after it seeds a fresh superpacket *after* the empty one on the
  410. // wire — per-flow arrival order (full, empty, full) must be preserved.
  411. func TestUDPCoalescerZeroLengthMidFlowSealsAndPreservesOrder(t *testing.T) {
  412. w := &fakeTunWriter{gsoEnabled: true}
  413. c := newTestUDPCoalescer(t, w)
  414. full := make([]byte, 800)
  415. if err := c.Commit(buildUDPv4(1000, 53, full)); err != nil {
  416. t.Fatal(err)
  417. }
  418. if err := c.Commit(buildUDPv4(1000, 53, nil)); err != nil { // zero-length
  419. t.Fatal(err)
  420. }
  421. if err := c.Commit(buildUDPv4(1000, 53, full)); err != nil {
  422. t.Fatal(err)
  423. }
  424. if err := c.Flush(); err != nil {
  425. t.Fatal(err)
  426. }
  427. // The empty datagram sealed the first slot, so the trailing full packet
  428. // can't join it. All three emit as plain writes (the two full datagrams
  429. // stayed single-segment; the empty one is verbatim) in per-flow
  430. // arrival order: full, empty, full.
  431. if len(w.writes) != 3 || len(w.gsoWrites) != 0 {
  432. t.Fatalf("want 3 plain writes, got gso=%d plain=%d", len(w.gsoWrites), len(w.writes))
  433. }
  434. for i, want := range []int{20 + 8 + 800, 20 + 8, 20 + 8 + 800} {
  435. if len(w.writes[i]) != want {
  436. t.Errorf("write %d len=%d want %d (order full, empty, full)", i, len(w.writes[i]), want)
  437. }
  438. }
  439. }
  440. // IPv4 with options is not admissible (we require IHL=5).
  441. func TestUDPCoalescerIPv4WithOptionsPassesThrough(t *testing.T) {
  442. w := &fakeTunWriter{gsoEnabled: true}
  443. c := newTestUDPCoalescer(t, w)
  444. pkt := buildUDPv4(1000, 53, make([]byte, 200))
  445. pkt[0] = 0x46 // IHL = 6 (24-byte IPv4 header — has options)
  446. if err := c.Commit(pkt); err != nil {
  447. t.Fatal(err)
  448. }
  449. if err := c.Flush(); err != nil {
  450. t.Fatal(err)
  451. }
  452. if len(w.writes) != 1 || len(w.gsoWrites) != 0 {
  453. t.Fatalf("ipv4-with-options must pass through plain, got writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
  454. }
  455. }
  456. // TestUDPCoalescerNonAtomicSequentialIDsCoalesce mirrors the TCP rule: DF
  457. // clear is fine as long as the IDs already run seed+1 per datagram, so
  458. // kernel USO's re-stamp reproduces them.
  459. func TestUDPCoalescerNonAtomicSequentialIDsCoalesce(t *testing.T) {
  460. w := &fakeTunWriter{gsoEnabled: true}
  461. c := newTestUDPCoalescer(t, w)
  462. pay := make([]byte, 1200)
  463. for i := range 2 {
  464. pkt := buildUDPv4(40000, 443, pay)
  465. setIPv4ID(pkt, uint16(40+i), false)
  466. if err := c.Commit(pkt); err != nil {
  467. t.Fatal(err)
  468. }
  469. }
  470. if err := c.Flush(); err != nil {
  471. t.Fatal(err)
  472. }
  473. if len(w.gsoWrites) != 1 || len(w.gsoWrites[0].pays) != 2 {
  474. t.Fatalf("sequential-ID DF=0 datagrams must coalesce: gso=%d", len(w.gsoWrites))
  475. }
  476. }
  477. // TestUDPCoalescerNonAtomicIDGapReseeds: an ID jump on a DF=0 flow breaks
  478. // the chain; each datagram stays a single-segment slot and flushes as a
  479. // plain write that keeps its own (meaningful) ID.
  480. func TestUDPCoalescerNonAtomicIDGapReseeds(t *testing.T) {
  481. w := &fakeTunWriter{gsoEnabled: true}
  482. c := newTestUDPCoalescer(t, w)
  483. pay := make([]byte, 1200)
  484. p1 := buildUDPv4(40000, 443, pay)
  485. setIPv4ID(p1, 40, false)
  486. p2 := buildUDPv4(40000, 443, pay)
  487. setIPv4ID(p2, 50, false)
  488. if err := c.Commit(p1); err != nil {
  489. t.Fatal(err)
  490. }
  491. if err := c.Commit(p2); err != nil {
  492. t.Fatal(err)
  493. }
  494. if err := c.Flush(); err != nil {
  495. t.Fatal(err)
  496. }
  497. if len(w.writes) != 2 || len(w.gsoWrites) != 0 {
  498. t.Fatalf("ID gap on DF=0 must reseed: writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
  499. }
  500. for i, want := range []uint16{40, 50} {
  501. if id := binary.BigEndian.Uint16(w.writes[i][4:6]); id != want {
  502. t.Errorf("write %d: ID=%d want %d (must be preserved)", i, id, want)
  503. }
  504. }
  505. }