Skip to content

Releases

v0.8.22 — xray loopback guard, one holder per credential, MTProto instance scoping, silent-core recovery, L2TP upgrade billing, x/net v0.62.0

Choose a tag to compare

@Free-Guy-IR Free-Guy-IR released this 04 Nov 14:38

Release commit e4b18be7589a76a6f1abb01f2d9f44284aba6a2e on main. Previous release: v0.8.21 (347da494). This
release carries the 41 commits in v0.8.21..v0.8.22 (86 files, +5673 / -308); every one is listed by
git log v0.8.21..v0.8.22. NodeVersion 0.6.22 (e05d805b). One patch step after v0.8.21.

Before the fleet rollout

  • One node must pass the L2TP upgrade check below (OBS-a5) before any node is moved from 0.6.20, because that is the
    path v0.8.21 billed wrongly.
  • Any panel config that gives two users the same VLESS id, VMess id or Trojan password now gives the credential to one
    of them only (C2). Check the panel for shared credentials before the rollout.

Xray: no user reaches a loopback destination (C1)

  • Own control ports (a6af5dd7 RED, 90759cd5 fix, 7fd97ebe pins): ApplyAPI writes two rules for this instance's
    API and metrics ports, ahead of every panel rule, to a blackhole outbound the node adds itself. They cover loopback,
    unspecified, link-local and the host's interface addresses, plus localhost, *.localhost and numeric host forms
    (2130706433, 127.1, 0x7f000001, localhost.). They live in the config every clone, Restart and
    restorePreviousConfig replays, and ApplyAPI stays idempotent.
  • Every loopback destination on the user inbounds (decd7f1e test infra, e8594146 RED, 2e5f9649 fix): two more
    rules send 127.0.0.0/8, 0.0.0.0/8, 169.254.0.0/16, ::1/128, ::/128, fe80::/10 and the IPv4-mapped forms, plus
    localhost and *.localhost, on every port, to the blackhole, only for inbounds the node manages users on (vless,
    vmess, trojan, shadowsocks, hysteria). Panel dokodemo-door and other non-user inbounds keep the panel's routing.
  • Known residual, accepted: under routing.domainStrategy AsIs, a public domain name whose DNS record points at
    loopback is matched by neither rule. Under IPIfNonMatch or IPOnDemand the address rule catches it. Operators of
    cores set to AsIs should add their own private-address block (for example geoip:private to a blackhole) or move
    the core to IPIfNonMatch.

Xray: one holder per credential on every inbound (C2)

  • Core patch (fbaddb22 RED, 6283e614 fix): patches/xray-validator-credential-owner.patch, the 20th fork patch at
    the xray pin d2758a0. VLESS and Trojan validators refuse a key another email holds and remove only what still
    belongs to the removed user; VMess refuses a command key whose decoder belongs to another email and removes a decoder
    only while the removed user holds it. The Dockerfile guards every changed file.
  • Node holder logic (bb2f4b63 RED, 86959bde fix, new backend/xray/credential.go): a user whose credential another
    user holds is refused and waits; it is installed when the holder leaves, including through UpdateUsersAndRestart. A
    removed user never takes another user's credential. A user that moves onto a held credential is refused and its old
    account leaves the core.
  • A batch frees the keys its users move off before it admits anyone, and revokes every replaced account before adding
    (db45fd9c RED, 3adeb7c8 fix).
  • Tests: 8083f007, 40dd004b, 96c12a19, 86da26e5.

Xray: a silent core is restarted after its full sync was abandoned

  • A health restart aborts a silent full sync even after its caller gave up (2d195875 RED, 92504e43 fix).
  • Readiness bounds are per-Xray fields with the production defaults (32147e60 RED, 388b3581 fix); an abort racing
    a core that turns ready ends in one clean outcome (f498a706, interleaving test).

MTProto: instance scoping and per-instance live cut (C3/C4)

  • Each MTProto instance accepts only users that have its inbound (308ffcfc RED, cae14431 fix). Before, a user taken
    off one MTProto inbound but kept on another still authenticated on the one it lost.
  • A live session is cut the moment its user loses the inbound it authenticated on (fafc7cbd RED, e8cd97f7 fix,
    merged in 9498dde6). Sessions on the instances the user keeps, and other users' sessions, survive.

L2TP: no rebilling after an upgrade from 0.6.20 (OBS-a5)

  • baf27844: each inbound keeps a persistent marker under /var/lib/pg-node/l2tp-counted, written on the first
    successful save of its counted state. A missing counted file without the marker takes the present sessions as already
    counted; with the marker it still means a fresh /run after a reboot and counts from zero.
  • Upgrade check: /var/lib/pg-node must be on persistent storage in the node's container setup, so the marker survives
    a container recreate.
  • Forward accept rules stay out of mangle chains (0bc4b8ca RED, 0544e60b fix); the linux-only IPsec guard tests
    build only on linux (caf0987e).

Upstream PasarGuard node parity

  • PasarGuard#55: recent node logs are kept and log streams fan out per subscriber (38744465, pkg/logstream).
  • PasarGuard#101: WireGuard forward accept rules stay out of mangle chains (3998a572).
  • Upstream 81af68e (released in upstream node v0.5.7): golang.org/x/net v0.60.0 -> v0.62.0 and the indirect module
    refresh at upstream's exact versions (3bf215e8); the go directive moves to 1.26.9, the toolchain stays 1.26.10.
  • Waiver ledger (69e17a0f, e4b18be7): 81af68e, 24e2b12 (PasarGuard#55) and a3d2d2f (PasarGuard#101) are ported; the version bumps
    739eab9 (v0.5.7) and 675bdaa (v0.5.6) are not applicable to the fork's version line.

Pins

  • mtg: Free-Guy-IR/mtg main 1ca247ba6e85e785d09c890301035096e5469bcb (was 1563871b36dd, 181af755): an accept
    loop stalled by a slow PROXY protocol header and a panic in conns.go are fixed, with tests.
  • sing-box: SINGBOX_COMMIT dbd00be874a06d9870b2602dd92885367384c779 (was 5a7c1f1a31cd, 46c8bc1e), still
    v1.13.21, adds the port of upstream sing-quic 7e0a586 (hysteria2: ignore EMSGSIZE in the port hopping receive
    loop).
  • xray: XRAY_COMMIT d2758a023cd7 unchanged; one new fork patch (C2).
  • Go toolchain 1.26.10 unchanged.

Build, CI and supply chain

  • Every published image carries org.opencontainers.image.revision, the commit it was built from (5a470e57 RED,
    3a911231 fix); a workflow gate refuses any image build step without it.
  • govulncheck: GO-2026-6866, GO-2026-6860 (x/net) and GO-2026-5841 (klauspost/compress) are gone. The node never called
    the fixed x/net symbols, so this is not a security release for the node.
  • The sing-box ledger test fails on the test goroutine instead of hanging (5b9281ab, D-a1).

Upgrade and compatibility

  • Panel side: no .proto or controller API change; only NodeVersion moves to 0.6.22.
  • Shared credentials across users on one xray inbound are no longer served to both (C2): the second user waits.
  • A user moved off one MTProto inbound is refused there and its live sessions there are cut (C3/C4).
  • An xray user on a user inbound can no longer reach loopback, unspecified or link-local destinations (C1), except the
    AsIs residual above.
  • L2TP needs /var/lib/pg-node to persist (OBS-a5).

Rollback

Redeploy the image the node ran before (ghcr.io/free-guy-ir/node:v0.8.20-degraded-start for nodes upgraded from
0.6.20). Rollback loses the C1, C2, C3/C4, OBS-a5 and silent-core fixes. Older images do not read the markers under
/var/lib/pg-node/l2tp-counted, so they can stay in place.

Known, tracked

  • C2H1-S1, C2H1-S2 (LOW): a batch claimant admitted while a stock core's revoke fails, and extra drops if the core
    crashes during the revoke phase until the panel resends; both need a fault-injecting fake core to test.
  • GO-2026-6651: the govulncheck waiver expires 2026-11-30.
  • Upstream waivers: 8d0a7cf8 expires 2026-11-10 and 36 more pending waivers expire 2026-11-20; the Upstream Drift
    gate goes red after an expiry unless the item is ported or the waiver renewed.
  • GO-2026-5932 (x/crypto openpgp) is reported with no call path.

v0.8.21 — accounting, revocation and core-crash fixes, upstream node v0.5.7, mtg v2.3.0

Choose a tag to compare

@Free-Guy-IR Free-Guy-IR released this 03 Nov 19:17

Release commit 347da49466514ccc2ef0a2384a46d8111c45fed3 on main. Previous release: v0.8.20-degraded-start
(2d8e11f3). This release carries the 545 commits in v0.8.20-degraded-start..v0.8.21; every one is listed by
git log v0.8.20-degraded-start..v0.8.21. NodeVersion 0.6.21. One patch step after v0.8.20.

Before the fleet rollout: dd-mode MTProto media check (DEPLOY)

The mtg pin now carries upstream 63049fa39c43 "preserve the sign of Telegram DC ids". A dd (plain) MTProto client that
asks for media DC -N now reaches Telegram as -N instead of N. This changes what every dd stream sends. Before the
image goes to the whole fleet, one node must pass a real-traffic check through a dd link: open a chat with photos,
videos and files that are not cached, and confirm they load. Only then roll out to the rest.

Pins

  • mtg: github.com/9seconds/mtg/v2 v2.3.0, replaced by Free-Guy-IR/mtg main 1563871b36dd518970490c8d018bdd2325d75e31
    (7fa5c6c3). That is upstream release v2.3.0 merged with the fork's fixes (950d39a9, pin 7f8c04762e3b) plus one
    fork commit that rejects a connection whose remote address has no TCP IP instead of panicking. The upstream v2.3.0
    commits it carries (transient accept errors, Linux accept(2) retry, socket-option drop logging, ordinary stop, v2
    network constructor, signed DC ids) are listed in docs/upstream-parity.md, "mtg v2.3.0 commits in the pin".
  • sing-box: SINGBOX_COMMIT 5a7c1f1a31cd11998ad249f00216f3a7add47d2b, Free-Guy-IR/sing-box main, still
    v1.13.21 (347da494, after 899005e1b8bf in 007bae8b and v1.13.21 in d189e942). It brings the vision close
    fix and three verbatim sing-vmess ports: the vision reshape split under the 8171-byte limit (upstream 40a1d62fd3a9),
    no zero-length end-of-stream chunk on an empty write (upstream 2a5002728353) and the reader port (upstream
    31ec11e8790c). sing-quic stays v0.6.5.
  • xray: XRAY_COMMIT d2758a023cd7, built against golang.org/x/net 0.60.0; Dockerfile.xray, which shipped an
    unpatched core, is removed (60ecdcac).
  • Go toolchain 1.26.10 in go.mod, CI and the image builders (410815f6).

Upstream PasarGuard node parity

  • v0.5.7: grpc 1.84.0 -> 1.85.0 (PasarGuard#122, bdd91efe, with the RED for Stop hanging behind a GracefulStop that waits on an
    orphaned handler, b45a71b5) and gopsutil 4.26.9 -> 4.26.10 (PasarGuard#121, 62059d0a). Waivers settled in b08ecee0; the
    indirect-module refresh 81af68e and the version bump 739eab9 stay recorded as pending.
  • PasarGuard#112: WireGuard canonical public keys and IPv6 NODE_HOST (74e9aaa4), and the counter-dip rule for the WireGuard
    tracker only (ee73b4f7).
  • PasarGuard#114: Shadowsocks 2022 inbounds stay multi-user when they start empty (3d18e70f).
  • PasarGuard#115: keep-alive refreshes on every call from the owning panel (c2e15f29).
  • PasarGuard#116: a core start can outlive the Start request (58a47bd8).
  • PasarGuard#90: configurable WireGuard server interface MTU (f9c4f05b).
  • The node v0.5.5 items, including the upstream grpc, gopsutil, x/sys, x/net and x/crypto bumps, recorded in
    docs/upstream-parity.md. go-chi moves to 5.5.0.
  • Not ported: node v0.5.6 MASQUE (PasarGuard#96, 88d5e75) and its version bump 675bdaa stay pending in
    .github/upstream-waivers.txt under NODE-SYNC-055, until 2026-11-20.

MTProto

  • Each instance's serve loop is supervised: an accept error cannot silence it, a panicking loop marks only its instance
    dead, one dead instance no longer stops the node, and state is reported per instance (a8f6b6a2, 09d06836,
    5a50d944).
  • A panic in mtg or in a node callback no longer takes the node down (15c04808).
  • A revoked user's open sessions are closed, including a stream begun before the revoke (dde62226, 90fd4897); a
    removal without a usable secret revokes by identity (63e33176); a stream context with no Close raises one alarm per
    instance (bf7eb766, F-V7-S7).
  • Accounting is handed to the next backend on every Start, force-closed middle-proxy relays bill what they write after
    Shutdown, concurrent Shutdowns hand over once, and Restart is refused after shutdown (472d2d7d, 217ea7c5,
    f7e3c7d5, 3a3157b6). Goroutine leaks on Start and the Restart listener swap are fixed (a6a82378, 0e84d610,
    4e434845).

Xray

  • A revoked or removed user's live connections and UDP flows are cut and new ones refused; a replaced account is revoked
    and only inbounds that held the user are touched (fdde3199, 6d56a6d4, 529d4067, f115e999, 7e97beea).
  • Health restarts wait for user syncs, never outlive shutdown, probe the core a sync installed and keep what a cancelled
    sync staged (82733dfc, a7c46ccf, 982bc47c, c2ed9854, 26bba5a8).
  • The config is serialised from a snapshot (797adcf0); per-user stats registration is atomic (df13041b);
    unchanged users are left alone during sync and the last entry for a user wins within a batch (40006fe1, 5a3c2ba0).
  • X-Forwarded-For is honoured only behind sockopt.trustedXForwardedFor and only from XRAY_TRUSTED_FORWARDER_CIDRS,
    which refuses IPv4-mapped entries (a170e667, c1e32b6c, a104cfb5); a pinned CA needs a server name (e1b77af8).
  • Core crash and race fixes carried at the pin: QUIC sniffer, WebSocket early data, SS2022 invalid destination, DNS
    names over 255 bytes, HTTP 1xx, tunnel inbound on a unix socket, WireGuard gVisor tun and outbound Close race, routing
    API race, XHTTP client races, singbridge datagram release (099f105f, d0b86f11, e9a433d0, 78ea868e, 448b373e,
    2d0272ec, 89e6a8e0, b7498bbc, a60ba6f2, 4c7f0a6d, 0abd7251); route picks no longer wait on rule updates
    (14cf5056); an empty hysteria auth is never installed (8882fc25).

L2TP accounting

  • Every session's bytes are billed once across restarts, renegotiations, reused pids, late ip-down and reboots: the
    session identity and final record are written atomically, watermarks survive a grace period and unreadable state, the
    fallback window is decided on boot time, and a missing run directory is an incomplete listing (b193386f,
    cf7f5008, ae0bc187, 5cd061b5, 4d3ff097, 28600993, 387d80cf, 7a39c000, 4f6bb0e9 and the rest of the
    fix(l2tp) series). ip-up/ip-down are replaced atomically and keep every character of a record value (6ae0fb84,
    9166cb5a). User syncs are refused after shutdown (db096a71).

Stats, WireGuard, OpenVPN, sing-box

  • Uplink is bytes received from the client on every protocol; node-level and per-inbound traffic are kept apart; a
    dipped counter rebases only its direction, and a restarted counter counts from zero with unread bytes carried
    (68d40f03, 61979286, 17dfdf50, e55fd11e, 83932cb0).
  • WireGuard accounts every peer byte through removals, replacements, Start and interface hand-over, and removes the peer
    when a user loses access (774fa21a, 054d4aca, a2c63c2d, cbb63a06).
  • OpenVPN drains final counters, settles sessions after the management loop exits, revokes a user without an OpenVPN
    block, and indexes users by email so bulk updates stay linear (143c2532, 8ab5dc8f, 514460a4, c3ff1c3b).
  • sing-box: health restarts wait for user syncs, a closed stats client returns an error instead of crashing, and an empty
    hysteria2 password is never installed (aa2131db, 7067a349, 8db51e50).

Controller, RPC and REST

  • The API key is checked before grpc-go reads the request, and an invalid or nil API_KEY fails fast (d489663a,
    88162e25).
  • User syncs are serialised with every core lifecycle change; a queued SyncUser fans out to every backend; a down primary
    is isolated from the extra backends; backend management requires the current client; the keep-alive watchdog is
    serialised with Start, and panel keepalive pings are accepted (af180e20, ee207282, 673524b5, de3493fb,
    40248f2c, 7192af88).

Build, CI and supply chain

  • install_core.sh and base images pinned and shipped geo data verified (b30c3e46); every patched xray file guarded
    in the image build (11d549d0, 8fbae98f); govulncheck refuses unproven patch application (1df3a918, 703eeaf2);
    tracked executables and files over 5 MB are refused (8c24aa4b and follow-ups).
  • Upstream Drift CI fails on unported upstream commits and releases; every unported upstream commit carries a reasoned
    waiver in .github/upstream-waivers.txt (the drift series).

Known, tracked

No production defect was found in v0.8.20-degraded-start..v0.8.21. These items are tracked, with an owner and a date:

  • D-a1: backend/singbox/inbound_ledger_lock_test.go calls t.Fatalf from a spawned goroutine (test-only). Due 2026-11-05.
  • OBS-a5: L2TP double bill at the 0.6.20 -> 0.6.21 upgrade when /run/pg-l2tp or the PID namespace outlives the
    container (bare metal, systemd, pid: host, bind-mounted /run/pg-l2tp). Not reachable in the stock image. The fix,
    a persistent upgrade marker under /var/lib/pg-node, goes into v0.8.22; deploy gate G3c, before node 70 is upgraded.
  • N4-E2: the 4f0bbc51 WireGuard test checks only aggregate bytes, so a per-user misattribution would pass
    (billing-relevant). Due 2026-11-10.
  • S3-W1..W6: weak tests in a5757d79, 6519b862, 84feaa37, ca520191, 09049248, cb1f730b. Due 2026-11-10.
  • N4-E1, N4-E3..E7: L2TP lock tests without useHostSeam (45f5b25e, 5e86ff90); 10c22bd4 does not prove Read is
    parked; ac063346/4f3a2302 rely on a 300 ms sleep; 5d015b5e/619015fd bounded under-bill awaits an owner decision;
    7044833b window-close and finals-without-identity assertions; ac29f67d pending-* cases and 9e9bd59c docs. Due 2026-11-10.
  • N5-E1..E5: 7b4e67fd, 0a3c68fa, 3a844f88, f1958fe6, 9fc13a1e not RED at their own commit; drift rule cases
    masked by rules 508/537; shadowsocks2022_test.go skips without an xray binary; 797adcf0 equivalence pin; parity doc
    count in f2e6adb0. Due 2026-11-10.
  • S2-N1..N8: Makefile `instal...
Read more

v0.8.20 — a filter rule must never keep a node down

Choose a tag to compare

A routing rule the core cannot resolve used to make xray refuse to start, which took five production nodes to status=error until the assignment was deleted by hand.

The node now retries once with the panel's pgcf-* rules stripped out and comes up unfiltered, and reports the degradation through BaseInfoResponse (fields 5-7) and its log stream, so the panel can see it and name the assignment that caused it.

Stripping is only reached when the config actually carries pgcf-* rules and the stripped retry succeeds. Messages that routing shares with DNS must arrive with routing context before they count. A core starved of memory or killed from outside is retried unchanged first, and an unchanged retry that fails for an unrelated reason is returned as it is.

NodeVersion 0.6.20.

v0.8.19 — carry the content-filter data file

Choose a tag to compare

@Free-Guy-IR Free-Guy-IR released this 18 Sep 15:58
b27b6da

A content-filter profile naming any pglist-* category builds a rule referencing ext:pgfilter.dat. That file shipped only inside the panel, and the rule is written into the shared core config, so xray did not ignore it — it refused to start:

Failed to start: main: failed to load config files: [stdin:]
  > infra/conf: failed to build routing configuration
  > failed to open file: pgfilter.dat

Every node on that core was one restart away from being down. Five production nodes proved it.

The data file is now built into the image beside geoip.dat and geosite.dat. Side-by-side on the exact rule that broke the fleet: v0.8.18 returns failed to open file: pgfilter.dat, this build returns Xray 26.3.27 started.

The xray source is also pinned to commit d2758a0 and the checkout asserted against it, matching the sing-box stage. A git tag can be moved; a tag alone does not say which code was built.

v0.8.18 — let a routing rule carry a large geosite category

Choose a tag to compare

@Free-Guy-IR Free-Guy-IR released this 18 Sep 15:21
9defaec

A routing rule could not carry a large geosite category

Installing a filter that names geosite:category-ads-all came back, on every node at once:

grpc: received message larger than max (4326122 vs. 4194304)

4194304 is grpc-go's default receive size. The ceiling is not this node's: controller/rpc/service.go has set MaxRecvMsgSize/MaxSendMsgSize to 64 MiB since upstream v0.2.0, and the node's own log line Method: AddRoutingRule, Code: ResourceExhausted proves it decoded the 4.33 MB message fine — that line comes from a unary interceptor, which grpc-go runs only after dec(in) has already enforced the limit.

The ceiling is Xray's own commander: app/commander/commander.go calls grpc.NewServer() with no options at all. A rule naming a geosite category is expanded into concrete domains before it reaches the core, and category-ads-all holds 170,624 of them.

Xray is now built from a pinned upstream tag with a two-line patch giving its commander the same 64 MiB this node already allows elsewhere. The build runs git apply --verbose and then greps for the result, so a patch that stops applying fails the build rather than silently shipping an unpatched core. The prebuilt binary the install script fetches still provides the geo assets; only the binary is replaced.

Proven end to end, not argued: built on the test host, the test node recreated on the patched image (Xray 26.3.27 … d2758a0-dirty), and the same 170,624-domain list applied — enforced: true, last_error: null, and the node logged Method: AddRoutingRule, Status: Success. The identical list fails on every unpatched node.

The gRPC server had no cap on concurrent streams

It ran at grpc-go's math.MaxUint32. Because the 64 MiB receive ceiling is enforced before the api-key interceptor runs — ClientAuth is NoClientCert, and the generated handler calls dec(in) before interceptor(...) — any peer that completed the TLS handshake could hold open unbounded concurrent 64 MiB allocations on hosts that publish the port. It is now GRPC_MAX_CONCURRENT_STREAMS, default 64, far above what one panel uses.

NodeVersion goes 0.6.16 → 0.6.18.

v0.8.17-named-identity

Choose a tag to compare

@Free-Guy-IR Free-Guy-IR released this 14 Sep 14:14
3928f3f

Pins the sing-box core that identifies hot-reloadable users by name instead of by slice position. NodeVersion moves to 0.6.17.

Why

Hot-reloadable inbounds resolved a user from an index frozen into the connection context at authentication and replayed for the life of the session. The panel pushes the full user set sorted by name on every change, so adding or removing any alphabetically earlier user shifted every later index. A live session then resolved to a different user and billed them for traffic they never generated, and a shrinking set panicked with index-out-of-range and killed the core.

What changed

SINGBOX_COMMIT now points at the core that carries identity by name for vless, vmess, trojan, tuic and shadowsocks, guards the hot-reloaded user maps with an RWMutex, and closes a removed user's live sessions.

Its TUIC regression test reproduces the wrong attribution and the panic against the previous pin, and passes on this one.

v0.8.16-multicore

Choose a tag to compare

@Free-Guy-IR Free-Guy-IR released this 10 Sep 16:42

Node 0.6.16: lock-free attach, 0600 configs, stats success-count, build retry

Carries the multi-core process-lifecycle hardening from the final audit with
its version constant correctly set to 0.6.16.

v0.8.15-multicore

Choose a tag to compare

@Free-Guy-IR Free-Guy-IR released this 10 Sep 16:17

Build a backend off the controller lock, 0600 configs, count stats successes

Final-audit hardening: AttachBackend no longer holds the controller lock
across the version probe; staged sing-box config is 0600; StatsAll keys the
all-failed decision on delivered-backend count, not payload size.

v0.8.14-multicore

Choose a tag to compare

@Free-Guy-IR Free-Guy-IR released this 10 Sep 15:59

Reap a forked probe child via Cancel, drop the post-Wait reap

WaitDelay bounds the call; the context-cancel handler group-kills a hung
binary while it is still alive. The post-Wait ReapProbe raced a reused pid
and is removed. No real core version command forks, so behaviour is unchanged.

v0.8.13-multicore

Choose a tag to compare

@Free-Guy-IR Free-Guy-IR released this 10 Sep 15:47

Only group-kill a probe proven to own its process group

ReapProbe now sends the group SIGKILL only when the target is provably not
this node's own process group, and always ends with a single-pid kill.