1
0

udp_coalesce.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. package batch
  2. import (
  3. "bytes"
  4. "encoding/binary"
  5. "io"
  6. "github.com/slackhq/nebula/overlay/tio"
  7. )
  8. // ipProtoUDP is the IANA protocol number for UDP.
  9. const ipProtoUDP = 17
  10. // udpCoalesceBufSize caps total bytes per UDP superpacket. Mirrors the
  11. // kernel's gso_max_size; payloads beyond this are emitted as-is.
  12. const udpCoalesceBufSize = 65535
  13. // udpCoalesceMaxSegs caps how many segments we'll coalesce. Kernel UDP-GSO
  14. // accepts up to 64 segments per skb (UDP_MAX_SEGMENTS); stay under that.
  15. const udpCoalesceMaxSegs = 64
  16. // udpSlot is one entry in the UDPCoalescer's ordered event queue.
  17. type udpSlot struct {
  18. verbatim bool
  19. // rawPkt is borrowed: the whole packet for verbatim slots, the seed
  20. // packet for coalesce slots. A coalesce slot that never grows past one
  21. // segment is emitted from rawPkt so its original (already valid) L4
  22. // checksum ships DATA_VALID instead of making the kernel recompute it.
  23. // A multi-segment slot's superpacket header is rawPkt's, patched in place at flush.
  24. rawPkt []byte
  25. fk flowKey
  26. hdrLen int
  27. ipHdrLen int
  28. isV6 bool
  29. gsoSize int // per-segment UDP payload length
  30. numSeg int
  31. totalPay int
  32. payIovs [][]byte
  33. }
  34. // UDPCoalescer accumulates adjacent in-flow UDP datagrams across multiple
  35. // concurrent flows and emits each flow's run as a single GSO_UDP_L4 superpacket via tio.GSOWriter.
  36. // Preserves the in-flow order of packets as they are Commit-ed
  37. //
  38. // Owns no locks; one coalescer per TUN write queue.
  39. type UDPCoalescer struct {
  40. w tio.GSOWriter
  41. slots []*udpSlot
  42. openSlots map[flowKey]*udpSlot
  43. // lastSlot caches the most recently touched open slot; see the
  44. // TCPCoalescer field of the same name. Single-flow QUIC bulk is the
  45. // dominant USO workload, and multi-flow arrival comes in GRO runs, so
  46. // the fk compare beats the map's 38-byte key hash on most packets.
  47. // Kept in lockstep with openSlots: nil whenever the slot it pointed at
  48. // is removed.
  49. lastSlot *udpSlot
  50. pool []*udpSlot
  51. }
  52. func NewUDPCoalescer(w io.Writer) *UDPCoalescer {
  53. gw, ok := tio.SupportsGSO(w, tio.GSOProtoUDP)
  54. if !ok {
  55. return nil
  56. }
  57. return &UDPCoalescer{
  58. w: gw,
  59. slots: make([]*udpSlot, 0, initialSlots),
  60. openSlots: make(map[flowKey]*udpSlot, initialSlots),
  61. pool: make([]*udpSlot, 0, initialSlots),
  62. }
  63. }
  64. // parsedUDP holds the fields extracted from a single parse so later steps
  65. // (admission, slot lookup, canAppend) don't re-walk the header.
  66. type parsedUDP struct {
  67. fk flowKey
  68. ipHdrLen int
  69. hdrLen int // ipHdrLen + 8
  70. payLen int
  71. }
  72. // parseAt extracts the flow key and IP/UDP offsets for a packet the dispatcher already knows is
  73. // UDP; ipHdrLen is the upstream-resolved L4 offset (see flowKey.parseIPAt). p must be zero on
  74. // entry and is filled in place. Returns false for malformed input or any shape that must not
  75. // coalesce (IPv4 options/fragmentation, IPv6 extension headers).
  76. func (p *parsedUDP) parseAt(pkt []byte, ipHdrLen int) bool {
  77. trimmed, ok := p.fk.parseIPAt(pkt, ipHdrLen)
  78. if !ok {
  79. return false
  80. }
  81. return p.parseTail(trimmed, ipHdrLen)
  82. }
  83. // parseTail layers the UDP-header parse on a validated IP prologue. pkt is the trimmed packet;
  84. // fk's addresses are already filled.
  85. func (p *parsedUDP) parseTail(pkt []byte, ipHdrLen int) bool {
  86. if len(pkt) < ipHdrLen+8 {
  87. return false
  88. }
  89. // UDP `length` field: must equal IP-derived length-of-UDP-header-plus-payload.
  90. udpLen := int(binary.BigEndian.Uint16(pkt[ipHdrLen+4 : ipHdrLen+6]))
  91. if udpLen < 8 || udpLen > len(pkt)-ipHdrLen {
  92. return false
  93. }
  94. p.ipHdrLen = ipHdrLen
  95. p.hdrLen = ipHdrLen + 8
  96. p.payLen = udpLen - 8
  97. p.fk.sport = binary.BigEndian.Uint16(pkt[ipHdrLen : ipHdrLen+2])
  98. p.fk.dport = binary.BigEndian.Uint16(pkt[ipHdrLen+2 : ipHdrLen+4])
  99. return true
  100. }
  101. // sealFlow closes fk's open chain, if any, keeping lastSlot in lockstep. The len guard skips
  102. // hashing the 38-byte key when no chains are open.
  103. func (c *UDPCoalescer) sealFlow(fk flowKey) {
  104. if len(c.openSlots) == 0 {
  105. return
  106. }
  107. if last := c.lastSlot; last != nil && last.fk == fk {
  108. c.lastSlot = nil
  109. }
  110. delete(c.openSlots, fk)
  111. }
  112. // commitStaged commits one staged packet dispatch routed to this lane. A shape the lane cannot
  113. // coalesce (any fragmentation, unparseable header) seals every open chain — its flow is unknown —
  114. // and rides the lane as an in-lane verbatim, still in transmission order.
  115. func (c *UDPCoalescer) commitStaged(sp stagedPacket) error {
  116. if sp.fragAny {
  117. c.sealAllOpen()
  118. c.addVerbatim(sp.pkt)
  119. return nil
  120. }
  121. var info parsedUDP
  122. if !info.parseAt(sp.pkt, int(sp.ipHdrLen)) {
  123. c.sealAllOpen()
  124. c.addVerbatim(sp.pkt)
  125. return nil
  126. }
  127. return c.commitParsed(sp.pkt, &info)
  128. }
  129. // commitParsed commits one parsed UDP packet. The caller (dispatch, via parseAt) supplies a
  130. // valid parse so the header is not re-walked here.
  131. func (c *UDPCoalescer) commitParsed(pkt []byte, info *parsedUDP) error {
  132. // A zero-length UDP datagram (length == 8) is legal and must reach the TUN, but cannot be
  133. // coalesced.
  134. if info.payLen == 0 {
  135. c.sealFlow(info.fk)
  136. c.addVerbatim(pkt)
  137. return nil
  138. }
  139. // Cached-slot fast path; see the TCPCoalescer equivalent.
  140. var open *udpSlot
  141. if last := c.lastSlot; last != nil && last.fk == info.fk {
  142. open = last
  143. } else {
  144. open = c.openSlots[info.fk]
  145. }
  146. if open != nil {
  147. if c.canAppend(open, pkt, info) {
  148. if c.appendPayload(open, pkt, info) {
  149. // Chain closed (short segment): stop extending it.
  150. c.sealFlow(info.fk)
  151. } else {
  152. c.lastSlot = open
  153. }
  154. return nil
  155. }
  156. // Can't extend: evict it from openSlots and fall through to seed a
  157. // fresh slot.
  158. c.sealFlow(info.fk)
  159. }
  160. c.seed(pkt, info)
  161. return nil
  162. }
  163. func (c *UDPCoalescer) Flush() error {
  164. var first error
  165. for _, s := range c.slots {
  166. var err error
  167. if s.verbatim || s.numSeg == 1 {
  168. // A slot that never grew is byte-identical to the packet it was
  169. // seeded from; ship the original so its valid checksum rides the
  170. // DATA_VALID path instead of paying a kernel software csum.
  171. _, err = c.w.Write(s.rawPkt)
  172. } else {
  173. err = c.flushSlot(s)
  174. }
  175. if err != nil && first == nil {
  176. first = err
  177. }
  178. c.release(s)
  179. }
  180. clear(c.slots)
  181. c.slots = c.slots[:0]
  182. clear(c.openSlots)
  183. c.lastSlot = nil
  184. return first
  185. }
  186. // sealAllOpen closes every open coalesce chain. Called for unparseable packets: the flow key is
  187. // unknown, so any open chain could otherwise absorb later data and emit it ahead of this packet.
  188. func (c *UDPCoalescer) sealAllOpen() {
  189. clear(c.openSlots)
  190. c.lastSlot = nil
  191. }
  192. func (c *UDPCoalescer) addVerbatim(pkt []byte) {
  193. s := c.take()
  194. s.verbatim = true
  195. s.rawPkt = pkt
  196. c.slots = append(c.slots, s)
  197. }
  198. func (c *UDPCoalescer) seed(pkt []byte, info *parsedUDP) {
  199. if info.hdrLen+info.payLen > udpCoalesceBufSize {
  200. // Pathological shape that can't ride a superpacket; emit as-is. No chain for this flow can
  201. // be open here (commitParsed evicts before seeding), so sealFlow is defense in depth
  202. // against a stale cache entry absorbing later data.
  203. c.sealFlow(info.fk)
  204. c.addVerbatim(pkt)
  205. return
  206. }
  207. s := c.take()
  208. s.verbatim = false
  209. // rawPkt serves the numSeg==1 fast path in Flush, is the header source for canAppend, and is
  210. // the superpacket header flushSlot patches in place.
  211. s.rawPkt = pkt
  212. s.hdrLen = info.hdrLen
  213. s.ipHdrLen = info.ipHdrLen
  214. s.isV6 = info.fk.isV6
  215. s.fk = info.fk
  216. s.gsoSize = info.payLen
  217. s.numSeg = 1
  218. s.totalPay = info.payLen
  219. s.payIovs = append(s.payIovs[:0], pkt[info.hdrLen:info.hdrLen+info.payLen])
  220. c.slots = append(c.slots, s)
  221. c.openSlots[info.fk] = s
  222. c.lastSlot = s
  223. }
  224. // canAppend reports whether info's packet extends the slot's seed.
  225. // Kernel UDP-GSO requires every segment except possibly the last to be
  226. // exactly gsoSize, and the last may be shorter (≤ gsoSize).
  227. func (c *UDPCoalescer) canAppend(s *udpSlot, pkt []byte, info *parsedUDP) bool {
  228. if info.hdrLen != s.hdrLen {
  229. return false
  230. }
  231. if s.numSeg >= udpCoalesceMaxSegs {
  232. return false
  233. }
  234. if info.payLen > s.gsoSize {
  235. return false
  236. }
  237. if s.hdrLen+s.totalPay+info.payLen > udpCoalesceBufSize {
  238. return false
  239. }
  240. // Header reads use rawPkt, which is never mutated before flush. A closed chain never reaches
  241. // here; closing removes the slot from openSlots, the only path in.
  242. if !s.isV6 && !ipv4CanCoalesceID(s.rawPkt, pkt, s.numSeg) {
  243. return false
  244. }
  245. if !udpHeadersMatch(s.rawPkt[:s.hdrLen], pkt[:info.hdrLen], s.isV6, s.ipHdrLen) {
  246. return false
  247. }
  248. return true
  249. }
  250. // appendPayload folds info's packet into s and reports whether the chain is now closed: kernel
  251. // UDP-GSO requires every segment but the last to be exactly gsoSize, so a short segment must be
  252. // the final one. The caller must deregister a closed slot from openSlots.
  253. func (c *UDPCoalescer) appendPayload(s *udpSlot, pkt []byte, info *parsedUDP) bool {
  254. s.payIovs = append(s.payIovs, pkt[info.hdrLen:info.hdrLen+info.payLen])
  255. s.numSeg++
  256. s.totalPay += info.payLen
  257. return info.payLen < s.gsoSize
  258. }
  259. func (c *UDPCoalescer) take() *udpSlot {
  260. if n := len(c.pool); n > 0 {
  261. s := c.pool[n-1]
  262. c.pool[n-1] = nil
  263. c.pool = c.pool[:n-1]
  264. return s
  265. }
  266. return &udpSlot{}
  267. }
  268. func (c *UDPCoalescer) release(s *udpSlot) {
  269. // Reset every field, identity ones included; see TCPCoalescer.release.
  270. clear(s.payIovs)
  271. *s = udpSlot{payIovs: s.payIovs[:0]}
  272. c.pool = append(c.pool, s)
  273. }
  274. // flushSlot patches the IP header total length / IPv6 payload length and
  275. // the UDP length to the *total* across all coalesced segments, then seeds
  276. // the UDP checksum field with the pseudo-header partial (single-fold, not
  277. // inverted) per virtio NEEDS_CSUM. The patches land in place in rawPkt; the
  278. // slot is released right after, so nothing re-reads the patched header.
  279. func (c *UDPCoalescer) flushSlot(s *udpSlot) error {
  280. hdr := s.rawPkt[:s.hdrLen]
  281. total := s.hdrLen + s.totalPay // full IP+UDP+all_payloads bytes
  282. l4Len := total - s.ipHdrLen // total UDP (8 + sum of payloads)
  283. if s.isV6 {
  284. binary.BigEndian.PutUint16(hdr[4:6], uint16(l4Len))
  285. } else {
  286. binary.BigEndian.PutUint16(hdr[2:4], uint16(total))
  287. hdr[10] = 0
  288. hdr[11] = 0
  289. binary.BigEndian.PutUint16(hdr[10:12], ipv4HdrChecksum(hdr[:s.ipHdrLen]))
  290. }
  291. // UDP length field (offset 4 inside the UDP header) = total UDP size.
  292. binary.BigEndian.PutUint16(hdr[s.ipHdrLen+4:s.ipHdrLen+6], uint16(l4Len))
  293. var psum uint32
  294. if s.isV6 {
  295. psum = pseudoSumIPv6(hdr[8:24], hdr[24:40], ipProtoUDP, l4Len)
  296. } else {
  297. psum = pseudoSumIPv4(hdr[12:16], hdr[16:20], ipProtoUDP, l4Len)
  298. }
  299. udpCsumOff := s.ipHdrLen + 6
  300. binary.BigEndian.PutUint16(hdr[udpCsumOff:udpCsumOff+2], foldOnceNoInvert(psum))
  301. return c.w.WriteGSO(hdr[:s.ipHdrLen], hdr[s.ipHdrLen:], s.payIovs, tio.GSOProtoUDP)
  302. }
  303. // udpHeadersMatch compares two IP+UDP header prefixes for byte-equality on
  304. // every field that must be identical across coalesced segments
  305. func udpHeadersMatch(a, b []byte, isV6 bool, ipHdrLen int) bool {
  306. if len(a) != len(b) {
  307. return false
  308. }
  309. if !ipHeadersMatch(a, b, isV6) {
  310. return false
  311. }
  312. // UDP: compare sport+dport ([0:4]). Skip length [4:6] and checksum [6:8]:
  313. // length varies (we rewrite at flush) and the checksum will be redone.
  314. udp := ipHdrLen
  315. return bytes.Equal(a[udp:udp+4], b[udp:udp+4])
  316. }