Commit Graph
37 Commits
Author SHA1 Message Date
aaaamoooo 05366d9177 perf(panel): kill per-request full-database clones (item 22, backend)
Data.Clone() was a full JSON marshal/unmarshal round trip (with a
per-record map dance for unknown fields): 6.3ms and ~13.8k allocs per
call on sandbox hardware, and every admin GET paid it at least twice
(RequireAdmin's user lookup + the handler). Clone is now a reflective
deep copy of the plain data graph — byte-identical output, proven
against the JSON clone on the real seed database by
TestCloneEquivalence — at 0.37ms / 538 allocs. CurrentAdmin resolves
the session user under a read-only store View and no longer clones
at all, so an admin GET takes exactly one snapshot. Sandbox
benchmarks (11 servers / 26 customers / 25 nodes): overview
17.2ms -> 0.38ms, servers 13.3 -> 0.85ms, customers 20.5 -> 1.54ms,
nodes 15.8 -> 3.45ms, jobs 13.0 -> 0.39ms per request.
2026-10-21 10:50:00 +00:00
aaaamoooo 0ab31ae32c feat: Reality target scanner (item 16)
Panel-side POST /api/reality/scan performs real TLS handshakes
against operator input (domains / IPs / CIDRs, port suffixes) or the
built-in pool (form presets plus microsoft/google/nvidia/samsung/
cloudflare and friends) and reports per target: usable or not, TLS
version, ALPN, key exchange, first cert SAN, latency, and a short
failure reason. Key-exchange support is proven by restricted-group
handshakes (X25519MLKEM768 first, then X25519, then a combined offer)
since the negotiated group is not exposed to Go clients. Bounds: 32
targets, 8 concurrent, 4s per probe. Node form gains a 查找目标
button beside the Reality preset; the scanner is a stacked overlay
so form state survives, and 使用 backfills SNI/dest/port.
2026-10-21 10:47:10 +00:00
aaaamoooo 4199a43660 test(panel): rename colliding login helper in reality-keys test 2026-10-21 10:44:54 +00:00
aaaamoooo a9d98675ac feat: show server Reality keypair on node edit form (item 17)
New read-only GET /api/nodes/:id/reality-keys returns the
panel-hosted keypair for the node's server from keyStore (display
only — it never mints a pair; a missing entry reports nulls). The
node edit form shows public and private key rows with copy buttons
for Reality protocols.
2026-10-21 10:44:32 +00:00
aaaamoooo 8189a513e1 feat(web): drop per-binding list from customer credentials tab (item 15)
The tab now shows only the subscription token and the shared
credential set. The Reality public key moved to the node edit form
(item 17) and the socks username to the node itself (item 21), so
nothing the bindings list showed is lost. Display-only change: the
credentials endpoint payload is unchanged.
2026-10-21 10:43:37 +00:00
aaaamoooo 80140f391f feat(panel): coalesce binding-class rebuilds in a fixed 10s window (item 23)
Assign/unassign, customer-form alignment, suspend/resume and
credential rotation no longer rebuild (and restart the node's
sing-box) per change. They mark the node rebuild-pending; a 1s
background flusher applies exactly one rebuild with the final state
when the FIXED window — 10s from the first pending change, never
extended by later changes — elapses. Manual deploy, repair, node
edits and server reconciles stay immediate and consume the mark.
Removal clears it, and the flusher discards marks on deleted /
removing / draft nodes, so a pending apply can never resurrect a
node. The mark is persisted in the data file, so a panel restart
inside the window cannot strand a node: the first post-boot pass
flushes whatever is due.
2026-10-21 10:43:27 +00:00
aaaamoooo 2fafd85d1f feat(panel): build-before-bind + socks5 fixed node account (items 20+21)
Item 20: drop the zero-client deploy gate in ValidateNodeDeploy for
all protocols, stop RebuildNode from auto-removing a node whose
bindings reach zero, and let server/heartbeat reconciliation rebuild
zero-client nodes like any other. Nodes are built and deployed first;
bindings can be added and removed freely.

Item 21: socks5-auth nodes carry one node-level fixed account
(node.socksUser/socksPassword, minted at creation or by the store-load
migration MigrateSocksNodeCredentials, idempotent). The sing-box
inbound's only user is that pair regardless of bindings; subscription
links for every bound customer carry the same pair. The generated
username avoids the ng: prefix so the agent never attributes the
account's traffic to a customer — it stays on the node's inbound
counters. Edit form shows both fields (copyable) for socks nodes;
NormalizeNode validates operator-supplied usernames.
2026-10-21 10:40:58 +00:00
aaaamoooo bb974e2ed7 fix(agent): fall back to install record for sing-box core version (item 12)
Locally built sing-box v1.13.21 carries no version stamp and reports
'unknown', leaving the panel core-version column empty. The agent probe
now falls back to /etc/nexusgate/sing-box/version when the binary
reports no version; agent-singbox.sh writes that record (version
without the v prefix) on every successful install path, including the
already-installed early exit. Agent version stays 0.6.8.
2026-10-21 10:36:09 +00:00
aaaamoooo aa2ee62d32 hotfix(panel): bind inbounds to stack wildcard instead of registered public address
Inbounds were pinned to the server's registered public address
(ipv4: PublicAddress, ipv6: PublicAddressV6), so addresses not
registered as 'the' public address - intranet NICs used for relay
landings, additional v6 addresses - had nothing listening and
intranet relays were unreachable (field-verified: ss showed the
landing port bound to the public IP only). 3x-ui/xray bind the
wildcard and serve these paths fine.

- listenAddress: explicit bindAddress still wins; ipv4 -> 0.0.0.0,
  ipv6 -> ::, dual -> :: (ipv6 wildcard per user decision
  2026-10-21: multi-v6 machines must serve all their v6 addresses).
- ResolveBindAddress: no longer resolves the registered address
  into a bind pin (that resolution was the production path of the
  pinning via resourceForNode/resourceForNodeExit); returns
  ("", nil) for all stacks, as it already did for dual/absent.
- serverAddress (client connection address / subscription host)
  is unchanged.
- Golden fixtures updated for the three listen fields that
  intentionally change; new listen_hotfix_test.go covers the
  listenAddress cases and the resourceForNode/exit deploy chains.
2026-10-21 07:16:38 +00:00
aaaamoooo 79639ac338 docs: report addendum for items 10-13 2026-10-20 21:47:59 +00:00
aaaamoooo 6d12e54606 feat(web): topbar/nav cleanup batch (items 10-13)
Item 10: remove the disabled decorative topbar search input (each page
has its own working search); CSS rule and media-query reference removed.
Item 11: menu/title renames — 服务器 -> 节点管理, 客户与订阅 -> 用户订阅,
转发与节点 -> 入站&出站, 设置与运维 -> 设置 (nav, TITLES/breadcrumb and
cross-references synced); the chains page add button is now 添加入站 and
its count reads N 个入站; the 安装与命令 page is merged into 设置
(commandsHTML block between the settings grid and the jobs table) and
the page itself is removed from nav, PAGES, TITLES, load() and the view
map — an old #commands hash now lands on the SPA's standard overview
fallback.
Item 12: remove the system page's 最近活动 panel (rendering and its
audit label map plus orphaned CSS); backend audit records untouched.
Item 13: the settings jobs table gets the w-jobs treatment — ID, status
and time cells never wrap, compact row padding, and the error column is
capped at 22% width so an empty error column no longer balloons.

Also fixes a class collision from the item-8 batch: the exit-mode
segmented control is renamed .seg -> .segctl so it no longer draws a
border box around the system page info-bar segments.
Golden re-captured: the only diff is the served index HTML inside
errors.badSubscription, audited character by character as exactly these
renames and removals.
2026-10-20 21:47:16 +00:00
aaaamoooo 070e699a14 docs: round-2 (v2) nine-item change report 2026-10-20 21:41:49 +00:00
aaaamoooo 18048f309e feat: manual landing endpoints for forward nodes (item 8)
A forward node's exit target is now a choice: a managed panel server
(existing behavior) or a manual landing endpoint — address + port +
protocol (VLESS / SS2022 / SOCKS5) + operator-supplied credentials,
including third-party nodes the panel does not run. The panel only
builds the entry-side sing-box outbound and route rule; there is no
exit resource, no exit job, and the node is exit-ready by definition.

- model: Node.ExitMode/ExitAddress; manual exits force fixed port mode
  and skip the exit-server port table entirely
- validation: address/port required; per-protocol credential presence;
  SS2022 passwords must be base64 key material of the method's key
  length (one PSK segment or serverKey:userKey) — the exact form
  sing-box accepts, rejected up front instead of failing at deploy
- orchestration: resource building, deploy validation and rebuilds all
  branch on manualExit; switching managed->manual queues removal of the
  old exit resource, manual->managed mints a fresh exit resource id
  (deploy validation previously dereferenced the nil exit server —
  regression test locks the fix)
- UI: exit target segmented control, link paste box that parses
  vless:// / ss:// / socks5:// links into the form, per-protocol
  credential fields (blank keeps the stored value on edit)
- golden: new manualCreate/manualAssign/manualDeploy/manualJobs/
  manualNodes sections — exactly one entry-side job whose outbound
  targets the manual address; all prior sections unchanged
- the generated combined sing-box config passes the real sing-box
  1.13.21 check
2026-10-20 21:36:20 +00:00
aaaamoooo ad18e16ac6 feat: customer-level credentials shared across all nodes (item 2)
One credential set per customer (UUID + 32-byte root relay key + shortId)
lives on the customer record; node bindings no longer store credentials.
SS2022 user keys derive deterministically from the root key by method
length (16/24/32-byte prefix), so a single password serves every protocol,
matching 3x-ui's per-client credential model. Reality public keys stay a
node property and are shown per binding in the credentials pane.

- store: load-time convergence — first binding's set wins; a converged
  password that is not a 32-byte root key is regenerated (uuid/shortId
  preserved); bindings are stripped; idempotent, covered by tests
- deploy: the orchestrator stamps the customer's set onto a build copy;
  subscriptions resolve the same set; the derivation applies in both the
  inbound builder and the URI builder (verified live: Alice's N1 link is
  byte-identical to the pre-change seed expectation, her N2 user key is
  exactly the root key's 16-byte prefix)
- API: GET /api/customers/:id/credentials returns {credentials, bindings};
  regenerate is now POST /api/customers/:id/credentials/regenerate and
  rotates the whole set, rebuilding every deployed node of the customer
- UI: credentials pane shows the single shared set plus per-binding
  node/protocol/Reality-public-key rows
- golden subscriptions fixture regenerated under one shared set (RF and
  SS entries byte-identical to before; five entries audited by hand)
- golden baseline re-captured for the new shapes; difftest normalizer now
  treats a base64 root key's prefix-truncation encodings and the Surge
  '='-mangled variants as the same secret
2026-10-20 21:21:13 +00:00
aaaamoooo bb8a0cb0c4 feat(agent+panel): sing-box core version probe and display (round-2 #4)
agent-go: engineReport gains singBoxVersion. The probe runs
`sing-box version` once, parses the version token from its first line
('unknown' from unstamped builds reports empty), and caches it keyed by
the binary's mtime+size — re-probed only when an engine install/update
replaces the file. Steady-state heartbeats pay one stat, no subprocess.
Agent version stays 0.6.8.

panel-go: EngineState.singBoxVersion persisted from heartbeats (old
agents without the field keep showing '-'), servers table core-version
column now renders it. Golden gains heartbeatWithEngine +
serversAfterEngineHeartbeat sections locking the full chain, plus a
listen-address (<hostip>) normalization for the sandbox bind address.
singboxversion_test.go covers parsing, cache hits, and re-probe on
binary replacement.
2026-10-20 21:02:59 +00:00
aaaamoooo 6e82d0d06a feat(panel): customers stats strip with 3x-ui depletion semantics (round-2 #1)
GET /api/customers gains a summary object (total/online/depleted/
depletingSoon/disabled/enabled) computed server-side; the customers page
renders it as a six-cell stats strip above the toolbar.

Predicates aligned verbatim with 3x-ui client_paging.go:
- depleted = (limit>0 && used>=limit) || (expiry set && expiry<=now),
  counted regardless of status;
- depletingSoon = active && !depleted && (expiry within expireDiff days
  || remaining traffic < trafficDiff GB), thresholds from new settings
  expireDiff/trafficDiff with 3x-ui's factory default 0 (nobody counts
  until configured);
- enabled = status active; disabled = manual suspensions only.
Online reuses the existing 3-minute presence projection.

Golden diff confined to the additive summary keys; summary_test.go
covers every bucket with thresholds off (soon=0) and on (soon=2).
2026-10-20 20:56:28 +00:00
aaaamoooo 161a5d78d1 feat(web): UI batch — content width, latency removal, icon tooltips, avatar removal (round-2 #3/#5/#6/#9)
#3 .content: drop max-width:1460px centering, padding 24px full width
   (3x-ui content-area parity), 8px under the 760px breakpoint.
#5 servers page: remove latency column + average-latency stat cell
   (stats grid 4->3, table 15->14 cols incl. detail colspan/empty row).
#6 icon buttons: native title replaced by data-tip with a 3x-ui style
   bubble (black/white, rounded, arrow, body-level fixed layer so table
   scroll containers cannot clip it); customer QR tip renamed to 二维码;
   covers customers/servers/nodes rows, credentials pane, links pane,
   address eye toggle, drag grip, filter chip clear, logout, modal close.
#9 remove decorative topbar avatar span#avatar, its showApp assignment
   and the dead .avatar CSS rule. Refresh button untouched.

Golden re-captured: sole diff was errors.badSubscription body (the SPA
index.html served there) reflecting the avatar/data-tip changes.
2026-10-20 20:52:38 +00:00
aaaamoooo 80f1370f1b fix(panel-go): random ports roll once — sticky across edits (round-2 #7)
NormalizeNode re-selected ports whenever the request merely CONTAINED
port/serverId keys; the browser form always submits them (port=null in
random mode), so every save re-rolled both entry and exit ports. Now an
assigned port only moves on an explicit addressing change: mode switch to
random, a different fixed port value, or a server move where the port is
taken (fixed mode surfaces the 409). random->fixed pins the current port.

Regression: portlock_test.go (UI-shape payload saved twice, rename-only,
mode switches, fixed change/conflict, server move free/taken).
Golden re-captured: diff confined to nodePatchUiStyle/nodePatchJobs/
nodesAfterPatch/backup port leaves returning to the seed ports.
2026-10-20 20:48:58 +00:00
aaaamoooo d576219a27 test(panel-go): freeze Go golden behavior baseline (Node retired, difftest superseded)
Single-source Go from this round on: golden.mjs replays a fixed API script
against a Go panel seeded from the difftest seed DB and diffs every response
(learning-normalized) against golden.json. 25 sections: lists, credentials,
all subscription formats, deploy job payloads, usage/presence, UI-style node
PATCH, /api/system shape, backup, error cases.
2026-10-20 20:45:33 +00:00
aaaamoooo 156a31c131 docs: UI reskin report 2026-10-20 18:09:12 +00:00
aaaamoooo e4ee6981f9 feat(web): 3x-ui style reskin — amo branding, light/dark themes, four pages + tabbed customer form 2026-10-20 18:09:12 +00:00
aaaamoooo 011eecc096 feat(panel): reskin backend — node sortIndex, presence/speed, /api/system, credentials, subscription aggregation (Node+Go parity) 2026-10-20 18:09:07 +00:00
aaaamoooo 7c21742810 feat(panel-go): monthly traffic reset for customers
- trafficResetDay/Period/At on the shared customer record (omitempty,
  inert for the Node panel, which preserves unknown fields)
- housekeeping applies due resets before quota evaluation: period-key
  idempotency, short-month clamping (31st fires Feb 28), missed-period
  catch-up after panel downtime, traffic suspensions auto-resume
- first sight of a schedule only arms the period marker, never zeroes;
  API create/PATCH accept trafficResetDay (0=off) and re-arm on change
- UI: reset-day input + last-reset display in the customer form
- tests: calendar math, apply pass, API lifecycle, persistence;
  full Go/Node suites green, difftest formal 50/50
2026-10-20 15:10:13 +00:00
aaaamoooo f4ef3d96a6 chore: migrate self-distribution links from GitHub to self-hosted Gitea
CI / test (push) Canceled after 0s
CI / anytls-smoke (push) Canceled after 0s
GitHub accounts suspended; all first-party distribution now served from
git.881218.xyz/rogue/NexusGate (Gitea):
- raw:  /rogue/NexusGate/raw/branch/<branch>/<path>
- release assets and source archives under the same repo path
- scripts gain a GIT_BASE variable built from NG_REPO (default
  rogue/NexusGate); NG_REPO/NG_BRANCH/--repo/--branch semantics unchanged
- panel enrollment/uninstall commands in public/app.js point at Gitea raw
- install.sh/nexusgate.sh/agent-singbox.sh stats source previously
  defaulted to the upstream repo (a2899882); defaults now this repo
Upstream dependency references (sagernet/sing-box Go module) untouched.
2026-10-20 11:05:55 +00:00
aaaamoooo 3951134940 feat: Go port of the panel in panel-go/ — drop-in replacement for server.js
CI / test (push) Canceled after 0s
CI / anytls-smoke (push) Canceled after 0s
Pure-stdlib Go implementation of the whole control plane: same
nexusgate.json schema (unknown fields preserved verbatim, explicit
null/false tri-states modeled), same HTTP API and error strings,
embedded frontend (NG_PUBLIC_DIR override), self-implemented scrypt
auth, X25519 Reality keys, panel-hosted keyStore and heartbeat
reconciliation included. Version stays 0.6.8.

Verified: Go tests 33/33 (scrypt + Reality vectors generated from
the Node implementation, subscription goldens byte-identical),
Node suite 52/52 untouched, differential E2E against the Node
panel 50/50 including two-way DB swap. See panel-go/PARITY.md
and panel-go/INSTALL-GO.md.
2026-10-20 08:21:54 +00:00
aaaamoooo abb1a4344b feat: heartbeat reconciliation — panel re-dispatches resources a node lost
Agents (Go + Node) now report the resource IDs they actually hold in
each heartbeat. When an expected resource is missing locally (e.g. the
node's /etc/nexusgate state was wiped and only the agent restarted, so
no re-enrollment happens), the panel re-queues applies for just the
affected nodes via reconcileFromHeartbeat: gated on no queued/running
jobs for the server (in-flight jobs are never failed by this path) and
throttled to one trigger per server per 120s, audited as
system:heartbeat. reconcileServer gains optional onlyNodeIds /
failInFlight parameters; the enrollment call is unchanged. Heartbeats
without a resources array (old agents) behave exactly as before.
2026-10-20 07:46:33 +00:00
aaaamoooo f2b7d8e181 feat(agent-go): consume panel-hosted Reality keys (persist keyMaterial, backfill unused-provided keys) 2026-10-20 07:20:59 +00:00
aaaamoooo 483b891306 feat: panel-hosted Reality keys (keyStore, payload keyMaterial, backfill, cleanup) 2026-10-20 07:20:42 +00:00
aaaamoooo 1b77c610f4 fix: keep current page on refresh via hash routing (restore page from location.hash, back/forward supported) 2026-10-20 15:09:44 +08:00
aaaamoooo 495b949f18 feat: ng-agent update migrates Node agents to Go by default (NG_AGENT_IMPL=node opts out) 2026-10-20 15:05:07 +08:00
aaaamoooo 9c3789a1a4 chore(uninstall): also remove nexusgate-agent-go test unit; docs(README): Go agent memory/install notes 2026-10-20 06:56:17 +00:00
aaaamoooo d29b4f7ef1 feat(update): detect installed impl; Go path downloads release asset and replaces binary, flush-usage via binary; Node path unchanged except unit ExecStart rewrite; default REPO now aaaamoooo/NexusGate 2026-10-20 06:56:07 +00:00
aaaamoooo 545c0668ba feat(install): default to Go agent binary from release agent-v0.6.8, drop Node.js dependency; NG_AGENT_IMPL=node fallback via run.sh; shell-only enroll JSON; units ExecStart the Go binary 2026-10-20 06:55:54 +00:00
aaaamoooo ddfc27efba feat: Go agent source (stdlib-only rewrite of agent.js) + parity/install docs; binaries ship as release assets 2026-10-20 06:55:29 +00:00
aaaamoooo c6615f0ab5 fix: systemd agent unit add --max-semi-space-size=4 (match run.sh); README memory numbers to measured values 2026-10-20 06:25:37 +00:00
aaaamoooo ae9e607527 fix: agent run.sh add node memory limits (max-old-space 64) 2026-10-20 13:41:43 +08:00
aaaamoooo 5f272ed356 fix: create /usr/local/sbin before fetching agent scripts (Alpine) 2026-10-20 13:35:33 +08:00