tcp_coalesce_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  1. package nebula
  2. import (
  3. "encoding/binary"
  4. "testing"
  5. )
  6. // fakeTunWriter records plain Writes and WriteGSO calls without touching a
  7. // real TUN fd. WriteGSO preserves the split between hdr and borrowed pays
  8. // so tests can inspect each independently.
  9. type fakeTunWriter struct {
  10. gsoEnabled bool
  11. writes [][]byte
  12. gsoWrites []fakeGSOWrite
  13. }
  14. type fakeGSOWrite struct {
  15. hdr []byte
  16. pays [][]byte
  17. gsoSize uint16
  18. isV6 bool
  19. csumStart uint16
  20. }
  21. // total returns hdrLen + sum of pay lens.
  22. func (g fakeGSOWrite) total() int {
  23. n := len(g.hdr)
  24. for _, p := range g.pays {
  25. n += len(p)
  26. }
  27. return n
  28. }
  29. // payLen sums the pays.
  30. func (g fakeGSOWrite) payLen() int {
  31. var n int
  32. for _, p := range g.pays {
  33. n += len(p)
  34. }
  35. return n
  36. }
  37. func (w *fakeTunWriter) Write(p []byte) (int, error) {
  38. buf := make([]byte, len(p))
  39. copy(buf, p)
  40. w.writes = append(w.writes, buf)
  41. return len(p), nil
  42. }
  43. func (w *fakeTunWriter) WriteGSO(hdr []byte, pays [][]byte, gsoSize uint16, isV6 bool, csumStart uint16) error {
  44. hcopy := make([]byte, len(hdr))
  45. copy(hcopy, hdr)
  46. paysCopy := make([][]byte, len(pays))
  47. for i, p := range pays {
  48. pc := make([]byte, len(p))
  49. copy(pc, p)
  50. paysCopy[i] = pc
  51. }
  52. w.gsoWrites = append(w.gsoWrites, fakeGSOWrite{
  53. hdr: hcopy,
  54. pays: paysCopy,
  55. gsoSize: gsoSize,
  56. isV6: isV6,
  57. csumStart: csumStart,
  58. })
  59. return nil
  60. }
  61. func (w *fakeTunWriter) GSOSupported() bool { return w.gsoEnabled }
  62. // buildTCPv4 constructs a minimal IPv4+TCP packet with the given payload,
  63. // seq, and flags. Assumes no IP options and a 20-byte TCP header.
  64. func buildTCPv4(seq uint32, flags byte, payload []byte) []byte {
  65. return buildTCPv4Ports(1000, 2000, seq, flags, payload)
  66. }
  67. // buildTCPv4Ports is buildTCPv4 with caller-specified ports so tests can
  68. // build distinct flows.
  69. func buildTCPv4Ports(sport, dport uint16, seq uint32, flags byte, payload []byte) []byte {
  70. const ipHdrLen = 20
  71. const tcpHdrLen = 20
  72. total := ipHdrLen + tcpHdrLen + len(payload)
  73. pkt := make([]byte, total)
  74. pkt[0] = 0x45
  75. pkt[1] = 0x00
  76. binary.BigEndian.PutUint16(pkt[2:4], uint16(total))
  77. binary.BigEndian.PutUint16(pkt[4:6], 0)
  78. binary.BigEndian.PutUint16(pkt[6:8], 0x4000)
  79. pkt[8] = 64
  80. pkt[9] = ipProtoTCP
  81. copy(pkt[12:16], []byte{10, 0, 0, 1})
  82. copy(pkt[16:20], []byte{10, 0, 0, 2})
  83. binary.BigEndian.PutUint16(pkt[20:22], sport)
  84. binary.BigEndian.PutUint16(pkt[22:24], dport)
  85. binary.BigEndian.PutUint32(pkt[24:28], seq)
  86. binary.BigEndian.PutUint32(pkt[28:32], 12345)
  87. pkt[32] = 0x50
  88. pkt[33] = flags
  89. binary.BigEndian.PutUint16(pkt[34:36], 0xffff)
  90. copy(pkt[40:], payload)
  91. return pkt
  92. }
  93. const (
  94. tcpAck = 0x10
  95. tcpPsh = 0x08
  96. tcpSyn = 0x02
  97. tcpFin = 0x01
  98. tcpAckPsh = tcpAck | tcpPsh
  99. )
  100. func TestCoalescerPassthroughWhenGSOUnavailable(t *testing.T) {
  101. w := &fakeTunWriter{gsoEnabled: false}
  102. c := newTCPCoalescer(w)
  103. pkt := buildTCPv4(1000, tcpAck, []byte("hello"))
  104. if err := c.Add(pkt); err != nil {
  105. t.Fatal(err)
  106. }
  107. // No sync write — passthrough is deferred to Flush.
  108. if len(w.writes) != 0 || len(w.gsoWrites) != 0 {
  109. t.Fatalf("no Add-time writes: got writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
  110. }
  111. if err := c.Flush(); err != nil {
  112. t.Fatal(err)
  113. }
  114. if len(w.writes) != 1 || len(w.gsoWrites) != 0 {
  115. t.Fatalf("want single plain write, got writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
  116. }
  117. }
  118. func TestCoalescerNonTCPPassthrough(t *testing.T) {
  119. w := &fakeTunWriter{gsoEnabled: true}
  120. c := newTCPCoalescer(w)
  121. pkt := make([]byte, 28)
  122. pkt[0] = 0x45
  123. binary.BigEndian.PutUint16(pkt[2:4], 28)
  124. pkt[9] = 1
  125. copy(pkt[12:16], []byte{10, 0, 0, 1})
  126. copy(pkt[16:20], []byte{10, 0, 0, 2})
  127. if err := c.Add(pkt); err != nil {
  128. t.Fatal(err)
  129. }
  130. if err := c.Flush(); err != nil {
  131. t.Fatal(err)
  132. }
  133. if len(w.writes) != 1 || len(w.gsoWrites) != 0 {
  134. t.Fatalf("ICMP should pass through unchanged")
  135. }
  136. }
  137. func TestCoalescerSeedThenFlushAlone(t *testing.T) {
  138. w := &fakeTunWriter{gsoEnabled: true}
  139. c := newTCPCoalescer(w)
  140. pkt := buildTCPv4(1000, tcpAck, make([]byte, 1000))
  141. if err := c.Add(pkt); err != nil {
  142. t.Fatal(err)
  143. }
  144. if len(w.writes) != 0 || len(w.gsoWrites) != 0 {
  145. t.Fatalf("unexpected output before flush")
  146. }
  147. if err := c.Flush(); err != nil {
  148. t.Fatal(err)
  149. }
  150. // Single-segment flush now goes through WriteGSO with GSO_NONE
  151. // (virtio NEEDS_CSUM lets the kernel fill in the L4 csum).
  152. if len(w.gsoWrites) != 1 || len(w.writes) != 0 {
  153. t.Fatalf("single-seg flush: writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
  154. }
  155. g := w.gsoWrites[0]
  156. if g.total() != 40+1000 {
  157. t.Errorf("super total=%d want %d", g.total(), 40+1000)
  158. }
  159. if g.payLen() != 1000 {
  160. t.Errorf("payLen=%d want 1000", g.payLen())
  161. }
  162. }
  163. func TestCoalescerCoalescesAdjacentACKs(t *testing.T) {
  164. w := &fakeTunWriter{gsoEnabled: true}
  165. c := newTCPCoalescer(w)
  166. pay := make([]byte, 1200)
  167. if err := c.Add(buildTCPv4(1000, tcpAck, pay)); err != nil {
  168. t.Fatal(err)
  169. }
  170. if err := c.Add(buildTCPv4(2200, tcpAck, pay)); err != nil {
  171. t.Fatal(err)
  172. }
  173. if err := c.Add(buildTCPv4(3400, tcpAck, pay)); err != nil {
  174. t.Fatal(err)
  175. }
  176. if err := c.Flush(); err != nil {
  177. t.Fatal(err)
  178. }
  179. if len(w.gsoWrites) != 1 {
  180. t.Fatalf("want 1 gso write, got %d (plain=%d)", len(w.gsoWrites), len(w.writes))
  181. }
  182. g := w.gsoWrites[0]
  183. if g.gsoSize != 1200 {
  184. t.Errorf("gsoSize=%d want 1200", g.gsoSize)
  185. }
  186. if len(g.hdr) != 40 {
  187. t.Errorf("hdrLen=%d want 40", len(g.hdr))
  188. }
  189. if g.csumStart != 20 {
  190. t.Errorf("csumStart=%d want 20", g.csumStart)
  191. }
  192. if len(g.pays) != 3 {
  193. t.Errorf("pay count=%d want 3", len(g.pays))
  194. }
  195. if g.total() != 40+3*1200 {
  196. t.Errorf("superpacket len=%d want %d", g.total(), 40+3*1200)
  197. }
  198. if tot := binary.BigEndian.Uint16(g.hdr[2:4]); int(tot) != g.total() {
  199. t.Errorf("ip total_length=%d want %d", tot, g.total())
  200. }
  201. }
  202. func TestCoalescerRejectsSeqGap(t *testing.T) {
  203. w := &fakeTunWriter{gsoEnabled: true}
  204. c := newTCPCoalescer(w)
  205. pay := make([]byte, 1200)
  206. if err := c.Add(buildTCPv4(1000, tcpAck, pay)); err != nil {
  207. t.Fatal(err)
  208. }
  209. if err := c.Add(buildTCPv4(3000, tcpAck, pay)); err != nil {
  210. t.Fatal(err)
  211. }
  212. if err := c.Flush(); err != nil {
  213. t.Fatal(err)
  214. }
  215. // Each packet flushes as its own single-segment WriteGSO now.
  216. if len(w.gsoWrites) != 2 || len(w.writes) != 0 {
  217. t.Fatalf("seq gap: want 2 gso writes got writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
  218. }
  219. }
  220. func TestCoalescerRejectsFlagMismatch(t *testing.T) {
  221. w := &fakeTunWriter{gsoEnabled: true}
  222. c := newTCPCoalescer(w)
  223. pay := make([]byte, 1200)
  224. if err := c.Add(buildTCPv4(1000, tcpAck, pay)); err != nil {
  225. t.Fatal(err)
  226. }
  227. // SYN|ACK is non-admissible. Must flush matching flow's slot (gso)
  228. // and then plain-write the SYN packet itself.
  229. syn := buildTCPv4(2200, tcpSyn|tcpAck, pay)
  230. if err := c.Add(syn); err != nil {
  231. t.Fatal(err)
  232. }
  233. if err := c.Flush(); err != nil {
  234. t.Fatal(err)
  235. }
  236. if len(w.writes) != 1 || len(w.gsoWrites) != 1 {
  237. t.Fatalf("flag mismatch: want 1 plain + 1 gso, got writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
  238. }
  239. }
  240. func TestCoalescerRejectsFIN(t *testing.T) {
  241. w := &fakeTunWriter{gsoEnabled: true}
  242. c := newTCPCoalescer(w)
  243. fin := buildTCPv4(1000, tcpAck|tcpFin, []byte("x"))
  244. if err := c.Add(fin); err != nil {
  245. t.Fatal(err)
  246. }
  247. if err := c.Flush(); err != nil {
  248. t.Fatal(err)
  249. }
  250. // FIN isn't admissible — passthrough as plain, no slot, no gso.
  251. if len(w.writes) != 1 || len(w.gsoWrites) != 0 {
  252. t.Fatalf("FIN should be passthrough, got writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
  253. }
  254. }
  255. func TestCoalescerShortLastSegmentClosesChain(t *testing.T) {
  256. w := &fakeTunWriter{gsoEnabled: true}
  257. c := newTCPCoalescer(w)
  258. full := make([]byte, 1200)
  259. half := make([]byte, 500)
  260. if err := c.Add(buildTCPv4(1000, tcpAck, full)); err != nil {
  261. t.Fatal(err)
  262. }
  263. if err := c.Add(buildTCPv4(2200, tcpAck, half)); err != nil {
  264. t.Fatal(err)
  265. }
  266. // Chain now closed; next packet seeds a new slot on the same flow
  267. // after flushing the old one.
  268. if err := c.Add(buildTCPv4(2700, tcpAck, full)); err != nil {
  269. t.Fatal(err)
  270. }
  271. if err := c.Flush(); err != nil {
  272. t.Fatal(err)
  273. }
  274. // Expect two gso writes: the first two packets coalesced, then the
  275. // third flushed alone (single-seg via GSO_NONE).
  276. if len(w.gsoWrites) != 2 {
  277. t.Fatalf("want 2 gso writes got %d", len(w.gsoWrites))
  278. }
  279. if len(w.writes) != 0 {
  280. t.Fatalf("want 0 plain writes got %d", len(w.writes))
  281. }
  282. if w.gsoWrites[0].gsoSize != 1200 {
  283. t.Errorf("gsoSize=%d want 1200", w.gsoWrites[0].gsoSize)
  284. }
  285. if got, want := w.gsoWrites[0].total(), 40+1200+500; got != want {
  286. t.Errorf("super len=%d want %d", got, want)
  287. }
  288. }
  289. func TestCoalescerPSHFinalizesChain(t *testing.T) {
  290. w := &fakeTunWriter{gsoEnabled: true}
  291. c := newTCPCoalescer(w)
  292. pay := make([]byte, 1200)
  293. if err := c.Add(buildTCPv4(1000, tcpAck, pay)); err != nil {
  294. t.Fatal(err)
  295. }
  296. if err := c.Add(buildTCPv4(2200, tcpAckPsh, pay)); err != nil {
  297. t.Fatal(err)
  298. }
  299. if err := c.Add(buildTCPv4(3400, tcpAck, pay)); err != nil {
  300. t.Fatal(err)
  301. }
  302. if err := c.Flush(); err != nil {
  303. t.Fatal(err)
  304. }
  305. // First two coalesce; the third seeds a fresh slot that flushes alone.
  306. if len(w.gsoWrites) != 2 {
  307. t.Fatalf("want 2 gso writes got %d", len(w.gsoWrites))
  308. }
  309. if len(w.writes) != 0 {
  310. t.Fatalf("want 0 plain writes got %d", len(w.writes))
  311. }
  312. }
  313. func TestCoalescerRejectsDifferentFlow(t *testing.T) {
  314. w := &fakeTunWriter{gsoEnabled: true}
  315. c := newTCPCoalescer(w)
  316. pay := make([]byte, 1200)
  317. p1 := buildTCPv4(1000, tcpAck, pay)
  318. p2 := buildTCPv4(2200, tcpAck, pay)
  319. binary.BigEndian.PutUint16(p2[20:22], 9999)
  320. if err := c.Add(p1); err != nil {
  321. t.Fatal(err)
  322. }
  323. if err := c.Add(p2); err != nil {
  324. t.Fatal(err)
  325. }
  326. if err := c.Flush(); err != nil {
  327. t.Fatal(err)
  328. }
  329. // Two independent flows, each flushes its own single-segment WriteGSO.
  330. if len(w.gsoWrites) != 2 || len(w.writes) != 0 {
  331. t.Fatalf("diff flow: want 2 gso writes got writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
  332. }
  333. }
  334. func TestCoalescerRejectsIPOptions(t *testing.T) {
  335. w := &fakeTunWriter{gsoEnabled: true}
  336. c := newTCPCoalescer(w)
  337. pay := make([]byte, 500)
  338. pkt := buildTCPv4(1000, tcpAck, pay)
  339. // Bump IHL to 6 to simulate 4 bytes of IP options. Don't actually add
  340. // bytes — parser should bail before it matters.
  341. pkt[0] = 0x46
  342. if err := c.Add(pkt); err != nil {
  343. t.Fatal(err)
  344. }
  345. if err := c.Flush(); err != nil {
  346. t.Fatal(err)
  347. }
  348. // Non-admissible parse → passthrough as plain.
  349. if len(w.writes) != 1 || len(w.gsoWrites) != 0 {
  350. t.Fatalf("IP options should passthrough, got writes=%d gso=%d", len(w.writes), len(w.gsoWrites))
  351. }
  352. }
  353. func TestCoalescerCapBySegments(t *testing.T) {
  354. w := &fakeTunWriter{gsoEnabled: true}
  355. c := newTCPCoalescer(w)
  356. pay := make([]byte, 512)
  357. seq := uint32(1000)
  358. for i := 0; i < tcpCoalesceMaxSegs+5; i++ {
  359. if err := c.Add(buildTCPv4(seq, tcpAck, pay)); err != nil {
  360. t.Fatal(err)
  361. }
  362. seq += uint32(len(pay))
  363. }
  364. if err := c.Flush(); err != nil {
  365. t.Fatal(err)
  366. }
  367. for _, g := range w.gsoWrites {
  368. segs := len(g.pays)
  369. if segs > tcpCoalesceMaxSegs {
  370. t.Fatalf("super exceeded seg cap: %d > %d", segs, tcpCoalesceMaxSegs)
  371. }
  372. }
  373. }
  374. // TestCoalescerMultipleFlowsInSameBatch proves two interleaved bulk TCP
  375. // flows coalesce independently in a single Flush.
  376. func TestCoalescerMultipleFlowsInSameBatch(t *testing.T) {
  377. w := &fakeTunWriter{gsoEnabled: true}
  378. c := newTCPCoalescer(w)
  379. pay := make([]byte, 1200)
  380. // Flow A: sport 1000. Flow B: sport 3000.
  381. if err := c.Add(buildTCPv4Ports(1000, 2000, 100, tcpAck, pay)); err != nil {
  382. t.Fatal(err)
  383. }
  384. if err := c.Add(buildTCPv4Ports(3000, 2000, 500, tcpAck, pay)); err != nil {
  385. t.Fatal(err)
  386. }
  387. if err := c.Add(buildTCPv4Ports(1000, 2000, 1300, tcpAck, pay)); err != nil {
  388. t.Fatal(err)
  389. }
  390. if err := c.Add(buildTCPv4Ports(3000, 2000, 1700, tcpAck, pay)); err != nil {
  391. t.Fatal(err)
  392. }
  393. if err := c.Add(buildTCPv4Ports(1000, 2000, 2500, tcpAck, pay)); err != nil {
  394. t.Fatal(err)
  395. }
  396. if err := c.Add(buildTCPv4Ports(3000, 2000, 2900, tcpAck, pay)); err != nil {
  397. t.Fatal(err)
  398. }
  399. if err := c.Flush(); err != nil {
  400. t.Fatal(err)
  401. }
  402. if len(w.gsoWrites) != 2 {
  403. t.Fatalf("want 2 gso writes (one per flow), got %d", len(w.gsoWrites))
  404. }
  405. if len(w.writes) != 0 {
  406. t.Fatalf("want no plain writes, got %d", len(w.writes))
  407. }
  408. // Each superpacket should carry 3 segments.
  409. for i, g := range w.gsoWrites {
  410. if len(g.pays) != 3 {
  411. t.Errorf("gso[%d]: segs=%d want 3", i, len(g.pays))
  412. }
  413. if g.gsoSize != 1200 {
  414. t.Errorf("gso[%d]: gsoSize=%d want 1200", i, g.gsoSize)
  415. }
  416. }
  417. // Verify each superpacket carries the source port it was seeded with.
  418. seenSports := map[uint16]bool{}
  419. for _, g := range w.gsoWrites {
  420. sp := binary.BigEndian.Uint16(g.hdr[20:22])
  421. seenSports[sp] = true
  422. }
  423. if !seenSports[1000] || !seenSports[3000] {
  424. t.Errorf("expected superpackets for sports 1000 and 3000, got %v", seenSports)
  425. }
  426. }
  427. // TestCoalescerPreservesArrivalOrder confirms that with passthrough and
  428. // coalesced events both queued, Flush emits them in Add order rather than
  429. // writing passthrough packets synchronously.
  430. func TestCoalescerPreservesArrivalOrder(t *testing.T) {
  431. w := &orderedFakeWriter{gsoEnabled: true}
  432. c := newTCPCoalescer(w)
  433. // Sequence: coalesceable TCP, ICMP (passthrough), coalesceable TCP on
  434. // a different flow. Expected emit order: gso(X), plain(ICMP), gso(Y).
  435. pay := make([]byte, 1200)
  436. if err := c.Add(buildTCPv4Ports(1000, 2000, 100, tcpAck, pay)); err != nil {
  437. t.Fatal(err)
  438. }
  439. icmp := make([]byte, 28)
  440. icmp[0] = 0x45
  441. binary.BigEndian.PutUint16(icmp[2:4], 28)
  442. icmp[9] = 1
  443. copy(icmp[12:16], []byte{10, 0, 0, 1})
  444. copy(icmp[16:20], []byte{10, 0, 0, 3})
  445. if err := c.Add(icmp); err != nil {
  446. t.Fatal(err)
  447. }
  448. if err := c.Add(buildTCPv4Ports(3000, 2000, 500, tcpAck, pay)); err != nil {
  449. t.Fatal(err)
  450. }
  451. // Nothing should have hit the writer synchronously.
  452. if len(w.events) != 0 {
  453. t.Fatalf("Add emitted events synchronously: %v", w.events)
  454. }
  455. if err := c.Flush(); err != nil {
  456. t.Fatal(err)
  457. }
  458. if got, want := w.events, []string{"gso", "plain", "gso"}; !stringSliceEq(got, want) {
  459. t.Fatalf("flush order=%v want %v", got, want)
  460. }
  461. }
  462. // orderedFakeWriter records only the sequence of call types so tests can
  463. // assert arrival order without inspecting bytes.
  464. type orderedFakeWriter struct {
  465. gsoEnabled bool
  466. events []string
  467. }
  468. func (w *orderedFakeWriter) Write(p []byte) (int, error) {
  469. w.events = append(w.events, "plain")
  470. return len(p), nil
  471. }
  472. func (w *orderedFakeWriter) WriteGSO(hdr []byte, pays [][]byte, gsoSize uint16, isV6 bool, csumStart uint16) error {
  473. w.events = append(w.events, "gso")
  474. return nil
  475. }
  476. func (w *orderedFakeWriter) GSOSupported() bool { return w.gsoEnabled }
  477. func stringSliceEq(a, b []string) bool {
  478. if len(a) != len(b) {
  479. return false
  480. }
  481. for i := range a {
  482. if a[i] != b[i] {
  483. return false
  484. }
  485. }
  486. return true
  487. }
  488. // TestCoalescerInterleavedFlowsPreserveOrdering checks that a non-admissible
  489. // packet (SYN) mid-flow only flushes its own flow, not others.
  490. func TestCoalescerInterleavedFlowsPreserveOrdering(t *testing.T) {
  491. w := &fakeTunWriter{gsoEnabled: true}
  492. c := newTCPCoalescer(w)
  493. pay := make([]byte, 1200)
  494. // Flow A two segments.
  495. if err := c.Add(buildTCPv4Ports(1000, 2000, 100, tcpAck, pay)); err != nil {
  496. t.Fatal(err)
  497. }
  498. if err := c.Add(buildTCPv4Ports(1000, 2000, 1300, tcpAck, pay)); err != nil {
  499. t.Fatal(err)
  500. }
  501. // Flow B two segments.
  502. if err := c.Add(buildTCPv4Ports(3000, 2000, 500, tcpAck, pay)); err != nil {
  503. t.Fatal(err)
  504. }
  505. if err := c.Add(buildTCPv4Ports(3000, 2000, 1700, tcpAck, pay)); err != nil {
  506. t.Fatal(err)
  507. }
  508. // Flow A SYN (non-admissible) — must flush only flow A's slot.
  509. syn := buildTCPv4Ports(1000, 2000, 9999, tcpSyn|tcpAck, pay)
  510. if err := c.Add(syn); err != nil {
  511. t.Fatal(err)
  512. }
  513. // Flow B continues — should still be coalesced with its seed.
  514. if err := c.Add(buildTCPv4Ports(3000, 2000, 2900, tcpAck, pay)); err != nil {
  515. t.Fatal(err)
  516. }
  517. if err := c.Flush(); err != nil {
  518. t.Fatal(err)
  519. }
  520. // Expected:
  521. // - 1 gso for flow A (first 2 segments)
  522. // - 1 plain for flow A SYN
  523. // - 1 gso for flow B (3 segments)
  524. if len(w.gsoWrites) != 2 {
  525. t.Fatalf("want 2 gso writes, got %d", len(w.gsoWrites))
  526. }
  527. if len(w.writes) != 1 {
  528. t.Fatalf("want 1 plain write (SYN), got %d", len(w.writes))
  529. }
  530. // Find the 3-segment gso (flow B) and the 2-segment gso (flow A).
  531. var segCounts []int
  532. for _, g := range w.gsoWrites {
  533. segCounts = append(segCounts, len(g.pays))
  534. }
  535. if !(segCounts[0] == 2 && segCounts[1] == 3) && !(segCounts[0] == 3 && segCounts[1] == 2) {
  536. t.Errorf("unexpected segment counts: %v (want 2 and 3)", segCounts)
  537. }
  538. }