mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-14 02:12:11 +03:00
main
133 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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
|
||
|
|
da01b7637d |
feat(sub): client-side balancers for the JSON subscription (#6243)
* feat(sub): add SubBalancer model and migration Client-side JSON-subscription balancer row: remark, strategy, member inbound ids, sort order, enabled. Registered in allModels and migrationModels so AutoMigrate and SQLite->Postgres copy pick it up. * feat(sub): add SubBalancer service List/Get/Create/Update/Delete over the sub_balancers table with remark trim, strategy allowlist (leastLoad/leastPing/random) and sort-order floor. Rows are read per request by the subscription builder, so mutations need no xray restart. * feat(sub): add SubBalancer API controller and routes GET/POST /panel/api/sub-balancers, POST /:id (update), DELETE /:id and POST /:id/del alias. inboundIds bind from repeated form keys. Mounted under the /panel/api group so the existing API token + CSRF middleware cover it. * feat(sub): emit client-side balancers in JSON subscription For each enabled balancer, append one config document whose outbounds are the selected inbounds' proxy outbounds retagged under a per-balancer prefix, with routing.balancers + burstObservatory selecting it. Balancer entries interleave with inbound entries by sort order; on equal numbers the balancer follows the inbound. Skipped when disabled or no member outbound is present. * test(sub): cover SubBalancer service and JSON output Service: validation gates (remark/strategy/inbound ids/sort order) and CRUD round-trip. JSON: balancer document shape, sort interleaving with inbounds, disabled/empty skip, and member tag dedup. * feat(sub): add sub-balancers i18n keys pages.settings.subBalancers.* block (menu, title, add, desc, field labels, strategy names, sort-order help, validation messages) added to all 13 locales. * feat(sub): add SubBalancer schema and API queries Zod schema (entity + form, strategy enum, validation messages wired to i18n keys), react-query hooks for list/create/update/delete, and the sub-balancers query key. * feat(sub): add subscription balancers settings tab SubscriptionBalancersTab lists balancers (sort order, remark, strategy, inbound count, enabled toggle, edit/delete) with a form modal (remark, strategy, sort order, multi-select inbounds filtered to multi-client protocols, enabled). Wired into SettingsPage under #subscription-balancers, and the sidebar shows the entry only when JSON subscription is enabled. * test(sub): add SubBalancer form modal test Covers add-mode (no validation errors, confirm with parsed values) and edit-mode (seeds from the balancer, preserves strategy/sort order/enabled). * feat(sub): register sub-balancers in API docs and OpenAPI Adds the sub-balancers endpoint group to endpoints.ts (list/create/update/delete + POST del alias) and regenerates frontend/public/openapi.json from it. * docs: sync openapi.json with frontend docs/public/openapi.json had fallen behind frontend/public/openapi.json (fewer paths/schemas). Copy the current frontend spec so the docs site renders the full API. * docs: add subscription balancers API reference Registers the sub-balancers page (generated MDX) and adds the sub-balancers paths to docs/public/openapi.json so the page renders the list/create/update/delete operations. * feat(sub): accept roundRobin balancer strategy Add roundRobin to the model oneof tag and the service strategy allowlist, alongside leastLoad/leastPing/random. Covered by a service-level create test that fails on the old allowlist. * feat(sub): add roundRobin strategy label pages.settings.subBalancers.strategyRoundRobin added to all 13 locales. * feat(sub): expose roundRobin in balancer form Zod strategy enum, form modal label key, and table strategy colour for roundRobin. * docs(sub): list roundRobin in strategy description The create/update strategy param description now mentions roundRobin alongside the other three. * feat(sub): add subJsonObservatory setting Panel-wide JSON string carrying the burstObservatory ping config (destination, connectivity, interval, sampling, timeout, httpMethod) emitted into client-side balancer docs. Stored like subJsonMux/Rules/FinalMask. * feat(sub): wire observatory config through sub controller WithSUBJsonObservatory option; the controller calls SubJsonService.SetObservatoryConfig after construction. * feat(sub): emit observatory conditionally with configurable probes burstObservatory is emitted only for leastPing/leastLoad; random/roundRobin get none (no fallback, so an observatory would only probe for nothing). Probe params come from the subJsonObservatory setting, falling back to the built-in defaults when empty or partial. Test covers the conditional emit and the override. * feat(sub): add subJsonObservatory to AllSetting model Frontend AllSetting model and Zod schema carry the new panel-wide observatory config string. * feat(sub): add balancer observatory config card New Sub Formats tab editing destination/connectivity/interval/sampling/timeout/httpMethod, stored as JSON in subJsonObservatory. Toggle off clears the setting; the backend then falls back to defaults. * fix(sub): hide save/restart header on sub-balancers tab Sub-balancer mutations are incremental (own CRUD API, no Save, no restart), so the page-wide 'every change needs to be saved / restart the panel' banner is misleading there. The in-tab alert already explains it correctly. * feat(sub): add observatory config i18n keys pages.settings.subBalancers.observatory.* (title, desc, probe field labels and help texts) added to all 13 locales. * feat(sub): regenerate openapi for subJsonObservatory openapigen picks up the new AllSetting field; openapi.json synced into docs. * feat(sub): add observatory tab to sub-balancers Mirrors the Xray Balancers page: two tabs (Balancers + Observatory). Wires allSetting/updateSetting into the tab and adds tabBalancers / tabObservatory labels to all locales. The page Save header is shown again on this tab so the observatory config can be saved. * refactor(sub): drop observatory tab from sub-formats Now that the observatory config lives under sub-balancers, remove the duplicate tab plus its state and defaults from sub-formats. * fix(sub): add missing inboundsCount i18n key The sub-balancers table rendered the raw key path in the Inbounds column because pages.settings.subBalancers.inboundsCount was not defined. Added it to all 13 locales. * test(sub): pin disabled-inbound exclusion from balancer The balancer builds its members from the subscriber's already-filtered entry set, so an inbound disabled for that user can never surface as a member. Adds tests for both shapes (one of several disabled, and the only selected one disabled). * fix(sub): make observatory toggle honest, default connectivity off, add balancer fallback Three coupled defects on the balancer observatory surface, flagged in PR review: - The Observatory Switch wrote '' which the Go side treats as "use built-in defaults", so leastPing/leastLoad still shipped a burstObservatory the admin could no longer see or edit. The observatory is mandatory for these strategies (Xray refuses to start leastPing/leastLoad without one — verified against Xray 26.7), so the switch is relabelled to "customise probe parameters vs built-in defaults" rather than on/off: '' keeps the defaults, a stored JSON overrides them. An info Alert explains this. - Connectivity defaulted to http://www.google.com/generate_204 and an explicit {"connectivity":""} restored it, so the UI's "Leave empty to skip" was unreachable and the direct pre-check was dead on arrival on censored client networks. Default to "" and honour an explicit empty value. - routing.balancers had no fallbackTag, so a leastPing/leastLoad balancer whose probes all fail selects nothing and dispatch fails. Emit fallbackTag pointing at the first member so a probe outage degrades instead of breaking. Also skip balancer entries (kind!=0) in the member scan so a balancer can never match another balancer's row id. Tests cover each fix and fail without it. * fix(sub-balancer): localize controller toasts and reject malformed ids Route the new controller's user-facing messages through I18nWeb so non-English admins get localized toasts like every other controller, and switch parseID to strconv.Atoi rejecting ids < 1 so "12abc" and negative ids no longer coerce to a silent no-op delete that reports success. * fix(sub-balancer): enforce remark length cap server-side The model's validate:"max=256" tag was never enforced (parseSubBalancerForm binds an ad-hoc struct without validate.Struct), so a scripted API client could store an unbounded remark that is emitted verbatim as the remarks field of every affected subscriber's config. Reject len > 256 in validate() to match the frontend Zod cap. * fix(sub-balancer): exclude mtproto from balancer member picker SubJsonService.getConfig has no mtproto case, so an mtproto inbound's first outbound is "direct" and the buildBalancerConfig "tag != proxy" guard drops it — an admin could select it, save without error, and get a balancer that silently omits it (or no document at all). Drop it from the picker and fix the comment. * docs(sub-balancers): add nav entry, fix tab pointer, note mirror scope - Add "subscription-balancers" to the en reference/api meta.json pages array so the new MDX page is reachable from the sidebar (fa/ru/zh have no MDX — gen-openapi.ts emits into en only). - Fix the endpoints.ts section description from "Settings -> Subscription" to "Settings -> Sub Balancers" (the feature's own tab) and regenerate the OpenAPI spec + MDX. - Note in docs/lib/xray/subscription.ts that balancer documents are intentionally out of scope for that mirror. * style(model): trim SubBalancer comment to 2-line cap CLAUDE.md caps committed Go comment blocks at 2 lines; this one was 3. * fix(sub-balancer): parse enabled explicitly and preserve it on partial update parseSubBalancerForm treated any non-"false" value as true (so "bogus" silently enabled) and always overwrote Enabled on update, so a PATCH that omitted the toggle reset a disabled balancer back to enabled. Parse the field with strconv.ParseBool and return *bool: absent means "no change" on update and "true" on create; a malformed value is rejected as 400. Update keeps the stored Enabled when the pointer is nil. * fix(sub-balancer): clear deleted inbound from sub_balancers.InboundIds DelInbound cascaded hosts but left the deleted inbound id in every sub_balancers.InboundIds, so the balancer kept emitting a member no subscriber could resolve — a dangling outbound tag with no proxy behind it. Strip the id inside the existing delete transaction (same shape as the hosts cascade, #5648); with the last member gone the balancer stops emitting. * fix(sub-balancer): return not-found when deleting a missing balancer Delete returned the gorm result error only, which is nil when no row matched, so the controller reported success:true for an id that never existed — a stale UI row looked like a clean delete. Check RowsAffected and return a not-found error on 0 so the toast reflects reality. * style(sub): shorten leastPing/leastLoad observatory comments The observatory-emission guard comment and its test comment ran a few lines long; trim them to a couple of lines each without dropping the invariant that leastPing/leastLoad require a burst observatory. * fix(sub): validate observatory setting instead of silently dropping it SetObservatoryConfig applied whatever survived json.Unmarshal with no checks, so a bad probe URL ("not-a-url"), non-duration interval/timeout, or even unparseable JSON was either silently applied or silently ignored. Validate each field: parse durations with time.ParseDuration, require http(s) URLs for destination/connectivity, and log a warning naming the field and the bad value on every fallback — including the unmarshal error, which was a quiet return. Bad values now keep the built-in defaults instead of leaking into the emitted burstObservatory. * fix(sub): deduplicate burst-observatory defaults across Go and frontend The burst-observatory ping defaults lived in three places that had drifted: Go defaultSubBalancerObservatoryConfig (http probe, sampling 3), the Zod PingConfigSchema, and DEFAULT_BURST_OBSERVATORY (both with a connectivity pre-check URL). Align them to one set: https probe destination, sampling 2, and empty connectivity (skip the direct pre-check). The settings tab now parses the stored JSON through PingConfigSchema and seeds its default from DEFAULT_BURST_OBSERVATORY instead of carrying its own literal. * refactor(sub): extract proxy outbounds once before the balancer loop buildBalancerConfig unmarshalled every inbound document and re-extracted its first outbound on each balancer, so with B balancers and N inbound docs the same document was parsed B*N times. Pull each doc's proxy outbound in a single pre-pass over the entries and cache it per entry; buildBalancerConfig now clones the cached map before retagging, so one parse serves every balancer. Output is byte-for-byte unchanged. * fix(sub): form balancer member tags from the inbound protocol, not tcp→vless balancerTransport derived the bal-N tag suffix from the outbound's transport network and hard-coded tcp→vless, so a vmess/tcp or trojan/tcp member was mislabelled "vless" in every client config — the tag lied about the proxy type. Use the outbound's real protocol as the suffix (bal-1-vmess, bal-1-vless, bal-1-trojan, …) so the tag names the actual proxy; the selector prefix and dedup suffix are unchanged. Update the existing tag assertions and add a vmess case that fails under the old mapping. * fix(sub-balancer): default strategy to random in the create form The create-balancer form seeded strategy to 'leastLoad', but the service validate() defaults an empty strategy to 'random' and the API docs say the default is 'random' — so a freshly opened form showed leastLoad while saving without touching the field silently stored random. Align the form default to 'random' so what the admin sees is what gets persisted. * feat(api-docs): document the SubBalancer response schema The five sub-balancer endpoints carried no responseSchema, so the API docs page rendered them without a typed example. Add example: tags to every SubBalancer field, allow the struct through openapigen, and point the list (responseSchemaArray) and single-row endpoints at 'SubBalancer'. Regenerate the Zod/JSON schemas and OpenAPI doc and mirror openapi.json into docs/. * style(sub-balancer): drop whitespace-only separator lines, add final newline subBalancer.ts and SubBalancerFormModal.tsx used single-space blank lines as separators between statements and had no trailing newline. Replace them with clean empty blank lines and end each file with a newline. * fix(i18n): translate sub-balancer toasts and observatory note The sub-balancer toast messages (list/create/update/delete/invalidId) and the observatory note were left in English across 11 non-English locales (ar, es, fa, id, ja, pt-BR, tr, uk, vi, zh-CN, zh-TW) while every other key in the subBalancers block was already translated. Translate them to match the meaning and terminology of the surrounding keys in each file; the JSON structure and keys are unchanged. * fix(sub-balancer): hide disabled inbounds from the member picker The picker offered every protocol-eligible inbound regardless of its enable flag, but getInboundsBySubId filters `AND inbounds.enable = true`. A disabled member is therefore dropped from every subscriber's entries, and when it was the balancer's only member the balancer document silently stops being emitted — with nothing in the UI explaining why. TestSubJson_BalancerSkippedWhenAll MembersDisabled already documents that backend behavior. Filter the way the sibling client picker has since #5645: hide disabled inbounds, but keep one that is already selected so editing an existing balancer cannot silently drop a member. Drop the `?? []` on the useWatch result so the new useMemo dependency stays referentially stable. * style(sub): trim the balancerMemberSuffix comment to the 2-line cap Comment blocks in committed Go are capped at 2 lines; the name already carries what the function picks, so keep only the why. --------- Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> |
||
|
|
81fcacab11 |
chore(build): bump Go toolchain to 1.27.0
Go 1.27.0 shipped on 2026-08-19. Raise the go directive and the builder image so Docker and release builds pick it up; every CI job already reads the version from go.mod, and golangci-lint v2.13.1 release binaries are themselves built with go1.27.0, so the lint job needs no pin change. |
||
|
|
380aff4d82 |
Add remote routing URL support (#6168)
* Add remote routing URL support * Harden remote routing refresh * fix(sub): harden remote routing fetch and accept Mihomo src rule flag Remote routing bytes reach the YAML/JSON parsers from goroutines that run outside Gin's recovery, so a parser panic on crafted input would take down the whole panel. Contain it in fetch() (a panic now degrades to a failed refresh that keeps the last-good value and releases the in-flight slot) and start the refresh, cache-load and startup-warm goroutines through common.GoRecover like the other background workers. The route-graph validator only skipped a trailing no-resolve flag, so a valid Mihomo rule like IP-CIDR,x,DIRECT,no-resolve,src was rejected as an unknown target; skip both option flags. Also deduplicate the HTTPS-source classification into common.ParseRemoteRoutingURL so the save-time validator and the resolver can never drift (internal/sub imports internal/web/service, so the copy existed only to avoid the import cycle), move the test-only mergeRemoteClashRulesYAML helper into the test file, and trim oversized comment blocks. --------- Co-authored-by: Duxxie <yelloduxx@users.noreply.github.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
abd320994a |
Add per-client external link controls (#5650)
* Add enable toggle for external client links * Document external link enable API fields * Extend external client link metadata * Fix external subscription cache status updates * fix(sub): address the review on per-client external link controls Blocking: the expiry filter dropped legacy rows. expiry_time was added without a default, so AutoMigrate makes it nullable and backfills NULL, and `expiry_time = 0 OR expiry_time > ?` is false for NULL under three-valued logic — every external link written before the upgrade vanished from all subscriptions. Add `default:0` on expiry_time and last_fetch_at, make the predicate NULL-tolerant, and backfill the NULLs a pre-fix build could already have written. Rework fetch-status recording. It ran inside the singleflight in-flight window, so every goroutine parked on the shared fetch waited for a DB write to commit on the public, unauthenticated subscription path — and because it was keyed on the row id, waiters and cache hits recorded nothing, leaving rows that lost the race stuck on "Not fetched yet" forever. fetchSubscriptionLinks now reports whether it did the network fetch and expandEntry records afterwards, off the serving path, keyed on kind+value so every row sharing the URL is stamped by the one fetch. Keying on value also closes the recycled-rowid hazard: saves delete and re-insert rows, and SQLite reuses rowids, so an in-flight write could land on an unrelated client's row. The write no longer discards its error either. Drop the inert id round-trip. The panel never sent it, and the byId branch was guarded by the exact kind+value equality that byKindValue already keys on, so it could not change an outcome. Matching on kind+value alone is what actually preserves fetch status across saves. Reject a negative expiryTime instead of storing a row that is silently invisible in every subscription — elsewhere a negative expiryTime means "a duration from first use", so an API caller reusing that convention got no error and no links. Drop the ~50 lines of .client-form-* / .client-inbounds-field CSS that no component renders; it is leftover from the WireGuard PR this one was split from. i18n: reuse the already-translated pages.inbounds.leaveBlankToNeverExpire instead of shipping an English duplicate under pages.clients, and translate namePrefix, lastFetchAt, lastFetchError and neverFetched into all 12 non-English locales. Cover the persistence path that had no test: the fetch-status writer over a real DB against a failing then a succeeding server, a cache hit writing nothing, and the negative-expiry rejection. --------- Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com> |
||
|
|
b8903fadf4 |
feat(clients): renew on a calendar day instead of a rolling interval (#6239)
* feat(clients): renew on a calendar day instead of a rolling interval Auto-renew advances the expiry by a fixed number of milliseconds, so a client set to 30 days drifts against the calendar: renewing on 31 January lands on 2 March, and by the end of the year the billing day has wandered a fortnight from where the operator's own plan resets. Add a per-client renewal day. When set, the expiry steps whole calendar months at midnight in the panel's time zone. A month too short for the chosen day renews on its last day and the following month returns to the chosen one, so the 31st does not decay into the 28th permanently. Zero keeps the interval mode, so existing clients are untouched. The interval branch now also refuses a zero step. It is unreachable while the selection filter holds, but that loop runs on the single traffic writer, and a zero interval there would hang every panel mutation behind it. * fix(clients): persist the calendar renewal day on the client record resetDay lived only in the inbound settings JSON and client_traffics, so every path that rebuilds a client from the clients table wrote it back as zero: an ordinary edit, an attach to a second inbound, a traffic reset on a disabled client. Calendar mode turned itself off during normal use and the operator only found out a month later. Adds reset_day to ClientRecord and threads it through ToRecord, ToClient, applyClientRecordMerge and the record update map, so the value survives the round trip. The clients page filter and ClientSlim now recognise the mode, nodeClientRenewed classifies a calendar renewal as a renewal, the node snapshot merge carries reset_day, and the service layer rejects a day outside 0-31 rather than clamping it silently. Also renames the label keys to renewOnDay to keep them apart from the existing renewDays, translates them and the new RESET_DAY subscription placeholder in all 13 locales, adds the field to the bulk-add modal, and drops the stray internal/web/dist/.gitkeep build stub. * fix(clients): let the billing day be changed after creation ClientService.Update writes the record columns directly only for a client with no inbounds. The normal path goes through SyncInbound and applyClientRecordMerge, which this change had not extended, so moving a client from the 20th to the 5th updated the inbound settings JSON while clients.reset_day kept the old value and the renewal kept using it. The existing test did not catch it: it asserted the day survived an unrelated edit, and it survived precisely because nothing on that path ever wrote it. TestClientEditChangesTheBillingDay moves the day and then switches calendar mode off again; removing the record write turns it red. * chore: drop the accidentally committed dist build stub internal/web/dist/.gitkeep is what make dist-stub creates locally. Committing it changes fresh-clone behaviour for everyone: today a bare go build fails loudly on //go:embed all:dist, which is the documented signal to run the stub target; with the file present the build succeeds and the panel serves an empty dist instead. --------- Co-authored-by: n0ctal <n0ctal@users.noreply.github.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
930a0ed59d |
feat(inbound): DisableFlow — opt an inbound out of auto XTLS Vision (#5689) (#5698)
* feat(inbound): add DisableFlow to opt an inbound out of auto XTLS Vision Adds an inbound-level DisableFlow flag so operators can suppress automatic xtls-rprx-vision injection on a specific inbound even when its transport is flow-capable — e.g. a tunneled/CDN-fronted XHTTP+vlessenc inbound where Vision is not wanted, while keeping it on the same client's Reality inbounds. When set, the inbound reports tlsFlowCapable=false, the write path clamps each attached client's flow to empty (so flow_override stores ""), and share links/subscriptions never carry the flow for it. The flag is panel-only metadata and is never sent to xray. Closes part of #5689. * feat(inbound): DisableFlow toggle in the inbound form (frontend) Wire the DisableFlow field through the form schema + adapters and add a VLESS-gated switch in the inbound form, plus en-US strings. tsc --noEmit and eslint pass. * fix(inbound): honor DisableFlow in all emitters + on toggle; regen OpenAPI Addresses review on #5690: - Clash (clash_service.go) and JSON (json_service.go) subscription emitters now also skip the flow for a DisableFlow inbound — previously only the raw share-link path was gated, so those two still advertised it (blocking 1). - UpdateInbound now strips any flow already stored on a DisableFlow inbound's clients (settings.clients[].flow + client_inbounds.flow_override) so xray and the subscription agree; otherwise toggling DisableFlow on an existing Vision client left xray expecting a flow the client no longer sends. - Regenerated the OpenAPI + zod/types/examples artifacts for the new field and added an example tag (blocking 2; make gen-check is clean). - Added Clash + JSON DisableFlow suppression tests alongside the raw-link one. * fix(inbound): make DisableFlow durable, clamp on create, guard live config Addresses the review + completeness audit on #5690: - UpdateInbound now persists inbound.DisableFlow onto the saved row. It was only read to branch strip-vs-restore, so toggling the flag on an existing inbound never stuck and MigrationRestoreVisionFlow re-injected the flow — the exact #5689 path (editing a multi-inbound client's inbound) self-reverted. - DBInbound (frontend) declares + initializes disableFlow so ObjectUtil .cloneProps carries the API value through; the edit Switch previously always read false and re-saving silently reverted the opt-out. - AddInbound strips client flow (settings + parsed clients) when DisableFlow is set, so a created-disabled inbound never persists a flow xray would expect. - GetXrayConfig forces flow="" for DisableFlow inbounds (VLESS + Trojan) as defense-in-depth, keeping the live config and the subscription in agreement. - genTrojanLink share link honors DisableFlow too. - Drop the dead explicit flow_override clear in UpdateInbound (SyncInbound rebuilds it from the stripped settings). - Clear disableFlow in the inbound form when switching to a non-VLESS protocol. - Add disableFlow/disableFlowHelp to the remaining 12 locales. Tests: stripClientFlows unit cases; DB-backed AddInbound clamp; UpdateInbound persist+strip+resist-restore regression (fails without the persist fix); frontend DBInbound + adapter round-trip (fails without the model field). * style(inbound): drop // line comments per repo CLAUDE.md The DisableFlow work followed the surrounding code's commenting style; the repo CLAUDE.md forbids // line comments in committed Go/TS. Remove the comments I added (Go + frontend + tests) and regenerate OpenAPI/schemas, which drops the generated field descriptions sourced from the Go doc comments. No behavior change; full go test (service+sub, CGO) + frontend typecheck/vitest green; golangci-lint clean on the changed files. * fix(runtime): propagate disableFlow to nodes Preserve the inbound DisableFlow flag when syncing inbounds across nodes and when recreating central records from remote traffic snapshots. This keeps multi-node deployments from reintroducing VLESS Vision flow in node configs and share links, and updates the related tests to cover the wired field and VLESS JSON generation. |
||
|
|
f22df49a71 |
fix(sub): restore the subscription info page for browser visits
Revert |
||
|
|
338822ab07 |
fix(sub): keep copy page within mobile viewport
Constrain the copy-only subscription page to the dynamic viewport, wrap long localized text, and infer text direction so mobile browsers cannot render a horizontally shifted desktop-width page. Add regression coverage for the responsive layout contract. |
||
|
|
43bc915397 |
fix(sub): serve a copy-only page when a subscription URL is opened in a browser (#6183)
* fix(sub): show copy-only page for browser subscription visits Browser navigation to /sub previously rendered the normal subscription page, which exposed subscription material in page data or raw base64 depending on request headers. Keep VPN clients on the raw subscription body, but classify browser document requests and return a neutral static copy-only HTML page with no embedded share links or page data. This preserves the C1 LimitIP parser fix in the same master candidate while avoiding a DE rollback of the browser subscription UX. * fix(sub): keep the themed page for an explicit html request Only implicit browser navigation is downgraded to the copy-only page. An operator who appends html=1 or view=html already holds the URL, so the themed subscription page keeps rendering for them and serveSubPage stays in use. * fix(sub): keep browser pages copy-only |
||
|
|
dafd3c0e64 |
feat(sub): warn when salamander settings cannot reach the client (#6177)
* feat(sub): warn when salamander settings cannot reach the client A hysteria2 share link carries obfuscation as obfs=salamander plus obfs-password, and nothing else. Xray's finalmask accepts more than that — packetSize among them — and those extra settings change what the server expects on the wire. The emitted URI then looks complete but describes a server the client cannot reach: every standard client applies plain salamander, the server drops the packets, and the failure is silent on both ends. Log the unexpressible keys when building such a link, naming the inbound, so the cause is visible instead of appearing as a client-side problem. * fix(sub): deduplicate salamander warnings |
||
|
|
2d669fa4b7 | feat(sub): add template variables to subscription metadata (#6163) | ||
|
|
694ad6deae |
feat(sub): add per-client subscription HWID limits (#5802)
* feat(sub): add per-client subscription HWID limits * fix(sub): address HWID review on shared subId and bulk create * fix(sub): store HWID devices by sub_id and drop anchor client workaround * fix(sub): restore UA auto-detect and HTML page routing in subs() The cherry-pick of the HWID gate onto main's refactored SUBController had dropped main's UA-based format auto-detection and sub-page handling from subs(). Restore those branches, slotting enforceHwid after the HTML page and before format detection so the gate only applies to machine-readable subscription bodies. Also adapt tests to main's options-struct constructor and to the ClientService.Update signature extended with limitHwid. * fix(frontend): drop axios from HttpUtil.delete The bulk-delete rework's committed version still referenced axios, which this file no longer imports, breaking typecheck in CI. Use the httpRequest wrapper like the other verbs. --------- Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
2217213e9f |
feat(sub): expose last subscription fetch time (#6217)
* feat(sub): expose last subscription fetch time Record successful GET subscription fetches per client and surface the timestamp in the client API and UI. * fix(frontend): include last subscription fetch in client traffic * Update sub_fetch_test.go --------- Co-authored-by: Hermes Agent <hermes-agent@localhost> |
||
|
|
ad32144c42 |
fix(sub): use a fullwidth percent in USAGE_PERCENTAGE (#6174)
* fix(sub): use a fullwidth percent in USAGE_PERCENTAGE A remark is placed in the share link fragment, so an ASCII percent is percent-encoded to %25. Happ treats such a fragment as malformed, discards the whole remark and falls back to showing the server hostname, which defeats the point of a remark template and leaks the host into the client's server list. Emit U+FF05 FULLWIDTH PERCENT SIGN instead. It renders the same to a reader, never produces %25, and round-trips through url.Parse unchanged. * test(sub): exercise production fragment encoding --------- Co-authored-by: n0ctal <293235942+n0ctal@users.noreply.github.com> |
||
|
|
3b19091547 |
fix(sub): render the full remark once per subscription, not once per credential (#6198)
* fix(sub): scope full remarks to subscription identity * fix(sub): preserve configured remark whitespace --------- Co-authored-by: n0ctal <293235942+n0ctal@users.noreply.github.com> |
||
|
|
20b3f84f77 |
fix(web): report unexpected HTTP serve failures (#6210)
* fix(web): report unexpected HTTP serve failures * test(web): cover normal close and all HTTP servers --------- Co-authored-by: n0ctal <293235942+n0ctal@users.noreply.github.com> |
||
|
|
c56f6447a8 |
chore: refresh dependencies and modernize Go test idioms
Frontend deps: @hookform/resolvers 5.4.3 -> 5.5.7, Storybook 10.5.4 -> 10.5.5 across the four packages we declare, globals 17.7.0 -> 17.8.0, and jsdom 29.1.1 -> 30.0.1. The jsdom major replaces its CSS and selector stack -- @asamuzakjp/css-color 5 -> 6, @asamuzakjp/dom-selector 7 -> 8, undici 7 -> 8, nwsapi and generational-cache folded into their parents, whatwg-url 17 nested underneath. Nothing in the Vitest suites reaches those directly and the whole frontend gate (typecheck, lint, tests, build, Storybook compile) is green. Panel frontend version to 0.6.0. Backend deps: mattn/go-sqlite3 1.14.48 -> 1.14.49 and valyala/fasthttp 1.72.0 -> 1.73.0, plus the golang.org/x/exp and genproto/googleapis/rpc indirect bumps that came with them. Go tests: modernize -fix output, covering range-over-int, sync.WaitGroup.Go in place of manual Add/Done pairs, maps.Copy, and Go 1.26 new(expr) for pointer-to-value in the forwarded-trust table. The storedAs helper is deleted instead of being left behind a //go:fix inline directive -- keeping it that way fails govet on the one call site the rewrite did not reach, and every caller now takes new(...) directly. Behaviour is unchanged. DnsTab: the hosts-sync effect tested dns while declaring dnsEnabled in its dependency array. Both carry the same truth value, so this is exhaustive-deps hygiene rather than a behaviour change. |