Rule 3 of the specification: when the spec is ambiguous or contradicts itself, the question is recorded here with the default chosen for implementation. Every entry stays open until the owner answers; answers are written under the entry and the code follows the answer.
Legend: [OPEN] waiting for the owner · [DEFAULT] implemented with the stated default until the owner decides · [ANSWERED] closed.
- [OPEN] Do services on the nodes listen on
127.0.0.1or0.0.0.0? Default implemented: target127.0.0.1:<listen>. - [OPEN] Do backup nodes always have the same panel/users (Marzban node or manual mirror)? Default: node failover is enabled and the UI shows the fixed warning "Backup only works if the same service runs on both nodes."
- [OPEN] Is the real client IP needed in the panel? If yes, is PROXY
protocol enabled on the node service? Default: no PROXY protocol; the UI marks
each transport
client IP: preserved / masked. - [OPEN] Expected number of nodes and tunnels in 12 months? Default sizing: probe worker pool 8, bbolt ≤ 50 MB, 64 port maps per tunnel.
- [OPEN] Are all servers amd64, or is arm64 used too? Default: both are built.
| Item | Default in code | Decide by |
|---|---|---|
Installer URL / mirror (RELEASE_BASE) |
[DEFAULT] GitHub only for now: https://github.com/localroot4/DeyRoute/releases/latest/download/install.sh; owner CDN can be set later in RELEASE_BASE |
phase 1 |
| Domain for ACME | none (tls.mode: auto) |
phase 3 |
| Decoy SNI list (Reality/Waterwall) | [DEFAULT] dl.google.com, www.speedtest.net, www.samsung.com in internal/backend/decoy.go (each carries both Reality rungs, S18); the owner can set others in hub.decoy_snis |
phase 6 |
| Default ladder | as in section 8, except rung 4 of default (C.31) and udp-default without wireguard/kernel (C.45) |
phase 4 |
| Backend control port range | 30000-31999 |
phase 2 |
Hub control_port |
44433 |
phase 2 |
| Telegram | off | phase 5 |
-
[DEFAULT] Repository / module name. Section 15 names the repository
deyroute; the actual repository islocalroot4/DeyRoute. The Go module isgithub.com/localroot4/deyroute; the binary, paths, units and all user-visible names staydeyrouteexactly as specified. -
[DEFAULT] Go version. Spec: Go ≥ 1.22.
go.moddeclaresgo 1.25.0: the security fixes forgolang.org/x/net(GO-2026-5026) andgolang.org/x/text(GO-2026-5970), reachable through the ACME client, require Go 1.25, and Go 1.24 no longer receives security releases. CI and release builds use the current stable Go (1.26.x), so shipped binaries carry a patched standard library.govulncheckruns in CI. -
[DEFAULT] Star (
*) items in the main menu. "Advanced only adds starred items" is read as: the 13 top-level items always exist with fixed numbers; the starred sub-items (thresholds,limits,view fingerprints) appear only in Advanced mode. -
[DEFAULT]
deyroute-tun@.serviceinstance name. Section 2 says the instance is<tunnel_id>, but sections 3/8 require one warm unit per (tunnel, node, transport) so that a switch is only stop+start. A single instance per tunnel cannot hold several warm transports. Implemented instance:<tunnel>.<node>.<backend>-<transport>(e.g.main.de-1.backhaul-wssmux), and<tunnel>.canaryfor the canary unit (section 9). The per-instance drop-in setsStandardOutput=append:/var/log/deyroute/tunnels/<tunnel>.logso logs stay per tunnel as specified. -
[DEFAULT]
DEY-Exxxin section 6. The UI rule "a red line with codeDEY-Exxx" is read as a placeholder for any DEY code; no separateEcategory exists (section 13 lists the categories and has noE). -
[DEFAULT] Extra error codes. Section 13 lists examples per category. Additional codes needed by the implementation were added in the same categories with Message/Why/Fix (see
docs/ERRORS.md), e.g.DEY-X003(daemon not running, named in section 14),DEY-C007invalid id,DEY-P011reserved port,DEY-X008not implemented yet. -
[DEFAULT] nftables library. Section 15 allows
google/nftableswithnft -f -as fallback. The implementation renders theinet deyroutetable as text and applies it atomically withnft -f -(one transaction, golden-tested). This avoids a netlink dependency and gives identical behaviour on all distributions;google/nftablescan be added later without changing callers. -
[DEFAULT] Hidden helper commands. Besides section 14, the binary has hidden commands used by systemd units and tests:
deyroute daemon hub|node(services),deyroute relay --tunnel <id>(thedirect/nativedata plane named in section 7.8),deyroute menu --once(renders the first screen without a TTY for smoke tests / S24). -
[ANSWERED] Backend binary checksums. Every sha256 in
internal/backend/backends.yamlis now recorded (computed from the upstream files withscripts/manifest-hashes, checked against upstream checksum files where they exist; see C.29). A backend without a sha256 would still be refused withDEY-S006. -
[DEFAULT] WireGuard key configuration. Section 15 allows
execofiponly (notwg). The kernel WireGuard transport therefore configures keys and peers through the WireGuard generic-netlink API directly (golang.org/x/sys/unix), and links/addresses throughip. -
[DEFAULT] PKCS#12 for rathole/tls. No PKCS#12 encoder is in the allowed library list. A minimal encoder (SHA-1/3DES PBE + SHA-1 MAC, the format OpenSSL/native-tls reads by default) is implemented in
internal/tlsutil. -
[DEFAULT] Doctor output directory.
deyroute-hub.serviceusesProtectHome=true, so/root/deyroute-doctor-<UTC>.tar.gzis written by the CLI process (which the owner runs as root), not by the daemon; the daemon only supplies data over the Local API. -
[DEFAULT] Phase tags. Rule 4 asks for one PR and one tag per phase. In this delivery each phase is one Conventional-Commit series on the working branch; acceptance evidence that needs real servers (VM installs, 72-hour test, videos, real-user test) cannot be produced from the build sandbox and is listed per phase in
docs/en/acceptance.mdas pending. -
[DEFAULT] Config keys beyond the section 4 sample. Section 4 says the sample lists all keys, but other sections require settings that have no key there:
failover.quarantine_s(§9 defaults), per-portprobe(auto|tcp|tls|http, §9), tunnelprobe_port(§14--probe-port),tls.cert_file/tls.key_file(custom mode, §10), hubdecoy_snis(§7.4),public_ip6(§10 IPv6),language(§14),mirror,update_check(§5),acme(email / Cloudflare DNS-01 token file, §10) and per-tunneladvanced:(connection_pool, Hysteria2 bandwidth/port hopping,proxy_protocol,backhaul_web_port). They are all optional and omitted when empty, so the section 4 sample validates unchanged; any other unknown key is stillDEY-C001.backhaul_web_portis refused by validation (DEY-C013): the pinned Backhaul v0.7.2 serves its stats page on every interface and has no bind-address key, so "stats only on 127.0.0.1" (§7.1) cannot be honoured; earlier it silently skipped every Backhaul rung instead. A config.yaml from such an earlier build still starts the hub (the key is ignored with a warning in hub.log and dropped at the next change);config validate,edit,applyandrestorerefuse it. -
[DEFAULT] Exec allow-list. Section 15 allows only
systemctl, nft, ss, ip, xray x25519, rathole --genkey, but section 10 requires detecting and (after confirmation) runningufw/firewall-cmd/iptables, and the backend userdeyroutemust be created. The allow-list is thereforesystemctl, nft, ss, ip, xray, rathole, ufw, firewall-cmd, iptables, systemd-sysusers. Key material for Rathole Noise and Xray Reality is generated in pure Go (X25519), soxray x25519/rathole --genkeyare allowed but not needed. -
[DEFAULT] Secrets readable by backends. Section 4 requires every file in
secrets/to be 0600 root, while section 7 runs backends as userdeyroute, which then cannot read the tunnel TLS key or token. Rendered backend directories (/etc/deyroute/backends/<backend>/<tunnel>/…) are0750and their files0640, ownerroot:deyroute, and contain the copies the process needs;secrets/itself stays 0700/0600 root. Paths are/etc/deyroute/backends/<backend>/<tunnel>/<node>/<transport>/(one directory per warm unit; section 2 shows only the first two levels). -
[DEFAULT] Last rung of the default ladder. The section 4 YAML sample ends with
direct/haproxy, section 8 says the default ladder ends withdirect/native(and HAProxy is optional, phase 7). Section 8 is followed: the embedded default ladder ends withdirect/native. -
[DEFAULT] Telegram from Iran.
api.telegram.orgis usually unreachable (filtered, DNS tampered) from Iranian datacenters. The hub first asks an online node to perform the HTTPS POST (http.postcommand, fastest control link first, at most 40 s for all nodes together, each node in turn a share of it) and posts directly second — the same order asfetch.proxyfor GitHub downloads. With no node online (before the first join) it posts directly at once. The bot token is part of the API address, so it goes to a node with every message (inside the control connection; the node keeps it out of its logs). No other data leaves the servers. (Changed after the filtering report of 2026-10-17: the order was direct first, through a node second, and a direct post that waited for its 20 s timeout delayed every alert.) -
[DEFAULT] Manifest file location.
backends.yamlis embedded frominternal/backend/backends.yaml(the backend registry owns it); the release publishes and signs that same file. -
[DEFAULT] AmneziaWG binary.
amneziawg-gopublishes no release binaries. DEYROUTE builds it reproducibly and publishes it on the GitHub releasebackend-builds(C.29); the manifest points there. The owner may mirror it and override the manifest. -
[DEFAULT] HAProxy. HAProxy has no official static Linux binaries.
direct/haproxy(optional, phase 7) uses the distribution'shaproxybinary when present and is otherwise marked unavailable with DEY-B006. -
[DEFAULT] Extra packages.
internal/version(build metadata) andinternal/backend/all(blank imports that register every backend) are small additions to the section 15 tree;internal/execis named in section 15's coding rules. Thedirect/nativerelay data plane lives ininternal/backend/direct. -
[DEFAULT] Control port access for joins and node IP changes. Spec §3/§11 open the control port only to joined nodes' IPs (
@nodes), but a joining node's IP is unknown until it connects, and §11/S27 require a node whose IP changed to reconnect from the new IP. Implemented: while any unexpired join token exists the control port is open to every source (the join window); otherwise@nodesis accepted without limit and unknown sources get a rate-limited accept (ct state new limit rate 6/minute) before the drop. mTLS client certificates, the single-use 256-bit join tokens and the per-IP limit (5 failures/hour, then a 1-hour block) protect the port. Owner may choose strict mode (drop unknown sources; a moved node then needsdeyroute node set-hubor a re-join). -
[DEFAULT] Reserved node id.
canarycannot be a node id (it names the canary unit directory and control-port keys of every tunnel; §9). -
[OPEN] hello NodeID vs certificate CN. A node stream whose hello names a different id than its client certificate is accepted under the certificate CN (the CN is what the hub signed) and a warning is logged. Owner: refuse instead?
-
[DEFAULT] Join limiter granularity. Failures are counted per exact IP as §11 says; IPv6 sources can rotate inside a /64. Harmless in practice (256-bit tokens, bounded table); owner may ask for /64 counting.
-
[DEFAULT] TLS trust for acme/custom tunnel certificates.
ca.crtgiven to clients holds the internal CA plus the served chain, so clients verify the public chain and host name. Pinning only the leaf would break OpenSSL-based clients without PARTIAL_CHAIN. -
[DEFAULT] Local toolchain vs lint. golangci-lint v2.5.0 is built with Go 1.25 and cannot analyse against the Go 1.26 standard library; CI lints with the go.mod version (1.25) and tests/builds with current stable Go.
-
[DEFAULT] Backend binaries DEYROUTE builds itself. Two pinned backends have no usable upstream binary: amneziawg-go publishes none (C.20), and chisel tagged v1.12.1 (the fix restricting reverse-UDP return peers, which
chisel/wssuses for/udpmaps) without release assets (v1.12.0 lacks the fix).scripts/build-backends.shbuilds both from the tagged Go modules with a pinned toolchain exactly like upstream (CGO off,-trimpath,-s -w, chisel's BuildVersion); the output is byte-for-byte reproducible, and.github/workflows/backend-builds.ymlpublishes it on the prereleasebackend-buildsonly when the hashes equal the manifest. Every other backend hash inbackends.yamlwas computed from the upstream file and, where upstream publishes checksums (frp, Xray, gost, chisel, hysteria), matches them (go run ./scripts/manifest-hashes -check). Owner may mirror these files and override the manifest instead. -
[DEFAULT] Uninstall removes the system account.
deyroute uninstalldeletes thedeyrouteuser and group the installer created (spec 5: the system is restored; scenario S23 compares accounts).userdelandgroupdelare therefore on the exec allow-list (internal/exec); they are run with the fixed namedeyrouteonly. Without them the step says so and the account stays. -
[DEFAULT] Rung 4 of the default ladder is
frp/tcp. Spec 8 namesfrp/wss, but frps v0.71.0 cannot accept wss without a separate TLS terminator (docs/backends/frp.md), so that rung would be skipped on every tunnel with a permanent yellow warning.frp/tcpkeeps the intent of the rung (a third program with a different traffic pattern) and is a TLS session on the wire.frp/wssstays a known id for custom ladders. -
[DEFAULT] Join applies the hub's kernel profile.
deyroute joinwithout a terminal (the one-line join command) applies the balanced profile the owner chose when setting up the hub, so hub and nodes match (spec 12);optimize apply --profile offon the hub reverts it everywhere. On a terminal it asks first. -
[DEFAULT] Changing the control port. A changed
hub.control_port(config apply) takes effect at the next hub restart: the online nodes are told the new address at once (ashub announce-move) and switch at the restart; offline nodes needdeyroute node set-hub. Until the restart the firewall keeps protecting the port the hub listens on. -
[DEFAULT] Removing a tunnel's only node is refused.
node removerefuses (DEY-C008) while a tunnel has no other node; the owner gives it a backup node first or deletes it. Texts and docs say so. -
[DEFAULT] A port range counts one port map per port. Spec 10 says more than 64 port maps is an error "with the advice to use a range", but every backend forwards port by port, so
2000-2100becomes 101 port maps and a range cannot get around the limit. Supporting a range as one map would change the config schema and every backend renderer; instead the limit counts every port of a range andDEY-P016/DEY-C015advise splitting the ports over several tunnels (up to 64 each). -
[DEFAULT] The join command pins the hub's release. Spec 3 (Join 1) shows
bash <(curl -fsSL <installer>) join 'dey://…'. A release build of the hub appends--version X.Y.Z(its own version; a development build appends nothing), so the command is the spec's form plus that flag. The installer URL is thelatestone: without the flag a node would install the newest release, and a hub on another major.minor could not use it (DEY-N004, no commands). The node cannot take the hub's binary instead, because/v1/assetsneeds the certificate it gets only by joining, and the hub sendsself.updateto its nodes only to finish its owndeyroute update.docs/en/join.mdanddocs/fa/join.mdshow the flag and say why it is there. -
[DEFAULT] The integration lab's blocker and client (spec 17). The spec's
blockeris a separate container withNET_ADMINon the same network; on a Docker bridge such a container is not on the path between hub and node and cannot drop their packets.test/integration/lib.shblock/unblockrun the same nftables DROP rules in the hub's network namespace instead (input and output, established flows cut too). Theclientis a real Xray VLESS+ws+tls client (the Xray pinned inbackends.yaml) that dialshub:<port>, with a real Xray server behind the tunnel on the node (vpn_up); curl uses it with--proxyand--proxytunnel, because Xray's plain-HTTP proxying fails short responses that the server closes at once. Offline (DEY_OFFLINE=1) Xray cannot be downloaded and the client requests the hub's port directly. S04's "user IP = hub IP" is checked as: the client talks only to the hub, the VPN service on the node never sees the client's address (it sees the tunnel: 127.0.0.1 for the reverse transports, 10.77.n.1 for WireGuard) and the user's traffic leaves at the node. -
[DEFAULT] rathole and Waterwall on Tier 2 and minimal images. The pinned rathole v0.5.0 amd64 asset needs glibc 2.34 and
libssl.so.3; the Waterwall v1.46.94 assets need glibc 2.34. On Ubuntu 20.04, Debian 11 and Rocky/Alma 8 (Tier 2), or an image without OpenSSL 3, those units fail at start (DEY-B003, WaterwallDEY-B043, the loader's message in the detail), the rungs are quarantined and failover uses the other rungs: the tunnel works, rungs 3 and 7 of the default ladder do not. The arm64 rathole asset has no TLS feature, sorathole/tlscannot start on arm64. A static (musl, rustls) rathole published onbackend-buildswould remove the first limit; it is not built yet. -
[DEFAULT]
frp/quicandfrp/kcpneed UDP. They carry the tunnel over UDP between node and hub, so they setNeedsUDPlike Hysteria2 and WireGuard (spec 7.6 names only those): the UDP probe and DEY-B007 skip apply to them too. -
[DEFAULT] Waterwall reverse-reality graph. The pinned Waterwall release has no reverse-reality example file in its repository (
tests/examples,tests/casesat v1.46.94); the graph is assembled from the node documentation of that tag (docs/backends/waterwall.md lists the sources). Owner check: compare a renderedconfig.jsonwith upstream's documentation page and report a difference. -
[DEFAULT]
awg/userspaceruntime directory. The unit does not use systemd'sRuntimeDirectory=; the backend'sPreStarthook creates/run/amneziawg. When systemd restarts the unit on its own before that, amneziawg-go exits once andRestart=alwaysbrings it back within 2 s. -
[DEFAULT] Backhaul with TCP and UDP port maps. The TCP-family Backhaul transports carry TCP only. In a tunnel with both, the rung runs two Backhaul processes in one unit through
deyroute pair: the main process with the TCP maps and abackhaul/udpprocess with the UDP maps on a second control port (docs/backends/backhaul.md). Such a rung needs the UDP probe to pass, like the other UDP rungs. -
[DEFAULT] The control channel looks like ordinary HTTPS. Section 11 names ALPN
deyroute/1for the control channel. On a real hub in Iran the node's stream to the hub was cut right after the TLS handshake (no data in either direction, the node reconnecting every 45 s), while ordinary HTTPS passed: a ClientHello with no server name and an unknown protocol in the clear is what the filter singles out. Nodes therefore offer ALPNh2and send a cover server name (node.control_sni, defaultwww.digikala.com); TLS 1.3 encrypts the certificates, so nothing else of the handshake is visible. The hub acceptsh2anddeyroute/1(older nodes), and still verifies every node by its client certificate; nodes still verify the hub only against the pinned CA, never by that name. -
[DEFERRED] Measuring the rungs and preferring the fastest one. The monitoring and tuning work planned a per-tunnel benchmark of the rungs (in the canary slot, at quiet hours, within a daily byte budget) and an opt-in
failover.prefer: fastestthat moves the fastest rung to the front of the ladder. It is not built. Adopting a faster rung goes through failback, which stops the active rung and makes every user reconnect, for a gain that is often small; and a preferred rung conflicts with the canary (which tests rung 1 of the primary node) and with the primary node itself, in ways that need a decision first. Until then the ladder order is the owner's:deyroute tunnel test-laddercompares the rungs (it interrupts the tunnel) anddeyroute ladder createsets the order. -
[DEFAULT]
udp-defaulthas nowireguard/kernelrung. Section 8 listswireguard/kernelas rung 3 of the UDP-only default ladder. A filtering report of 2026-10-17 (one datacenter in Iran to Hetzner in Germany, connections started from Iran) measured plain WireGuard blocked over IPv4 (cut after about 2 s) and over IPv6 (no answer), in the direction this rung uses (the hub dials the node). DEYROUTE cannot see this before it uses the rung: the UDP test (up to three small plain datagrams echoed by the node) passes on such a network, and a UDP-only tunnel has no TCP port to probe, so its rungs are checked only by the state of their units (hub and node unit active). So DEYROUTE would choose the rung, and the filter would then cut it.udp-defaultis thereforebackhaul/udp→hysteria2/udp→direct/native, andwireguard/kernelis optional likeawg/userspace; owners add it to a ladder of their own. This is the same kind of deviation as C.31. Open for later: a WireGuard-specific reachability check before the rung is added back. -
[DEFAULT] Failback waits for rung 1's quarantine. Against filtering that lets a rung come up and then cuts it, a rung that passes a probe keeps its quarantine duration for an hour (a new cut doubles it: 10, 20, 40, 60 minutes by default), and failback never goes to a rung 1 that is still quarantined (blind failback and canary). A rung 1 that fails its probes is quarantined when the tunnel leaves it, so failback comes at the earliest when that quarantine ends: at the later of
failback_after_s(or the canary'srecover_thresholdpasses) and the end of the quarantine, plus up to one probe interval. Spec section 16 (phase 5 acceptance) and section 17 (S09) ask for failback ≤failback_after_s+ 20 s after the block is lifted; that bound now holds only whenquarantine_s≤failback_after_s. Under the defaults (300 s and 600 s) it does not: the first failback comes about 10 minutes after the switch. This also departs from section 9 phase 5, which fails back once the tunnel has been stable UP forfailback_after_s: failback now also waits for the quarantine. Lab S09 and S10 setquarantine_stofailback_after_s, S13 sets it to 5 s.tunnel resetandtunnel switchstill clear the history of the rung they move to. Owner decision: keep this, or reword the criterion.
The spec audit of 2026-10-01 found 100 gaps; the critical and major ones in code are fixed (see CHANGELOG). These remain, none of them a security issue:
- Lab coverage. The lab now runs a real Xray VLESS+ws+tls client (S04,
S08, S19, S32), the phase 4 manual switch (S32), awg/userspace (S33) and
every Forward transport from inside (S17).
wireguard/kerneldid not run in the recorded lab: the kernel of the build host has no WireGuard module (CONFIG_WIREGUARDis not set;ip link add type wireguardfails even in the privileged containers, which share that kernel). S33 and S17 add it by themselves on a host whose kernel has it (the CI runners), so its first real run is the next CIintegrationrun. Real provider firewalls, the resource budget on 1 vCPU / 1 GB servers and the 72-hour test need real servers (docs/en/acceptance.md, docs/en/benchmarks.md).