mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-29 13:16:51 +08:00
dev-latest
150 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6f40a75909 |
feat(inbound): excludeFromSub hides links without disabling (#6463)
* feat(inbound): excludeFromSub hides links without disabling Add a per-inbound flag that omits subscription output while keeping the inbound enabled for Xray, auth, and traffic accounting. Fixes #6435. * fix(inbound): excludeFromSub review follow-ups gofumpt model.go, sync docs OpenAPI, keep excludeFromSub master-authored on node mirror, and exercise the legacy add-column migration path in tests. * fix(sub): keep excluded inbounds' clients in the usage header The excludeFromSub filter sat in getInboundsBySubId's SQL, so an excluded inbound's clients never reached seenEmails in the raw, Clash or JSON renderer. A client that lives only on a hidden inbound (one client per inbound sharing a subId) dropped out of the Subscription-Userinfo usage, quota and expiry and out of the info-node state, while the inbound kept serving it and counting its traffic. The query returns every enabled inbound again; each renderer skips an excluded inbound's links but still counts its clients, the same rule the Clash renderer already applies to external links it cannot express. --------- Co-authored-by: mrchatam <mrchatam@users.noreply.github.com> Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com> |
||
|
|
12d51d7195 |
perf(tests): copy a migrated template DB instead of migrating per test
Most tests opened a throwaway panel DB with database.InitDB, which runs the full AutoMigrate + seed on an empty file every time: ~230ms, and ~850ms under -race because GORM's reflection-heavy migration is what the detector slows most. internal/web/service does this in ~550 of its 830 tests, so the CI race job spent ~10 of its ~14.6 minutes re-migrating empty databases. internal/database/dbtest.InitDB migrates once per test process, then hands each test its own copy of that file (~130ms under -race) and registers the CloseDB cleanup. The copy then goes through InitDB like a panel restart, so every test still starts from the state a fresh install has. Tests that reopen an existing file, migrate a hand-built legacy DB or target Postgres keep calling database.InitDB. Locally under -race: internal/web/service 626s (last CI run) -> 114s, internal/sub 246s -> 35s. |
||
|
|
71e38367c1 |
feat(sub): add Incy app-management parameters (#6650)
* feat(sub): add Incy app-management parameters The panel already pushes a set of Happ headers, but INCY documents its own lowercase header names and its own value domains, so a Happ-shaped payload gets ignored by the client (per-app mode is bypass|proxy, not on|bypass, and per-app-proxy-enable has no Happ counterpart at all). Add a sibling Incy path that emits exactly the documented headers. Covered, per https://docs.incy.cc/en/app-management/: - profile-description, sort-order, support-email, announce-url, premium-url - banner text/button/URL and the two hex colours - hide-url, hide-check, no-limit-enabled - per-app split tunnelling (enable/mode/list) - TCP fragmentation (enable/length/interval/packets) - UDP noise packets (enable/type/packet/delay) - DoH pre-resolution (enable/domain/IP) Each string setting is tri-state: an empty value omits the header, so an untouched panel never overrides the subscriber's own choice in the app. Values are validated against the documented domains and dropped when they do not match, and non-ASCII text is base64-wrapped the way the docs require for Cyrillic. INCY identifies itself as INCY/<version>/<platform>, which gates the headers behind the same auto-detect switch the Happ path uses. Headers the panel already emits for every client (Profile-Title, Support-Url, Profile-Web-Page-Url, Announce, Profile-Update-Interval, Subscription-Userinfo) and Incy's routing line are left as they are. The Premium API (theme, defaultPingProtocol, fallbackHosts, ...) is a separate encrypted endpoint and stays out of scope here. * fix(sub): keep Incy per-app list entries separate on the wire The Incy settings textarea takes one package per line, as Incy documents for per-app-proxy-list, but the header path ran the value through sanitizeHeaderValue, which deletes CR/LF. "com.google.chrome\norg.telegram.messenger" reached the client as the single bogus package "com.google.chromeorg.telegram.messenger", so per-app split tunnelling silently matched no app. Join comma- or line-separated entries as CSV instead. Also drop three tests that could not fail: TestIncyExcludesHappOnlyHeaders (ApplyIncyHeaders has no path that emits Happ headers, and the non-Happ UA gate is already pinned by TestApplyHappHeaders_Gating) and two UI tests that only asserted updateSetting received the key the JSX passes it. --------- Co-authored-by: DIMFLIX <dimflix@users.noreply.github.com> Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com> |
||
|
|
bd9ccde1f4 |
feat(sub): make external subscription fetch User-Agent configurable (#6613)
* feat(sub): make external subscription fetch User-Agent configurable Some providers reject fetches that do not send a known client User-Agent. Expose externalSubUserAgent as a panel setting (default v2rayNG/1.8.5) and use it when fetching client external subscription URLs. Fixes #6383 * ci: retrigger frontend after npm registry maintenance The frontend job failed solely on `npm audit` while registry.npmjs.org returned 503 (Service Under Maintenance). Lint, typecheck, vitest, vite build, and storybook all passed. Local `npm audit --omit=dev --audit-level=high` now reports 0 vulnerabilities. * fix(sub): fall back to the default UA when the DB is not initialised externalSubUserAgent read the setting through SettingService.getSetting, which calls Model() on database.GetDB() and panics on a nil *gorm.DB. The fetch path's other DB read, service.ExternalSubscriptionHwid, already treats a nil DB as unreachable and sends no header; the new UA lookup did not, so any fetch before InitDB panicked instead of sending the historical v2rayNG/1.8.5. Production initialises the DB before the sub server starts, but the internal/sub fetch tests run without one: under make test-go's -shuffle=on, whenever one of them ran before the first InitDB test the panic aborted the whole package. Reproduced deterministically with go test -run '^TestDoFetchSubscriptionLinks_RejectsOversizedBody$'. --------- Co-authored-by: mrchatam <mrchatam@users.noreply.github.com> Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com> |
||
|
|
6ab718f813 |
fix(sub): make Happ require auth on its local SOCKS/HTTP proxy (#6628)
Happ ships its local SOCKS5 (127.0.0.1:10808) and HTTP inbounds with authorization disabled by default. Any app on the same device can then connect to that proxy, bypassing Android's per-app VPN routing, and learn the VPN server address - the leak publicly described in March-April 2026 for Happ, v2rayNG and other VLESS clients. Happ fixed its Xray API exposure, but the unauthenticated local proxy remained. Happ exposes a standard subscription header for this (no Provider ID required): socks-auth-mode / http-auth-mode = auto|manual|from-json| disable. A new subscription setting, subHappLocalProxyAuth (default "auto"), sends both headers to Happ clients. Like every other Happ header it is emitted only when Happ auto-detect is enabled and the User-Agent is Happ, so panels that never opted into the Happ integration see no change. An empty value sends nothing and keeps the client's own setting. Verified on Happ Android 4.4.1 (Xray 26.7.28): a subscription carrying socks-auth-mode manual + a test user/password switched the client's Inbounds screen to Manual with those credentials on "refresh subscription", and "auto" switched it to Auto with generated credentials. Co-authored-by: Kirill Rudenko <rudenko@npp-energy.ru> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
d42e2133c7 |
fix(sub): keep serverDescription literal in external link fragments (#6580)
* fix(panel): accept 2FA codes from adjacent TOTP windows CheckUser compared only gotp.Now(), so a code submitted at the end of its 30s window (or with slight client/server clock drift) failed with 'invalid 2fa code', while the immediate retry in the next window succeeded. Accept current +/-1 window, the standard TOTP skew tolerance. Fixes MHSanaei/3x-ui#6535 * fix(panel): share TOTP skew tolerance with VerifyTwoFactorCode Move the +/-1 window helper to internal/util/totp so both 2FA acceptance points use it: login (CheckUser) and disable/rebind plus username/password changes (VerifyTwoFactorCode). Also shrink comments to the 2-line house rule and anchor the unit test mid-window to avoid a step-boundary flake. Addresses review on #6546 (MEDIUM + 2 LOWs). * fix(sub): keep serverDescription literal in external link fragments Client external links escaped the whole remark, turning ?serverDescription=<base64> into %3F...%2F... so Happ lost its subtitle. Split on ?serverDescription= like appendQueryAndFragment (#6488): escape only the display name, keep a clean base64 tail literal, fall back to full escaping otherwise. Fixes MHSanaei/3x-ui#6575 * refactor(sub): share one serverDescription fragment split across link paths #6488 fixed the split in appendQueryAndFragment and #6575 was the same bug on the external-link path, which had its own copy. Both now call escapeLinkFragment with their own escaper, so a later change to the tail check cannot reach one path and miss the other. --------- Co-authored-by: sdhfsl <sdhfsl@users.noreply.github.com> Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com> |
||
|
|
a2ca023336 |
fix(sub): send panel guid as X-HWID on outbound subscription fetch (#6579)
* fix(panel): accept 2FA codes from adjacent TOTP windows CheckUser compared only gotp.Now(), so a code submitted at the end of its 30s window (or with slight client/server clock drift) failed with 'invalid 2fa code', while the immediate retry in the next window succeeded. Accept current +/-1 window, the standard TOTP skew tolerance. Fixes MHSanaei/3x-ui#6535 * fix(panel): share TOTP skew tolerance with VerifyTwoFactorCode Move the +/-1 window helper to internal/util/totp so both 2FA acceptance points use it: login (CheckUser) and disable/rebind plus username/password changes (VerifyTwoFactorCode). Also shrink comments to the 2-line house rule and anchor the unit test mid-window to avoid a step-boundary flake. Addresses review on #6546 (MEDIUM + 2 LOWs). * fix(sub): send panel guid as X-HWID on outbound subscription fetch Outbound subscriptions hit the same HWID-limited donor 404 as client external links (#6559/#6567). Identify this panel with GetPanelGuid plus X-Device-OS, honoring the externalSubSendHwid opt-out. Fixes MHSanaei/3x-ui#6574 * fix(sub): send the external-subscription X-HWID from outbound fetches too The outbound fetch used panelGuid while client external links send the externalSubHwid id from #6567, so an HWID-limited provider counted one panel as two devices. It also re-added the externalSubSendHwid opt-out that #6567 dropped. Move the id into service.ExternalSubscriptionHwid, keeping the externalSubHwid row so existing installs keep their slot, and send it from both paths. The outbound test now fails on the panelGuid version. --------- Co-authored-by: sdhfsl <sdhfsl@users.noreply.github.com> Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com> |
||
|
|
169cd86e00 |
fix(sub): never use X-Real-IP as the subscription host (#6608)
ResolveRequest and the panel's resolveHost fell back to X-Real-IP for the host when a trusted proxy sent no X-Forwarded-Host. X-Real-IP names the visitor, so behind nginx with only that header set, subscription and exported links advertised the subscriber's own public IP as the server. The host now comes from a trusted X-Forwarded-Host, else the dialed request Host. X-Real-IP stays a client-IP source only. Fixes #6589. |
||
|
|
c54c28d92d |
fix(sub): drop legacy freedom.domainStrategy from JSON sub template (#6609)
The JSON-subscription template still set settings.domainStrategy on its freedom outbound, the placement #6515 moved off everywhere else, so xray-core migrated it to sockopt with a deprecation warning on every load. AsIs is the core default when the key is absent, so dropping it changes nothing else. Fixes #6482. |
||
|
|
5d41e65a3c |
fix(sub): preserve external VLESS encryption in Clash subscriptions (#6576)
* fix(sub): preserve external VLESS encryption in Clash subscriptions Copy non-empty, non-none encryption from parsed external VLESS settings, matching local proxy export. This prevents merged Clash/Mihomo subscriptions from losing the encryption parameters of externally added nodes. Cover encryption normalization and omission, plus merged YAML from pasted links and HTTPS subscriptions containing plain or Base64 share-link lists. Validation: regression cases fail before the fix and pass after it; the full subscription package and go build ./... pass. Four unrelated packages still fail on Windows, with the same failures reproduced using the original code. Refs: MHSanaei/3x-ui#6572 * test(nodes): wait for chart effects in history panel assertions The DOM can commit its accessible labels before Sparkline updates the refs used by uPlot range callbacks. Wait for the existing assertions together so the test does not read the empty-data range. Reproduced the original CI failure locally on attempt 5. The fixed test passed 12 consecutive runs; lint, format and TypeScript checks pass. The full frontend suite passed 1607 of 1608 tests, including Storybook. The unrelated input-number guard fails on Windows because execFileSync cannot launch the extensionless oxlint shim (ENOENT); invoking oxlint.cmd reports all three expected diagnostics. |
||
|
|
0dec3d65ba |
fix(sub): preserve per-inbound tunnel identity in subscriptions (#6653)
matchingClients primed the per-request link cache with the shared clients rows, whose wg_* columns hold whichever WireGuard/AmneziaWG inbound synced last. A client on several such inbounds (one per node) therefore got the same tunnel address and keys in every subscription profile. Membership, subId, enable, quota and expiry still come from the normalized tables. For WireGuard and AmneziaWG the tunnel identity (keys, AllowedIPs, keepalive) is now overlaid from this inbound's own settings, the source clientsForLinkExport already uses for direct links. A member with no settings entry, or malformed settings, yields no link for that inbound rather than another inbound's credentials. Fixes #6641. |
||
|
|
07ee638a50 |
fix(sub): emit Hysteria certificate pin for Mihomo (#6651)
buildHysteriaProxy dropped pinnedPeerCertSha256 from Clash/Mihomo YAML although the raw share link already carries it as pinSHA256, so Mihomo rejected a self-signed Hysteria2 certificate whenever allowInsecure was off. Emit the first valid SHA-256 pin as Mihomo's fingerprint field in its colon-separated form, honouring an external endpoint's override. client-fingerprint stays the uTLS setting. Mihomo accepts a single fingerprint, so of several pins the first valid one wins. Refs #4683. |
||
|
|
d59b77bcdb |
fix(sub): send stable X-HWID on external subscription fetch (#6567)
* fix(panel): accept 2FA codes from adjacent TOTP windows
CheckUser compared only gotp.Now(), so a code submitted at the end of
its 30s window (or with slight client/server clock drift) failed with
'invalid 2fa code', while the immediate retry in the next window
succeeded. Accept current +/-1 window, the standard TOTP skew
tolerance.
Fixes MHSanaei/3x-ui#6535
* fix(panel): share TOTP skew tolerance with VerifyTwoFactorCode
Move the +/-1 window helper to internal/util/totp so both 2FA
acceptance points use it: login (CheckUser) and disable/rebind plus
username/password changes (VerifyTwoFactorCode). Also shrink comments
to the 2-line house rule and anchor the unit test mid-window to avoid
a step-boundary flake.
Addresses review on #6546 (MEDIUM + 2 LOWs).
* fix(sub): send stable X-HWID on external subscription fetch
A Master panel fetching a donor subscription sent no X-HWID, so an
HWID-limited donor rejected it with 404. Identify this panel with a
stable per-installation id (persisted in settings), occupying exactly
one donor device slot.
Fixes MHSanaei/3x-ui#6559
* fix(sub): address review on external X-HWID
- Serialize first-time id creation with a mutex so concurrent
first fetches cannot mint two UUIDs.
- Fix goimports grouping for the new third-party import.
- Add externalSubSendHwid opt-out (default send); document it.
- Cover header send/omit with httptest in TestFetchSendsStableHwid.
* fix(sub): drop the SQL-only X-HWID opt-out
The externalSubSendHwid opt-out added in
|
||
|
|
17e89db979 |
feat(hosts): add a cipher suites override and accept custom suites
The inbound TLS form offered cipherSuites as a closed single-choice list, but xray reads the value as a colon-separated list and accepts any name Go knows, so several suites or one missing from the list could not be set. Both the inbound and the new host field now use a tag picker that keeps the stored value as the colon-joined string xray expects; old single values open unchanged. A host's cipher suites replace the inbound's in the JSON subscription stream, and a blank field inherits them. Share links and Clash carry no cipher suite parameter, so their output is unchanged. |
||
|
|
e26cf1d3ed |
feat(sub): redesign the subscription page around usage, tabs and app imports
The info page was a long key/value table followed by every link and two app dropdowns, and it rendered left-to-right even for Persian and Arabic. It now leads with a usage ring, the remaining quota and a stats grid, and splits the rest into Subscription / Apps / Configs tabs. - Status tells expired, data-used-up and disabled apart instead of one "Inactive", replacing the hard-coded English expiry chip. - The Apps tab keeps every Android and iOS app with its existing deep link, preselects the visitor's platform and adds Windows: Hiddify and Clash Verge Rev import directly, v2rayN copies the link. - fa-IR and ar-EG render right-to-left; URLs, IDs and sizes stay LTR. - The footer shows the support link and the client refresh interval, so subPageContext now carries subUpdates (also in ?format=info). - Status, days-left and app deep-link logic lives in subPageModel.ts, with unit tests pinning the deep links the page already shipped. |
||
|
|
1d85ef138e |
fix(sub): prevent default profile page URL disclosure (#6538)
* fix(sub): prevent default profile page URL disclosure Add explicit none, builtin, and custom profile page modes. Preserve existing custom URLs and warn before exposing the built-in page. Cover mode selection, legacy settings, and subscription response headers. * fix(subscription): add profile page link options and upgrade notes |
||
|
|
c90996eda3 |
feat(sub): add opt-in month-end expiry presentation (#6517)
Offer monthly calendar subscriptions an explicit last-valid-second display without moving their real billing boundary or spending renewal allowances. Keep the option off by default and limit conversion to a shared fixed day-1 midnight cutoff at an actual month transition in the panel timezone. Use the authoritative client calendar mode when aggregating node traffic, and share the header formatter across raw, JSON, and Clash exports. Expose the setting in the existing settings API/UI, regenerate its schemas, and document that clients may report expiry one second early or format the date differently in another timezone. Add HTTP, settings, and DST coverage. Stored deadlines, access enforcement, info/remark expiry values, and renewal accounting remain unchanged. Refs: #6516 Co-authored-by: JacktheRanger <219502738+JacktheRanger@users.noreply.github.com> |
||
|
|
ff1a6c3caf |
fix(sub): drop external Clash shadowsocks nodes the panel cannot express (#6508)
* fix(sub): gate external Clash shadowsocks links like the inbound path clashProxyFromExternal returned as soon as it had built the ss proxy, so an ss:// link skipped applyTransport/applySecurity: a node whose tcp/http obfuscation Clash cannot express was emitted anyway (mihomo then opens a plain shadowsocks stream at a server that requires the header, and the node silently never connects), and security=tls was silently stripped. The inbound path runs both helpers for every protocol, so the two Clash importers disagreed about the same node. * fix(sub): count a dropped external link in the quota header The client email that feeds AggregateTrafficByEmails was recorded only when a proxy came out of the link, so a node Clash cannot represent also vanished from the Subscription-Userinfo header of every other node in the same subscription — the header reported another client's numbers as the whole subscription's. The inactive-link branch already counted an email without a proxy; make that unconditional so the header describes the subscribers, not the representable subset of their nodes. * docs(sub): describe clashProxyFromExternal by what it does, not by protocol The protocol list in the doc comment went stale the moment the shadowsocks branch stopped returning early, and it restated what the switch already says. |
||
|
|
8fc4fc0bf8 |
fix(link): rebuild shadowsocks tcp/http obfuscation on import (#6505)
* fix(link): rebuild shadowsocks tcp/http obfuscation on import genShadowsocksLink encodes tcp/http obfuscation only as the SIP002 plugin=obfs-local;obfs=http;obfs-host=... parameter, deleting type, headerType, path and host in the process, because SIP002 clients ignore those and read `plugin` alone. ParseLink read none of them, so importing a link the panel had just exported produced a plain tcp outbound with header.type none: the obfuscation the inbound requires was gone, and the client could not connect to the very inbound the link came from. The plugin is now mapped back onto the header it stands for. Credentials and every other parameter are untouched, and other plugin values are left as they were because Xray has no equivalent for them. * fix(link): map the SIP002 plugin in both importers The panel parses share links twice: link.ParseLink in Go, which the external subscriptions use, and parseShadowsocksLink in outbound-link-parser.ts, which the Add Outbound button calls. Mapping the plugin in Go alone left the UI path still saving header.type none for a link the panel had exported itself, so one panel answered the same link with two different outbounds. The unencoded plugin=obfs-local;obfs=http;... form maps as well now: stdlib drops any query pair whose value holds a literal semicolon, and that is the shape clients which skip percent-encoding emit, so the raw query is read as a fallback when the parsed parameter is missing. |
||
|
|
f3dba07e13 |
fix(link): read the vmess certificate checks on import (#6507)
* fix(link): read the vmess certificate checks on import applyVmessTLSParams writes ech, vcn and pcs into the vmess share object, but parseVmess only read sni, fp and alpn back. Importing a link the panel had just exported therefore dropped all three: no pinned certificate, no verify-by-name, no ECH. On a server whose certificate is only trusted through a pin, the imported outbound falls back to public-CA verification against the system roots and cannot connect to the inbound the link came from. The url-param protocols already read the same three in applySecurity, and the core takes pinnedPeerCertSha256 as one joined string there, so the vmess path now fills them the same way. * fix(frontend): read the vmess certificate checks on import The panel parses share links twice: link.ParseLink in Go and parseVmessLink in outbound-link-parser.ts, which is what the Add Outbound button calls. Reading ech, vcn and pcs in Go alone would have made the two sides disagree on one link, leaving the UI path — the one an operator uses by hand — still dropping the pin the panel had just exported. |
||
|
|
cba8f0672f |
feat(sub): refine Happ routing presets, serverDescription escaping, and auto-detect placement (#6488)
* feat(sub): refine Happ routing presets, serverDescription escaping, and auto-detect placement * fix(sub): address PR review findings on routing parity, agent regex, and i18n |
||
|
|
2730e4d071 |
feat(sub): let the panel set the JSON subscription DNS servers (#6485)
* feat(sub): let the panel set the JSON subscription DNS servers A baked routing profile (#6402) carries only the DNS its preset defines, so an operator who wants their own resolvers has to override the whole profile or patch the subscription behind a proxy. Add the subJsonDns setting: either a full xray dns block or a bare array of servers. It wins over the profile's DNS while leaving the profile's routing rules intact, and reaches per-inbound, balancer and info-node documents alike. The value is validated with xray's own schema (internal/xray/dnsconf): a block the client could not load is rejected when the settings are saved and ignored with a warning at request time, instead of being baked into every document. Both the sub server and the settings API share that validator, so a stored value can never be silently dropped. xray's Build() is deliberately not used for validation: it resolves geosite tokens from the geodata files and would reject valid configs whenever those are absent from the panel's working directory. * style(dnsconf): drop the ineffectual initial map assignment golangci's ineffassign flagged the zero-value map whose value both paths overwrite: the object branch now assigns the decoded map directly. * docs(sub): scope the DNS setting to the documents it rewrites The Routing header mirrored to Happ/INCY keeps the routing profile's own resolvers, so the setting description and the header-source comment now say so instead of claiming the profile's DNS is replaced everywhere. Also trims two comments in the new dnsconf package to the repo's two-line cap. |
||
|
|
67addab343 |
fix(inbounds): serve fresh client UUIDs for list and allLinks (#6458)
* fix(inbounds): serve fresh client UUIDs for list and allLinks (#6436) Resolve clients from the clients table in inboundLinks and backfillClientStats so /inbounds/list ClientStats and allLinks match the running Xray identity when embedded settings JSON is stale. * fix(sub): keep WG/AWG settings identity in link exports (#6436) clientsForLinkExport uses the clients table for UUID-bearing protocols and the inbound settings JSON for WireGuard/AmneziaWG so allLinks and per-client QR links stay consistent without collapsing per-inbound tunnel keys. * fix(sub): fall back to settings clients for link export (#6458) Prefer ListClientsForInbound for UUID protocols, but when the clients table is empty or unavailable fall back to GetClients so settings-only inbounds (and share-link unit tests) still produce links. Keep WG/AWG on settings identity. --------- Co-authored-by: mrchatam <mrchatam@users.noreply.github.com> Co-authored-by: mrchatam <287639636+mrchatam@users.noreply.github.com> |
||
|
|
6d96accd63 |
Feature/tuic v5 (#6337)
* Feat(tuic): Implement native TUIC v5 protocol support via Rust sidecar daemon - Add internal/tuic package for official tuic-server sidecar lifecycle management, configuration generation, and graceful process control - Bridge decrypted TUIC QUIC traffic into loopback Xray SOCKS5 inbounds (63200+id) for traffic accounting, statistics, and routing rules - Implement periodic reconciliation job (cadence @every 10s) and immediate runtime synchronization on inbound/client mutations - Add TUIC inbound & multi-user client settings (UUID + Password authentication) in Web UI with SNI auto-fill and panel certificate loader - Integrate tuic:// subscription links and Clash.Meta (Mihomo) proxy generation for TUIC - Update install.sh to automatically download and install official tuic-server release for x86_64, aarch64, and armv7 - Add full localization for TUIC protocol across all 13 supported languages * Feat(install): Support custom repository and branch in install and update scripts * Ci(release): Enable publish-dev for feature branch and workflow dispatch * Feat(sub): Add TUIC to subscription resolution and client QR config generator - Add 'tuic' to getInboundsBySubId SQL allowlist to resolve TUIC inbounds in subscriptions and sub links - Enhance buildTuicProxy in Clash subscription generator with robust host and credentials resolution - Add tuicConfig.ts to generate standalone Clash/Mihomo YAML configuration - Add dedicated TUIC Config tab in ClientQrModal with QR code and .yaml download button - Add localization keys for TUIC config across all 13 supported languages * Fix(tuic): Exclude TUIC from native Xray inbounds and strip udp_relay_mode from server config - Exclude model.TUIC from native Xray inbounds in GetXrayConfig to prevent Xray startup failure - Remove udp_relay_mode from tuic-server JSON configuration builder - Update install.sh to install tuic-server binary to both xui_folder/bin and /usr/local/bin * Fix(install): Fallback to dev-latest when releases/latest is not present on fork * Feat(tuic): Add real-time online status and LastOnline tracking for TUIC clients - Track client activity by mapping client UUID in tuic-server logs to email - Integrate TUIC active clients into XrayTrafficJob to refresh local online clients - Bump LastOnline timestamp in database and broadcast live online status over WebSocket * Feat(tuic): Implement real-time traffic statistics and live speed reporting for TUIC - Collect precise I/O traffic deltas for tuic-server child processes via /proc/<pid>/io - Aggregate and attribute TUIC traffic deltas per client in tuic Manager - Integrate TUIC traffic deltas into XrayTrafficJob to update database and broadcast live speed * Feat(tuic): Finalize TUIC v5 integration with 1:1 traffic counting and orphan process cleanup - Use exact 1:1 byte delta accounting from /proc/<pid>/io - Add killStrayTuicProcesses to terminate orphan sidecars on panel startup - Fully integrate TUIC with subscriptions, live speed meter, and all 13 locales * Feat(frontend): Polish TUIC UI, support bulk operations, and update translations - Align TUIC inbound certificate form with standard 3X-UI layout (Set Default Cert, Clear) - Remove extra subtitle hint text from TUIC inbound form fields - Support TUIC in client bulk attach/detach and bulk add modals - Add TUIC badge color to client info modal, clients table, and host list - Update password tooltip across all 13 locales to include TUIC - Remove obsolete dead translation keys across all 13 locales * Chore(ci): Finalize TUIC v5 bundling across release workflow, Docker, and scripts * Feat(openapi): Update OpenAPI generator and schemas for TUIC types * Fix(backend): Address core review findings for TUIC types, port checks, and xray bridge * Refactor(traffic): Isolate proc reading with build tags and decouple TUIC metering into TuicJob * Feat(client): Add TuicServer to InboundOption, fix config export and clean share links * Fix(frontend): Register TUIC in multi-user helpers, tracked protocols, and tag derivation * Chore(openapi): Re-generate OpenAPI specification and sync Zod schemas * Chore(scripts): Add Alpine musl binaries, 386 and Windows packaging, and anchor pkill * Fix(review): Remove stale import, correct binary names, switch to musl, and drop unreachable relay gate * Feat(frontend): Show share link in Inbound Info and display UDP tag for TUIC * Docs: Add TUIC v5 configuration guide and link specifications * Docs(tuic): Correct Clash Meta configuration parameter to reduce-rtt * Fix(tuic): Generate client credentials on copy, enforce ID/password validation, and add i386 to DockerInit * Fix(tuic): drop unused relay, fix traffic accounting, and honor host endpoints - Drop unused loopback SOCKS relay and eliminate port collision with AmneziaWG - Correct inbound traffic calculation without double-counting - Drop heuristic client traffic division while retaining online tracking - Support externalProxy host fan-out and conditional parameters in share links - Scope orphan process termination to managed config directory * Fix(tuic): enforce client quotas, decouple Xray restart, and sync openapi schemas - Regenerate OpenAPI, Zod schemas, and TypeScript types without route_through_xray - Populate clientTraffics in TuicJob to enforce client quotas and first-use expiry - Split process I/O delta into up and down in Process.CollectTraffic - Remove SetNeedRestart from updateTuicInbound to prevent Xray session drops - Use InstanceFromInbound for default ALPN and UDP relay mode in tuic:// share links - Support allow_insecure on externalProxy host endpoints without parameter collision * Fix(tuic): attribute client traffic only on single-user inbounds and sync link defaults - Attribute I/O deltas to the client only when the inbound has exactly one configured client, avoiding false billing and disablings on multi-user inbounds - Aggregate client traffic by email in TuicJob so clients on multiple inbounds don't lose deltas - Match frontend genTuicLink defaults for alpn and udp_relay_mode with backend subscription links * Fix(tuic): gate client traffic by total sidecar clients and require client email * Fix(tuic): enforce inbound-only traffic limits and disable client totalGB * fix(tuic): restore delayed start, remove client totalGB rejection, and document linux-only limits * fix(tuic): anchor pkill, fix io baseline/split, escape yaml, and deduplicate start errors * fix(tuic): prevent traffic double-counting, ensure info log level for delayed start, and broaden pkill matching * fix(tuic): address review round 11 findings - internal/sub/json_service: skip tuic protocol in json subscription to prevent direct routing leak - internal/sub/clash_service: honor externalProxy/host row allowInsecure, sni, and alpn in buildTuicProxy - internal/web/runtime: decouple tuic inbound add/delete from xray restart - internal/tuic/config: restore user log-level options (warn, error) without forced info clamp - frontend/src/lib/xray/inbound-link: fix duplicate remark suffix and apply externalProxy TLS overrides - frontend/src/schemas/protocols/stream/external-proxy: propagate allowInsecure through host mapping - tests: add coverage for json sub skip, clash proxy overrides, and link generation * fix(tuic): meter inbound traffic through a UDP relay and bracket IPv6 binds Review repairs on the TUIC v5 sidecar integration: - Inbound traffic was read from the sidecar's /proc/<pid>/io rchar, but the kernel only counts read()/write() there and tuic-server moves its sockets with recvfrom/recvmmsg/sendmmsg/sendto, so an inbound's up/down stayed at 0 forever and inbound total limits never tripped (measured: 12 MiB relayed, rchar delta 0). The panel now owns the inbound's public UDP port with a small relay and runs tuic-server behind it on a loopback port, counting up/down exactly on every OS. tuic-server therefore logs 127.0.0.1 as every client's address; per-client attribution stays unsupported since QUIC is opaque. - Instance.BindTo formatted an IPv6 listen address as ":::8443", which tuic-server rejects with "invalid socket address syntax", so an inbound listening on "::" or any IPv6 literal never started. It now uses net.JoinHostPort; IPv4 output is unchanged. - The log level is passed to the sidecar as chosen. Online status, last-online and delayed start are read from its Info lines, so the Log Level field now says that Warn and Error switch them off for the inbound, and the docs say the same. - Drop two frontend tests that only exercised a getter and a set lookup, and strip the trailing blank line that made gofumpt fail on two of the new Go test files. * fix(tuic): harden tag updates, runtime routing, and relay stability --------- Co-authored-by: poise52 <equipoise52@gmail.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
0a2cd789ba | fix(sub): enable ML-KEM for Mihomo REALITY subscriptions (#6451) | ||
|
|
89ee1242bd |
feat(sub): add read-only HWID device-slot status endpoint (#6380)
* feat(sub): add read-only HWID device-slot status endpoint Closes #6357 A client with an HWID limit had no way to tell a subscriber how many device slots were left: /{subPath}/{subId} only exposes the gate as a boolean through X-Hwid-* headers on a 404, and ?format=info carries no limitHwid or registered count. Every "why can't I connect on my new phone" case therefore had to be answered by the operator by hand. GET /{subPath}/{subId}/hwid-status now returns the aggregate counters: {"active":true,"limit":2,"registered":1,"remaining":1,"full":false} - SELECT-only. It never registers an hwid, never touches last_seen and never calls the enforcement path, so asking about a slot cannot spend one. - Counters only: no hwid value or hash, no email, no device metadata, no IP, no User-Agent, and none of the X-Hwid-* gate headers. - The subscription id is already the bearer secret for /{subPath}/{subId}, so no admin token and no new auth mechanism. - Unknown and disabled subscriptions both answer a bare 404, with identical status, headers and body, so the route cannot be used to probe which subscription ids exist. - No HWID limit configured returns {"active":false,"limit":0,...}. - No schema change and no migration. Scoped to enabled clients exactly like effectiveHwidLimitForSubID, so the reported limit is always the limit the gate enforces on a shared sub_id, and remaining clamps at zero when the effective limit drops below the number of registered devices. A separate route leaves /{subPath}/{subId}, ?format=info and the JSON/Clash routes byte-for-byte unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(sub): document hwid-status as the bare object it returns The OpenAPI operation for GET /{subPath}/{subId}/hwid-status inherited the {success,msg,obj} panel envelope from build-openapi.mjs's default 200 response, while the handler writes the HwidSlotStatus struct bare. A client generated from the spec would read `obj` and never find the counters, and the description prose contradicted the schema with a hand-written example. HwidSlotStatus now sits in openapigen's StructAllow with example: tags, the entry references the generated schema through a `responses` block, and build-openapi.mjs attaches the generated example to any `responses` entry that $refs a generated schema, so no example is hand-written. The HEAD variant the controller registers is documented like its siblings, and the summary follows the "path prefix is configured by subPath" wording now that fresh panels randomise the prefix. Regenerated frontend/public/openapi.json, docs/public/openapi.json and the subscription-server MDX. openapi-runtime-contracts.test.ts pins the bare schema, the generated example and the HEAD operation. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
8f162994ef |
feat(clients): let admins set PersistentKeepalive on tunnel clients (#6377)
* feat(clients): let admins set PersistentKeepalive on tunnel clients
model.Client already carries KeepAlive, and every AmneziaWG/WireGuard client
config emitter already writes PersistentKeepalive when it is above zero -- but
nothing in the UI could set it, so it stayed 0 and the line was never emitted.
Without it a peer that goes quiet has nothing to trigger a handshake: WireGuard
only initiates when it has data to send. An idle client stays disconnected
after any interruption -- a NAT mapping timing out, a device sleeping, the
panel restarting -- until the user generates traffic themselves.
New clients default to 25, the conventional value, which also keeps the NAT
mapping open. Existing clients keep whatever they have, and 0 remains valid and
means "do not send keepalives".
* fix(clients): let an explicit 0 actually disable PersistentKeepalive
Addresses review feedback on the previous commit.
UpdateInboundClient carries a stored keepalive forward whenever the incoming
one is zero, so the settings JSON and the running peer survive a metadata-only
edit that omits the field. That was a 0 -> 0 no-op while no UI could set a
nonzero value. Now that the client form can, the carry-forward became reachable
in the other direction: a client created at the form's default of 25 could
never be returned to 0, and the hint text shipped to all 13 locales -- "0
disables it" -- described something the backend silently refused. The save even
reported success, because a settings blob that came back byte-identical skips
the transaction entirely.
The zero value cannot carry that distinction, so model.Client.KeepAlive becomes
*int: nil means the field was never sent, &0 means "send no keepalives". The
pointer survives the internal marshal in ClientService.Update, which is where an
explicit 0 was being erased by omitempty before UpdateInboundClient ever saw it.
ClientRecord.KeepAlive stays a plain int -- it is the stored column, where
"unset" has no meaning -- and the conversions bridge the two.
Two tests, both red before this change in the direction they cover: an explicit
0 must reach wg_keep_alive, and an update that omits the field must still leave
a stored 25 alone.
Also adds the output transform every other numeric field in the client form
already has, so a cleared box sends 0 rather than null.
* fix(clients): repair the keepalive pointer conversion after the main merge
Merging main brought buildAmneziaWGProxy (#6326) in beside the
Client.KeepAlive int -> *int change without reconciling the new call site,
so internal/sub stopped compiling and took every package importing it with
it. The two sides touched different lines, so git merged them without a
conflict -- the green `make verify` on
|
||
|
|
64b6e43e2b |
feat(sub): add legacy Clash subscription endpoint (#6338)
* feat(sub): add legacy Clash subscription endpoint * fix(deps): update js-yaml to patched release Raise the Swagger UI js-yaml override to 4.3.2 and refresh the lockfile to resolve GHSA-2883-xcg3-v3hh without changing Swagger UI. * fix(sub): preserve client detection and normalize legacy cipher Keep the original Clash/Mihomo auto-detection default so existing subscription URLs continue returning YAML. Normalize the panel-supported chacha20-poly1305 alias when generating legacy Clash profiles, and cover both regressions through HTTP endpoint tests. * refactor(sub): drop an unreachable guard and make the alias test assert Review of the legacy Clash subscription endpoint left three LOW findings, all introduced by the change: - The comment above the routing merge ran to three lines, over CLAUDE.md's two-line cap. - validateClashRouteGraph on the legacy path could never fail: the legacy branch skips the routing merge, so it validated the literal config built a few lines above against itself. Dead code that reads as a guard. - TestClashAliasesSkipConfiguredPathConflicts asserted nothing — it could only fail on an escaping gin panic, so a regression that registered the alias handler on the configured path went unnoticed. It now drives each collision through the router and asserts which format answers each path. --------- Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
2dd903ea8e |
feat(sub): bake Happ/INCY routing profiles into the JSON subscription (#6402)
* feat(sub): parse generic Happ/INCY routing payloads for the JSON subscription Accepts the routing-rules format emitted for Happ and INCY (inline JSON, happ:// or incy:// deeplink, or a remote https:// URL resolved through the existing remote routing cache). The JSON subscription will bake these rules into its documents so header-ignoring clients still get routing. * feat(sub): bake Happ/INCY routing profiles into JSON subscription documents When subJsonRoutingRules is set, every emitted document (per-inbound and balancer alike) carries the profile's dns and routing rules baked in, so header-ignoring clients like Happ and INCY still get routing; the legacy simple-rules merge only applies when no profile is set. The balancer document builder keeps rewriting proxy-tag rules to the balancer. * feat(sub): add the subJsonRoutingRules setting Plumbed from the settings store through the subscription server into SubJsonService, so admins can set a routing profile once and every JSON subscription document carries it. * chore(api): regenerate OpenAPI artifacts for subJsonRoutingRules * feat(web): routing profile editor for the JSON subscription A textarea inside the JSON card accepts the routing profile (inline JSON, happ/incy deeplink, or https URL) with a remote-source badge; the badge helper moves to a shared module. Keys added to all 13 locales. * fix(sub): warm and lazily resolve the baked JSON routing source The routing profile was resolved once at service construction: a remote URL that was cold at that moment baked default routing forever, and the cron job never warmed it. The job now warms the subJsonRoutingRules URL, and the profile resolves per request with an in-memory memo (a failed resolve is not cached), so a warmed cache takes effect without a restart. * feat(sub): fall back to the JSON routing profile for the Routing header Happ and INCY download the geo files a routing profile references through the Routing response header. When the Happ header setting was blank the header stayed unset, and clients fetched no geo files even though a JSON routing profile was configured. A blank setting now falls back to the JSON profile: happ/incy deeplinks pass through, inline JSON and remote URLs are normalized to a happ:// deeplink; an unusable or oversized value leaves the header unset. Locale captions mention the fallback. * fix(sub): pass routingRules arg at call sites added by main Main gained four NewSubJsonService call sites after this branch forked; update them to the five-arg signature so internal/sub builds again. * fix(sub): address code review findings on the baked JSON routing The memoised baked template never invalidated, so an edited remote profile kept serving the superseded dns/routing subtrees until a panel restart; bakedTemplate now re-resolves the spec per request and rebuilds only when the payload actually changed (regression-tested). subJsonRoutingRules shared the happ persistence row with subRoutingRules, so only the last-written setting survived a restart; it now resolves under its own jsonhapp kind with the same validation and size caps. The setting also joins validateSettingsURLs, so remote values are canonicalised and bad URLs are rejected on save. Also: drop the unreachable half of the remote-source guard, cut the overlong comment blocks to the two-line convention, and deduplicate remoteSourceBadge in the General tab. Merges upstream/main (call sites for the widened NewSubJsonService signature). * style(sub): gofumpt the json_routing imports * fix(sub): accept happ add/ deeplinks and bound the routing warning The baked-JSON routing parser only recognised happ://routing/onadd/, but normalizeHappRouting treats happ://routing/add/ as an equally valid routing deeplink. An operator pasting the add/ form got the Routing header set, so the panel looked configured, while every JSON subscription document silently carried the default routing instead of their profile. resolveJsonRoutingSpec logged one warning per call and bakedTemplate calls it once per emitted document, so a single fetch of an unusable profile wrote one identical warning per document. On the public subscription server that floods the 10240-entry buffer the panel's log view reads, evicting real entries. Log only when the message changes, and reset on a successful resolve so a profile that recovers and fails again is still reported. Also resolve the template once in buildBalancerConfig: two resolves could straddle a profile refresh and pair one revision's dns with the other's routing. |
||
|
|
1456658028 |
feat(sub): add Happ client integration, routing presets, and app management (#6434)
* feat(sub): add Happ client integration, routing presets, and app management Implement comprehensive Happ proxy client integration according to official developer specifications. - Fix header emission on disabled routing and hidden settings to send explicit '0' headers rather than omitting, allowing Happ clients to reset cached settings. - Add support for 'happ://routing/off' deeplink in routing validation. - Preserve '?serverDescription=' query parameters in link fragments without escaping to support Happ server subtitles across VMess, VLESS, Trojan and SS. - Add Happ application management headers: ProviderID, New-Url, Fallback-Url, Sub-Info banners, Sub-Expire notifications, No-Limit mode, hardware ID enforcement, TUN modes/types, route exclusions, APNS exclusions, and per-app proxy settings. - Add curated routing presets (Iran Bypass, China Direct, AdBlock, Global) and interactive visual rule generator in frontend settings. - Synchronize all 13 translation locales with native Persian, Russian, and Chinese translations. * fix(sub): keep Happ header overrides behind the auto-detect opt-in The Routing-Enable/Hide-Settings off values were emitted on the User-Agent alone, so every panel that upgraded would push "Routing-Enable: 0" — documented by happ.su as disabling routing globally — to every Happ client without the operator enabling anything. They now ride subHappAutoDetect like every other Happ header. Two further mismatches against the vendor spec: - serverDescription was written as a key of the VMess base64 JSON object. happ.su documents it as a "#Title?serverDescription=<base64>" link parameter or a JSON "meta" entry, so the caption never reached Happ while every other VMess consumer received an unknown key. Dropped rather than moved: emitting the documented form is unsafe here because our own parser base64-decodes the whole VMess body (internal/util/link/outbound.go). - The TUN Mode dropdown stored the literal "default", forwarded as "Tun-Mode: default", where happ.su documents system|gvisor only. It now stores the unset value so no header is sent. TUN Type "default" is a documented value and is unchanged. Each fix carries a test that fails without it. |
||
|
|
876497db6e |
feat(sub): add AmneziaWG proxy generation for Clash subscriptions (#6326)
* feat(sub): add AmneziaWG proxy generation for Clash subscriptions Add buildAmneziaWGProxy to generate mihomo-compatible wireguard proxy entries with amnezia-wg-option sub-block for AmneziaWG inbounds. Previously, AmneziaWG inbounds were silently skipped in Clash subscriptions (buildProxy returned nil), making them unusable with mihomo/Clash clients. The new function reuses the wireguard proxy base structure and adds: - v1.0 obfuscation fields (jc/jmin/jmax/s1-s4/h1-h4/i1-i5) - v1.5 fields (s3/s4/i1-i5) - v3 fields (header-protection-key, content-padding-addition, timing, random-trailers, disable-cookies) with automatic version: 3 tagging Closes #6310 * fix review nits: doc comment and test call * fix(sub): use the AmneziaWG inbound's own tunnel address in Clash proxies buildAmneziaWGProxy took the peer address from model.Client.AllowedIPs, which matchingClients resolves out of the shared clients.wg_allowed_ips column. That column holds one address per identity, so a client attached to both a WireGuard and an AmneziaWG inbound gets the other protocol's address written into its Clash proxy - an unroutable peer, since the running interface accepts only the AllowedIPs InstanceFromInbound derives from the inbound's own settings JSON. Read the address from settings.clients[] and fall back to the shared column. Also emit remote-dns-resolve alongside dns: mihomo gates its whole `dns` list on that flag (adapter/outbound/wireguard.go, NewWireGuard), so the panel's primaryDns/secondaryDns were inert in Clash while the vpn:// .conf turned them into a real DNS line. Restricted to bare IPs - mihomo aborts the entire config when dns.ParseNameServer rejects an entry, and nothing validates those fields. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(sub): emit the AmneziaWG effective MTU in Clash proxies main's EffectiveMTU landed while this branch was open, so the Clash builder was the one AmneziaWG emitter left omitting mtu when the operator set none. The running interface uses EffectiveMTU (internal/amneziawgnet/device.go), as do the vpn:// .conf and both TS builders; mihomo instead falls back to its own 1408, which sits above the tunnel once s4 passes 12 and fragments every packet the client sends. GenerateObfuscation31 draws s4 from 12..27, so that is the default. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(sub): reject a zoned DNS address before opting into remote-dns-resolve netip.ParseAddr accepts "fe80::1%eth0", but mihomo's parsePureDNSServer brackets it into "udp://[fe80::1%eth0]" and url.Parse rejects "%et" as a bad escape, so parseNameServer errors and parseProxies aborts the entire config - the whole subscription's Clash profile, not just this proxy (#4641 class). |
||
|
|
d0edbcec81 |
feat(xray): update xray-core to v26.9.9 and follow the udpHop move
Bump xtls/xray-core to 52a412d9e2f5 (v26.9.9) and the three binary pins in DockerInit.sh and release.yml in lockstep. Upstream moved UDP port hopping out of finalmask.quicParams.udpHop and into a standalone "udphop" UDP mask with a different shape (mode / interval / remotePorts / remoteIPs). The old key is gone from QuicParams, and since the config loader ignores unknown fields it is now silently dropped rather than rejected — port hopping just stops. The panel adapts where that key was live: - Both link importers rebuilt quicParams.udpHop from the standard mport param, so an imported hysteria2 link produced an outbound that no longer hops. They now emit a udphop mask in intervalremote mode, which is what the old key did. The mode is required: UDPHop.Build() rejects an empty or unknown one. - validFinalMaskUDPTypes and UdpMaskTypeSchema learn "udphop", otherwise the Go link generator strips the mask from every link and sub, and Zod strips it on the next form round trip. - mport generation (Go and frontend) reads the mask first and keeps reading the legacy key, so inbounds stored before the upgrade still advertise their range. On an inbound the old key was always inert — only hysteria's dialer consumed it — so nothing regresses server-side and no migration is needed. udphop stays out of the mask dropdown on purpose: it is client-only in core, which refuses to wrap a server socket, and that form is shared with the inbound editor. |
||
|
|
4e423fa452 |
fix(sub): drop Reality parameters when a host forces plain TLS
A Host row may set Security to tls on an inbound whose own stream is Reality. The emitted link then carried security=tls next to pbk, sid, spx and the Reality dest as sni: the endpoint no longer performs a Reality handshake, so those describe a server the client will never reach, and clients that honour them fail to connect. Only the security key was rewritten at emit time, and the existing strip covered alpn/sni/fp/pcs for forceTls=none alone. Clear the Reality-only parameters before the endpoint's own TLS overrides are applied, so a host that supplies its own sni or fingerprint still wins. Closes #6424 |
||
|
|
246d9207a5 |
fix(sub): emit a bare host in Clash proxies
A Clash "server" is a bare host, not a URI authority, but the custom share address strategy stores an IPv6 literal with brackets so the address normalizer can hand it to the raw link generators. The Clash renderer copied that value into every proxy verbatim, so mihomo received server: "[2001:db8::1]" and failed to parse the node. Raw links were unaffected because joinHostPort strips the brackets and re-adds exactly one. Strip them once where the renderer takes the resolved dest, which is the single place all three proxy builders read the address from. Closes #6373 |
||
|
|
9f76a66dcf |
feat(sub): add dummy info node and status configs for subscriptions (#6412)
* feat(settings): add subInfoNodeEnable and status template settings * feat(sub): add dummy info node and status configs for raw links * feat(sub): support dummy info node in clash and json subscriptions * feat(ui): add subscription info node switch and status templates to settings * style: apply gofumpt formatting * fix(sub): address review feedback on subscription info node - Restore GetSubs contract to avoid unintended remark expansions on non-subscription-body calls. - Exclude dummy info node from Clash PROXY select group when active server nodes exist. - Track hasEnabledClient and set traffic.Enable in JSON and Clash paths so status tokens evaluate correctly. - Deterministically sort client emails across subscriptions before selecting primaryEmail. - Consolidate duplicated info-node evaluation logic into resolveInfoNodeRemark helper. - Remove redundant pure-getter test from setting_sub_info_node_test.go. Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: Claude Code <noreply@anthropic.com> |
||
|
|
3cd3836d77 |
fix(amneziawg): account for S4 junk in the default tunnel MTU (#6376)
* fix(amneziawg): account for S4 junk in the default tunnel MTU
amneziawg prepends S4 random bytes to every transport packet
(device.NewOutboundElement) and, unlike content padding and random trailers,
never clamps them against the tunnel MTU. A full-size packet therefore lands on
the wire at MTU + 60 + S4 bytes: 20 IPv4 + 8 UDP + S4 + 16 transport header +
16 poly1305 tag.
With the 1420 default that overflows a 1500-byte link once S4 exceeds 20, and
GenerateObfuscation31 draws S4 from 12..27 inclusive -- so roughly 44% of newly
created inbounds fragment every full-size packet they send.
Measured on a live pair of interfaces, predicted against observed:
MTU 1380 S4 12 -> 1452 on the wire (fits)
MTU 1420 S4 12 -> 1492 (fits)
MTU 1420 S4 20 -> 1500 (exactly at the limit)
MTU 1420 S4 21 -> 1501 (fragments)
MTU 1420 S4 27 -> 1507 (fragments)
EffectiveMTU now subtracts S4 from the default; an explicit MTU is untouched.
Client configs carry the same number. They previously omitted the MTU line
whenever the server had no explicit value, which left the client on its own
1420 default and fragmented the client-to-server direction even after the
server side was fixed -- silently, and only in one direction. All three
emitters (the Go subscription text and the two TypeScript ones) now agree,
which is what the existing parity test exists to protect.
* fix(amneziawg): rebuild the device when S4 changes the derived MTU
Addresses review feedback on the previous commit.
Deriving the default MTU from S4 made a construction-time-only property depend
on a hot-reloadable input, but addressFingerprint -- ensureLocked's only rebuild
trigger -- still hashed the raw inst.MTU. S4 is a UAPI field, so an S4-only edit
took the in-place IpcSet branch and the gVisor netstack kept the MTU derived
from the old S4 while all three client emitters already advertised the new one.
Every panel-created inbound leaves mtu unset, so that was the normal case, not
an edge one: with S4 raised far enough the fragmentation this fix exists to
remove came straight back, and stayed until a panel restart or an unrelated
address edit.
Folding EffectiveMTU into the fingerprint fixes it. An explicit MTU still takes
the in-place branch on an S4 edit, since it does not move the interface MTU.
Also trims four comment blocks to the 2-line cap in CLAUDE.md, and points
NewDevice's doc comment at EffectiveMTU instead of the deleted defaultMTU.
|
||
|
|
5a63d5d468 |
fix(mtproto): use hosts for public share links (#6369)
* fix(mtproto): use hosts for public share links Generate MTProto subscription, client, copy, QR, and export links from managed Hosts so reverse-proxied public ports are advertised correctly. Migrate the redundant legacy custom share address into a Host and keep old imports compatible. Closes #5126. * fix(mtproto): keep host share links lossless and consistent Address review on the MTProto hosts share-link change. The migration no longer drops a legacy custom share address: an unrelated (or disabled) Host stopped suppressing it, so only a Host already advertising the same address does. An imported address now clears the same validation the strict normalizer applies to every other protocol before it becomes a Host. Panel and subscription agree on the endpoint a Host advertises: a portless host string inherits the inbound port rather than the group's, and a port-only host inherits the inbound address instead of emitting server=%3A8443. LinksForClient prefers host endpoints for every protocol, the way getSubs and inboundLinks already do, so the client-links API no longer ignores managed hosts. * fix(mtproto): migrate legacy share address past unusable hosts The seeder skipped the conversion whenever any Host already carried the address, including one that is disabled or excludes the raw sub type. hostEndpoints drops those, so nothing advertised the address afterwards and the marker committed with no way back. The duplicate check now mirrors that same predicate. UpdateInbound cleared a legacy MTProto shareAddr without the Host conversion AddInbound runs, so re-applying an inbound definition through the API dropped the public address silently. Both paths share one capture helper now. Refresh the generated clients API reference for the summary reworded in the previous commit. * fix(inbounds): wait for the hosts list before building mtproto links The page destructured only `hosts` from useHostsQuery, and that list reads empty both while /panel/api/hosts/list is in flight and after it fails. withMtprotoHostEndpoints then returns the inbound untouched, so Copy, QR and Export advertise the internal listen port — the endpoint this branch exists to replace. It is worse than not fixing it: the seeder has already moved a legacy custom share address into a Host, so the fallback is the panel's own hostname instead of the operator's address, and the Go generators reading the same rows from the DB stay correct, so the two disagree for one inbound. Fold the query into the page's existing readiness gate, the same way useInbounds and HostsPage already consume that hook, so an empty list means "no hosts" rather than "not loaded yet". The error branch fires only when nothing is cached, so a refetch failing on window focus does not blank a page whose host rows are still perfectly usable. --------- Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
2d151d7648 |
Update deps and simplify parsing
Bump frontend and Go dependencies, then modernize a few hot paths with newer Go string/range helpers. Also widen the client form quota/limit fields to improve the layout. |
||
|
|
4e355edc15 | fix(sub): skip AmneziaWG JSON entries (#6420) | ||
|
|
0f6e1ae8d7 |
fix(sub): bind JSON local inbounds to 127.0.0.1 and keep mux.cool off Vision outbounds (#6418)
* fix(sub): bind JSON local inbounds to 127.0.0.1 and keep mux.cool off Vision outbounds The JSON subscription's local SOCKS/HTTP inbounds had no listen address, so every client that runs the profile verbatim bound an unauthenticated proxy on 0.0.0.0, and iOS packet-tunnel clients could not reach it at all (Happ iOS: CONNECTED with zero traffic, same symptom as #6379 — on the same device the mixed inbound also worked once bound to 127.0.0.1). Bind both to loopback, which is what every client's own generated config does. The global subJsonMux was also applied to VLESS outbounds carrying xtls-rprx-vision. XTLS flows do not support mux.cool: Xray answers the mux handshake with "common/mux: unexpected network TCP" and the tunnel passes nothing, on every platform (verified with Happ iOS/Android/macOS, V2Box iOS and desktop Xray 26.6.27 against a 3x-ui 3.7.0 box with per-client traffic counters). Skip the mux block whenever the outbound carries a flow. Refs #6379 * fix(sub): keep XUDP settings when disabling TCP mux on Vision outbounds Clearing the whole mux object also dropped xudpConcurrency, xudpProxyUDP443 and any per-host muxParams override. Xray reads those only under mux.enabled, so set concurrency to -1 instead: TCP mux.cool (which XTLS flows reject) is off, XUDP and the UDP/443 policy stay. The test now decodes each outbound into a fresh map. --------- Co-authored-by: Farhan Zare <farhan.zare@openscreen.com> |
||
|
|
2ddcf53020 |
Feature/fix external subscription client expiry (#6333)
* fix(sub): honor client expiry for external links * fix(ui): show client expiry on external links * fix(sub): address external expiry review |
||
|
|
f17e4684e0 |
fix(sub): apply the device limit to ?view=raw
subJsons and subClashs served the raw body and returned before enforceHwid ran, so appending ?view=raw to a JSON or Clash subscription URL handed out a complete, client-consumable config however many devices were already registered. The branch exists to stop a browser's Accept: text/html from being answered with the info page, not to skip the gate. Gate the raw branch and leave the other gate where it was, below maybeServeSubPage, so the HTML info page stays ungated as before. |
||
|
|
540caa4e93 |
fix(hysteria): standard geco share links and persistent uTLS None (#6325)
- Export standard gecko obfs query params in hysteria2 share links - Enforce packet size bounds across Go and TypeScript link handlers - Persist uTLS None explicitly and initialize new TLS inbounds to chrome - Tear down stackTun safely without closeMu deadlock against WriteNotify Co-authored-by: rqzbeh <rqzbeh@users.noreply.github.com> |
||
|
|
0c72dd8384 | fix(sub): restore compatible SOCKS subscription inbound (#6395) | ||
|
|
7100fbcd08 |
feat(sub): leastLoad member weights for subscription balancers (#6304)
* feat(model): add MemberWeights to SubBalancer Per-inbound leastLoad weights, stored with the same gorm json serializer as InboundIds so AutoMigrate adds the text column on every dialect (postgresModelSettled sees the missing column and re-runs). Absent entries mean weight 1.0; only meaningful for strategy leastLoad. * feat(sub): accept memberWeights on the sub-balancer API Parsed as one JSON form field (gin cannot bind bracket-keyed maps from urlencoded bodies). validate() rejects weights under any strategy but leastLoad — xray would silently ignore costs there, so storing them would pretend a knob exists. Non-positive weights error instead of defaulting: a zero usually means a typo'd "never pick this node". Entries for inbounds no longer selected are dropped on save. * feat(sub): emit leastLoad strategy costs from member weights costs[] is built after the tagging loop reuses the exact retagged tags (bal-N-protocol[-k]) and each member's owning inbound id. Members without a configured weight default to 1.0, but costs are omitted entirely unless at least one explicit weight survives — an all-1.0 array would bloat every subscription response for no effect. * feat(sub-balancers): leastLoad member weight inputs Weight fields render only under leastLoad and hide on strategy change without dropping their values, so an accidental toggle away and back loses nothing until save; non-leastLoad submits strip them entirely because xray would ignore costs. Weights travel as one JSON form field (gin cannot bind bracket-keyed maps) and every locale gets the three new keys in the same commit per the dead-keys rule. * docs(api): document memberWeights on sub-balancers leastLoad-only JSON form field; update notes that omitting it clears stored weights. Regenerated openapi artifacts via make gen + the docs copy/gen:api step nothing checks automatically. * fix(api-docs): use the allowed object ParamType for memberWeights * fix(sub-balancers): cap the member-weight list height Many selected inbounds pushed the modal body past the viewport. The weight rows now scroll inside a 220px viewport, mirroring the inbound picker's listHeight so both lists read the same. * fix(sub): anchor leastLoad cost matches to exact member tags Verified against xray-core: without regexp, WeightManager matches costs by substring (strings.Index), so the bare tag "bal-1-vless" also hits the deduplicated "bal-1-vless-2" and both members get the first entry's weight. Anchored ^tag$ regexps make every cost entry match only its own member. Also confirmed value<=0 makes xray derive a weight from the first digit of the matched tag — validating weights > 0 server-side was the right call. * fix(sub-balancers): keep member weights across the enabled toggle The table's toggleEnabled re-posted a full-row payload without memberWeights, and the update path treats an absent key as "erase" — flipping the switch silently dropped every configured weight. Round-trip the stored weights through the toggle payload, and prove persistence with a re-Get in the weight-validation test (the returned struct alone would stay green even if Save skipped the column). * fix(sub-balancers): address review on member weights - omitempty on MemberWeights: the panel sends null for every pre-existing and non-leastLoad balancer, which failed the hand-written zod response schema on every fetch (zod .optional() accepts undefined only; switched to .nullish() per repo convention) and drifted the generated contract. Regenerated openapi artifacts + docs copy + MDX. - Bound weights to the positive float32 range: xray decodes costs as float32, so an over-range value makes clients reject the whole subscription document and an underflow decays to the tag-digit fallback weight. Tests for both directions. - Trim six comment blocks to the 2-line cap from CLAUDE.md. --------- Co-authored-by: DIMFLIX <dimflix@users.noreply.github.com> |
||
|
|
7a595cb46d |
fix(sub): keep Hysteria2 mport on external-proxy links
genHysteriaLink only looked up the UDP hop range on the no-endpoint path, after the externalProxy fan-out had already returned. An inbound with Hosts therefore emitted per-host links without mport, so clients pinned themselves to the single listening port and silently lost port hopping. Set the param before the fan-out so every endpoint inherits it, matching the frontend link builder and the Clash emitter. Closes #6264 |
||
|
|
effcccceac |
feat(amneziawg): add native AmneziaWG protocol support (#6105)
* feat(amneziawg): add native AmneziaWG protocol backend AmneziaWG (WireGuard plus DPI-resistant obfuscation) needs no Docker here — it runs as a genuine kernel interface via awg-quick/awg, managed the same way internal/mtproto manages mtg: one Inbound row is one desired Instance, and a Manager reconciles running interfaces toward the database every 10s (internal/web/job/amneziawg_job.go) plus immediately after a client edit (applyLocalAmneziaWG). Clients reuse model.Client verbatim (the same PrivateKey/PublicKey/ PreSharedKey/AllowedIPs fields WireGuard already uses), so bulk operations, the QR/share-link modal and subscriptions come from the shared inbound infrastructure instead of a parallel implementation. internal/amneziawg owns the obfuscation param generator/validator (ported from coinman-dev/3ax-ui, upgraded to AmneziaWG 2.0's S3/S4 padding and I1 signature packet) and the exec wrapper around awg-quick/awg, with fingerprint-based reconcile (noop / reload-via- syncconf / full restart) mirroring mtproto.Manager so a same-protocol edit doesn't force an unnecessary interface bounce that would drop every peer's connection. Frontend and install.sh's DKMS/awg-tools setup are tracked separately; this is backend-only. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * 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> * 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> * docs(install): note that AmneziaWG kernel module install is still manual Tracked separately (not yet ported into this script) — see coinman-dev/3ax-ui's install_amneziawg for the reference approach (ppa:amnezia/ppa). Also serves as a real, path-filter-matching change to get the previous empty commit's CI trigger to actually fire — release.yml's push trigger is paths-scoped and an empty commit changes no files, so it never matched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * 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> * feat(install): auto-install the AmneziaWG DKMS module + amneziawg-tools Ports install_amneziawg from coinman-dev/3ax-ui's install.sh, adapted to this script's broader distro coverage and NONINTERACTIVE convention: - Ubuntu/Debian/Armbian: ppa:amnezia/ppa (primary, tested path), with a reachability pre-check for the Launchpad PPA host — often blocked by hosting providers, especially Russian VPS — so a flaky network skips the feature instead of hanging apt through several retries. - Fedora/RHEL-family, Arch/Manjaro/Parch: best-effort fallback to plain wireguard-tools (+ AUR amneziawg-dkms via yay/paru when available), with a manual-install pointer. - Everything else: manual-install pointer only. Also installs ndppd and persists IPv4/IPv6 forwarding (for the future IPv6/NDP phase, not yet wired into the panel) and adds a Secure Boot warning at the end of the run, since a DKMS-built module is unsigned and won't load while it's enabled — a common trap on cloud VPS images. Never fatal: the panel installs and runs fine either way, an AmneziaWG inbound just won't bring up its tunnel until the module is present. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(amneziawg): resolve all 3 real CI failures (typecheck/lint/codegen) Found by checking the fork's Actions tab after the last two pushes — the release build passed (it doesn't run these checks) but the separate CI workflow caught three real issues: - golangci-lint (noctx): every internal/amneziawg/manager.go exec.Command call is now exec.CommandContext with a 30s timeout, so a hung awg-quick/awg invocation can't block the reconcile job indefinitely (mirrors internal/mtproto/process.go's own CommandContext usage). - tsc --noEmit: frontend/src/schemas/client.ts's hand-maintained InboundOptionSchema (used by the useClients hook, separate from the auto-generated one in generated/) never got an awgServer field added when the AmneziaWG frontend work was done — every read of inbound.awgServer.* in amneziawgConfig.ts was typing as {}. Added AwgServerOptionSchema, nested (not flattened like wg*) to match what amneziawgConfig.ts already expects. Also guarded server.publicKey in inbound-link.ts's genAmneziaWGLink against the schema's optional type. - codegen staleness: frontend/public/openapi.json is produced by a Node script (gen:api) this machine can't run; hand-applied the exact diff the CI failure log already showed (amneziawg protocol enum entry, ServerSettings schema, InboundOption.awgServer, one example payload), verified as valid JSON. Also confirmed independently by this run: install_amneziawg (previous commit) installed and loaded the DKMS module successfully on both amd64 and arm64 CI runners. The two "Deploy Smoke Tests" failures are unrelated to this change — this fork has only ever published the dev-latest pre-release, and GitHub's /releases/latest API deliberately excludes pre-releases, so the smoke test's no-argument install path (which resolves "latest") has nothing to find. Not a regression; needs an actual tagged release whenever that's wanted. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * 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> * fix(amneziawg): fill in IPv6 fields missed by the Phase 2a commit Two real gaps the CI caught (both new fields, both my miss): - inbound-defaults.ts's createDefaultAmneziawgInboundSettings() built a server object literal predating ipv6Enabled/ipv6Subnet/ ipv6ExternalInterface — AmneziawgServer's inferred type now requires them (zod .default() fields are non-optional post-parse), so this didn't typecheck at all. - openapi.json's ipv6Enabled property was missing the description the real generator attaches (the Go doc comment covering all three IPv6 fields is attached to the first one) — a one-line diff, but git diff --exit-code doesn't care how small. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * 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> * feat(amneziawg): route a client's traffic through Xray via the Routing page Every enabled AmneziaWG inbound gets its own Xray TPROXY bridge automatically, with no toggle to enable first: a loopback dokodemo-door inbound (sockopt.tproxy) tagged with the AmneziaWG inbound's own real tag, so it's already selectable in the existing Routing page's inbound-tag picker — the same trick the mtproto sidecar's own bridge already relies on (InboundService.GetInboundTags is a plain, protocol-blind SELECT over every inbound row's tag, no dedicated UI plumbing needed). internal/amneziawg's defaultPostUpDown TPROXYs every peer's traffic into that bridge unconditionally; the bridge's port is derived deterministically from the inbound's id (EgressPortForInbound) so the kernel-side reconcile loop and the Xray-config generator never need to negotiate a runtime value between them. injectAmneziawgEgress never generates a routing rule itself — whether a client's traffic goes anywhere beyond Xray's default routing is entirely up to whatever rules the admin adds through the existing Routing UI (pick the AmneziaWG inbound's tag as source, optionally a specific peer's IP via that page's own Source-IP field, and an outbound), exactly the same workflow as routing any other protocol. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(amneziawg): recover orphaned interfaces after an ungraceful exit Two gaps left an AmneziaWG interface stuck outside the manager's control after a crash (kill -9/OOM/panic skips StopAll): - ensureRestart's teardown was gated on the in-memory `exists` map, which is always empty on a fresh process, so a survived interface never got interfaceDown before interfaceUp tried `ip link add` against a name the kernel already had — failing forever and never populating m.ifaces, so traffic accounting silently stopped and the inbound could never be removed. Gate on isInterfaceUp instead, which checks real kernel state rather than this process's own bookkeeping. - An inbound deleted from the database entirely while the panel was down has no entry in `desired` ever again, so it never reaches the per-id cleanup loop in Reconcile (which only walks m.ifaces). Add a one-time sweepOrphansLocked scan of configDir, mirroring mtproto.Manager.sweepOrphansLocked, that tears down and removes any leftover interface/config not in the current desired set. Found by the automated review on MHSanaei/3x-ui#6105 (Finding 1). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * i18n(amneziawg): backfill IPv6/obfuscation/port-forwarding keys in 11 locales Only en-US/ru-RU ever got these 9 keys as each AmneziaWG feature landed (the regenerate-obfuscation button, then Phase 2a's IPv6 fields, then Phase 2b's per-client ForwardedPorts) — the other 11 locale files were never backfilled, so i18next has been silently falling back to English for all of them since Phase 1. Cosmetic-only (never broke anything), but now closed for every shipped locale. * fix(amneziawg): resolve 7 Medium findings from the automated PR review Each is independently reproducible; fixed together since one review pass found all of them. - manager.go: the shared "ip rule add fwmark" policy route had no existence check, so it duplicated in "ip rule show" on every interface bounce (which hostRulesFingerprint forces on any client add/remove/ re-IP). Now checked via "ip rule list | grep -q ..." first. (Finding 2) - params.go: ExternalInterface, IPv6ExternalInterface, and subnetIp/ subnetCidr are interpolated unescaped into a shell-executed PostUp/ PostDown line, but only obfuscation and the IPv6 subnet were validated before save. Added ValidateInterfaceName (a strict charset+length pattern) and ValidateSubnetIPv4 (netip.ParsePrefix), wired into normalizeAmneziaWGSettings. (Finding 3) - amneziawg_job.go: IsAwgInstalled() existed but nothing ever called it, so a host without awg/awg-quick (the Docker image, RHEL, Arch, a failed install.sh PPA step) logged a reconcile failure every 10s forever. Now checked once an inbound actually needs it, warning once instead of spamming. (Finding 4) - client_inbound_apply.go: the WireGuard/AmneziaWG credential carry-forward (added so a metadata-only client edit doesn't rotate keys) never covered ForwardedPorts, so a partial edit -- an API call or Telegram-bot toggle that omits the field -- silently wiped a client's port-forwarding spec. Carried forward and written back the same way the key fields already are. (Finding 5) - manager.go: hostRulesFingerprint keyed each peer on its IPv4 address only, and structuralFingerprint omitted IPv6Enabled/IPv6ExternalInterface entirely, so an IPv6-only change could pick the syncconf reload path (which never re-runs PostUp, leaving a stale NDP-proxy entry) or be a complete no-op. Both fingerprints now cover the IPv6 fields. (Finding 6) - port_conflict.go: the AmneziaWG egress bridge (injectAmneziawgEgress) binds 127.0.0.1:63100+id with no collision check anywhere, since it isn't a database row the ordinary port-conflict query can see -- same blind spot the reserved Xray API port already has its own check for. Added the equivalent check for the AmneziaWG bridge port. (Finding 7) - install.sh: install_amneziawg ran unconditionally for every install/ update, building a DKMS kernel module and enabling host-wide IPv4/IPv6 forwarding whether or not the feature is ever used. Gated behind a new should_install_amneziawg (XUI_INSTALL_AMNEZIAWG=true/false, or an interactive y/N prompt defaulting to no). Also replaced the deprecated apt-key adv with a dedicated keyring + signed-by= on the Debian branch, and guarded its sources.list appends against duplication on a retried install. (Finding 8) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * 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> * fix(amneziawg): resolve 4 Low findings from the automated PR review - manager.go: serverAddress assumed subnetIp always ends in ".0"; a base like "10.8.1.5" was used verbatim as the server's own address, eventually colliding with peer allocation (which starts at .2 upward). Now derives the first host of the actual subnetIp/subnetCidr network via netip, matching serverAddressV6's own approach. A /32 base (no host bits at all) is still used as-is. (Finding 12, partial -- the /16 pool-widening half of this finding only exists on the upstream-pr/amneziawg branch's merged client_wireguard.go, not here; handled separately on that branch.) - manager.go: ensureLocked carried the previous per-peer traffic counters (`last`) forward even through a full restart, but awg-quick down+up resets the kernel's own counters to zero -- the next CollectTraffic computed a large negative delta (clamped to 0), silently discarding real traffic. Extracted the decision into nextTrafficBaseline: only a reload (syncconf) preserves the baseline. (Finding 13) - portfwd.go: exported ForwardedPortsInclude; inbound_amneziawg.go's new checkForwardedPortsConflict uses it to reject, at save time, a client's forwardedPorts that would DNAT the panel's own port or another enabled inbound's port to the tunnel client -- portForwardLines has no destination restriction, so this collision was previously silent. Wired into both the single-client update path and the add-client path (client_inbound_apply.go), plus normalizeAmneziaWGSettings for the whole-inbound save path. (Finding 14) - inbound.go: InboundOption.AwgServer sent the whole ServerSettings struct including PrivateKey to GetInboundOptions callers -- a shared, admin-wide dropdown-filling endpoint the frontend's own AwgServerOptionSchema never reads that field from. Redacted it before assigning. (Finding 11) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(amneziawg): don't widen the peer address pool past AmneziaWG's own subnet Completes Finding 12 from the automated PR review (the serverAddress half of this finding was already fixed on main and cherry-picked here). This half is specific to this branch: allocateWireguardAddress's /16 pool-widening fallback is an independent addition from upstream's own main that this branch inherited during the cherry-pick rebase -- it doesn't exist on the fork's own main at all, so this fix can't be cherry-picked the normal way and is committed directly here. Widening is safe for WireGuard's own Xray-native inbound (AllowedIPs isn't tied to a strict kernel interface subnet), but AmneziaWG's kernel interface Address is exactly the configured subnet -- an address allocated from the containing /16 once the /24 fills up would be silently unroutable. allocateWireguardAddress now takes an explicit allowWidening bool: WireGuard's own caller passes true (unchanged behavior), AmneziaWG's passes false (fails loudly on exhaustion instead). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * docs(docker): note that AmneziaWG doesn't work in this image Investigated: the image is Alpine-based, and AmneziaWG's own packaging (DKMS module + amneziawg-tools) doesn't target Alpine/musl at all -- unlike the Debian/Ubuntu/Fedora/Arch paths install.sh already handles, there's no package to apk add even with full host network/capabilities. The panel already degrades gracefully (IsAwgInstalled() logs one warning instead of retrying forever), so no code change is needed -- just made the reason explicit at the point where a user would reach for cap_add/ network_mode to try to work around it. * fix(sub): include amneziawg inbounds in subscription links getInboundsBySubId's SQL protocol allowlist never had 'amneziawg' added, so every AmneziaWG client was silently excluded from all three subscription formats (plain/individual links, JSON, Clash) and from the Telegram bot's QR/individual-link buttons, which fetch through the same path. genAmneziaWGLink itself was already fully implemented and already wired into GetLink's dispatch switch -- it just never got a chance to run. Same bug shape as the earlier TRACKED_PROTOCOLS frontend gap: a hardcoded protocol list one entry short. Found while investigating whether the Telegram bot needed AmneziaWG- specific client-management code -- it doesn't (the bot itself is fully protocol-agnostic), but this is the actual root cause of "can't share an AmneziaWG client's config via the bot." Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(inbound): enforce node-eligibility server-side, not just in the UI Investigated multi-node interaction with AmneziaWG: the master's own reconcile (DesiredAmneziaWGInstances) and Xray config generation (injectAmneziawgEgress, the GenXrayInboundConfig protocol skip) all correctly filter on NodeID IS NULL, so a node-assigned AmneziaWG (or MTProto) inbound would never be managed by the master. But nothing stopped one from being created that way: NODE_ELIGIBLE_PROTOCOLS (frontend/src/pages/inbounds/form/InboundFormModal.tsx) only hides the node picker client-side -- a direct API call could set nodeId on an AmneziaWG inbound, which every node then reconciles as an ordinary local inbound (nodes run the identical binary, full cron suite included), leaving it running unmanaged and untracked by the master's own AmneziaWG bookkeeping. Added isNodeEligibleProtocol (inbound_protocol.go), mirroring the frontend's allowlist, and enforced it in both AddInbound (the actually exploitable path -- nodeId comes straight from the request) and UpdateInbound (defense in depth; NodeID is already restored from the stored row there before this check, so it mainly guards against a protocol change on an existing node-hosted inbound). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(amneziawg): allow TPROXY-marked traffic through a default-deny INPUT chain TPROXY never rewrites a packet's own destination address, only the routing decision. A default-deny firewall whose INPUT chain sanity-checks "is this destination actually local" (UFW's ufw-not-local, via addrtype --dst-type LOCAL, is a concrete example) silently drops the redirected packet before Xray's socket ever sees it -- RouteThroughXray looked fully configured (TPROXY rule present and counting, Xray listening with IP_TRANSPARENT set) yet every peer's traffic vanished with no trace on either side. Adds an idempotent, never-torn-down "iptables -I INPUT 1 -m mark --mark <fwmark> -j ACCEPT" alongside the existing shared policy route, so this works regardless of which firewall manager owns the rest of the INPUT chain. * 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. * feat(logs): show which AmneziaWG client an access-log line belongs to The dokodemo-door TPROXY bridge every AmneziaWG peer's traffic is routed through has no per-user identity, so Xray's own access log never carries an "email:" token for these lines -- the Access Logs modal showed a blank Email column for every in-*-udp row, even though every other protocol's rows show the client normally. The peer's decapsulated tunnel IP does survive as the log's "from" address, and that IP deterministically maps to exactly one configured peer. Builds a "<inbound tag>|<ip>" -> email index from the same AmneziaWG inbounds already parsed elsewhere (amneziawg.InstanceFromInbound), and fills in Email from it whenever the raw log line didn't have one. * fix(amneziawg): enable sniffing on the TPROXY bridge Domain-based Routing rules could never match RouteThroughXray traffic: an AmneziaWG peer resolves DNS itself, through the tunnel, before ever sending a packet, so the decapsulated traffic TPROXY hands to the bridge is already a bare destination IP with no domain name attached at the network layer. Every other inbound recovers this via sniffing (confirmed working for the stock wireguard inbound, which does have it configured); the bridge never got a sniffing block at all, so only tag/IP/network-based rules could ever match it -- any domain rule above it in the list was silently unreachable. * docs: add an AmneziaWG config page and list it as a supported protocol Closes the PR checklist gap: the feature shipped with zero mention on the docs site. Mirrors reality.mdx's structure (key settings, setup steps, config excerpt) and notes the Docker/multi-node/Telegram-bot caveats the PR itself is honest about not having confirmed. * fix: address the fresh review round on PR #6105 (8 findings) 1. hostRulesFingerprint didn't account for ForwardedPorts when RouteThroughXray was off, so re-IPing a peer with port-forwarding configured left stale DNAT rules pointing at an address the next peer could be handed. 2. Server/client config values (keys, email, I1) were never validated for control characters before being written into the generated .conf; a newline could smuggle a PostUp hook into awg-quick's parser. Added ValidateConfigValue at save time and a sanitizeConfigValue backstop at render time. 3. checkForwardedPortsConflict didn't scope to node_id IS NULL, so a port used only on a different node produced a false collision; also hoisted the panel-port/inbounds lookup out of the per-client loop (portConflictContext) so N clients cost one query, not N. 4. PostDown commands were ";"-joined and abort on the first failure; appendOrTrue makes teardown best-effort so an external firewall flush can't leave DNAT rules to accumulate across bounces. 5. The "ip rule list | grep -q" existence check could SIGPIPE under pipefail and re-add a duplicate rule; switched to grep -c >/dev/null. 6. Ported the vpn:// share-link format (base64url of the plain .conf text, matching the real AmneziaVPN app) onto this branch -- it had only ever landed on our own fork's main, so this PR branch was still on the old amneziawg://+query-params scheme our own docs no longer described. Also corrected the docs' install.sh claim (opt-in/ interactive, not automatic) and stale pre-opt-in comments in route_egress.go. 7. install.sh: Arch's ndppd install used pacman -Syu (full system upgrade) instead of -Sy like every other call in the script; and should_install_amneziawg re-prompted on every `x-ui update` even when awg was already installed. 8. CollectTraffic could clobber a concurrent restart's freshly-reset (empty) traffic baseline with stale pre-restart counters, since getPeerStats runs lock-free; now checks pointer identity before writing back. sweepOrphansLocked permanently disabled itself on a transient os.ReadDir failure instead of allowing a retry. go build/vet/test and frontend typecheck/lint/build/vitest all pass. * fix(install.sh): check the live sysctl value, not sysctl.conf text Reviewer feedback (cherts, PR #6105): grepping /etc/sysctl.conf for the setting name is unreliable -- many distros split sysctl config across /etc/sysctl.d/*.conf, and /etc/sysctl.conf can be a symlink into that directory, so the check can miss an already-active setting (harmless duplicate append) or match a disabled/commented line (forwarding silently stays off). Query the live value via `sysctl -n` instead, which is accurate regardless of which file set it. Applied the same fix to both the IPv6 and IPv4 checks for consistency. * fix: update inbound_amneziawg.go to the split buildInboundForLocalRuntime Same fork-only-file blind spot as the one caught on our own main after the 3.6.0 sync: upstream split buildRuntimeInboundForAPI into buildInboundForNodePush / buildInboundForLocalRuntime (part of the node-sync client-deletion fix, |
||
|
|
d9b599b9aa |
fix(sub): forward tlsSettings.cipherSuites into the JSON subscription
tlsData rebuilds the client-side tlsSettings from a whitelist of keys and never copied cipherSuites, so an inbound configured with e.g. "TLS_AES_256_GCM_SHA384" handed clients a config that negotiated any suite. Copy it through when non-empty; it is a real xray-core tlsSettings field, unlike the non-standard "cs" share-link param. |
||
|
|
cc245a908e |
style: format struct literals and whitespace
Clean up trailing braces, commas, and unnecessary blank lines in struct initializations across sub and network packages. |
||
|
|
c26ff59b47 |
chore(sub): drop the inlined externalLinkEnabled test helper
|