1
0

tcp_coalesce.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. package batch
  2. import (
  3. "bytes"
  4. "encoding/binary"
  5. "io"
  6. "log/slog"
  7. "github.com/slackhq/nebula/overlay/tio"
  8. )
  9. // ipProtoTCP is the IANA protocol number for TCP. Defined here to help Windows out.
  10. const ipProtoTCP = 6
  11. // tcpCoalesceBufSize caps total bytes per superpacket. Mirrors the kernel's
  12. // sk_gso_max_size of ~64KiB; anything beyond this would be rejected anyway.
  13. const tcpCoalesceBufSize = 65535
  14. // tcpCoalesceMaxSegs caps how many segments we'll coalesce into a single
  15. // superpacket. Keeping this well below the kernel's TSO ceiling bounds latency.
  16. const tcpCoalesceMaxSegs = 64
  17. // coalesceSlot is one entry in the coalescer's ordered event queue. A verbatim slot holds a single
  18. // borrowed packet emitted as-is (pure ACK, non-admissible TCP, unparseable, or oversize seed); a
  19. // non-verbatim slot is an in-progress coalesced superpacket. payIovs are borrowed slices of the
  20. // caller's plaintext buffers; the caller must keep them alive until Flush.
  21. type coalesceSlot struct {
  22. verbatim bool
  23. // rawPkt is borrowed: the whole packet for verbatim slots, the seed packet for coalesce
  24. // slots. A slot that never grows past one segment is emitted from rawPkt so its original
  25. // (already valid) L4 checksum ships DATA_VALID instead of making the kernel recompute it.
  26. // A multi-segment slot's superpacket header is rawPkt's, patched in place at flush.
  27. rawPkt []byte
  28. fk flowKey
  29. hdrLen int
  30. ipHdrLen int
  31. isV6 bool
  32. gsoSize int
  33. numSeg int
  34. totalPay int
  35. nextSeq uint32
  36. payIovs [][]byte
  37. }
  38. // TCPCoalescer accumulates adjacent in-flow TCP data segments across multiple concurrent flows and
  39. // emits each flow's run as a single TSO superpacket via tio.GSOWriter. Input must be in sender
  40. // transmission order (MultiCoalescer sorts by (epoch, counter) before dispatch); slots are emitted
  41. // in creation order, so emission reproduces transmission order except for the pure-ACK case in
  42. // commitParsed. Owns no locks; one coalescer per TUN write queue.
  43. type TCPCoalescer struct {
  44. w tio.GSOWriter
  45. // slots is the ordered event queue. Flush walks it once and emits each
  46. // entry as either a WriteGSO (coalesced) or a w.Write (verbatim).
  47. slots []*coalesceSlot
  48. // openSlots maps a flow key to its open slot so new segments can extend an in-progress
  49. // superpacket in O(1). Removal is what closes a chain: on PSH or a short last segment, on a
  50. // non-admissible packet for the flow, or in Flush.
  51. openSlots map[flowKey]*coalesceSlot
  52. // lastSlot caches the most recently touched open slot. Bulk traffic
  53. // arrives in same-flow runs (single-flow steady state, or GRO bursts
  54. // under multi-flow), so comparing the incoming key against the cached
  55. // slot's own fk lets the hot path skip the map lookup (and the aeshash
  56. // of a 38-byte key) for the length of each run.
  57. // Kept in lockstep with openSlots: nil whenever the slot it pointed
  58. // at is removed.
  59. lastSlot *coalesceSlot
  60. pool []*coalesceSlot // free list for reuse
  61. l *slog.Logger
  62. }
  63. // NewTCPCoalescer wraps w, returning nil if w can't accept GSO_TCP writes.
  64. func NewTCPCoalescer(w io.Writer, l *slog.Logger) *TCPCoalescer {
  65. gw, ok := tio.SupportsGSO(w, tio.GSOProtoTCP)
  66. if !ok {
  67. return nil
  68. }
  69. return &TCPCoalescer{
  70. w: gw,
  71. slots: make([]*coalesceSlot, 0, initialSlots),
  72. openSlots: make(map[flowKey]*coalesceSlot, initialSlots),
  73. pool: make([]*coalesceSlot, 0, initialSlots),
  74. l: l,
  75. }
  76. }
  77. // parsedTCP holds the fields extracted from a single parse so later steps
  78. // (admission, slot lookup, canAppend) don't re-walk the header.
  79. type parsedTCP struct {
  80. fk flowKey
  81. ipHdrLen int
  82. hdrLen int
  83. payLen int
  84. seq uint32
  85. flags byte
  86. }
  87. // parseAt extracts the flow key and IP/TCP offsets for a packet the dispatcher already knows is
  88. // TCP; ipHdrLen is the upstream-resolved L4 offset (see flowKey.parseIPAt). p must be zero on
  89. // entry and is filled in place; see flowKey.parseIPAt for why. Returns false for malformed input
  90. // or any shape that must not coalesce (IPv4 options/fragmentation, IPv6 extension headers).
  91. func (p *parsedTCP) parseAt(pkt []byte, ipHdrLen int) bool {
  92. trimmed, ok := p.fk.parseIPAt(pkt, ipHdrLen)
  93. if !ok {
  94. return false
  95. }
  96. return p.parseTail(trimmed, ipHdrLen)
  97. }
  98. // parseTail layers the TCP-header parse on a validated IP prologue. pkt is the trimmed packet;
  99. // fk's addresses are already filled.
  100. func (p *parsedTCP) parseTail(pkt []byte, ipHdrLen int) bool {
  101. if len(pkt) < ipHdrLen+20 {
  102. return false
  103. }
  104. tcpOff := int(pkt[ipHdrLen+12]>>4) * 4
  105. if tcpOff < 20 || tcpOff > 60 {
  106. return false
  107. }
  108. if len(pkt) < ipHdrLen+tcpOff {
  109. return false
  110. }
  111. p.ipHdrLen = ipHdrLen
  112. p.hdrLen = ipHdrLen + tcpOff
  113. p.payLen = len(pkt) - p.hdrLen
  114. p.fk.sport = binary.BigEndian.Uint16(pkt[ipHdrLen : ipHdrLen+2])
  115. p.fk.dport = binary.BigEndian.Uint16(pkt[ipHdrLen+2 : ipHdrLen+4])
  116. p.seq = binary.BigEndian.Uint32(pkt[ipHdrLen+4 : ipHdrLen+8])
  117. p.flags = pkt[ipHdrLen+13]
  118. return true
  119. }
  120. // TCP flag bits (byte 13 of the TCP header). Only the bits the coalescer consults are named;
  121. // FIN/SYN/RST/URG/CWR are rejected by the negative mask in commitParsed.
  122. const (
  123. tcpFlagPsh = 0x08
  124. tcpFlagAck = 0x10
  125. tcpFlagEce = 0x40
  126. )
  127. // sealAllOpen closes every open coalesce chain. Called for unparseable packets: the flow key is
  128. // unknown, so any open chain could otherwise absorb later data and emit it ahead of this packet.
  129. func (c *TCPCoalescer) sealAllOpen() {
  130. clear(c.openSlots)
  131. c.lastSlot = nil
  132. }
  133. // sealFlow closes fk's open chain, if any, keeping lastSlot in lockstep. The len guard skips
  134. // hashing the 38-byte key when no chains are open (e.g. ack-dominant queues).
  135. func (c *TCPCoalescer) sealFlow(fk flowKey) {
  136. if len(c.openSlots) == 0 {
  137. return
  138. }
  139. if last := c.lastSlot; last != nil && last.fk == fk {
  140. c.lastSlot = nil
  141. }
  142. delete(c.openSlots, fk)
  143. }
  144. // commitStaged commits one staged packet dispatch routed to this lane. A shape the lane cannot
  145. // coalesce (any fragmentation, unparseable header) seals every open chain
  146. // and rides the lane as an in-lane verbatim, still in transmission order.
  147. func (c *TCPCoalescer) commitStaged(sp stagedPacket) error {
  148. if sp.fragAny {
  149. c.sealAllOpen()
  150. c.addVerbatim(sp.pkt)
  151. return nil
  152. }
  153. var info parsedTCP
  154. if !info.parseAt(sp.pkt, int(sp.ipHdrLen)) {
  155. c.sealAllOpen()
  156. c.addVerbatim(sp.pkt)
  157. return nil
  158. }
  159. return c.commitParsed(sp.pkt, &info)
  160. }
  161. // commitParsed commits one parsed TCP packet. The caller (dispatch, via parseAt) supplies a
  162. // valid parse so the header is not re-walked here.
  163. func (c *TCPCoalescer) commitParsed(pkt []byte, info *parsedTCP) error {
  164. // Admission: only ACK, ACK|PSH, ACK|ECE, ACK|PSH|ECE may ride a coalesce chain. CWR marks a
  165. // one-shot congestion transition the receiver must observe at a segment boundary. NB: AccECN
  166. // reuses CWR as ACE counter bits; revisit this check if inner hosts adopt AccECN.
  167. if info.flags&tcpFlagAck == 0 || info.flags&^(tcpFlagAck|tcpFlagPsh|tcpFlagEce) != 0 {
  168. // SYN/FIN/RST/URG/CWR must be observed in sequence. Seal the flow's open slot so later
  169. // in-flow packets cannot extend it and emit ahead of this verbatim.
  170. c.sealFlow(info.fk)
  171. c.addVerbatim(pkt)
  172. return nil
  173. }
  174. if info.payLen == 0 {
  175. // Pure ACK: no ordering obligation toward the flow's data. Delivering it after
  176. // later-transmitted data only makes it a stale ACK, which receivers ignore. Not sealing
  177. // keeps a bidirectional flow's data run coalescing across interleaved peer ACKs, matching
  178. // kernel GRO. This is the only place emission deviates from transmission order.
  179. c.addVerbatim(pkt)
  180. return nil
  181. }
  182. // Cached-slot fast path. Arrival isn't per-packet interleaved even with
  183. // many flows: wire-side GRO delivers runs of same-flow packets
  184. // (deliverSegments splits a superdatagram into up to 64), so the cache
  185. // hits for the length of each run and a miss costs one fk compare
  186. // before the map lookup carries the weight.
  187. var open *coalesceSlot
  188. if last := c.lastSlot; last != nil && last.fk == info.fk {
  189. open = last
  190. } else {
  191. open = c.openSlots[info.fk]
  192. }
  193. if open != nil {
  194. if c.canAppend(open, pkt, info) {
  195. if c.appendPayload(open, pkt, info) {
  196. // Chain closed (PSH or short segment): stop extending it.
  197. c.sealFlow(info.fk)
  198. } else {
  199. c.lastSlot = open
  200. }
  201. return nil
  202. }
  203. // Can't extend (seq gap from upstream loss, header change, or a full
  204. // chain): evict it from openSlots and fall through to seed a fresh slot.
  205. c.sealFlow(info.fk)
  206. }
  207. c.seed(pkt, info)
  208. return nil
  209. }
  210. func (c *TCPCoalescer) Flush() error {
  211. var first error
  212. for _, s := range c.slots {
  213. var err error
  214. if s.verbatim || s.numSeg == 1 {
  215. // A slot that never grew is byte-identical to its seed packet; ship the original so
  216. // its valid checksum rides the DATA_VALID path instead of a kernel software csum.
  217. // rawPkt is only mutated once numSeg >= 2 (PSH propagate, flush patches), so it is
  218. // pristine here.
  219. _, err = c.w.Write(s.rawPkt)
  220. } else {
  221. err = c.flushSlot(s)
  222. }
  223. if err != nil && first == nil {
  224. first = err
  225. }
  226. c.release(s)
  227. }
  228. clear(c.slots)
  229. c.slots = c.slots[:0]
  230. clear(c.openSlots)
  231. c.lastSlot = nil
  232. return first
  233. }
  234. func (c *TCPCoalescer) addVerbatim(pkt []byte) {
  235. s := c.take()
  236. s.verbatim = true
  237. s.rawPkt = pkt
  238. c.slots = append(c.slots, s)
  239. }
  240. func (c *TCPCoalescer) seed(pkt []byte, info *parsedTCP) {
  241. if info.hdrLen+info.payLen > tcpCoalesceBufSize {
  242. // Pathological shape that can't ride a superpacket; emit as-is. No chain for this flow can
  243. // be open here (commitParsed evicts before seeding), so sealFlow is defense in depth
  244. // against a stale cache entry absorbing later data.
  245. c.sealFlow(info.fk)
  246. c.addVerbatim(pkt)
  247. return
  248. }
  249. s := c.take()
  250. s.verbatim = false
  251. // rawPkt serves the numSeg==1 fast path in Flush, is the header source for canAppend, and is
  252. // the superpacket header flushSlot patches in place.
  253. s.rawPkt = pkt
  254. s.hdrLen = info.hdrLen
  255. s.ipHdrLen = info.ipHdrLen
  256. s.isV6 = info.fk.isV6
  257. s.fk = info.fk
  258. s.gsoSize = info.payLen
  259. s.numSeg = 1
  260. s.totalPay = info.payLen
  261. s.nextSeq = info.seq + uint32(info.payLen)
  262. s.payIovs = append(s.payIovs[:0], pkt[info.hdrLen:info.hdrLen+info.payLen])
  263. c.slots = append(c.slots, s)
  264. if info.flags&tcpFlagPsh == 0 {
  265. c.openSlots[info.fk] = s
  266. c.lastSlot = s
  267. } else {
  268. // PSH on the seed closes the chain immediately; it is never registered as open.
  269. // Drop any stale entry for this flow too (defense in depth, unreachable if lastSlot's lockstep invariant holds).
  270. c.sealFlow(info.fk)
  271. }
  272. }
  273. // canAppend reports whether info's packet extends the slot's seed: same header shape and stable
  274. // contents, adjacent seq, not oversized. A closed chain never reaches here; closing removes the
  275. // slot from openSlots, the only path in. The header fields read from rawPkt are always pristine:
  276. // the only pre-flush mutation is the PSH propagate, which also closes the chain.
  277. func (c *TCPCoalescer) canAppend(s *coalesceSlot, pkt []byte, info *parsedTCP) bool {
  278. if info.hdrLen != s.hdrLen {
  279. return false
  280. }
  281. if info.seq != s.nextSeq {
  282. return false
  283. }
  284. if s.numSeg >= tcpCoalesceMaxSegs {
  285. return false
  286. }
  287. if info.payLen > s.gsoSize {
  288. return false
  289. }
  290. if s.hdrLen+s.totalPay+info.payLen > tcpCoalesceBufSize {
  291. return false
  292. }
  293. // ECE state must be stable across a burst.
  294. // Receivers expect the flag set on every segment of a CE-echoing window or none.
  295. seedFlags := s.rawPkt[s.ipHdrLen+13]
  296. if (seedFlags^info.flags)&tcpFlagEce != 0 {
  297. return false
  298. }
  299. if !s.isV6 && !ipv4CanCoalesceID(s.rawPkt, pkt, s.numSeg) {
  300. return false
  301. }
  302. if !headersMatch(s.rawPkt[:s.hdrLen], pkt[:info.hdrLen], s.isV6, s.ipHdrLen) {
  303. return false
  304. }
  305. return true
  306. }
  307. // appendPayload folds info's packet into s and reports whether the chain is now closed: the
  308. // segment was sub-gsoSize (kernel TSO allows only the final segment to be short) or carried PSH.
  309. // The caller must deregister a closed slot from openSlots.
  310. func (c *TCPCoalescer) appendPayload(s *coalesceSlot, pkt []byte, info *parsedTCP) bool {
  311. s.payIovs = append(s.payIovs, pkt[info.hdrLen:info.hdrLen+info.payLen])
  312. s.numSeg++
  313. s.totalPay += info.payLen
  314. s.nextSeq = info.seq + uint32(info.payLen)
  315. if info.flags&tcpFlagPsh != 0 {
  316. // Propagate PSH into the seed header so kernel TSO sets it on the last segment. Mutating
  317. // rawPkt is safe: PSH also closes the chain, so no admission check re-reads this header.
  318. s.rawPkt[s.ipHdrLen+13] |= tcpFlagPsh
  319. }
  320. return info.payLen < s.gsoSize || info.flags&tcpFlagPsh != 0
  321. }
  322. func (c *TCPCoalescer) take() *coalesceSlot {
  323. if n := len(c.pool); n > 0 {
  324. s := c.pool[n-1]
  325. c.pool[n-1] = nil
  326. c.pool = c.pool[:n-1]
  327. return s
  328. }
  329. return &coalesceSlot{}
  330. }
  331. func (c *TCPCoalescer) release(s *coalesceSlot) {
  332. clear(s.payIovs)
  333. *s = coalesceSlot{payIovs: s.payIovs[:0]}
  334. c.pool = append(c.pool, s)
  335. }
  336. // flushSlot patches the superpacket header in place in rawPkt (total length, IPv4 header
  337. // checksum, pseudo-header checksum seed) and calls WriteGSO. The slot is released right after,
  338. // so nothing re-reads the patched header. Does not remove the slot from c.slots.
  339. func (c *TCPCoalescer) flushSlot(s *coalesceSlot) error {
  340. total := s.hdrLen + s.totalPay
  341. l4Len := total - s.ipHdrLen
  342. hdr := s.rawPkt[:s.hdrLen]
  343. if s.isV6 {
  344. binary.BigEndian.PutUint16(hdr[4:6], uint16(l4Len))
  345. } else {
  346. binary.BigEndian.PutUint16(hdr[2:4], uint16(total))
  347. hdr[10] = 0
  348. hdr[11] = 0
  349. binary.BigEndian.PutUint16(hdr[10:12], ipv4HdrChecksum(hdr[:s.ipHdrLen]))
  350. }
  351. var psum uint32
  352. if s.isV6 {
  353. psum = pseudoSumIPv6(hdr[8:24], hdr[24:40], ipProtoTCP, l4Len)
  354. } else {
  355. psum = pseudoSumIPv4(hdr[12:16], hdr[16:20], ipProtoTCP, l4Len)
  356. }
  357. tcsum := s.ipHdrLen + 16
  358. binary.BigEndian.PutUint16(hdr[tcsum:tcsum+2], foldOnceNoInvert(psum))
  359. return c.w.WriteGSO(hdr[:s.ipHdrLen], hdr[s.ipHdrLen:], s.payIovs, tio.GSOProtoTCP)
  360. }
  361. // headersMatch compares two IP+TCP header prefixes for byte-for-byte
  362. // equality on every field that must be identical across coalesced
  363. // segments. Size/IPID/IPCsum/seq/flags/tcpCsum are masked out.
  364. func headersMatch(a, b []byte, isV6 bool, ipHdrLen int) bool {
  365. if len(a) != len(b) {
  366. return false
  367. }
  368. if !ipHeadersMatch(a, b, isV6) {
  369. return false
  370. }
  371. // TCP: compare [0:4] ports, [8:13] ack+dataoff, [14:16] window,
  372. // [18:tcpHdrLen] options (incl. urgent).
  373. tcp := ipHdrLen
  374. if !bytes.Equal(a[tcp:tcp+4], b[tcp:tcp+4]) {
  375. return false
  376. }
  377. if !bytes.Equal(a[tcp+8:tcp+13], b[tcp+8:tcp+13]) {
  378. return false
  379. }
  380. if !bytes.Equal(a[tcp+14:tcp+16], b[tcp+14:tcp+16]) {
  381. return false
  382. }
  383. if !bytes.Equal(a[tcp+18:], b[tcp+18:]) {
  384. return false
  385. }
  386. return true
  387. }
  388. // ipv4HdrChecksum computes the IPv4 header checksum over hdr (which must
  389. // already have its checksum field zeroed) and returns the folded/inverted
  390. // 16-bit value to store.
  391. func ipv4HdrChecksum(hdr []byte) uint16 {
  392. var sum uint32
  393. for i := 0; i+1 < len(hdr); i += 2 {
  394. sum += uint32(binary.BigEndian.Uint16(hdr[i : i+2]))
  395. }
  396. if len(hdr)%2 == 1 {
  397. sum += uint32(hdr[len(hdr)-1]) << 8
  398. }
  399. for sum>>16 != 0 {
  400. sum = (sum & 0xffff) + (sum >> 16)
  401. }
  402. return ^uint16(sum)
  403. }
  404. // pseudoSumIPv4 / pseudoSumIPv6 build the L4 pseudo-header partial sum
  405. // expected by the virtio NEEDS_CSUM kernel path: the 32-bit accumulator
  406. // before folding. proto selects the L4 (TCP or UDP); the UDP coalescer
  407. // reuses these helpers.
  408. func pseudoSumIPv4(src, dst []byte, proto byte, l4Len int) uint32 {
  409. var sum uint32
  410. sum += uint32(binary.BigEndian.Uint16(src[0:2]))
  411. sum += uint32(binary.BigEndian.Uint16(src[2:4]))
  412. sum += uint32(binary.BigEndian.Uint16(dst[0:2]))
  413. sum += uint32(binary.BigEndian.Uint16(dst[2:4]))
  414. sum += uint32(proto)
  415. sum += uint32(l4Len)
  416. return sum
  417. }
  418. func pseudoSumIPv6(src, dst []byte, proto byte, l4Len int) uint32 {
  419. var sum uint32
  420. for i := 0; i < 16; i += 2 {
  421. sum += uint32(binary.BigEndian.Uint16(src[i : i+2]))
  422. sum += uint32(binary.BigEndian.Uint16(dst[i : i+2]))
  423. }
  424. sum += uint32(l4Len >> 16)
  425. sum += uint32(l4Len & 0xffff)
  426. sum += uint32(proto)
  427. return sum
  428. }
  429. // foldOnceNoInvert folds the 32-bit accumulator to 16 bits and returns it unchanged (no one's complement).
  430. // This is what virtio NEEDS_CSUM wants in the L4 checksum field
  431. func foldOnceNoInvert(sum uint32) uint16 {
  432. for sum>>16 != 0 {
  433. sum = (sum & 0xffff) + (sum >> 16)
  434. }
  435. return uint16(sum)
  436. }