//go:build !android && !e2e_testing // +build !android,!e2e_testing package udp import ( "encoding/binary" "errors" "fmt" "log/slog" "net" "net/netip" "strconv" "strings" "sync/atomic" "syscall" "unsafe" "github.com/rcrowley/go-metrics" "github.com/slackhq/nebula/config" "golang.org/x/sys/unix" ) type StdConn struct { sysFd int closed atomic.Bool isV4 bool l *slog.Logger batch int // sendmmsg scratch. Each queue has its own StdConn, so no locking is // needed. Sized to MaxWriteBatch at construction; WriteBatch chunks // larger inputs. writeMsgs []rawMessage writeIovs []iovec writeNames [][]byte // Per-entry cmsg scratch. writeCmsg is one contiguous slab of // MaxWriteBatch * writeCmsgSpace bytes; each entry holds two cmsg // headers (UDP_SEGMENT then IP_TOS / IPV6_TCLASS) pre-filled once in // prepareWriteMessages. WriteBatch only rewrites the per-call data // payloads and toggles Hdr.Control / Hdr.Controllen to point at // whichever subset of the two cmsgs applies. writeCmsg []byte writeCmsgSpace int writeCmsgSegSpace int writeCmsgEcnSpace int // writeEntryEnd[e] is the bufs index *after* the last packet packed // into mmsghdr entry e. Used to rewind `i` on partial sendmmsg success. writeEntryEnd []int // UDP GSO (sendmsg with UDP_SEGMENT cmsg) support. gsoSupported is // probed once at socket creation. When true, WriteBatch packs same- // destination consecutive packets into a single sendmmsg entry with a // UDP_SEGMENT cmsg; otherwise each packet is its own entry. gsoSupported bool maxGSOSegments int // UDP GRO (recvmsg with UDP_GRO cmsg) support. groSupported is probed // once at socket creation. When true, listenOutBatch allocates larger // RX buffers and a per-entry cmsg slot so the kernel can coalesce // consecutive same-flow datagrams into a single recvmmsg entry; the // delivered cmsg carries the gso_size used to split them back apart. groSupported bool // ecnRecvSupported is true when IP_RECVTOS / IPV6_RECVTCLASS was // successfully enabled — the kernel will deliver the outer IP-ECN of // each arriving datagram as a per-slot cmsg, and listenOutBatch passes // the parsed value to the EncReader callback for RFC 6040 combine. ecnRecvSupported bool } func NewListener(l *slog.Logger, ip netip.Addr, port int, multi bool, batch int) (Conn, error) { af := unix.AF_INET6 if ip.Is4() { af = unix.AF_INET } syscall.ForkLock.RLock() fd, err := unix.Socket(af, unix.SOCK_DGRAM, unix.IPPROTO_UDP) if err == nil { unix.CloseOnExec(fd) } syscall.ForkLock.RUnlock() if err != nil { return nil, fmt.Errorf("unable to open socket: %w", err) } if multi { if err = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil { _ = unix.Close(fd) return nil, fmt.Errorf("unable to set SO_REUSEPORT: %w", err) } } var sa unix.Sockaddr if ip.Is4() { sa4 := &unix.SockaddrInet4{Port: port} sa4.Addr = ip.As4() sa = sa4 } else { sa6 := &unix.SockaddrInet6{Port: port} sa6.Addr = ip.As16() sa = sa6 } if err = unix.Bind(fd, sa); err != nil { _ = unix.Close(fd) return nil, fmt.Errorf("unable to bind to socket: %w", err) } out := &StdConn{sysFd: fd, isV4: ip.Is4(), l: l, batch: batch} out.prepareWriteMessages(MaxWriteBatch) out.prepareGSO() // GRO delivers coalesced superpackets that need a cmsg to split back // into segments. The single-packet RX path uses ReadFromUDPAddrPort // and cannot see that cmsg, so only enable GRO for the batch path. if batch > 1 { out.prepareGRO() } // Best-effort: ask the kernel to deliver outer IP-ECN as ancillary data // on every recvmmsg slot so the decap side can apply RFC 6040 combine. // On older kernels these may not exist; failing here just means we get // 0 (Not-ECT) on every slot, which is the same as ecn_mode=disable. out.prepareECNRecv() return out, nil } // prepareWriteMessages allocates one mmsghdr/iovec/sockaddr/cmsg scratch // slot per sendmmsg entry. The iovec slab is sized to n so all entries' // iovecs share one allocation; per-entry fan-out is further capped at // maxGSOSegments. Hdr.Iov / Hdr.Iovlen / Hdr.Control / Hdr.Controllen are // wired per call since each entry can span a variable number of iovecs // and may or may not carry a cmsg. // // Per-mmsghdr cmsg layout. Each entry's slot of length writeCmsgSpace holds // up to two cmsg headers placed at fixed offsets: // // [0 .. writeCmsgSegSpace) UDP_SEGMENT (gso_size, uint16) // [writeCmsgSegSpace .. writeCmsgSpace) IP_TOS or IPV6_TCLASS (int32) // // Both headers are pre-filled once here; per-call we only rewrite the data // payload and toggle Hdr.Control / Hdr.Controllen to point at whichever // subset applies (none / segment-only / ecn-only / both). func (u *StdConn) prepareWriteMessages(n int) { u.writeMsgs = make([]rawMessage, n) u.writeIovs = make([]iovec, n) u.writeNames = make([][]byte, n) u.writeEntryEnd = make([]int, n) u.writeCmsgSegSpace = unix.CmsgSpace(2) u.writeCmsgEcnSpace = unix.CmsgSpace(4) u.writeCmsgSpace = u.writeCmsgSegSpace + u.writeCmsgEcnSpace u.writeCmsg = make([]byte, n*u.writeCmsgSpace) // Default the ECN header to the socket's own family. writeEntryCmsg // finalizes Level/Type per entry from the destination address (a v4-mapped // dst on a dual-stack v6 socket needs IP_TOS, not IPV6_TCLASS), so this is // only the value used before the first per-entry rewrite. ecnLevel := int32(unix.IPPROTO_IP) ecnType := int32(unix.IP_TOS) if !u.isV4 { ecnLevel = unix.IPPROTO_IPV6 ecnType = unix.IPV6_TCLASS } for k := 0; k < n; k++ { base := k * u.writeCmsgSpace seg := (*unix.Cmsghdr)(unsafe.Pointer(&u.writeCmsg[base])) seg.Level = unix.SOL_UDP seg.Type = unix.UDP_SEGMENT setCmsgLen(seg, unix.CmsgLen(2)) ecn := (*unix.Cmsghdr)(unsafe.Pointer(&u.writeCmsg[base+u.writeCmsgSegSpace])) ecn.Level = ecnLevel ecn.Type = ecnType setCmsgLen(ecn, unix.CmsgLen(4)) } for i := range u.writeMsgs { u.writeNames[i] = make([]byte, unix.SizeofSockaddrInet6) u.writeMsgs[i].Hdr.Name = &u.writeNames[i][0] } } // maxGSOBytes bounds the total payload per sendmsg() when UDP_SEGMENT is // set. The kernel stitches all iovecs into a single skb whose length the // UDP length field can represent, and also enforces sk_gso_max_size (which // on most devices is 65536). We use 65000 to leave headroom under the // 65535 UDP-length cap, avoiding EMSGSIZE on large TSO superpackets. const maxGSOBytes = 65000 // prepareGSO probes UDP_SEGMENT support and sets u.gsoSupported on success. // Best-effort; failure leaves it false. func (u *StdConn) prepareGSO() { u.maxGSOSegments = 63 //gotta be one less than the max so we can still attach a header if err := unix.SetsockoptInt(u.sysFd, unix.IPPROTO_UDP, unix.UDP_SEGMENT, 0); err != nil { u.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 { u.l.Info("udp: GSO disabled", "reason", "kernel uname probe failed", "error", err) recordCapability("udp.gso.enabled", false) return } u.maxGSOSegments = gsoMaxSegments(string(un.Release[:])) u.gsoSupported = true u.l.Info("udp: GSO enabled", "maxGSOSegments", u.maxGSOSegments) recordCapability("udp.gso.enabled", true) } // gsoMaxSegments returns the largest number of UDP_SEGMENT segments a single // sendmsg may carry on the running kernel, reserving one segment for the // header. UDP_MAX_SEGMENTS was 64 until Linux v6.9 (commit 1382e3b6a350, // "udp: change maximum number of UDP segments to 128") raised it to 128; // nothing about this changed in 5.5. On kernels older than 6.9 packing more // than 64 segments gets the sendmsg rejected with EINVAL, so cap at 63 there // and only use 127 from 6.9 on. (Maintainer stance: update your kernel if you // want to go fast — this is a plain version gate, not a runtime probe.) func gsoMaxSegments(release string) int { major, minor := parseRelease(release) if major > 6 || (major == 6 && minor >= 9) { return 127 } return 63 } // udpGROBufferSize sizes the per-entry recvmmsg buffer when UDP_GRO is on. // The kernel stitches a run of same-flow datagrams into a single skb whose // length is bounded by sk_gso_max_size (typically 65535); anything larger // would be MSG_TRUNCed. We use the maximum representable UDP length so a // full superpacket always lands intact. const udpGROBufferSize = 65535 // udpGROCmsgPayload is the size of the UDP_GRO cmsg data delivered by the // kernel: a single int (gso_size in bytes). See udp_cmsg_recv() in // net/ipv4/udp.c. const udpGROCmsgPayload = 4 // prepareGRO turns on UDP_GRO so the kernel coalesces consecutive same-flow // datagrams into one recvmmsg entry, with a cmsg carrying the gso_size used // to split them back apart on the application side. func (u *StdConn) prepareGRO() { err := unix.SetsockoptInt(u.sysFd, unix.IPPROTO_UDP, unix.UDP_GRO, 1) if err != nil { u.l.Info("udp: GRO disabled", "reason", "kernel rejected probe", "error", err) recordCapability("udp.gro.enabled", false) return } u.groSupported = true u.l.Info("udp: GRO enabled") recordCapability("udp.gro.enabled", true) } // prepareECNRecv turns on IP_RECVTOS / IPV6_RECVTCLASS so the outer IP-ECN // field of each arriving datagram is delivered as ancillary data alongside // the payload. listenOutBatch reads it via parseRecvCmsg and passes the // codepoint through the EncReader for RFC 6040 combine on the decap side. // Best-effort: we keep going on failure. func (u *StdConn) prepareECNRecv() { var v4err, v6err error v4err = unix.SetsockoptInt(u.sysFd, unix.IPPROTO_IP, unix.IP_RECVTOS, 1) if !u.isV4 { v6err = unix.SetsockoptInt(u.sysFd, unix.IPPROTO_IPV6, unix.IPV6_RECVTCLASS, 1) } if u.isV4 { //only check the V4 attempt if v4err != nil { u.l.Info("udp: outer-ECN RX disabled", "reason", "kernel rejected probe", "error", v4err) recordCapability("udp.ecn_rx.enabled", false) } else { u.ecnRecvSupported = true u.l.Info("udp: outer-ECN RX enabled") recordCapability("udp.ecn_rx.enabled", true) } return } else { if v6err != nil { //no V6 ECN? disable it. u.l.Info("udp: outer-ECN RX disabled", "reason", "kernel rejected probe", "error", errors.Join(v4err, v6err)) recordCapability("udp.ecn_rx.enabled", false) return } else if v4err != nil { //no V4, but yes V6? Low level warning. Could be a V6-specific bind. u.l.Debug("udp: outer-ECN RX degraded", "reason", "kernel rejected probe on IPv4", "error", v4err) } // all good u.ecnRecvSupported = true u.l.Info("udp: outer-ECN RX enabled") recordCapability("udp.ecn_rx.enabled", true) return } } // recordCapability registers (or updates) a boolean gauge for one of the // kernel-feature probes. Gauges go to 1 when the feature is enabled, 0 when // it is not — dashboards can show degraded state on partially-supported // kernels at a glance. Calling repeatedly with the same name updates the // existing gauge rather than registering a duplicate. func recordCapability(name string, enabled bool) { g := metrics.GetOrRegisterGauge(name, nil) if enabled { g.Update(1) } else { g.Update(0) } } func (u *StdConn) SupportsMultipleReaders() bool { return true } func (u *StdConn) Rebind() error { return nil } func (u *StdConn) SetRecvBuffer(n int) error { return unix.SetsockoptInt(u.sysFd, unix.SOL_SOCKET, unix.SO_RCVBUFFORCE, n) } func (u *StdConn) SetSendBuffer(n int) error { return unix.SetsockoptInt(u.sysFd, unix.SOL_SOCKET, unix.SO_SNDBUFFORCE, n) } func (u *StdConn) SetSoMark(mark int) error { return unix.SetsockoptInt(u.sysFd, unix.SOL_SOCKET, unix.SO_MARK, mark) } func (u *StdConn) GetRecvBuffer() (int, error) { return unix.GetsockoptInt(u.sysFd, unix.SOL_SOCKET, unix.SO_RCVBUF) } func (u *StdConn) GetSendBuffer() (int, error) { return unix.GetsockoptInt(u.sysFd, unix.SOL_SOCKET, unix.SO_SNDBUF) } func (u *StdConn) GetSoMark() (int, error) { return unix.GetsockoptInt(u.sysFd, unix.SOL_SOCKET, unix.SO_MARK) } func (u *StdConn) LocalAddr() (netip.AddrPort, error) { sa, err := unix.Getsockname(u.sysFd) if err != nil { return netip.AddrPort{}, err } switch sa := sa.(type) { case *unix.SockaddrInet4: return netip.AddrPortFrom(netip.AddrFrom4(sa.Addr), uint16(sa.Port)), nil case *unix.SockaddrInet6: return netip.AddrPortFrom(netip.AddrFrom16(sa.Addr), uint16(sa.Port)), nil default: return netip.AddrPort{}, fmt.Errorf("unsupported sock type: %T", sa) } } // recvmmsg does one blocking recvmmsg (MSG_WAITFORONE), reading up to len(msgs) datagrams func (u *StdConn) recvmmsg(msgs []rawMessage) (int, error) { r, _, errno := unix.Syscall6( unix.SYS_RECVMMSG, uintptr(u.sysFd), uintptr(unsafe.Pointer(&msgs[0])), uintptr(len(msgs)), unix.MSG_WAITFORONE, 0, 0, ) if errno != 0 { if u.closed.Load() { return 0, net.ErrClosed } return 0, &net.OpError{Op: "recvmmsg", Err: errno} } n := int(r) if (n == 0 || msgs[0].Len == 0) && u.closed.Load() { return 0, net.ErrClosed } return n, nil } // recvmsg does one blocking recvmsg into msgs[0] func (u *StdConn) recvmsg(msgs []rawMessage) (int, error) { r, _, errno := unix.Syscall6( unix.SYS_RECVMSG, uintptr(u.sysFd), uintptr(unsafe.Pointer(&msgs[0].Hdr)), 0, 0, 0, 0, ) if errno != 0 { if u.closed.Load() { return 0, net.ErrClosed } return 0, &net.OpError{Op: "recvmsg", Err: errno} } if r == 0 && u.closed.Load() { return 0, net.ErrClosed } msgs[0].Len = uint32(r) return 1, nil } func getFrom(names [][]byte, i int, isV4 bool) netip.AddrPort { var ip netip.Addr // Its ok to skip the ok check here, the slicing is the only error that can occur and it will panic if isV4 { ip, _ = netip.AddrFromSlice(names[i][4:8]) } else { ip, _ = netip.AddrFromSlice(names[i][8:24]) } return netip.AddrPortFrom(ip.Unmap(), binary.BigEndian.Uint16(names[i][2:4])) } func (u *StdConn) ListenOut(r EncReader, flush func()) error { bufSize := MTU cmsgSpace := 0 if u.groSupported { bufSize = udpGROBufferSize cmsgSpace = unix.CmsgSpace(udpGROCmsgPayload) } if u.ecnRecvSupported { // IP_TOS arrives as 1 byte; IPV6_TCLASS arrives as a 4-byte int. // Reserve enough for the wider of the two so the same buffer fits // either family alongside any UDP_GRO cmsg. cmsgSpace += unix.CmsgSpace(4) } msgs, buffers, names, _ := u.PrepareRawMessages(u.batch, bufSize, cmsgSpace) read := u.recvmmsg if u.batch == 1 { read = u.recvmsg } for { if cmsgSpace > 0 { for i := range msgs { setMsgControllen(&msgs[i].Hdr, cmsgSpace) } } n, err := read(msgs) if err != nil { if errors.Is(err, unix.EINTR) { continue // interrupted by a signal, retry the read } // net.ErrClosed after Close() is teardown, absorbed by the caller's // closed flag like the other platforms; anything else is a real error. return err } for i := 0; i < n; i++ { from := getFrom(names, i, u.isV4) payload := buffers[i][:msgs[i].Len] segSize := 0 outerECN := byte(0) if cmsgSpace > 0 { segSize, outerECN = parseRecvCmsg(&msgs[i].Hdr, u.groSupported, u.ecnRecvSupported) } if segSize <= 0 || segSize >= len(payload) { r(from, payload, RxMeta{OuterECN: outerECN}) } else { for off := 0; off < len(payload); off += segSize { end := off + segSize if end > len(payload) { end = len(payload) } seg := payload[off:end] r(from, seg, RxMeta{OuterECN: outerECN}) } } } flush() } } // parseRecvCmsg walks the per-slot ancillary buffer once and extracts up to // two values of interest in a single pass: the UDP_GRO gso_size (when // wantGRO is true) and the outer IP-level ECN codepoint stamped on the // carrier (when wantECN is true). Returns zeros for whichever field is not // requested or not present. // // The outer ECN is accepted from EITHER an IP_TOS (IPPROTO_IP, 1-byte) or an // IPV6_TCLASS (IPPROTO_IPV6, 4-byte int) cmsg, regardless of the socket's // family: a dual-stack v6 socket (isV4 == false) delivers IPv4 peers' outer // ECN as an IP_TOS cmsg — gating on socket family here dropped v4-underlay // ECN entirely. Whichever cmsg the kernel delivered carries the value. func parseRecvCmsg(hdr *msghdr, wantGRO, wantECN bool) (gso int, ecn byte) { controllen := int(hdr.Controllen) if controllen < unix.SizeofCmsghdr || hdr.Control == nil { return 0, 0 } ctrl := unsafe.Slice(hdr.Control, controllen) off := 0 for off+unix.SizeofCmsghdr <= len(ctrl) { ch := (*unix.Cmsghdr)(unsafe.Pointer(&ctrl[off])) clen := int(ch.Len) if clen < unix.SizeofCmsghdr || off+clen > len(ctrl) { return gso, ecn } dataOff := off + unix.CmsgLen(0) switch { case wantGRO && ch.Level == unix.SOL_UDP && ch.Type == unix.UDP_GRO: if dataOff+udpGROCmsgPayload <= len(ctrl) { gso = int(int32(binary.NativeEndian.Uint32(ctrl[dataOff : dataOff+udpGROCmsgPayload]))) } case wantECN && ch.Level == unix.IPPROTO_IP && ch.Type == unix.IP_TOS: // IP_TOS arrives as a single byte; only the low 2 bits are ECN. // A dual-stack v6 socket carries v4 peers' outer ECN here. if dataOff+1 <= len(ctrl) { ecn = ctrl[dataOff] & 0x03 } case wantECN && ch.Level == unix.IPPROTO_IPV6 && ch.Type == unix.IPV6_TCLASS: // IPV6_TCLASS arrives as a 4-byte int; ECN is the low 2 bits. if dataOff+4 <= len(ctrl) { ecn = byte(binary.NativeEndian.Uint32(ctrl[dataOff:dataOff+4])) & 0x03 } } // Advance by the aligned cmsg space. off += unix.CmsgSpace(clen - unix.CmsgLen(0)) } return gso, ecn } func (u *StdConn) WriteTo(b []byte, ip netip.AddrPort) error { if u.isV4 { return u.writeTo4(b, ip) } return u.writeTo6(b, ip) } func (u *StdConn) writeTo6(b []byte, ip netip.AddrPort) error { var rsa unix.RawSockaddrInet6 rsa.Family = unix.AF_INET6 rsa.Addr = ip.Addr().As16() binary.BigEndian.PutUint16((*[2]byte)(unsafe.Pointer(&rsa.Port))[:], ip.Port()) for { _, _, err := unix.Syscall6( unix.SYS_SENDTO, uintptr(u.sysFd), uintptr(unsafe.Pointer(&b[0])), uintptr(len(b)), uintptr(0), uintptr(unsafe.Pointer(&rsa)), uintptr(unix.SizeofSockaddrInet6), ) if err != 0 { return &net.OpError{Op: "sendto", Err: err} } return nil } } func (u *StdConn) writeTo4(b []byte, ip netip.AddrPort) error { if !ip.Addr().Is4() { return ErrInvalidIPv6RemoteForSocket } var rsa unix.RawSockaddrInet4 rsa.Family = unix.AF_INET rsa.Addr = ip.Addr().As4() binary.BigEndian.PutUint16((*[2]byte)(unsafe.Pointer(&rsa.Port))[:], ip.Port()) for { _, _, err := unix.Syscall6( unix.SYS_SENDTO, uintptr(u.sysFd), uintptr(unsafe.Pointer(&b[0])), uintptr(len(b)), uintptr(0), uintptr(unsafe.Pointer(&rsa)), uintptr(unix.SizeofSockaddrInet4), ) if err != 0 { return &net.OpError{Op: "sendto", Err: err} } return nil } } // WriteBatch sends bufs via sendmmsg(2) using the preallocated scratch on // StdConn. Consecutive packets to the same destination with matching segment // sizes (all but possibly the last) are coalesced into a single mmsghdr entry // carrying a UDP_SEGMENT cmsg, so one syscall can mix runs of GSO superpackets // with plain one-off datagrams. Without GSO support every packet is its own // entry, matching the prior behaviour. // // Chunks larger than the scratch are processed across multiple syscalls. If // sendmmsg returns an error AND zero entries went out we fall back to // per-packet WriteTo for that chunk so the caller still gets best-effort // delivery; on a partial-success error we just replay the remainder. func (u *StdConn) WriteBatch(bufs [][]byte, addrs []netip.AddrPort, ecns []byte) error { if len(bufs) != len(addrs) { return fmt.Errorf("WriteBatch: len(bufs)=%d != len(addrs)=%d", len(bufs), len(addrs)) } if ecns != nil && len(ecns) != len(bufs) { return fmt.Errorf("WriteBatch: len(ecns)=%d != len(bufs)=%d", len(ecns), len(bufs)) } // Callers deliver same-destination packets contiguously and in counter // order, so we run the GSO planner directly without a pre-sort. A // sorting pass measurably hurt throughput in microbenchmarks while // providing no observed reordering benefit. i := 0 sendChunks: for i < len(bufs) { baseI := i entry := 0 iovIdx := 0 for entry < len(u.writeMsgs) && i < len(bufs) { iovBudget := len(u.writeIovs) - iovIdx if iovBudget < 1 { break } runLen, segSize := u.planRun(bufs, addrs, ecns, i, iovBudget) if runLen == 0 { break } for k := 0; k < runLen; k++ { b := bufs[i+k] if len(b) == 0 { u.writeIovs[iovIdx+k].Base = nil setIovLen(&u.writeIovs[iovIdx+k], 0) } else { u.writeIovs[iovIdx+k].Base = &b[0] setIovLen(&u.writeIovs[iovIdx+k], len(b)) } } nlen, err := writeSockaddr(u.writeNames[entry], addrs[i], u.isV4) if err != nil { // One destination in this chunk has an address family the // socket can't send to (e.g. an IPv6 remote on a v4-bound // socket → ErrInvalidIPv6RemoteForSocket). Abandoning the whole // sendmmsg here would drop every packet already packed for this // chunk plus every packet still ahead of us in bufs. Instead // fall back to per-packet WriteTo for the packets packed so far // in this chunk and the offending one: WriteTo delivers each // good destination and only errors on the bad one, which we // drop and keep going. One bad destination costs one packet, // never the batch. (Same fallback the zero-sent sendmmsg path // below uses, extended to cover the misaddressed packet.) for k := baseI; k <= i; k++ { if werr := u.WriteTo(bufs[k], addrs[k]); werr != nil && k != i { return werr } } i++ continue sendChunks } hdr := &u.writeMsgs[entry].Hdr hdr.Iov = &u.writeIovs[iovIdx] setMsgIovlen(hdr, runLen) hdr.Namelen = uint32(nlen) var ecn byte if ecns != nil { ecn = ecns[i] } // ECN cmsg family follows the destination, not the socket: a // v4-mapped dst on a dual-stack v6 socket must be stamped via // IP_TOS. addrs[i] is this run's destination (i advances below). dstIsV4 := addrs[i].Addr().Unmap().Is4() u.writeEntryCmsg(entry, runLen, segSize, ecn, dstIsV4) i += runLen iovIdx += runLen u.writeEntryEnd[entry] = i entry++ } if entry == 0 { return fmt.Errorf("sendmmsg: no progress") } sent, serr := u.sendmmsg(entry) if serr != nil && sent <= 0 { // Nothing went out for this chunk; fall back to WriteTo for each // packet that was queued this iteration. We only enter this path // when sendmmsg returned an error AND zero entries succeeded — // otherwise the partial-success advance below replays only the // remainder, avoiding duplicates of already-sent packets. // // sent=-1 from sendmmsg means message 0 itself failed (partial // success returns the count instead), so log entry 0's parameters // — that's the entry the kernel rejected. hdr0 := &u.writeMsgs[0].Hdr runLen0 := u.writeEntryEnd[0] - baseI seg0 := len(bufs[baseI]) ecn0 := byte(0) if ecns != nil { ecn0 = ecns[baseI] } u.l.Warn("sendmmsg had problem", "sent", sent, "err", serr, "entries", entry, "entry0_runLen", runLen0, "entry0_segSize", seg0, "entry0_iovlen", hdr0.Iovlen, "entry0_controllen", hdr0.Controllen, "entry0_namelen", hdr0.Namelen, "entry0_ecn", ecn0, "entry0_dst", addrs[baseI], "isV4", u.isV4, "gso", u.gsoSupported, "gro", u.groSupported, ) for k := baseI; k < i; k++ { if werr := u.WriteTo(bufs[k], addrs[k]); werr != nil { return werr } } continue } if sent == 0 { return fmt.Errorf("sendmmsg made no progress") } // Rewind i to the end of the last successfully sent entry. For a // full-success send this leaves i unchanged; for a partial send it // replays the remainder on the next outer-loop iteration. i = u.writeEntryEnd[sent-1] } return nil } // planRun groups consecutive packets starting at `start` that can be sent as // a single UDP GSO superpacket (one sendmmsg entry with UDP_SEGMENT cmsg). // A run of length 1 means the entry carries no UDP_SEGMENT cmsg and the // kernel treats it as a plain datagram. Returns the run length and the // per-segment size (which equals len(bufs[start])). Without GSO support // every call returns runLen=1. Outer ECN (when ecns != nil) is also a run // boundary — the kernel stamps one outer codepoint per sendmsg entry, so // mixing values inside a run would lose information. func (u *StdConn) planRun(bufs [][]byte, addrs []netip.AddrPort, ecns []byte, start, iovBudget int) (int, int) { if start >= len(bufs) || iovBudget < 1 { return 0, 0 } segSize := len(bufs[start]) if !u.gsoSupported || segSize == 0 || segSize > maxGSOBytes { return 1, segSize } dst := addrs[start] var ecn byte if ecns != nil { ecn = ecns[start] } maxLen := u.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 ecns != nil && ecns[start+runLen] != ecn { 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 sets up the per-mmsghdr Hdr.Control / Hdr.Controllen for one // entry. It writes the UDP_SEGMENT payload when runLen >= 2 and the // IP_TOS/IPV6_TCLASS payload when ecn != 0, then points hdr.Control at the // smallest contiguous span that covers whichever cmsg(s) actually apply. // // The outer-ECN cmsg family must match the *destination*, not the socket: on // the default dual-stack v6 bind, a v4-mapped destination is routed through // the kernel's IPv4 path, which parses IP_TOS (IPPROTO_IP) and ignores an // IPV6_TCLASS cmsg. prepareWriteMessages pre-fills a default header; here we // rewrite its Level/Type (and Len) per entry from dstIsV4 so v4 peers get // IP_TOS and v6 peers get IPV6_TCLASS. The data payload is a 4-byte int for // both families, so the pre-computed cmsg space is unchanged. func (u *StdConn) writeEntryCmsg(entry, runLen, segSize int, ecn byte, dstIsV4 bool) { hdr := &u.writeMsgs[entry].Hdr useSeg := runLen >= 2 useEcn := ecn != 0 base := entry * u.writeCmsgSpace if useSeg { dataOff := base + unix.CmsgLen(0) binary.NativeEndian.PutUint16(u.writeCmsg[dataOff:dataOff+2], uint16(segSize)) } if useEcn { ecnHdr := (*unix.Cmsghdr)(unsafe.Pointer(&u.writeCmsg[base+u.writeCmsgSegSpace])) if dstIsV4 { ecnHdr.Level = int32(unix.IPPROTO_IP) ecnHdr.Type = int32(unix.IP_TOS) } else { ecnHdr.Level = int32(unix.IPPROTO_IPV6) ecnHdr.Type = int32(unix.IPV6_TCLASS) } setCmsgLen(ecnHdr, unix.CmsgLen(4)) dataOff := base + u.writeCmsgSegSpace + unix.CmsgLen(0) binary.NativeEndian.PutUint32(u.writeCmsg[dataOff:dataOff+4], uint32(ecn)) } switch { case useSeg && useEcn: hdr.Control = &u.writeCmsg[base] setMsgControllen(hdr, u.writeCmsgSpace) case useSeg: hdr.Control = &u.writeCmsg[base] setMsgControllen(hdr, u.writeCmsgSegSpace) case useEcn: hdr.Control = &u.writeCmsg[base+u.writeCmsgSegSpace] setMsgControllen(hdr, u.writeCmsgEcnSpace) default: hdr.Control = nil setMsgControllen(hdr, 0) } } // sendmmsg issues sendmmsg(2) over u.rawConn against the first n entries // of u.writeMsgs. func (u *StdConn) sendmmsg(n int) (int, error) { r1, _, errno := unix.Syscall6(unix.SYS_SENDMMSG, uintptr(u.sysFd), uintptr(unsafe.Pointer(&u.writeMsgs[0])), uintptr(n), 0, 0, 0, ) sent := int(r1) if errno != 0 { return sent, &net.OpError{Op: "sendmmsg", Err: errno} } return sent, nil } // writeSockaddr encodes addr into buf (which must be at least // SizeofSockaddrInet6 bytes). Returns the number of bytes used. If isV4 is // true and addr is not a v4 (or v4-in-v6) address, returns an error. func writeSockaddr(buf []byte, addr netip.AddrPort, isV4 bool) (int, error) { ap := addr.Addr().Unmap() if isV4 { if !ap.Is4() { return 0, ErrInvalidIPv6RemoteForSocket } // struct sockaddr_in: { sa_family_t(2), in_port_t(2, BE), in_addr(4), zero(8) } // sa_family is host endian. binary.NativeEndian.PutUint16(buf[0:2], unix.AF_INET) binary.BigEndian.PutUint16(buf[2:4], addr.Port()) ip4 := ap.As4() copy(buf[4:8], ip4[:]) for j := 8; j < 16; j++ { buf[j] = 0 } return unix.SizeofSockaddrInet4, nil } // struct sockaddr_in6: { sa_family_t(2), in_port_t(2, BE), flowinfo(4), in6_addr(16), scope_id(4) } binary.NativeEndian.PutUint16(buf[0:2], unix.AF_INET6) binary.BigEndian.PutUint16(buf[2:4], addr.Port()) binary.NativeEndian.PutUint32(buf[4:8], 0) ip6 := addr.Addr().As16() copy(buf[8:24], ip6[:]) binary.NativeEndian.PutUint32(buf[24:28], 0) return unix.SizeofSockaddrInet6, nil } func (u *StdConn) ReloadConfig(c *config.C) { b := c.GetInt("listen.read_buffer", 0) if b > 0 { if err := u.SetRecvBuffer(b); err == nil { if s, err := u.GetRecvBuffer(); err == nil { u.l.Info("listen.read_buffer was set", "size", s) } else { u.l.Warn("Failed to get listen.read_buffer", "error", err) } } else { u.l.Error("Failed to set listen.read_buffer", "error", err) } } b = c.GetInt("listen.write_buffer", 0) if b > 0 { if err := u.SetSendBuffer(b); err == nil { if s, err := u.GetSendBuffer(); err == nil { u.l.Info("listen.write_buffer was set", "size", s) } else { u.l.Warn("Failed to get listen.write_buffer", "error", err) } } else { u.l.Error("Failed to set listen.write_buffer", "error", err) } } b = c.GetInt("listen.so_mark", 0) s, err := u.GetSoMark() if b > 0 || (err == nil && s != 0) { if err := u.SetSoMark(b); err == nil { if s, err := u.GetSoMark(); err == nil { u.l.Info("listen.so_mark was set", "mark", s) } else { u.l.Warn("Failed to get listen.so_mark", "error", err) } } else { u.l.Error("Failed to set listen.so_mark", "error", err) } } } func (u *StdConn) getMemInfo(meminfo *[unix.SK_MEMINFO_VARS]uint32) error { var vallen uint32 = 4 * unix.SK_MEMINFO_VARS _, _, err := unix.Syscall6(unix.SYS_GETSOCKOPT, uintptr(u.sysFd), uintptr(unix.SOL_SOCKET), uintptr(unix.SO_MEMINFO), uintptr(unsafe.Pointer(meminfo)), uintptr(unsafe.Pointer(&vallen)), 0) if err != 0 { return err } return nil } func (u *StdConn) Close() error { u.closed.Store(true) // Wake the reader parked in recvmmsg/recvmsg. shutdown(2) on an unconnected socket // returns ENOTCONN but still wakes it, so ignore the error. // The reader then sees closed and stops touching the fd, making the Close below safe. _ = unix.Shutdown(u.sysFd, unix.SHUT_RDWR) return unix.Close(u.sysFd) } func NewUDPStatsEmitter(udpConns []Conn) func() { // Check if our kernel supports SO_MEMINFO before registering the gauges var udpGauges [][unix.SK_MEMINFO_VARS]metrics.Gauge var meminfo [unix.SK_MEMINFO_VARS]uint32 if err := udpConns[0].(*StdConn).getMemInfo(&meminfo); err == nil { udpGauges = make([][unix.SK_MEMINFO_VARS]metrics.Gauge, len(udpConns)) for i := range udpConns { udpGauges[i] = [unix.SK_MEMINFO_VARS]metrics.Gauge{ metrics.GetOrRegisterGauge(fmt.Sprintf("udp.%d.rmem_alloc", i), nil), metrics.GetOrRegisterGauge(fmt.Sprintf("udp.%d.rcvbuf", i), nil), metrics.GetOrRegisterGauge(fmt.Sprintf("udp.%d.wmem_alloc", i), nil), metrics.GetOrRegisterGauge(fmt.Sprintf("udp.%d.sndbuf", i), nil), metrics.GetOrRegisterGauge(fmt.Sprintf("udp.%d.fwd_alloc", i), nil), metrics.GetOrRegisterGauge(fmt.Sprintf("udp.%d.wmem_queued", i), nil), metrics.GetOrRegisterGauge(fmt.Sprintf("udp.%d.optmem", i), nil), metrics.GetOrRegisterGauge(fmt.Sprintf("udp.%d.backlog", i), nil), metrics.GetOrRegisterGauge(fmt.Sprintf("udp.%d.drops", i), nil), } } } return func() { for i, gauges := range udpGauges { if err := udpConns[i].(*StdConn).getMemInfo(&meminfo); err == nil { for j := 0; j < unix.SK_MEMINFO_VARS; j++ { gauges[j].Update(int64(meminfo[j])) } } } } } 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 }