Overview

Orange uses TOML format for configuration. The configuration file controls all aspects of the network engine including DNS resolution, proxy services, routing rules, and TUN mode settings.

Quick start

Copy the bundled example to start from a known-good baseline, then edit as needed.

Copy example-config.toml to config.toml
cp example-config.toml config.toml

File locations

config_dir state_dir

Every path in this reference resolves against one of two roots, and they are not the same directory. Files you supply resolve against the directory holding the config file. Files Orange manages for itself resolve against the state directory. Passing --config changes where the config is read from — it does not move the state directory.

RootTypeWhat resolves against itFields
config_dir
input
Files you provide to Orange.
dataset paths, hosts_file, cert_path, key_path
state_dir
managed
Files Orange creates and owns.
log_file, the Fake-IP store, mesh-identity.key, the mesh control state database, admin.token, cache/datasets/
Where the config file is read from
ProcessTypeDefault path—
root
system
/var/lib/orange/config.toml
—
non-root, $XDG_STATE_HOME set
user
$XDG_STATE_HOME/orange/config.toml
—
non-root, otherwise
user
$HOME/.local/state/orange/config.toml
—

Explicit overrides win in this order: --config <path>, then the ORANGE_CONF_PATH environment variable, then the default above. orange mesh init-server --public-endpoint <host> writes its generated config to the same default location, which --state-dir <path> or ORANGE_STATE_DIR can redirect.

Managed paths are validated rather than trusted. An explicit one may not be empty, may not carry leading or trailing whitespace, and may not contain ..; an absolute path must still land inside the state directory, and an explicit state_dir must itself be absolute. Resolution and read-only validation never create directories — a directory appears only when something is genuinely written to it.

Config version

version

A top-level integer, required in every config, that must appear before the first table. The runtime accepts only an exact match for the version it was built against — missing, older, or newer all fail at startup rather than being interpreted loosely.

OptionTypeDescriptionCurrent
version
integer
Config schema version. Must be the first key, before any [table].
1
The whole schema is strict: unknown fields are rejected, not ignored. A typo in a key name stops startup instead of silently disabling the setting you thought you configured. Every listener also defaults to enabled = false — nothing opens a port unless you say so.

General

[general]

Global settings for the Orange engine including logging and IP address family policy. Both log_level and ip_version are restart-required — a reload will report them rather than apply them, which is worth knowing before you try to raise the log level on a running process.

Option Type Description Default
log_level
string
Log level: error | warn | info | debug | trace
error: production, info: default, debug: development, trace: profiling
"info"
log_fileoptional
string
File path for JSON-formatted log output. Logs are also written to the console. Relative paths resolve against the state directory, and an absolute path must stay inside it. The directory is created automatically if missing. The active file is capped at 5 MiB; past that it is compacted within the same inode, keeping up to 2 MiB of the most recent complete lines. Long-term archiving and retention stay with the host or deployment system.
—
ip_versionoptional
string
IP address family policy. Controls DNS record types queried and Happy Eyeballs connect order.
dual: both A + AAAA, RFC 8305 default order (IPv6 first, parallel IPv4 after 250ms).
prefer_ipv4: dual-stack resolve, force IPv4 first for connect.
prefer_ipv6: dual-stack resolve, prefer IPv6 primary IP.
ipv4: A records only, IPv4 only.
ipv6: AAAA records only, IPv6 only.
"dual"
Note: dns.rules host overrides are explicit user mappings and are not constrained by ip_version. URL host literals (IPv4/IPv6 written directly in URLs) that conflict with the policy are rejected.

DNS listener

[listeners.dns]

Where DNS queries enter Orange. Every listener lives under [listeners.*]; resolver behaviour is configured separately in [dns]. Changing a listener needs a restart — sockets and TLS are fixed at startup.

OptionTypeDescriptionDefault
enabled
boolean
Serve DNS on UDP and TCP.
false
bind_addr
string
Listen address, IP:Port.
"127.0.0.1:11223"
concurrent_limit
integer
Maximum queries handled concurrently.
1024
max_stream_message_size
integer
Per-message ceiling for TCP, DoT and DoH streams, in bytes. Valid range 512–65535.
4096

DNS rate limit

[listeners.dns.rate_limit] optional

Per-source-address rate limiting on the DNS listener. Omit the table to leave it off.

OptionTypeDescriptionDefault
max_requests
integer
Requests allowed per source within one window.
—
window_ms
integer
Window length in milliseconds.
—
max_entriesoptional
integer
Maximum tracked source addresses, bounding the limiter's own memory.
10000

DNS over TLS

[listeners.dns.dot] optional

Serve DoT to clients. Certificate and key paths resolve against the config file directory.

OptionTypeDescriptionDefault
enabled
boolean
Serve DNS over TLS.
false
bind_addr
string
Listen address, conventionally port 853.
—
cert_path
string
Certificate file. Only needs to be a readable regular file.
—
key_path
string
Private key. Must be a regular 0600 file owned by the running user, inside a 0700 directory, and must not be a symlink.
—
certbot layouts do not work directly. The live/ directory is a symlink tree and ssl-cert group-readable keys are 0640 — both are rejected. Install the key as a daemon-owned snapshot in your renewal hook, then restart Orange; certificates are read once at startup.

DNS over HTTPS

[listeners.dns.doh] optional

Serve DoH to clients, over HTTP/2 or HTTP/1.1. Both GET (RFC 8484 base64url parameter) and POST are accepted.

OptionTypeDescriptionDefault
enabled
boolean
Serve DNS over HTTPS.
false
bind_addr
string
Listen address, conventionally port 443.
—
cert_path / key_path
string
Same certificate contract as DoT, including the 0600 / 0700 / no-symlink rules on the key.
—
pathoptional
string
HTTP path that answers queries.
"/dns-query"

Proxy listeners

[listeners.proxy.http] [listeners.proxy.socks] [listeners.proxy.mixed]

Three inbound proxy entry points, each with the same three fields. Mixed detects HTTP or SOCKS on a single port, so one listener can serve both. Proxy routing behaviour lives in [proxy.routing], not here.

OptionTypeDescriptionDefault
enabled
boolean
Serve this listener.
false
bind_addr
string
Listen address. HTTP handles both plain forwarding and CONNECT; SOCKS5 handles TCP and UDP ASSOCIATE.
—
concurrent_limitoptional
integer
Maximum concurrent inbound connections for this listener.
512
Inbound authentication [listeners.proxy.auth]
OptionTypeDescriptionDefault
username
string
Shared across all three proxy listeners. Must not be blank.
—
password
string
Must not be blank. SOCKS5 enforces it per RFC 1929; HTTP uses Proxy-Authorization.
—
Binding a proxy listener to anything other than loopback exposes an open proxy on that interface. Configure [listeners.proxy.auth] and a firewall rule before changing bind_addr away from 127.0.0.1.

DNS resolver

[dns]

How queries are answered, independent of where they arrive. The resolution order is: dns.rules reject or host mapping, then the hosts file, then MagicDNS when mesh is on, then Fake-IP, then cache, then upstream. Ports and TLS for the listener are in [listeners.dns].

OptionTypeDescriptionDefault
fallback_to_system_dnsoptional
boolean
Fall back to the OS resolver when every configured upstream fails. Keep it false when Orange is the system resolver, or the lookup recurses into itself.
false
hosts_fileoptional
string
System hosts file to load, re-read every 5 seconds when it changes. An empty string disables it explicitly.
Enabled with /etc/hosts on macOS and Linux; off on iOS and Android, where the sandbox cannot read it.
"/etc/hosts"
cache_sizeoptional
integer
Cached entries. 500 is a reasonable figure for memory-constrained mobile builds.
1000
An expired answer keeps serving while it refreshes. When a cached record passes its TTL, the next request doesn't wait for a fresh lookup — Orange answers from cache for a bounded grace window while a refresh runs in the background. The window is deliberately capped: even with an upstream down, you are never pinned to an old address long enough to miss a CDN or hosting change, and once it closes the entry is dropped rather than served. /api/v1/dns/cache/status reports how many entries are currently in that window.
In TUN mode, fallback_to_system_dns is a leak path. The system's own lookups can be routed back into Orange — typical on macOS and iOS Network Extensions. Even without recursing, those queries bypass dns.rules entirely. If you must enable it, use tun.exclude_routes to keep the system resolver on the physical interface.

DNS reverse cache

[dns.reverse_cache] optional

IP-to-domain reverse lookup cache. Used by TUN mode to recover the original hostname from a destination IP so that domain-based routing rules and logging still work after DNS has been resolved. Set either threshold to 0 to disable.

OptionTypeDescriptionDefault
per_ip_max
integer
Maximum records retained per IP. The least-recently-accessed entries are evicted first. Set to 0 to disable reverse caching.
16
global_max
integer
Global record cap that bounds total memory. When it is reached, Orange trims per-IP retention so the total stays within the cap. On memory-constrained mobile builds: 2000. Set to 0 to disable.
10000

DNS bootstrap

[dns.bootstrap]

DNS servers used to resolve infrastructure hostnames (upstream proxies, DoH/DoT providers, WireGuard endpoints). Bootstrap queries are forced to bypass the TUN tunnel and go out on the physical interface, preventing circular routing. Static hostname-to-IP mappings for infrastructure are now unified under [dns.rules].

OptionTypeDescriptionDefault
servers
string[]
Bootstrap DNS servers. Must be literal IPs, not hostnames, to avoid circular resolution. Format: IP (port 53) or IP:Port. Defaults include both IPv4 and IPv6 addresses for NAT64 / IPv6-only networks.
["1.1.1.1:53", "8.8.8.8:53", "[2606:4700:4700::1111]:53", "[2001:4860:4860::8888]:53"]

DNS local network

[dns.local_network] optional

Opts local-network domains (mDNS, LAN suffixes, service discovery) out of upstream DNS so they stay on the local link and do not leak to external resolvers. Matched queries return NoError + empty Answer (not NXDOMAIN) so that RFC 2308 negative caching does not block subsequent mDNS attempts.

OptionTypeDescriptionDefault
bypass_private_ptr
boolean
Intercept PTR queries for private IP ranges so LAN addresses are never leaked to upstream DNS.
true
bypass_suffixes
string[]
Domain patterns to bypass. Suffix match (.local) matches anything ending in .local; mid-string pattern match (._dns-sd.) matches anything containing the substring. Set to [] to disable.
[".local", ".loc", "._dns-sd."]
Client queries only. This section never touches the bootstrap resolver, which is what resolves control-plane names — WireGuard endpoints, DoH/DoT upstreams. Bootstrap carries its own non-configurable table holding just the two suffixes an RFC forbids a stub resolver from sending upstream: .local (RFC 6762 §3, must go to mDNS) and .invalid (RFC 6761 §6.4, must answer negatively at once). So adding a suffix here can never cut off your own VPN reconnect.
Two names are deliberately absent from the defaults. localhost is left out because the correct behaviour is to answer with loopback, not to refuse (RFC 6761 §6.3) — point control-plane names straight at 127.0.0.1 or ::1 instead. home.arpa is left out because RFC 8375 §4 forbids a resolver from treating it specially — put your home router (say 192.168.1.1) in dns.bootstrap.servers and a name like vpn.home.arpa resolves normally, since an NXDOMAIN from a public server in that list is skipped automatically.

DNS rules

[dns.rules]

Unified DNS routing table. The same rule set applies to every inbound (TUN, HTTP, SOCKS). Shorthand syntax covers reject and simple host mappings; the full { ... } form is required for DNS upstreams, conditional matches, and rewrites. Infrastructure hostnames (upstream proxies, DoH/DoT providers, WireGuard endpoints) should use { host = "..." } mappings here so they resolve without a circular lookup.

Match patterns
PatternTypeMeaningExample match
"example.com"
exact
Exact match — only matches example.com.
example.com
"*.example.org"
suffix + root
Matches example.org itself and every subdomain, at any depth.
example.org, a.b.example.org
"+.example.org"
suffix only
Subdomains only, at any depth — the apex example.org is not matched. More specific than *. of the same length, so it wins when both are present.
a.example.org, a.b.example.org
".example.org"
deprecated
Do not use. A leading dot is not stripped, so the pattern matches nothing and the rule is silently inert. Write *.example.org or +.example.org instead.
— nothing
"@geosite:<tag>"
geosite
Matches every domain in the named GeoSite category.
Dataset entries
"default"
fallback
Fallback rule used when no pattern matches.
*
Rule values

A value is either a shorthand string, a single object, or an array of objects evaluated top to bottom until one matches.

FieldTypeDescriptionExample
"reject"
shorthand
Reject the query, answering NXDOMAIN. Equivalent to { reject = true }.
"*.ads.com" = "reject"
"<ip>"
shorthand
A bare IP is a hosts mapping, not an upstream. To point at a DNS server you must use the full { to = [...] } form.
"router.home" = "192.168.1.1"
host
string
Answer with this address directly. Use it to pin infrastructure hostnames — proxy servers, DoH/DoT providers, WireGuard endpoints — so resolving them never depends on the thing they are needed for.
{ host = "203.0.113.10" }
to
string[]
Upstream DNS servers. Several entries are queried concurrently and the first valid answer wins.
{ to = ["8.8.8.8", "1.1.1.1"] }
viaoptional
string
Send the query through a named outbound instead of directly — typically a WireGuard tunnel whose far side hosts the internal resolver. A pool name works here too, in which case one member is chosen per logical query and reused for every upstream in that query.
{ to = [...], via = "wg-home" }
reject
boolean
Full form of the reject shorthand.
{ reject = true }
whenoptional
table
Condition. Currently one field: src, a source CIDR supporting negation with ! and multiple values separated by commas.
{ when = { src = "10.0.0.0/8" } }
rewriteoptional
string
Rewrite the suffix before querying upstream, then reverse it on the answer — useful when a router serves .loc while clients ask for .local. Must begin with . and is only valid on a wildcard pattern.
rewrite = ".loc"
The "default" entry is the fallback every unmatched query lands on. It has to be written in the full form — { to = ["8.8.8.8"] } — because a bare IP string is read as a hosts mapping, never as an upstream.
Conditions

Rule values can be a shorthand string ("reject" or a literal IP hosts mapping), a single full-form object, or an array of objects evaluated top-to-bottom. Conditions use the when object; today the supported field is src (CIDR, supports negation with ! and multi-value with ,).

dns.rules examples TOML
# Shorthand — reject
"*.ads.com" = "reject"

# Shorthand — hosts mapping
"example.com" = "127.0.0.1"

# Full form — hosts mapping
"example.com" = { host = "127.0.0.1" }

# DNS upstream
"@geosite:example-tag" = { to = ["9.9.9.9"] }

# Multiple upstreams (concurrent)
"example.com" = { to = ["8.8.8.8", "1.1.1.1"] }

# With specific outbound
"*.internal" = { to = ["10.0.0.53"], via = "wg-home" }

# Conditional rules (by source IP)
"*.corp" = [
  { when = { src = "10.0.0.0/8" },  to     = ["10.0.0.53"] },
  { when = { src = "!10.0.0.0/8" }, reject = true },
]

# Suffix rewrite — ask the router for .loc while the client queries .local
# rewrite requires a wildcard pattern (*.local / +.local) and must start with '.'
"*.local" = [
  { when = { src = "192.168.0.0/16" }, to = ["192.168.1.2"], rewrite = ".loc" },
]

# Default upstream (required)
"default" = { to = ["8.8.8.8"] }

Fake-IP

[dns.fake_ip] optional

Assigns virtual IPs from 198.18.0.0/15 to hostnames so that clients can start a connection immediately; Orange then recovers the hostname at the TUN/proxy boundary to make routing decisions. Best suited to TUN transparent proxy mode. IPv4 only — AAAA queries return empty responses, and the 198.18.0.0/15 range must not be used for anything else.

OptionTypeDescriptionDefault
enabled
boolean
Enable Fake-IP.
false
store_path
string
Persistent storage path. Relative paths resolve against the state directory, and an absolute path must stay inside it. Omit it and Orange uses its own managed filename inside that directory.
managed
save_interval_secs
integer
Auto-save interval in seconds. 0 disables the timer; saves still happen when changes accumulate.
60
max_entries
integer
Maximum in-memory mappings. Overflow falls back to normal DNS resolution.
100000
expire_days
integer
Mappings not accessed for this many days are cleaned up.
15
access_update_interval
integer
Minimum seconds between last_access write-backs. Larger values reduce IO pressure.
3600
mode
string
blacklist: every hostname uses Fake-IP except those matched by rules.
whitelist: only hostnames matched by rules use Fake-IP.
"blacklist"
rules
string[]
Domain patterns, interpreted according to mode. Same syntax as dns.rules and proxy.rules: exact (example.com), subdomains only (+.example.com), root plus subdomains (*.example.com), or @geosite:tag. A bare leading dot such as .local is invalid — the dot is not stripped, so the rule matches nothing.
Blacklist mode already skips *.local, *.lan, *.home.arpa, *.localhost, *.invalid, *.test, *.internal, *.intranet and *.localdomain.
—

Proxy core

[proxy]

A container table. Everything that shapes proxy behaviour lives in its own sub-table: routing policy in [proxy.routing], outbound TLS and connection pooling under [proxy.outbound.*], upstreams and pools in their own arrays. Inbound ports and inbound authentication are not here — they are listeners.

Routing policy

[proxy.routing]

Decides how ordinary proxy data flows are routed. This never changes how DNS queries themselves are resolved — the DNS listener, DoT and DoH paths are unaffected by anything here.

OptionTypeDescriptionDefault
mode
string
rules: match against proxy.rules.
direct: ignore the rules, connect everything directly.
reject: ignore the rules, refuse everything.
upstream: ignore the rules, send everything to one upstream.
"rules"
upstream
string
Required when mode = "upstream". Accepts an upstream name or a pool name.
—
resolve_for_ip_rules
string
When the destination is a hostname, an ip_cidr or geoip rule can only match it once a lookup has happened. This decides whether Orange makes that lookup.
always: resolve every hostname before matching, so IP rules always have an address to work with.
on_demand: resolve only when it could change the outcome — that is, when an IP rule capable of triggering a lookup sits above the first rule that matches without one.
never: no lookup before matching, so IP rules apply only to destinations whose address is already known.
"always"
rule_match_cache_size
integer
Cached rule-match results. 0 is treated as 1 rather than disabling the cache. 2000 suits memory-constrained mobile builds.
10000
Skipping the lookup is a privacy feature, not just a latency one. Under never — or under on_demand when a domain rule matches first — a hostname bound for a name-addressed upstream (HTTPS CONNECT, Trojan, Shadowsocks) is never resolved locally. The upstream resolves it instead, so the name never reaches your local resolver and you save a round trip while connecting. UDP behaves the same way as TCP. WireGuard is different: it addresses packets by IP, so those targets are always resolved once the route is decided.
An address Orange already knows — an IP written literally, a dns.rules host mapping, or the real destination that TUN traffic carries — takes part in matching under every setting. This option only governs whether Orange performs a lookup it doesn't already have an answer for. And once a lookup does happen, matching restarts from the top of the list with the address in hand, so an IP rule you placed earlier still gets its turn.
Tuning on_demand: find the trigger line. One rule decides everything — the first IP rule in effective order that you haven't marked resolve_ip = false. Destinations matched above it are answered with no lookup; reaching it means a lookup happens first. Orange logs which rule that is at startup and after every reload, so you can read it rather than guess. There are two ways to move it: put domain rules higher, or mark an IP rule that isn't worth a lookup — a broad private-range catch-all is the usual case — with resolve_ip = false. Coming from Clash or Surge, that marker plays the role of their no-resolve.
One behavioural difference to weigh before switching. When a target resolves to one of this machine's own interface addresses, Orange downgrades it to direct. Under always that check covers every hostname. Under on_demand, a hostname caught by a domain rule is never resolved — so there is no address to check, and it stays on the upstream that rule chose even if it would have resolved to a local address. That is the trade you accept in exchange for the skipped lookups.
Routing turns a Fake-IP back into its hostname before applying dns.rules reject and host overrides. That does not change the real DNS query pipeline — it only stops a Fake-IP literal from walking straight past a DNS blocklist.
mode = "upstream" overrides proxy.rules completely. If you also run a mesh, the direct rules that keep peers peer-to-peer are discarded along with everything else. To send most traffic upstream while keeping mesh direct, stay on mode = "rules" and end the rule list with a catch-all to = "...".

Outbound HTTP pool

[proxy.outbound.http_pool] optional

Connection reuse toward HTTP-based upstreams, so a new request does not pay for a fresh handshake.

OptionTypeDescriptionDefault
max_idle_per_upstream
integer
Idle connections kept per upstream.
32
idle_timeout_secs
integer
Close an idle connection after this many seconds.
30
max_hosts
integer
Maximum cached upstream hosts, evicted LRU. Bounds memory when the host set is large.
1024

Outbound TLS

[proxy.outbound.tls] optional

TLS verification for connections Orange makes to upstreams.

OptionTypeDescriptionDefault
allow_invalid_certs
boolean
Accept invalid or self-signed upstream certificates. Keep this false in production — it disables the check that stops an interceptor from impersonating your upstream. Intended for a lab, or a self-hosted server whose certificate you verified by hand.
false

Underlay socket policy

[underlay] optional

When Orange runs in TUN or VPN mode, its own outbound traffic — bootstrap DNS, upstream proxies, mesh control and relay — must not be captured by the tunnel it just created. This section decides how those sockets escape. HTTP and SOCKS-only deployments never need it.

OptionTypeDescriptionDefault
mode
string
disabled: no policy.
host: use only the interface or protect callback injected by the host app; never auto-detect.
auto_detect: prefer the host value, otherwise detect the default physical interface.
interface_name: pin by name — Linux and Android only.
interface_index: pin by index — macOS and iOS only.
"disabled"
interface_name
string
Only valid with mode = "interface_name"; setting it in any other mode is a startup error. Find it with ip link show.
—
interface_index
integer
Only valid with mode = "interface_index"; setting it in any other mode is a startup error. Find it with ifconfig.
—
Do not leave this disabled when TUN captures the whole internet. Orange's own upstream DNS queries get routed back into the TUN, DNS hijack catches them again, and every lookup times out. Bootstrap DNS and proxy upstream addresses are excluded automatically; dns.rules upstreams are not, because they may legitimately live on the far side of a tunnel. Desktop and server deployments generally want auto_detect. Orange logs a warning at startup when it sees this risky combination.
Restart required. The UDP and global socket pools capture this policy when they are built, so /reload reports underlay.* instead of applying it. Note that the pools' own rebuild after a network change is driven by network events, not by this setting.

Upstreams

[[proxy.upstreams]]

Each entry declares one upstream, named so that proxy.rules can reference it. Four types are supported: https, trojan, shadowsocks and wireguard. Any upstream can override the global probe with its own health_check block, or opt out with health_check = { disabled = true }. Changing this array requires a restart.

Credentials and private keys are stored in plaintext in this file. Protect it with chmod 600.
HTTPS

A CONNECT tunnel over TLS. Carries streams only — it has no datagram support, so a UDP flow matching a rule that points here is rejected with an ICMP Port Unreachable rather than being quietly re-routed. QUIC falls back to TCP on its own; UDP-only protocols such as NTP will fail.

HTTPS upstreamTOML
[[proxy.upstreams]]
type = "https"
name = "https-1"

[proxy.upstreams.config]
addr = "http-proxy.example.com:443"
auth = "user:pass"

# Optional H2 pool tuning — every field must be >= 1
[proxy.upstreams.config.h2]
soft_max_age_secs = 120  # retire a connection, existing streams continue
idle_timeout_secs = 30   # mark unusable after this idle period
max_streams       = 100  # concurrent streams per connection
max_conns         = 2    # connections accepting new streams
ping_interval_secs = 30   # keepalive PING, sent even when idle
ping_timeout_secs  = 15   # PING timeout only stops new requests
ready_timeout_secs = 3    # wait for a connection to become ready
response_timeout_secs = 10 # wait for the upstream CONNECT response
forward_response_timeout_secs = 300 # HTTP forward; covers SSE and long polling
No H2 field treats 0 as "disabled" — each has a pathological meaning, so 0 is rejected at startup. max_conns bounds connections that accept new streams; a retired connection pinned by a long-lived stream still exists, so the physical count has no constant ceiling. Watch orange_h2_connections_active if long-lived tunnels accumulate.
Trojan

TLS with a fixed request header. Supports TCP and UDP; a UDP flow matching a Trojan rule automatically uses UDP ASSOCIATE, which makes DNS-over-Trojan and QUIC-over-Trojan work.

Trojan upstreamTOML
[[proxy.upstreams]]
type = "trojan"
name = "trojan-1"

[proxy.upstreams.config]
addr     = "trojan.example.com:443"
password = "your-password-here"
# sni = "trojan.example.com"   # only when the TLS name differs from addr
An IPv6 literal addr must set sni explicitly. Without it the SNI falls back to the bare IP, which RFC 6066 forbids — the TLS stack may then silently succeed, fail the handshake, or return the wrong certificate.
Shadowsocks 2022

SIP022 only. Legacy Shadowsocks (AEAD-2017 and stream ciphers) is not supported and is rejected at startup. TCP and session-based UDP both work, with replay protection.

Shadowsocks upstreamTOML
[[proxy.upstreams]]
type = "shadowsocks"
name = "ss-1"

[proxy.upstreams.config]
addr   = "ss.example.com:8388"
method = "2022-blake3-aes-256-gcm"
psk    = "<32 random bytes, base64>"
# udp = true
OptionTypeDescriptionDefault
method
string
Either 2022-blake3-aes-128-gcm or 2022-blake3-aes-256-gcm, written in full including the prefix. 2022-blake3-chacha20-poly1305 is defined for UDP only and is rejected, because every upstream must currently provide a TCP stream.
—
psk
string
Base64 of fixed-length random bytes, not a passphrase — 16 bytes for aes-128, 32 for aes-256. Generate with openssl rand -base64 32. The SIP023 identity-PSK chain (ipsk) is not supported and is rejected.
—
udpoptional
boolean
Enable UDP relay for this upstream. Note that pool members must agree on this — see Pools.
true
If several Shadowsocks upstreams fail their handshake at once, check the system clock first — SIP022 carries a timestamp, so a skew of roughly half a minute is enough to break the handshake.
WireGuard

The configuration maps one-to-one onto a standard wg0.conf: [Interface] becomes config.interface, [Peer] becomes config.peer. One upstream describes one remote peer.

WireGuard upstreamTOML
[[proxy.upstreams]]
type = "wireguard"
name = "wg-home"

[proxy.upstreams.config]
# mode = "auto"      # auto | userspace | kernel

# [Interface] — this machine
[proxy.upstreams.config.interface]
private_key = "<base64 or 64-hex>"
address     = "10.0.0.2/24"
dns         = "10.0.0.1"
# mtu = 1420         # 576-9000; lower for PPPoE or IPv6 transport

# [Peer] — the remote end
[proxy.upstreams.config.peer]
public_key  = "<remote interface public key>"
endpoint    = "vpn.example.com:51820"
allowed_ips = "0.0.0.0/0,::/0"
OptionTypeDescriptionDefault
modeoptional
string
auto picks kernel mode on CLI and userspace on mobile. kernel is faster but needs privileges — CAP_NET_ADMIN and ip on Linux, root plus ifconfig and route on macOS. userspace needs no privileges but requires you to arrange routing yourself.
"auto"
interface.dnsoptional
string
DNS server inside the tunnel, IP or IP:Port. Mirrors [Interface] DNS in wg0.conf; drop the line entirely when the tunnel has no resolver of its own.
—
peer.allowed_ips
string
Destinations routed through the tunnel. "0.0.0.0/0,::/0" for everything, or a specific range such as "10.0.0.0/8".
—
The most common misconfiguration: putting your own public key in peer.public_key. That field is the remote interface's public key — the value under [Peer] PublicKey in your local wg0.conf. Your own public key belongs on the server. Orange detects this and refuses to start. There is deliberately no interface.public_key field, since it is derivable from the private key and a second copy could only ever disagree.

Upstream pools

[[proxy.pools]] optional

Groups several interchangeable upstreams under one name. A pool name can be used anywhere an upstream name can — proxy.rules to, proxy.routing.upstream, and dns.rules via all share one namespace, so a pool and an upstream may not have the same name.

OptionTypeDescriptionDefault
name
string
Pool name, referenced exactly like an upstream name.
—
members
string[]
Names of existing upstreams. Pools cannot nest. Members must share a protocol and the same effective capabilities — a pool mixing UDP-capable and UDP-disabled Shadowsocks members is rejected at startup, while a pool where no member supports UDP is fine.
—
strategyoptional
string
hash: hashes the normalised hostname, so one hostname keeps one exit IP — useful against destination-side risk controls, but it is stable sharding rather than load awareness, so a single hot hostname stays on one member.
round_robin: one step per logical unit — per TCP connection, per UDP flow, per DNS query. The cursor is shared across all listeners in the process.
"hash"
Selection is sticky per flow. A TCP connection picks once; a UDP flow picks once and stays on that member while it is active, so a QUIC or game session is never moved mid-stream. One SOCKS UDP association tracks at most 1024 concurrent targets — beyond that new flows are refused and their packets dropped, while flows already established keep running. The single exception to stickiness is TUN UDP: if the first packet carried no hostname and a later one reveals an SNI or reverse-lookup name, the route is recalculated once and the old transport session is evicted cleanly.
There is no mid-connection failover. If the chosen member fails to dial, the attempt fails — it is not retried on a sibling. Failed members are removed by health probing instead, which is why a pool without [proxy.upstream_health] enabled cannot avoid a dead member at all; Orange warns about that at startup.

Upstream health check

[proxy.upstream_health] optional

Actively probes upstream latency and availability. Results feed metrics, /api/v1/proxy/upstreams/health, and pool member selection. The values here are the global default; any upstream can override them with its own health_check block, or opt out with health_check = { disabled = true }. The whole section is restart-required — the probe registry and interval are built once at startup, so /reload returns proxy.upstream_health with a non-zero status rather than applying a change.

OptionTypeDescriptionDefault
enabled
boolean
Enable probing. Every configured upstream is probed, including standby nodes no rule currently references.
false
interval_secs
integer
Seconds between probes, 1–86400. Do not shorten this on mobile without evidence — each probe wakes the radio.
120
timeout_secs
integer
Seconds before a probe counts as failed, 1–3600.
5
url
string
Probe target. HTTP-based upstreams take an HTTP URL; WireGuard takes an IP or IP:Port, defaulting to port 443. Trojan and Shadowsocks are probed with a plain TCP connect to their address — no protocol handshake is sent.
"http://www.gstatic.com/generate_204"
Removal has hysteresis. A member leaves the pool only after two consecutive failed probes and returns only after two consecutive successes, so one transient blip does not shuffle your traffic. If probing stalls entirely the stale verdict stops being used as grounds for removal, and if every member ends up removed the full set is restored rather than failing all traffic.

Routing rules

[[proxy.rules]]

Decides what each connection does: stay direct, take a tunnel, or be rejected. A rule has exactly two parts — a when object holding every match condition, and one action. Unknown top-level keys are rejected at startup, so match fields written outside when will not load.

Rule shape
KeyTypeDescriptionDefault
whenoptional
table
All match conditions live here. Omitting it makes the rule a fallback, which is moved to the end of the list automatically.
—
reject / direct / to
boolean / string
The action. Exactly one applies — see Actions below.
—
resolve_ipoptional
boolean
Whether reaching this rule is reason enough to look a hostname up. Only valid on a rule that needs a destination IP — ip_cidr, geoip, or domain = "@geoip:…". On those, both true and false are accepted; on a domain or catch-all rule it is rejected at startup either way. It only has an effect under on_demand, where false means "don't resolve on my account" — and if a lookup happens for some other reason, the rule still matches on the real address.
true
Match dimensions when = { ... }
FieldTypeDescriptionExample
domain
string
Domain match. Supports exact (example.com), +.example.com, *.example.com, and @geosite:tag.
"*.home.lab"
ip_cidr
string
IP range in CIDR notation.
"192.168.0.0/16"
geosite
string
GeoSite dataset tag.
"category-ads-all"
geoip
string
GeoIP dataset tag.
"private"
src
string
Source IP CIDR. Supports negation (!) and multi-value (,).
"!10.0.0.0/8"
port
string
Destination port. Supports ranges and negation.
"443,8000-9000"
inbound
string
Inbound types: tcp | udp | tun | h3 | mixed. Supports multi-value and negation.
"tcp,tun"
alpn
string
ALPN protocols: h2 | http/1.1 | h3.
"h2"
Actions
SyntaxTypeDescriptionExample
reject = true
boolean
Reject the connection.
true
direct = true
boolean
Connect directly to the target.
true
to = "upstream-name"
string
Forward to the named upstream.
"wg-home"
Routing rules examplesTOML
# Exact domain — office intranet
[[proxy.rules]]
when = { domain = "api.internal.corp" }
to   = "wg-office"

# Subdomains only — the apex ads.example.com is not matched
[[proxy.rules]]
when   = { domain = "+.ads.example.com" }
reject = true

# Several conditions in one when: same name, two paths by location
[[proxy.rules]]
when   = { domain = "*.home.lab", src = "192.168.1.0/24" }
direct = true

[[proxy.rules]]
when = { domain = "*.home.lab" }
to   = "wg-home"

# Dataset tags and ports use the same when object
[[proxy.rules]]
when   = { geoip = "private" }
direct = true

[[proxy.rules]]
when   = { port = "22" }
direct = true

# A broad private-range rule isn't worth a lookup of its own.
# Under on_demand, resolve_ip = false keeps it from triggering one.
[[proxy.rules]]
when       = { ip_cidr = "192.168.0.0/16" }
resolve_ip = false
direct     = true

# Fallback: no when at all — moved to the end automatically
[[proxy.rules]]
direct = true
Priority. Within one rule type the more specific pattern wins, wherever you put it in the file — you do not hand-sort your rules. For domains that means exact beats a long suffix, which beats a short suffix, and at equal length +. beats *. because it excludes the apex. For addresses, the longer prefix wins: 10.1.2.0/24 before 10.1.0.0/16 before 10.0.0.0/8. Across different types — a domain rule versus a geosite rule, or ip_cidr versus geoip — declaration order decides, so put the more specific type first when they overlap. A rule with no when is a fallback and is moved to the end automatically; if you write several unconditional fallbacks, only the first is reachable and Orange warns about the rest.

TUN device

[tun]

System-level network gateway over a virtual interface. Captures all OS-level traffic and feeds it into the routing engine. Requires admin privileges on Linux/macOS and is not supported on Windows. SNI/HTTP sniffing lives in its own section.

OptionTypeDescriptionDefault
enabled
boolean
Enable TUN mode. Requires root or CAP_NET_ADMIN.
false
name
string
TUN device name. Linux: tun0, macOS: utun0.
—
ipv4_address
string
TUN device IPv4 address; applications reach the proxy through it.
"198.18.0.1"
ipv4_prefix
integer
IPv4 subnet prefix length — 24 for a /24 (255.255.255.0), 16 for a /16 (255.255.0.0).
24
ipv6_address
string
TUN device IPv6 address (optional).
—
ipv6_prefix
integer
IPv6 prefix length. Only takes effect once ipv6_address is set.
64
mtu
integer
Maximum transmission unit. Standard: 1500, WireGuard: 1420.
—
routes
string[]
IP ranges routed into the TUN device. ["0.0.0.0/0"] captures all IPv4; on macOS prefer the equivalent split range (1.0.0.0/8 through 128.0.0.0/1) so it does not conflict with the system default route.
[]
exclude_routesoptional
string[]
IP ranges that must bypass the TUN device. Orange probes the default gateway at startup and installs more-specific system routes for each excluded CIDR; they are removed on shutdown. Typical uses: excluding upstream server IPs, LAN networks, or ranges that should stay on the physical interface.
[]
driveroptional
string
Selects the TUN device backend. The field exists for future alternatives; today there is a single implementation, so leave it out and the build uses what it shipped with.
compile-time
netstackoptional
string
Selects the TCP/IP stack backend. As with driver, the field exists for future alternatives — leave it out and the build uses what it shipped with.
compile-time
tcp_concurrent_limit
integer
Maximum concurrent TCP connections. Each needs 2 file descriptors, so this limit has to fit the host's descriptor budget alongside UDP and the rest of the system. This is the largest memory knob in the file — every connection holds its own send and receive buffers, so peak memory scales with it. Memory-constrained mobile builds should drop to 128–256.
2048
udp_session_limit
integer
Maximum concurrent UDP sessions. Most sessions share a pooled socket; only sessions that would collide, plus DNS queries, get a socket of their own. Raise it if you see repeated "UDP session limit reached" warnings; 256–512 suits memory-constrained builds.
2048
tcp_idle_timeout_secs
integer
Close a TCP connection after this long with no data. Deliberately shorter than timeouts.tunnel_idle_secs: a TUN connection holds substantially larger stack buffers than a proxy tunnel does, and there are far more of them, so idling is much more expensive here. Raising it protects long-lived push and IM connections whose heartbeat can reach 15–30 minutes, at the cost of holding those buffers longer.
300
udp_idle_timeout_secs
integer
UDP session idle timeout in seconds.
60
udp_socket_pool_size
integer
Size of the global UDP socket pool, created separately for IPv4 and IPv6. A larger pool reduces the chance that two sessions to the same destination collide and have to fall back to a dedicated socket. After a network change the current pool is invalidated conservatively and new direct sessions use their own sockets, while Orange rebuilds the pool in place with backoff — no restart involved.
8
udp_force_ephemeral_ports
integer[]
Destinations on these ports always get a dedicated socket rather than sharing the pool, preserving source-port entropy. Port 53 is included by default because predictable DNS source ports weaken spoofing resistance.
[53]
udp_quic_sniff
boolean
Enable QUIC SNI sniffing for routing.
true
udp_route_by_ip
boolean
Force UDP routing by destination IP only.
false
dns
string[]
DNS servers advertised to the system on the TUN interface, usually the TUN address itself.
[]
dns_hijack
string[]
Intercept DNS traffic to these destinations and answer it with the internal resolver. "*:53" catches IPv4 and IPv6 on port 53 and is the usual choice for preventing DNS leaks; "0.0.0.0:53" or "[::]:53" narrow it to one family, and "8.8.8.8:53" targets a single server. Empty means no hijacking.
[]
dns_worker_count
integer
DNS hijack worker count.
4
dns_queue_size
integer
DNS request queue size.
1024
udp_worker_count
integer
UDP packet worker count. Recommended: CPU cores × 2–4.
16
udp_queue_size
integer
UDP packet queue capacity. Packets are dropped when full. Each slot holds one packet, so memory scales linearly with this value. On memory-constrained mobile builds: 1024–2048.
8192
ffi_input_queue_size
integer
Mobile builds only. Capacity of the inbound packet queue between the host app and the network engine. Packets are dropped when full. Memory-constrained mobile builds should lower this to 256–512.
1024
ffi_output_backlog_hwm
integer
Mobile builds only. Outbound packet queue high-water mark in bytes. Once reached, Orange stops reading from the TCP/IP stack until drain falls below the low-water mark. On memory-constrained mobile builds: 2097152 (2 MB).
8388608
ffi_output_backlog_lwm
integer
Mobile builds only. Outbound packet queue low-water mark in bytes. Reads resume once the queue drains below this value. Must be strictly less than ffi_output_backlog_hwm. On memory-constrained mobile builds: 1048576 (1 MB).
4194304

SNI / HTTP sniffing

[tun.sniff]

Extracts TLS SNI or HTTP Host from the first bytes of a TCP connection so that domain-based routing rules still work when only an IP destination is visible. Sniffing requires the client to speak first; enabling it on server-first protocols (MySQL, SMTP, SSH) introduces timeout delays. The default port allow-list keeps it limited to HTTP/HTTPS-style traffic.

OptionTypeDescriptionDefault
enabled
boolean
Enable TCP sniffing for SNI / HTTP Host.
true
timeout_ms
integer
Maximum wait time (ms) for the first client packet. Only affects connections whose hostname cannot be recovered via DNS reverse lookup (Fake-IP / DNS cache). Fake-IP connections skip sniffing entirely and are not subject to this timeout.
150
ports
string[]
Port allow-list for sniffing. Each entry can be a literal port (443), a range (8080-9999), or a comparison expression (<1000, <=1024, >1000, >=1024). Mix forms freely — but note that every entry is a string, including plain port numbers.
["80", "443", "8080", "8443"]

Timeouts

[timeouts]

Connection and idle timeouts for proxy inbounds. TUN has its own idle settings under [tun], deliberately separate — a TUN connection holds far larger buffers than a proxy tunnel does. Both fields are restart-required: the proxy servers capture them at startup, so a reload would report success while the old values stayed in force.

OptionTypeDescriptionDefault
connect_ms
integer
How long to wait for a TCP connection. 3000 fails fast, 5000 is a reasonable standard, 10000 suits slow links.
5000
tunnel_idle_secsoptional
integer
Idle timeout for HTTP CONNECT and SOCKS tunnels, 1–86400. 0 does not mean "off" and is rejected. This caps how long a tunnel may carry no data, not how long it may live — a heartbeat keeps refreshing the deadline.
1800
The 30-minute default is deliberate. Push and IM connections can go 15–30 minutes between heartbeats, and a shorter timeout kills them repeatedly, which costs both delivery latency and battery. Half-dead connections are not this setting's job: TCP keep-alive catches those within roughly 30 seconds.

Admin API

[listeners.admin] [admin]

One local HTTP surface for health, metrics, live streams, request traces, mesh administration, and configuration reload. It binds to a loopback address only — a non-loopback bind is rejected at startup — and every route requires a bearer token. There is no TLS, no trusted-proxy mode, and no way to turn authentication off; for remote access, use an SSH tunnel.

OptionTypeDescriptionDefault
listeners.admin.enabled
boolean
Start the admin server. Off by default, like every other listener.
false
listeners.admin.bind_addr
string
Listen address. Must be a loopback IP.
"127.0.0.1:9898"
admin.auth.token_hash
string
Required when the listener is enabled. Generate the pair with orange admin token generate, which writes the raw token to <state_dir>/admin.token with 0600 permissions and prints a token_hash line — paste that value here exactly as printed. The raw token is not echoed unless you ask for it. Only the hash belongs in this file; keep the raw token in admin.token and pass that to clients.
—
admin.probesoptional
table
public_livez and public_readyz expose those two probes without a token. Both default to false — opt in per probe when a load balancer or orchestrator needs them.
false
admin.rate_limit_per_minuteoptional
integer
Request ceiling per minute for the admin surface.
120
admin.request_body_limit_bytesoptional
integer
Maximum accepted request body size.
65536
admin.max_join_code_ttl_secsoptional
integer
Upper bound on the TTL an issued mesh join-code may request. Mesh builds only.
604800
admin.metrics_live.interval_msoptional
integer
Push interval for the metrics SSE stream. Pushing is subscriber-driven — with no subscriber the broadcast task parks and nothing is rendered, so there is no idle cost.
1000
Endpoints
MethodTypePath & description—
GET
JSON
/livez, /readyz — liveness and readiness. Public exposure is opt-in per probe.
—
GET
text
/metrics — Prometheus scrape, in text exposition format.
—
GET
SSE
/api/metrics/live — live metric stream, one frame per interval.
—
GET
WS
/api/traces/live — request lifecycle stream, with an inflight snapshot and history replay on subscribe.
—
GET
JSON
/api/traces/history — completed traces, paged with a composite cursor.
—
GET
JSON
/api/v1/proxy/upstreams/health — per-upstream latency and availability from the active probes. /health is an authenticated alias.
—
GET
JSON
/api/v1/dns/status, /api/v1/dns/cache/status — resolver state, cache occupancy, negative and stale entry counts.
—
GET
JSON
/api/v1/proxy/rules/status — rule counts by kind. Domains, addresses and upstream names are never exposed.
—
GET
JSON
/api/v1/mesh/status, /nodes, /tokens, /join-codes — mesh state, peers, dynamic tokens and issued join codes. /api/v1/mesh/status is the complete state entry point; logs are only for troubleshooting. Mesh builds only.
—
GET
SSE
/api/v1/mesh/direct/live — direct-path transitions plus a 1 Hz traffic matrix. A node that only runs control and relay, carrying no peer traffic of its own, answers 503. POST /api/v1/mesh/direct/reprobe forces a fresh discovery round.
—
GET
WS
/api/mesh/traces/live — per-packet mesh traces on their own schema_version = 1, carrying drop_reason and acl_result. 503 when mesh is off.
—
GET
JSON
/api/v1/admin/status, /api/v1/admin/endpoints — admin surface state and the live route inventory.
—
GET
JSON
/api/v1/metrics/status, /api/v1/traces/status, /api/v1/stats/traffic — subsystem status for metrics and tracing, and cumulative traffic counters.
—
POST
JSON
/api/v1/reload — apply the config, or return the fields that need a restart; GET /api/v1/reload/status reports the last outcome. /reload is an authenticated alias. See the note below.
—
CLI examplesshell
TOKEN=$(cat /var/lib/orange/admin.token)

curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:9898/readyz
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:9898/metrics
curl -N -H "Authorization: Bearer $TOKEN" http://127.0.0.1:9898/api/metrics/live
curl -X POST -H "Authorization: Bearer $TOKEN" http://127.0.0.1:9898/api/v1/reload
Built-in dashboards: open http://127.0.0.1:9898/trace.html for live requests or /metrics.html for the metric panel. The page shell is served without a token; paste the raw token from admin.token — not the hash — to connect the data stream. It is held in memory only and cleared on refresh.
Reload is all-or-nothing. A pure preflight runs first: if any changed field needs a restart, the call returns 409 with the exact field names and nothing is written to runtime state. Hot-updatable today: proxy.rules, dns.rules, dns.fake_ip, DNS bootstrap, admin token and probe policy, and the tracing mode. Everything else needs a restart — datasets, upstreams, pools, listeners, TUN, underlay, all of mesh, both timeouts values, and — less obviously — general.log_level and general.ip_version. One more subtlety: rules are hot-updatable, but if an edit changes which GeoSite/GeoIP tags are referenced, the dataset index has to be rebuilt, so that reload asks for a restart too.
Streaming endpoints and query tokens: browsers can't attach an Authorization header to a WebSocket or EventSource, so the three stream routes also accept ?access_token=<raw-token>. A token in a URL lands in browser history and in the access log of any reverse proxy — programmatic clients should always use the header instead.

Tracing

[tracing]

Records the lifecycle of each request — matched rule, route decision, connect timing, DNS answer source, failure class — and streams it to subscribers over WebSocket.

OptionTypeDescriptionDefault
modeoptional
string
off: record nothing, and clear existing records immediately on switch.
on-subscribe: record only while an observer is connected — recommended on mobile.
always: keep recording with no subscriber, so history_capacity can replay after a disconnect.
When unset, it is derived from enabled. Hot-reloadable.
"on-subscribe"
enabledoptional
boolean
Simplified switch: true maps to on-subscribe, false to off. An explicit mode wins. Hot-reloadable.
true
capacityoptional
integer
Maximum in-flight traces, hard ceiling 20000. Memory scales with this value. Restart required — a reload rejects a change here explicitly.
5000
history_capacityoptional
integer
Completed traces retained in a ring buffer for replay, ceiling 20000. 0 keeps nothing. Restart required. With mode = "always" and no history, anything that completes while you are disconnected is unrecoverable — Orange warns about that combination at startup.
0
On subscribe, history and the in-flight snapshot together replay a bounded number of entries, with the snapshot taking priority — a request still in progress is worth more screen space than one that already finished. Use GET /api/traces/history for the full record. If memory pressure reaches critical, the ring is cleared and writing pauses.

Tracing WebSocket

[tracing.websocket] optional

Controls how trace events are aggregated before being delivered to WebSocket subscribers. Tighter flush intervals reduce end-to-end latency; larger batches reduce frame overhead.

OptionTypeDescriptionDefault
flush_interval_ms
integer
Aggregation window in milliseconds before flushing a batch to subscribers.
100
max_batch_size
integer
Maximum trace entries per batch. Larger batches reduce overhead, smaller batches reduce latency.
50

Memory pressure

[memory_pressure]

Adaptive protection for memory-constrained environments (e.g. iOS NetworkExtension's ~50 MB limit). A background monitor samples how much memory Orange has allocated and applies tiered mitigations as thresholds are crossed, so the OS does not kill the process outright.

Default behaviour: omitting the entire [memory_pressure] block disables pressure management (desktop default). When present, at least one non-zero threshold must be set and thresholds must be strictly increasing. Pressure-level changes log a warn-level message for diagnostics. This block does not support hot-reload; restart is required to apply changes.
Pressure levels
LevelTriggerActionEffect
Warning
warning_bytes
New proxy connections are admitted at half the configured limit, and idle connections are reclaimed sooner.
Admission 50%, idle 60s
Critical
critical_bytes
Tighter admission, shorter idle timeouts.
Admission 25%, idle 15s
Emergency
emergency_bytes
Reject all new proxy connections (DNS hijack exempt).
Idle 3s
OptionTypeDescriptionDefault
warning_bytes
integer
Bytes threshold for entering the Warning level. Set to 0 to disable this level.
—
critical_bytes
integer
Bytes threshold for entering the Critical level. Must be greater than warning_bytes.
—
emergency_bytes
integer
Bytes threshold for entering the Emergency level. Must be greater than critical_bytes.
—
check_interval_ms
integer
How often memory use is sampled, in milliseconds (min 100). A short interval keeps a worker thread waking up and costs battery on mobile, while the 5-second default is well within acceptable latency for reacting to pressure during idle periods.
5000
iOS NetworkExtension (50 MB limit)TOML
[memory_pressure]
warning_bytes     = 31457280   # 30 MB
critical_bytes    = 39845888   # 38 MB
emergency_bytes   = 46137344   # 44 MB
check_interval_ms = 5000
DNS is exempt from pressure-induced rejection. DNS is infrastructure — blocking it would cause cascading failures in everything downstream.

Dataset

[dataset] optional

GeoSite and GeoIP datasets classify large numbers of domains and IP ranges under a tag, so a rule can name one tag instead of listing thousands of entries. Local paths resolve against the config file directory; HTTP(S) URLs are downloaded on first use. Orange bundles no dataset and recommends no particular publisher — bring a Protobuf-format dataset you trust, and reference whichever tags it defines.

OptionTypeDescriptionDefault
geosite
string[]
Domain dataset sources in Protobuf form. Referenced from rules as geosite = "<tag>" or domain = "@geosite:<tag>".
—
geoip
string[]
IP-range dataset sources in Protobuf form. Referenced from rules as geoip = "<tag>" or ip_cidr = "@geoip:<tag>".
—
Dataset configurationTOML
[dataset]
geosite = [
  "./GeoSite.dat",
  "https://datasets.example.com/geosite.dat",
]
geoip = [
  "./GeoIP.dat",
  "https://datasets.example.com/geoip.dat",
]
Caching. A remote source is cached under <state_dir>/cache/datasets/ only after it downloads and decodes successfully, and later starts reuse that cache — so a machine that has run once keeps working offline. To refresh, delete the cache directory. A cache file that is readable but fails to decode is reported with its exact path; delete it and Orange downloads again.
Loading fails closed. Listing several sources is not a fallback chain. If a local file that is actually needed cannot be read, a remote URL fails its first download, or content referenced by a rule cannot be decoded, the whole core refuses to start — it will not skip that source and run with an incomplete rule set, because silently losing a blocklist is worse than not starting. Dataset types and tags that no DNS or proxy rule references are skipped by the normal filter and do not trigger this.
Inspecting a dataset: orange dataset --geosite --path ./GeoSite.dat, optionally narrowed with --tags <tag>,<tag>, or searched with orange dataset --geoip --path ./GeoIP.dat --search 8.8.8.8. The --path argument is relative to your current directory. Note that orange test --full only reads and validates datasets, hosts and TLS inputs — it never creates or updates the cache.

Mesh core

[mesh] optional

Mesh is a top-level subsystem alongside DNS and Proxy — not a proxy upstream. It gives every joined device a stable private IPv4 address and a name that resolves inside your network, carries traffic over WireGuard on the TUN packet path, and upgrades a pair to a direct peer-to-peer path once that path has been verified — your own relay carries the traffic until then, and again if the path degrades. Every field here requires a restart; /reload reports them and applies nothing.

Running control and relay roles? The Mesh Server is a standalone service with its own installation, initialization, system service, state, backup, and migration lifecycle. Follow the Mesh Server deployment guide before configuring clients.
OptionTypeDescriptionDefault
enabled
boolean
Start the mesh subsystem. false is identical to omitting [mesh] — DNS, proxy and TUN behaviour is untouched.
false
node_name
string
DNS-safe label, 1–63 chars of [a-z0-9-]. Reachable as <node_name>.<domain>. Not an identity — identity comes from the node key, so renaming changes nothing else. Control is authoritative: a rename issued there is pushed to every online peer.
—
roles
string[]
One of five accepted sets: ["edge"], ["edge","gateway"], ["control","relay"], ["control","edge","relay"], or all four. gateway is Linux only and fails fast elsewhere.
["edge"]
join
string[]
Control endpoint, https://host:port. TLS is mandatory and there is no plaintext fallback. Required for edge roles; must be empty when this node runs control itself. One endpoint only — control has no HA in v1.
—
domain
string
MagicDNS suffix. Names under it are answered authoritatively and never forwarded to a public resolver — including on a miss.
"orange.mesh"
address_pool
string
IPv4 CIDR that mesh addresses are allocated from; prefix must be /30 or wider. Cannot overlap local interfaces, the Fake-IP range 198.18.0.0/15, or advertised routes. See the note below — the value init-server writes is not the value you get by omitting the field.
"100.64.0.0/10"
auto_route
boolean
Let Orange reconcile the self address, mesh pool route, accepted subnet routes, control/relay excludes, and Linux gateway NAT. Turning it off makes route management entirely your responsibility.
true
control_cert_sha256optional
string
SHA-256 of the control leaf certificate in DER form, 64 hex chars — not a SPKI hash and not a hash of the PEM text. Leave empty when the control host has a real domain and a public CA certificate.
—
relay_cert_sha256optional
string
Same rule for the relay certificate. When control and relay share one certificate, both pins are identical.
—
identity_key_pathoptional
string
32-byte identity seed; generated on first start. The node ID and its WireGuard key both derive from it, so losing this file means losing the identity and re-joining. Must be a 0600 file inside a 0700 directory.
"<state_dir>/mesh-identity.key"
advertise_routesoptional
string[]
LAN CIDRs published by a gateway node, so devices that can't run Orange stay reachable. Prefix between /8 and /30; cannot overlap the address pool, and a single node's routes cannot overlap each other. Requires IP forwarding on the host.
[]
exclude_routesoptional
string[]
This machine's own LAN CIDRs, reported so the edge skips any advertised subnet route that would collide with them. Desktop and server builds detect their interfaces automatically and need nothing here — the field exists for mobile hosts that have to declare their LAN.
[]
join_tokenoptional
string
Legacy inline token for a first join. Leave it empty: orange mesh join writes a one-time token to managed state instead, and the runtime will not rewrite this field for you. To migrate an old config, stop the process and run orange mesh migrate-config, which moves the token into pending state before clearing it.
""
bridge_relay_endpointoptional
string
Pin the relay this node connects to, as host:port. Normally unset — control broadcasts a relay directory and the edge selects automatically. Use it only for disaster recovery, debugging, or bypassing the broadcast endpoint temporarily.
—
relay_udp_sidecar_enabledoptional
boolean
Use the UDP fast path for relayed packets. Disabling it sends all relayed data over the existing TLS/TCP relay — useful for A/B comparison or when UDP is blocked.
true
Two different defaults, depending on how the config was made. orange mesh init-server writes 172.31.240.0/20 — roughly 4,000 addresses, chosen to stay clear of the ranges most networks already use. Omitting the field from a hand-written config gives you 100.64.0.0/10 instead, which is the full CGNAT range and matches Tailscale's default exactly; if Tailscale runs on the same host or LAN, startup fails with a pool or route conflict. Whichever you use, pick the range before the first device joins — it is part of the mesh's identity, and changing it later means re-enrolling everything.
Bootstrapping a mesh: on the control node, orange mesh init-server --public-endpoint <host> writes the mesh config to the default location. Under the default --trust pin it computes the leaf certificate pin and embeds it in the join code; --trust webpki --cert <fullchain> --key <privkey> uses a public CA certificate instead, and no pin is needed. Every other device then runs orange mesh join --join-code-file <file> rather than having a raw token pasted into its config: that command writes a one-time token to managed state, merges mesh settings into an existing config while preserving your DNS, proxy and TUN sections, and validates the candidate before writing it. Neither hot-reloads — restart Orange afterwards.
Scope in v1. One control node, no HA, no automatic leader election, and control cannot be split into its own process — it must share a process with relay, optionally alongside edge and gateway. Not supported yet: an IPv6 address pool or IPv6 subnet routes; layer-2 LAN behaviour — ARP, broadcast, mDNS flooding, full ICMP and traceroute; mesh on Windows; gateway NAT on macOS, iOS and Android; and ACL changes pushed at runtime, which need a control restart.
Reaching the mesh from an HTTP, SOCKS or mixed client. There is no type = "mesh" upstream — mesh is a subsystem, not a proxy destination. Let proxy.rules pick direct for <node>.<domain> and for mesh addresses: when a target falls inside the address pool or an accepted subnet route, Orange deliberately does not bind or protect that socket to the physical underlay, so the OS route hands it to the TUN interface and the mesh data path takes over. This needs [mesh] and [tun] both enabled with the routes in place. And never point *.<mesh.domain> at an external DNS upstream — an explicit dns.rules upstream outranks MagicDNS, and mesh names stop resolving.

Control

[mesh.control]

Required when roles contains control. Control issues identities and addresses, distributes the signed peer map and ACL policy, approves subnet routes, and signs relay tickets. It holds no session keys and never sees payload. In v1 control must run in the same process as relay.

OptionTypeDescriptionDefault
listen
string
Bind address, host:port. Typically 0.0.0.0:8088 behind a firewall that restricts source addresses.
—
state_pathoptional
string
Embedded database holding node records, name and address indexes, disabled state, and token usage. Losing it resets the mesh — back it up alongside the identity key. Omit it and Orange uses its own managed filename inside the state directory.
managed, in <state_dir>
tls.cert_path
string
PEM certificate. A self-signed certificate is fine when edges pin it; the subject alt name must include the control hostname.
—
tls.key_path
string
PEM private key. Must be a regular 0600 file owned by the running user, inside a 0700 directory, and not a symlink — a certbot live/ symlink is rejected. Install a daemon-owned copy in your renewal hook and restart.
—
join_tokens[]
table[]
Static bootstrap tokens: token, reusable, max_uses, expires_at_unix_seconds, bind_roles. An empty bind_roles authorises edge only — gateway, control and relay must be listed explicitly, so a token holder can't claim a privileged role.
—
Prefer dynamic tokens in production. orange mesh issue-join --reusable --max-uses 50 --expires-in-secs 86400 --output-file batch.omesh produces a join code you can hand to a whole fleet and revoke online with orange mesh token revoke. Static tokens in TOML have no online listing or revocation and need a restart to change.

Relay

[mesh.relay]

Required when roles contains relay. The relay is a dumb forwarder: it moves sealed WireGuard packets between peers, holds no session keys, cannot decrypt payload, and does not trust a client's self-reported identity — the binding comes from the authenticated session. It is the availability floor beneath the direct path, not the normal data route.

OptionTypeDescriptionDefault
listen
string
TCP bind address. When the UDP sidecar is enabled, Orange also listens on the same address at port + 1 — open both in your security group.
—
public_endpoint
string
The host:port edges dial. Control broadcasts it to every peer, so edges discover relays automatically.
—
idoptional
string
Stable relay identifier. Setting it lets you change the hostname or port later without invalidating issued tickets.
= public_endpoint
max_conns_per_nodeoptional
integer
Concurrent relay sessions allowed per node. Enforced strictly — beyond the limit new attachments are refused.
16
idle_timeout_secsoptional
integer
Idle relay sessions are closed after this long.
600
data_queue_capacityoptional
integer
Per-session outbound queue depth in frames, 1–16384. Too small drops packets to slow receivers; too large causes bufferbloat.
256
data_queue_overflow_policyoptional
string
drop_oldest discards the head of the queue; drop_newest refuses the incoming frame and keeps the ordered head, which is usually friendlier to the inner TCP stream.
"drop_oldest"
tls.cert_path / tls.key_path
string
Relay certificate and key, same file-permission contract as control. Usually the same pair when both roles share a process.
—
Send pacing [mesh.relay_udp_sidecar_pacing]

How fast the UDP sidecar pushes data frames. Restart required.

OptionTypeDescriptionDefault
modeoptional
string
legacy keeps the original fixed-rate pacing. bandwidth paces to a real target and is what full end-to-end throughput testing should use. unlimited hands everything to the kernel and the link — worth an A/B only once you know the underlay, queueing discipline and receiver can all take it.
"legacy"
target_bps
integer
Required with mode = "bandwidth". Bits per second, not bytes — 50 Mbps is 50000000, about 6.25 MB/s. If the edge sits behind a slower link than the server, use the lower of the two.
—
burst_bytesoptional
integer
Burst allowance. A good starting point is target_bps / 400, roughly 20 ms worth of the target rate.
—

Direct path

[mesh.direct.stun] optional

Peers discover each other's public address through a first-party STUN server that the control node runs itself — no third-party STUN service is involved. A path is promoted to direct only after the same endpoint completes repeated signed probe round trips within a short verification window; until then, and whenever the path degrades, traffic rides the relay. Device type never influences this: a phone-to-phone pair is evaluated exactly like any other.

OptionTypeDescriptionDefault
enabled
boolean
Enable STUN observation. With the section omitted, peers can still reach each other over addresses they can see themselves — same LAN, same underlay — but two peers behind different NATs have no way to learn a reachable address for one another, so they stay on the relay.
false
listenoptional
string
UDP bind address for the STUN server started by a control node. An unspecified address expands to IPv4 and IPv6 sockets at startup.
"0.0.0.0:3478"
public_endpoint
string
The host:port this node advertises and edges query. A hostname is resolved at startup and can yield both IPv4 and IPv6 candidates; an IP literal covers only its own family.
—
Watching it work: GET /api/v1/mesh/direct/live streams path transitions and a 1 Hz traffic matrix, so you can see a pair move relay → direct and back. POST /api/v1/mesh/direct/reprobe forces a fresh round of discovery.

Data plane

[mesh.wireguard] optional

Mesh payloads are encrypted with WireGuard end to end between peers. Decryption alone doesn't grant access: after a packet is decrypted, its inner source address must belong to that peer's mesh address or an approved subnet before access control is even evaluated. Tunnels are created per peer on first use.

OptionTypeDescriptionDefault
mtuoptional
integer
Inner MTU, 1200–1420. The default matches the IPv6 minimum MTU and rarely triggers a path-MTU black hole across public relays; raise it toward 1400 only when the real link MTU is high and the relay path is stable. When TUN is enabled with an explicit tun.mtu, a mesh.wireguard.mtu larger than it fails at startup.
1280
max_peer_tunnelsoptional
integer
Per-peer WireGuard tunnel ceiling. Reaching it evicts the least recently used tunnels, and a peer with an active flow is never evicted. At the ~100-node scale this targets, 256 covers every peer with headroom to spare.
256

Access control

[[mesh.acl]] optional

With no rules configured, every registered peer can reach every other peer. Adding a single rule flips the network to explicit-only: anything unmatched is denied, with no implicit fallback. Rules are authoritative only on the control node — the same tables on an edge are inert templates. Changing them requires a restart; for an urgent cutoff, disable the node online instead.

OptionTypeDescriptionDefault
from
string[]
Source node names, or group:<name> referencing a [mesh.groups] entry. A name that isn't registered yet is allowed and simply never matches until it joins.
—
to
string[]
Destinations as node:port or cidr:port, IPv4 only. Ports are single values in 1–65535 — no ranges, wildcards, or negation.
—
protocol
string[]
tcp, udp, or icmp-echo. An echo request may open a flow; an echo reply can only match an established one, so it can't be used to punch a hole inbound.
—
viaoptional
string
Gateway node name. Required for a CIDR destination and rejected for a node destination.
—
[mesh.groups]
table
Named sets of node names, referenced as group:<name>. Groups cannot nest, and duplicate members are a startup error.
—
Replies still work. The sender verifies it owns the source address and records the flow once the data is actually sent; the rule itself is enforced at the receiving side. Legitimate return traffic matches the established flow, so a one-directional rule doesn't break TCP or UDP responses.