wireguard-go — API Surface#

API types#

Three distinct API surfaces, in decreasing complexity:

  1. Library (Go embedding API) — the primary surface; used by Tailscale, wireguard-windows, Android/iOS wrappers
  2. UAPI (WireGuard Userspace API Protocol) — Unix socket/named-pipe text protocol; the runtime configuration API
  3. CLI — minimal, single binary; only two flags; manual os.Args parsing

No HTTP, no gRPC, no REST.


CLI (if applicable)#

  • Framework: None — stdlib os.Args parsed manually (11 lines of switch)
  • 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_LEVELverbose/debug, error, silent (default: error)
    • WG_TUN_FD — pre-opened TUN file descriptor (used by Android/iOS; skips CreateTUN)
    • WG_UAPI_FD — pre-opened UAPI socket fd (skips ipc.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 via ipc.UAPIOpen(name) (ipc/uapi_unix.go) and listened via ipc.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:

KeyTypeDescription
private_keyhex stringdevice private key (32 bytes)
listen_portuint16UDP listen port
fwmarkuint32Linux socket mark

Per-peer fields returned (one block per peer):

KeyTypeDescription
public_keyhex stringpeer’s public key
preshared_keyhex stringoptional preshared key
protocol_versionintalways 1
endpointip:portpeer’s UDP endpoint
last_handshake_time_secint64seconds since last handshake
last_handshake_time_nsecint64nanoseconds component
tx_bytesuint64bytes sent to peer
rx_bytesuint64bytes received from peer
persistent_keepalive_intervaluint16keepalive interval in seconds
allowed_ipCIDR prefixone entry per allowed IP range

set=1 — apply configuration#

Device keys accepted:

KeyEffect
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=trueRemove all peers before applying new peer config

Peer keys accepted (after public_key=<hex> switches context):

KeyEffect
remove=trueDelete this peer
update_only=trueSkip 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=trueClear peer’s allowed IP list before adding new ones
allowed_ip=<CIDR>Add an allowed IP prefix (prefix -<CIDR> removes it)
protocol_version=1Accepted; 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) *Device

The single entry point for embedding. All three parameters are interfaces or structs the embedder controls. No DI framework.

*Device public methods:

MethodDescription
Up() errorBring device up; opens bind, starts I/O goroutines
Down() errorBring 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() boolReturns true if the cookie-based DoS mitigation is active
SetPrivateKey(NoisePrivateKey) errorUpdate device private key at runtime
LookupPeer(NoisePublicKey) *PeerLook up a peer by its public key
RemovePeer(NoisePublicKey)Remove a peer by public key
RemoveAllPeers()Remove all peers
Bind() conn.BindReturn the current conn.Bind implementation
BindUpdate() errorRebind UDP socket (e.g. after port change)
BindSetMark(uint32) errorSet socket mark on the Bind
BindClose() errorClose the Bind without shutting down the device
BatchSize() intReturn 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) errorConvenience: run set=1 with the provided UAPI config string
IpcGetOperation(io.Writer) errorLow-level: write UAPI get response to any io.Writer
IpcSetOperation(io.Reader) errorLow-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 function

Logger 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, EventMTUUpdate

Platform 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 bind

tun/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 pipe

Plugin / 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:

InterfacePackageExtension pointUsed by
tun.DevicetunCustom TUN implementationTailscale (tun.TUN wrapper), wireguard-windows (wintun), Android (tun_android.go), gVisor netstack (tun/netstack)
conn.BindconnCustom UDP transportwireguard-windows (RIO bind), wireguard-android (custom routing), Tailscale (magicsock)
conn.BindSocketToInterfaceconnOptional: bind socket to a specific NICwireguard-windows
conn.PeekLookAtSocketFdconnOptional: expose raw fd for platform routingwireguard-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#

  1. Minimal surface, maximal extensibility. The public API of device is ~20 methods on a single *Device type. All configurability comes from injecting tun.Device and conn.Bind at construction time.

  2. 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.

  3. No functional options, no builder, no config struct. The constructor takes exactly three parameters. Runtime mutation happens exclusively through IpcSetOperation or the handful of explicit methods (Up, Down, SetPrivateKey, BindUpdate).

  4. Interface extensions via type assertion, not registration. BindSocketToInterface and PeekLookAtSocketFd are discovered by type-asserting the conn.Bind at runtime. This lets platform-specific capabilities be added without changing the core interface.

  5. IpcGet/IpcSet as the programmatic API. Even in library usage, the intended way to add/remove peers is device.IpcSet(uapiString). This keeps the UAPI text protocol as the single source of truth for configuration semantics.