Orange configuration.
Orange is configured with a single TOML file that controls DNS resolution, proxy listeners, routing rules, TUN mode, tracing and the private mesh. This page is the authoritative reference: every option, its type, its default and its constraints, grouped by the table it belongs to.
| Configuration format | TOML, one file, sectioned by table name ([dns], [proxy.routing], [mesh], …) |
|---|---|
| Default path (root) | /var/lib/orange/config.toml |
| Default path (non-root) | $XDG_STATE_HOME/orange/config.toml, otherwise $HOME/.local/state/orange/config.toml |
| Path overrides | --config <path> wins, then ORANGE_CONF_PATH, then the default |
| Inbound entry points | HTTP / HTTPS CONNECT, SOCKS5 (TCP and UDP), Mixed (one port, both), and TUN for the whole system |
| Egress modes | HTTPS CONNECT, Trojan (TCP + UDP), Shadowsocks 2022, WireGuard, plus direct and reject |
| DNS transports | UDP, TCP, DoT (DNS over TLS) and DoH (DNS over HTTPS); DNS listener defaults to 127.0.0.1:11223 |
| Routing match keys | domain, CIDR, GeoSite, GeoIP, source address, destination port, inbound type, ALPN — specific rules beat broad ones automatically |
| Sections in this reference | 39, each linkable by anchor (for example #dns-rules, #mesh-relay) |
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.
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.
/var/lib/orange/config.toml$XDG_STATE_HOME set$XDG_STATE_HOME/orange/config.toml$HOME/.local/state/orange/config.tomlExplicit 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.
..; 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.
[table].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.
error | warn | info | debug | traceerror: production, info: default, debug: development, trace: profiling
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.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.
IP:Port.DNS rate limit
[listeners.dns.rate_limit]
optional
Per-source-address rate limiting on the DNS listener. Omit the table to leave it off.
DNS over TLS
[listeners.dns.dot]
optional
Serve DoT to clients. Certificate and key paths resolve against the config file directory.
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.
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.
CONNECT; SOCKS5 handles TCP and UDP ASSOCIATE.[listeners.proxy.auth][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].
false when Orange is the system resolver, or the lookup recurses into itself.Enabled with
/etc/hosts on macOS and Linux; off on iOS and Android, where the sandbox cannot read it./api/v1/dns/cache/status reports how many entries are currently in that window.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.
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].
IP (port 53) or IP:Port. Defaults include both IPv4 and IPv6 addresses for NAT64 / IPv6-only networks.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.
.local) matches anything ending in .local; mid-string pattern match (._dns-sd.) matches anything containing the substring. Set to [] to disable..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.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.
example.com.example.org itself and every subdomain, at any depth.example.org is not matched. More specific than *. of the same length, so it wins when both are present.*.example.org or +.example.org instead.A value is either a shorthand string, a single object, or an array of objects evaluated top to bottom until one matches.
{ reject = true }.{ to = [...] } form.src, a source CIDR supporting negation with ! and multiple values separated by commas..loc while clients ask for .local. Must begin with . and is only valid on a wildcard pattern."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.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 ,).
# 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.
0 disables the timer; saves still happen when changes accumulate.last_access write-backs. Larger values reduce IO pressure.blacklist: every hostname uses Fake-IP except those matched by rules.whitelist: only hostnames matched by rules use Fake-IP.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.
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.mode = "upstream". Accepts an upstream name or a pool name.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.0 is treated as 1 rather than disabling the cache. 2000 suits memory-constrained mobile builds.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.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.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.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.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.
Outbound TLS
[proxy.outbound.tls]
optional
TLS verification for connections Orange makes to upstreams.
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.
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.mode = "interface_name"; setting it in any other mode is a startup error. Find it with ip link show.mode = "interface_index"; setting it in any other mode is a startup error. Find it with ifconfig.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./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.
chmod 600.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.
[[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
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.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.
[[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
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.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.
[[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
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.openssl rand -base64 32. The SIP023 identity-PSK chain (ipsk) is not supported and is rejected.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.
[[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"
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.IP or IP:Port. Mirrors [Interface] DNS in wg0.conf; drop the line entirely when the tunnel has no resolver of its own."0.0.0.0/0,::/0" for everything, or a specific range such as "10.0.0.0/8".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.
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.[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.
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.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.
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.when = { ... }example.com), +.example.com, *.example.com, and @geosite:tag.!) and multi-value (,).tcp | udp | tun | h3 | mixed. Supports multi-value and negation.h2 | http/1.1 | h3.# 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
+. 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.
24 for a /24 (255.255.255.0), 16 for a /16 (255.255.0.0).ipv6_address is set.["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.driver, the field exists for future alternatives — leave it out and the build uses what it shipped with.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."*: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.ffi_output_backlog_hwm. On memory-constrained mobile builds: 1048576 (1 MB).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.
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.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.
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.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.
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.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./livez, /readyz — liveness and readiness. Public exposure is opt-in per probe./metrics — Prometheus scrape, in text exposition format./api/metrics/live — live metric stream, one frame per interval./api/traces/live — request lifecycle stream, with an inflight snapshot and history replay on subscribe./api/traces/history — completed traces, paged with a composite cursor./api/v1/proxy/upstreams/health — per-upstream latency and availability from the active probes. /health is an authenticated alias./api/v1/dns/status, /api/v1/dns/cache/status — resolver state, cache occupancy, negative and stale entry counts./api/v1/proxy/rules/status — rule counts by kind. Domains, addresses and upstream names are never exposed./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./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./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./api/v1/admin/status, /api/v1/admin/endpoints — admin surface state and the live route inventory./api/v1/metrics/status, /api/v1/traces/status, /api/v1/stats/traffic — subsystem status for metrics and tracing, and cumulative traffic counters./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.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
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.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.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.
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.true maps to on-subscribe, false to off. An explicit mode wins. Hot-reloadable.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.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.
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.
[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.warning_bytes.critical_bytes.[memory_pressure] warning_bytes = 31457280 # 30 MB critical_bytes = 39845888 # 38 MB emergency_bytes = 46137344 # 44 MB check_interval_ms = 5000
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.
geosite = "<tag>" or domain = "@geosite:<tag>".geoip = "<tag>" or ip_cidr = "@geoip:<tag>".[dataset] geosite = [ "./GeoSite.dat", "https://datasets.example.com/geosite.dat", ] geoip = [ "./GeoIP.dat", "https://datasets.example.com/geoip.dat", ]
<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.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.
false is identical to omitting [mesh] — DNS, proxy and TUN behaviour is untouched.[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.["edge"], ["edge","gateway"], ["control","relay"], ["control","edge","relay"], or all four. gateway is Linux only and fails fast elsewhere.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./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.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.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.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.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.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.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.
host:port. Typically 0.0.0.0:8088 behind a firewall that restricts source addresses.live/ symlink is rejected. Install a daemon-owned copy in your renewal hook and restart.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.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.
port + 1 — open both in your security group.host:port edges dial. Control broadcasts it to every peer, so edges discover relays automatically.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.[mesh.relay_udp_sidecar_pacing]How fast the UDP sidecar pushes data frames. Restart required.
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.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.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.
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.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.
tun.mtu, a mesh.wireguard.mtu larger than it fails at startup.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.
group:<name> referencing a [mesh.groups] entry. A name that isn't registered yet is allowed and simply never matches until it joins.node:port or cidr:port, IPv4 only. Ports are single values in 1–65535 — no ranges, wildcards, or negation.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.group:<name>. Groups cannot nest, and duplicate members are a startup error.