Releases: tytsxai/anyreality-resi-stack
Release list
v2.1.1 — documentation: protocol scorecard + SEO/GEO
Summary
Documentation-only release. No installer, protocol, secrets, or runtime changes. Existing nodes do not need reinstall or client re-import.
Highlights
- Protocol scorecard in README: side-by-side scoring for AnyReality vs VLESS+REALITY+XHTTP/Vision (China-facing path marked stagnant), bare AnyTLS, Hysteria2, Trojan/TLS, SS2022 — framed as the China-region best-pick argument for AnyTLS + REALITY (AnyReality).
- SEO / GEO README restructure: clear what / why / who, dry-run quick start, features, use cases, limits; GitHub About and Topics suggestions;
llms.txtentity card for AI retrieval. - FAQ, deployment, beginner, clients, comparison docs, and subscription package metadata aligned with the above.
Upgrade
No action required on already-deployed hosts. Optional: re-pull docs or re-run install only if you want the latest repo copy under /opt/anyreality-resi-stack for operator scripts.
Links
- Changelog
- README protocol scorecard
- Install (unchanged default AnyReality):
bash <(curl -fsSL https://raw.githubusercontent.com/tytsxai/anyreality-resi-stack/v2.1.1/install/install.sh) \
--node-name "US-Resi-01" \
--sni addons.mozilla.org \
--with-subscriptionPin this release with ANYREALITY_RESI_STACK_REF=v2.1.1.
v2.1.0 — production readiness
Production-readiness release. No breaking changes — re-running the installer is the upgrade path, secrets.env is reused, and no client needs to re-import.
bash <(curl -fsSL https://raw.githubusercontent.com/tytsxai/anyreality-resi-stack/main/install/install.sh) \
--node-name "US-Resi-01" --sni addons.mozilla.org --with-subscriptionRe-running on an existing host creates the anyreality-sub service account, moves the subscription services onto it, installs the logrotate policy and the operator tools, and leaves the sing-box keys untouched.
Re-pull your subscription after upgrading. Client profiles are regenerated with a full routing stack (see below). Nothing needs to be re-imported — refreshing the subscription URL in your client is enough.
Why this release exists
These are the gaps that only show up after a node has been running a while, or in the middle of an incident:
| Gap | Consequence |
|---|---|
| Client profiles had no routing rules | Everything went through the node; in TUN mode there is no direct toggle, so domestic traffic silently detoured abroad |
| Rule sets download from GitHub at first start | A blocked download left the set empty and proxied every domestic site, with no local fallback |
--ssh-port was read as "move sshd" |
Passing a port sshd was not listening on fenced off the real one at ufw --force enable |
uninstall.sh aborted partway |
UFW rules and the fail2ban jail were left behind on a "successful" uninstall |
box.log had no size bound |
Any persistent fault fills a small VPS disk, taking journald and the backup timer with it |
| Backups were never verified | A corrupt archive silently rotates out the last good one; you find out when you need to roll back |
| Backups were local-only | They die with the host |
| Subscription servers ran as root | The only publicly-listening component here held root and could read secrets.env |
Bad EnvironmentFile → bare traceback |
Endless crash loop under Restart=always with no indication of which variable was wrong |
| Accounting failure → failed request | A full /var/lib broke every client's subscription refresh, though serving the profile is the primary job |
| systemd default start limit | One transient fault could park sing-box permanently in a failed state |
| Single public-IP provider | Install fell through to a vague warning and a profile with no server address |
| No health check, no alerting | Nothing to answer "is this node healthy" |
| Leaked subscription URL | Only remedy was a full reinstall |
safe_target_path accepted .. |
FILE_DIR/.. got through; only the caller's is_file() stopped it |
Routing
The generated sing-box profiles previously carried a single ip_is_private → direct rule and sent everything else through the node. That is merely wasteful in a mixed-inbound client and unusable in TUN mode, where there is no global/direct toggle to fall back on.
- Four-layer rule stack in both
client-single.json.tmplandclient-dual.json.tmpl: baseline (sniff,hijack-dns, private-direct) → ad/trackerreject→ China-direct → a fallback that rejects UDP/443 before sending the rest to the node. The Clash templates gain the matching ad-block rule and the same inline safety net. - China-direct is deliberately two-tier. An inline 61-entry
domain_suffixlist is evaluated before the remotegeosite-cn/geoip-cnrule sets, because those download from GitHub — commonly unreachable at first start, and a failed download yields an empty set that proxies every domestic site. The inline list needs no network request, so high-traffic domestic services stay direct regardless.download_detourroutes the rule-set downloads themselves through the node. - Split DNS — domestic names resolve via
223.5.5.5, everything else over DoT through the node, withexperimental.cache_filepersisting rule sets across restarts. Content-Dispositionnow emits an ASCIIfilename="…"alongside the percent-encoded RFC 5987filename*=UTF-8''…form. Clients that cannot parse the header were inventing a numeric snowflake id for the imported profile.
Routing is documented in docs/zh-CN/ROUTING.md / docs/en/ROUTING.md, including how to verify a domain really goes direct without being fooled by a TUN client hijacking the test.
Stability
- Log rotation for sing-box —
/etc/logrotate.d/sing-box: daily, immediate rotation past 20 MiB, 7 compressed generations,copytruncate. Validated at install time withlogrotate -d. - Verified backups — the archive must list cleanly under
tar -tzfand actually containetc/sing-box/conf/andetc/anyreality-resi-stack/. A failure deletes the bad archive and exits non-zero instead of rotating out the last good backup in its favour. Archives also stop carrying/etc/sing-box/logs, which the header comment already claimed was excluded. StartLimitIntervalSec=0on all three units. systemd's default 5-starts-in-10s limit can park sing-box — the only proxy on the box — in a failed state that nothing recovers from.- Accounting failures degrade instead of failing the request. Serving the profile is the primary job; a full or read-only
/var/libnow means "the counter stops moving".
Security
- Subscription services no longer run as root. They run as the unprivileged
anyreality-subaccount with onlyAmbientCapabilities=CAP_NET_BIND_SERVICE. systemd reads the EnvironmentFile as root before dropping privileges, sosecrets.envstays 0600 root-only and is unreadable from the HTTP server itself. The subscription units additionally gainProtectKernelTunables,RestrictRealtime,RestrictSUIDSGID,LockPersonality, andSystemCallArchitectures=native. - Optional TLS for the subscription endpoint —
TLS_CERT_FILE/TLS_KEY_FILE, or--sub-tls-cert/--sub-tls-keyat install time. Standard library only, TLS 1.2 floor. The subscription URL is a credential (the profile behind it contains the node password), so plain HTTP exposes it to anyone on the path. The health check fails when the certificate expires within 7 days — expiry is otherwise a silent outage where clients refuse the subscription while liveness probes stay green. REMOTE_STATUS_URLis pinned to http(s) —urlopenwould otherwise acceptfile://and read a local path into the usage cache.- Dot entries rejected in path resolution. pathlib does not normalise
.., so the separator check letFILE_DIR/..through; the caller'sis_file()was load-bearing by accident. - Startup configuration validation. Every environment variable is range-checked; a typo exits 2 naming the variable instead of crash-looping on a traceback.
SECURITY.mddocuments the subscription URL exposure surface — the profile is served over plain HTTP on :80 and contains the node password, so the URL is a credential; anything placed inFILE_DIRis served under the same token path, which is why backups must not go there.
Installer and uninstaller
phase_firewallno longer risks locking you out.--ssh-port Nonly tells UFW which port to keep open — it does not move sshd, which is--harden-ssh— so passing a port sshd was not listening on fenced off the real one atufw --force enable. The ports sshd is actually bound to are now always allowed, with a warning. A failing--harden-sshphase is also no longer swallowed by a trailing|| true.uninstall.shno longer aborts partway through. Two[[ -f … ]] && rm …loop bodies returned non-zero when the last candidate file was absent — the normal case — whichset -Eeuo pipefailturned into an aborted uninstall that left UFW rules and the fail2ban jail behind. It now also removes the UFW rule for the port actually configured in/etc/sing-box/confinstead of assuming 443 (deliberately leaving SSH rules alone), plus the new/usr/local/sbinoperator tools and/etc/logrotate.d/sing-box.- Public IP detection is no longer a single point of install failure. Detection tries four independent providers with a 5 s timeout each and validates the result is an IPv4 address;
api.ipify.orgalone is blocked or rate-limited often enough that fresh installs produced a profile with no server address. --dry-runworks on a host that already hassecrets.env. The reuse path returned without defining the template variables, so the next phase died on an unbound variable; dry-run now exports placeholders instead of reading real secrets.- A flag given without its value (
--snias the final argument) died with a bare$2: unbound variablefromset -u. The parser now names the flag.
Operations
-
anyreality-resi-stack-healthcheck— read-only, 15 checks (service state and restart count,sing-box check, inbound port listening, subscription/healthz, TLS certificate expiry, profile file, backup freshness/failure, off-box hook presence, disk,box.logsize, clock sync, UFW,secrets.envpermissions). Non-zero when degraded, silent while healthy, so one cron line is a complete alerting setup:*/10 * * * * /usr/local/sbin/anyreality-resi-stack-healthcheck --quiet -
anyreality-resi-stack-rotate-sub-token— rotates the subscription token in place, verifies the new path serves a profile, and rolls back automatically if the service does not come back healthy. -
Off-box backup hooks — executables in
/etc/anyreality-resi-stack/backup-hooks.d/receive the verified archive path. A failing hook fails the backup run, because an off-box copy you believe exists but does not is the dangerous case. A non-executable10-offsite.sh.exampledocuments the contract on the box, and the health chec...
v2.0.0 — AnyReality default + rebrand
[2.0.0] — 2026-07-19
Project renamed to
anyreality-resi-stack(formerlyreality-resi-stack). GitHub automatically redirects old repository URLs, so existingcurl | bashinstall commands keep working. Runtime filesystem paths (/etc,/var/lib,/usr/local/lib,/var/backups), systemd unit names, the backup script/archives, and the environment variable (nowANYREALITY_RESI_STACK_REF, with the legacyREALITY_RESI_STACK_REFstill honored) are all renamed to theanyreality-resi-stackprefix. Upgrading a v1.x host runs a migration phase (phase_migrate_legacy_paths) that moves the oldreality-resi-stackdirectories to the new prefix and retires the old backup unit, so existing secrets, usage state, backups, and therefore already-imported clients are preserved.Breaking — default protocol changed. Fresh installs now default to AnyReality (AnyTLS + Reality) instead of VLESS + Reality + xtls-rprx-vision. Existing servers keep their current protocol until the installer is re-run. AnyReality is sing-box-only; if you rely on Clash/mihomo clients, install (or re-run) with
--protocol vless-visionto stay on the legacy protocol. Because AnyReality authenticates with a password rather than a UUID/flow and the default subscription file changed fromprofile.yamltoprofile.json, clients must be re-imported after switching protocols.
Changed
- AnyTLS + REALITY (AnyReality) is now the default protocol. New installs deploy a sing-box
anytlsinbound fronted by Reality, authenticated with a per-server password (ANYTLS_PASSWORDinsecrets.env). AnyTLS's custom padding hardens against TLS-in-TLS fingerprinting while Reality keeps the certless server camouflage. Still no domain or TLS certificate required. - The default subscription profile is now a full sing-box client config served as
profile.json(mixed inbound on127.0.0.1:2080, AnyReality outbound, domain-based routing in dual-node mode); the legacy Clashprofile.yamlis served only under--protocol vless-vision. - Runtime layout renamed to the
anyreality-resi-stackprefix:/etc/anyreality-resi-stack,/var/lib/anyreality-resi-stack,/usr/local/lib/anyreality-resi-stack,/var/backups/anyreality-resi-stack,/opt/anyreality-resi-stack, thebackup-anyreality-resi-stack.shscript andanyreality-resi-stack-*.tar.gzarchives, and theanyreality-resi-stack-backupsystemd units. A newphase_migrate_legacy_pathsinstaller phase migrates existingreality-resi-stackhosts in place;uninstall.shalso cleans up either prefix.
Added
--protocol anytls-reality(default) /--protocol vless-vision(legacy) selects the inbound protocol. VLESS + Reality + xtls-rprx-vision remains fully supported for Clash/mihomo users, which cannot parse AnyReality.- AnyReality templates:
templates/singbox/11_anytls-reality_inbounds.json.tmpl(server inbound),templates/singbox-client/anytls-outbound.json.tmpl, and full sing-box client configstemplates/singbox-client/client-single.json.tmpl/client-dual.json.tmpl(dual-node smart routing via sing-boxrouterules). - Installers predating AnyReality mint and append an
ANYTLS_PASSWORDto an existingsecrets.envon re-run, so switching to--protocol anytls-realityworks without regenerating the UUID or Reality keypair. Re-running with a different protocol drops the stale inbound and profile file to avoid port collisions or format mismatches.
Fixed
- Leaf subscription accounting now samples interface counters while holding the state lock, preventing concurrent requests/background polling from applying stale samples out of order.
- Aggregator usage cache writes now use per-thread temporary files plus atomic replace, avoiding
.tmpcollisions during concurrent refreshes. - Backup archives now exclude hidden runtime
.tmpstate/cache files and placemanifest.txtat the archive root. - Leaf subscription accounting now keeps usage fresher by sampling in the background every
USAGE_POLL_INTERVAL_SECONDSseconds instead of only updating when a client pulls the subscription URL. - Leaf subscription accounting now supports provider billing reset days via
BILLING_CYCLE_DAY, so plans that reset on the 11th do not roll over on the 1st by mistake. - Leaf subscription accounting now counts bytes already present in the current boot on first state creation by default (
COUNT_CURRENT_BOOT_ON_INIT=true), while still supporting baseline-only mode andUSAGE_OFFSET_BYTEScalibration. - Leaf accounting now carries usage forward across reboots or NIC counter rollovers by adding the new boot's current counter instead of silently dropping it.
- Aggregator subscription accounting now refreshes the leaf status cache in the background via
REMOTE_POLL_INTERVAL_SECONDS, keeping usage cards warm even before the next client request. - Re-running the installer with an existing
secrets.envre-exports the reused UUID, Reality keys, subscription token, and short ID before rendering templates. --with-subscriptionand--with-aggregatorare now mutually exclusive, and aggregator installs fail early unless the residential-node template variables are provided.
Added
ANYREALITY_RESI_STACK_REFlets remote-piped installs fetch a specific branch or tag while defaulting tomain.- Standard-library
unittestcoverage for leaf accounting and aggregator cache fallback, wired intomake testand GitHub Actions. make mdchecknow falls back tonpx --yes markdown-link-checkwhen the binary is not installed globally, retries transient link-checker failures once, and GitHub Actions runs the same Markdown link gate.
Security
- Aggregator leaf-status polling now caps each remote status response with
MAX_REMOTE_STATUS_BYTES(default 64 KiB) before parsing JSON. - Subscription systemd units now use basic sandboxing (
NoNewPrivileges,PrivateTmp,ProtectHome,ProtectSystem=strict) and only keep/var/lib/anyreality-resi-stackwritable. - Config backups now exclude runtime usage/cache state, set backup directory permissions to
700, and write archives as600.
v1.0.3 — Documentation: FAQ + llms.txt + residential-IP GEO/SEO
📝 Documentation release
Documentation-only release on top of v1.0.2. Installer, sing-box config, subscription server, and dual-node aggregator are unchanged. No need to re-deploy if you're on v1.0.2.
Added
llms.txt— AI-search-engine index that explicitly positions the project for the residential-IP-as-asset use case and answers the questions people actually search for:- "Why is Telegram slow on my residential VPS?"
- "Why does OpenAI block my data-center VPS but not residential?"
- "Does Reality protocol need a domain?"
- "Is the installer idempotent?"
- "How is this different from 3x-ui / XHTTP-Installer?"
- README — FAQ section with the same 7 Q&As in bilingual format.
- README — Keywords block + nav row (Release / Docs / llms.txt / Changelog / Issues).
Why this matters for discovery
- Generative search (ChatGPT / Claude / Perplexity / Gemini) can now ingest `llms.txt` and answer "what's the right VLESS toolkit for a residential-IP VPS that needs OpenAI on residential but Telegram fast?" with a direct pointer here.
- Long-tail SEO keywords newly covered:
- 中文:OpenAI 住宅 IP 出口、ChatGPT 住宅 IP 代理、Telegram 住宅 IP 卡顿、Discord 住宅 IP 降权、双节点智能分流、Clash 域名分流住宅
- English: residential IP VLESS, sing-box residential installer, residential VPS smart routing, alternative to 3x-ui for residential VPS
Full changelog
See CHANGELOG.md.
v1.0.2 — refuse install when manual sing-box exists
Fixed
- Preflight now refuses to proceed if a manually-installed sing-box already exists on the target host (binary at
/usr/local/bin/sing-boxwithout a matching apt-managed/usr/bin/sing-box, or any systemd unit matchingsing-box*.serviceother than the standardsing-box.service). Without this check,apt install sing-boxsilently adds a second, independent binary + systemd unit + config dir alongside the existing manual install — peacefully coexisting until the next reboot, at which point both copies race on ports 443/8443. Hard-learned during v1.0.1 verification on a real host.
Upgrade
bash <(curl -fsSL https://raw.githubusercontent.com/tytsxai/reality-resi-stack/v1.0.2/install/install.sh) \
--node-name "US-Resi-01" --sni addons.mozilla.org --with-subscriptionTested on
- Real Ubuntu 24.04.4 LTS host with pre-existing manual sing-box → preflight correctly aborts with actionable message
- Real Ubuntu 24.04.4 LTS host without prior sing-box install (v1.0.1) → full GPG verification + apt install path verified end-to-end
v1.0.1 — fix install-blocking GPG fingerprint
Fixed (critical)
SINGBOX_APT_KEY_FPRwas a placeholder value in v1.0.0 that did not match the real Sagernet GPG key bundle. Every real install would have died atphase_install_singboxwith a fingerprint mismatch. This bug was not caught by--dry-runbecause dry-run intentionally skips the GPG check. Verified against the livehttps://sing-box.app/gpg.keyon Ubuntu 24.04 LTS.phase_verifyran live checks in--dry-runmode, producing fake-looking failures and a non-zero installer exit even though nothing had been installed. Now correctly no-ops in dry-run.- GPG verification logic now requires the pinned fingerprint to be present anywhere in the bundle rather than to be the first fingerprint — Sagernet bundles a primary key plus a signing subkey, so the first-fingerprint check was fragile against subkey rotation.
Upgrade
bash <(curl -fsSL https://raw.githubusercontent.com/tytsxai/reality-resi-stack/v1.0.1/install/install.sh) \
--node-name "US-Resi-01" \
--sni addons.mozilla.org \
--with-subscriptionIf you already cloned v1.0.0, just git -C /opt/reality-resi-stack pull --tags && git -C /opt/reality-resi-stack checkout v1.0.1 then re-run the installer (it is idempotent).
v1.0.0 withdrawal
v1.0.0 has been marked withdrawn. It will install nothing on a fresh box because it dies at the GPG check before sing-box is installed — meaning v1.0.0 is harmless to those who tried it, just blocked.
v1.0.0 — initial release
⚠️ WITHDRAWN — DO NOT USE
This release contained an install-blocking GPG fingerprint bug. Every real install would have aborted at phase_install_singbox. The bug was caught and fixed in v1.0.1 ~30 minutes after publication. v1.0.0 is harmless (it doesn't install anything before dying), just unusable.
Use v1.0.1 instead.
Original v1.0.0 release notes preserved below for historical reference, but do not deploy this version.