wireguard-go — Interfaces#
Interface catalog#
tun.Device#
- Package:
golang.zx2c4.com/wireguard/tun - File:
tun/tun.go - Methods:
File() *os.File Read(bufs [][]byte, sizes []int, offset int) (n int, err error) Write(bufs [][]byte, offset int) (int, error) MTU() (int, error) Name() (string, error) Events() <-chan Event Close() error BatchSize() int - Purpose: Abstracts the kernel TUN network interface.
ReadandWriteare batch-oriented: they accept slices of byte slices so that a single syscall can transfer multiple IP packets. Theoffsetparameter lets the caller pre-allocate a header region in each buffer (used by the device package to place the WireGuard message header before the payload).Events()returns a channel that signalsEventUp,EventDown, orEventMTUUpdateso the engine can react without polling.BatchSize()advertises the maximum batch size the implementation can handle efficiently, allowing the engine to size its work queues accordingly. - Implementations:
tun.NativeTun— per-platform concrete type (separate files for Linux, Darwin, FreeBSD, OpenBSD, Windows/Wintun)netstack.netTun— in-process userspace network stack using gVisor (tun/netstack/tun.go)tun.ChannelTUN— in-memory channel-based TUN for testing (tun/tuntest/tuntest.go)
- Design quality: Tightly segregated. Every method is load-bearing; nothing is vestigial.
BatchSize()is a deliberate performance contract rather than a hint — callers must honor it. The batch API ([][]byte) is more complex than a single-packetRead([]byte)interface would be, but the complexity is justified by the 1–2× throughput gain from reduced syscall overhead. Follows ISP well: no unrelated concerns are bundled in.
conn.Bind#
- Package:
golang.zx2c4.com/wireguard/conn - File:
conn/conn.go - Methods:Supporting type alias:
Open(port uint16) (fns []ReceiveFunc, actualPort uint16, err error) Close() error SetMark(mark uint32) error Send(bufs [][]byte, ep Endpoint) error ParseEndpoint(s string) (Endpoint, error) BatchSize() inttype ReceiveFunc func(packets [][]byte, sizes []int, eps []Endpoint) (n int, err error) - Purpose: Abstracts the UDP socket layer.
Openreturns a slice ofReceiveFuncvalues — typically one per address family (IPv4 and IPv6) — rather than a single blocking call. EachReceiveFuncis meant to be called from a dedicated goroutine (RoutineReceiveIncoming).Sendis also batch-oriented.SetMarksetsSO_MARKfor policy routing and VPN split-tunneling.ParseEndpointis a factory forEndpointvalues appropriate to this bind type. - Implementations:
conn.StdNetBind— cross-platform implementation usinggolang.org/x/net/ipv4andipv6for batch I/O with GSO/GRO on Linux (conn/bind_std.go)conn.WinRingBind— Windows Registered I/O (RIO) implementation for high-performance I/O completion ports (conn/bind_windows.go)conn.ChannelBind— in-memory channel-based bind for testing (conn/bindtest/bindtest.go)
- Design quality: Excellent. The
ReceiveFuncindirection (returning a slice of functions rather than aReceivemethod) is an uncommon pattern that enables per-address-family goroutines without exposing address-family logic in the interface.BatchSize()mirrors the same contract astun.Device.BatchSize(), enabling the device package to coordinate batch sizes end-to-end. The interface is small (6 methods) relative to its responsibility.
conn.Endpoint#
- Package:
golang.zx2c4.com/wireguard/conn - File:
conn/conn.go - Methods:
ClearSrc() SrcToString() string DstToString() string DstToBytes() []byte DstIP() netip.Addr SrcIP() netip.Addr - Purpose: Encapsulates the source+destination address pairing for a peer’s UDP path. WireGuard caches the local source address (
src) so that replies go out on the same interface the peer’s packet arrived on — critical for multi-homed hosts and containers.ClearSrc()is called when the source cache must be invalidated (e.g. after a route change).DstToBytes()is used specifically for cookie (MAC2) computation per the WireGuard spec. All addressing usesnetip.Addr(the modern, allocation-free address type introduced in Go 1.18). - Implementations:
conn.StdNetEndpoint(Linux/macOS/BSD, embedded inStdNetBind)- Windows endpoint (embedded in
WinRingBind) conn.ChannelEndpoint(test)
- Design quality: Well-segregated. The interface is provider-owned —
Bind.ParseEndpointis the factory, keeping construction and type knowledge co-located in the bind implementation. TheClearSrc+ src caching design is specific to the WireGuard protocol’s source-IP stickiness requirement; a general-purpose endpoint interface would not need it, but this is not a general-purpose interface.
conn.BindSocketToInterface#
- Package:
golang.zx2c4.com/wireguard/conn - File:
conn/conn.go - Methods:
BindSocketToInterface4(interfaceIndex uint32, blackhole bool) error BindSocketToInterface6(interfaceIndex uint32, blackhole bool) error - Purpose: Optional capability extension for Windows. Allows the caller to bind the underlying socket to a specific network interface index.
blackholemode discards packets on that interface rather than sending, used to implement split-tunneling exclusion routes. wireguard-windows performs a runtime type assertion (if b, ok := bind.(BindSocketToInterface); ok) to use this when available. - Implementations:
WinRingBind(Windows only) - Design quality: A classic capability interface — narrow, optional, platform-specific. Correct use of the extension interface pattern: the main
Bindcontract stays clean and cross-platform, while platform-specific capabilities are exposed via separate interfaces discovered via type assertion.
conn.PeekLookAtSocketFd#
- Package:
golang.zx2c4.com/wireguard/conn - File:
conn/conn.go - Methods:
PeekLookAtSocketFd4() (fd int, err error) PeekLookAtSocketFd6() (fd int, err error) - Purpose: Optional capability extension for Android. Android’s VPN service needs to “protect” (exclude from VPN routing) the WireGuard UDP sockets themselves to prevent a routing loop. This interface lets the Android wrapper obtain the raw file descriptor and pass it to
VpnService.protect(). - Implementations:
StdNetBind(whenGOOS=android) - Design quality: Same extension pattern as
BindSocketToInterface. Extremely narrow (2 methods) and single-use. The namePeekLookAtis slightly awkward but the semantics are unambiguous.
conn.batchReader / conn.batchWriter (internal)#
- Package:
golang.zx2c4.com/wireguard/conn(unexported) - File:
conn/bind_std.go - Methods:
type batchReader interface { ReadBatch([]ipv6.Message, int) (int, error) } type batchWriter interface { WriteBatch([]ipv6.Message, int) (int, error) } - Purpose: Internal abstraction within
StdNetBindovergolang.org/x/net/ipv4.PacketConnandipv6.PacketConn. Both types share the same underlyingipv6.Messageslice type (verified by a compile-time assertion:_ ipv6.Message = ipv4.Message{}), so a singlereceiveIPfunction handles both address families via these interfaces. - Implementations:
*ipv4.PacketConn,*ipv6.PacketConn - Design quality: Pragmatic internal deduplication rather than a public abstraction contract. Not visible outside the
connpackage.
Interface patterns#
Size distribution: Very lean. The two primary interfaces (
tun.Device,conn.Bind) have 8 and 6 methods respectively. The supporting interfaces (Endpoint,BindSocketToInterface,PeekLookAtSocketFd) have 6, 2, and 2 methods. Average ~5 methods per interface. No God interfaces.Embedding: Not used between the public interfaces. There is no interface embedding (e.g.
Binddoes not embedio.Closereven though it has aClose()method). This is a deliberate choice for clarity over structural conciseness.Implicit satisfaction: Interfaces are defined by consumers (
devicepackage consumestun.Deviceandconn.Bind), but the definitions live in provider packages (tunandconn). This is a design compromise: the interfaces are co-located with the default implementations to keep the package structure simple, but conceptually they represent the protocol engine’s requirements. The capability extension interfaces (BindSocketToInterface,PeekLookAtSocketFd) follow pure implicit satisfaction — platform-specific implementations satisfy them without the interface knowing.stdlib interfaces used:
net.Listener—UAPIListenerwraps itnet.Conn— UAPI connectionsos.File— returned bytun.Device.File()- The
io.Reader/io.Writerinterfaces are intentionally not used for packet I/O; the batch-oriented[][]byteAPI is incompatible with single-read semantics.
Key abstractions#
1. tun.Device — the TUN seam
This is the most critical interface in the codebase. It completely insulates the protocol engine from the OS kernel’s TUN driver, enabling the same device package to run on Linux, macOS, Windows, and inside a userspace network stack (gVisor/netstack). Its batch-read design is architecturally significant: without it, the CPU overhead of per-packet syscalls would dominate at high throughput.
2. conn.Bind — the UDP socket seam
The second critical seam. The ReceiveFunc-slice return from Open is the most unusual design choice in the codebase. By returning functions rather than a Receive method, the interface cleanly exposes that there may be multiple independent receive loops (one per AF) without forcing the engine to understand address families. The Windows RIO implementation plugs in here without any changes to the engine.
3. conn.Endpoint — the address cache
Less visible but architecturally important. The source-address caching inside Endpoint implements WireGuard’s “roaming” behavior (a peer’s IP address can change, and the stack must both detect and adapt to this). The ClearSrc() method is the hook for route-change invalidation. Without this interface, the engine would need to know about OS-level routing events per platform.
4. conn.ReceiveFunc (function type)
Not an interface in the Go sense, but a named function type used as a first-class abstraction. Returning []ReceiveFunc from Bind.Open treats receive loops as composable values rather than requiring a multi-method interface or a callback registration mechanism. This is idiomatic Go: use a function type when the abstraction is a single behavior.
5. Capability extension interfaces (BindSocketToInterface, PeekLookAtSocketFd)
Exemplify the “optional interface” pattern: the main contract stays clean and cross-platform, while platform-specific features are discovered at runtime via type assertion. Used by wireguard-windows and wireguard-android respectively. This pattern avoids either polluting the main interface with no-op methods or fragmenting the type hierarchy.
Interface-driven extensibility#
wireguard-go’s extensibility is entirely interface-driven through the two primary seams:
Embedding as a library: The canonical extension point is device.NewDevice(tun tun.Device, bind conn.Bind, logger *Logger). Any project (Tailscale, wireguard-windows, mobile apps) can provide its own tun.Device and conn.Bind implementations and receive a fully functional WireGuard engine. No plugin registry, no hooks, no callbacks — the interface parameters are the extension mechanism.
Platform adaptation: Each supported OS provides its own tun.NativeTun and, for Windows, a WinRingBind. The GOOS-specific files are compiled in via build constraints. No runtime dispatch or factory registry is needed; the appropriate implementation is wired in at build time and passed to NewDevice at startup.
Test doubles: Both interfaces have complete in-memory implementations in tun/tuntest and conn/bindtest. ChannelTUN and ChannelBind use Go channels as the transport, enabling deterministic unit tests of the device engine without any OS network resources.
gVisor netstack integration: tun/netstack implements tun.Device on top of gVisor’s userspace TCP/IP stack. This enables wireguard-go to operate as a full userspace network stack (used by Tailscale for its “userspace networking” mode), again without any changes to the engine.
The overall interface design reflects a philosophy of minimum necessary abstraction: exactly two seams are created, each precisely sized to their responsibility. The result is a codebase that is simultaneously easy to embed, easy to port, and easy to test — with zero abstraction overhead in the steady-state data path.