Add standard HTTP/2 CONNECT to HTTP and mixed listeners and opt-in HTTP proxy clients, allowing concurrent TCP tunnels to share a connection while preserving legacy HTTP/1 and NaiveProxy behavior. Keep the H2 transport independent of NaiveProxy, preserve route isolation and per-stream authentication, require matching TLS ALPN for explicit H2 clients, and handle flow control, cancellation, and graceful shutdown. Include configuration documentation, an example, compatibility regressions, and interoperability coverage for sing-box, Mihomo, and the native Caddy-based Naive server.
32 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
- Client Config
- Client Protocols
- Rules System
- Named Groups
- Named PEMs
- Advanced Features
- Command Line
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
- 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