Files
shoes/CONFIG.md
T
cfal 001495285a Add HTTP/2 CONNECT proxy support (#184)
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.
2026-10-16 17:54:08 -04:00

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

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_fd from VpnService.Builder.establish(). Routes configured via VpnService.
  • iOS: Use device_fd from NEPacketTunnelProvider.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 routing
  • block-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.1 instead of 0.0.0.0 for 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: true for TLS-in-TLS scenarios
  • Use tcp_settings.no_delay: true for low latency
  • Set quic_settings.num_endpoints to 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-run for detailed errors