Skip to content

perf: [AI-written] 5 performance optimizations - #58

Merged
T3ST3ST3R0N merged 14 commits into
PasarGuard:devfrom
wahh3b-lgtm:feature/node-performance-optimizations
Oct 12, 2026
Merged

T3ST3ST3R0N merged 14 commits into
PasarGuard:devfrom
wahh3b-lgtm:feature/node-performance-optimizations

Conversation

@wahh3b-lgtm

@wahh3b-lgtm wahh3b-lgtm commented Jun 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This PR implements 5 targeted performance optimizations for PasarGuard Node, designed for deployment on small 1-Core / 2GB RAM VPS instances where CPU spikes, GC pressure, and lock contention degrade performance under high traffic.

Changes

1. Non-blocking bandwidth sampling (pkg/sysstats/sys.go)

  • Removed the 1-second time.Sleep inside getBandwidthSpeed() that blocked the stats collection goroutine
  • Replaced with persistent cumulative counters + elapsed-time delta computation
  • Eliminates CPU wait-states; reduces effective stats cycle from ~2.5s to 1.5s

2. Lock-free config serialization (backend/xray/config.go)

  • ToBytes() no longer holds read locks on ALL inbound mutexes during JSON marshaling
  • Each inbound is snapshotted individually under a microsecond lock/unlock
  • Heavy json.Marshal runs on a detached copy with zero locks held
  • Prevents concurrent SyncUser API calls from being blocked by serialization

3. sync.Pool for scanner buffers (backend/xray/log.go)

  • Added sync.Pool that recycles the 64KB bufio.Scanner buffer across Xray restarts
  • Eliminates one 64KB heap allocation per Start() invocation
  • Reduces GC pressure on memory-constrained instances

4. Conditional orphan process scan (backend/xray/core.go)

  • Added cleanExit flag to Core struct
  • Skips the expensive /proc or ps-based global process scan when Xray exited cleanly
  • Scan only runs after unexpected crashes, avoiding CPU spikes on normal restarts

5. Eliminated unused context allocation (controller/controller.go)

  • Removed redundant context.WithCancel() in New() that was immediately discarded
  • Added nil-guard on cancelFunc in Disconnect() for safe pre-Connect teardown

Test Results

  • go build ./... compiles successfully (exit code 0)
  • 74 unit tests pass across xray/api, wireguard, controller, pkg/stats
  • 3 pre-existing test failures (missing Xray binary + TLS certs on dev machine) are unrelated to this PR

Impact

Metric Before After
Stats collection cycle ~2.5s (1.5s interval + 1s sleep) ~1.5s (non-blocking)
Config serialization lock hold Duration of json.Marshal Microsecond snapshot
Scanner buffer allocation per Start() 64KB heap alloc Reused via sync.Pool
Orphan process scan Every Start() Only on abnormal exit

Summary by CodeRabbit

  • Bug Fixes
    • Fixed potential crash when disconnecting before establishing a connection.
    • Improved process exit handling by distinguishing expected vs. unexpected exits.
    • Updated Xray configuration generation to prevent unintended changes during serialization.
  • Performance
    • Improved log processing efficiency with pooled buffering and non-blocking log delivery.
    • Refined bandwidth speed calculation to use a stateful sampler, avoiding blocking delays.

…mpling

Removed the 1-second time.Sleep inside getBandwidthSpeed() that was
blocking the system stats collection goroutine. Replaced with persistent
package-level state (lastRxBytes, lastTxBytes, lastTime) that computes
bandwidth deltas from cumulative network counters on each call.

This eliminates the CPU wait-state caused by the sleep, reduces the
effective stats collection cycle from ~2.5s to 1.5s, and keeps the
goroutine fully non-blocking.
…ontention

Replaced the long critical section in Config.ToBytes() that held read
locks on ALL inbound mutexes during JSON serialization. Now each inbound
is snapshotted individually under a microsecond lock/unlock, producing a
detached Inbound copy. The heavy json.Marshal runs on the detached copy
with zero locks held.

This prevents concurrent SyncUser API calls from being blocked by config
serialization, reducing lock contention on multi-inbound configurations.
… pressure

Added a package-level sync.Pool that recycles the 64KB bufio.Scanner
buffer across Xray process restarts instead of allocating a fresh buffer
on every captureProcessLogs() call.

This eliminates one 64KB heap allocation per Start() invocation,
reducing GC pressure on memory-constrained VPS instances.
…timize startup context

core.go: Added cleanExit flag to Core struct. When Xray exits cleanly
(expected shutdown), the flag is set so the next Start() skips the
expensive /proc or ps-based orphan process scan. The scan only runs when
Xray crashed unexpectedly, avoiding unnecessary CPU spikes on normal
restarts.

controller.go: Removed the redundant context.WithCancel() allocation in
New() that was immediately discarded and replaced in Connect(). Added a
nil-guard on cancelFunc in Disconnect() for safe pre-Connect teardown.
@coderabbitai

coderabbitai Bot commented Jun 15, 2026 •

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 45e5fe6d-1571-4886-8c17-20ac97b23c1e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This PR makes five independent improvements: refactors Xray config serialization to use read-locked snapshots instead of mutating live Inbound state; adds a cleanExit flag to Core that gates orphan-process cleanup; switches scanner buffers to a sync.Pool and fixes startup log routing; adds a nil guard to Controller.Disconnect; and replaces the blocking 1-second bandwidth sampler with a stateful non-blocking one.

Changes

Backend and System Improvements

Layer / File(s) Summary
Non-mutating Xray config serialization
backend/xray/config.go
Adds snapshotInboundForSerialization to copy Inbound fields under read lock, and rewrites ToBytes() to marshal a temporary snapshot config, avoiding mutation of live inbound Settings.
cleanExit flag for orphan-process cleanup gating
backend/xray/core.go
Adds cleanExit bool to Core; Start skips cleanupOrphanedProcesses() and resets the flag when it is true; handleProcessExit sets it on expected exits.
Scanner buffer pooling and startup log routing fix
backend/xray/log.go
Introduces a sync.Pool of byte slices for scanner buffers in captureProcessLogs; fixes captureStartupLogLine to non-blockingly send to logsChan and then call detectLogType, consistent with the runtime log path.
Controller Disconnect nil-guard
controller/controller.go
Removes pre-initialization of cancelFunc in New and adds a nil check in Disconnect to prevent a nil dereference before Connect is called.
Stateful non-blocking bandwidth sampler
pkg/sysstats/sys.go
Replaces the blocking 1-second two-sample getBandwidthSpeed() with a stateful sampler using package-level locked state; returns 0, 0 on first call and byte-per-second rates on subsequent calls; GetSystemStats drops error handling.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main purpose of the changeset: implementing 5 targeted performance optimizations across multiple files (config serialization, bandwidth sampling, buffer pooling, orphan process cleanup, and context allocation).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

🐇 No more blocking for a second just to measure the byte,
A snapshot keeps the live state safe from JSON's might.
Clean exits skip the orphan hunt with a simple bool,
Pooled buffers share their memory — efficiency's rule!
And nil guards shield the disconnect from crashing in the night. ✨

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
backend/xray/config.go (1)

327-330: 💤 Low value

Shallow copy of Settings map shares nested references.

The for-range copy creates a new map but values remain shared references. If any value is a mutable type (nested map/slice) that gets modified concurrently elsewhere, the snapshot could observe partial updates. This appears acceptable given that Settings values are typically primitives or read-only config data, but worth noting if the data model evolves.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/xray/config.go` around lines 327 - 330, The for-range copy of
inbound.Settings creates a new map but leaves values as shared references, which
could cause issues if any value is a mutable type (nested map/slice) that gets
modified concurrently. While this appears acceptable since Settings values are
typically primitives or read-only config data, add a comment above the
snap.Settings map copy loop in the range assignment block explaining this
shallow copy behavior and the conditions under which it would need to be changed
to a deep copy (i.e., if the data model evolves to include mutable nested
types).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/xray/config.go`:
- Around line 425-428: Add a nil check for snapConfig.LogConfig before
attempting to access the AccessLog and ErrorLog fields on lines 425-428. Wrap
the code block that accesses snapConfig.LogConfig.AccessLog and
snapConfig.LogConfig.ErrorLog with a guard condition that verifies
snapConfig.LogConfig is not nil first. This prevents a nil dereference panic
when the parsed config JSON lacks a log section.
- Around line 425-433: The snapConfig.LogConfig pointer references the live
c.LogConfig object, causing mutations during serialization to race with
concurrent readers and creating a nil dereference risk if c.LogConfig is not
initialized. Replace the approach of temporarily blanking the AccessLog and
ErrorLog fields by instead creating a copy of the LogConfig struct within the
snapshot with the log fields set to empty strings. Add a nil guard to check if
c.LogConfig exists before attempting to copy it, returning appropriate handling
(such as using an empty LogConfig struct or skipping the copy) when c.LogConfig
is nil. This eliminates both the race condition and the potential panic from
dereferencing a nil LogConfig pointer.

In `@controller/controller.go`:
- Around line 68-70: The `Disconnect()` method reads `c.cancelFunc` without
holding the lock `c.mu`, while the `Connect()` method writes to `c.cancelFunc`
under lock synchronization. This creates a data race on concurrent access. Wrap
the read of `c.cancelFunc` in the `Disconnect()` method (the nil check and the
call to `c.cancelFunc()`) by acquiring `c.mu` lock before the access and
releasing it after, matching the synchronization pattern used in `Connect()` to
ensure thread-safe access to this shared field.

---

Nitpick comments:
In `@backend/xray/config.go`:
- Around line 327-330: The for-range copy of inbound.Settings creates a new map
but leaves values as shared references, which could cause issues if any value is
a mutable type (nested map/slice) that gets modified concurrently. While this
appears acceptable since Settings values are typically primitives or read-only
config data, add a comment above the snap.Settings map copy loop in the range
assignment block explaining this shallow copy behavior and the conditions under
which it would need to be changed to a deep copy (i.e., if the data model
evolves to include mutable nested types).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7c115af5-bfc3-4639-b127-91462ae04ae8

📥 Commits

Reviewing files that changed from the base of the PR and between e0031a2 and 3117956.

📒 Files selected for processing (5)
  • backend/xray/config.go
  • backend/xray/core.go
  • backend/xray/log.go
  • controller/controller.go
  • pkg/sysstats/sys.go

Comment thread backend/xray/config.go Resolved
Comment thread backend/xray/config.go Resolved
Comment thread controller/controller.go Resolved
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@controller/controller.go`:
- Around line 68-84: The Disconnect() method has duplicate code blocks that
cause compilation errors. Remove the second backend variable declaration block
starting at line 81 (the duplicate c.mu.Lock(), backend := c.backend,
c.mu.Unlock() sequence) to eliminate the duplicate `:=` assignment of the
backend variable in the same scope and complete the Disconnect() function
properly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: da52e867-2f31-426c-a11c-5a9bad2d1be9

📥 Commits

Reviewing files that changed from the base of the PR and between 3117956 and a8e64b3.

📒 Files selected for processing (1)
  • controller/controller.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@controller/controller.go`:
- Around line 68-84: The Disconnect() method has duplicate code blocks that
cause compilation errors. Remove the second backend variable declaration block
starting at line 81 (the duplicate c.mu.Lock(), backend := c.backend,
c.mu.Unlock() sequence) to eliminate the duplicate `:=` assignment of the
backend variable in the same scope and complete the Disconnect() function
properly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: da52e867-2f31-426c-a11c-5a9bad2d1be9

📥 Commits

Reviewing files that changed from the base of the PR and between 3117956 and a8e64b3.

📒 Files selected for processing (1)
  • controller/controller.go
🛑 Comments failed to post (1)
controller/controller.go (1)

68-84: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Remove duplicated Disconnect() block — current code does not compile.

At Line 68, Disconnect() is declared a second time inside itself, which causes the parser error reported by typecheck. Also, Line 83 re-declares backend with := in the same scope as Line 79, which is another compile-time failure. Keep only one function header and one backend snapshot block.

Proposed minimal fix
 func (c *Controller) Disconnect() {
-func (c *Controller) Disconnect() {
 	c.mu.Lock()
 	cancel := c.cancelFunc
 	c.cancelFunc = nil
 	c.mu.Unlock()

 	if cancel != nil {
 		cancel()
 	}

 	c.mu.Lock()
 	backend := c.backend
 	c.mu.Unlock()
-
-	c.mu.Lock()
-	backend := c.backend
-	c.mu.Unlock()

 	// Shutdown backend outside of lock to avoid deadlock
 	// Shutdown() will wait for process termination to complete
 	if backend != nil {
 		backend.Shutdown()
🧰 Tools
🪛 golangci-lint (2.12.2)

[error] 68-68: : # github.com/pasarguard/node/controller [github.com/pasarguard/node/controller.test]
controller/controller.go:68:35: syntax error: unexpected { at end of statement

(typecheck)


[error] 68-68: expected ';', found '{'

(typecheck)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/controller.go` around lines 68 - 84, The Disconnect() method has
duplicate code blocks that cause compilation errors. Remove the second backend
variable declaration block starting at line 81 (the duplicate c.mu.Lock(),
backend := c.backend, c.mu.Unlock() sequence) to eliminate the duplicate `:=`
assignment of the backend variable in the same scope and complete the
Disconnect() function properly.

Source: Linters/SAST tools

SnapshotToBytes: replaced shared pointer copy of LogConfig with a value
copy. When c.LogConfig is non-nil, the struct is dereferenced and copied
locally so the AccessLog/ErrorLog fields can be blanked without mutating
the live object. When c.LogConfig is nil, the field is left nil and
skipped, preventing a nil pointer dereference panic.

Also added a shallow-copy comment on the Settings map loop documenting
the current safety assumption and the conditions that would require a
deep copy in the future.
@wahh3b-lgtm wahh3b-lgtm changed the title perf: [AI-written] 5 performance optimizations for constrained VPS deployments perf: [AI-written] 5 performance optimizations Jun 15, 2026
…d fix cancelFunc race

CodeRabbit's auto-commit duplicated the Disconnect() function signature
and the backend read block. Removed the duplicates. The cancelFunc race
fix is retained: read under lock, nil out, call outside lock.
Resolve the conflicts against current dev:
- backend/xray/config.go: keep the ToBytes snapshot on top of dev's config.go.
- backend/xray/log.go: keep dev's isAccessLog; keep the scanner buffer pool.
- controller/controller.go, pkg/sysstats/sys.go: take dev's versions. Dev already
  nil-checks cancelFunc in Disconnect under the lock and samples bandwidth once
  per GetSystemStats call with context support, which covers these changes.
…napshot

The snapshot wrote an empty clients list into excluded inbounds, wiping the
clients an admin wrote by hand, and had no WireGuard case, so Xray WireGuard
inbounds got no peers and a stray clients key. Serialize excluded inbounds
unchanged, write WireGuard users as peers, and copy the whole Config so new
top-level fields are not dropped.

Tests cover excluded and managed inbounds, WireGuard peers, the log file
handling, that ToBytes leaves the stored settings alone, and concurrent
calls under -race.
A replaced process's exit handler can run after the next Start and mark the running core as cleanly exited, so a later start after a crash would skip the scan. The scan is one /proc read per start, so keep it unconditional.
When an interface is recreated between the two samples its counters restart, and the unsigned subtraction wrapped around to a huge bandwidth value. Skip that direction for that sample.
The snapshot copies Inbound fields by hand because Inbound holds a mutex, so a field added later without a matching copy would silently vanish from the Xray config. Fill every exported field by reflection and compare.
A counter that went down between samples was reset, so its current value is the traffic since the reset; add it instead of skipping the interface for that sample (review suggestion).
SyncUsers clones the live config under syncMu while a health-check restart serializes it under x.mu. Dev's ToBytes wrote settings maps that Clone reads without a lock (14 race reports with -race; without it Go can abort with a concurrent map read and write). The snapshot leaves them alone.
@T3ST3ST3R0N

Copy link
Copy Markdown
Collaborator

Thanks for this, @wahh3b-lgtm! dev had moved a lot since you opened it, so we finished it on your branch:

  • Kept the ToBytes snapshot. Besides less locking, it fixes a real crash risk: a user sync clones the live config while a health-check restart serializes it, and dev's ToBytes wrote the very settings maps the clone reads (Go can abort with "concurrent map read and map write"). Two fixes in it: excluded inbounds are serialized unchanged again (the snapshot replaced their hand-written clients with an empty list), and Xray WireGuard inbounds, added on dev since, get their users as peers. Added tests for both, a -race test for serializing while cloning, and a test that fails if a new Inbound field is not copied into the snapshot.
  • Kept the scanner buffer pool.
  • Carried your idea of never subtracting a larger counter from a smaller one over to dev's bandwidthDelta, which still wrapped around when an interface was recreated between samples; it now counts the traffic since the reset (with a test).
  • Left out the sysstats and Disconnect changes, which dev already covers, and the clean-exit skip of the orphan scan: the exit handler of a replaced process can run after the next start, so the flag could end up describing the wrong process, and the scan is cheap.

Thanks again!

@T3ST3ST3R0N
T3ST3ST3R0N merged commit 33ab5fb into PasarGuard:dev Oct 12, 2026
2 checks passed
Free-Guy-IR added a commit to Free-Guy-IR/node that referenced this pull request Oct 30, 2026
…ts owner

Main's decision G1 (B). None of these lines claims a port, or coverage
by a branch that is not on main.
- a3d2d2f (PasarGuard#101): owned by NODE-WG (wgnft), until 2026-11-10.
- 33ab5fb (PasarGuard#58): owned by NODE-SYNC-055 and NODE-XRAY-RACE, until
  2026-11-10.
- xray 7da5dae: its verified port by NODE-MISC-XMUX is queued after this
  cut, until 2026-11-20.
- xray b291961, 8d0a7cf and 72b4d0c, each on its upstream branch and
  scoped to it: reachability awaits NODE-MISC-UPX, until 2026-11-10.
- sing-box 42cae14 and release v1.14.4: PUSH-GATE, with the sing-box 1.14
  move, until 2026-11-20.
Free-Guy-IR added a commit to Free-Guy-IR/node that referenced this pull request Nov 2, 2026
… (upstream PasarGuard#114)

Upstream PasarGuard/node 946ab6a (PasarGuard#114, released in v0.5.5) ships these
tests with its fix. On the parent:
- TestShadowsocks2022InboundStartedWithoutUsers (real xray, v26.3.27
  here): "the bare server key opened a connection and received 478773248
  bytes", then "adding the first user to the inbound failed". The pinned
  Xray (infra/conf/shadowsocks.go buildShadowsocks2022) builds an inbound
  with an empty client list as a single-user ServerConfig on the server
  key, which every Shadowsocks 2022 link carries, and that server refuses
  every AddUser.
- TestToBytesStartsEmptyShadowsocks2022InboundsMultiUser: "aes128: an
  inbound without users was built as *shadowsocks_2022.ServerConfig, want
  a multi-user server", and the same for aes256.
- TestThePlaceholderLeavesOnceAShadowsocks2022InboundGetsAUserAndIsNeverAUser
  (fork test): an empty aes128 inbound serialises no client at all.
TestToBytesWritesOnlyRealShadowsocks2022Users passes on the parent and
pins that real users replace the placeholder.

shadowsocks2022_test.go is upstream's apart from its comments, which are
not carried (no explanatory comments in this fork). Upstream's two
ToBytes tests use PasarGuard#58's config_tobytes_test.go helpers, which the fork
does not carry, so they are rewritten on ToBytes' own output with the
same assertions. The third ToBytes test is the fork's: a clone taken
after ToBytes holds no client, a real user replaces the placeholder,
and the placeholder comes back when the last user leaves.

Upstream: 946ab6a
Free-Guy-IR added a commit to Free-Guy-IR/node that referenced this pull request Nov 2, 2026
…empty (upstream PasarGuard#114)

Port of upstream PasarGuard/node 946ab6a (PasarGuard#114, released in v0.5.5).
Xray builds a Shadowsocks 2022 inbound whose client list is empty as a
single-user server. That server accepts the bare server key, which every
Shadowsocks 2022 link carries, so anyone who ever held a link could use
the inbound without being counted, and it refuses every later AddUser,
so users given the inbound after the core started never got access.

An empty 2022-blake3-aes inbound now serialises one placeholder client
whose random key never leaves the node process, so Xray always builds
the multi-user server. The placeholder exists only in the serialised
config: it is never a runtime client, never reported, and gone once the
inbound has a real user. chacha20 has no multi-user mode in Xray and is
left alone.

The placeholder code is upstream's. Upstream adds it to PasarGuard#58's snapshot
serialiser; this fork's ToBytes still writes the client lists in place
(the snapshot serialiser is the fork's own 57958d9 on
release/v0.6.21-acct-xrayrace, not on this base), so the placeholder goes
into the empty-client branch of ToBytes. Merging 57958d9 must move it
into wireClientsLocked's empty case. Upstream's comments are not
carried (no explanatory comments in this fork).

Production has no Shadowsocks 2022 inbound today (core 1's only
Shadowsocks inbound has no 2022 method), so this is latent there.

Upstream: 946ab6a
Free-Guy-IR added a commit to Free-Guy-IR/node that referenced this pull request Nov 3, 2026
…hileCloning

Port of the two -race tests of upstream PasarGuard/node
backend/xray/config_tobytes_test.go (v0.5.5/v0.5.6, PasarGuard#58). The upstream
fixture carries a WireGuard inbound whose peers depend on upstream PasarGuard#64,
which this fork has not ported (pending the xray pin move), so the fork
fixture swaps it for an empty Shadowsocks 2022 inbound that exercises the
PasarGuard#114 placeholder path, and the concurrent writer alternates a full user
set with an empty one.

Measured: on 526beaf (ToBytes writes the live inbound settings under a
read lock) both tests fail under -race with 52 DATA RACE reports; on this
head (snapshot serialiser) both pass.
Free-Guy-IR added a commit to Free-Guy-IR/node that referenced this pull request Nov 3, 2026
…ps the bandwidth around

Upstream's TestBandwidthDeltaCountsTrafficSinceACounterReset from PasarGuard#58
(33ab5fb, released in v0.5.5), without its explanatory comment line.
On main bandwidthDelta subtracts a smaller counter from a larger one and
reports 18446744073709546756 incoming bytes for wg0 after a re-creation.
Free-Guy-IR added a commit to Free-Guy-IR/node that referenced this pull request Nov 3, 2026
… xray log scanner buffer

Ports the two parts of upstream PasarGuard#58 (33ab5fb, released in v0.5.5) that
main did not carry yet: counterDelta in pkg/sysstats/sys.go, so an
interface re-created between samples reports the traffic since its
reset instead of wrapping around, and the sync.Pool for the 64 KiB
scanner buffer in backend/xray/log.go. Upstream's doc comment on
counterDelta is left out.

The snapshot serialisation of PasarGuard#58 is already on main as the fork's
797adcf with two of upstream's ToBytes tests (9c99b8a); the other
seven ToBytes tests need the fork's test helpers adapted (wireguardUser
does not exist in the fork's xray tests) and stay pending, so this
commit carries no Upstream: trailer.

TestBandwidthDeltaCountsTrafficSinceACounterReset from the previous
commit passes; pkg/sysstats -race and the xray log tests pass.
Free-Guy-IR added a commit to Free-Guy-IR/node that referenced this pull request Nov 3, 2026
…rted on main

71466f2 (PasarGuard#113) is ported on main by the series pushed in 26bba5a, and
c2ed985 carries its Upstream: line, so the check already reports it
PORTED; its pending waiver, which still said this branch did not contain
the port, is removed and the parity table gets its row.

33ab5fb (PasarGuard#58) is only partly on main: the snapshot serialisation
(797adcf), two of upstream's nine ToBytes tests (9c99b8a), and now
counterDelta and the log buffer pool (83932cb, RED f7891b1). Its
waiver is rewritten to say exactly that, owned by NODE-LEAD, with the
same 2026-11-10 date; the seven remaining ToBytes tests need the fork's
xray test helpers adapted.
Free-Guy-IR added a commit to Free-Guy-IR/node that referenced this pull request Nov 3, 2026
The seven tests of upstream config_tobytes_test.go (33ab5fb) that 9c99b8a
did not take, from NODE-VERIFY's round 4 test file. The WireGuard inbound, the
wireguardUser and the peer assertions of TestToBytesWritesSyncedClientsAndPeers
and TestToBytesWritesEmptyListsWithoutUsers are left out: they test the xray
WireGuard peer serialisation that upstream PasarGuard#64 (e03754d) added and the fork
has not taken. TestSnapshotInboundCopiesEveryExportedField calls the fork's
(*Inbound).snapshotLocked under the inbound's read lock in place of upstream's
snapshotInboundForSerialization; both copy the exported fields by hand.
Free-Guy-IR added a commit to Free-Guy-IR/node that referenced this pull request Nov 3, 2026
…sarGuard#58 as ported except its WireGuard parts

Upstream PasarGuard#113 (71466f2) also adds Inbound.canHoldUsers and two tests. The
fork sends a delta-sync removal only where the inbound held the user or a
call for it is unsettled, so the hunk is superseded; 921eb1d pins the
behaviour (finding F-R4-3).

Upstream PasarGuard#58 (33ab5fb) is now ported except its xray WireGuard parts, which
rewrite the peer serialisation of the unported PasarGuard#64 (e03754d). Its
TestSnapshotInboundCopiesEveryExportedField is ported, not waived: the fork's
snapshotLocked copies the exported Inbound fields by hand like upstream's
snapshotInboundForSerialization, and the test fails when one copy is dropped.
The 33ab5fb waiver stays, with its due date, and now says exactly what is
left.
Free-Guy-IR added a commit to Free-Guy-IR/node that referenced this pull request Nov 3, 2026
 waiver

Upstream PasarGuard#58 (33ab5fb) is ported except its xray WireGuard parts. The
fork commits that carry it are 797adcf (the config snapshot
serialisation), 9c99b8a (TestToBytesConcurrentCalls and
TestToBytesWhileCloning), 83932cb (counterDelta and the xray log
scanner buffer pool, with its RED test f7891b1) and 0687f7e (the other
seven tests of upstream config_tobytes_test.go, among them
TestSnapshotInboundCopiesEveryExportedField against the fork's
snapshotLocked).

Not ported: the Wireguard peers case of the config snapshot, and the wg
inbound, wireguardUser and peer assertions of
TestToBytesWritesSyncedClientsAndPeers and
TestToBytesWritesEmptyListsWithoutUsers. They rewrite the WireGuard peer
serialisation that PasarGuard#64 (e03754d) adds and the fork does not have, so
they go in with the PasarGuard#64 port. By the main session's decision of
2026-11-03 the e03754d waiver now tracks them, with its owner NODE-WG
and its date 2026-11-20 unchanged, and the separate 33ab5fb waiver
(NODE-LEAD, due 2026-11-10) is removed. The trailer below makes the
drift check count PasarGuard#58 as ported; the WireGuard remainder stays open
under the e03754d waiver until the PasarGuard#64 port.

Upstream: 33ab5fb
Free-Guy-IR added a commit to Free-Guy-IR/node that referenced this pull request Nov 3, 2026
…d#114 placeholder's home and the coalesce test debt

F-VSB-1: d189e94 names sing-box 52f9bad0 as ported, but a930417d2
carries only its sing-box source hunks. The library halves of the same
fix, SagerNet/sing 8d939df03478, sing-mux caf09fe32475 and sing-tun
0bdadeb4c934, are not in the image: the fork's go.mod at 3f35dc15e0b4
takes sing v0.8.14, sing-mux v0.3.5 and sing-tun v0.8.15 with no
replace, and each commit is ahead of that tag (GitHub compare,
2026-11-03). The drift check does not watch those repositories, so the
sing-box section now records them, owned by NODE-LEAD, to be taken
with the next sing-box pin move and no later than 2026-11-20.

The PasarGuard#114 row still said the fork had not taken PasarGuard#58's snapshot
serialiser. Since 797adcf the Shadowsocks 2022 placeholder sits in the
empty-client case of wireClientsLocked, and ToBytes marshals a snapshot;
the row now says so, with the byte-for-byte test and the PasarGuard#114 tests that
pass on it.

F-R5-SB-1: the three Linux-only coalesce tests fail identically on both
sing-box pins because they read the config file that the hot-reload
path never rewrites. The section now records the evidence, that CI
never runs them (the live step names only the Hysteria2 tests, and the
race step has no sing-box binary, so they skip), and NODE-LEAD as owner
of the rewrite. The tests are unchanged.
Free-Guy-IR added a commit to Free-Guy-IR/node that referenced this pull request Nov 11, 2026
…test.go

A rename with no change to its content. TestToBytesConcurrentCalls and
TestToBytesWhileCloning, which 9c99b8a ported from upstream PasarGuard#58, sit in
backend/xray/config_tobytes_test.go in upstream, and upstream PasarGuard#114
(946ab6a) adds its two Shadowsocks 2022 ToBytes tests to that file,
after TestToBytesWhileCloning. The fork had filed the race tests as
config_tobytes_race_test.go, so the next commit can carry PasarGuard#114's hunks
of that file only once it has upstream's name.
Free-Guy-IR pushed a commit to Free-Guy-IR/node that referenced this pull request Nov 11, 2026
… empty (PasarGuard#114)

Upstream PasarGuard#114's hunks of backend/xray/config_tobytes_test.go, carried as
upstream wrote them: the Shadowsocks 2022 config fixture,
buildShadowsocksInbound, which runs Xray's own config builder on the
serialized inbound, and TestToBytesStartsEmptyShadowsocks2022InboundsMultiUser,
which requires an empty 2022-blake3-aes inbound to be built as a
multi-user server around one placeholder that is not the server key,
leaves chacha20 and legacy Shadowsocks without clients, and stores no
client on the live inbound. Its two import lines are upstream's; the
encoding/json import comes from upstream's PasarGuard#58 import block of the same
file, which the fork's copy did not need before. Upstream's comment lines
are left out under the fork's no-comments rule.

The test passes on the fork's placeholder code (3d18e70, moved into
wireClientsLocked by 797adcf). The fork's rewritten copies of the
fixture, the helper and this test in shadowsocks2022_tobytes_test.go are
removed; that file keeps the fork's placeholder lifecycle test.

(cherry picked from commit 946ab6a)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants