| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402 |
- //go:build linux && !android && !e2e_testing
- package udp
- import (
- "context"
- "encoding/binary"
- "errors"
- "fmt"
- "log/slog"
- "net"
- "net/netip"
- "strconv"
- "strings"
- "sync"
- "unsafe"
- "golang.org/x/sys/unix"
- )
- // batchWriter owns the sendmmsg(2)/UDP-GSO transmit path for a StdConn: the
- // scratch WriteBatch packs mmsghdr entries into, plus the GSO capability
- // state probed at socket creation.
- //
- // One socket can have several senders: under multiport a routine writes lane s
- // traffic to the socket at its own group position on lane s's port, so every
- // socket is shared by the routines at that group position, one per port, since
- // a packet's lane comes from its flow rather than from the routine that read
- // it. The scratch is per socket, not per sender, so mu serializes
- // packing and draining. It is taken once per flush — a syscall's worth of work —
- // and only contends when two routines flush the same socket at the same instant.
- //
- // Terminology, smallest to largest:
- //
- // packet one element of bufs: a single UDP datagram. The unit of the
- // returned written count.
- // run consecutive packets planRun groups into one entry: same
- // destination, equal sizes (a shorter packet only last), within
- // maxGSOBytes and maxGSOSegments. Without GSO a run is always one
- // packet. Runs are atomic: packed whole into one entry, or
- // skipped whole if the socket cannot address their destination,
- // leaving a hole (bufs indices covered by no entry).
- // entry one mmsghdr slot of the sendmmsg array; the kernel's unit of
- // success and failure. A multi-packet entry carries a UDP_SEGMENT
- // cmsg and is sent as one superpacket the kernel segments into
- // gso_size-byte datagrams. Entries never split.
- // chunk the entries packed for one sendmmsg call, at most MaxWriteBatch.
- // batch the caller's whole bufs/addrs pair, processed as one or more chunks.
- type batchWriter struct {
- fd int
- isV4 bool
- // mu guards everything below it: the scratch, and the GSO state WriteBatch
- // can clear.
- mu sync.Mutex
- // UDP GSO (sendmsg with UDP_SEGMENT cmsg) support, probed once at
- // socket creation and cleared by WriteBatch if the kernel later rejects
- // a GSO send (the setsockopt probe cannot see per-route limitations).
- // When true, WriteBatch coalesces runs into UDP_SEGMENT entries;
- // otherwise each packet is its own entry.
- gsoSupported bool
- maxGSOSegments int
- // sendmmsg scratch, sized to MaxWriteBatch at construction; WriteBatch
- // chunks larger inputs.
- msgs []rawMessage
- iovs []iovec
- names [][]byte
- l *slog.Logger
- // sendFn sends n prepared entries beginning at w.msgs[start]
- // This is a function pointer to facilitate testing.
- sendFn func(start, n int) (int, error)
- // Per-entry cmsg scratch: one contiguous slab of
- // MaxWriteBatch * cmsgSpace bytes holding one UDP_SEGMENT cmsg per entry.
- cmsg []byte
- cmsgSpace int
- // entryEnd[e] is the bufs index after the last packet packed into entry e.
- // entryEnd[e]-entryPkts[e] recovers the bufs index the entry's run started at,
- // used to rewind i for the GSO-disable replay.
- entryEnd []int
- // entryPkts[e] is the number of packets packed into entry e.
- entryPkts []int
- }
- func newBatchWriter(fd int, isV4 bool, l *slog.Logger, offloadsEnabled bool) *batchWriter {
- w := &batchWriter{fd: fd, isV4: isV4, l: l}
- w.sendFn = w.sendmmsg
- if offloadsEnabled {
- w.prepareGSO()
- }
- w.prepareWriteMessages(MaxWriteBatch, offloadsEnabled)
- return w
- }
- // prepareWriteMessages allocates the per-entry mmsghdr/iovec/sockaddr/cmsg
- // scratch. Hdr.Iov/Iovlen/Control/Controllen are wired per call, since an
- // entry spans a variable number of iovecs and may or may not carry a cmsg.
- //
- // Each entry's cmsg slot holds one UDP_SEGMENT (gso_size, uint16) header,
- // pre-filled here; only its payload is rewritten per call.
- // Hdr.Control/Controllen select whether it applies (none / segment).
- func (w *batchWriter) prepareWriteMessages(n int, offloadsEnabled bool) {
- w.msgs = make([]rawMessage, n)
- w.iovs = make([]iovec, n)
- w.names = make([][]byte, n)
- w.entryEnd = make([]int, n)
- w.entryPkts = make([]int, n)
- w.cmsgSpace = unix.CmsgSpace(2)
- for i := range w.msgs {
- w.names[i] = make([]byte, unix.SizeofSockaddrInet6)
- w.msgs[i].Hdr.Name = &w.names[i][0]
- }
- if !offloadsEnabled || !w.gsoSupported {
- return //avoid allocating cmsg space if we will never use it
- }
- w.cmsg = make([]byte, n*w.cmsgSpace)
- for k := 0; k < n; k++ {
- base := k * w.cmsgSpace
- seg := (*unix.Cmsghdr)(unsafe.Pointer(&w.cmsg[base]))
- seg.Level = unix.SOL_UDP
- seg.Type = unix.UDP_SEGMENT
- setCmsgLen(seg, unix.CmsgLen(2))
- }
- }
- // maxGSOBytes bounds the total payload of one UDP_SEGMENT send. The kernel
- // builds a single skb, which must fit the 16-bit UDP length field and
- // sk_gso_max_size (65536 on most devices); 65000 leaves headroom for headers.
- const maxGSOBytes = 65000
- // prepareGSO probes UDP_SEGMENT support and sets w.gsoSupported on success.
- // Best-effort; failure leaves it false.
- func (w *batchWriter) prepareGSO() {
- w.maxGSOSegments = 63 // pre-6.9 cap; see gsoMaxSegments
- if err := unix.SetsockoptInt(w.fd, unix.IPPROTO_UDP, unix.UDP_SEGMENT, 0); err != nil {
- w.l.Info("udp: GSO disabled", "reason", "rawconn control failed", "error", err)
- recordCapability("udp.gso.enabled", false)
- return
- }
- var un unix.Utsname
- if err := unix.Uname(&un); err != nil {
- w.l.Warn("udp: kernel version probe failed, capping GSO at 63 segments", "error", err)
- } else {
- w.maxGSOSegments = gsoMaxSegments(string(un.Release[:]))
- }
- w.gsoSupported = true
- w.l.Info("udp: GSO enabled", "maxGSOSegments", w.maxGSOSegments)
- recordCapability("udp.gso.enabled", true)
- }
- // gsoMaxSegments returns the most segments one UDP_SEGMENT send may carry:
- // the kernel cap (UDP_MAX_SEGMENTS: 64 before 6.9, 128 after) minus one,
- // because the kernel counts the 8-byte UDP header against the gso_size * UDP_MAX_SEGMENTS budget.
- func gsoMaxSegments(release string) int {
- major, minor := parseRelease(release)
- if major > 6 || (major == 6 && minor >= 9) {
- return 127
- }
- return 63
- }
- func parseRelease(r string) (major, minor int) {
- // strip anything after the second dot or any non-digit
- parts := strings.SplitN(r, ".", 3)
- if len(parts) < 2 {
- return 0, 0
- }
- major, _ = strconv.Atoi(parts[0])
- // minor may have trailing junk like "15-generic"
- mp := parts[1]
- for i, c := range mp {
- if c < '0' || c > '9' {
- mp = mp[:i]
- break
- }
- }
- minor, _ = strconv.Atoi(mp)
- return
- }
- // WriteBatch sends bufs via sendmmsg(2), coalescing runs into UDP_SEGMENT
- // entries, so one syscall can mix GSO superpackets and plain datagrams.
- // Without GSO support every packet is its own entry.
- // Callers shall deliver same-destination packets contiguously and in counter order
- //
- // Batches larger than the scratch take one sendmmsg per chunk.
- // A partial success resumes the same prepared entries at the first unsent entry.
- // A zero-sent error means the kernel rejected the first remaining entry:
- // its packets are dropped and the rest of the chunk resumes in place.
- //
- // Returns the number of packets sent. An error means the call itself failed.
- // A short count means some destinations were undeliverable.
- func (w *batchWriter) WriteBatch(bufs [][]byte, addrs []netip.AddrPort) (int, error) {
- if len(bufs) != len(addrs) {
- return 0, fmt.Errorf("WriteBatch: len(bufs)=%d != len(addrs)=%d", len(bufs), len(addrs))
- }
- w.mu.Lock()
- defer w.mu.Unlock()
- // A destination the kernel rejects results in us dropping that entry (one packet, or one same-destination GSO run).
- // We count what actually made it out rather than returning an error.
- written := 0
- i := 0
- for i < len(bufs) {
- entry := 0
- iovIdx := 0
- for entry < len(w.msgs) && i < len(bufs) {
- iovBudget := len(w.iovs) - iovIdx
- if iovBudget < 1 {
- break
- }
- runLen, segSize := w.planRun(bufs, addrs, i, iovBudget)
- if runLen == 0 {
- break
- }
- for k := 0; k < runLen; k++ {
- b := bufs[i+k]
- if len(b) == 0 {
- w.iovs[iovIdx+k].Base = nil
- setIovLen(&w.iovs[iovIdx+k], 0)
- } else {
- w.iovs[iovIdx+k].Base = &b[0]
- setIovLen(&w.iovs[iovIdx+k], len(b))
- }
- }
- nlen, err := writeSockaddr(w.names[entry], addrs[i], w.isV4)
- if err != nil {
- // The destination's address family does not match the socket
- // (e.g. an IPv6 remote on a v4-bound socket). The packets are
- // undeliverable and no entry is committed yet: skip the run.
- if w.l.Enabled(context.Background(), slog.LevelDebug) {
- w.l.Debug("skipping unroutable batch destination", "udpAddr", addrs[i], "packets", runLen, "error", err)
- }
- i += runLen
- continue
- }
- hdr := &w.msgs[entry].Hdr
- hdr.Iov = &w.iovs[iovIdx]
- setMsgIovlen(hdr, runLen)
- hdr.Namelen = uint32(nlen)
- w.writeEntryCmsg(entry, runLen, segSize)
- i += runLen
- iovIdx += runLen
- w.entryEnd[entry] = i
- w.entryPkts[entry] = runLen
- entry++
- }
- if entry == 0 {
- // Every remaining packet was skipped; i reached len(bufs).
- break
- }
- // Drain the packed entries without repacking: everything the packing
- // loop wired (iovecs, names, cmsgs) stays intact until the next chunk
- // overwrites it, so a partial success resumes the same sendmmsg array
- // at the first unsent entry, and a rejected entry is skipped in place.
- // Only the GSO-disable path replans, since its entries change shape.
- done := 0
- for done < entry {
- sent, serr := w.sendFn(done, entry-done)
- if sent > 0 {
- // Count packets per entry; the bufs index span would
- // overcount across holes left by skipped runs.
- for e := done; e < done+sent; e++ {
- written += w.entryPkts[e]
- }
- done += sent
- continue
- }
- if serr == nil {
- return written, fmt.Errorf("sendmmsg made no progress")
- }
- // sent<=0 means the first remaining entry itself failed.
- // EIO on a superpacket means the route cannot carry a GSO send even though the setsockopt probe passed:
- // udp_send_skb() returns EIO when:
- // * the egress device lacks TX checksum offload (kernels through 6.10)
- // * or when an xfrm policy covers the route.
- // Persistent, so disable GSO and replay from the failed run as one-packet entries.
- if w.gsoSupported && w.entryPkts[done] >= 2 && errors.Is(serr, unix.EIO) {
- w.gsoSupported = false
- w.l.Warn("udp: kernel rejected GSO send, disabling GSO", "error", serr)
- recordCapability("udp.gso.enabled", false)
- i = w.entryEnd[done] - w.entryPkts[done]
- break
- }
- // Any other zero-sent error is a per-entry failure.
- // Transient errnos (EINTR, ENOBUFS) were already retried inside sendFn.
- // These packets are doomed. Log them and move on.
- if w.l.Enabled(context.Background(), slog.LevelDebug) {
- w.l.Debug("sendmmsg rejected entry",
- "error", serr,
- "udpAddr", addrs[w.entryEnd[done]-w.entryPkts[done]],
- "packets", w.entryPkts[done],
- "gso", w.gsoSupported,
- )
- }
- done++
- }
- }
- return written, nil
- }
- // planRun returns the length of the run starting at start and its segment
- // size (len(bufs[start])). A run of length 1 carries no UDP_SEGMENT cmsg
- // and is sent as a plain datagram; without GSO support planRun always returns 1.
- func (w *batchWriter) planRun(bufs [][]byte, addrs []netip.AddrPort, start, iovBudget int) (int, int) {
- if start >= len(bufs) || iovBudget < 1 {
- return 0, 0
- }
- segSize := len(bufs[start])
- if !w.gsoSupported || segSize == 0 || segSize > maxGSOBytes {
- return 1, segSize
- }
- dst := addrs[start]
- maxLen := w.maxGSOSegments
- if iovBudget < maxLen {
- maxLen = iovBudget
- }
- runLen := 1
- total := segSize
- for runLen < maxLen && start+runLen < len(bufs) {
- nextLen := len(bufs[start+runLen])
- if nextLen == 0 || nextLen > segSize {
- break
- }
- if addrs[start+runLen] != dst {
- break
- }
- if total+nextLen > maxGSOBytes {
- break
- }
- total += nextLen
- runLen++
- if nextLen < segSize {
- // A short packet must be the last in the run.
- break
- }
- }
- return runLen, segSize
- }
- // writeEntryCmsg writes one entry's UDP_SEGMENT payload when runLen >= 2 and
- // points Hdr.Control at it; a single-packet entry carries no cmsg.
- func (w *batchWriter) writeEntryCmsg(entry, runLen, segSize int) {
- hdr := &w.msgs[entry].Hdr
- base := entry * w.cmsgSpace
- if runLen >= 2 {
- dataOff := base + unix.CmsgLen(0)
- binary.NativeEndian.PutUint16(w.cmsg[dataOff:dataOff+2], uint16(segSize))
- hdr.Control = &w.cmsg[base]
- setMsgControllen(hdr, w.cmsgSpace)
- } else {
- hdr.Control = nil
- setMsgControllen(hdr, 0)
- }
- }
- // sendmmsg issues sendmmsg(2) against n entries of w.msgs starting at start.
- //
- // EINTR is automatically retried and will never be returned.
- // ENOBUFS is retried enobufsRetries times, and should be treated like any other error
- func (w *batchWriter) sendmmsg(start, n int) (int, error) {
- const enobufsRetries = 3
- for enobufs := 0; ; {
- r1, _, errno := unix.Syscall6(unix.SYS_SENDMMSG, uintptr(w.fd),
- uintptr(unsafe.Pointer(&w.msgs[start])), uintptr(n),
- 0, 0, 0,
- )
- switch {
- case errno == unix.EINTR: //similar to stdlib's ignoringEINTRIO
- continue
- case errno == unix.ENOBUFS && enobufs < enobufsRetries:
- enobufs++ //worth a retry or three
- continue
- case errno != 0:
- return int(r1), &net.OpError{Op: "sendmmsg", Err: errno}
- }
- return int(r1), nil
- }
- }
|