WireGuard — sing-box
Endpoint, not outbound
WireGuard is configured as an endpoint in sing-box, not as an outbound. The block sits under the root endpoints[] array with type: "wireguard", and is referenced from routing rules just like any named outbound.
Endpoint options
type: "wireguard" under endpoints[]:
| Field | Type | Default | Allowed values | Description |
|---|---|---|---|---|
system | bool | false | true | false | Use the operating-system TUN device instead of the userspace gVisor stack. Higher throughput, but requires elevated privileges on most platforms. |
name | string | (auto) | <string> | Display name for the endpoint. Used in stats and the routing engine. |
mtu | uint32 | 1408 | <bytes> | Tunnel MTU. The default is 1408 (12 bytes lower than Xray's 1420 because sing-box accounts for the IPv6 header overhead on the inner packet). |
address | badoption.Listable[netip.Prefix] | (required) | [<CIDR>] | Local tunnel addresses, list of netip.Prefix. At least one entry is required. |
private_key | string | (required) | <base64 key> | Local private key, 32 bytes base64-encoded. |
listen_port | uint16 | 0 (random) | <port> | UDP port used for outgoing WireGuard packets. 0 picks a random ephemeral port. |
peers | []WireGuardPeer | [] | [WireGuardPeer] | Remote peers. |
udp_timeout | badoption.Duration | 5m | <duration> | Idle timeout for the underlying UDP session, used by gVisor's session table. |
udp_mapping | UDPNATBehavior | endpoint_independent | endpoint_independent | address_dependent | address_and_port_dependent | UDP NAT mapping: reuse one mapping per source address and port for all destinations (default), or a separate mapping per destination address / address and port. |
udp_filtering | UDPNATBehavior | endpoint_independent | endpoint_independent | address_dependent | address_and_port_dependent | UDP NAT filtering: accept packets from any remote endpoint (default), or only from addresses / address-and-port pairs already sent to. |
udp_nat_max | uint32 | 0 (auto) | <uint32> | Maximum number of UDP NAT sessions; the least recently used one is closed when full. 0 means 4096 on iOS, otherwise 4096–16384 based on total memory (16384 if it can't be detected). |
workers | int | (CPU-based) | <int> | Number of encryption-pipeline workers. 0 uses runtime.NumCPU(). |
Source: option/wireguard.go:9-23 · pinned at v1.14.2 (af6e64c)
The struct also embeds DialerOptions for the underlying UDP socket — bind_interface, routing_mark, detour, etc.
peers[]
| Field | Type | Default | Allowed values | Description |
|---|---|---|---|---|
address | string | (unset) | <host> | Peer hostname or IP. Resolved at handshake time when a hostname is supplied. |
port | uint16 | 51820 | <port> | Peer UDP port. |
public_key | string | (required) | <base64 key> | Peer public key, 32 bytes base64-encoded. |
pre_shared_key | string | (unset) | <base64 key> | Optional pre-shared-key (32 bytes base64) for additional handshake mixing. |
allowed_ips | badoption.Listable[netip.Prefix] | [] | [<CIDR>] | Source/destination CIDRs routed to this peer. |
persistent_keepalive_interval | uint16 | 0 | <seconds> | WireGuard persistent-keepalive interval. 0 disables keepalives. |
reserved | []uint8 | (empty) | <3 bytes> | Three-byte WireGuard reserved-field override used by some commercial providers. |
Source: option/wireguard.go:25-33 · pinned at v1.14.2 (af6e64c)
Examples
Single peer with system TUN:
json
{
"endpoints": [
{
"type": "wireguard",
"tag": "wg-ep",
"system": true,
"name": "wg0",
"address": ["10.0.0.2/32", "fd00::2/128"],
"private_key": "<base64-private-key>",
"mtu": 1408,
"peers": [
{
"address": "wg.example.com",
"port": 51820,
"public_key": "<base64-peer-public-key>",
"allowed_ips": ["0.0.0.0/0", "::/0"],
"persistent_keepalive_interval": 25
}
]
}
],
"route": {
"rules": [
{ "domain": ["geosite-private"], "outbound": "wg-ep" }
]
}
}Userspace device (no system TUN) for unprivileged runs:
json
{
"endpoints": [
{
"type": "wireguard",
"tag": "wg-userspace",
"system": false,
"address": ["172.16.0.2/24"],
"private_key": "<base64>",
"peers": [
{ "address": "1.2.3.4", "port": 51820, "public_key": "<base64>", "allowed_ips": ["0.0.0.0/0"] }
]
}
]
}Notes
- WireGuard is always an endpoint in sing-box. A config that uses
outbound: { type: wireguard }fails to load — use the endpoints model. system: trueuses the platform's TUN device directly. This is faster but requires root / Administrator.system: false(the default) uses gVisor's userspace TCP/IP stack — slower but unprivileged.- Keys must be base64 (32-byte raw, then base64). Hex keys (which Xray accepts) are not supported here.
- The default MTU is
1408, not the wire-format 1420 — sing-box reserves 12 bytes for inner IPv6 header overhead. - TCP, UDP and ICMP from L3 inbounds (TUN, WireGuard, Tailscale) routed to a WireGuard endpoint are forwarded at L3 during pre-match, skipping the L3→L4 translation. This also applies when the endpoint is the default outbound; FakeIP destinations need a
resolveaction in pre-match, otherwise they are rejected.
Cross-core notes
- Xray-core keeps WireGuard as a protocol-style outbound under
outbounds[]withprotocol: "wireguard". Accepts hex or base64 keys, defaults MTU to 1420. See WireGuard — Xray-core. - mihomo also keeps it as an outbound with a simplified single-peer form (top-level
server/port/public-key/allowed-ips) and an optionalamnezia-wg-optionblock. See WireGuard — mihomo.
Source: option/wireguard.go:9-33 · v1.14.2 (af6e64c)
