mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-25 08:42:09 +03:00
59dff059c146ab514f76cc91a1473b201c2a90d9
295 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
59dff059c1 |
feat(amneziawg): retire the kernel-module OS-shellout code and install.sh path
Hard cutover, part 3: everything that only ever existed to drive the kernel-module (DKMS) + awg-quick + TPROXY architecture is gone now that internal/amneziawgnet's embedded path is wired in as the real thing. internal/amneziawg/manager.go -> instance.go (renamed, ~90% smaller): kept InstanceFromInbound and its direct helpers (interfaceNameForID, serverAddress, serverAddressV6) plus the exported FirstIPv4 (still used by server.go's access-log email index) -- all pure, protocol-shape-only code with no OS dependency, reused by both the old and new paths historically. Deleted the old Manager (GetManager/Ensure/Reconcile/StopAll/CollectTraffic/ the fingerprint methods), generateServerConfig and everything under it (writeObfuscation, defaultPostUpDown, appendOrTrue, detectDefaultInterface), and process control (interfaceUp/Down, syncConfig, getPeerStats, IsAwgInstalled). route_egress.go deleted entirely (the TPROXY bridge's port/fwmark/table constants and rule-rendering, fully superseded by internal/amneziawgnet's SOCKSPortForInbound/SocksPassword). portfwd.go trimmed to just the parsing/validation half (ForwardedPortsInclude, still used for save-time conflict checks); the iptables DNAT rendering half is gone -- per-client port-forwarding has no equivalent under the embedded path yet (tracked as Phase 3.6). install.sh: removed install_ndppd, enable_ipv6_forwarding, enable_tproxy_support, should/install_amneziawg, and check_secure_boot (and their call sites) -- roughly 265 lines. No more DKMS build, PPA/keyring setup, TPROXY kernel module loading, or Secure Boot warning: the embedded path needs none of it. Not in this commit (tracked as an explicit follow-up, not silently dropped): the frontend's routeThroughXray toggle is now vestigial (the field stays in the Go/JSON schema for backward compat with existing stored settings, see types.go) but its UI/schema removal needs the frontend type-regen + openapi.json hand-patch dance this fork always does for a settings-shape change, which is its own separate pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
f78dfa6f67 |
feat(amneziawg): swap the app's integration points to the embedded manager
Hard cutover, part 2: every real call site that used to drive internal/amneziawg's kernel-module Manager now drives internal/amneziawgnet's instead -- - internal/web/job/amneziawg_job.go: the reconcile cron job. Traffic/ online-status accounting is dropped entirely (not ported) -- once a peer's traffic is relayed through Xray's own SOCKS5 inbound, it's an ordinary Xray user and XrayTrafficJob's existing generic stats polling already handles it, with zero AmneziaWG-specific code. - internal/web/runtime/local.go: the immediate-apply CRUD path (AddInbound/DelInbound/updateAmneziaWGInbound). - internal/web/web.go: panel shutdown's StopAll. internal/amneziawgnet.Manager gains Remove(id) to match the kernel-module Manager's shape at these call sites (Reconcile alone doesn't cover a single-inbound removal outside a full reconcile pass). internal/web/service/inbound_amneziawg.go's applyLocalAmneziaWG needed no change: it already goes through runtime.Runtime.UpdateInbound, which now resolves to the updated local.go path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
3450d872d9 |
feat(amneziawg): replace the TPROXY bridge with a SOCKS5 relay inbound (Phase 3 start)
Hard cutover, part 1: injectAmneziawgnetSocks replaces injectAmneziawgEgress as the AmneziaWG-side Xray config injector. Every enabled AmneziaWG inbound now gets an always-on loopback SOCKS5 inbound (built by amneziawgnet.SocksInboundSettings) instead of an opt-in dokodemo-door TPROXY bridge -- there's no RouteThroughXray gate anymore since the embedded path has no alternative datapath once traffic is decapsulated in gVisor. Reuses the real inbound's own tag, same as before, so per-inbound stats totals keep matching. internal/amneziawgnet gains SOCKSPortForInbound (deterministic port derivation, its own range distinct from the kernel-module bridge's) and SocksPassword (a process-wide, lazily-generated, not-persisted password -- this traffic never leaves loopback). port_conflict.go's port-reservation check is updated to match: the new SOCKS5 relay port is reserved unconditionally for every qualifying AmneziaWG inbound, not gated on RouteThroughXray. Not yet done (tracked in the migration plan): swapping the actual manager call sites (cron job, immediate-apply CRUD, shutdown) from the kernel-module Manager to amneziawgnet's, and deleting the now-dead TPROXY/awg-quick code. This commit could not be locally verified beyond internal/amneziawgnet itself (this machine has no C compiler, so internal/database and anything that imports it -- including internal/web/service -- can't be built or vetted here); pushing for real CI feedback before continuing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
0ea0e4512d |
fix: update inbound_amneziawg.go to the split buildInboundForLocalRuntime
Fork-only file, invisible to upstream's own rename of buildRuntimeInboundForAPI into buildInboundForNodePush / buildInboundForLocalRuntime (part of the node-sync client-deletion fix). Every other call site was migrated by that commit; this was the one straggler, caught by CI after the 3.6.0 sync landed on main. |
||
|
|
c97ab56245 |
Merge remote-tracking branch 'upstream/main' into sync-3.6.0
# Conflicts: # .github/workflows/claude-bot.yml # .github/workflows/release.yml # DockerInit.sh # frontend/package-lock.json # frontend/package.json # frontend/src/hooks/useClients.ts # frontend/src/layouts/AppSidebar.tsx # frontend/src/main.tsx # internal/config/version # internal/database/model/model.go # internal/web/service/client_wireguard.go # internal/web/service/inbound.go |
||
|
|
5bc81dfd1d |
fix(node): stop the node sync from deleting clients it never meant to
A client that hit its quota or expiry was disabled, then destroyed on both panels a few seconds later. Five defects fed the same hard delete. ReconcileNode pushed buildRuntimeInboundForAPI, which strips disabled clients. Every other call site targets an in-memory Xray config, where dropping a user is harmless; a node target is a peer panel's DATABASE, so the node deleted the row, stopped reporting it, and the master mirrored that deletion back. Split the builder in two: buildInboundForNodePush injects fallbacks only, buildInboundForLocalRuntime adds the strip on top. The names now say which targets they are safe for. setRemoteTrafficLocked trusted a config_dirty the caller sampled before the snapshot round-trip. A client added inside that window commits on the same serialized writer and marks the node dirty, but the merge still treated the older snapshot as authoritative and deleted it. Re-read the flag inside the writer. In "selected" sync mode, FilterNodeSnapshot strips a deselected tag, but the sweep loaded every inbound with node_id set, so deselecting a tag read as "the node deleted it" and wiped an inbound the node still serves. Skip tags outside the node's managed set. A failed SyncInbound was logged and swallowed; on SQLite the transaction still commits, and the sweep then deleted the innocent clients whose links that failure had left unbuilt. Skip the sweep for such an inbound, and close the trigger: SyncInbound now stores the trimmed email it looks up by, and email validation rejects every unicode space rather than only U+0020. ClientService.Delete tombstones up front and deliberately keeps the record when an inbound fails, so the next attempt can retry the leftovers. The tombstone did not lift with it, so the next merge dropped the client from the synced settings and finished the deletion this path had refused. Add withdrawClientTombstones on every failure path, in BulkDelete too. Finally, make the sweep itself recoverable. "Ended the merge unattached" is true for a real remote deletion and equally true for a bad merge, so it now stamps sync_orphaned_at instead of deleting; any later merge that sees the client attached clears the mark, and a reaper removes only what stayed orphaned past the grace period. The traffic row survives that window too, or a reclaimed client would come back with its usage, quota and expiry reset. The mark is written by this sweep alone, so orphans from any other cause keep their existing manual-cleanup semantics. |
||
|
|
f4b7b08e08 |
fix(ldap): stop auto-delete from wiping every client on an empty directory
FetchVlessFlags returns (empty map, nil) whenever the bind succeeds but the search yields nothing usable — a renamed OU, a service account that lost read on the user attribute, a filter that stopped matching. The only guard on the destructive half of the sync was `err != nil`, so that answer was read as "every user is gone" and the job detached every client from the configured inbounds, once a minute, for as long as the directory stayed broken. Gate auto-delete behind autoDeleteSafeForFetch: refuse an empty fetch, and refuse one that collapsed below half of the last successful sync, which is a misconfigured directory far more often than real churn. Also stop splitCsv from defaulting an empty string to DefaultTruthyValues. That default belongs to the truthy-value setting, but splitCsv is also what parses ldapInboundTags, so an unconfigured tag list silently resolved to ["true","1","yes","on"]. It only ever bounded the blast radius by accident. |
||
|
|
31c1eed5dc |
fix dead code, typo, and minor bugs in main.go, process.go and index.go (#6167)
Fixes several small issues found during code review: - fix(xray): return explicit nil instead of stale err in getLogPath - fix(xray): remove duplicate doc comment on GetErrorLogPath - refactor: remove unreachable return after log.Fatalf (×4) - fix(cli): add missing newline to listen IP success message - fix(cli): typo "form" → "from" in migrate help text - refactor: simplify var+assign to short declaration for server/subServer - fix(controller): return error from getTwoFactorEnable instead of swallowing it |
||
|
|
5373786faa |
feat(ui): let users pin the sidebar
Restore a persistent expanded-sidebar choice while preserving the compact hover rail as the default. |
||
|
|
c56f6447a8 |
chore: refresh dependencies and modernize Go test idioms
Frontend deps: @hookform/resolvers 5.4.3 -> 5.5.7, Storybook 10.5.4 -> 10.5.5 across the four packages we declare, globals 17.7.0 -> 17.8.0, and jsdom 29.1.1 -> 30.0.1. The jsdom major replaces its CSS and selector stack -- @asamuzakjp/css-color 5 -> 6, @asamuzakjp/dom-selector 7 -> 8, undici 7 -> 8, nwsapi and generational-cache folded into their parents, whatwg-url 17 nested underneath. Nothing in the Vitest suites reaches those directly and the whole frontend gate (typecheck, lint, tests, build, Storybook compile) is green. Panel frontend version to 0.6.0. Backend deps: mattn/go-sqlite3 1.14.48 -> 1.14.49 and valyala/fasthttp 1.72.0 -> 1.73.0, plus the golang.org/x/exp and genproto/googleapis/rpc indirect bumps that came with them. Go tests: modernize -fix output, covering range-over-int, sync.WaitGroup.Go in place of manual Add/Done pairs, maps.Copy, and Go 1.26 new(expr) for pointer-to-value in the forwarded-trust table. The storedAs helper is deleted instead of being left behind a //go:fix inline directive -- keeping it that way fails govet on the one call site the rewrite did not reach, and every caller now takes new(...) directly. Behaviour is unchanged. DnsTab: the hosts-sync effect tested dns while declaring dnsEnabled in its dependency array. Both carry the same truth value, so this is exhaustive-deps hygiene rather than a behaviour change. |
||
|
|
f52c3c4837 |
perf(clients): make the clients page scale to large panels
The clients page was slow on panels with many clients for two independent reasons: the server rebuilt the whole picture on every request, and the browser rebuilt the whole table on every poll. Server side, ListPaged loaded every client row, every client_inbounds link and every client_traffics row into Go memory, then filtered, sorted and paginated in a loop -- on a request the page repeats every five seconds. Every predicate now runs in SQL and only the requested page's ids are hydrated, so the cost tracks the page size rather than the client count. Measured on SQLite with a realistic status mix: the default view at 100k clients goes from 1,072ms to 64ms. Behaviour is preserved deliberately in the subtle places -- the cross-panel global-traffic overlay is folded into the same used-bytes expression the predicates and sort use, LIKE wildcards are escaped so a search for "a_b" stays literal, and the two different tiebreak rules the in-memory comparator had are reproduced per sort key. The summary's per-bucket email lists are capped at 200 with exact counters beside them. They only back hover popovers, but shipping every match made the response grow with the panel: at 100k clients it carried ~42k emails, and the page revalidated all of them through a strict Zod parse every five seconds. The popover now shows a "+N" chip for the remainder. Browser side, the page fired three sequential list requests per load and threw the first two away: the query went out before the persisted sort was applied, and again before the configured page size was known -- 0 meaning "one long page" is indistinguishable from "not loaded yet". The page size is now derived rather than mirrored through an effect, and the previous visit's value is remembered so the single request goes out at mount instead of queueing behind /setting/defaultSettings. Then the per-poll work. Reading isFetching made it a tracked property, so the refetch interval notified twice per cycle and re-rendered the page even when structural sharing left the data identical. Xray reports a traffic row per client whether or not it moved bytes, so the speed map was mostly zeros and was replaced wholesale every push; zero rows are now dropped and an unchanged result returns the previous object, which lets React bail out instead of re-rendering. The five Tooltip-wrapped buttons and the inbound chips per row do not depend on traffic at all and are now memoised, keyed on the email because a push replaces the row object of every client whose counters moved. antd's hashed:false drops 3,311 :where(.css-<hash>) wrappers and 29% of the generated stylesheet, and a pinned cssVar key stops each of the eleven page-level ConfigProviders minting its own token scope. Two callers that only need the mutations, GroupsPage and ClientBulkAddModal, no longer start the list query -- the groups page had been polling the full paged list every five seconds for data it never renders. |
||
|
|
34c43c8a9d |
fix: address the automated review round on PR #6154
- Replace parseGeodataFile's full proto.Unmarshal with a protowire-based
scan that reads only each entry's Code, skipping every Domain/CIDR
payload without allocating it -- the actual bulk of a real
geoip.dat/geosite.dat. Also caps the file read at 256 MiB.
- Hold geodataMu across the full scan-and-maybe-parse in
GetGeodataCategories instead of releasing it around the parse, so
concurrent cache misses (e.g. several browser tabs) can't all
independently re-parse every file; clone the cached slices before
returning them so a caller mutating its result can't corrupt the cache.
- Gate useGeodataCategories on the rule editor's own `open` state instead
of firing on every visit to the Routing tab.
- formatGeodataSuggestion now compares filenames with strings.EqualFold,
matching scanGeodataFiles' own case-insensitive match -- a file that IS
the default one on a case-insensitive filesystem (e.g. Windows) no
longer gets the long ext: form.
- Fix a real bug the review's hypothesis led to: Select mode="tags" only
commits the search text on Enter/comma, so clicking Save right after
typing (a blur, not an Enter) silently dropped the value entirely, with
no domain/ip key at all in the saved rule. Wrap it in a small
TagsAutocomplete that also commits on blur. Same autocomplete now
applies to sourceIP, which accepts geoip:/ext: too.
- Guard useGeodataCategories' fetch per-field with Array.isArray instead
of a single top-level `?? EMPTY_CATEGORIES`, since parseMsg returns the
original unvalidated obj (not null) on a schema mismatch.
- Test fixes: exact slices.Equal instead of slices.Contains-only
assertions, t.Run subtests, a cache-hit-skips-reparse test (via a
test-only parse counter), a returns-independent-slices test, a
file-size-cap test, and four new frontend tests covering the tags
round-trip including the blur-commit regression above.
- GeodataCategories now goes through the same generated-example path as
every other response type (StructAllow + example: tags + responseSchema
in endpoints.ts) instead of a hand-written response string. The
existing hand-written GeodataCategoriesSchema in schemas/routing.ts is
unrelated to this and is left alone -- CLAUDE.md is explicit that Zod
schemas under src/schemas/ are the source of truth and only the
generated example/openapi path comes from Go example: tags.
- Drop the two PR-illustration screenshots from media/ -- nothing in the
repo referenced them; they only ever needed to exist in the PR
description itself.
Not changed: leaving geodataFileKind's leak into generated/{types,zod}.ts
as-is. internal/web/service's openapigen request has no AliasAllow at
all, so every non-struct type in the package already leaks this way
(e.g. staticEgressResolver, transportBits predate this PR) -- scoping an
AliasAllow for the whole package is a real cleanup but a separate, wider
change than this PR's own footprint, and needs checking nothing already
depends on those existing generated aliases first.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
af5a8e5d40 |
fix(database): create SQLite backup snapshots online (#6137)
* fix(database): snapshot SQLite backups online Use SQLite's online backup API for downloadable backups and SQLite migration exports instead of checkpointing then reading the live database file. The regression test validates a backup made while writes continue. * style(database): group SQLite driver imports * fix(database): bound online backup retries Use a single backup step and a bounded connection-acquisition/retry context. Tighten temporary-file cleanup and regression assertions while removing the unused checkpoint helper. * test(database): cover existing backup destinations * fix(database): harden SQLite snapshot lifecycle Sweep interrupted snapshot directories at SQLite startup, keep rollback-journal backups incremental, and make caller-owned cleanup explicit. Reuse one scheduled Telegram snapshot across administrators and make the direct SQLite driver dependency explicit. --------- Co-authored-by: PathGao <gaoyanbo@gaoyanbodeMacBook-Air.local> |
||
|
|
ad288a7ecc |
fix(sub): honor trustedProxyCIDRs before forwarded URLs (#6135)
* fix(sub): honor trustedProxyCIDRs before forwarded URLs * fix(sub): avoid unused trust-setting lookups Skip the trustedProxyCIDRs lookup when no forwarded header can affect a subscription URL. Keep the shipped proxy default in one exported setting constant and document the subscription-link behavior for custom proxy boundaries. * fix(frontend): meet config text contrast requirements Keep compact configuration text readable in the light theme and satisfy the Storybook accessibility check. --------- Co-authored-by: PathGao <gaoyanbo@gaoyanbodeMacBook-Air.local> |
||
|
|
ad5f2a28cb |
fix(xray): synchronize lifecycle state (#6138)
* fix(xray): synchronize lifecycle snapshots Protect process replacement and result caching with a lifecycle state object, so read paths keep one process snapshot while restarts swap state safely. Bound version probing to prevent a stalled binary from holding the restart lock. * test(xray): cover concurrent lifecycle reads Exercise status, result, and traffic reads while the managed process is replaced, so the race detector guards the lifecycle snapshot boundary. * fix(xray): guard process config snapshots Synchronize hot-applied config snapshots, keep Telegram reads on one lifecycle snapshot, and strengthen lifecycle timeout and concurrency regression coverage. --------- Co-authored-by: PathGao <gaoyanbo@gaoyanbodeMacBook-Air.local> |
||
|
|
c3fa73d5a0 |
feat(ui): redesign the overview page as a trend-first command deck
Replace the ten-small-cards overview with an action bar, four vitals
tiles carrying 72-sample sparklines seeded from /server/history, a
two-series throughput chart, a TCP/UDP connections chart, and a
grouped system strip (uptime xray|os, panel ram|threads, ip
addresses). StatusCard and XrayStatusCard are deleted; every modal
stays reachable from the action bar, the Xray error message moves
into a tooltip on the state pill, and the panel version text keeps
opening the update modal (the dev-channel switch lives there) even
when no update is available. Live values sit beside the
upload/download and tcp/udp legends, a health sentence appears only
when a vital crosses the shared warn/crit thresholds now exported
from models/status, and load average is left to System History.
The sidebar becomes an auto-collapsed 72px icon rail that expands as
an overlay on hover: rail width, brand-row height and menu paddings
are pinned so nothing shifts during the transition, the collapsed-menu
tooltips are disabled, hover state survives the per-page sidebar
remounts (with a matches(':hover') resync), and the manual collapse
trigger is gone.
Sparkline gains rgb()/rgba() support in its fill gradient, a
showLegend prop so pages stop reaching into its internals, and loses
a dependency-less repaint effect that doubled canvas paints. Chart
tooltips show clock time via the new TimeFormatter.formatClock;
accents come from theme tokens instead of status.cpu.color. Verified
by screenshot at 390/800/1150/1280/1400/1600px in light and dark,
en and fa-IR, plus programmatic geometry checks on the sidebar.
Locale files gain 8 keys and lose 9 dead ones across all 13
languages.
|
||
|
|
87ebcc7a6f |
feat(ui): tag settings that sit at their shipped default value (#6128)
* feat(ui): tag settings that sit at their shipped default value A field showing 2096 reads identically whether the install never set it or the operator saved 2096 — newcomers cannot tell which knobs they have touched, and after the cleared-port fix (#6121) a port can never visually return to an unset state. Add a small grey tag next to numeric settings whose current value equals the shipped default. The tag deliberately compares values, not provenance: a stored 2096 and a fallback 2096 behave identically, so they read identically, and the tag reacts live as the user types. The backing endpoint filters defaultValueMap through the AllSetting field set, so per-install material (secret, panelGuid, node mTLS keys) and redacted credential fields never leave the server; a test pins that. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): keep the default tag out of the accessible name, pin the defaults contract From review, in order of severity: The badge was rendered inside the element whose id feeds the control's aria-labelledby, so a visible tag changed every field's accessible name ('Panel Port Default'). The title text now carries the id on its own span and the badge sits beside it. The same default values live in three places: the Go defaultValueMap, the frontend AllSetting class, and the tag's verdict. A new contract test parses the Go map's string literals and asserts every shared key matches the AllSetting class default through the tag's own comparison — and on first run it caught two real drifts (tgEnabledEvents / smtpEnabledEvents defaulted to '' in the class but 'login.attempt,cpu.high' on the server), now aligned. matchesFactoryDefault no longer coerces blank or unparsable defaults (Number('') is 0; a junk string is not false). The Go tests are table-driven t.Run subtests and gained the structural invariant: every returned key is an AllSetting json tag outside the credential deny-list. The service doc comment now describes the projection mechanism instead of overclaiming; the i18n key is re-indented and placed at the head of pages.settings in all 13 locales; the fetch falls back to {} when validation fails; and smtpPort gets the tag so plain numeric settings-list fields are covered uniformly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
33f72f8f4a |
fix(api): authenticate GET /panel/api/openapi.json + pin the route registry to the router (#6133)
* test(web): pin the endpoints.ts registry to the actual Gin routes endpoints.ts is a hand-maintained registry and nothing checked it against the router: an omitted API route silently vanishes from the generated OpenAPI docs, and an entry for a removed route documents an endpoint that 404s. Two new tests construct the real router against a throwaway DB and diff the /panel/api surface both ways. The check found one gap on arrival: GET /panel/api/openapi.json — the endpoint that serves the docs — was itself undocumented. Registered. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(api)+test: authenticate openapi.json, fold the two route-contract tests into one Three things from the review, in severity order. The bot found that GET /panel/api/openapi.json was registered on the base-path group one line before the /panel/api group installs checkAPIAuth, so Gin's snapshot of the parent chain meant the whole admin API surface plus build version was fetchable without a session — while this very PR was about to document it as auth-required. Move the registration inside the authed api group. Verified: unauthenticated it now 404s exactly like server/status (was 200), and a logged-in session still serves it 200, so the docs page is unaffected. The existing api_docs_test.go already checked the forward direction by regex-scanning controller source against a hand-maintained per-file path switch — which is why it missed this web.go-registered route, and whose fall-through default silently mis-paths any unlisted controller file. The new router-based test is a strict superset, so fold in the extra surface it guarded (/login, /logout, /csrf-token, /getTwoFactorEnable, /ws) and delete the old test rather than run two. Harden the endpoints.ts parser: pair each method with the next path sequentially instead of a brace-crossing regex, and fail loudly when the parsed count doesn't match the declared method fields. Construct the server once across both subtests, cancel it, and restore the previous global on cleanup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ea35884390 |
chore(i18n): delete 230 dead translation keys and guard against new ones (#6132)
* chore(i18n): delete 230 dead translation keys and guard against new ones The 13 locale files carried 230 keys (11% of the set) that nothing in the frontend or Go sources references — leftovers of renamed features (the email notifier reuses tgbot.messages.* for subjects, the old email.subject*/title* set was orphaned; likewise menu.*, the clients bulk-copy strings, and the secAlert* family). Nothing detected this: a missing key falls back to en-US and an unused key fails nothing. A new test now fails the build when an en-US key has no reference in frontend/src or internal Go sources (dynamic keys are covered by harvesting concatenation and template-literal prefixes), and pins that all 13 locales carry exactly the en-US key set, so parity drift surfaces at test time instead of as a silent fallback. Each locale shrinks by the same 230 keys; net -2,900 lines across the translation set. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(i18n): restore the 29 live remarkVars keys, match whole tokens, unmask 9 more From review: the template-literal harvester required the prefix to end on a dot, so pages.hosts.remarkVars.desc${token} harvested nothing and all 29 desc* tooltip keys were wrongly deleted — and the guard shared the flawed logic, so CI stayed green while the Hosts page would have shown raw key names in 13 languages. Restored from the parent commit; the harvester now requires at least one dot but not a trailing one. Also from review: references are matched as whole dotted tokens instead of substrings (a dead key can no longer hide behind a longer sibling — that unmasked 9 more genuinely dead keys, each verified by hand before deletion), and the test excludes itself from the scan so its own prose cannot whitelist a subtree. Net: -210 keys per locale instead of the previous -230. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d3da7abdf0 |
fix: port the PR #6105 review-round fixes into this fork's own AmneziaWG code
Same 8 findings fixed on upstream-pr/amneziawg, ported here since this fork's internal/amneziawg + related web/service files predate that PR branch's own fix-up commits: 1. hostRulesFingerprint now folds in a peer's IPv4 whenever ForwardedPorts is set, not only when RouteThroughXray is on, so a re-IP forces the bounce needed to move the DNAT rule too. 2. ValidateConfigValue (new, params.go) rejects control characters in server/client keys, email and I1 at save time; sanitizeConfigValue strips them defensively at .conf-render time. 3. checkForwardedPortsConflict now scopes to node_id IS NULL and takes a pre-loaded portConflictContext (loadPortConflictContext), so a port used only on another node isn't a false collision and an inbound with N clients costs one query instead of N. 4. PostDown commands are now best-effort (appendOrTrue) so an external firewall flush can't abort the rest of the teardown chain. 5. The "ip rule list | grep -q" existence check now uses grep -c >/dev/null, avoiding a pipefail/SIGPIPE false negative that could re-add a duplicate rule. 6. route_egress.go's stale "always present, no opt-in" comment corrected to describe the real RouteThroughXray-gated behavior. (This fork's genAmneziaWGLink already emits vpn://, and there's no upstream-facing docs page here, so neither needed the PR branch's Finding 6 docs/link-format changes.) 7. install.sh: Arch's ndppd install uses pacman -Sy, not -Syu, matching every other pacman call in the script; should_install_amneziawg short-circuits to yes when awg is already installed, so `x-ui update` doesn't re-prompt -- this fork's own opt-out-by-default philosophy for should_install_amneziawg is unchanged, only the redundant-reprompt behavior is fixed. 8. CollectTraffic checks pointer identity before writing back a traffic-counter baseline, so a concurrent restart's freshly-reset (empty) baseline can't be clobbered by stale pre-restart counters. sweepOrphansLocked no longer permanently disables itself on a transient os.ReadDir failure. go build/vet/test and frontend typecheck/lint/build/vitest all pass. |
||
|
|
17e6b5a460 | inbounds: allow custom monthly traffic reset days (#6071) | ||
|
|
ca6955d88b |
feat(ui): validate the REALITY client version range at save time (#6126)
* feat(ui): validate the REALITY client version range at save time The impossible range from PR #6125 — a max below the effective minimum — could still be saved; the tooltip only helps a user who hovers it. Add save-time validation mirroring xray-core's parser (up to three dot-separated parts, each 0-255) on both fields, plus a cross-field check that a non-empty max is not below a non-empty min. Errors are field-level i18n keys following the REALITY target precedent, so the modal stays open and points at the offending field instead of storing a config that rejects every client. A malformed min is reported by its own field and skipped by the max comparison, so the user sees one precise error per field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): reject untrimmed client versions and revalidate max on min edits From review: the validators trimmed but the save path ships the value verbatim, and xray-core's part parser accepts no surrounding whitespace — so a green form could still save a config the core refuses to load. Reject any value that differs from its trimmed form. Also revalidate the max field after a min edit when max already shows an error, so correcting the min clears the stale cross-field message without waiting for the next submit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6af2995930 |
feat(api): add GET endpoint to look up clients by Telegram ID (#5945)
* feat(api): add GET endpoint to look up clients by Telegram ID
GET /panel/api/clients/getByTgId/:tgId returns all clients matching the given Telegram user ID. tgId is not unique, so the response is an array of {client, inboundIds, externalLinks, usedTraffic} objects.
* fix: guard tgId=0 sentinel, index tg_id, deduplicate enrichment in getByTgId
Three issues from the code review on the new GET /panel/api/clients/getByTgId/:tgId
endpoint: the lookup did not short-circuit tgId <= 0 (this codebase's sentinel
for 'no Telegram ID'), had no index on clients.tg_id causing a full table scan
on every call, and duplicated the per-record enrichment (inbound IDs, external
links, effective flow, traffic) identically between get and getByTgId.
- Reject tgId <= 0 in GetRecordsByTgId with a clear error, matching the
'0 = none' convention used elsewhere in the codebase.
- Add index:idx_clients_tg_id to ClientRecord.TgID (struct tag + idempotent
startup migration for existing databases).
- Extract buildClientPayload helper used by both get and getByTgId.
- Update client_lookup_test.go to verify sentinel rejection instead of
expecting tgId=0 to be a valid lookup.
* refactor(api): move Telegram client lookup under /get/tgId/:tgId
Nest the Telegram-ID lookup beside the email lookup as /get/tgId/:tgId
instead of the flat /getByTgId/:tgId, so both client fetch routes share the
/get prefix. Gin resolves the static tgId segment ahead of the :email
wildcard, so /get/:email keeps matching plain email lookups, including a
literal 'tgId' email. The endpoint is unreleased, so no compatibility
concern.
|
||
|
|
ff954ec48c |
fix: stop deleting client_traffics for detached-but-alive clients (#6110)
* fix: stop deleting client_traffics for detached-but-alive clients MigrationRemoveOrphanedTraffics keyed "orphaned" off presence in some inbound's settings.clients[] JSON, a definition that predates #4469's standalone clients table. ClientService.Detach intentionally keeps a client's traffic row when it drops its last inbound attachment (so it can be re-attached later without losing stats/expiry), but that client has no entry in any inbound's JSON anymore - so every x-ui migrate run or backup restore deleted its traffic row anyway, even though the client itself was untouched and still listed. Scope the query to the clients table instead, which is the function's actual intent. Separately, frontend/src/hooks/useClients.ts recomputed the clients summary from the client_stats WS snapshot as soon as it arrived, even when that snapshot held fewer rows than the server's own total (e.g. exactly the gap above, or any other client with no client_traffics row). The recompute can only bucket the clients it was given, so the missing ones silently fell out of every bucket while the headline total still counted them - the Ended/Disabled cards read 0 and their hover lists were empty even though the table below listed those rows, leaving the Filter drawer as the only way to reach them. Extracted the decision into pickClientsSummary and added the guard: fall back to the server summary (built from the clients table, always sums to total) whenever the snapshot doesn't cover every client. Fixes #6102. * fix: union both keep-sets instead of replacing (review feedback) Address the automated review on this PR: switching MigrationRemoveOrphanedTraffics to key solely off the clients table traded the original bug for a worse one. The one-shot ClientsTable seeder (internal/database/db.go) skips a client it fails to unmarshal and never retries, so a client still live in an inbound's settings.clients[] JSON can have no clients row at all - the new predicate deleted its traffic row too, and an empty clients table would have emptied client_traffics outright. Union both keep-sets: a row survives if it's referenced by either the clients table or any inbound's JSON, and is removed only when it's in neither. Log the delete's outcome instead of discarding it silently, since a whole-table wipe would otherwise leave no trace. Rewrote the migration test as a table of all four combinations, driven through real ClientService calls (SyncInbound, Detach) rather than hand-built rows wherever a real path produces the state, so it tracks actual behavior instead of an assumption about it. Added the missing case the review flagged: a client live in JSON only, with no clients row, must survive. Also stripped the // comments this PR had added - CLAUDE.md states committed Go/TS carries none, which the review separately flagged. |
||
|
|
8f49327efb |
feat(sub): allow identity tokens on every subscription link (#5935)
Keep usage tokens first-link-only while adding an opt-in setting for repeating EMAIL and USERNAME in subscription-body remarks. Co-authored-by: x06579 <x06579@ai-dashboard> |
||
|
|
a2774bf212 |
fix(ui): explain the REALITY client version gate and drop the impossible placeholder (#6125)
* fix(ui): explain the REALITY client version gate and drop the impossible placeholder An empty Min Client Ver looks unrestricted, but Xray-core silently falls back to a built-in minimum (currently 26.3.27) that rejects third-party cores such as Mihomo and sing-box with a bare REALITY verification failure, and nothing in the panel points at the field. Add tooltips to both version fields explaining the fallback and its TLS-fingerprint-freshness rationale. The Max Client Ver placeholder (25.9.11) sat below the built-in minimum, so filling in both placeholders produced a range that rejects every client. Remove it; empty genuinely means no upper limit for that field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(reality): warn that an empty min client version rejects old cores Common pitfalls covered bad targets, SNI mismatches, leaked keys and wrong flow, but not the client version gate that currently bites Mihomo and sing-box users. Add it to all four doc languages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): word the version hints against the effective minimum Address the automated review: the Max Client Ver hint said only 'not lower than Min Client Ver', which re-establishes the empty-means-unset mental model when the effective floor is the core's built-in minimum. Both hints now name the effective minimum and tie the quoted 26.3.27 to the core build the panel runs, since operators can install any Xray-core version. Also from review: full-width quotes and a missing verb in the zh doc bullet, the idiomatic Arabic opening, and a format-only x.y.z placeholder on Max Client Ver so the field still conveys its shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
48675ff197 |
style(i18n): normalize Chinese-English spacing (#6076)
Add consistent spacing between Chinese text and Latin terms in the Simplified and Traditional Chinese translations to improve readability without changing keys or placeholders. |
||
|
|
4605f00a15 |
fix(nodes): keep the credential-presence flag on the node heartbeat push
The Nodes page cache is overwritten wholesale by the heartbeat websocket push, but the job broadcast a raw []*model.Node while the REST list returns []*service.NodeView. model.Node tags the api token json:"-" and carries no hasApiToken field, so every push stripped the flag the edit form reads to decide whether a token is already stored. One 5s tick after the page loaded, editing any non-mTLS node then failed with "Name, address, port and API token are required" — and stayed failed, because setQueryData refreshes dataUpdatedAt, so the query never goes stale and never refetches the intact REST payload. Broadcast the NodeView read contract instead. |
||
|
|
dc6a16019e |
fix(xray): reject configs xray-core refuses, and check the fixtures against it
The frontend's golden fixtures are the panel's model of an xray config, but nothing ever asked xray-core whether it would accept them: the snapshots only prove the Zod schemas agree with themselves. Building every fixture through the same config builders the panel hands its config to — conf.InboundDetourConfig for the full-config and AddInbound paths, conf.RouterConfig for ApplyRoutingConfig, conf.DNSConfig for the dns section — found seven the core refuses, three of them reachable from the panel's own UI. A refusal is not scoped to one inbound: the config fails to load and every inbound stays down. Hysteria: xray-core builds version 2 only, in both the protocol settings and the transport settings, but the inbound settings schema accepted any version from 1 up and its comment claimed upstream still supported v1. Both fixtures carried version 1. The schema now pins 2, GenXrayInboundConfig heals stored rows on the way out the way it already heals shadowsocks ciphers and wireguard peers, and the share link drops the dead hysteria:// scheme — the subscription server already emitted hysteria2:// for the same inbound. XHTTP uplinkDataPlacement: both transport forms offered "query", which the core has never accepted for that field (auto and body always, cookie and header in packet-up mode). Replaced with auto, which was missing, and the default label now names auto rather than body. FinalMask items: switching an item to the rand-driven array kind wrote packet:[] next to the rand. xray-core counts an empty array as a packet and every item kind is exclusive, so noise answers "len(item.Packet) > 0 && item.Rand.To > 0" and header-custom "exactly one item kind must be set". The editor now clears the packet, and GetXrayConfig strips the residue from rows already saved with it. The remaining four were stale fixtures: an xmc mask still on the usernames shape v26.7.28 replaced with profiles, a fragment mask with no length, and header-custom and noise items passing an array to the string packet kind — all shapes the panel's own editors cannot produce. golden_fixtures_xray_test.go keeps this from drifting again: every fixture in every category is built through xray-core on each run, with a self-signed pair standing in for the deployment certificate paths, so the next core bump reports which fixture it broke. |
||
|
|
fea6a20f7c |
fix(xray): stop the runtime user API from crashing xray-core
Exercising the whole XrayAPI surface against a real xray-core 26.7.28 (the version go.mod pins) turned up a way for ordinary panel activity to kill the core process, plus two smaller mismatches with what the core actually does. buildUserAccount picked the shadowsocks account type by falling through to a 2022 account whenever the cipher was not one of six hardcoded names. xray's legacy and 2022 inbounds cast the account they are handed without checking (proxy/shadowsocks/validator.go, proxy/shadowsocks_2022/inbound_multi.go), so the wrong type is not an error — it panics the core and drops every connection on the server. The fallback was reachable without any misconfiguration: autoRenewClients hands AddUser the client object straight out of the inbound's settings, where the cipher lives under "method", never "cipher", so every auto-renewed client on a legacy-cipher shadowsocks inbound took xray down. The xray-valid aead_* aliases hit it too. The cipher is now read from either key, matched with the same table (and case-insensitivity) the core's own conf package uses, and an unrecognized one is an error instead of a guess. The legacy shadowsocks validator is also the only one that accepts a second user under an email it already holds, and RemoveUser then drops just one of them — a disabled or expired client kept connecting. AddUser now drops the email first on that account type so a single removal fully revokes the client. GetTraffic skipped every stat the first time it saw it. xray creates a counter on a user's first use, so that dropped a new client's traffic for a whole polling interval, as did the counter reset after a core restart. Only the first poll of a process is a baseline now; later, unseen and rewound counters both count from zero. Also fixes three unchecked settings["method"].(string) assertions that panic the panel on a shadowsocks inbound whose settings carry no method, and bounds TestRoute's port so an out-of-range value cannot wrap into the uint32 the core is asked about. Tests: api_users_e2e_test.go drives add/remove for every protocol against a real core and asserts it survives each one (skipped unless XRAY_E2E_BINARY is set); the account-type, traffic-delta and renew paths get unit coverage. |
||
|
|
7f7b7e16a4 |
feat(xray): update xray-core to v26.7.28 and adapt panel
Bump xtls/xray-core to 5ca6f4b7d4dc (v26.7.28) and move the three binary pins (DockerInit.sh, the Linux and Windows URLs in release.yml) in lockstep so the in-process conf.Build() validation and the child binary agree. XMC finalmask (#6487) is the breaking change. The mask's `usernames` string list is gone, replaced by a required `profiles` array whose entries each need a 3-16 character [A-Za-z0-9_] username, a parseable UUID and both Mojang texture fields; the "default to Dream when empty" fallback was removed, so an xmc mask saved by an older panel now fails to build and takes the whole config down with it rather than degrading one inbound. The textures are a signed blob only Mojang's session server can issue, so a legacy username cannot be upgraded automatically. The panel now: - rejects an incomplete xmc mask at save time (AddInbound/UpdateInbound), pointing at the specific field that is missing; - drops only the offending mask when generating the core config, for rows that never went through the form (upgrade, node sync, restored backup, direct DB edit), warning which inbound lost its obfuscation instead of leaving every inbound offline; - carries legacy usernames into profile stubs in the finalmask form so the operator keeps their player names and sees exactly what still needs filling in, and edits profiles through a list editor. No destructive DB migration: unlike the removed shadowsocks ciphers there is no valid replacement to rewrite to, and dropping the mask from stored rows would discard the operator's hostname and password for config they can still repair. The generation-time strip already prevents the startup failure. Also track the core's xmux maxConnections fallback, lowered from 6 to 3 for anti-TSPU, in the fresh-XMUX seed so a new panel config matches what the core would pick on its own. TUN gained a `desc` key and random utunN naming, but the Go validator no longer accepts TUN inbounds and the panel only renders legacy saved rows, so nothing there needs adapting. The remaining commits are REALITY log-warning wording, gRPC/XHTTP localAddr accuracy and a routing tweak, none of which change the JSON config surface. Tests cross-check the panel's profile predicate against conf.XMCProfile.Build() so a future core release that tightens or relaxes the rules fails loudly rather than silently emitting configs the core refuses to start on. |
||
|
|
bb6a874dc5 |
fix(amneziawg): enable sniffing on the TPROXY bridge
Domain-based Routing rules could never match RouteThroughXray traffic: an AmneziaWG peer resolves DNS itself, through the tunnel, before ever sending a packet, so the decapsulated traffic TPROXY hands to the bridge is already a bare destination IP with no domain name attached at the network layer. Every other inbound recovers this via sniffing (confirmed working for the stock wireguard inbound, which does have it configured); the bridge never got a sniffing block at all, so only tag/IP/network-based rules could ever match it -- any domain rule above it in the list was silently unreachable. |
||
|
|
5b58369af2 |
feat(logs): show which AmneziaWG client an access-log line belongs to
The dokodemo-door TPROXY bridge every AmneziaWG peer's traffic is routed through has no per-user identity, so Xray's own access log never carries an "email:" token for these lines -- the Access Logs modal showed a blank Email column for every in-*-udp row, even though every other protocol's rows show the client normally. The peer's decapsulated tunnel IP does survive as the log's "from" address, and that IP deterministically maps to exactly one configured peer. Builds a "<inbound tag>|<ip>" -> email index from the same AmneziaWG inbounds already parsed elsewhere (amneziawg.InstanceFromInbound), and fills in Email from it whenever the raw log line didn't have one. |
||
|
|
8bc00d1e90 |
style: drop the line comments added with the triage fixes
CLAUDE.md rules out // line comments in committed Go. The rationale they carried is in the commit messages for each fix; doc comments that already existed are kept, updated where the code they describe changed. Also replaces reflect.Ptr with reflect.Pointer and rewrites the YAML keyword alternation as a lookup table, both flagged by golangci-lint. |
||
|
|
6f4cc1e53c |
fix(xray): emit an empty client array instead of null in the generated config (#6117)
finalClients was a nil slice, so an inbound that has a clients key but whose clients are all filtered out — disabled by an admin, or cut by the traffic job for quota or expiry — was handed to xray-core as "clients": null. The panel already treats a stored null client list as invalid data and coerces it to [] at startup, and null is what reporters see in bin/config.json when they go looking for a connectivity problem, which sends the diagnosis after a serialization bug that is not there. Build the slice empty so the same state serializes as []. The reported inbound also needs the clients table to be in sync, which is a separate question still open on the issue. |
||
|
|
0e69f64e56 |
fix(job): bound the traffic-notify POST so a stalled receiver can't wedge it (#6115)
informTrafficToExternalAPI posted through the package-level fasthttp.Do, which carries no read or write deadline. Run() is scheduled @every 5s under cron.SkipIfStillRunning, so a receiver that accepts the connection and then neither answers nor closes did not just delay one notification — it held the job, and every following tick was skipped for the duration. What stops with it is more than counters: AddTraffic runs autoRenewClients and disableInvalidClients in the same call, so quota and expiry enforcement stall too, and an over-quota client keeps transiting for the whole hang. The online-client refresh and the websocket broadcasts sit later in the same tick. Give the endpoint its own client with read/write deadlines and a DoTimeout budget under the poll cadence, close the connection rather than pooling it for a call this infrequent, and skip the POST outright when there is nothing to report. Retries stay off: the payload carries per-tick deltas, so a resend after a failed response leg would double-count on the receiver. Verified against a listener that accepts and stalls: fasthttp.Do was still blocked after 8s, the new client returns at its 3s budget. |
||
|
|
f8e9f2f087 |
fix(node): stop a departed master's frozen traffic from disabling clients (#6113)
client_global_traffics rows are keyed by (master_guid, email) and are only
ever overwritten by a push from that same master. A master that stops
pushing — decommissioned, reinstalled under a fresh GUID, or detached from
the node — therefore leaves its last snapshot behind permanently.
depletedClientsCond's cross-panel EXISTS branch matched any such row, so a
node kept comparing a client's quota against counters frozen weeks earlier.
Once they exceeded the quota the node disabled the client on every traffic
poll, and the node -> master enable merge latched that off on the master too,
where nothing sets it back. The reported symptom is exactly this: a client at
11 GB of a 24 GB quota, enabled on two nodes, disabled on the third, which
still held a 27-day-old row from a previous master reporting 30 GB.
Bound both the enforcement predicate and the display overlay to rows a master
refreshed within globalTrafficFreshWindow. Masters push every 30s, so a live
master is never affected; a master that is merely unreachable for a while
keeps enforcing for a full day before its numbers are set aside.
The one-way enable merge that makes such a disable permanent on the master is
deliberate (
|
||
|
|
ea00e69d4d |
chore: remove Docker support entirely from this fork
Never used by this fork's own distribution (install.sh/x-ui.sh is the only supported install path), AmneziaWG structurally can't run in the Alpine-based image anyway, and Docker Hub publishing was failing on every release for lack of configured credentials. Removed Dockerfile, docker-compose.yml, DockerEntrypoint.sh, DockerInit.sh, .dockerignore, and the docker.yml CI workflow; dropped the now-dead "Docker" README subsection (all 7 languages), the "Docker image"/"Docker Compose" options from the issue/PR templates, and corrected claude-bot.yml's now-stale references to the deleted files, the never-actually-ours ghcr.io/mhsanaei/3x-ui image, and (caught in passing) an already-stale claim that Windows is a supported platform. Left untouched: generic container-runtime adaptations that apply regardless of image source (x-ui.sh's running-in-docker detection, the virtual-interface-name filters, the DNS-over-container-network note) and deploy/test/smoke-noninteractive.sh, which uses Docker only as its own test sandbox, not as something this repo ships. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
1aa81428b8 |
fix(traffic): show live Speed for AmneziaWG and MTProto inbounds/clients
The Speed column showed "--" for AmneziaWG (and MTProto, which has the identical gap) even while cumulative traffic totals were correct. XrayTrafficJob drives live speed by querying xray-core's own stats API and broadcasting the delta over websocket -- but AmneziaWG/MTProto never run inside xray-core's own runtime inbounds, so they're invisible to that API. Their own jobs already compute the same per-poll delta shape (that's what keeps cumulative totals correct) but never broadcast it. Reusing the existing "traffics"/"clientTraffics" broadcast would have two real bugs: the frontend's existing scope/replace logic would let each side clobber the other's speed on its next unrelated tick, and the websocket hub's per-message-type throttle is keyed only by message type, not caller -- since both sidecar jobs run on identical "@every 10s" grids registered milliseconds apart, one would silently lose almost every broadcast if both protocols were ever configured together. Fixed with a small unthrottled broadcast path (both sidecar jobs are already self-rate-limited by their own cron cadence) and protocol- namespaced wire keys, tracked in their own frontend state and merged into the existing inboundSpeed/clientSpeed only at read time -- so every existing consumer needs zero changes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
2736f9beb3 |
feat(xray): autocomplete geosite/geoip categories in the routing rule editor
Writing a routing rule today means remembering exact geosite:/geoip:/ ext:file:code syntax by hand, with no way to discover what categories actually exist in the .dat files sitting in the bin folder -- including custom ones like geosite_roscom.dat added via the Geodata auto-update feature. The Domain/IP fields in the rule editor now suggest categories as you type (e.g. "you" -> "geosite:youtube"), built live from whatever .dat files are actually on disk, while still accepting any free-typed value exactly as before. Backend: GET /panel/api/xray/getGeodataCategories scans the bin folder, parses matched geosite*/geoip*.dat files via xray-core's own exported protobuf types, and formats each category as the exact rule syntax xray-core's parser accepts -- geosite:/geoip: for the default files, ext:<file>:<code> for anything else (there's no shorthand for custom files). Cached in memory keyed by each file's (name, size, modTime) so a request-time scan is cheap until a file actually changes. Frontend: the Domain/IP inputs become Select "tags" fields fed by a new useGeodataCategories() query hook, with an explicit substring filter so "you" matches "geosite:youtube" (not a prefix). The array<->CSV-string adapter lives entirely at the FormField transform boundary, so the underlying form schema and saved rule shape are unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
814369da38 |
fix(panel): recognize this fork's -awg.N tags in version comparison
internal/config/version was never bumped past 3.5.0 when v3.5.0-awg.1 was tagged, so a freshly-updated panel kept reporting its own version as the plain upstream base. On top of that, parseVersionParts (Go and its TypeScript mirror) required exactly 3 dot-separated numeric parts, so it rejected the -awg.N suffix entirely and fell back to a raw string inequality that reports "update available" any time the strings merely differ -- which they always do here, even when already on the latest tag. Bump the embedded version to 3.5.0-awg.1 and extend both parsers to treat "-awg.N" as an optional 4th, lower-priority component (defaulting to 0 for a plain tag), so e.g. 3.5.0-awg.2 > 3.5.0-awg.1 > 3.5.0 and a matching tag compares equal instead of always looking outdated. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
1ed9cd8ea1 |
fix(inbound): enforce node-eligibility server-side, not just in the UI
Investigated multi-node interaction with AmneziaWG: the master's own reconcile (DesiredAmneziaWGInstances) and Xray config generation (injectAmneziawgEgress, the GenXrayInboundConfig protocol skip) all correctly filter on NodeID IS NULL, so a node-assigned AmneziaWG (or MTProto) inbound would never be managed by the master. But nothing stopped one from being created that way: NODE_ELIGIBLE_PROTOCOLS (frontend/src/pages/inbounds/form/InboundFormModal.tsx) only hides the node picker client-side -- a direct API call could set nodeId on an AmneziaWG inbound, which every node then reconciles as an ordinary local inbound (nodes run the identical binary, full cron suite included), leaving it running unmanaged and untracked by the master's own AmneziaWG bookkeeping. Added isNodeEligibleProtocol (inbound_protocol.go), mirroring the frontend's allowlist, and enforced it in both AddInbound (the actually exploitable path -- nodeId comes straight from the request) and UpdateInbound (defense in depth; NodeID is already restored from the stored row there before this check, so it mainly guards against a protocol change on an existing node-hosted inbound). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
df6d2f7652 |
fix(amneziawg): resolve 4 Low findings from the automated PR review
- manager.go: serverAddress assumed subnetIp always ends in ".0"; a base like "10.8.1.5" was used verbatim as the server's own address, eventually colliding with peer allocation (which starts at .2 upward). Now derives the first host of the actual subnetIp/subnetCidr network via netip, matching serverAddressV6's own approach. A /32 base (no host bits at all) is still used as-is. (Finding 12, partial -- the /16 pool-widening half of this finding only exists on the upstream-pr/amneziawg branch's merged client_wireguard.go, not here; handled separately on that branch.) - manager.go: ensureLocked carried the previous per-peer traffic counters (`last`) forward even through a full restart, but awg-quick down+up resets the kernel's own counters to zero -- the next CollectTraffic computed a large negative delta (clamped to 0), silently discarding real traffic. Extracted the decision into nextTrafficBaseline: only a reload (syncconf) preserves the baseline. (Finding 13) - portfwd.go: exported ForwardedPortsInclude; inbound_amneziawg.go's new checkForwardedPortsConflict uses it to reject, at save time, a client's forwardedPorts that would DNAT the panel's own port or another enabled inbound's port to the tunnel client -- portForwardLines has no destination restriction, so this collision was previously silent. Wired into both the single-client update path and the add-client path (client_inbound_apply.go), plus normalizeAmneziaWGSettings for the whole-inbound save path. (Finding 14) - inbound.go: InboundOption.AwgServer sent the whole ServerSettings struct including PrivateKey to GetInboundOptions callers -- a shared, admin-wide dropdown-filling endpoint the frontend's own AwgServerOptionSchema never reads that field from. Redacted it before assigning. (Finding 11) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
71dc453970 |
feat(amneziawg): make the Xray TPROXY bridge a per-inbound opt-in
Addresses Finding 10 from the automated PR review: an always-on TPROXY bridge makes every AmneziaWG tunnel hard-depend on Xray being up (all traffic, including DNS, drops whenever Xray restarts), and forces a full awg-quick down+up bounce on any client add/remove/re-IP, permanently losing the syncconf fast path. Adds ServerSettings.RouteThroughXray (off by default): - defaultPostUpDown only emits the TPROXY/policy-route rules when it's on; a plain AmneziaWG tunnel now has zero Xray dependency out of the box. - structuralFingerprint covers it (toggling it changes whether PostUp/ PostDown contain any TPROXY rules at all -- structural, not a per-peer host-rule). hostRulesFingerprint's IPv4 tracking is now itself conditional on RouteThroughXray (and IPv6 tracking on IPv6Enabled), so an instance that never uses either keeps the syncconf fast path for a plain peer re-IP. - injectAmneziawgEgress only creates a bridge for inbounds that opted in; checkAmneziawgEgressConflict (the Finding-7 fix) now parses each candidate through InstanceFromInbound so a non-routed inbound's port is correctly never treated as reserved. - New inbound-level Switch in the AmneziaWG form; the actual outbound decision is still made entirely through the panel's stock Routing page, same as before -- only whether the bridge exists at all is now a choice. Translation keys added to all 13 locales in the same commit this time, not backfilled later (see Finding 9's lesson). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
c41f97cf86 |
fix(amneziawg): resolve 7 Medium findings from the automated PR review
Each is independently reproducible; fixed together since one review pass found all of them. - manager.go: the shared "ip rule add fwmark" policy route had no existence check, so it duplicated in "ip rule show" on every interface bounce (which hostRulesFingerprint forces on any client add/remove/ re-IP). Now checked via "ip rule list | grep -q ..." first. (Finding 2) - params.go: ExternalInterface, IPv6ExternalInterface, and subnetIp/ subnetCidr are interpolated unescaped into a shell-executed PostUp/ PostDown line, but only obfuscation and the IPv6 subnet were validated before save. Added ValidateInterfaceName (a strict charset+length pattern) and ValidateSubnetIPv4 (netip.ParsePrefix), wired into normalizeAmneziaWGSettings. (Finding 3) - amneziawg_job.go: IsAwgInstalled() existed but nothing ever called it, so a host without awg/awg-quick (the Docker image, RHEL, Arch, a failed install.sh PPA step) logged a reconcile failure every 10s forever. Now checked once an inbound actually needs it, warning once instead of spamming. (Finding 4) - client_inbound_apply.go: the WireGuard/AmneziaWG credential carry-forward (added so a metadata-only client edit doesn't rotate keys) never covered ForwardedPorts, so a partial edit -- an API call or Telegram-bot toggle that omits the field -- silently wiped a client's port-forwarding spec. Carried forward and written back the same way the key fields already are. (Finding 5) - manager.go: hostRulesFingerprint keyed each peer on its IPv4 address only, and structuralFingerprint omitted IPv6Enabled/IPv6ExternalInterface entirely, so an IPv6-only change could pick the syncconf reload path (which never re-runs PostUp, leaving a stale NDP-proxy entry) or be a complete no-op. Both fingerprints now cover the IPv6 fields. (Finding 6) - port_conflict.go: the AmneziaWG egress bridge (injectAmneziawgEgress) binds 127.0.0.1:63100+id with no collision check anywhere, since it isn't a database row the ordinary port-conflict query can see -- same blind spot the reserved Xray API port already has its own check for. Added the equivalent check for the AmneziaWG bridge port. (Finding 7) - install.sh: install_amneziawg ran unconditionally for every install/ update, building a DKMS kernel module and enabling host-wide IPv4/IPv6 forwarding whether or not the feature is ever used. Gated behind a new should_install_amneziawg (XUI_INSTALL_AMNEZIAWG=true/false, or an interactive y/N prompt defaulting to no). Also replaced the deprecated apt-key adv with a dedicated keyring + signed-by= on the Debian branch, and guarded its sources.list appends against duplication on a retried install. (Finding 8) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
510b43f32d |
i18n(amneziawg): backfill IPv6/obfuscation/port-forwarding keys in 11 locales
Only en-US/ru-RU ever got these 9 keys as each AmneziaWG feature landed (the regenerate-obfuscation button, then Phase 2a's IPv6 fields, then Phase 2b's per-client ForwardedPorts) — the other 11 locale files were never backfilled, so i18next has been silently falling back to English for all of them since Phase 1. Cosmetic-only (never broke anything), but now closed for every shipped locale. |
||
|
|
db8253421a |
refactor(amneziawg): route via Xray through the stock Routing page, not custom toggles
Simplifies RouteViaXray after realizing the panel already has everything needed: the Routing page already lets an admin pick a source inbound tag and a target outbound (plus, if they want it, a specific source IP) for any protocol. Bolting a parallel routeThroughXray/routeOutboundTag pair onto both the client and inbound forms duplicated that mechanism instead of using it. Removed entirely: Client/ClientRecord/ServerSettings/Peer's RouteThroughXray + RouteOutboundTag fields, the effective-routing OR/ fallback logic in InstanceFromInbound, and the Switch+Select UI on both forms. Nothing configures "route via Xray" as a setting anymore. In its place, every enabled AmneziaWG inbound now gets its own Xray TPROXY bridge unconditionally, by default, no toggle: - internal/amneziawg: every peer's traffic is always TPROXY'd into that instance's own bridge (defaultPostUpDown, port derived from the inbound's id via EgressPortForInbound so the kernel side and the Xray-config side never need to negotiate a runtime value). Since the TPROXY rule is now tied to a peer's mere presence rather than an opt-in flag, hostRulesFingerprint now covers every peer unconditionally (add/remove/re-IP forces a restart, the same way ForwardedPorts always did) instead of skipping peers with nothing to opt into. - internal/web/service/xray.go's injectAmneziawgEgress creates one dokodemo-door bridge per qualifying inbound, tagged with that inbound's own real tag — the same trick injectMtprotoEgress already uses (reusing a real inbound's tag), which is why it's already selectable in the panel's Routing page: InboundService.GetInboundTags() is a plain, protocol-blind SELECT over every inbound row's tag, no dedicated UI plumbing needed. The function never generates a routing rule itself anymore — where (if anywhere) that traffic goes is entirely up to whatever rules the admin adds through the existing Routing UI. Frontend: no new UI at all. Tests rewritten to match — one bridge per inbound with its own tag/port, no rule generation, no opt-in gating. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
909feefd1d |
fix(amneziawg): make RouteViaXray an inbound-level option too
RouteThroughXray/RouteOutboundTag were client-only, but the more common case is "route this whole AmneziaWG server's traffic through Xray", not configuring every peer individually. Add the same pair to ServerSettings (inbound-level) while keeping the per-client fields as an override — matching how ExternalInterface/IPv6Enabled already work at the server level next to per-client settings like ForwardedPorts. Effective per-peer decision (computed once, in InstanceFromInbound, not duplicated at each consumer): - routed = client.RouteThroughXray || server.RouteThroughXray - outbound tag = client's own if set, else the server's default This means a peer can be routed by the inbound-wide default with no config of its own, opt in on its own even when the default is off, or keep the default's on/off but pick a different outbound than everyone else. internal/web/service/xray.go's injectAmneziawgEgress now calls amneziawg.InstanceFromInbound instead of re-parsing InboundSettings and reading model.Client fields directly — the same effective-routing computation the kernel-side TPROXY rules use, so the two independent reconcile loops (Xray-config generation and the AWG manager) can never quietly disagree about which peers are actually routed. Frontend: Switch + conditional outbound Select added to the AWG inbound form (mirroring the client-form version and mtproto's own UI), plus the inbound-defaults.ts default-object fix that's bitten this project's CI before (Phase 2a) whenever ServerSettings gains a new required-shaped field. Test fixtures in xray_config_inject_test.go needed a real Server block and PublicKey once injectAmneziawgEgress started requiring a usable InstanceFromInbound result — both were implicit fixture gaps, not behavior the old tests were actually asserting. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
d1b77b2aa4 |
feat(amneziawg): Phase 2c — RouteViaXray (TPROXY into Xray)
Per-client toggle (RouteThroughXray + RouteOutboundTag) that TPROXYs a peer's traffic into Xray instead of NAT'ing it straight out the host's network interface, so it can egress through any configured Xray outbound (or balancer) — a VLESS/proxy chain, WARP, etc. Discovered mid-design that internal/mtproto already solved the "let a native sidecar's traffic egress through Xray" problem once, via routeThroughXray/routeXrayPort/outboundTag + injectMtprotoEgress: a loopback bridge inbound plus a routing rule. AmneziaWG can't reuse it directly — mtg is a userspace process that dials *out* through a local SOCKS proxy, while AmneziaWG is a kernel tunnel interface with no process of its own to redirect. The Xray-side shape carries over almost exactly, the kernel-side plumbing is new: - internal/amneziawg/route_egress.go: EgressPort/EgressTag/EgressFwmark/ EgressTable are one shared constant set, not one bridge per peer. Every routed peer, across every AmneziaWG instance, TPROXYs into the *same* loopback dokodemo-door bridge; the per-peer distinction happens downstream, in Xray's own router, matched against each peer's TPROXY-preserved source IP (Xray's field-rule `source` matcher — a capability the router already had). This avoids two independent reconcile loops (the AWG manager and the Xray-config generator) ever having to agree on a dynamically-picked port for each peer. - manager.go's defaultPostUpDown emits a per-peer mangle-table TPROXY rule (matched by tunnel source IP) for each opted-in peer, plus the fwmark->table->local-everywhere policy route TPROXY needs to deliver those packets to the bridge. That policy route is system-wide, not interface-specific, so — like the existing IPv6-forwarding sysctl — it's added idempotently and never torn down in PostDown; a second AmneziaWG instance with its own routed peers must find it already in place, not race to remove what the first still needs. - The existing portForwardFingerprint became hostRulesFingerprint, covering both ForwardedPorts and RouteThroughXray/RouteOutboundTag: both only ever take effect through PostUp/PostDown, which `awg syncconf` never re-runs, so either one changing must force the same full interface bounce. - internal/web/service/xray.go's new injectAmneziawgEgress mirrors injectMtprotoEgress/injectPanelEgress's safety rules, adapted for one bridge serving many peers: an invalid or missing outbound target skips only that one peer's rule (not the whole bridge, since other peers may still need it), while the bridge itself is skipped entirely when nothing needs it or its tag is already taken by a real inbound. Frontend: a Switch + conditional outbound Select on the client form (showAmneziawg only), mirroring mtproto's own routeThroughXray UI and reusing its useOutboundTags hook. install.sh now modprobes the mainline TPROXY modules (xt_TPROXY, nf_tproxy_ipv4/ipv6) alongside the existing AmneziaWG setup — ordinary upstream kernel modules, no DKMS/PPA needed unlike the AmneziaWG module itself. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
69de904bf6 |
feat(amneziawg): Phase 2b — per-client port-forwarding
Admins can now set a per-client ForwardedPorts string (e.g. "80, 443, 8000-8100") that gets DNAT'd + FORWARD'd to that peer's tunnel address via iptables rules in PostUp/PostDown, ported and simplified from coinman-dev/3ax-ui's shared/portfwd. Two decisions worth flagging for future readers: - The iptables --comment tag on each rule is awg-fwd-<fnv32a(email)>, not the raw client email. Email is admin/API-supplied free text that ends up embedded in a shell-executed PostUp/PostDown line; a hash can never carry a shell metacharacter through where raw interpolation could. - The reconcile manager gained a third fingerprint (portFwdFP, next to the existing structural/peers ones). `awg syncconf` only touches the WireGuard peer table — it never re-applies PostUp/PostDown iptables rules — so a port-forward-only change has to force a full awg-quick down+up bounce, same as a structural change, rather than the lighter sync a plain peer add/remove can use. Also fixes a real pre-existing bug found while wiring up IPv6 client allocation in the previous commit's spirit: allocateWireguardAddress always suffixed "/32" regardless of address family, which produced invalid host bits for IPv6 (needs "/128"). ForwardedPorts flows through model.Client -> model.ClientRecord (gorm column wg_forwarded_ports, auto-migrated) -> ToRecord/ToClient/ MergeClientRecord, mirroring the awgServer field's earlier lesson that new fields need checking against a second, hand-maintained persistence-layer struct. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |