decenzed-node is a standalone VLESS proxy server you run on your own machine
(Windows, macOS, Linux). It runs an embedded xray-core and prints share links
you hand to friends. There is no coordination server — it's fully
self-contained and open source.
- VLESS (XTLS-Vision), Trojan, Shadowsocks classic + Shadowsocks-2022 protocols
- Two camouflage modes: REALITY, or TLS behind your own auto-hosted website
- Automatic Let's Encrypt certificates (obtain + renew) for the TLS mode
- One subscription link per client — apps pull every protocol automatically
- Per-user speed cap and per-client / per-protocol traffic stats
- Per-user domain & IP filtering (blacklist / whitelist) — geosite/geoip
categories, custom lists, and
geoip:privateLAN-egress block on by default - DDNS (DuckDNS) for dynamic IPs
- Wide architecture support — x86, ARM, MIPS (desktops + OpenWRT routers)
- Single self-contained binary on embedded xray-core, no coordination server
It offers two camouflage modes for VLESS/Trojan (you pick one in setup):
- REALITY (default) — scans for a live third-party TLS 1.3 + HTTP/2 site to borrow as cover and generates its own REALITY keys. No domain or certificate of your own required.
- TLS + your own website — masquerades behind your own domain. The website is raised automatically by the node — you don't create or host anything yourself: it serves a small built-in site, obtains a Let's Encrypt certificate automatically (DNS-01 via DuckDNS, so no port 80 needed), and xray falls back to that site on any non-proxy traffic. To a probe or a stray browser the node is just an ordinary HTTPS website on your domain. The same domain doubles as your dynamic-DNS host and the certificate/website host — that's by design, nothing separate to set up. Needs a DuckDNS domain.
- A public IP (static or dynamic). If the machine is on a home LAN, forward its port on the router. CGNAT is not supported (some ISPs / mobile put you behind carrier NAT — inbound connections can't reach you).
- If the node runs on a desktop/laptop behind a home router, give that machine a fixed LAN IP via a DHCP reservation (bind its MAC to one address in your router's DHCP settings). Otherwise its LAN IP can change on reboot/lease renewal and your port-forward will silently point at the wrong device.
- Open/forward one TCP port for VLESS (default 8443). One TCP port hosts
exactly one protocol — if you also enable Trojan and/or Shadowsocks,
each needs its own additional forwarded/opened TCP port.
setupasks for every port the same way and pre-fills a free one (Trojan32000–35000, Shadowsocks35000–38000). - Admin/root to install and manage the background service. The interactive CLI requests it automatically (see §3); on OpenWRT you already run as root.
- A duckdns.org subdomain — required for the TLS camouflage mode (certificate + DNS-01), optional otherwise (a stable domain in share links).
Download the prebuilt binary for your OS, or build from source (Go 1.26+):
cd src
go build -o decenzed-node ./cmd/decenzed-node # Windows: decenzed-node.exePut it in a folder you can write to — it keeps its data in a decenzed-data/
folder next to the executable (config, xray.json, stats, logs).
You can run the node on the router itself. One line installs the correct
binary for your router's CPU (arm64, armv7, mipsel, …) from the latest
release, verifies its SHA-256, and puts decenzed-node on your PATH:
wget -O - https://github.com/icecube092/decenzed-node/releases/latest/download/install-openwrt.sh | shThen, on the router:
decenzed-node setup # network check + port 8443 (443 is taken by LuCI) + camouflage
# (REALITY or your own TLS site), then installs the procd boot service
decenzed-node link # the connection link to paste into your client
# later: decenzed-node check # re-verify the RUNNING node is reachable from outsideNotes for routers:
- The flash-tight router binaries (32-bit ARM and 32-bit MIPS) are
UPX-compressed (~10–12 MB on disk; ~30 MB uncompressed, decompressed
into RAM at startup). Everything else ships uncompressed — desktop/server
(amd64, 386, Windows, macOS) and arm64 routers (which have ample flash) to avoid
antivirus false positives, and the 64-bit MIPS arches because UPX can't pack
them. Devices with only 8–16 MB of
flash may still need USB storage +
extroot:
re-run the installer with
DIR=/mnt/usb, and/or setDECENZED_DATA=/mnt/usb/decenzed-dataso config/logs live off the flash. - On OpenWRT the background service is managed by procd (
/etc/init.d/decenzed-node), soservice install|status|start|stop|restartwork natively, andupdateself-replaces the binary and restarts the service. - Domain/IP filters: the
geosite.dat/geoip.datdata files are several MB each and load into RAM — heavy for flash-tight routers. They're fetched only if you use ageosite:/geoip:COUNTRYsource; prefer custom text lists orgeoip:private, or move data to USB withDECENZED_DATA. See Per-user domain & IP filters. - Manual control:
/etc/init.d/decenzed-node {start|stop|restart|status}; logs vialogread -e decenzed.
How it works on a router — where the port points. The node is a TCP server: it listens on your chosen port (e.g. 8443) on the router, and clients connect inbound to it. What you do to expose that port depends on where the node sits:
- Node on your main/edge router (the box that holds the public IP — e.g. a
Routerich AX3000 as your primary router). The listening port is already on the
WAN edge; there is nothing to "forward". OpenWRT blocks WAN input by default, so
the port just has to be allowed through the firewall — and the installer
does this for you: it adds an idempotent
Allow-decenzed-noderule accepting inbound TCP8443from WAN. Chose a non-default port? Re-run the installer withPORT=<port>(or edit that rule in LuCI → Network → Firewall → Traffic Rules). To skip the automatic rule entirely, install withNO_FIREWALL=1. - Node on a router behind another router/ISP box. Forward TCP 8443 on the
upstream box to this router's LAN IP (Port Forwarding / Virtual Server), exactly
as
decenzed-node checkprints.
External port ≠ internal port (443 → 8443). If you want clients to connect on
a tidy public port (e.g. 443) but the node listens on a different local one
(e.g. 8443, since 443 is often taken by LuCI), forward WAN 443 → LAN 8443
on your router and tell setup about it: after the bind-port prompt, setup asks for
the external port clients dial (default same) — enter 443. The node keeps
binding 8443, while share links, subscriptions, and self-checks all use
443. Each protocol has its own bind/external pair; stats and check show the
mapping as 443(->8443) when the two differ.
Either way you need a real public IP. Behind CGNAT (your ISP shares one
IP across many customers) inbound connections can't reach you — ask your ISP for
a public/"white" IP, or front the node with a cheap VPS. decenzed-node check
detects CGNAT/private IPs and warns you.
VLESS is always on. setup asks a separate y/n question for each optional
protocol:
- Trojan — shares VLESS's camouflage (REALITY, or TLS with a fallback to your website; no XTLS flow).
- Shadowsocks — classic
chacha20-ietf-poly1305, multi-user. The broadest client support; no TLS/REALITY masking (a distinct, less-stealthy traffic type). - Shadowsocks-2022 —
2022-blake3-aes-128-gcm, stronger, but many clients reject it, so it's offered as a separate inbound/port for the clients that do accept it. (If a client won't import your Shadowsocks entry, use the classic one — or VLESS/Trojan.)
If you answer yes, setup asks for that protocol's port, pre-filled with a
random free port from a recommended range (Trojan 32000–35000, Shadowsocks
35000–38000); on a re-run your saved port is offered as the default, even if
it's currently in use. VLESS keeps 8443.
One TCP port hosts one protocol, so each enabled protocol needs its own
forwarded/opened port. decenzed-node link prints one subscription link per
client (TLS mode) covering every enabled protocol. Per-user speed caps and traffic
stats apply across all of them (each client is metered by a single identity
regardless of which protocol it connects with). On OpenWRT open the extra WAN ports
by re-running the installer with the full list, e.g.
PORT="8443 33001 36001 36002" ./install-openwrt.sh (space- or comma-separated).
Every release ships prebuilt static binaries (pure Go, CGO_ENABLED=0, no libc
dependency — they run on glibc and musl/OpenWRT alike). install-openwrt.sh
auto-detects the router's CPU (uname -m + ELF endianness) and downloads the
matching one.
Desktop
| OS | Asset |
|---|---|
| Windows x86-64 | decenzed-node-windows-amd64.exe |
| macOS (Intel) | decenzed-node-darwin-amd64 |
| macOS (Apple Silicon) | decenzed-node-darwin-arm64 |
| Linux x86-64 | decenzed-node-linux-amd64 |
Routers / OpenWRT
| Asset | Go arch | Typical OpenWRT targets & CPUs |
|---|---|---|
decenzed-node-linux-arm64 |
arm64 (aarch64) | Filogic MT798x — Routerich AX3000, mvebu, ipq807x, bcm27xx |
decenzed-node-linux-armv7 |
arm, GOARM=7 | ipq40xx, mvebu (32-bit), sunxi, bcm53xx |
decenzed-node-linux-armv6 |
arm, GOARM=6 | bcm2708 / older ARMv6 |
decenzed-node-linux-armv5 |
arm, GOARM=5 | kirkwood / older ARMv5 |
decenzed-node-linux-mipsle-softfloat |
mipsle, softfloat | ramips (mt7620/mt76x8), rt305x — most budget routers |
decenzed-node-linux-mips-softfloat |
mips, softfloat | ath79, lantiq (big-endian MIPS) |
decenzed-node-linux-mips64le |
mips64le | octeon / newer 64-bit MIPS (LE) |
decenzed-node-linux-mips64 |
mips64 | 64-bit MIPS (BE) |
decenzed-node-linux-amd64 |
amd64 | x86_64 routers & VMs |
decenzed-node-linux-386 |
386 | legacy x86 |
MIPS builds use GOMIPS=softfloat because router SoCs have no FPU. If your device
isn't covered, open an issue with the output of uname -m and
. /etc/openwrt_release; echo "$DISTRIB_TARGET".
- Interactive shell — double-click the
.exe(Windows) or run with no arguments. A prompt opens where you type commands. Typeqto leave the current command (setup, debug,logs -f, …) and return to the prompt; exit the shell withexit/quit/qor Ctrl+D. - One command —
decenzed-node <command>. - Service — once installed, it runs in the background on boot.
- Admin only when needed — elevation (Windows: UAC prompt; Linux/macOS:
sudo) happens once when you open the interactive shell, and for one-shot commands only when they need it (service,update,setup, and the config-changinglink add/remove/editanddebug, which restart the service). Read-only commands (version,link,stats,config,check,logs) never prompt. On OpenWRT you're already root, so nothing changes. Skip elevation entirely withDECENZED_NO_ELEVATE=1.
decenzed-node setupThe wizard starts with a network readiness check, in order:
- Detects your public IP (warns if it looks like CGNAT) and auto-detects
your country (used to label proxies, e.g.
RS [VLESS]). - Picks the VLESS TCP port (default 8443), then prints step-by-step port-forward instructions for your router.
- Self-checks that port from your public IP (it spins up a temporary listener, since the node isn't running yet).
- Runs a speed test.
It then asks a y/n for each optional protocol (Trojan, Shadowsocks, and Shadowsocks-2022), each on its own port pre-filled with a free one — see the Extra protocols section.
Then it asks the policy questions — press Enter to keep the value shown in
[brackets], or type no to clear/disable it:
- Blocked protocols (default
bittorrent;no= block none). - Block private/LAN IP ranges (default yes, recommended) — stops proxy users from reaching the node's own local network. See Per-user domain & IP filters.
- Default domain filter for new users (default
no) — a mode (blacklist/whitelist) + sources applied to newly created clients only. - Per-user speed cap (default 50 Mbit/s;
no= unlimited). - Domain for share links (so links keep working when your IP changes). Setup
first asks whether it should set up DuckDNS for you:
- No — you bring your own domain (one you bought, or one already kept
pointed at this node's IP by another program). Just type it in; the node uses
it in links but never updates it — your own DNS is responsible for that. If
you have a dynamic IP and don't want the built-in DuckDNS updater, you can
point your own domain at the node and run
ddclient yourself to keep its A record
updated. Answer
nohere too if you have no domain — links then use the raw public IP. - Yes — the DuckDNS flow: it asks for your token, then the
subdomain you created on duckdns.org — DuckDNS
does not auto-create it, so sign in, add a subdomain, and enter its label
without
.duckdns.org. The node then keeps<subdomain>.duckdns.orgpointed at your current IP.
- No — you bring your own domain (one you bought, or one already kept
pointed at this node's IP by another program). Just type it in; the node uses
it in links but never updates it — your own DNS is responsible for that. If
you have a dynamic IP and don't want the built-in DuckDNS updater, you can
point your own domain at the node and run
ddclient yourself to keep its A record
updated. Answer
- Public IP for share links (
no= auto-detect each time; only asked when you configure no domain at all).
Then it asks for the camouflage mode (reality or tls):
reality— scans for a REALITY camouflage domain (a live TLS 1.3 + HTTP/2 site near you) and generates your REALITY keypair.tls— masquerade behind your own website (requires a DuckDNS domain, configured just above). It asks the Let's Encrypt account details: a contact email and acceptance of the Subscriber Agreement, then obtains the certificate right away over DNS-01 (so a misconfiguration fails now, not at first start). No domain scan is done in this mode.
It then creates your first client, writes the xray config, and — as its final step — offers to install & start the boot service (needs admin/root). It prints your first share link at the end.
Testing the TLS mode. The staging-vs-production Let's Encrypt CA is fixed at build time. Normal binaries use production; build a test binary with the Let's Encrypt staging CA (untrusted certs, far higher rate limits) via
make build-test/make build-test-win, orgo build -tags staging.
decenzed-node checkRun this once the node is up (setup installs the service for you). It shows your public IP, runs a speed test, refreshes your DuckDNS record, and dials back to your own domain/IP on every enabled protocol's port to confirm the running service is reachable from outside; disabled protocols are reported as such. If a port isn't reachable it prints port-forward instructions (listing the ports to open). (A serving machine mostly uploads, so ≥10 Mbit/s upload is recommended; the loopback self-check may fail from inside your own LAN even when forwarding is correct — test from mobile data to be sure.)
setup already installs and starts the boot service on its last step. To manage
it directly:
decenzed-node service status
decenzed-node service install # (re)install + start now (needs admin/root)
decenzed-node service restart # apply a new binary after 'update'Or run in the foreground for a quick test: decenzed-node start.
decenzed-node link # subscription link per client
decenzed-node link -l # + one line per per-protocol connection link
decenzed-node link -s # + a sing-box outbound per protocol
decenzed-node link add alice # create a client (prompts for its domain filter)
decenzed-node link edit alice # configure a client's per-user domain/IP filter
decenzed-node link remove alice # revoke a friendIn TLS mode each client gets one subscription link —
https://<your-domain>:<port>/sub/<client-id> — that you paste into a client
(v2rayN/NG, nekobox, Hiddify, sing-box, …) as a subscription. The app fetches
every enabled protocol (VLESS/Trojan/Shadowsocks) from it automatically, names
the profile Decenzed-<node-id>, and labels each proxy by location+protocol
(e.g. RS [VLESS]); it picks up changes on refresh. The subscription is served
by the node's own decoy website, behind xray's TLS fallback on your domain —
so it needs no extra port and looks like an ordinary HTTPS request. Use -l
to also print each protocol's raw link (to copy individually) or -s for
sing-box outbounds. (In REALITY mode there's no hosted subscription, so link
prints the per-protocol links directly.) Adding/removing a client restarts the
service so the change takes effect immediately.
decenzed-node stats # protocols, per-client/per-inbound traffic, status
decenzed-node logs # tail the log (both app and xray)
decenzed-node logs xray -f # follow only xray's logs (type q to stop)
decenzed-node debug # toggle verbose logging (all xray logs)
decenzed-node config node|xray # inspect app-config / generated xray JSON
decenzed-node update # check for a newer version; ask before installingupdate checks the release manifest and, only if a newer version exists, asks
y/n before downloading. After installing it restarts the background service
and re-launches the CLI so the session runs the new version immediately (the
running process keeps the old code in memory until it restarts — replacing the
binary file alone doesn't change an already-running program, on any OS).
After an update, re-run decenzed-node setup. A new version may add config
fields or change defaults (e.g. new protocol options or the split bind/public
port). Re-running setup re-asks every field with your current value as the
default — press Enter to keep each one — and regenerates xray.json from the
updated schema, so nothing is lost and the node picks up any new behaviour. Then
decenzed-node check re-verifies the running node is still reachable.
stats shows the enabled protocols, whether debug mode is on, and lifetime
traffic broken down per protocol (across all clients) and per client
(across all protocols, with the per-user speed cap noted there — the overall load
is uncapped). xray exposes per-user and per-inbound counters separately but not
their cross, so a full per-protocol-per-client matrix isn't available.
Logs are one file, each line tagged app (the node) or xray (the embedded
core). logs app/logs xray filter by source; -f follows. Runtime xray
errors land here too. debug turns on verbose logging (every xray line,
including debug) until you turn it off; the log is capped at 10 MB (the oldest
lines are dropped, recent history kept).
To change any setting, re-run decenzed-node setup — it re-asks every field
with the current value as the default and rebuilds xray.json. You never edit
xray JSON by hand.
Each client can have its own domain/IP filter, enforced by xray itself (routing rules keyed by the client). A filter has a mode and a list of sources:
- blacklist — block the listed targets, allow everything else.
- whitelist — allow only the listed targets, block everything else.
Configure a client interactively — link add prompts for it on creation (pre-filled
with the setup default), and link edit <name> opens a small REPL:
decenzed-node link edit alice
user> mode blacklist
user> domains geosite:category-ads-all, geoip:ru, file:blocked
user> done # saves + reloads the service (q cancels)
setup asks for a default mode + sources applied to new users only
(existing clients are never touched). stats shows each client's active filter.
| Source | Matches | Needs a data file? |
|---|---|---|
domain:example.com |
that domain + subdomains | no |
full: / keyword: / regexp: |
explicit xray domain matcher | no |
example.com (bare) |
same as domain: |
no |
geosite:CATEGORY |
a domain list (e.g. geosite:category-ads-all) |
geosite.dat |
geoip:CODE |
destination IP by country (e.g. geoip:ru) |
geoip.dat |
geoip:private |
private/LAN/loopback ranges | no (expanded to CIDRs) |
file:NAME |
your text list at decenzed-data/domains/NAME.txt |
no |
url:https://… |
a remote text list (e.g. hosted on GitHub), cached locally | no |
ext:FILE.dat:CATEGORY |
a category in your own .dat |
that .dat |
There's no ! negation in xray — "allow everything except X" is just
blacklist mode with X; "only X" is whitelist mode.
A custom list is a plain text file, one source per line — any source from
the table above (bare domains, domain:, geoip:, …). Blank lines and #
comments are ignored (a trailing # … on a line is stripped too):
# my blocklist
domain:ru # all .ru domains + subdomains
mail.ru
vk.com
geoip:ru # + Russian IPs (needs geoip.dat)
geoip:private # + LAN ranges
Local file — save it as decenzed-data/domains/<NAME>.txt (create the
domains/ folder next to the binary if it doesn't exist) and reference it by its
name without the path or .txt:
decenzed-node link edit alice
user> mode blacklist
user> domains file:blocklist # -> decenzed-data/domains/blocklist.txt
user> done
Remote list (GitHub, etc.) — host the same text file anywhere reachable over HTTPS (a GitHub repo works well) and reference it by its raw URL:
user> domains url:https://raw.githubusercontent.com/<you>/<repo>/main/blocklist.txt
On GitHub, open the file and click Raw to get the
https://raw.githubusercontent.com/… URL. The node downloads and caches the
list into decenzed-data/domains/ when you add it, and decenzed-node update
re-fetches every url: list in your config (if a refresh fails, the last cached
copy is kept). You maintain only the file in your repo — no local editing, and it
works offline from the cache. Lists combine, so you can mix
file:blocklist, url:https://…, geosite:category-ads-all, geoip:ru in one filter.
- geosite = domain lists (matched against the sniffed domain/SNI). Best for
categories of services (ads, trackers, social, …).
Loyalsoldier's geosite is
China-centric — it has
geosite:cn,geolocation-!cn,category-ads-all,gfw, … but no per-country domain lists (nogeosite:ru). The full category list lives in v2fly/domain-list-community. - geoip = IP ranges by country (
geoip:ru,geoip:cn,geoip:us, … all two-letter codes) plusgeoip:private,geoip:cloudflare,geoip:telegram, …. Best for "by country of the destination server" and catching direct connections to an IP (no domain to sniff). Using a country geoip source switches xray todomainStrategy: IPIfNonMatch(a DNS lookup per otherwise unmatched connection) so it also applies to domain traffic.
geoip:private is blocked globally by default (the setup question above) as an
egress-safety measure, and needs no download.
geosite.dat and geoip.dat are several megabytes each (and grow over time),
downloaded into decenzed-data/domains/ and loaded into RAM by xray at start.
On budget OpenWRT routers with only 8–16 MB of flash / limited RAM this can be
too much. Recommendations for routers:
- The node only downloads them if you actually use a
geosite:/geoip:COUNTRYsource — if you don't, nothing is fetched. - Prefer custom text lists (
file:NAME, a few KB) orgeoip:private(no file at all) over the big.datfiles where possible. - If you need the
.datfiles, put data on USB/extroot and setDECENZED_DATA=/mnt/usb/decenzed-dataso they live off the router's flash. - Country geoip matching also enables per-connection DNS resolution
(
IPIfNonMatch) — extra CPU on weak SoCs.
The files are downloaded on demand when you add such a source (with a y/n prompt),
and decenzed-node update refreshes them (geosite/geoip before the binary),
only for the files you actually use. They come from the
Loyalsoldier/v2ray-rules-dat
release (verified by its SHA-256 sidecar); override the source with the
DECENZED_GEOSITE_URL / DECENZED_GEOIP_URL env vars.
- Per-user speed cap is enforced by a small throttle proxy in front of xray (keyed by client source IP) — application-level, no OS/tc config.
- Data lives next to the binary in
decenzed-data/so the CLI and the service (which may run as a different user) share the same files. - TLS mode stores the Let's Encrypt certificate, key, and ACME account key in
decenzed-data/(cert.pem,key.pem,account.key). The node renews the certificate automatically (checked daily, renewed ~30 days before expiry) and xray hot-reloads it with no restart and no dropped connections. The built-in decoy website serves on127.0.0.1and is reachable only through xray's TLS fallback, never directly. - Uninstall the service:
decenzed-node service uninstall.
This project is licensed under the GNU Affero General Public License v3.0. See the LICENSE file for the full license text.