tcp_coalesce.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. package nebula
  2. import (
  3. "bytes"
  4. "encoding/binary"
  5. "io"
  6. "github.com/slackhq/nebula/overlay"
  7. )
  8. // ipProtoTCP is the IANA protocol number for TCP. Hardcoded instead of
  9. // reaching for golang.org/x/sys/unix — that package doesn't define the
  10. // constant on Windows, which would break cross-compiles even though this
  11. // file runs unchanged on every platform.
  12. const ipProtoTCP = 6
  13. // tcpCoalesceBufSize caps total bytes per superpacket. Mirrors the kernel's
  14. // sk_gso_max_size of ~64KiB; anything beyond this would be rejected anyway.
  15. const tcpCoalesceBufSize = 65535
  16. // tcpCoalesceMaxSegs caps how many segments we'll coalesce into a single
  17. // superpacket. Keeping this well below the kernel's TSO ceiling bounds
  18. // latency.
  19. const tcpCoalesceMaxSegs = 64
  20. // tcpCoalesceHdrCap is the scratch space we copy a seed's IP+TCP header
  21. // into. IPv6 (40) + TCP with full options (60) = 100 bytes.
  22. const tcpCoalesceHdrCap = 100
  23. // initialSlots is the starting capacity of the slot pool. One flow per
  24. // packet is the worst case so this matches a typical UDP recvmmsg batch.
  25. const initialSlots = 64
  26. // flowKey identifies a TCP flow by {src, dst, sport, dport, family}.
  27. // Comparable, so linear scans over the slot list stay tight.
  28. type flowKey struct {
  29. src, dst [16]byte
  30. sport, dport uint16
  31. isV6 bool
  32. }
  33. // coalesceSlot is one entry in the coalescer's ordered event queue. When
  34. // passthrough is true the slot holds a single borrowed packet that must be
  35. // emitted verbatim (non-TCP, non-admissible TCP, or oversize seed). When
  36. // passthrough is false the slot is an in-progress coalesced superpacket:
  37. // hdrBuf is a mutable copy of the seed's IP+TCP header (we patch total
  38. // length and pseudo-header partial at flush), and payIovs are *borrowed*
  39. // slices from the caller's plaintext buffers — no payload is ever copied.
  40. // The caller (listenOut) must keep those buffers alive until Flush.
  41. type coalesceSlot struct {
  42. passthrough bool
  43. rawPkt []byte // borrowed when passthrough
  44. fk flowKey
  45. hdrBuf [tcpCoalesceHdrCap]byte
  46. hdrLen int
  47. ipHdrLen int
  48. isV6 bool
  49. gsoSize int
  50. numSeg int
  51. totalPay int
  52. nextSeq uint32
  53. // psh closes the chain: set when the last-accepted segment had PSH or
  54. // was sub-gsoSize. No further appends after that.
  55. psh bool
  56. payIovs [][]byte
  57. }
  58. // tcpCoalescer accumulates adjacent in-flow TCP data segments across
  59. // multiple concurrent flows and emits each flow's run as a single TSO
  60. // superpacket via overlay.GSOWriter. All output — coalesced or not — is
  61. // deferred until Flush so arrival order is preserved on the wire. Owns
  62. // no locks; one coalescer per TUN write queue.
  63. type tcpCoalescer struct {
  64. plainW io.Writer
  65. gsoW overlay.GSOWriter // nil when the queue doesn't support TSO
  66. // slots is the ordered event queue. Flush walks it once and emits each
  67. // entry as either a WriteGSO (coalesced) or a plainW.Write (passthrough).
  68. slots []*coalesceSlot
  69. // openSlots maps a flow key to its most recent non-sealed slot, so new
  70. // segments can extend an in-progress superpacket in O(1). Slots are
  71. // removed from this map when they close (PSH or short-last-segment),
  72. // when a non-admissible packet for that flow arrives, or in Flush.
  73. openSlots map[flowKey]*coalesceSlot
  74. pool []*coalesceSlot // free list for reuse
  75. }
  76. func newTCPCoalescer(w io.Writer) *tcpCoalescer {
  77. c := &tcpCoalescer{
  78. plainW: w,
  79. slots: make([]*coalesceSlot, 0, initialSlots),
  80. openSlots: make(map[flowKey]*coalesceSlot, initialSlots),
  81. pool: make([]*coalesceSlot, 0, initialSlots),
  82. }
  83. if gw, ok := w.(overlay.GSOWriter); ok && gw.GSOSupported() {
  84. c.gsoW = gw
  85. }
  86. return c
  87. }
  88. // parsedTCP holds the fields extracted from a single parse so later steps
  89. // (admission, slot lookup, canAppend) don't re-walk the header.
  90. type parsedTCP struct {
  91. fk flowKey
  92. ipHdrLen int
  93. tcpHdrLen int
  94. hdrLen int
  95. payLen int
  96. seq uint32
  97. flags byte
  98. }
  99. // parseTCPBase extracts the flow key and IP/TCP offsets for any TCP packet,
  100. // regardless of whether it's admissible for coalescing. Returns ok=false
  101. // for non-TCP or malformed input. Accepts IPv4 (no options, no fragmentation)
  102. // and IPv6 (no extension headers).
  103. func parseTCPBase(pkt []byte) (parsedTCP, bool) {
  104. var p parsedTCP
  105. if len(pkt) < 20 {
  106. return p, false
  107. }
  108. v := pkt[0] >> 4
  109. switch v {
  110. case 4:
  111. ihl := int(pkt[0]&0x0f) * 4
  112. if ihl != 20 {
  113. return p, false
  114. }
  115. if pkt[9] != ipProtoTCP {
  116. return p, false
  117. }
  118. // Reject actual fragmentation (MF or non-zero frag offset).
  119. if binary.BigEndian.Uint16(pkt[6:8])&0x3fff != 0 {
  120. return p, false
  121. }
  122. totalLen := int(binary.BigEndian.Uint16(pkt[2:4]))
  123. if totalLen > len(pkt) || totalLen < ihl {
  124. return p, false
  125. }
  126. p.ipHdrLen = 20
  127. p.fk.isV6 = false
  128. copy(p.fk.src[:4], pkt[12:16])
  129. copy(p.fk.dst[:4], pkt[16:20])
  130. pkt = pkt[:totalLen]
  131. case 6:
  132. if len(pkt) < 40 {
  133. return p, false
  134. }
  135. if pkt[6] != ipProtoTCP {
  136. return p, false
  137. }
  138. payloadLen := int(binary.BigEndian.Uint16(pkt[4:6]))
  139. if 40+payloadLen > len(pkt) {
  140. return p, false
  141. }
  142. p.ipHdrLen = 40
  143. p.fk.isV6 = true
  144. copy(p.fk.src[:], pkt[8:24])
  145. copy(p.fk.dst[:], pkt[24:40])
  146. pkt = pkt[:40+payloadLen]
  147. default:
  148. return p, false
  149. }
  150. if len(pkt) < p.ipHdrLen+20 {
  151. return p, false
  152. }
  153. tcpOff := int(pkt[p.ipHdrLen+12]>>4) * 4
  154. if tcpOff < 20 || tcpOff > 60 {
  155. return p, false
  156. }
  157. if len(pkt) < p.ipHdrLen+tcpOff {
  158. return p, false
  159. }
  160. p.tcpHdrLen = tcpOff
  161. p.hdrLen = p.ipHdrLen + tcpOff
  162. p.payLen = len(pkt) - p.hdrLen
  163. p.seq = binary.BigEndian.Uint32(pkt[p.ipHdrLen+4 : p.ipHdrLen+8])
  164. p.flags = pkt[p.ipHdrLen+13]
  165. p.fk.sport = binary.BigEndian.Uint16(pkt[p.ipHdrLen : p.ipHdrLen+2])
  166. p.fk.dport = binary.BigEndian.Uint16(pkt[p.ipHdrLen+2 : p.ipHdrLen+4])
  167. return p, true
  168. }
  169. // coalesceable reports whether a parsed TCP segment is eligible for
  170. // coalescing. Accepts only ACK or ACK|PSH with a non-empty payload.
  171. func (p parsedTCP) coalesceable() bool {
  172. const ack = 0x10
  173. const psh = 0x08
  174. if p.flags&^(ack|psh) != 0 || p.flags&ack == 0 {
  175. return false
  176. }
  177. return p.payLen > 0
  178. }
  179. // Add borrows pkt. The caller must keep pkt valid until the next Flush,
  180. // whether or not the packet was coalesced — passthrough (non-admissible)
  181. // packets are queued and written at Flush time, not synchronously.
  182. func (c *tcpCoalescer) Add(pkt []byte) error {
  183. if c.gsoW == nil {
  184. c.addPassthrough(pkt)
  185. return nil
  186. }
  187. info, ok := parseTCPBase(pkt)
  188. if !ok {
  189. // Non-TCP or malformed — can't possibly collide with an open flow.
  190. c.addPassthrough(pkt)
  191. return nil
  192. }
  193. if !info.coalesceable() {
  194. // TCP but not admissible (SYN/FIN/RST/URG/CWR/ECE or zero-payload).
  195. // Seal this flow's open slot so later in-flow packets don't extend
  196. // it and accidentally reorder past this passthrough.
  197. delete(c.openSlots, info.fk)
  198. c.addPassthrough(pkt)
  199. return nil
  200. }
  201. if open := c.openSlots[info.fk]; open != nil {
  202. if c.canAppend(open, pkt, info) {
  203. c.appendPayload(open, pkt, info)
  204. if open.psh {
  205. delete(c.openSlots, info.fk)
  206. }
  207. return nil
  208. }
  209. // Can't extend — seal it and fall through to seed a fresh slot.
  210. delete(c.openSlots, info.fk)
  211. }
  212. c.seed(pkt, info)
  213. return nil
  214. }
  215. // Flush emits every queued event in arrival order. Coalesced slots go out
  216. // via WriteGSO; passthrough slots go out via plainW.Write. Returns the
  217. // first error observed; keeps draining so one bad packet doesn't hold up
  218. // the rest. After Flush returns, borrowed payload slices may be recycled.
  219. func (c *tcpCoalescer) Flush() error {
  220. var first error
  221. for _, s := range c.slots {
  222. var err error
  223. if s.passthrough {
  224. _, err = c.plainW.Write(s.rawPkt)
  225. } else {
  226. err = c.flushSlot(s)
  227. }
  228. if err != nil && first == nil {
  229. first = err
  230. }
  231. c.release(s)
  232. }
  233. for i := range c.slots {
  234. c.slots[i] = nil
  235. }
  236. c.slots = c.slots[:0]
  237. for k := range c.openSlots {
  238. delete(c.openSlots, k)
  239. }
  240. return first
  241. }
  242. func (c *tcpCoalescer) addPassthrough(pkt []byte) {
  243. s := c.take()
  244. s.passthrough = true
  245. s.rawPkt = pkt
  246. c.slots = append(c.slots, s)
  247. }
  248. func (c *tcpCoalescer) seed(pkt []byte, info parsedTCP) {
  249. if info.hdrLen > tcpCoalesceHdrCap || info.hdrLen+info.payLen > tcpCoalesceBufSize {
  250. // Pathological shape — can't fit our scratch, emit as-is.
  251. c.addPassthrough(pkt)
  252. return
  253. }
  254. s := c.take()
  255. s.passthrough = false
  256. s.rawPkt = nil
  257. copy(s.hdrBuf[:], pkt[:info.hdrLen])
  258. s.hdrLen = info.hdrLen
  259. s.ipHdrLen = info.ipHdrLen
  260. s.isV6 = info.fk.isV6
  261. s.fk = info.fk
  262. s.gsoSize = info.payLen
  263. s.numSeg = 1
  264. s.totalPay = info.payLen
  265. s.nextSeq = info.seq + uint32(info.payLen)
  266. s.psh = info.flags&0x08 != 0
  267. s.payIovs = append(s.payIovs[:0], pkt[info.hdrLen:info.hdrLen+info.payLen])
  268. c.slots = append(c.slots, s)
  269. if !s.psh {
  270. c.openSlots[info.fk] = s
  271. }
  272. }
  273. // canAppend reports whether info's packet extends the slot's seed: same
  274. // header shape and stable contents, adjacent seq, not oversized, chain not
  275. // closed.
  276. func (c *tcpCoalescer) canAppend(s *coalesceSlot, pkt []byte, info parsedTCP) bool {
  277. if s.psh {
  278. return false
  279. }
  280. if info.hdrLen != s.hdrLen {
  281. return false
  282. }
  283. if info.seq != s.nextSeq {
  284. return false
  285. }
  286. if s.numSeg >= tcpCoalesceMaxSegs {
  287. return false
  288. }
  289. if info.payLen > s.gsoSize {
  290. return false
  291. }
  292. if s.hdrLen+s.totalPay+info.payLen > tcpCoalesceBufSize {
  293. return false
  294. }
  295. if !headersMatch(s.hdrBuf[:s.hdrLen], pkt[:info.hdrLen], s.isV6, s.ipHdrLen) {
  296. return false
  297. }
  298. return true
  299. }
  300. func (c *tcpCoalescer) appendPayload(s *coalesceSlot, pkt []byte, info parsedTCP) {
  301. s.payIovs = append(s.payIovs, pkt[info.hdrLen:info.hdrLen+info.payLen])
  302. s.numSeg++
  303. s.totalPay += info.payLen
  304. s.nextSeq = info.seq + uint32(info.payLen)
  305. if info.payLen < s.gsoSize || info.flags&0x08 != 0 {
  306. s.psh = true
  307. }
  308. }
  309. func (c *tcpCoalescer) take() *coalesceSlot {
  310. if n := len(c.pool); n > 0 {
  311. s := c.pool[n-1]
  312. c.pool[n-1] = nil
  313. c.pool = c.pool[:n-1]
  314. return s
  315. }
  316. return &coalesceSlot{}
  317. }
  318. func (c *tcpCoalescer) release(s *coalesceSlot) {
  319. s.passthrough = false
  320. s.rawPkt = nil
  321. for i := range s.payIovs {
  322. s.payIovs[i] = nil
  323. }
  324. s.payIovs = s.payIovs[:0]
  325. s.numSeg = 0
  326. s.totalPay = 0
  327. s.psh = false
  328. c.pool = append(c.pool, s)
  329. }
  330. // flushSlot patches the header and calls WriteGSO. Does not remove the
  331. // slot from c.slots.
  332. func (c *tcpCoalescer) flushSlot(s *coalesceSlot) error {
  333. total := s.hdrLen + s.totalPay
  334. l4Len := total - s.ipHdrLen
  335. hdr := s.hdrBuf[:s.hdrLen]
  336. if s.isV6 {
  337. binary.BigEndian.PutUint16(hdr[4:6], uint16(l4Len))
  338. } else {
  339. binary.BigEndian.PutUint16(hdr[2:4], uint16(total))
  340. hdr[10] = 0
  341. hdr[11] = 0
  342. binary.BigEndian.PutUint16(hdr[10:12], ipv4HdrChecksum(hdr[:s.ipHdrLen]))
  343. }
  344. var psum uint32
  345. if s.isV6 {
  346. psum = pseudoSumIPv6(hdr[8:24], hdr[24:40], ipProtoTCP, l4Len)
  347. } else {
  348. psum = pseudoSumIPv4(hdr[12:16], hdr[16:20], ipProtoTCP, l4Len)
  349. }
  350. tcsum := s.ipHdrLen + 16
  351. binary.BigEndian.PutUint16(hdr[tcsum:tcsum+2], foldOnceNoInvert(psum))
  352. return c.gsoW.WriteGSO(hdr, s.payIovs, uint16(s.gsoSize), s.isV6, uint16(s.ipHdrLen))
  353. }
  354. // headersMatch compares two IP+TCP header prefixes for byte-for-byte
  355. // equality on every field that must be identical across coalesced
  356. // segments. Size/IPID/IPCsum/seq/flags/tcpCsum are masked out.
  357. func headersMatch(a, b []byte, isV6 bool, ipHdrLen int) bool {
  358. if len(a) != len(b) {
  359. return false
  360. }
  361. if isV6 {
  362. // IPv6: bytes [0:4] = version/TC/flow-label, [6:8] = next_hdr/hop,
  363. // [8:40] = src+dst. Skip [4:6] payload length.
  364. if !bytes.Equal(a[0:4], b[0:4]) {
  365. return false
  366. }
  367. if !bytes.Equal(a[6:40], b[6:40]) {
  368. return false
  369. }
  370. } else {
  371. // IPv4: [0:2] version/IHL/TOS, [6:10] flags/fragoff/TTL/proto,
  372. // [12:20] src+dst. Skip [2:4] total len, [4:6] id, [10:12] csum.
  373. if !bytes.Equal(a[0:2], b[0:2]) {
  374. return false
  375. }
  376. if !bytes.Equal(a[6:10], b[6:10]) {
  377. return false
  378. }
  379. if !bytes.Equal(a[12:20], b[12:20]) {
  380. return false
  381. }
  382. }
  383. // TCP: compare [0:4] ports, [8:13] ack+dataoff, [14:16] window,
  384. // [18:tcpHdrLen] options (incl. urgent).
  385. tcp := ipHdrLen
  386. if !bytes.Equal(a[tcp:tcp+4], b[tcp:tcp+4]) {
  387. return false
  388. }
  389. if !bytes.Equal(a[tcp+8:tcp+13], b[tcp+8:tcp+13]) {
  390. return false
  391. }
  392. if !bytes.Equal(a[tcp+14:tcp+16], b[tcp+14:tcp+16]) {
  393. return false
  394. }
  395. if !bytes.Equal(a[tcp+18:], b[tcp+18:]) {
  396. return false
  397. }
  398. return true
  399. }
  400. // ipv4HdrChecksum computes the IPv4 header checksum over hdr (which must
  401. // already have its checksum field zeroed) and returns the folded/inverted
  402. // 16-bit value to store.
  403. func ipv4HdrChecksum(hdr []byte) uint16 {
  404. var sum uint32
  405. for i := 0; i+1 < len(hdr); i += 2 {
  406. sum += uint32(binary.BigEndian.Uint16(hdr[i : i+2]))
  407. }
  408. if len(hdr)%2 == 1 {
  409. sum += uint32(hdr[len(hdr)-1]) << 8
  410. }
  411. for sum>>16 != 0 {
  412. sum = (sum & 0xffff) + (sum >> 16)
  413. }
  414. return ^uint16(sum)
  415. }
  416. // pseudoSumIPv4 / pseudoSumIPv6 build the TCP pseudo-header partial sum
  417. // expected by the virtio NEEDS_CSUM kernel path: the 32-bit accumulator
  418. // before folding.
  419. func pseudoSumIPv4(src, dst []byte, proto byte, l4Len int) uint32 {
  420. var sum uint32
  421. sum += uint32(binary.BigEndian.Uint16(src[0:2]))
  422. sum += uint32(binary.BigEndian.Uint16(src[2:4]))
  423. sum += uint32(binary.BigEndian.Uint16(dst[0:2]))
  424. sum += uint32(binary.BigEndian.Uint16(dst[2:4]))
  425. sum += uint32(proto)
  426. sum += uint32(l4Len)
  427. return sum
  428. }
  429. func pseudoSumIPv6(src, dst []byte, proto byte, l4Len int) uint32 {
  430. var sum uint32
  431. for i := 0; i < 16; i += 2 {
  432. sum += uint32(binary.BigEndian.Uint16(src[i : i+2]))
  433. sum += uint32(binary.BigEndian.Uint16(dst[i : i+2]))
  434. }
  435. sum += uint32(l4Len >> 16)
  436. sum += uint32(l4Len & 0xffff)
  437. sum += uint32(proto)
  438. return sum
  439. }
  440. // foldOnceNoInvert folds the 32-bit accumulator to 16 bits and returns it
  441. // unchanged (no one's complement). This is what virtio NEEDS_CSUM wants in
  442. // the L4 checksum field — the kernel will add the payload sum and invert.
  443. func foldOnceNoInvert(sum uint32) uint16 {
  444. for sum>>16 != 0 {
  445. sum = (sum & 0xffff) + (sum >> 16)
  446. }
  447. return uint16(sum)
  448. }