udp_linux_writebatch.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. //go:build linux && !android && !e2e_testing
  2. package udp
  3. import (
  4. "encoding/binary"
  5. "fmt"
  6. "log/slog"
  7. "net"
  8. "net/netip"
  9. "strconv"
  10. "strings"
  11. "unsafe"
  12. "golang.org/x/sys/unix"
  13. )
  14. // batchWriter owns the sendmmsg(2)/UDP-GSO transmit path for a StdConn: the
  15. // per-queue scratch WriteBatch packs mmsghdr entries into, plus the GSO
  16. // capability state probed once at socket creation. Each queue has its own
  17. // StdConn and therefore its own batchWriter, so no locking is needed.
  18. type batchWriter struct {
  19. fd int
  20. isV4 bool
  21. l *slog.Logger
  22. // UDP GSO (sendmsg with UDP_SEGMENT cmsg) support. gsoSupported is
  23. // probed once at socket creation. When true, WriteBatch packs same-
  24. // destination consecutive packets into a single sendmmsg entry with a
  25. // UDP_SEGMENT cmsg; otherwise each packet is its own entry.
  26. gsoSupported bool
  27. maxGSOSegments int
  28. // sendmmsg scratch, sized to MaxWriteBatch at construction; WriteBatch
  29. // chunks larger inputs.
  30. msgs []rawMessage
  31. iovs []iovec
  32. names [][]byte
  33. // Per-entry cmsg scratch. cmsg is one contiguous slab of
  34. // MaxWriteBatch * cmsgSpace bytes; each entry holds two cmsg headers
  35. // (UDP_SEGMENT then IP_TOS / IPV6_TCLASS) pre-filled once in
  36. // prepareWriteMessages. WriteBatch only rewrites the per-call data
  37. // payloads and toggles Hdr.Control / Hdr.Controllen to point at
  38. // whichever subset of the two cmsgs applies.
  39. cmsg []byte
  40. cmsgSpace int
  41. cmsgSegSpace int
  42. cmsgEcnSpace int
  43. // entryEnd[e] is the bufs index *after* the last packet packed into
  44. // mmsghdr entry e. Used to rewind `i` on partial sendmmsg success.
  45. entryEnd []int
  46. }
  47. func newBatchWriter(fd int, isV4 bool, l *slog.Logger) *batchWriter {
  48. w := &batchWriter{fd: fd, isV4: isV4, l: l}
  49. w.prepareWriteMessages(MaxWriteBatch)
  50. w.prepareGSO()
  51. return w
  52. }
  53. // prepareWriteMessages allocates one mmsghdr/iovec/sockaddr/cmsg scratch
  54. // slot per sendmmsg entry. The iovec slab is sized to n so all entries'
  55. // iovecs share one allocation; per-entry fan-out is further capped at
  56. // maxGSOSegments. Hdr.Iov / Hdr.Iovlen / Hdr.Control / Hdr.Controllen are
  57. // wired per call since each entry can span a variable number of iovecs
  58. // and may or may not carry a cmsg.
  59. //
  60. // Per-mmsghdr cmsg layout. Each entry's slot of length cmsgSpace holds
  61. // up to two cmsg headers placed at fixed offsets:
  62. //
  63. // [0 .. cmsgSegSpace) UDP_SEGMENT (gso_size, uint16)
  64. // [cmsgSegSpace .. cmsgSpace) IP_TOS or IPV6_TCLASS (int32)
  65. //
  66. // Both headers are pre-filled once here; per-call we only rewrite the data
  67. // payload and toggle Hdr.Control / Hdr.Controllen to point at whichever
  68. // subset applies (none / segment-only / ecn-only / both).
  69. func (w *batchWriter) prepareWriteMessages(n int) {
  70. w.msgs = make([]rawMessage, n)
  71. w.iovs = make([]iovec, n)
  72. w.names = make([][]byte, n)
  73. w.entryEnd = make([]int, n)
  74. w.cmsgSegSpace = unix.CmsgSpace(2)
  75. w.cmsgEcnSpace = unix.CmsgSpace(4)
  76. w.cmsgSpace = w.cmsgSegSpace + w.cmsgEcnSpace
  77. w.cmsg = make([]byte, n*w.cmsgSpace)
  78. // Default the ECN header to the socket's own family. writeEntryCmsg
  79. // finalizes Level/Type per entry from the destination address (a v4-mapped
  80. // dst on a dual-stack v6 socket needs IP_TOS, not IPV6_TCLASS), so this is
  81. // only the value used before the first per-entry rewrite.
  82. ecnLevel := int32(unix.IPPROTO_IP)
  83. ecnType := int32(unix.IP_TOS)
  84. if !w.isV4 {
  85. ecnLevel = unix.IPPROTO_IPV6
  86. ecnType = unix.IPV6_TCLASS
  87. }
  88. for k := 0; k < n; k++ {
  89. base := k * w.cmsgSpace
  90. seg := (*unix.Cmsghdr)(unsafe.Pointer(&w.cmsg[base]))
  91. seg.Level = unix.SOL_UDP
  92. seg.Type = unix.UDP_SEGMENT
  93. setCmsgLen(seg, unix.CmsgLen(2))
  94. ecn := (*unix.Cmsghdr)(unsafe.Pointer(&w.cmsg[base+w.cmsgSegSpace]))
  95. ecn.Level = ecnLevel
  96. ecn.Type = ecnType
  97. setCmsgLen(ecn, unix.CmsgLen(4))
  98. }
  99. for i := range w.msgs {
  100. w.names[i] = make([]byte, unix.SizeofSockaddrInet6)
  101. w.msgs[i].Hdr.Name = &w.names[i][0]
  102. }
  103. }
  104. // maxGSOBytes bounds the total payload per sendmsg() when UDP_SEGMENT is
  105. // set. The kernel stitches all iovecs into a single skb whose length the
  106. // UDP length field can represent, and also enforces sk_gso_max_size (which
  107. // on most devices is 65536). We use 65000 to leave headroom under the
  108. // 65535 UDP-length cap, avoiding EMSGSIZE on large TSO superpackets.
  109. const maxGSOBytes = 65000
  110. // prepareGSO probes UDP_SEGMENT support and sets w.gsoSupported on success.
  111. // Best-effort; failure leaves it false.
  112. func (w *batchWriter) prepareGSO() {
  113. w.maxGSOSegments = 63 //gotta be one less than the max so we can still attach a header
  114. if err := unix.SetsockoptInt(w.fd, unix.IPPROTO_UDP, unix.UDP_SEGMENT, 0); err != nil {
  115. w.l.Info("udp: GSO disabled", "reason", "rawconn control failed", "error", err)
  116. recordCapability("udp.gso.enabled", false)
  117. return
  118. }
  119. var un unix.Utsname
  120. if err := unix.Uname(&un); err != nil {
  121. w.l.Info("udp: GSO disabled", "reason", "kernel uname probe failed", "error", err)
  122. recordCapability("udp.gso.enabled", false)
  123. return
  124. }
  125. w.maxGSOSegments = gsoMaxSegments(string(un.Release[:]))
  126. w.gsoSupported = true
  127. w.l.Info("udp: GSO enabled", "maxGSOSegments", w.maxGSOSegments)
  128. recordCapability("udp.gso.enabled", true)
  129. }
  130. // gsoMaxSegments returns the largest number of UDP_SEGMENT segments a single
  131. // sendmsg may carry on the running kernel, reserving one segment for the
  132. // header. UDP_MAX_SEGMENTS was 64 until Linux v6.9 (commit 1382e3b6a350,
  133. // "udp: change maximum number of UDP segments to 128") raised it to 128;
  134. // nothing about this changed in 5.5. On kernels older than 6.9 packing more
  135. // than 64 segments gets the sendmsg rejected with EINVAL, so cap at 63 there
  136. // and only use 127 from 6.9 on. (Maintainer stance: update your kernel if you
  137. // want to go fast — this is a plain version gate, not a runtime probe.)
  138. func gsoMaxSegments(release string) int {
  139. major, minor := parseRelease(release)
  140. if major > 6 || (major == 6 && minor >= 9) {
  141. return 127
  142. }
  143. return 63
  144. }
  145. func parseRelease(r string) (major, minor int) {
  146. // strip anything after the second dot or any non-digit
  147. parts := strings.SplitN(r, ".", 3)
  148. if len(parts) < 2 {
  149. return 0, 0
  150. }
  151. major, _ = strconv.Atoi(parts[0])
  152. // minor may have trailing junk like "15-generic"
  153. mp := parts[1]
  154. for i, c := range mp {
  155. if c < '0' || c > '9' {
  156. mp = mp[:i]
  157. break
  158. }
  159. }
  160. minor, _ = strconv.Atoi(mp)
  161. return
  162. }
  163. // WriteBatch sends bufs via sendmmsg(2) using the preallocated scratch on
  164. // batchWriter. Consecutive packets to the same destination with matching
  165. // segment sizes (all but possibly the last) are coalesced into a single
  166. // mmsghdr entry carrying a UDP_SEGMENT cmsg, so one syscall can mix runs of
  167. // GSO superpackets with plain one-off datagrams. Without GSO support every
  168. // packet is its own entry, matching the prior behaviour.
  169. //
  170. // Chunks larger than the scratch are processed across multiple syscalls. If
  171. // sendmmsg returns an error AND zero entries went out we fall back to
  172. // per-packet sendto for that chunk so the caller still gets best-effort
  173. // delivery; on a partial-success error we just replay the remainder.
  174. func (w *batchWriter) WriteBatch(bufs [][]byte, addrs []netip.AddrPort, ecns []byte) error {
  175. if len(bufs) != len(addrs) {
  176. return fmt.Errorf("WriteBatch: len(bufs)=%d != len(addrs)=%d", len(bufs), len(addrs))
  177. }
  178. if ecns != nil && len(ecns) != len(bufs) {
  179. return fmt.Errorf("WriteBatch: len(ecns)=%d != len(bufs)=%d", len(ecns), len(bufs))
  180. }
  181. // Callers deliver same-destination packets contiguously and in counter
  182. // order, so we run the GSO planner directly without a pre-sort. A
  183. // sorting pass measurably hurt throughput in microbenchmarks while
  184. // providing no observed reordering benefit.
  185. i := 0
  186. sendChunks:
  187. for i < len(bufs) {
  188. baseI := i
  189. entry := 0
  190. iovIdx := 0
  191. for entry < len(w.msgs) && i < len(bufs) {
  192. iovBudget := len(w.iovs) - iovIdx
  193. if iovBudget < 1 {
  194. break
  195. }
  196. runLen, segSize := w.planRun(bufs, addrs, ecns, i, iovBudget)
  197. if runLen == 0 {
  198. break
  199. }
  200. for k := 0; k < runLen; k++ {
  201. b := bufs[i+k]
  202. if len(b) == 0 {
  203. w.iovs[iovIdx+k].Base = nil
  204. setIovLen(&w.iovs[iovIdx+k], 0)
  205. } else {
  206. w.iovs[iovIdx+k].Base = &b[0]
  207. setIovLen(&w.iovs[iovIdx+k], len(b))
  208. }
  209. }
  210. nlen, err := writeSockaddr(w.names[entry], addrs[i], w.isV4)
  211. if err != nil {
  212. // One destination in this chunk has an address family the
  213. // socket can't send to (e.g. an IPv6 remote on a v4-bound
  214. // socket → ErrInvalidIPv6RemoteForSocket). Abandoning the whole
  215. // sendmmsg here would drop every packet already packed for this
  216. // chunk plus every packet still ahead of us in bufs. Instead
  217. // fall back to per-packet sendto for the packets packed so far
  218. // in this chunk and the offending one: sendto delivers each
  219. // good destination and only errors on the bad one, which we
  220. // drop and keep going. One bad destination costs one packet,
  221. // never the batch. (Same fallback the zero-sent sendmmsg path
  222. // below uses, extended to cover the misaddressed packet.)
  223. for k := baseI; k <= i; k++ {
  224. if werr := sendto(w.fd, bufs[k], addrs[k], w.isV4); werr != nil && k != i {
  225. return werr
  226. }
  227. }
  228. i++
  229. continue sendChunks
  230. }
  231. hdr := &w.msgs[entry].Hdr
  232. hdr.Iov = &w.iovs[iovIdx]
  233. setMsgIovlen(hdr, runLen)
  234. hdr.Namelen = uint32(nlen)
  235. var ecn byte
  236. if ecns != nil {
  237. ecn = ecns[i]
  238. }
  239. // ECN cmsg family follows the destination, not the socket: a
  240. // v4-mapped dst on a dual-stack v6 socket must be stamped via
  241. // IP_TOS. addrs[i] is this run's destination (i advances below).
  242. dstIsV4 := addrs[i].Addr().Unmap().Is4()
  243. w.writeEntryCmsg(entry, runLen, segSize, ecn, dstIsV4)
  244. i += runLen
  245. iovIdx += runLen
  246. w.entryEnd[entry] = i
  247. entry++
  248. }
  249. if entry == 0 {
  250. return fmt.Errorf("sendmmsg: no progress")
  251. }
  252. sent, serr := w.sendmmsg(entry)
  253. if serr != nil && sent <= 0 {
  254. // Nothing went out for this chunk; fall back to sendto for each
  255. // packet that was queued this iteration. We only enter this path
  256. // when sendmmsg returned an error AND zero entries succeeded —
  257. // otherwise the partial-success advance below replays only the
  258. // remainder, avoiding duplicates of already-sent packets.
  259. //
  260. // sent=-1 from sendmmsg means message 0 itself failed (partial
  261. // success returns the count instead), so log entry 0's parameters
  262. // — that's the entry the kernel rejected.
  263. hdr0 := &w.msgs[0].Hdr
  264. runLen0 := w.entryEnd[0] - baseI
  265. seg0 := len(bufs[baseI])
  266. ecn0 := byte(0)
  267. if ecns != nil {
  268. ecn0 = ecns[baseI]
  269. }
  270. w.l.Warn("sendmmsg had problem",
  271. "sent", sent, "err", serr,
  272. "entries", entry,
  273. "entry0_runLen", runLen0,
  274. "entry0_segSize", seg0,
  275. "entry0_iovlen", hdr0.Iovlen,
  276. "entry0_controllen", hdr0.Controllen,
  277. "entry0_namelen", hdr0.Namelen,
  278. "entry0_ecn", ecn0,
  279. "entry0_dst", addrs[baseI],
  280. "isV4", w.isV4,
  281. "gso", w.gsoSupported,
  282. )
  283. for k := baseI; k < i; k++ {
  284. if werr := sendto(w.fd, bufs[k], addrs[k], w.isV4); werr != nil {
  285. return werr
  286. }
  287. }
  288. continue
  289. }
  290. if sent == 0 {
  291. return fmt.Errorf("sendmmsg made no progress")
  292. }
  293. // Rewind i to the end of the last successfully sent entry. For a
  294. // full-success send this leaves i unchanged; for a partial send it
  295. // replays the remainder on the next outer-loop iteration.
  296. i = w.entryEnd[sent-1]
  297. }
  298. return nil
  299. }
  300. // planRun groups consecutive packets starting at `start` that can be sent as
  301. // a single UDP GSO superpacket (one sendmmsg entry with UDP_SEGMENT cmsg).
  302. // A run of length 1 means the entry carries no UDP_SEGMENT cmsg and the
  303. // kernel treats it as a plain datagram. Returns the run length and the
  304. // per-segment size (which equals len(bufs[start])). Without GSO support
  305. // every call returns runLen=1. Outer ECN (when ecns != nil) is also a run
  306. // boundary — the kernel stamps one outer codepoint per sendmsg entry, so
  307. // mixing values inside a run would lose information.
  308. func (w *batchWriter) planRun(bufs [][]byte, addrs []netip.AddrPort, ecns []byte, start, iovBudget int) (int, int) {
  309. if start >= len(bufs) || iovBudget < 1 {
  310. return 0, 0
  311. }
  312. segSize := len(bufs[start])
  313. if !w.gsoSupported || segSize == 0 || segSize > maxGSOBytes {
  314. return 1, segSize
  315. }
  316. dst := addrs[start]
  317. var ecn byte
  318. if ecns != nil {
  319. ecn = ecns[start]
  320. }
  321. maxLen := w.maxGSOSegments
  322. if iovBudget < maxLen {
  323. maxLen = iovBudget
  324. }
  325. runLen := 1
  326. total := segSize
  327. for runLen < maxLen && start+runLen < len(bufs) {
  328. nextLen := len(bufs[start+runLen])
  329. if nextLen == 0 || nextLen > segSize {
  330. break
  331. }
  332. if addrs[start+runLen] != dst {
  333. break
  334. }
  335. if ecns != nil && ecns[start+runLen] != ecn {
  336. break
  337. }
  338. if total+nextLen > maxGSOBytes {
  339. break
  340. }
  341. total += nextLen
  342. runLen++
  343. if nextLen < segSize {
  344. // A short packet must be the last in the run.
  345. break
  346. }
  347. }
  348. return runLen, segSize
  349. }
  350. // writeEntryCmsg sets up the per-mmsghdr Hdr.Control / Hdr.Controllen for one
  351. // entry. It writes the UDP_SEGMENT payload when runLen >= 2 and the
  352. // IP_TOS/IPV6_TCLASS payload when ecn != 0, then points hdr.Control at the
  353. // smallest contiguous span that covers whichever cmsg(s) actually apply.
  354. //
  355. // The outer-ECN cmsg family must match the *destination*, not the socket: on
  356. // the default dual-stack v6 bind, a v4-mapped destination is routed through
  357. // the kernel's IPv4 path, which parses IP_TOS (IPPROTO_IP) and ignores an
  358. // IPV6_TCLASS cmsg. prepareWriteMessages pre-fills a default header; here we
  359. // rewrite its Level/Type (and Len) per entry from dstIsV4 so v4 peers get
  360. // IP_TOS and v6 peers get IPV6_TCLASS. The data payload is a 4-byte int for
  361. // both families, so the pre-computed cmsg space is unchanged.
  362. func (w *batchWriter) writeEntryCmsg(entry, runLen, segSize int, ecn byte, dstIsV4 bool) {
  363. hdr := &w.msgs[entry].Hdr
  364. useSeg := runLen >= 2
  365. useEcn := ecn != 0
  366. base := entry * w.cmsgSpace
  367. if useSeg {
  368. dataOff := base + unix.CmsgLen(0)
  369. binary.NativeEndian.PutUint16(w.cmsg[dataOff:dataOff+2], uint16(segSize))
  370. }
  371. if useEcn {
  372. ecnHdr := (*unix.Cmsghdr)(unsafe.Pointer(&w.cmsg[base+w.cmsgSegSpace]))
  373. if dstIsV4 {
  374. ecnHdr.Level = int32(unix.IPPROTO_IP)
  375. ecnHdr.Type = int32(unix.IP_TOS)
  376. } else {
  377. ecnHdr.Level = int32(unix.IPPROTO_IPV6)
  378. ecnHdr.Type = int32(unix.IPV6_TCLASS)
  379. }
  380. setCmsgLen(ecnHdr, unix.CmsgLen(4))
  381. dataOff := base + w.cmsgSegSpace + unix.CmsgLen(0)
  382. binary.NativeEndian.PutUint32(w.cmsg[dataOff:dataOff+4], uint32(ecn))
  383. }
  384. switch {
  385. case useSeg && useEcn:
  386. hdr.Control = &w.cmsg[base]
  387. setMsgControllen(hdr, w.cmsgSpace)
  388. case useSeg:
  389. hdr.Control = &w.cmsg[base]
  390. setMsgControllen(hdr, w.cmsgSegSpace)
  391. case useEcn:
  392. hdr.Control = &w.cmsg[base+w.cmsgSegSpace]
  393. setMsgControllen(hdr, w.cmsgEcnSpace)
  394. default:
  395. hdr.Control = nil
  396. setMsgControllen(hdr, 0)
  397. }
  398. }
  399. // sendmmsg issues sendmmsg(2) against the first n entries of w.msgs.
  400. func (w *batchWriter) sendmmsg(n int) (int, error) {
  401. r1, _, errno := unix.Syscall6(unix.SYS_SENDMMSG, uintptr(w.fd),
  402. uintptr(unsafe.Pointer(&w.msgs[0])), uintptr(n),
  403. 0, 0, 0,
  404. )
  405. sent := int(r1)
  406. if errno != 0 {
  407. return sent, &net.OpError{Op: "sendmmsg", Err: errno}
  408. }
  409. return sent, nil
  410. }