mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-10 23:25:02 +03:00
124665c5ef4d33f05ebac04ae5a9720d2bfe8b3c
425 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
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. |
||
|
|
2c943da3e0 |
fix(frontend): keep DNS hosts synchronized (#6158)
* fix(frontend): keep DNS hosts synchronized * fix(frontend): preserve incomplete DNS hosts * fix(frontend): reset DNS host drafts when disabled * fix(frontend): clear DNS host drafts when disabled --------- Co-authored-by: PathGao <gaoyanbo@gaoyanbodeMacBook-Air.local> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
55f0281692 |
chore(lint): forbid the Number-or-clamp idiom in direct-write settings pages (#6129)
* chore(lint): forbid the Number-or-clamp idiom in direct-write settings pages Follow-up promised in #6127's review thread: the settings and xray pages write numeric changes straight into state, so a regressed handler silently ships the cleared-port bug again. A scoped no-restricted-syntax rule now rejects Number(...) || N inside an onChange attribute in those directories, pointing at onNumber(). The one remaining match, the Telegram notify interval, moves onto the helper with its floor intact: clearing now keeps the stored count instead of writing 1, and Math.max still clamps typed values. Form modals that stage values behind Zod keep their deliberate clear-means-zero semantics; the rule deliberately does not apply there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(lint): widen the numeric-clamp guard to the shapes that actually drift From review: the rule matched only the Number-or-literal shape, while two semantically identical ternary sites already lived inside its own directories, so 'zero suppressions' reflected the selector's narrowness rather than a clean subtree. The rule now catches the ternary typeof form and the nullish-coalescing form too, is anchored to InputNumber elements so its message can never point a ChangeEvent handler at a number-typed helper, and documents the extracted-handler shape it cannot see. The xray form modals stage values behind Zod like the clients modals do, so a follow-up config object exempts them explicitly instead of the comment claiming they were never in scope. BasicsTab's Happy Eyeballs try-delay — the one genuine direct-write ternary — moves onto onNumber: clearing keeps the stored delay instead of writing 0, and 0 stays reachable by typing it. The Telegram interval gains precision={0} so a typed decimal cannot compose an @every value its own parser rejects on reload. 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> |
||
|
|
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> |
||
|
|
411271b454 |
refactor(ui): share one onNumber handler for numeric setting inputs (#6127)
* refactor(ui): share one onNumber handler for numeric setting inputs The Number(v) || 0 idiom in InputNumber onChange handlers is the root pattern behind the cleared-port bug (#6121): AntD reports a cleared field as null, and || 0 turns that into a stored zero or a min-clamp. The port fields got an inline null-guard; the other sixteen numeric settings kept the idiom, so every new field is a chance to reintroduce the bug. Extract the guard into onNumber(apply): null, empty and NaN change events are ignored so a cleared field snaps back to its stored value on blur, and numeric events pass through unchanged. Convert all sixteen sites in the settings and xray pages. Two sites keep their deliberate different semantics: smtpPort falls back to 587 on clear, and the Telegram notify interval clamps through Math.max. For the non-port fields this changes clearing from storing 0 to keeping the stored value; zero remains reachable by typing it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(ui): fold the remaining hand-rolled numeric guards into onNumber From review: ObservatorySettingsTab's sampling field hand-rolled the same ignore-null semantic and smtpPort kept a fallback-to-587 on clear that nothing documents as intentional and that silently overwrites a configured non-standard port — both now go through the shared helper, leaving the Telegram interval clamp as the one deliberate exception. Also from review: narrow the helper to numbers only (no stringMode input exists in the repo, and the string branch codified a guarantee the number-typed callback cannot honour), soften the docblock to describe behavior rather than promise prevention, add a GeneralTab component test covering the clear-vs-typed-zero semantics, and assert the blur snap-back in both settings tests so a display/state desync cannot ship unnoticed. 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> |
||
|
|
579acbc669 |
fix(settings): keep the stored port when a port field is cleared (#6121)
* fix(settings): keep the stored port when a port field is cleared Clearing the panel-port, subscription-port or LDAP-port InputNumber fired onChange(null), which the handlers coerced to 0; on blur Ant Design clamped the empty field to min=1 and the next save silently persisted port 1. For subPort that breaks the generated subscription links; for webPort it moves the panel itself to port 1 and locks the admin out until the port is fixed via the x-ui CLI. Ignore null changes so clearing a port field snaps back to the last valid value instead of committing a bogus port. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(settings): pin cleared port fields to the stored value Clearing the subscription-port field must not reach updateSetting at all, while typed ports still pass through unchanged. Pins the fix so a handler refactor cannot silently reintroduce the clamped port 1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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. |
||
|
|
89f17f6fef |
fix(frontend): give AmneziaWG the same UDP tag and its own tag color
The Inbounds list only special-cased isWireguard/isHysteria for the "UDP" network badge, so an AmneziaWG row showed just the bare protocol tag with no transport badge next to it. Added the missing isAmneziawg flag (mirrors isWireguard exactly) and wired it into the same branch. Client-row protocol-color maps in ClientsPage/HostList had no amneziawg entry, silently falling back to grey -- ClientInfoModal already had amneziawg: 'yellow' from earlier work, these two just never got it. |
||
|
|
d7a922dcf3 |
fix(frontend): point remaining GitHub repo links at this fork
The sidebar's version badge and the subscription-templates docs link still hardcoded MHSanaei/3x-ui. install.sh and every README already point at Kuzz007/3x-ui; these were the last two functional (non-credit) repo links left pointing upstream. DONATE_URL/DOCS_URL intentionally left alone -- those point at the original author's own donation page and docs site, which is deliberate per this fork's existing convention of crediting/linking upstream for general (non-fork-specific) info. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
3760e01806 |
fix(clients): remove the redundant download button on the SUB link row
Downloading the raw subscription URL as a .txt file duplicates the adjacent copy button for the vast majority of actual usage, and the row already has a real, useful download further down (the per-client config, e.g. AmneziaWG's own .conf). Left the JSON/Clash rows' download buttons alone -- only the plain SUB row's was removed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
032dbabc39 |
fix(deps): migrate off abandoned react-router-dom, fix eslint's own brace-expansion
Two real, forward-compatible fixes for npm audit's high-severity advisories (not the downgrades npm audit fix --force offers): - react-router (GHSA-qwww-vcr4-c8h2, RSC CSRF bypass): react-router-dom is frozen at 7.18.1, pinning the vulnerable react-router@7.18.1 -- no newer react-router-dom release exists pointing at the fixed line. react-router itself has shipped the real fix at 8.3.0. Migrated the 9 files importing from react-router-dom (all using plain createBrowserRouter/RouterProvider/useLocation/useNavigate/Outlet, no RSC anywhere) to import from react-router directly instead. - brace-expansion/minimatch (GHSA-mh99-v99m-4gvg): fixed for eslint's own dependency chain (minimatch@10.2.5, used by eslint itself, storybook, typescript-eslint, swagger-client) via a scoped "minimatch@^10" override forcing brace-expansion to the now-published 5.0.8 patch -- within the range minimatch@10.2.5 already declares wanting (^5.0.5), so this isn't a version-pin workaround, just unblocking a patch release npm's resolver hadn't picked up. One advisory remains genuinely unfixable from our side: eslint-plugin-jsx-a11y pins minimatch@^3.1.2 (old major, never patched); forcing it to the 10.x line via override breaks npm's own dependency-tree validation (a real incompatibility, not just an npm quirk), so this needs an eslint-plugin-jsx-a11y release bumping its own minimatch. Lint-time only, no untrusted input reaches it -- ci.yml's Audit step comment updated to reflect the new, smaller remaining scope. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
f70002d9a6 |
Revert "fix(routing): insert new rules after api instead of appending at the end"
This reverts commit
|
||
|
|
638458bdea |
fix(routing): insert new rules after api instead of appending at the end
A new routing rule created through the Routing tab was always appended to the end of the list. Xray matches rules top-to-bottom, first hit wins, so any pre-existing broader rule (e.g. a block rule with no inboundTag restriction) silently shadows a newly created, more specific rule forever -- it looks saved and enabled but never actually fires. Root-caused this from a report that an AmneziaWG inbound routed through a real outbound lost all connectivity while routing it "direct" worked fine: the AmneziaWG bridge's own TPROXY/interface code turned out to be entirely fine (tunnel stayed up, client stayed online) -- the new rule was just sitting below existing RU-IP/bittorrent/domain block rules with no inbound restriction, so those matched first. Confirmed directly: manually dragging the rule to sit right after the pinned api rule fixed it. This isn't AmneziaWG-specific -- any protocol's newly added rule can be shadowed the same way. New rules now insert right after the pinned api rule (or at the very top if it's absent) instead of at the end, so a newly created rule takes effect by default; drag it lower afterward if a lower priority is actually wanted. 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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
ef13e8567e |
feat(amneziawg): Phase 2a — IPv6 support + NDP proxy
Adds native dual-stack IPv6 to AmneziaWG inbounds, ported from coinman-dev/3ax-ui's approach: - ServerSettings gets ipv6Enabled/ipv6Subnet/ipv6ExternalInterface; Instance carries the server's own IPv6 address (first host of the subnet) alongside its IPv4 one. - defaultAmneziaWGClients allocates an IPv6 host address per client (second AllowedIPs entry) when the server has IPv6 enabled, reusing allocateWireguardAddress — which needed a real fix along the way: it always suffixed "/32" regardless of address family, which is wrong for an IPv6 host address (needs /128). Now family-aware. - generateServerConfig's PostUp/PostDown gains IPv6 forward-accept rules, proxy_ndp sysctl, and one `ip -6 neigh add/del proxy` entry per enabled peer with an IPv6 address — the lightweight per-client method, not the ndppd-daemon whole-subnet method (not worth the config-file-management complexity at this scale; ndppd itself is still installed by install.sh in case that changes later). - ValidateIPv6Subnet rejects a malformed subnet before save. - Frontend: ipv6Enabled/ipv6Subnet/ipv6ExternalInterface fields on the AmneziaWG inbound form, EN+RU translations, openapi.json/generated/* regenerated (the latter via `go run ./tools/openapigen`, pure Go). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
5617cdcf31 |
feat(amneziawg): add a button to randomize obfuscation parameters
Mirrors the existing key-regenerate button next to the private key field. Client-side randomization matches the ranges/constraints of GenerateObfuscation20's "default" preset (internal/amneziawg/params.go) closely enough for a form suggestion — the user can still hand-edit any field afterward. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
edb487a005 |
chore(deps): migrate to react-router 8 and refresh frontend dependencies
react-router-dom 7 is superseded by react-router 8, which folds the DOM bindings back into the core package. RouterProvider now comes from `react-router/dom`, while the hooks and `createBrowserRouter` move to `react-router`. Updates the nine importing modules and the router line in docs/architecture.md to match. Also refreshes antd, react-i18next, storybook, eslint, lint-staged and playwright to current patch/minor releases, and restores alphabetical order in devDependencies for the @vitest/browser-playwright and playwright entries. Bumps brace-expansion to 5.0.8, the only release outside the affected range of GHSA-mh99-v99m-4gvg (unbounded expansion length causing an OOM crash). `npm audit fix` could not apply this on its own: the lockfile pinned 5.0.7 and npm will not re-resolve a transitive-only dependency in place, so the entry was updated directly and reinstalled. |
||
|
|
266f40d79f |
feat(amneziawg): complete frontend parity for the Inbounds list page
The Clients page (form, CRUD, QR/config) already worked from the prior commit; this closes the remaining gap on the Inbounds side and in a couple of protocol allowlists that a plain search for existing wireguard/mtproto handling turned up. lib/xray/inbound-link.ts gets amneziawg-specific link/config builders (genAmneziaWGLink/genAmneziaWGConfig, plus the *s fan-out variants) mirroring the wireguard ones — AmneziaWG has no legacy peers-array to fall back to, so these read settings.clients directly and add the obfuscation lines every client must share with the server. Wired into genInboundLinks generically, and into three consumers that call the wireguard builders directly rather than through that dispatcher: QrCodeModal, InboundInfoModal, and InboundsPage's bulk export. ClientInfoModal, ClientBulkAddModal, and the bulk attach/detach modals each had their own protocol allowlist that needed amneziawg added alongside wireguard/mtproto. Two real gaps surfaced by grepping every remaining 'wireguard' / Protocols.WIREGUARD hit in frontend/src rather than trusting the checklist was exhaustive: - useInbounds.ts's TRACKED_PROTOCOLS gates the deactive/depleted/ expiring/online client counts shown per inbound on the list page; without amneziawg those counts would silently read zero. - inbound-tag.ts is an explicit client-side mirror of the Go backend's port_conflict.go (the file says so itself: "Keep in sync"). It still only special-cased wireguard for UDP, so an amneziawg inbound would have fallen through to the TCP default and disagreed with the backend's own port-conflict math. Also finishes translating the AmneziaWG UI strings into the 11 locale files that were still falling back to English (ar-EG, es-ES, fa-IR, id-ID, ja-JP, pt-BR, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW), matching en-US/ru-RU key-for-key (26 new keys, verified by count in every file). Not run anywhere: npm run typecheck / build. This machine has neither Node nor npm, so nothing here has compiled — reviewed by hand plus brace/paren balance checks and cross-referencing the generated Zod/TS types. Treat this as needing a real typecheck before shipping. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
19082fdfe9 |
feat(amneziawg): add frontend support and fix a Go->Zod generator gap
Wires the amneziawg protocol through the panel UI the same way every
other protocol is registered: a Zod settings schema (nested
{server, clients}, matching the Go JSON exactly), the protocol enum,
the inbound-form's per-protocol fields component and its
tab-visibility allowlist, the default-settings factory, the client
schema dispatcher, and the sniffing-capability exclusion (no Xray
inbound exists for amneziawg, same as mtproto).
Client key/allowedIPs fields are reused rather than duplicated: since
AmneziaWG clients are wire-identical to WireGuard clients (same
model.Client fields), ClientFormModal renders one shared field block
for both, switching only the visible label by which protocol is
active. The private-key input also gets a live public-key sync via a
new useEffect, because unlike WireGuard's Xray-native inbound (which
re-derives its public key at runtime and never stores one),
AmneziaWG's server.publicKey is a real persisted field the Go backend
reads directly — free-typing a new private key without this would
silently save a mismatched keypair.
Adds a downloadable per-client .conf (amneziawgConfig.ts, mirroring
wireguardConfig.ts) with the obfuscation lines, and an
InboundOption.AwgServer field on the Go side so the config builder
gets the full server block in one round trip.
Along the way, running tools/openapigen surfaced a real bug: it
doesn't flatten anonymously-embedded Go structs the way encoding/json
does, so ServerSettings embedding Obfuscation20 produced a Zod schema
with a nested `obfuscation20` key that never matches the real wire
JSON. Fixed by un-embedding (flat fields + an accessor method) and
registering internal/amneziawg in the generator's own package list,
which had been silently emitting a dangling schema reference.
English and Russian translations are complete; the other 10 locale
files still fall back to English for the new keys.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
cd674c8d4f |
feat(sub): expose live online status and add ?format=info endpoint
Custom subscription templates only received the lastOnline timestamp, so template authors had to fake an online indicator by comparing it against the current time, and the page was a one-shot server render with no way to refresh usage without reloading the whole HTML. The template context (and window.__SUB_PAGE_DATA__) now carries isOnline, computed from the panel's own online-client tracking (local xray plus remote nodes) at render time. The subscription URL also answers ?format=info with the page view-model as JSON — minus the links, with emails deduplicated — so templates can poll live status cheaply. The shared view-model construction moved into buildSubPageData/subPageContext so the HTML page, the SPA payload and the info JSON cannot drift apart. Also documents the previously injected but undocumented announce template variable. |
||
|
|
b319dd0c3a |
fix(panel): align telegram icon with its label in home card actions
The .tg-icon override (display: inline-block; vertical-align: -2px) defeated the default .anticon flex centering that every other card action icon relies on, so the icon rendered ~2px below the @XrayUI text. Dropping the override lets AntD center it like its neighbors. |
||
|
|
c77608bc47 |
fix(nodes): make node API tokens write-only (#5613)
* fix(nodes): make node API tokens write-only * fix(nodes): keep token optional on edit for write-only API tokens NodeView no longer returns apiToken, so the edit form must consume hasApiToken and not require re-entering the token. Relaxes the form validation on edit, adds a keep-current placeholder, and adds the i18n key to all 13 locales. |
||
|
|
892c06c8bc |
Bug-label issue sweep: 16 fixes (#6083)
* fix(xray): block private-range egress in default freedom finalRules (#6037)
With domainStrategy AsIs the router never resolves domains, so a domain
with a private A record (e.g. 127-0-0-1.nip.io) sails past the
geoip:private routing block and freedom's allow-all finalRules let it
reach loopback services such as the xray gRPC API and metrics listener.
Prepend a block rule for geoip:private to the default template and add
the FreedomFinalRulesPrivateEgressBlock seeder so existing installs
still carrying the stock allow-only (or legacy private-only-allow)
finalRules are upgraded in place; customized rules are left untouched.
* fix(sub): version-gate unencrypted-outbound drops in outbound subscriptions (#6033)
Commit
|
||
|
|
9e117bbdd3 |
fix(clients): keep VLESS xtls-rprx-vision flow when inbound options reload (#5971)
The client form cleared the `flow` field whenever `showFlow` was false, but `showFlow` is derived from the inbound options list, which is transiently empty while the options query (re)loads (`inboundOptionsQuery.data ?? []`). During that window `showFlow` is a false negative, so the effect silently dropped a valid `xtls-rprx-vision` flow the user had picked for a Reality/TLS inbound and never restored it — the client was then saved with an empty flow and could not connect with XTLS Vision. Guard the clear so it only runs once the inbound options are actually available. Adds a regression test that reproduces the drop across an options reload. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
123fac222b |
feat(sub): add raw subscription download actions (#6017)
* feat(sub): add raw subscription downloads * fix(sub): address review feedback * feat(sub): add download buttons to client subscriptions * fix(sub): fetch subscription before download --------- Co-authored-by: w3struk <w3struk@gmail.com> |
||
|
|
11f602fe04 |
fix(sub): preserve external link names in Clash/JSON (#6049)
expandEntry cleared Name for external subscriptions and single links with empty remark, so Clash/JSON fell back to the client email. Keep each link's original name (#fragment / vmess ps); use the row remark when set. Pass remark from the client form for single external links. Fixes #6032 |
||
|
|
22ff07b24b |
fix(warp): preserve outbound customization when rotating IP (#6052)
changeIp() rebuilt the warp outbound from a fixed default template via collectConfig and replaced the whole in-memory object through onResetOutbound, so only secretKey/address/reserved and the first peer's publicKey/endpoint survived — a user's custom mtu, domainStrategy, noKernelTun, the first peer's keepAlive / allowedIPs / preSharedKey, and any extra peers were silently dropped from the editor. Because the page keeps rendering that in-memory copy and a later page Save persists it to /panel/api/xray/update, the loss could become permanent, even though the backend ChangeWarpIP already patches only the rotated fields and leaves everything else intact in the stored template. Mirror the server-side UpdateWarpXraySetting: merge only the rotated fields (secretKey, address, reserved, first peer publicKey/endpoint) into the existing outbound, preserving the rest. register() (first-time creation, nothing to preserve) still uses collectConfig's full build. Extracted as a pure module-level mergeWarpRotation so it is unit-testable without rendering the modal. Fixes #6019 |
||
|
|
a9d5d9afdb |
fix(balancer): pin loopback routing rules ahead of general rules (#6054)
* fix(balancer): pin loopback routing rules ahead of general rules
ensureBalancerLoopback appended a balancer's fallback loopback rule
({ inboundTag: ['_bl_<target>'], balancerTag: <target> }) to the END of
routing.rules via Array.push, and only updated balancerTag in place when
the rule already existed. Xray evaluates routing rules top-to-bottom,
first match wins, and a general domain rule (no inboundTag restriction)
matches every inbound including the loopback one. So once such a general
rule targeting the parent balancer ended up earlier in the array — added
before the fallback was configured, or recreated later by an edit or by
ensureMissingBalancerLoopbacks — the fallback re-entered routing through
the _bl_<target> loopback outbound, the general rule matched again, and
traffic routed straight back into the parent balancer: an unbounded loop
and the CPU spike in the report. detectBalancerCycles only catches
mutual fallback cycles, so the plain acyclic Balancer1 -> Balancer2
chain passed silently.
Insert the loopback rule before the first general (no-inboundTag) rule
instead of pushing to the end, and reposition any existing loopback rule
that already landed after a general rule (preserving its other fields,
mirroring the EnsureStatsRouting hoist used for the same class of bug).
Rules with an inboundTag restriction (api, real inbounds) are left in
place — they can never match loopback traffic, so only the unrestricted
rules are dangerous. ensureMissingBalancerLoopbacks runs on every Save
All and calls ensureBalancerLoopback per fallback, so this also repairs
configs loaded from the DB with the wrong order.
Fixes #5960
* ci: re-trigger checks after flaky FuzzDecodeCertPin timeout
---------
Co-authored-by: x06579 <x06579@ai-dashboard>
|
||
|
|
5e1cb7693b |
Repo-wide self-correcting audit: 54 verified bug fixes (#5970)
* fix(email): resolve a name-addr smtpFrom into bare envelope address and display name The save-time validator accepts any RFC 5322 address form, so a value like '3x-ui Panel <panel(at)example.com>' passes validation, but Send and TestConnection fed that raw string to MAIL FROM, which strict servers reject with 501, and buildMessage mangled it into a quoted local part. Parse the configured sender at the point of use: the envelope gets the bare address and, when no explicit sender name is set, the display name embedded in the setting is used for the From header. * fix(email): report a missing sender address from the SMTP connection test TestConnection skipped the empty-from guard that Send enforces, so with no sender and no username configured the test issued the null reverse-path and could report success against a lenient relay while every real notification send kept failing with the missing-sender error. Guard the test path the same way and surface a dedicated translated message. * fix(sub): fall back to the raw subscription when an auto-detected format has no content With format auto-detection enabled, a client whose User-Agent matched the Clash or JSON regex was routed straight to that format handler. For a subscription whose entries convert to neither format (an MTProto-only subscription, for example) the handler returns an empty document and the request ended as 404, breaking a URL that served the raw list before the toggle. The auto-detect branches now serve the detected format only when it produces content and otherwise continue to the raw response; the explicit format endpoints keep answering 404 for empty documents. * fix(node): match prefixed central tags when filtering a selected-mode node snapshot FilterNodeSnapshot compared a node snapshot's inbound tags against the raw selected-tag list with an exact match, while its two siblings (SnapshotHasUnadoptedInbounds and the reconcile tagToCentral map) expand each selected tag to both its bare node-side form and its n<id>- prefixed central form. A panel-created node inbound is recorded in the selected list under the central prefixed tag but reported by the node under the bare tag, so the exact match dropped it from every snapshot and the orphan sweep then deleted its central row one tick after creation. Expand the allowed set with the same prefix flip the siblings use. * fix(client): refuse a bulk quota reduction that would fall to or below zero BulkAdjust clamped a client's new traffic limit with max(total+addBytes, 0). Because 0 is the unlimited sentinel, reducing a client's quota by more than it had left silently granted that client unlimited traffic. The sibling expiry branch already refuses an over-reduction; mirror it for quota so the adjustment is skipped with a clear reason instead of crossing the sentinel. * fix(client): persist a bulk adjustment's applied field even when the sibling field is skipped In a mixed BulkAdjust (both a days delta and a bytes delta), a per-field planning skip such as "unlimited expiry" or "unlimited traffic" was recorded in the same map that gated the client_traffics write. The applied field was already written to the inbound JSON and the clients table, but the enforcement row was left untouched, so the depletion job cut the client on the old limit while the panel showed the new one. Gate the traffic-row write on an actual inbound-processing failure rather than on any planning-phase skip note. * fix(inbound): always create in AddInbound instead of overwriting a row whose id was posted The add controller binds the inbound model's id form field and never clears it, and AddInbound persisted with GORM Save, which updates in place when the primary key is non-zero. A client that reused an existing id (for instance by duplicating an inbound fetched from /get and changing the port) silently overwrote that stored row instead of creating a new inbound. Zero the id at the top of AddInbound, matching how it already zeroes the client-stat ids. * fix(inbound): accept WireGuard clients when creating an inbound AddInbound's per-client validation switch had cases for every protocol except WireGuard, so a WireGuard client fell through to the default branch that requires a non-empty id. WireGuard clients are keyed by their public key and carry no id, so importing a WireGuard inbound or re-adding one to a reconciling node was rejected with "empty client ID". Add a wireguard case that validates the client key, mirroring addInboundClient. * fix(client): stop holding the inbound-lock registry mutex while waiting on one inbound lockInbound acquired the global registry mutex and then blocked on the per-inbound mutex without releasing the registry first. A slow client operation holding one inbound's mutex (for example a bulk delete pushing to an unreachable node) made the next waiter park on that inbound while still holding the registry mutex, which in turn blocked lockInbound for every other inbound — freezing client mutations panel-wide. Release the registry mutex before taking the per-inbound lock. * fix(client): honor keepTraffic when deleting a client that is attached to inbounds Delete, DeleteByEmail and BulkDelete all pass keepTraffic to their final cleanup transaction, but each called the per-inbound delete helper with a hardcoded false. That helper purges the client's traffic, IP and stat rows before the gated cleanup runs, so keepTraffic=true still destroyed all traffic history for any client actually attached to an inbound (the pinned test only covered a record with no inbound mappings). Thread the caller's keepTraffic through to the per-inbound helper at all three call sites. * fix(inbound): defer a local MTProto inbound edit's sidecar push until after commit UpdateInbound applied a local MTProto inbound change by calling the runtime UpdateInbound (which stops/starts the mtg sidecar or talks to it) from inside runSerializedTx. That runs process and network I/O on the single traffic-writer goroutine while a DB transaction is open, so a slow sidecar stalls traffic accounting and every concurrent client mutation, and a later step failing the transaction leaves the sidecar ahead of the rolled-back row. Move the push into the post-commit hook, matching the xray branch. Adds a SetLocalRuntimeOverride test seam mirroring the existing node override so the deferral is regression tested. * fix(client): delete external-link rows when bulk-deleting clients The single-client Delete path removes a client's client_external_links rows, but BulkDelete (and the DelDepleted reaper that routes through it) deleted the record, mappings and traffic while leaving the external-link rows keyed by the now-dead client id, so they accumulated as orphans. Delete them in the same cleanup transaction, keyed by client id like the single path. * fix(inbound): request an xray restart when toggling a routed MTProto inbound AddInbound, DelInbound and UpdateInbound all flag needRestart when an inbound routes MTProto through xray, so the egress SOCKS bridge is regenerated. Only SetInboundEnable's local path omitted it, so toggling a routed MTProto inbound off then on left the bridge out of the running config while the sidecar dialed its loopback port, blackholing that inbound until an unrelated restart. Flag the restart on the local enable path too. * fix(client): apply enable-by-email to every inbound a client is attached to ToggleClientEnableByEmail (Telegram bot) and SetClientEnableByEmail (LDAP sync) resolved a single inbound via the legacy client_traffics pointer and flipped enable only there. A client attached to several inbounds kept connecting through the siblings' running Xray after being disabled, and the next edit could re-enable it everywhere from a stale sibling. Route both through the applyClientFieldByEmail fan-out (the #5039 fix path) so the whole multi-inbound identity is toggled at once, dropping the circular Set/Toggle dependency. * fix(traffic): commit a traffic tick even when a best-effort maintenance helper fails addTrafficLocked stages the inbound and client deltas, then runs three helpers (auto-renew, disable depleted clients, disable depleted inbounds) that are meant to log and continue. All three reused the function-scope err that the deferred commit/rollback inspects, so the last helper's error decided the whole tick: a failure in disableInvalidInbounds rolled back the already-staged traffic while AddTraffic reported success, and because xray had already advanced its counter baseline that traffic was lost for good. Give each best-effort helper its own error variable so only a genuine staging failure rolls the tick back. * fix(traffic): re-enable clients and serialize the write in Reset All Client Traffic ClientService.ResetAllTraffics zeroed up/down but, unlike every sibling reset path, never restored enable=true, so clients that had been auto-disabled for exceeding their quota stayed cut with zero usage after a reset. It also wrote client_traffics directly on the shared DB handle instead of through the serial traffic writer, reintroducing the cross-transaction lock-order deadlock the writer exists to prevent. Restore enable and run the reset inside submitTrafficWrite within one transaction. * fix(traffic): keep node reset propagation out of the serial traffic writer ResetAllTraffics and ResetInboundTraffic performed their remote-node reset HTTP calls inside submitTrafficWrite. Each call can block up to the remote timeout, and Reset All Traffics loops every node serially, so the single traffic-writer goroutine was held for seconds — long enough that the concurrent 5s traffic poll timed out submitting its own write and dropped the deltas it had already drained from xray. Do the DB reset inside the writer, then propagate to the nodes after it returns, matching how the mtproto quota reset is already sequenced. * fix(sub): stop the subscription from 500ing on valid-but-unusual stream settings The raw share-link generators used unchecked type assertions and unguarded array indexing: an empty Reality shortIds/serverNames array (random.Num(0) panics), a tcp-http header with no request block or an empty request.path, a grpc block missing its keys, empty stream settings, and a non-string Host header all panicked mid-generation. Because getSubs loops every client's link with no recover, one such client 500s the entire subscription for everyone. The sibling JSON, Clash and frontend generators already guard these; make the raw generators match with comma-ok assertions and length checks. * fix(sub): tolerate a hysteria inbound without hysteriaSettings in the JSON subscription genHy asserted stream["hysteriaSettings"].(map[string]any) without the comma-ok form, so a hysteria inbound whose StreamSettings omit the hysteriaSettings key (a valid, representable shape the raw generator renders fine) panicked and 500ed the entire JSON subscription. Use comma-ok; the downstream reads already guard each key, so a nil map degrades gracefully. * fix(sub): emit the pinned peer cert sha256 in Clash subscriptions The Clash stream builder computed tlsSettings["pin-sha256"] from the inbound's pinnedPeerCertSha256, but applySecurity's tls case never copied it onto the proxy, so it was written with no reader and silently dropped. Clash subscribers lost certificate pinning while JSON subscribers kept it. Surface pin-sha256 on the proxy in the tls case, matching the JSON emitter. * fix(link): parse the snake_case and extra-blob xhttp fields when importing a share link The panel's share-link emitters (Go and TS) carry advanced xhttp knobs as a snake_case x_padding_bytes plus an extra=<json> payload, but the Go parser's xhttp branch read only top-level camelCase params, so importing an xhttp link via the outbound-subscription feature dropped xPaddingBytes, scMaxEachPostBytes and the rest, silently reverting them to the stream defaults and producing a non-working outbound. Mirror the TS parser: read the snake_case alias, merge the extra JSON blob, then let explicit camelCase params win. * fix(frontend): decode URL-safe base64 when parsing an imported share link Base64.decode called window.atob directly, which rejects the base64url alphabet (- and _) and unpadded input. But the panel's own share-link emitter uses Base64.encode(x, true) (URL-safe, unpadded), and real SIP002 links do too, so importing a Shadowsocks link whose method:password encodes with a - or _ threw, fell back to the raw undecoded string, and produced a wrong method and garbage password (the vmess parser shared the same limitation). Normalize base64url and re-pad before atob so decode round-trips every emitted link. * fix(link): honor the vmess ws path and hysteria2 vcn params on import Two Go/TS parser parity gaps in the outbound share-link import path: parseVmess only applied a ws link's path when the inner JSON also carried a host key, so a generator that omits host dropped the path back to the default; and parseHysteria2 hardcoded verifyPeerCertByName to empty, ignoring the vcn param the panel emits, so a hysteria2 outbound with a decoy SNI and a distinct cert name failed TLS verification after import. The TS parser handles both; make the Go parser match. * fix(ui): stop the sniffing form island from clobbering unrendered fields antd's Form.useWatch only reports registered fields, so while the sniffing toggle was off the island emitted { enabled: false } upward and replaced the full Sniffing object in form state. Saving a VLESS reverse outbound then crashed in sniffingToWire on the missing ipsExcluded array; the loopback outbound and the inbound sniffing tab shared the same hole. Watch the store with preserve: true so unrendered fields keep their values, and seed a missing value from the schema defaults instead of an empty cast. * fix(sub): drop empty remark segments instead of leaving a stray separator expandSegment dropped a "|" segment only when its tokens rendered the unlimited mark, so a segment whose only token resolved to the empty string (a client with no comment, an unlimited client's expiry date) was kept as bare decoration, leaving a trailing "|" or a dangling emoji on every share link's remark. Drop a token-bearing segment whenever none of its tokens produce a real value, while still keeping pure-literal segments. * fix(xray): keep source- and domains-scoped routing rules when an inbound is deleted removeInboundTagFromRules drops a routing rule whose inboundTag list becomes empty only if the rule has no other matcher, but routingMatcherKeys omitted xray-core's canonical source and domains keys. A rule scoped by source or domains (common in hand-authored or imported configs) therefore lost its whole body — including a security-relevant block — when its single listed inbound was deleted, instead of just having the tag trimmed. Recognize source and domains as live matchers. * fix(xray): guard RemoveUser against an uninitialized handler client Every XrayAPI handler method returns an error when HandlerServiceClient is nil, except RemoveUser, which dereferenced it directly. A depletion sweep runs Init with the port ignored and, during a restart window where the fresh process's api port is still 0, Init fails and leaves the client nil — so RemoveUser panicked (recovered by the traffic writer, but re-thrown every poll) instead of returning an error. Add the same nil guard the siblings have. * fix(xray): do not revive a manually stopped Xray on a background restart RestartXray cleared isManuallyStopped unconditionally at its top, so the @30s pending-config cron (and warp/ldap/outbound reconcile jobs) that call RestartXray(false) resurrected an Xray the admin had deliberately stopped — unlike the crash-detector, which honors the manual-stop flag. Skip a non-forced restart while the stop flag is set; only an explicit forced restart clears it. * fix(xray): retry a failed pending-restart instead of dropping the config change The 30s cron consumed the need-restart flag with IsNeedRestartAndSetFalse before calling RestartXray and only logged a failure. If RestartXray failed early (a transient GetXrayConfig DB error) the old process kept running the old config, the crash detector saw a running process and never retried, and the flag stayed cleared — so an admin's saved change silently never reached the core. Move the consume/restart/retry into ApplyPendingRestart, which re-arms the flag on failure so the next tick retries. * fix(xray): synchronize the process version and apiPort fields Start writes p.version and p.apiPort (via refreshVersion/refreshAPIPort) after flipping the process to running, while GetXrayVersion and GetAPIPort read them lock-free from the status and traffic poll goroutines. The struct mutex deliberately excluded these fields, so a restart racing a poll was a real data race — a torn read of the version string header can crash. Extend the mutex to cover version and apiPort, doing the blocking version probe before taking the lock. * fix(settings): detect a wildcard listen collision between the web and sub ports The web/sub same-port check compared the two listen addresses as raw strings, so binding both on all interfaces with different spellings (webListen 0.0.0.0 vs an empty subListen) slipped past validation and only failed at startup with an opaque bind error. Treat any wildcard listen ('', 0.0.0.0, ::) as overlapping so the clash is reported up front, while still allowing two distinct specific addresses to share a port. * fix(db): mark the IP-limit cleanup seeder done on a fresh install ResetIpLimitNoFail2ban is a one-time migration that, on a host without fail2ban, zeroes every existing client's limitIp because the limit can't be enforced. It was missing from the fresh-install fast-path seeder list, so on a brand-new DB it did not run on the first boot but fired on the second — wiping any IP limits the admin had set in between. Add it to the fast-path so a truly fresh install marks it done up front (there is nothing to clean), leaving later admin-set limits intact. * fix(security): dial outbound subscriptions through the SSRF guard The outbound-subscription fetch validated the URL host once (resolving DNS and rejecting private targets) but then fetched with a plain HTTP client that re-resolves the host at dial time, so a subscription domain the attacker controls could pass validation as a public IP and rebind to 127.0.0.1 / a cloud metadata endpoint / an internal host for the actual dial — a blind SSRF into the panel's network. Route the direct fetch (and its redirects) through netsafe.SSRFGuardedDialContext, which resolves, checks and dials the same IP atomically, carrying the subscription's AllowPrivate flag on the request context; a configured egress proxy still dials its loopback bridge unguarded. * fix(security): bound the login-limiter attempts map The login rate limiter keys its records on the caller-supplied username and only evicted a record when that exact key was revisited or the login succeeded. An unauthenticated attacker replaying one CSRF token while rotating a fresh username per request seeded a record that was never revisited, growing the map without bound until the panel OOMs. Cap the map: before inserting a new record, reclaim records whose block has lapsed and whose failures aged out, and if the map is still at the ceiling under a broad flood, drop one so memory can never grow past the cap. * fix(tgbot): require admin for privileged callbacks, not just the first switch answerCallback wraps only its first callback switch in an isAdmin guard; the second switch (server usage, inbound/online enumeration, database backup export, ban logs, mass traffic reset, client creation) ran for every caller. Telegram delivers a callback with the tapping user's id, so a non-admin who can see an admin's inline keyboard — as when the bot runs in a group — could tap Backup and receive the full database and config, or reset all traffic. Default-deny before the second switch: a non-admin may only run the per-user client_* callbacks that resolve their own data from their Telegram id. * fix(eventbus): dispatch each subscriber in its own goroutine The fan-out loop called every subscriber's handler sequentially on the single dispatch goroutine. The email and Telegram notifiers block on network I/O for tens of seconds (or minutes when the remote is slow), so one slow subscriber stalled the whole loop: the 256-slot channel then filled and Publish silently dropped later events — including high-value xray.crash and node.down notifications unrelated to the slow handler. Hand each delivered event to every handler in its own goroutine so a blocking subscriber can no longer stall delivery to the others. safeCall already recovers panics, so a detached handler cannot take down the bus. * fix(integration): cap WARP API response body size doWarpRequest read the response with an unbounded io.ReadAll, unlike the sibling NordVPN client which already caps every read at maxResponseSize. A hostile panel egress proxy or a MITM on the Cloudflare WARP endpoint could stream an arbitrarily large body and force the panel into an unbounded allocation. Wrap the body in an io.LimitReader(maxResponseSize) to match the NordVPN client. * fix(email): bound every SMTP step with a connection deadline The "starttls"/"none" transport delivered through net/smtp.SendMail, which dials with an untimed net.Dial and never sets a socket deadline. When an SMTP server accepted the TCP connection but then stalled (or was a blackhole), the caller was released by Send's 30s select, but the sender goroutine and its socket stayed blocked until the OS TCP timeout — minutes per notification, leaking a goroutine and a connection each time. sendWithTLS dialed with a timeout but likewise armed no deadline on the protocol phase, and TestConnection (called synchronously from the settings handler, with no select guard) could hang the request indefinitely. Replace SendMail with sendPlain, which dials with smtpConnectTimeout and arms conn.SetDeadline(smtpDeadline) before the greeting read, preserving SendMail's opportunistic STARTTLS upgrade. Arm the same deadline in sendWithTLS and TestConnection so every SMTP step is bounded. * fix(server): guard access-log parser against malformed lines GetXrayLogs split each Xray access-log line on whitespace and then read fixed offsets — parts[1] for the timestamp and parts[i+1] after the "from", "accepted" and "email:" markers — without checking the line had that many fields. A truncated or malformed line (the logged destination is attacker-influenced) indexed past the slice and panicked; the panel handler returned a 500 via Gin's recovery. Extract the per-line field parsing into parseAccessLogFields and length guard every positional lookup so a short line yields a partial entry instead of panicking. * fix(server): guard xray key-generator output parsing GetNewX25519Cert, GetNewmldsa65 and GetNewmlkem768 parsed xray's stdout by reading lines[0], lines[1] and each line's second colon-separated field without any length check — unlike GetNewEchCert, which already guards its line count. If the xray binary printed fewer than two lines or reformatted its labels (a version change, or a silent failure that emitted nothing), the fixed slice index panicked and the handler 500'd. Extract the shared parsing into parseXrayKeyPairOutput, which length guards the line count and each label split and returns an error instead of panicking, then route all three generators through it. * fix(tgbot): stop auto-deleted messages from resetting wizard state SendMsgToTgbotDeleteAfter spawns a goroutine that, after the display delay, deleted the transient message and then unconditionally cleared the chat's conversation state. Every caller that ends a wizard step already clears the state synchronously, so that call was redundant — and harmful: if within the delay the user advanced to the next step (a callback sets a fresh awaiting_* state), the late goroutine wiped it, and the user's next message fell through unrecognized, silently dropping their input. Move the delayed deletion into deleteMessageAfterDelay, which only removes the message and no longer touches the conversation state. Guard deleteMessageTgBot against a nil bot so the deletion path is unit-testable. * fix(frontend): refetch a fresh CSRF token on 403 instead of reusing the stale meta tag On a 403 to an unsafe method the client cleared its cached CSRF token and called ensureCsrfToken to retry. But ensureCsrfToken prefers the <meta name="csrf-token"> tag baked into the page, which the production panel always injects, so the "refresh" re-read the same stale token and the /csrf-token refetch was never reached — the retry re-sent the token that had just been rejected and the save failed with an error toast. The token lives in the session and rotates when the session is regenerated (for example re-login in another tab), leaving the tab's baked-in meta token stale. Fetch the current token straight from /csrf-token in the 403 branch so the retry uses the authoritative server value. The existing tests only passed because they strip the meta tag; the new test keeps a stale tag present. * fix(frontend): surface backend error text from failed requests HttpUtil.get/post read the thrown HttpError body as response.data.message, but the backend error envelope (entity.Msg) serializes its text as msg. On any non-2xx JSON response the real reason was therefore dropped and the operator saw only the generic "Request failed with status N" toast. Read response.data.msg first (keeping message and the native error text as fallbacks). The sibling test had pinned the wrong body shape ({ message }); correct it to the real backend shape ({ success:false, msg }) so it exercises the actual envelope. * fix(frontend): share one WebSocket connection across bridge and hooks websocketBridge.ts and useWebSocket.ts each declared their own module-scoped sharedClient plus an identical getSharedClient, so the "shared" client was not shared between them: whenever a page using useWebSocket (Clients/Inbounds) mounted alongside the always-mounted bridge, the panel opened two sockets to /ws. The server then pushed every traffic/stats/nodes/inbounds snapshot to both, doubling WebSocket bandwidth and running two independent reconnect loops, and the hook's socket was never disconnected on unmount. Hoist a single getSharedWebSocketClient into api/websocket.ts and route both the bridge and the hook through it, so exactly one connection is opened. * fix(frontend): guard the outbounds WebSocket handler against non-array payloads onOutbounds wrote the raw WebSocket payload straight into the outboundsTraffic cache, unlike the sibling onNodes/onInbounds handlers which first check Array.isArray. A malformed non-array push (for example an object) would land in the cache with staleTime Infinity; consumers that call .find()/.map() on the outbounds list would then throw and crash the Outbounds tab. Add the same Array.isArray guard so a bad push is ignored. * fix(frontend): key the node table by the computed row key, not id The desktop node table used rowKey="id", but transitive sub-nodes (the read-only rows surfaced from downstream nodes) all carry id 0, so a topology with two or more transitive rows gave React duplicate keys. antd's rowKey prop overrides the row object's own computed `key` (`t-${guid}` for transitive rows, the numeric id otherwise), so the unique key the code already builds was ignored — causing row-state/DOM mis-association on any re-render (heartbeat refetch, address-eye toggle). The mobile card path already keyed by record.key. Key the table by "key" so transitive rows get their distinct t-${guid} identity; direct nodes keep key === id, so row selection (filtered to numeric keys) is unchanged. * fix(frontend): map routing row actions through the rule's real index The routing table hides balancer-loopback rules (`_bl_*`) but keeps each visible row's original index in `key`, then handed antd's positional row index straight to edit/delete/toggle/move/drag — all of which mutate the full, unfiltered routing.rules array. Once a hidden loopback rule precedes a visible one (e.g. a balancer whose fallback is another balancer, plus any rule added afterwards), the positional index no longer matches the array index, so deleting or editing a rule silently hit the wrong one — including destroying the loopback rule that keeps the balancer alive. Add originalRuleIndex to translate a positional row index back through the row's `key`, and route every mutating handler (openEdit, confirmDelete, toggleRule, moveUp/moveDown, drag) through it. When no loopback rows are hidden the mapping is the identity, so ordinary configs are unaffected. * fix(frontend): map outbound row actions through the outbound's real index The outbounds table hides balancer-loopback outbounds (`_bl_*`) but keeps each visible row's original index in `key`, then passed antd's positional row index to edit/delete/move and to the per-row probe (onTest) and its result lookup — all of which address the full, unfiltered outbounds array. Once a hidden loopback outbound precedes a visible one, the positional index diverges from the array index, so deleting or editing an outbound hit the wrong one (its deletion-impact plan and removal targeting the wrong entry), and the test button probed / showed results against the wrong outbound. Add originalOutboundIndex and route the mutating handlers through it; key the probe trigger and test-result columns by record.key. With no loopback rows hidden the mapping is the identity, so ordinary configs are unaffected. * fix(frontend): tolerate a malformed happyEyeballs value in the Xray Basics tab BasicsTab derived directHappyEyeballs by calling HappyEyeballsSchema.parse during render, guarding only against null/non-object. A wrong-typed field (e.g. happyEyeballs.tryDelayMs as a string) or any other shape mismatch — reachable via the Complete Template JSON editor or an imported config — threw straight out of render, white-screening the default Xray landing tab. Use safeParse and fall back to null so a bad value degrades to "no override" instead of crashing the page. * fix(frontend): preserve routing-rule fields the form does not surface The rule form rebuilt the rule from a fixed literal of only the fields it edits, and RoutingTab replaces the rule wholesale on confirm. Fields the form never exposes — localPort, localIP, process, ruleTag, webhook — are in the rule schema and can arrive via the advanced JSON editor or Import Rules; opening such a rule in the form and saving silently dropped them. Carry over every key of the original rule the form does not manage before applying the form-derived fields, so an edit only touches what it surfaces. * fix(frontend): re-sync the sniffing island when its value changes externally The sniffing config editor froze its seed value at mount and only watched its own inner AntD form, never reflecting a later change to the shared RHF `sniffing` path. Because the inbound form mounts every tab with forceRender, the friendly Sniffing tab and the Advanced JSON editor are live at once: editing sniffing in the JSON editor updated the RHF value but not the frozen island, so the next interaction with the friendly tab emitted the stale value and silently discarded the JSON edit. Add an effect that pushes an external value change into the inner form, guarded by the same lastEmitted marker the emit path uses so the island never re-seeds from its own echo and no update loop forms. * fix(frontend): don't drift a client's byte quota on a no-op save The quota field shows the total in GB rounded to two decimals; editing a client and saving converted that display value straight back to bytes. A byte total not aligned to 0.01 GB — one set via the API or an import — was therefore rewritten to the rounded value on any save that never touched the field, losing a few MB each time. Add resolveTotalBytes: keep the original byte total when the displayed GB still matches it, and only re-derive from GB when the user actually changed the field. * fix(eventbus): deliver events on a bounded per-subscriber worker The previous fix dispatched each event to every subscriber with a bare `go safeCall`. That unblocked the dispatch loop, but removed the bus's backpressure: under a login-attempt flood (which both notifier subscribers process without rate-limiting) with email/Telegram enabled, every attempt spawned handler goroutines that each block on network I/O for up to ~30s, with no bound — a goroutine and outbound-connection storm. It also let a subscriber's handler run concurrently with itself, racing the Telegram notifier's lazily-cached hostname. Give each subscriber its own bounded queue drained by a single worker goroutine. Dispatch does a non-blocking send per subscriber (dropping only that subscriber's event when its queue is full), so a slow subscriber still can't stall the others, concurrency is bounded to one in-flight handler per subscriber, per-subscriber event order is preserved, and Stop again waits for in-flight handlers to finish. * fix(frontend): map outbound mobile-card actions through the real index too The desktop outbounds table was keyed by the outbound's real index, but the mobile card list was left keying the probe trigger and every test-state lookup by the positional row index. With a hidden balancer-loopback outbound present, tapping Check on a mobile card probed the wrong outbound and the Test-All results landed on the wrong card. Key onTest and the testResult/isTesting reads by record.key, matching the desktop columns. * fix(frontend): meet WCAG AA contrast on the config-block link text The Storybook accessibility test flagged the share-link <code> block: with no explicit color it inherited a muted grey that renders as #888888 on the #f8f8f8 tertiary-fill background in CI's Chromium — a 3.33:1 contrast, below the 4.5:1 AA threshold. Set the text to the theme's primary text token so the colour is explicit and high-contrast in both light and dark themes instead of depending on an inherited value that varies by browser. * style(sub): simplify a negated conjunction to satisfy staticcheck QF1001 golangci-lint (staticcheck QF1001) flagged the `!(a && b)` guard in expandSegment. Rewrite it via De Morgan's law to the equivalent `!a || !b` form so the linter passes; behavior is unchanged. * fix: close panics and races the audit's own fixes left nearby Second-pass review of the 54-commit self-correcting audit. Each item below was confirmed by reading the surrounding source (and, where practical, the pre-fix code) before being changed; regression tests are included for every behavioral fix. Concurrency: - eventbus: Bus.Subscribe called wg.Add with no synchronization against a concurrent Bus.Stop's wg.Wait, a real "WaitGroup misuse" panic risk (e.g. a Telegram-bot settings save racing panel shutdown/restart). Stop now flips a mu-guarded `stopped` flag before waiting, and Subscribe checks it under the same lock, so Add and Wait can no longer race. Security: - login_limiter: evictForRoom's fallback eviction picked an arbitrary map key, including ones still under an active cooldown - an attacker flooding /login with fresh usernames could evict their own (or anyone's) blocked record and reset the lockout. The fallback now skips actively-blocked records, only falling back to an unconditional evict if the map is somehow entirely full of active blocks (preserves the hard memory cap). Subscription-endpoint panics (reachable by any client hitting /sub): - internal/sub/service.go: applyPathAndHostParams/Obj (ws/httpupgrade/xhttp with no path settings object) and the TLS alpn readers in three places used unchecked type assertions - exactly the bug class |
||
|
|
129f50d92a |
feat(sub): auto-detect subscription format by User-Agent (Updated) (#5826)
* feat(settings): add subscription format controls
* feat(sub): auto-detect subscription formats
* fix(xray): validate balancer regexes before save
* Revert "fix(xray): validate balancer regexes before save"
This reverts commit
|
||
|
|
f2b17397f4 |
fix(frontend): stabilize speed tags on inbound and client pages (#5930)
* fix(frontend): add shared stable speed-tag style Give live up/down rate tags a fixed width, centered layout, nowrap, and tabular numerals so digit/unit changes cannot reflow the Speed column. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix(frontend): stabilize InboundSpeedTag and ClientSpeedTag layout Apply the shared speed-tag class/style to both live rate tags and lock the behavior with a focused component test for small and large rates. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix(frontend): align speed columns with stable tag width Widen inbound/client Speed columns to match the fixed tag and apply the same stable style to idle dash cells so active/idle swaps do not jitter. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix(frontend): scope stable speed tags to table cells and fit content --------- Co-authored-by: x06579 <x06579@ai-dashboard> Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> |
||
|
|
658e6ab3d3 |
feat(frontend): show client comments on mobile cards (#5942)
* feat(frontend): show client comments on mobile cards * fix(frontend): bound mobile comment height --------- Co-authored-by: sanmaxdev <sanmaxdev@users.noreply.github.com> |
||
|
|
1cfd7b49b0 |
fix(email): build an RFC 5322 message with a proper From address and name (#5941)
The notification/test email carried only From/To/Subject/MIME headers, and
the From header was the raw SMTP username. Two problems:
- When the SMTP login is not a bare email address (common with relays and
submission services), the From header has no valid address and strict
receivers reject the message — e.g. Gmail returns "550-5.7.1 ... Messages
missing a valid address in From: header".
- There was no Date (mandatory per RFC 5322 section 3.6) and no Message-ID,
which also raises spam score.
Add smtpFrom (sender address) and smtpFromName (display name) settings and
assemble the message with net/mail: a name-addr From ("Name" <addr>), a
Date, a Message-ID, and an RFC 2047 encoded Subject, in a deterministic
header order. From falls back to the username when smtpFrom is empty, so
existing setups keep working. Wire the settings through the model, the SMTP
send and test paths, the Email settings UI, and all 13 locale files;
regenerate the Zod/OpenAPI artifacts.
Validate smtpFrom in AllSetting.CheckValid (reject anything net/mail cannot
parse), which surfaces a bad address at configuration time and prevents CRLF
header injection; strip CR/LF in buildMessage as defense in depth. Add
buildMessage and CheckValid tests.
|