batchudp is a small UDP transport module extracted from wireguard-go. Its
main job is to expose a single Bind abstraction that:
- opens IPv4 and IPv6 UDP sockets on the same port,
- returns receive functions for each active address family,
- sends one or more datagrams to a parsed
Endpoint, - hides platform-specific socket setup, batching, and ancillary data details.
It also exposes an optional BatchingConn upgrade path for callers that start
from a single gonnect.PacketConn / gonnect.UDPConn instead of the
WireGuard-style Bind lifecycle.
The public contracts live in conn.go.
The concrete implementations are:
StdNetBindinbind_std.go: the gonnect-backed implementation used on every non-Windows platform and also the Windows fallback when RIO is unavailable or the supplied network is not suitable for RIO.WinRingBindinbind_windows.go: the Windows-specific fast path using Registered I/O.
The batching-conn upgrade is implemented separately:
batching_conn_linux.go: Linux-only wrapper that upgrades a native-backedgonnect.UDPConnwithReadBatchandWriteBatchTo.batching_conn_default.go: no-op upgrade path for non-Linux builds.
NewDefaultBind selects the implementation:
default.go: non-Windows usesNewStdNetBind(network).bind_windows.go: Windows usesNewWinRingBind(network)only when the supplied network is native and also exposesSubscribeCloser(io.Closer); otherwise it usesStdNetBind(network).NewDefaultBindWithOptionsappliesStdNetBindOptions; on Windows it keeps the RIO path only for the zero-option case and otherwise usesStdNetBind. Setting a positiveStdNetBindOptions.BatchSizeis therefore also a request for theStdNetBindpath.
TryUpgradeToBatchingConn is the entry point for the socket-upgrade path.
- It accepts a
gonnect.PacketConn, a UDP network string (udp4orudp6), and a desired batch size. A non-positive batch size preserves the native default ofIdealBatchSize; a positive value is used for both reported capacity and pooled send-message allocation. - On Linux, it upgrades only values that also implement
gonnect.UDPConnand unwrap to a native*net.UDPConn. - The upgraded wrapper keeps the original
gonnect.UDPConnfor ordinary UDP methods while constructing anipv4.PacketConnoripv6.PacketConnaround the native socket for batched I/O. - On all other platforms, or for unsupported connection types, the function returns the original connection unchanged.
Bind.Open is the entry point that constructs the runtime receive/send path.
For StdNetBind:
Openinbind_std.golocks the bind and rejects reopening withErrBindAlreadyOpen.- It calls
listenNet, which builds sockets through the suppliedgonnect.Network. listenConfig()incontrolfns.goasks gonnect to apply bind-time socket controls such asIPV6_V6ONLYbeforebind(2). Some networks may ignore the control hook or call it without raw-socket access.- After open,
configureSocket()applies the platform-specific post-open control hooks such as buffer sizing, PKTINFO reception, and optionalUDP_GRO. - By default
Opentries IPv4 first and then IPv6 on the first socket's actual port.StdNetBindOptions.FamilyOrdercan prefer IPv6 first. PositiveStdNetBindOptions.BatchSizevalues override the bind's effective batch size; non-positive values preserveIdealBatchSizefor native Linux/Android binds and1elsewhere. Smaller batches reduce retained per-bind message allocation, while larger batches can improve throughput under load by reducing syscall overhead. - Strict mode preserves the dual-family behavior: non-
EAFNOSUPPORTsibling failures close the first socket and failOpen. WhenAllowSingleFamilyis set,Openkeeps the first family if the sibling fails, callsOnFamilyOpenErrorwhen configured, and returns one receive function. Sending to the unopened family returnssyscall.EAFNOSUPPORT. - For each opened socket,
Openprobes UDP offload support viasupportsUDPOffload, and on Linux/Android wraps the unwrapped underlying*net.UDPConninipv4.PacketConnoripv6.PacketConnonly when native batch I/O is actually available. Openreturns one receive closure per active family:makeReceiveIPv4andmakeReceiveIPv6, both of which callreceiveIP.
For WinRingBind:
Openinbind_windows.gocreates IPv4 and IPv6 RIO sockets throughafWinRingBind.Open.- Each per-family bind allocates RX/TX rings, completion queues, and a RIO request queue.
- The bind preposts
packetsPerRingreceive requests for both families. - If the supplied network exposes
SubscribeCloser,Opensubscribes the bind as an external closer soNetwork.Down()closes it. Openreturns two receive functions,receiveIPv4andreceiveIPv6.
For BatchingConn, the receive path lives in ReadBatch in
batching_conn_linux.go:
- Callers pass a slice of
ipv6.Messagevalues, each with at least one data buffer and control-buffer capacity of at leastMinControlMessageSize(). - Without RX offload,
ReadBatchforwards directly toReadBatchon the wrappedipv4.PacketConn/ipv6.PacketConn. - With UDP GRO enabled, it reads into the tail of the caller's message slice
and then reuses
splitCoalescedMessagesto expand coalesced datagrams back into packet-per-buffer results at the head of the slice. - Single-packet
ReadFromUDPandReadFromUDPAddrPortare intentionally rejected withErrSinglePacketReadUnsupported, because they cannot safely represent GRO-coalesced reads.
For StdNetBind, the receive path lives in receiveIP inbind_std.go:
- Callers must pass
packets,sizes, andepsslices whose lengths are at leastBatchSize(). Shorter slice lists are rejected withErrReadBufferTooShort. StdNetBindallocates pooled message slices with exactlyBatchSize()entries and passes that configured length to nativeReadBatch, even when a caller supplies longer receive slices.- Linux and Android use
ReadBatchthroughipv4.PacketConn/ipv6.PacketConnonly when the opened connection unwraps to a suitable native*net.UDPConn. - Otherwise
gonnect.UDPConn.ReadMsgUDPis used and packets are processed one datagram at a time. - Received control data is parsed by
getSrcFromControl. - The returned
Endpointis aStdNetEndpoint, which holds destination address data plus optional cached source control data. IPv4-mapped IPv6 source addresses are normalized back to plain IPv4 before being exposed.
When Linux/Android RX offload is enabled:
controlfns_linux.goattempts to enableUDP_GROat socket creation time.supportsUDPOffloadinfeatures_linux.gochecks whether the socket actually supportsUDP_GROandUDP_SEGMENT.receiveIPreads into a reduced number of large buffers, thensplitCoalescedMessagesexpands a coalesced GRO datagram back into the packet-per-buffer API expected by callers. The reduced read window is derived from the configured batch size rather than fromIdealBatchSize.getGSOSizefromgso_linux.goextracts the segment size from ancillary data. Non-Linux builds usegso_default.go, where these helpers are no-ops.
For WinRingBind:
receiveIPv4andreceiveIPv6validate that the caller provided at leastBatchSize()entries inpackets,sizes, andeps, returningErrReadBufferTooShortif not.receiveIPv4andreceiveIPv6callafWinRingBind.Receive.Receivedrains the completion queue, re-arms the receive request, copies the payload into the caller-provided buffer, and returns aWinRingEndpoint.- The Windows fast path is intentionally unbatched at the
BindAPI level:BatchSize()is1.
For BatchingConn, WriteBatchTo in batching_conn_linux.go:
- validates that the caller supplied no more than
BatchSize()datagrams, - converts the target
netip.AddrPortinto a poolednet.UDPAddr, - coalesces same-destination datagrams with UDP GSO when TX offload is available, using scatter-gather buffers rather than copying into one large payload,
- falls back to plain
sendmmsg-style batched writes without GSO when offload is unavailable or gets disabled after a kernel error.
For StdNetBind, Send in bind_std.go:
- snapshots the selected family socket and feature flags under
mu, - converts the destination
StdNetEndpointinto a poolednet.UDPAddr, - optionally attaches sticky source control data with
setSrcControl, - sends via
send, using eitherWriteBatchon Linux/Android orgonnect.UDPConn.WriteMsgUDPAddrPortelsewhere.
On Linux/Android TX offload:
coalesceMessagesmerges multiple same-destination datagrams into one larger payload when size and batch rules allow.setGSOSizeingso_linux.goappendsUDP_SEGMENTcontrol data that tells the kernel how to segment the payload back into packets.- If a send fails with a kernel error that indicates broken UDP GSO support,
Senddisables TX offload for that socket, retries without GSO, and returnsErrUDPGSODisabledwrapping the retry result.
For WinRingBind, Send dispatches each buffer individually through
afWinRingBind.Send, which writes the payload and destination into the TX ring
and submits a winrio.SendEx request. The Windows ring now sizes each slot for
full UDP payloads and uses a smaller ring depth to keep the total allocation
bounded.
For StdNetBind, Close closes the IPv4 and IPv6 gonnect UDP sockets, clears
cached packet-conn wrappers, blackhole flags, and offload state.
For WinRingBind, Close first flips isOpen so receive/send paths start
returning net.ErrClosed, unregisters from gonnect lifecycle tracking when
present, wakes any completion-queue waiters, and then tears down RIO queues,
buffers, and sockets.
The repository uses Go build tags and *_os.go naming to isolate behavior:
bind_std.go: sharedStdNetBindimplementation used everywhere.bind_windows.go: Windows RIO implementation.controlfns_linux.go: Linux and Android socket setup before bind.controlfns_unix.go: non-Windows, non-Linux socket setup.controlfns_windows.go: Windows socket buffer sizing for theStdNetBindfallback.sticky_linux.go: Linux-only sticky-socket source address capture and replay.sticky_default.go: no-op sticky behavior for every non-Linux build, including Android.gso_linux.go: Linux ancillary data helpers for UDP GSO/GRO.gso_default.go: no-op GSO/GRO helpers elsewhere.features_linux.go: per-socket UDP offload probing.features_default.go: offload probing stub for non-Linux builds.mark_unix.go: packet marking on Linux, Android, FreeBSD, and OpenBSD.mark_default.go:SetMarkno-op elsewhere.boundif_android.go: Android-only socket fd exposure for integration withwireguard-android.
There are four main hook points:
- Socket creation:
StdNetBind.Open -> listenNet -> gonnect.Network.listenConfig()applies bind-time controls first when the supplied network honors the hook and provides raw-socket access. - Post-open socket configuration:
StdNetBind.Open -> configureSocket -> socketOpenControlFns. This is where buffer sizes, PKTINFO reception, andUDP_GROare configured when raw-socket access exists. - Receive path:
StdNetBind.receiveIPcallsgetSrcFromControland, on Linux/Android,splitCoalescedMessages. - Send path:
StdNetBind.SendcallssetSrcControland, on Linux/Android with TX offload,coalesceMessagesplussetGSOSize.
Windows RIO bypasses gonnect socket creation entirely because it creates sockets
and I/O queues directly in bind_windows.go.
Sticky sockets let a received packet carry enough local addressing information to send the reply from the same local address/interface later.
On Linux:
configureSocketpluscontrolfns_linux.goenablesIP_PKTINFOfor IPv4 andIPV6_RECVPKTINFOfor IPv6 when raw socket access is available.getSrcFromControlinsticky_linux.gocopies the PKTINFO control message intoStdNetEndpoint.srcduring receive.setSrcControlwrites the cached control message back onto outgoing packets during send.StdNetEndpoint.SrcIP,SrcIfidx, andSrcToStringdecode that cached control data for callers.
On all other builds, including Android:
sticky_default.goprovides stub implementations.StdNetEndpointstill exists, but its source metadata accessors return zero values and outgoing sends do not attach source-selection control data.
Android uses the shared StdNetBind implementation, but not the full Linux
sticky-socket feature set.
boundif_android.goaddsPeekLookAtSocketFd4andPeekLookAtSocketFd6toStdNetBind.- These methods expose the live UDP socket file descriptors without
transferring ownership. The fd remains owned by the bind and becomes invalid
after
Close. - The interface is declared as
PeekLookAtSocketFdinconn.goand is intended forwireguard-android.
Typical Android integration pattern:
- create the bind with
NewDefaultBind(network)orNewStdNetBind(network), - call
Open, - type-assert the bind to
PeekLookAtSocketFd, - fetch the IPv4 and/or IPv6 fd,
- hand those fds to Android-specific code that needs to inspect or exempt the sockets,
- continue using the bind normally for receive/send.
- Sticky socket support is intentionally disabled on Android:
controlfns_linux.goskips enablingIP_PKTINFOandIPV6_RECVPKTINFOwhenruntime.GOOS == "android", andsticky_default.gois selected instead ofsticky_linux.go.
The public contract is documented in conn.go.
The implementation behavior behind that contract is:
StdNetBind.OpenandStdNetBind.Closeserialize lifecycle changes withmu.StdNetBind.Sendonly holdsmulong enough to snapshot the current socket and flags, so sends can proceed concurrently with each other and with receives. A concurrentClosemay cause an in-flight send to fail because the underlying socket was closed, but it should not corrupt bind state.- When the supplied network tracks opened connections,
Network.Down()also closes trackedStdNetBindsockets.WinRingBindis only selected for native networks withSubscribeCloserand subscribes itself so the same lifecycle shutdown closes the RIO bind too. WinRingBindusesRWMutexplusisOpento let send/receive operations race safely withClose.ReceiveFuncs are intended to block in dedicated goroutines and to terminate withnet.ErrClosedafterClose.Endpointvalues are implementation-specific.StdNetEndpointcontains mutable cached source state (src), so callers should not mutate a shared endpoint concurrently withSend.
bind_std_test.gofocuses onStdNetBindbatching, GSO/GRO splitting, and close behavior.sticky_linux_test.govalidates Linux sticky control parsing and formatting.conn_test.goexercises the higher-level bind contract.bindtest/bindtest.goprovides a channel-backedBindimplementation used for focused tests.winrio/contains the Windows RIO wrapper used byWinRingBind.