wireguard-go — API Surface#
API types#
Three distinct API surfaces, in decreasing complexity:
- Library (Go embedding API) — the primary surface; used by Tailscale, wireguard-windows, Android/iOS wrappers
- UAPI (WireGuard Userspace API Protocol) — Unix socket/named-pipe text protocol; the runtime configuration API
- CLI — minimal, single binary; only two flags; manual
os.Argsparsing
No HTTP, no gRPC, no REST.
CLI (if applicable)#
- Framework: None — stdlib
os.Argsparsed manually (11 lines ofswitch) - Binary:
wireguard-go [-f/--foreground] INTERFACE-NAME - Command structure: Single command, no subcommands
- Flags:
-f/--foreground— suppress daemonization; run in the foreground--version— print version and exit- positional:
INTERFACE-NAME— TUN interface name (e.g.wg0)
- Environment variables (act as implicit config flags):
LOG_LEVEL—verbose/debug,error,silent(default:error)WG_TUN_FD— pre-opened TUN file descriptor (used by Android/iOS; skipsCreateTUN)WG_UAPI_FD— pre-opened UAPI socket fd (skipsipc.UAPIOpen)WG_PROCESS_FOREGROUND=1— set in the child process during self-re-exec daemonization
The CLI is intentionally Spartan: wireguard-go is a daemon, not a tool. All runtime configuration goes through the UAPI socket, which is exactly what wg(8) (the standard userspace configuration tool) uses.
UAPI Protocol (primary runtime configuration surface)#
The WireGuard Userspace API (UAPI) is a line-oriented text protocol over a Unix domain socket (Linux/macOS/BSD) or a Windows named pipe. It is specified at https://www.wireguard.com/xplatform/#configuration-protocol and implemented in device/uapi.go.
Transport#
- Linux/macOS/BSD: Unix domain socket at
/var/run/wireguard/<INTERFACE>.sock; opened viaipc.UAPIOpen(name)(ipc/uapi_unix.go) and listened viaipc.UAPIListen(name, file)(ipc/uapi_linux.go,ipc/uapi_bsd.go) - Windows: Named pipe via
ipc/namedpipe/;ipc.UAPIListen(name)(ipc/uapi_windows.go) - Accept loop:
go device.IpcHandle(conn)for each accepted connection (one goroutine per client)
Protocol grammar#
request: "get=1\n\n"
| "set=1\n" (<key>=<value>\n)* "\n"
response: (<key>=<value>\n)* "errno=0\n\n"
| "errno=<POSIX code>\n\n"All keys and values are plain ASCII. Keys/values are separated by =. Records are separated by blank lines. The protocol is stateful within a single set operation (keys are processed in sequence; a public_key= line switches context from device to peer).
get=1 — dump current configuration#
Device fields returned:
| Key | Type | Description |
|---|---|---|
private_key | hex string | device private key (32 bytes) |
listen_port | uint16 | UDP listen port |
fwmark | uint32 | Linux socket mark |
Per-peer fields returned (one block per peer):
| Key | Type | Description |
|---|---|---|
public_key | hex string | peer’s public key |
preshared_key | hex string | optional preshared key |
protocol_version | int | always 1 |
endpoint | ip:port | peer’s UDP endpoint |
last_handshake_time_sec | int64 | seconds since last handshake |
last_handshake_time_nsec | int64 | nanoseconds component |
tx_bytes | uint64 | bytes sent to peer |
rx_bytes | uint64 | bytes received from peer |
persistent_keepalive_interval | uint16 | keepalive interval in seconds |
allowed_ip | CIDR prefix | one entry per allowed IP range |
set=1 — apply configuration#
Device keys accepted:
| Key | Effect |
|---|---|
private_key=<hex> | Update device private key; re-derives all peer session keys |
listen_port=<uint16> | Update UDP listen port; triggers BindUpdate() (rebind) |
fwmark=<uint32> | Set Linux socket mark; triggers BindSetMark() |
replace_peers=true | Remove all peers before applying new peer config |
Peer keys accepted (after public_key=<hex> switches context):
| Key | Effect |
|---|---|
remove=true | Delete this peer |
update_only=true | Skip creation if peer doesn’t exist |
preshared_key=<hex> | Set optional preshared key |
endpoint=<ip:port> | Set peer’s remote endpoint |
persistent_keepalive_interval=<uint16> | Set keepalive interval (0 = disable) |
replace_allowed_ips=true | Clear peer’s allowed IP list before adding new ones |
allowed_ip=<CIDR> | Add an allowed IP prefix (prefix -<CIDR> removes it) |
protocol_version=1 | Accepted; any other value returns error |
Error responses#
Error codes are POSIX errno values (ipc/ package defines: IpcErrorIO=5, IpcErrorProtocol=71, IpcErrorInvalid=22, IpcErrorPortInUse=98, IpcErrorUnknown=0). On success: errno=0\n\n.
Library API (Go embedding — the most important surface)#
wireguard-go has no internal/ barrier. All packages are exported and intended for use as a library. This is a documented, first-class design decision. Known embedders: Tailscale (tailscale.com/wgengine), wireguard-windows, Android/iOS official apps.
device package — core engine#
Constructor:
func NewDevice(tunDevice tun.Device, bind conn.Bind, logger *Logger) *DeviceThe single entry point for embedding. All three parameters are interfaces or structs the embedder controls. No DI framework.
*Device public methods:
| Method | Description |
|---|---|
Up() error | Bring device up; opens bind, starts I/O goroutines |
Down() error | Bring device down; closes bind, stops I/O |
Close() | Permanently shut down the device and all goroutines |
Wait() chan struct{} | Returns a channel closed when the device has stopped |
IsUnderLoad() bool | Returns true if the cookie-based DoS mitigation is active |
SetPrivateKey(NoisePrivateKey) error | Update device private key at runtime |
LookupPeer(NoisePublicKey) *Peer | Look up a peer by its public key |
RemovePeer(NoisePublicKey) | Remove a peer by public key |
RemoveAllPeers() | Remove all peers |
Bind() conn.Bind | Return the current conn.Bind implementation |
BindUpdate() error | Rebind UDP socket (e.g. after port change) |
BindSetMark(uint32) error | Set socket mark on the Bind |
BindClose() error | Close the Bind without shutting down the device |
BatchSize() int | Return the batch size for packet I/O |
SendKeepalivesToPeersWithCurrentKeypair() | Force keepalives to all peers with active keypairs |
IpcHandle(net.Conn) | Serve one UAPI protocol connection (for custom UAPI transports) |
IpcGet() (string, error) | Convenience: run get=1 and return result as a string |
IpcSet(string) error | Convenience: run set=1 with the provided UAPI config string |
IpcGetOperation(io.Writer) error | Low-level: write UAPI get response to any io.Writer |
IpcSetOperation(io.Reader) error | Low-level: apply UAPI set config from any io.Reader |
Logger:
func NewLogger(level int, prepend string) *Logger
// level constants: LogLevelSilent, LogLevelError, LogLevelVerbose
func DiscardLogf(format string, args ...any) // no-op log functionLogger struct:
type Logger struct {
Verbosef func(format string, args ...any)
Errorf func(format string, args ...any)
}Embedders can inject any log function — log.Printf, zerolog, zap wrapper, etc.
tun package — TUN device interface#
type Device interface {
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
}
type Event int // EventUp, EventDown, EventMTUUpdatePlatform constructors (all return tun.Device):
// Linux
func CreateTUN(name string, mtu int) (Device, error)
func CreateTUNFromFile(file *os.File, mtu int) (Device, error)
func CreateUnmonitoredTUNFromFD(fd int) (Device, string, error)
// macOS, FreeBSD, OpenBSD — same signature as Linux variants
func CreateTUN(name string, mtu int) (Device, error)
func CreateTUNFromFile(file *os.File, mtu int) (Device, error)
// Windows (uses wintun driver)
func CreateTUN(ifname string, mtu int) (Device, error)
func CreateTUNWithRequestedGUID(ifname string, requestedGUID *windows.GUID, mtu int) (Device, error)The batch-oriented Read/Write API (reading/writing [][]byte rather than a single []byte) was a deliberate design upgrade (vs. the original scalar API) to support Linux’s sendmmsg/recvmmsg and similar batching on other platforms.
conn package — UDP socket interface#
type Bind interface {
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() int
}
type Endpoint interface {
ClearSrc()
SrcToString() string
DstToString() string
DstToBytes() []byte
DstIP() netip.Addr
SrcIP() netip.Addr
}
type ReceiveFunc func(packets [][]byte, sizes []int, eps []Endpoint) (n int, err error)Optional Bind extension interfaces (type-asserted at runtime):
// Implemented by Windows bind — used to tie to a specific network interface
type BindSocketToInterface interface {
BindSocketToInterface4(interfaceIndex uint32, blackhole bool) error
BindSocketToInterface6(interfaceIndex uint32, blackhole bool) error
}
// Implemented by Linux/Android bind — lets Android peer at the fd for routing rules
type PeekLookAtSocketFd interface {
PeekLookAtSocketFd4() (fd int, err error)
PeekLookAtSocketFd6() (fd int, err error)
}Constructors:
func NewDefaultBind() Bind // platform-default (StdNetBind on Unix, WinRingBind on Windows)
func NewStdNetBind() Bind // stdlib net.UDPConn-based (all Unix platforms)
func NewWinRingBind() Bind // Windows Registered I/O (RIO) high-performance bindtun/netstack package — in-process network stack#
An optional package that creates a WireGuard tunnel that terminates inside a gVisor userspace TCP/IP stack rather than the OS kernel. Enables Go programs to make TCP/UDP connections through a WireGuard tunnel without root privileges and without any OS network configuration.
func CreateNetTUN(localAddresses, dnsServers []netip.Addr, mtu int) (tun.Device, *Net, error)*Net methods (the userspace network stack handle):
// TCP
func (n *Net) DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) (*gonet.TCPConn, error)
func (n *Net) DialTCP(addr *net.TCPAddr) (*gonet.TCPConn, error)
func (n *Net) ListenTCP(addr *net.TCPAddr) (*gonet.TCPListener, error)
// UDP
func (n *Net) DialUDP(laddr, raddr *net.UDPAddr) (*gonet.UDPConn, error)
func (n *Net) ListenUDP(laddr *net.UDPAddr) (*gonet.UDPConn, error)
// ICMP (ping)
func (n *Net) DialPing(laddr, raddr *PingAddr) (*PingConn, error)
func (n *Net) ListenPing(laddr *PingAddr) (*PingConn, error)
// DNS resolution (uses the dnsServers passed to CreateNetTUN)
func (n *Net) LookupHost(host string) (addrs []string, err error)
// net.Dialer-compatible interface
func (n *Net) DialContext(ctx context.Context, network, address string) (net.Conn, error)This makes the netstack sub-package act as a drop-in net.Dialer replacement for any Go HTTP client or gRPC client, routing all traffic through the WireGuard tunnel without needing CAP_NET_ADMIN.
ipc package — UAPI socket management#
// Unix (Linux, macOS, BSD)
func UAPIOpen(name string) (*os.File, error) // creates the Unix socket file
func UAPIListen(name string, file *os.File) (net.Listener, error) // wraps it for Accept()
// Windows
func UAPIListen(name string) (net.Listener, error) // creates and listens on a named pipePlugin / Extension system#
wireguard-go uses Go interfaces as its extension mechanism — there is no plugin registry, no RPC, no dynamic loading. Extension happens at compile time by implementing the two core interfaces:
| Interface | Package | Extension point | Used by |
|---|---|---|---|
tun.Device | tun | Custom TUN implementation | Tailscale (tun.TUN wrapper), wireguard-windows (wintun), Android (tun_android.go), gVisor netstack (tun/netstack) |
conn.Bind | conn | Custom UDP transport | wireguard-windows (RIO bind), wireguard-android (custom routing), Tailscale (magicsock) |
conn.BindSocketToInterface | conn | Optional: bind socket to a specific NIC | wireguard-windows |
conn.PeekLookAtSocketFd | conn | Optional: expose raw fd for platform routing | wireguard-android |
The IpcSet/IpcGet convenience methods let embedders configure the device programmatically without touching the Unix socket at all — Tailscale uses IpcSet(uapiConf) to configure peers from its own configuration system.
API style observations#
Minimal surface, maximal extensibility. The public API of
deviceis ~20 methods on a single*Devicetype. All configurability comes from injectingtun.Deviceandconn.Bindat construction time.UAPI as the lingua franca. Both the CLI path (via Unix socket) and the library path (via
IpcSet) use the same text protocol. This means any embedder can reuse the WireGuard CLI tools (wg,wg-quick) for inspection/configuration.No functional options, no builder, no config struct. The constructor takes exactly three parameters. Runtime mutation happens exclusively through
IpcSetOperationor the handful of explicit methods (Up,Down,SetPrivateKey,BindUpdate).Interface extensions via type assertion, not registration.
BindSocketToInterfaceandPeekLookAtSocketFdare discovered by type-asserting theconn.Bindat runtime. This lets platform-specific capabilities be added without changing the core interface.IpcGet/IpcSetas the programmatic API. Even in library usage, the intended way to add/remove peers isdevice.IpcSet(uapiString). This keeps the UAPI text protocol as the single source of truth for configuration semantics.