Add named userspace WireGuard endpoints so Shoes can route inbound and outbound traffic through WireGuard without a kernel interface. Includes TCP/UDP forwarding, proxy-chain transports, reload lifecycle management, configuration validation, documentation, and dependency notices for release artifacts.
42 KiB
Configuration Reference
shoes uses YAML configuration files. Multiple configuration types can be combined in a single file or split across multiple files.
Table of Contents
- Configuration Structure
- Server Config
- Server Protocols
- TUN Config
- WireGuard Endpoints
- Client Config
- Client Protocols
- Rules System
- Named Groups
- Named PEMs
- Advanced Features
- Command Line
WireGuard Endpoints
WireGuard is a named top-level endpoint, not a TCP server protocol or a stream wrapper. Each configuration generation owns one GotaTun device and one smoltcp stack per endpoint name. All references share that endpoint. Unix builds support this mode; Windows rejects WireGuard configuration.
See server and
client examples. Their demonstration keys
must be replaced before deployment. Generate a private key with wg genkey
and derive its public key with wg pubkey. Never share the private key.
| Field | Meaning |
|---|---|
wireguard |
Unique, nonempty endpoint name |
private_key |
Base64-encoded 32-byte private key |
addresses |
Nonempty local tunnel CIDRs; at most one address per family |
listen |
Optional literal UDP socket address in direct mode; omission chooses an ephemeral dual-stack wildcard, with IPv4 fallback when IPv6 is unavailable |
bind_interface |
Optional outer UDP interface, supported on Linux/Android |
outer_client_chain |
Optional client chain carrying encrypted UDP to the peers; conflicts with listen and endpoint bind_interface |
mtu |
Inner IP MTU, default 1408; supported range 1280-1420 |
peers |
Nonempty static peer list |
inbound |
Optional forwarding policy; omitted disables unsolicited inbound flows |
resource_limits |
Same fields as TUN, with bounded WireGuard defaults |
Each peer requires public_key and nonempty allowed_ips. Optional fields
are pre_shared_key, endpoint: "host:port", and
persistent_keepalive in seconds (zero disables it). Omit endpoint for a
peer that initiates toward this server. Hostnames are resolved through native
bootstrap DNS during preparation and refreshed on reload. Direct mode selects
the first resolved address compatible with the bound socket at startup.
Authenticated WireGuard roaming can update the active peer address without a reload in direct mode.
Overlapping allowed prefixes between different peers, duplicate keys, and
self-peering are rejected. Assign each client distinct tunnel addresses.
0.0.0.0:51820 listens only on IPv4; [::]:51820 uses a dual-stack socket
where supported. Outer sockets retain mobile socket protection and interface
binding. No OS interface, route, firewall rule, or NAT rule is created.
An explicit inbound requires nonempty rules and may specify dns.
Rules use the normal destination masks, overrides, and client chains. There
is no implicit allow-all fallback for an empty inbound block. Outbound socket
replies remain enabled when inbound is omitted. Per-peer authorization
beyond cryptokey source validation is not supplied by destination rules.
Authenticated peers can reach the gateway's local services unless rules block
them. The server example blocks loopback and link-local destinations; restrict
private networks too when peers should not access the gateway's LAN.
Reference an endpoint from the first hop:
client_chain:
- protocol: {type: wireguard, endpoint: wg-out}
- address: "10.77.0.10:1080"
protocol: {type: socks}
The second hop is optional. A WireGuard reference must not specify address,
bind_interface, QUIC, or TCP/QUIC socket settings. WireGuard cannot appear
inside TLS/WebSocket or at later hops. A missing route, address family, failed
endpoint, or unavailable peer never causes direct fallback.
To put proxies before WireGuard, configure outer_client_chain on the endpoint:
outer_client_chain:
- address: "relay.example:443"
protocol:
type: tls
protocol:
type: vless
user_id: "b85798ef-e9dc-46a4-9a87-8da4499d36d0"
udp_enabled: true
The effective path is Shoes -> outer proxy chain -> WireGuard peer -> optional inner proxy chain -> destination. The routing rule still references WireGuard
at hop zero. See the complete detour example.
Outer chains use the normal inline, named-group and pool syntax. Their final hop must support UDP; earlier hops may be TCP-only. VLESS, VMess and compatible Shadowsocks/AnyTLS UDP-over-TCP configurations work. Shoes' SOCKS outbound only implements CONNECT, and Trojan outbound UDP is not implemented, so neither can be the final UDP hop. Pools select UDP-capable final members. Selection remains fixed for the life of an association; reconnection may select another member.
Every detoured peer must specify endpoint. One persistent association is shared
by all inner flows to each resolved peer destination; peers with the same resolved
destination share an association. No native WireGuard UDP listener is opened.
Configure physical interface binding on the first outer client, not the endpoint.
inbound.rules still handles authenticated connections received over established
associations, but cannot accept unsolicited public peers through a proxy detour.
Inbound-only detours require a nonzero persistent_keepalive on every peer:
Shoes must initiate a handshake to establish the proxy's UDP return mapping.
Without keepalive, an outbound inner flow must initiate that handshake first.
Detoured destinations are fixed for the configuration generation. Their message
streams do not expose source addresses, so arbitrary peer address roaming is not
supported. Peer hostname changes require reload. Peer and outer-proxy bootstrap
use native DNS independently of routed DNS and inbound.dns. Ensure host DNS and
host routes do not themselves require the tunnel being established.
Outer connect and blocked-write attempts time out after 10 seconds. Failed streams
retry with backoff from 250 ms to 10 seconds, never falling back to direct UDP.
Offline/overflow packets are dropped rather than indefinitely buffered; WireGuard
and inner TCP perform their normal retries. Use persistent_keepalive to maintain
idle proxy UDP mappings. Silent blackholes without stream errors remain UDP loss.
Queues hold at most 64 packets per destination and 64 aggregate received packets,
with a 2048-byte outer datagram limit, above the supported inner MTU plus WireGuard
overhead. Shutdown waits for workers to release their associations.
An outer chain can itself start with another named WireGuard endpoint. Dependencies
start first, regardless of declaration order; self-dependencies and indirect cycles
(including pool alternatives) are rejected. A direct WireGuard-in-WireGuard carrier
requires round_up(inner_mtu, 16) + 80 <= carrier_mtu, conservatively accounting for
IPv6, UDP and WireGuard overhead. For example, inner MTU 1280 fits carrier MTU 1408.
A TCP proxy after the carrier segments packets instead. External path MTU still
needs operator tuning; UDP-over-TCP can introduce head-of-line blocking.
DNS remains independently configured. Merely defining a WireGuard endpoint
does not tunnel the application's default native DNS. Explicit TCP DNS,
DoT, and DoH client chains may use WireGuard with a literal upstream IP.
Use server_name for TLS authentication where needed. UDP DNS, HTTP/3 DNS,
hostname upstreams on WireGuard chains, and bootstrap dependencies on
WireGuard-routed DNS groups are rejected. Endpoint bootstrap itself is native
DNS, not DNS through the not-yet-active tunnel.
This is TCP/UDP proxying, not arbitrary IP routing. Remote destinations see the gateway's outgoing proxy connection, not the original client's source address. A non-Shoes gateway must provide the required routes and forwarding or NAT for destinations behind it. ICMP forwarding, IP fragment reassembly, peer-to-peer IP forwarding, WARP reserved bytes, and wg-quick import are not implemented.
Fragmented input and unsupported IPv6 extension chains are dropped before transport parsing. TCP advertises an MSS derived from the MTU. Outbound UDP writes larger than MTU minus 28 bytes (IPv4) or 48 bytes (IPv6) are dropped and counted without closing the flow; they are never truncated. UDP queue-budget exhaustion also drops individual writes. At MTU 1408 the payload limits are 1380 and 1360 bytes. Oversized UDP responses on inbound forwarding are dropped. Large DNS replies should use TCP-based transports. Reduce MTU for constrained underlays; automatic path-MTU discovery is not provided. Packets received from a peer above the local MTU are also dropped. Configure matching MTUs on both sides, including peers whose default is 1420 rather than Shoes' 1408; TCP MSS negotiation does not protect UDP traffic.
Default admission limits are 256 TCP connections, 32 KiB TCP buffers, a 32 MiB TCP buffer budget, 1024 UDP sessions, 4096 UDP destinations, 64 destinations per inbound UDP session, and 8 MiB queued UDP payloads. UDP forwarding and outbound associations have separate bounded pools. Missing or null limit fields use WireGuard defaults rather than the unlimited TUN defaults. Packet and command queues are bounded at 64. These budgets cover modeled Shoes-owned state, not total process RSS or all GotaTun state.
The TCP buffers bound the advertised receive window and in-flight data across
the tunnel. A 32 KiB window limits a single flow to roughly 5 Mbit/s at 50 ms RTT
or 2.6 Mbit/s at 100 ms RTT, before other bottlenecks. For higher-bandwidth,
higher-latency links, set resource_limits.tcp_buffer_size to, for example,
262144 (256 KiB). Four buffers are budgeted per flow, so retaining 256 connections
at that size also requires tcp_memory_bytes: 268435456 (256 MiB); the default
32 MiB budget would admit only 32 connections. Tune against measured RTT,
throughput, and available memory rather than increasing buffers unconditionally.
Periodic logs report endpoint flow/drop counts and each peer's public key, handshake age, and byte counters. Private keys and PSKs are redacted.
Preparation failures retain the old configuration. Successful reloads disconnect existing tunnel flows and rebuild endpoints; sessions are not migrated. Bind failures during activation can still terminate the process after the old generation has stopped. Shutdown waits for buffered UDP tasks to release their sockets before rebind. No host route cleanup is necessary.
Configuration Structure
A configuration file is a YAML array containing one or more configuration entries. Each entry can be:
- Server Config - Defines a proxy server instance
- TUN Config - Defines a TUN/VPN device for transparent proxying
- WireGuard Endpoint - Defines a shared userspace WireGuard server/client
- Client Config Group - Defines reusable upstream proxy configurations
- Rule Config Group - Defines reusable routing rules
- Named PEM - Defines reusable certificate/key data
# Server configs have 'address' or 'path'
- address: "0.0.0.0:8080"
protocol: ...
# TUN configs have 'device_name' or 'device_fd'
- device_name: "tun0"
address: "10.0.0.1"
...
# Client config groups have 'client_group'
- client_group: my-upstream
client_proxy: ...
# Rule config groups have 'rule_group'
- rule_group: my-rules
rules: ...
# Named PEMs have 'pem'
- pem: my-cert
path: /path/to/cert.pem
Server Config
# Bind to IP address and port
address: "0.0.0.0:8080" # IPv4
address: "[::]:8080" # IPv6
address: "0.0.0.0:443-445" # Port range
# OR bind to multiple addresses and port ranges (`address` is also accepted)
addresses:
- "127.0.0.1:22223"
- "172.17.0.1:22223-22225"
# OR bind to Unix socket (TCP only)
path: "/tmp/shoes.sock"
# Protocol configuration (required)
protocol: ServerProxyConfig
# Transport layer (default: tcp)
transport: tcp | quic
# TCP settings (only when transport: tcp)
tcp_settings:
no_delay: true # Default: true
# QUIC settings (required when transport: quic)
quic_settings:
cert: string # TLS certificate (path or named PEM)
key: string # TLS private key (path or named PEM)
alpn_protocols: [string] # Optional ALPN protocols
client_ca_certs: [string] # Optional client CA certificates
client_fingerprints: [string] # Optional client certificate fingerprints
num_endpoints: int # Optional, 0 = auto (based on thread count)
key_exchange_groups: [X25519MLKEM768] # Optional; see TLS Key Exchange below
# Routing rules (default: allow-all-direct)
rules: string | [RuleConfig]
Server Protocols
HTTP
protocol:
type: http
username: string? # Optional authentication
password: string?
Accepts HTTP/1.0 and HTTP/1.1 CONNECT and absolute-form forwarding, plus multiplexed TCP CONNECT over HTTP/2. H2 is detected from its connection preface; cleartext H2 requires prior knowledge, not HTTP Upgrade. Authentication applies independently to every H2 stream. A successful CONNECT response is sent only after the destination connects.
H2 connections with no active tunnels close after 60 seconds; PINGs and rejected authentication attempts do not extend that interval. Missing credentials receive a 407 challenge. Incorrect credentials also begin connection shutdown, preventing repeated password guesses on one carrier.
For HTTPS proxying, wrap HTTP in a TLS server and explicitly configure
alpn_protocols: [h2, http/1.1] on its TLS target. HTTP does not change the
parent layer's ALPN defaults. H2 GET/POST forwarding, extended CONNECT,
CONNECT-UDP, and HTTP/3 are not supported. This is standard HTTP proxying, not
NaiveProxy or XHTTP.
SOCKS5
protocol:
type: socks # Aliases: socks5
username: string?
password: string?
udp_enabled: true # Default: true (enables UDP ASSOCIATE)
Mixed (HTTP + SOCKS5)
protocol:
type: mixed # Aliases: http+socks, socks+http
username: string?
password: string?
udp_enabled: true # Default: true (enables UDP ASSOCIATE for SOCKS5)
Auto-detects HTTP (including H2 CONNECT) or SOCKS5.
Shadowsocks
protocol:
type: shadowsocks # Aliases: ss
cipher: string # See supported ciphers below
password: string
# Supported ciphers:
# - aes-128-gcm
# - aes-256-gcm
# - chacha20-ietf-poly1305
# - 2022-blake3-aes-128-gcm
# - 2022-blake3-aes-256-gcm
# - 2022-blake3-chacha20-ietf-poly1305
VMess
protocol:
type: vmess
cipher: string # aes-128-gcm, chacha20-poly1305, none
user_id: string # UUID
udp_enabled: true # Default: true (enables XUDP)
Note: VMess AEAD mode is always enabled. The legacy force_aead field is deprecated and non-AEAD mode is no longer supported.
Replay protection uses a process-wide cache of at most 65,536 Auth-IDs, shared across credentials, listeners, and reloads. Entries remain protected through their timestamp's 120-second validity window; future-dated IDs can occupy a slot for about four minutes. A full cache rejects new authentication rather than evicting live entries. Heavy traffic from one credential can therefore exhaust capacity for other VMess users in the same process. A backward system-clock step also pauses authentication until the clock catches up.
VLESS
protocol:
type: vless
user_id: string # UUID
udp_enabled: true # Default: true (enables XUDP)
fallback: string? # Optional fallback destination for failed auth (e.g., "127.0.0.1:80")
Trojan
protocol:
type: trojan
password: string
shadowsocks: # Optional encryption layer
cipher: string
password: string
Snell v3
protocol:
type: snell
cipher: string # aes-128-gcm, aes-256-gcm, chacha20-ietf-poly1305
password: string
udp_enabled: true # Default: true
udp_num_sockets: 1 # Default: 1, sockets per UDP session
TLS Server
protocol:
type: tls
# Standard TLS targets (by SNI)
tls_targets: # Aliases: sni_targets, targets
"example.com":
cert: string # Certificate (path or named PEM)
key: string # Private key (path or named PEM)
alpn_protocols: [string] # Optional ALPN
client_ca_certs: [string] # Optional client CA certs
client_fingerprints: [string] # Optional client cert fingerprints
vision: false # Enable Vision (requires VLESS inner protocol)
key_exchange_groups: [X25519MLKEM768] # Optional; see TLS Key Exchange below
protocol: ServerProxyConfig
override_rules: [RuleConfig] # Optional rule override
# Default TLS target (for unmatched/no SNI)
default_tls_target: # Aliases: default_target
cert: string
key: string
# ... same fields as tls_targets
# Reality targets (by SNI)
reality_targets:
"www.cloudflare.com":
private_key: string # X25519 private key (base64url)
mldsa65_seed: string? # Optional 32-byte ML-DSA-65 seed (unpadded base64url)
short_ids: [string] # Valid client IDs (hex, 0-16 chars)
dest: string # Fallback destination (e.g., "example.com:443")
dest_client_chain: ClientChain? # Optional proxy chain for reaching dest
max_time_diff: 60000 # Max timestamp diff in ms (default: 60000)
min_client_version: [1, 8, 0] # Optional [major, minor, patch]
max_client_version: [2, 0, 0] # Optional [major, minor, patch]
cipher_suites: [string] # Optional TLS 1.3 cipher suites (see below)
vision: false # Enable Vision (requires VLESS inner protocol)
protocol: ServerProxyConfig
override_rules: [RuleConfig]
# ShadowTLS v3 targets (by SNI)
shadowtls_targets:
"example.com":
password: string
handshake:
# Local handshake (with own certificate):
cert: string
key: string
alpn_protocols: [string]
client_ca_certs: [string]
client_fingerprints: [string]
# OR Remote handshake (proxy to real server):
address: string # e.g., "google.com:443"
client_proxies: [ClientConfig] # Optional proxies for handshake
protocol: ServerProxyConfig
override_rules: [RuleConfig]
# Buffer size for TLS (optional, min 16384)
tls_buffer_size: int
WebSocket
protocol:
type: websocket # Aliases: ws
targets:
- matching_path: string? # Optional path filter (e.g., "/ws")
matching_headers: # Optional header filters
X-Custom-Header: "value"
protocol: ServerProxyConfig
ping_type: ping-frame # disabled | ping-frame | empty-frame
override_rules: [RuleConfig]
Port Forward
protocol:
type: forward # Aliases: port_forward, portforward
targets: string | [string] # Target address(es)
Hysteria2
protocol:
type: hysteria2
password: string
udp_enabled: true # Default: true
TUIC v5
protocol:
type: tuic # Aliases: tuicv5
uuid: string # UUID
password: string
zero_rtt_handshake: false # Default: false (enables 0-RTT for lower latency)
AnyTLS
protocol:
type: anytls
users: # One or more users
- name: string? # Optional display name
password: string # User password
udp_enabled: true # Default: true (enables UDP over TCP)
padding_scheme: [string]? # Optional custom padding (e.g., ["stop=8", "0=30-30"])
fallback: string? # Optional fallback destination for failed auth
AnyTLS is a TLS-based multiplexing proxy protocol with traffic obfuscation. Should be used within TLS or Reality.
NaiveProxy
protocol:
type: naiveproxy # Aliases: naive
users: # One or more users
- name: string? # Optional display name
username: string # Basic Auth username
password: string # Basic Auth password
padding: true # Default: true (enables padding protocol)
udp_enabled: true # Default: true (enables UDP over TCP)
fallback: string? # Optional path to serve static files for probe resistance
NaiveProxy implements HTTP/2 CONNECT with padding for censorship resistance. Should be used within TLS with alpn_protocols: ["h2"].
TUN Config
TUN (network TUNnel) devices operate at the IP layer (Layer 3), allowing shoes to act as a transparent VPN.
# Linux: Create TUN device by name
device_name: string # Device name (e.g., "tun0")
address: string # Device IP address (e.g., "10.0.0.1")
netmask: string? # Netmask (e.g., "255.255.255.0")
destination: string? # Gateway/destination (Linux only)
# iOS/Android: Use existing file descriptor
device_fd: int # FD from VpnService (Android) or NEPacketTunnelProvider (iOS)
# Common settings
mtu: 1500 # Default: 1500 (Linux), 9000 (Android), 4064 (iOS)
tcp_enabled: true # Default: true
udp_enabled: true # Default: true
icmp_enabled: true # Default: true
# Routing rules
rules: [RuleConfig]
Platform notes:
- Linux: Requires root or
CAP_NET_ADMIN. Creates device with specified name/address. - Android: Use
device_fdfromVpnService.Builder.establish(). Routes configured via VpnService. - iOS: Use
device_fdfromNEPacketTunnelProvider.packetFlow.
Example (Linux):
- device_name: "tun0"
address: "10.0.0.1"
netmask: "255.255.255.0"
mtu: 1500
tcp_enabled: true
udp_enabled: true
rules:
- masks: "0.0.0.0/0"
action: allow
client_chain:
address: "proxy.example.com:443"
protocol:
type: tls
protocol:
type: vless
user_id: "uuid"
Client Config
Used in rules to specify upstream proxies.
address: string # Proxy server address (e.g., "proxy.example.com:1080")
protocol: ClientProxyConfig
transport: tcp | quic # Default: tcp
bind_interface: string # Optional, Linux/Android/Fuchsia only
tcp_settings:
no_delay: true
quic_settings:
verify: true # Default: true
server_fingerprints: [string]
sni_hostname: string
alpn_protocols: [string]
cert: string # Client certificate for mTLS
key: string # Client key for mTLS
key_exchange_groups: [X25519MLKEM768] # Optional; see TLS Key Exchange below
Client Protocols
Direct
protocol:
type: direct
HTTP
protocol:
type: http
username: string?
password: string?
http_version: h1 # h1 (default) or h2
resolve_hostname: false # Resolve destinations locally before CONNECT
Both versions establish TCP CONNECT tunnels. When the H2 proxy is the first and only hop, eligible routes reuse a connection for multiple concurrent tunnels. Chained H2 hops use the carrier established through the selected prefix rather than sharing tunnels across routes. H1 retains its existing one-tunnel-per-connection behavior. There is no automatic version fallback or replay after a failed CONNECT.
For H2 over TLS, set http_version: h2 here and alpn_protocols: [h2] on
the enclosing TLS client. The handshake must negotiate h2. Omitted or explicit
http_version: h1 preserves legacy behavior: it sends HTTP/1.1 without checking
the negotiated ALPN. Set alpn_protocols: [http/1.1] on the enclosing TLS client
when the peer requires ALPN to match the HTTP version. Cleartext H2 uses prior
knowledge. See examples/http2-proxy.yaml.
SOCKS5
protocol:
type: socks
username: string?
password: string?
Shadowsocks
protocol:
type: shadowsocks
cipher: string
password: string
Snell
protocol:
type: snell
cipher: string
password: string
VMess
protocol:
type: vmess
cipher: string
user_id: string
h2mux: # Optional h2mux multiplexing (see below)
max_connections: 4
min_streams: 4
max_streams: 0
padding: false
Note: VMess AEAD mode is always enabled. The legacy aead field is deprecated.
VLESS
protocol:
type: vless
user_id: string
h2mux: # Optional h2mux multiplexing (see below)
max_connections: 4
min_streams: 4
max_streams: 0
padding: false
Trojan
protocol:
type: trojan
password: string
shadowsocks: # Optional
cipher: string
password: string
h2mux: # Optional h2mux multiplexing (see below)
max_connections: 4
min_streams: 4
max_streams: 0
padding: false
H2MUX Multiplexing
H2MUX multiplexes multiple proxy streams over a single HTTP/2 connection, reducing connection overhead. Compatible with sing-box. Available for VMess, VLESS, and Trojan client protocols.
h2mux:
max_connections: 4 # Maximum connections to maintain (default: 4)
min_streams: 4 # Min streams before opening new connection (default: 4)
max_streams: 0 # Max streams per connection, 0 = unlimited (default: 0)
padding: false # Enable padding for traffic obfuscation (default: false)
Server support: H2MUX is auto-detected on servers. No configuration needed.
TLS Client
protocol:
type: tls
verify: true # Default: true
server_fingerprints: [string]
sni_hostname: string
alpn_protocols: [string]
tls_buffer_size: int
cert: string # Client certificate for mTLS
key: string # Client key for mTLS
vision: false # Enable Vision (requires VLESS inner protocol)
key_exchange_groups: [X25519MLKEM768] # Optional; see TLS Key Exchange below
protocol: ClientProxyConfig
TLS Key Exchange
key_exchange_groups is an optional, ordered allowlist for ordinary TLS clients,
TLS server targets (including default_tls_target), and client/server
quic_settings. It applies to that TLS layer, regardless of the inner proxy
protocol. Supported names are case-sensitive:
X25519MLKEM768: native rustls X25519 + ML-KEM-768 hybrid.SecP256r1MLKEM768: native rustls P-256 + ML-KEM-768 hybrid.X25519,SecP256r1,SecP384r1: classical groups.
Omitting the field preserves rustls defaults: currently X25519+ML-KEM-768 is
preferred, with classical fallback. An explicit list replaces those defaults;
the first client group normally supplies the initial key share. Empty lists, duplicates,
null, unknown names, standalone ML-KEM, and SecP384r1MLKEM1024 are rejected.
No custom cryptographic implementations are used.
Client QUIC allowlists require a non-direct client at the first chain hop
(hop 0), where Shoes creates the QUIC transport. Explicit allowlists on direct
clients or later hops are rejected, including entries resolved from groups or
pools. Use ordinary TLS in a later hop's protocol when that hop needs its own
hybrid-protected tunnel.
To require hybrid exchange without classical fallback:
key_exchange_groups: [X25519MLKEM768, SecP256r1MLKEM768]
Hybrid-only lists force TLS 1.3 and disable early application data. TLS 1.3
resumption remains enabled and still performs a fresh exchange using an allowed
hybrid group. Peers offering only classical groups or TLS 1.2 cannot connect.
For opportunistic hybrid exchange with explicit classical fallback, use a mixed
list such as [X25519MLKEM768, X25519, SecP256r1].
Hybrid-only lists cannot be combined with vision: true on the same TLS layer:
Vision direct copy bypasses that layer's encryption. They also cannot be
combined with TUIC zero_rtt_handshake: true. Ordinary outer TLS or QUIC can
still protect a separate inner Vision connection. The setting does not apply to
REALITY, VLESS encryption, or ShadowTLS camouflage handshakes; it is rejected on
ShadowTLS configurations and with deprecated shadowtls_password.
Hybrid key exchange protects session-key establishment, not certificate
signatures. Certificate verification, fingerprints, and mTLS work as before.
verify: false without a trusted fingerprint still permits active impersonation.
Reality Client
protocol:
type: reality
public_key: string # Server's X25519 public key (base64url)
mldsa65_verify: string? # Optional pinned 1952-byte ML-DSA-65 key (unpadded base64url)
short_id: string # Your client ID (hex, 0-16 chars)
sni_hostname: string # SNI to send (must match server's reality_targets key)
cipher_suites: [string] # Optional TLS 1.3 cipher suites (see below)
vision: false # Enable Vision (requires VLESS inner protocol)
protocol: ClientProxyConfig # Inner protocol (typically VLESS)
Reality cipher suites: Valid values are TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256. If not specified, all three are offered/supported.
Reality offers X25519MLKEM768 first, with X25519 fallback. Servers follow the camouflage target's selected group; choose a PQ-preferring TLS 1.3 target to negotiate hybrid key exchange. Legacy X25519 clients remain supported. Debug logs report the authenticated connection's negotiated_group.
For Xray servers, use v25.5.16 or newer. Xray v25.4.30 and earlier cannot synthesize a hybrid-selected target handshake: when a PQ-capable target selects X25519MLKEM768, those servers forward to the real target and Shoes rejects its certificate. X25519 fallback does not prevent this failure because the target has already selected the hybrid group.
mldsa65_seed enables optional server signing. Clients with mldsa65_verify require a matching signature and reject unsigned or incorrectly signed connections. These fields correspond to Xray's mldsa65Seed and mldsa65Verify; neither replaces the existing X25519 key. Omit the fields to disable this extra authentication; empty or incorrectly sized values are invalid.
Enable server signing before distributing verification keys. The camouflage certificate flight must have room for the extra 3309-byte signature and DER overhead; insufficient capacity falls back to the real target instead of silently removing the signature. Removing signing later requires coordinating clients that already pin its key. Vision's direct mode relies on the inner TLS connection for payload encryption, not the outer Reality hybrid exchange.
ShadowTLS Client
protocol:
type: shadowtls
password: string
sni_hostname: string? # Optional SNI override
protocol: ClientProxyConfig
WebSocket Client
protocol:
type: websocket
matching_path: string?
matching_headers:
header_name: string
ping_type: ping-frame # disabled | ping-frame | empty-frame
protocol: ClientProxyConfig
Port Forward (No-op)
protocol:
type: portforward # Aliases: noop
Passes through the raw connection without protocol wrapping. Useful for testing or transparent proxying.
AnyTLS Client
protocol:
type: anytls
password: string # User password
udp_enabled: true # Default: true (enables UDP over TCP)
padding_scheme: [string]? # Optional custom padding scheme
NaiveProxy Client
protocol:
type: naiveproxy # Aliases: naive
username: string # Basic Auth username
password: string # Basic Auth password
padding: true # Default: true (enables padding protocol)
NaiveProxy retains its own HTTP/2 client transport policies, independent of
http_version: h2 on standard HTTP proxies: 256 KiB stream and connection
receive windows, the H2 library's 16 MiB response-header decoding limit, and no periodic
HTTP/2 PING watchdog. Peer stream limits and timeouts still apply. In particular,
Caddy v2.11.7 defaults to a one-minute request-body idle timeout, which can end a
quiet CONNECT tunnel; connection-level PINGs do not reset that timeout.
Rules System
Rules determine how incoming connections are routed.
Rule Config
rules:
- masks: string | [string] # IP/CIDR or hostname masks
action: allow | block
# For action: allow
override_address: string? # Optional address override
client_chain: ClientChain | [ClientChain] # Proxy chain(s) for routing
Client Chains
Client chains define how traffic is routed through upstream proxies. Each chain is a sequence of "hops" - proxies that traffic passes through in order.
# Single proxy (simplest form)
client_chain: my-proxy-group # Reference a named group
client_chain: # Or inline config
address: "proxy.example.com:1080"
protocol:
type: socks
# Multi-hop chain (traffic goes: client -> hop1 -> hop2 -> target)
client_chain:
chain:
- first-proxy-group
- second-proxy-group
# Multiple chains (round-robin selection)
client_chains:
- us-proxy-group # Chain 1: single hop
- chain: [proxy1, proxy2] # Chain 2: multi-hop
# Load balancing at a hop (pool)
client_chain:
chain:
- pool: [us-proxies, eu-proxies] # Round-robin between pool members
- final-proxy
Migration note: The client_proxy / client_proxies fields still work but are deprecated. Please migrate to client_chain / client_chains.
Mask Syntax
# IP/CIDR masks
masks: "0.0.0.0/0" # All IPv4
masks: "::/0" # All IPv6
masks: "192.168.0.0/16" # Subnet
masks: "10.0.0.1:80" # Specific IP and port
# Hostname masks
masks: "*.google.com" # Wildcard subdomain
masks: "example.com" # Exact match
# Multiple masks
masks:
- "192.168.0.0/16"
- "10.0.0.0/8"
- "*.internal.com"
Built-in Rule Groups
allow-all-direct- Allow all connections, direct routingblock-all- Block all connections
Example Rules
rules:
# Direct connection for local networks
- masks: ["192.168.0.0/16", "10.0.0.0/8", "172.16.0.0/12"]
action: allow
client_chain:
protocol:
type: direct
# Block specific domains
- masks: ["*.ads.example.com", "tracking.example.com"]
action: block
# Route through upstream proxy
- masks: "0.0.0.0/0"
action: allow
client_chain:
address: "proxy.example.com:1080"
protocol:
type: socks
Named Groups
Client Proxy Group
- client_group: my-upstream
client_proxies: # Define proxies in this group
- address: "proxy1.example.com:1080"
protocol:
type: socks
- address: "proxy2.example.com:1080"
protocol:
type: socks
# Reference in rules
- address: "0.0.0.0:8080"
protocol:
type: http
rules:
- masks: "0.0.0.0/0"
action: allow
client_chain: my-upstream # Reference by name
Rule Group
- rule_group: standard-rules
rules:
- masks: ["192.168.0.0/16"]
action: allow
client_chain:
protocol:
type: direct
- masks: "0.0.0.0/0"
action: allow
client_chain: my-upstream
# Reference in server config
- address: "0.0.0.0:8080"
protocol:
type: http
rules: standard-rules # Reference by name
Named PEMs
Define certificates once and reference throughout configuration.
# From file
- pem: my-cert
path: /path/to/certificate.pem
# Inline data
- pem: my-key
data: |
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
# Reference in config
- address: "0.0.0.0:443"
protocol:
type: tls
tls_targets:
"example.com":
cert: my-cert # Reference by name
key: my-key
protocol:
type: http
Advanced Features
Vision (XTLS-Vision)
Vision optimizes TLS-in-TLS scenarios by detecting inner TLS traffic and switching to direct mode for zero-copy performance.
Requirements:
- Inner protocol MUST be VLESS
- Works with both TLS and Reality
# TLS + Vision
protocol:
type: tls
tls_targets:
"example.com":
cert: cert.pem
key: key.pem
vision: true
alpn_protocols: ["http/1.1"]
protocol:
type: vless
user_id: "uuid"
# Reality + Vision
protocol:
type: tls
reality_targets:
"www.google.com":
private_key: "..."
short_ids: ["..."]
dest: "www.google.com:443"
vision: true
protocol:
type: vless
user_id: "uuid"
XUDP Multiplexing
Automatically enabled for VMess and VLESS when udp_enabled: true. Multiplexes UDP traffic over a single connection.
Proxy Chaining
Protocol nesting (wrap one protocol in another):
client_chain:
address: "proxy.example.com:443"
protocol:
type: tls
protocol:
type: vmess
cipher: aes-128-gcm
user_id: "uuid"
Multi-hop chains (route through multiple proxies sequentially):
client_chain:
chain:
- address: "proxy1.example.com:1080"
protocol:
type: socks
- address: "proxy2.example.com:443"
protocol:
type: tls
protocol:
type: vless
user_id: "uuid"
Hot Reloading
Configuration changes are automatically detected. --no-reload disables file watching; on Unix, SIGHUP still requests a reload.
On Linux, if directory permissions prevent watching a readable configuration's parent, startup falls back to direct file watches and logs a warning. In-place edits remain detectable, but atomic replacements may be missed; use SIGHUP or grant directory read permission when replacing files.
Replacement configurations, certificates, and DNS resolvers are prepared while the current listeners continue serving. A preparation failure leaves the current configuration running. Once preparation succeeds, listeners are replaced. A failure to activate replacements exits with status 1; there is no rollback after listeners are retired. Existing TCP connections may drain for global_limits.reload_grace_secs during successful reloads; QUIC connections are closed.
QUIC listener sockets must finish closing before replacements bind. Closing endpoints get up to one second for best-effort close delivery, then their drivers are stopped locally so peer-controlled timers cannot delay reloads. A five-second socket-release guard remains; exceeding it indicates a local cleanup failure and exits with an error rather than announcing a partially ready generation.
Candidate QUIC buffer sizes are used during DNS preparation, but global_limits admission caps change only at activation. DNS bootstrap over HTTP/3 or QUIC proxy chains therefore needs headroom under the live cap. A cap-increasing reload may require a restart if preparation cannot fit. Lowering a cap does not revoke existing reservations, including those made during preparation; new admissions are blocked until usage falls sufficiently. At cold startup, preparation uses the initial unlimited admission caps.
On macOS, edit the configured file path or use SIGHUP when writing through a hard-link alias in another directory. Automatic detection of such alias writes is backend-dependent. System-volume directory symlinks are resolved without watching their immutable links.
SIGINT/SIGTERM (or Ctrl-C on Windows) stop listeners and exit with status 0. Process shutdown does not wait for the TCP reload grace period.
mTLS (Mutual TLS)
Require client certificates for authentication:
# Server side
protocol:
type: tls
tls_targets:
"example.com":
cert: server.crt
key: server.key
client_ca_certs: [ca.crt] # Required CA
client_fingerprints: ["sha256:..."] # Optional specific certs
protocol: ...
# Client side
client_chain:
address: "example.com:443"
protocol:
type: tls
cert: client.crt
key: client.key
protocol: ...
Command Line
shoes [OPTIONS] <config.yaml> [config.yaml...]
OPTIONS:
-t, --threads NUM Worker threads (default: CPU count)
-d, --dry-run Validate config and certificates without starting listeners
--no-reload Disable automatic reloads on file changes
COMMANDS:
check <config.yaml> Alias for --dry-run
version Alias for --version
generate-reality-keypair Generate Reality X25519 keypair
generate-reality-mldsa65-keypair Generate optional Reality ML-DSA-65 keys
generate-shadowsocks-2022-password <cipher> Generate Shadowsocks 2022 password
check and --dry-run exit with status 0 for valid configuration and 1 for errors. They do not open listeners, create a file watcher, or perform DNS bootstrap networking. Runtime failures such as unavailable bind addresses remain possible after a successful check.
Tips
Generate Keys
Reality keypair:
shoes generate-reality-keypair
Optional Reality post-quantum authentication keys:
shoes generate-reality-mldsa65-keypair
Keep mldsa65_seed on the server; distribute only mldsa65_verify to clients.
Shadowsocks 2022 password:
shoes generate-shadowsocks-2022-password 2022-blake3-aes-256-gcm
UUID:
uuidgen
TLS certificate fingerprint:
openssl x509 -in cert.pem -noout -fingerprint -sha256
Security Best Practices
- Use strong, random passwords
- Keep private keys secure
- Use
127.0.0.1instead of0.0.0.0for local-only access - Use firewall rules to restrict access
- Enable client certificate authentication for sensitive services
- Use Vision with Reality for maximum privacy
Performance Tips
- Enable
vision: truefor TLS-in-TLS scenarios - Use
tcp_settings.no_delay: truefor low latency - Set
quic_settings.num_endpointsto match worker threads - Use QUIC transport for high-latency or lossy networks
Common Issues
- "Address already in use": Change port or stop conflicting service
- "Permission denied": Ports < 1024 require root/admin
- Reality connection fails: Verify keys match, UUID matches, SNI matches server's reality_targets key
- Vision not working: Ensure inner protocol is VLESS
- Config validation fails: Run with
--dry-runfor detailed errors