mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 19:25:05 +03:00
1c748adadfd05aca2febfdbaadb3730bff249ac9
169 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1fa8cc785d |
fix(outbound): keep imported links to remote-server settings
A share link describes a remote server, but extra=, fm= and the vmess JSON copy provider objects verbatim, so a link could carry keys that only make sense on the importing host. Both link parsers now drop dial-redirect and host file keys (matched case-folded, as the core binds them). Outbound subscriptions also refuse outbounds that dial this host or its network without allowPrivate, any that target the core's own API or metrics port, and any that take their server from DNS records. Rows stored earlier are filtered the same way when the config is built. |
||
|
|
7f82daa42f |
feat(finalmask): tlshello fragment presets for client editors
The host, JSON subscription and outbound mask editors now offer PattN's two chained tlshello fragment presets; a pick replaces the TCP mask list. The fragment length validator rejected a 0 minimum in every entry, but xray-core only refuses it in the last lengths entry. An earlier 0 is an empty segment, which the tlshello-0-len preset relies on. |
||
|
|
c3d4a153b3 |
fix(sub): pin fp=unsafe on links that carry cipher suites
Xray builds the ClientHello with uTLS for every fingerprint except
"unsafe", and GetFingerprint("") resolves to chrome, so cipherSuites are
dropped unless the fingerprint is exactly "unsafe". A host or inbound
that set cs and left the fingerprint blank shipped links whose cs did
nothing in v2rayN, PattN and every other Xray-based client.
Raw links (query and vmess JSON), the JSON subscription and the panel's
own link generator now emit fp=unsafe when cs is present and the final
fingerprint is empty. An explicit fingerprint is left as configured.
|
||
|
|
56ac8d0fc7 |
feat(amneziawg): add opt-in Linux kernel datapath (#6877)
* feat(amneziawg): add opt-in Linux kernel datapath Add the kernel backend as an explicit per-inbound option while keeping the embedded backend as the default. Include host preflight, UI/API support, the bundled installer CLI, lifecycle and accounting integration, regression tests, and upstream-facing documentation. Kernel mode is NAT-only and rejects unsupported routing, IPv6, forwarding, and pinned-listen configurations. Local saves check the local host; remote nodes check their own host during apply. Preserve adopted interface accounting across panel restarts, retain manager state after failed teardown so the job can retry, prevent embedded startup while a kernel interface remains live, and prune collector state for removed peers. Tested the restart accounting path on Debian 13 with XanMod 7.1.11 on x86_64; Docker and cross-kernel coverage are not claimed. * fix(amneziawg): align readiness and endpoint reporting Expire endpoint sightings using the latest handshake and sweep stale addresses for the polled interface. Keep the readiness API schema, generated client types, and ready-host empty blocker list consistent; watch the nested form field and defer local readiness checks for remote targets. Document safe rollback before using a build without kernel support. Regression evidence: the idle endpoint test failed before the collector change because the old address remained visible after 31 minutes; endpoint tests pass after the fix. The form component suite passes 3 tests; restoring the invalid array watch made its off-mode severity test fail, while the corrected dot-path watch passes. Targeted Go regressions across amneziawgkern, job, and service pass. * style(amneziawg): format readiness test * test: reproduce kernel AWG client validation gaps * test(service): cover kernel AWG shared client fanout * fix(amneziawg): validate kernel client fanout and config values * test(amneziawgkern): cover owned interface lifecycle recovery * fix(amneziawgkern): recover owned interfaces on restart * test(amneziawgkern): retain visibility after recovered cleanup failure * fix(amneziawgkern): report recovered interfaces while cleanup retries * style(amneziawgkern): satisfy static analysis * test(amneziawgkern): cover long rendered peer comments * fix(amneziawgkern): accept long generated config comments * test(amneziawgkern): refuse foreign occupied interfaces Capture the current Ensure ordering failure with real temporary config paths and manager side-effect boundaries. * test(amneziawgkern): cover stale interface collisions Extend the Ensure regression across absent links, mismatched live interfaces, cached manager state, and Remove teardown. * fix(amneziawgkern): verify interface ownership before mutation Refuse to replace or tear down links unless the canonical config is a valid panel-owned file and its key matches the live interface. Preserve normal adoption and key rotation, and document migration of legacy unmarked configs. Validation: go test ./internal/amneziawgkern -count=1; go test -race ./internal/amneziawgkern -count=1; golangci-lint run ./internal/amneziawgkern/...; git diff --check. * build: update Go toolchain to 1.27.3 * test(amneziawg): reproduce unsafe key rotation recovery Keep the previous owned config authoritative until teardown succeeds, and require fresh managers to rebuild structural key changes. These regressions expose the stuck live-key/disk-key mismatch after teardown failure. * fix(amneziawg): recover interrupted key rotations Keep the old owned config on disk until a structural rebuild has confirmed the previous interface is down. Fresh managers only adopt when the live config and desired structural identity match, so retries and removals retain the key needed to prove ownership. * fix(scanner): align Go image with module version * fix(docs): pin pnpm release with bootstrap fix pnpm 12.9.1 restores a Node launcher before its postinstall replaces the target with ELF; pnpm 11 then runs that binary as JavaScript. Pin 12.10.0, which includes the shim fix, so the docs CI bootstrap can switch versions safely. * fix(xray): build core from pinned module The configured Xray release asset tag is unpublished, and the latest published binary lacks GetUsersStats used by the panel. Build the packaged core from the exact Xray module version already selected in go.mod for Docker and release targets. * fix(tests): stabilize local network fixtures Keep the TUIC stream fragmentation check on a working UDP loopback path, allocate the AmneziaWG fixture ports dynamically, and wait for a real Xray stats response before starting API mutation assertions. Recreate failed readiness channels so an early connection cannot stall the probe. Verified host and offline parallel Go suites, repeated occupied-port and UDP tests, race/shuffle runs, and golangci-lint. Production behavior and dependency versions are unchanged. * fix(i18n): add the kernel-mode AmneziaWG keys to sk-SK main gained the sk-SK locale after this branch added its kernel-mode keys to the other 13, so i18n-dead-keys.test.ts failed on the merged tree. * refactor(amneziawgkern): apply kernel interfaces with direct commands, no awg-quick awg-quick evaluates PostUp/PostDown through a root shell, so every value the panel rendered into its config was one validation slip away from root command execution. The interface is now built with direct ip/awg/nft execs from a setconf-only file: link add, awg setconf, address, mtu, an atomically replaced nftables table per inbound, and the IPv4 forwarding sysctl written directly. - Panel-owned names and paths: xui-awg<id> interfaces, xui_awg<id> NAT tables and configs under the panel's data dir, so nothing collides with an operator's own awg0/awg1 awg-quick tunnels in /etc/amnezia/amneziawg. - FORWARD accepts (comment 3x-ui:xui-awg<id>) when iptables is present: Docker and ufw set the FORWARD policy to DROP, which silently black-holed the tunnel. - A full panel stop records the final counters and tears the interfaces down, like the embedded datapath; a stopped panel enforces no quota or expiry. Crash survivors are still adopted without a rebuild. - The job reads counters before reconciling, and from every managed interface, so a client disabled for quota, or a removed inbound, is charged for its last interval instead of losing it with the dropped peer. - An unchanged reconcile tick forks no process (it probed the live key with `awg show` every 10s); readiness no longer loads the module from a GET. Verified on the real module (WSL2 6.6 kernel, v3.1.20260906): create, peer and egress changes in place, SIGKILL adoption with the same ifindex, SIGTERM teardown leaving no link, table, FORWARD rule or config behind. * fix(amneziawg): gate kernel host readiness only when a save starts the datapath Every save of a kernel-mode inbound re-ran the host preflight, so a host that lost its module (a kernel upgrade without a DKMS rebuild) refused even disabling the inbound or switching it back to embedded mode. Host blockers now apply only when the save starts the kernel datapath: a new inbound, a switch from embedded, or enabling a disabled one. Feature blockers (IPv6, forwarded ports, pinned listen) still apply to every save. * feat(nodes): refuse kernel-mode AmneziaWG pushes to nodes that predate it A node built before kernel mode drops the unknown kernelMode field and serves the inbound on the embedded datapath while the master's form shows kernel mode on. Nodes now advertise awg-kernel in X-3x-Node-Caps, and Remote refuses to push a kernel-mode inbound to a node that has not advertised it, learning the caps with an inbound list first when this Remote has not heard from it. * fix(amneziawg): pin the kernel module installer to verified commits The installer ran as root and built whatever upstream tag sorted newest at install time. It now clones fixed tags (module v3.1.20260906, tools v3.1.20260812) and refuses unless each resolves to its recorded commit. The udp_tunnel perl patch is gone: those releases carry the kernel 7.1.5 compat upstream, so the patch never matched. * fix(i18n): translate the kernel-mode AmneziaWG strings All thirteen non-English locales carried the English text verbatim. * fix(amneziawg): skip the kernel traffic flush when no kernel interface runs Shutdown listed the desired AmneziaWG inbounds from the database even on a panel with no kernel interface, an unneeded query on every stop. A server stopped before its database was opened panicked on it, which turned TestServerStopWhenXrayNotRunningSucceeds red whenever it ran first. --------- Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com> |
||
|
|
8da3b3c344 |
feat(hosts): give a host a pool of SNIs, random per config or shared (#6923)
Rotating SNIs meant one host entry per domain. A host now takes a pool of SNIs: with more than one, every generated config gets a random entry, or, with the shared switch on, the whole host group uses one pick per subscription fetch. Raw, JSON and Clash output, the panel's link preview and node adoption all carry the pool. sni stays the pool's first entry, so a rolled-back binary and API callers that only know the single field keep working; an sni outside the pool becomes its primary. A host with one SNI renders exactly as before. |
||
|
|
2b759f3916 |
feat(sub): export TLS cipher suites as the cs query param in share links (#6825)
Hosts and TLS inbounds can set cipher suites, but they reached only the JSON and Clash subscriptions; raw share links never carried them, so clients that read cs from links (v2rayNG forks used with fp=unsafe) had to enter them by hand. Share links now emit cs from the host or inbound TLS settings, the panel's own link generator and previews do the same, and both link parsers (the Go one and the panel's outbound import) read cs back into tlsSettings.cipherSuites. |
||
|
|
35e09191de |
feat(amneziawg): support persistent keepalive ranges (#6881)
AmneziaWG clients accept a randomised PersistentKeepalive range such as 15-25, which makes keepalive timing harder to fingerprint; the panel only stored a single number. A client's keepalive may now be a range, validated the way AmneziaWG parses it. The downloadable .conf and vpn:// profile carry the range, Clash/Mihomo output gets its lower bound, and plain WireGuard stays scalar-only. The range lives in an additive wg_keep_alive_range column, so the integer column and an older panel binary keep working after a rollback. |
||
|
|
f43645656c |
feat(tls): add a dynamic DoH helper for echConfigList, with CDN guidance (#6869)
echConfigList already accepted Xray's dynamic form, target domain + DoH URL, but nothing in the inbound or host forms explained or built it. A helper now builds that value from presets or a custom domain and resolver, accepts only the schemes Xray dials (https, h2c, udp; scheme case normalised), warns that a key generated locally does not work behind a CDN, and asks before overwriting local ECH keys. The helper's controls carry accessible labels, and the resolver list holds only identified public resolvers. |
||
|
|
40cea0c2fe |
feat(sub): generate random IPs from CIDR host addresses (#6784)
Operators who front a server with an IP range (a CDN or anycast prefix) had to list each address as its own host. A host address may now be a CIDR, and every subscription fetch expands it into up to randomCount (1–100, default 1) unique random host IPs from the range, IPv4 and IPv6 alike, skipping the network and broadcast addresses. The panel's own previews, QR codes and tunnel config exports (WireGuard, AmneziaWG, TUIC, MTProto) expand it the same way, and the externalProxy importer writes the model's default count so the re-sync still recognises untouched imported hosts. |
||
|
|
0faad4f4f3 |
fix(wireguard): check every line of generated client configs
The WireGuard and AmneziaWG .conf builders (subscription service, clients page, inbound modal and subscription page) checked only a few of the values they write. Each builder now assembles the file line by line and renders nothing when a line holds a control character, so every value is covered whatever stored it. A decoded vpn:// payload is refused when it carries a wg-quick hook directive, which no panel builder writes, and the subscription page hides a config block that rendered nothing. The inbound form now validates a WireGuard/AmneziaWG client's allowedIPs on create and on update, including the stored clients a protocol switch keeps, as the client endpoints already did. |
||
|
|
e2184777a0 |
fix(frontend): make the panel usable with screen readers (#6907)
* fix(frontend): make the panel usable with screen readers * fix(frontend): keep mobile tab names and the inbound expiry hint Review of #6907 found two gaps in the screen-reader pass: - On phone-width screens catTabLabel renders only the icon and names the tab by putting aria-label on it, but callers now pass that icon with aria-hidden for the desktop label, so every Settings, Xray and Host category tab lost its name under TalkBack. catTabLabel now clears aria-hidden on the icon it uses as the name. - DateTimePicker forwarded id and aria-label but not aria-describedby, so the inbound expiry hint ("leave blank to never expire") was never attached. It now forwards it on both the Gregorian and Jalali input. |
||
|
|
61b129d5b3 |
feat(xray): update xray-core to v26.10.21 and adapt panel
Bump xtls/xray-core to 1b1647cd9ddf (v26.10.21) and move the three binary pins (DockerInit.sh, both release.yml fetches) in lockstep. The v26.10.10 release was pulled upstream, so the old pins already 404. XDRIVE (#7182): the core now runs XDRIVE only for VLESS inside VLESS encryption, with no flow, and an outbound only over Mux; it refuses the whole config otherwise. The panel offers XDRIVE only for VLESS, refuses to save an inbound without decryption, never emits a client flow on an XDRIVE stream (config, JSON subscription, forms), and turns Mux on when an outbound picks XDRIVE. An inbound or template outbound stored before the upgrade that the core would refuse is left out of the generated config with a warning, so it no longer stops every other inbound. The service "Google Drive" is now "google-drive" (old name read as a deprecated alias); saves, the schema and both form adapters store the new name. XDRIVE and MASQUE gain a client xmux; both schemas keep it instead of stripping it on an inbound save. Lua scripts (#7124, #7136): the core refuses a routing script beside rules or domainStrategy, and a dns script beside the fallback/parallel switches or a domain-scoped server. The panel always keeps its api routing rule, so a template save now refuses any routing script and a dns script in a refused combination, and a stored one is dropped from the generated config so the core still starts. Noise masks gain a Lua script that the loader compiles inside Build: the in-process build (template save check, hot add) now resolves a relative script the way the xray child does, beside the xray binary, instead of failing it. The mask editor gets a Script field that replaces the noise list. XDNS (#7140): a resolver takes https:// DoH addresses with worker, tlsConfig and quicConfig. The mask editor registered only addrs, so a save dropped the rest; it now edits all four. Finalmask filter (#7206): a new TCP/UDP mask screens an inbound's source IPs. The inbound mask editor offers it with its allow and block lists; it is a no-op on a dialer and an older client core refuses the type, so the share link and JSON subscription drop it like the Go link already does. |
||
|
|
ff3f42ee2a |
fix(link): read fm and the mKCP fields from vmess links (#6809)
#6480 taught the vless/trojan importers to rebuild the mKCP header and seed and left the vmess importers ignoring fm. The vmess emitter writes the finalmask as fm, the mKCP header as type and the seed as path, plus mtu/tti. Both importers dropped all of them, so a vmess mKCP inbound with a seed, a header or any finalmask UDP mask (salamander, ...) came back from an outbound subscription, a client external link or the outbound form's link import as an outbound that cannot connect. Feed those fields through the same finalmask and kcp helpers the url-param protocols use, in the Go and TS importers. |
||
|
|
6f1dd0d757 |
feat(sponsors): drop the sidebar slot and simplify the Sponsors page
The sidebar no longer carries a sponsor card. The backend stops serving the "sidebar" slot, so a sponsors.json entry that still lists it keeps only its other slots and one listing nothing else is dropped. Its rotation, three-sponsor cap and collapsed-rail logo rendering go with it. The Sponsors page now lists only real sponsors: the "Your brand here" card shows only as the empty state instead of sitting in the grid and repeating the header's Become a sponsor button, and the Available placements section and its slot-occupancy helper are removed. Cards are larger, two per row on wide screens. Unused i18n keys are dropped from every locale. |
||
|
|
c5e1e54c75 |
fix(xray): keep XDRIVE storage and Vision flow within what xray-core runs
xray-core opens an XDRIVE stream's storage only when the inbound starts listening, so a Google Drive inbound saved with the form's defaults (no folder, no secrets), a local one without a folder, or a template one whose template xray cannot use stopped xray and every other inbound with it. validateXDriveStorage applies the core's storage rules at save, on inbound add/update and on the xray template's inbounds and outbounds: the service must be local, Google Drive or template; local and Google Drive need a remoteFolder; Google Drive needs its three non-empty secrets; a template must pass newTemplateStorage's checks (put, get, list and delete urls, and a list namesRegex with one capture group). A VLESS client kept its xtls-rprx-vision flow when its inbound moved to a transport that cannot carry it, and GetXrayConfig only honoured DisableFlow, so the server demanded Vision there and refused every client. GetXrayConfig and the outbound form now apply the Vision gate, and that gate now follows the core in the backend, the subscription service and the frontend: TLS/REALITY on raw TCP, or VLESS encryption on any transport (xray runs Vision inside encryption.CommonConn whatever the transport, not only on XHTTP). The outbound modal passes the VLESS encryption into the gate and gates Mux on the effective flow. |
||
|
|
c20082d254 |
refactor(inbounds): drop the single-user shadowsocks inbound paths
isSSMultiUser treated 2022-blake3-chacha20-poly1305 as a single-user inbound, and the inbounds page grew a row QR modal, a separate link panel in the info modal and a client-count skip for that case. The method is no longer allowed on inbounds, so every shadowsocks inbound carries clients and those paths cannot be reached: remove them along with the inbound-level QrCodeModal they alone opened. |
||
|
|
f1ed1a9b17 |
fix(inbounds): refuse 2022-blake3-chacha20-poly1305 on shadowsocks inbounds
xray-core builds a Shadowsocks-2022 inbound with users only for the AES methods: the multi-user identity header is AES-only, so the config builder and the multi-user server both reject chacha20. Every panel inbound carries clients, so picking 2022-blake3-chacha20-poly1305 in the inbound form kept the whole core from starting; its links also failed on the client with "multi-key is not supported". The inbound form no longer offers the method and AddInbound/UpdateInbound reject it. Startup moves stored inbounds to 2022-blake3-aes-256-gcm, which takes the same 32-byte keys, so every stored key stays valid. Outbounds keep the method, since a single-key outbound to such a server works. The share-link generator drops the single-user branch that only this method reached. |
||
|
|
0466e8b0bd |
fix(finalmask): refuse a salamander password xray-core cannot open
xray-core needs a salamander (and gecko) password of at least 4 bytes (smPSKMinLen), but checks it only when a socket opens: xray run -test reports Configuration OK. On an inbound the listener then fails and Xray does not start at all, taking every other inbound down; on a client the dial fails. The panel saved any password, and the finalmask editor starts a new salamander mask with an empty one. maskcompat.CheckSalamanderPasswords applies the core's byte rule, pinned against the core's own ListenPacket, and every path that stores a finalmask refuses a short password: inbound add and update, hosts, the subscription's global finalmask, and the inbounds and outbounds of the Xray template. The editor flags it inline as it is typed. |
||
|
|
6321297f68 |
fix(finalmask): keep a single udphop mask last in every client finalmask
xray-core reverses finalmask.udp and refuses, at dial time, a mask that dials its own socket (udphop) anywhere but the socket end: "incorrect index". xray run -test still says Configuration OK, so the failure only shows as a client that never connects. UDPHopClientMaskFix turned a global subJsonFinalMask hop and a host hop into udphop masks; the JSON subscription then appended both, leaving two hoppers with the first one misplaced, and a host mask merged after a global hop pushed it off the end too. A hand-ordered template outbound or an imported fm= could carry the same order. maskcompat.NormalizeUDPHop keeps the last-listed udphop alone at the end of udp, and runs wherever a client finalmask is produced: each JSON subscription endpoint, share-link fm=, the Go and frontend link importers and the template outbound heal in GetXrayConfig. The JSON subscription resolves competing hop ranges most specific first: a host hop wins over the inbound's own, which wins over the global default. |
||
|
|
c7d89ceac2 |
feat(outbounds): TLS for XDRIVE outbounds
xray-core 26.10.10 dials an XDRIVE outbound's storage API with the
stream's tlsSettings: their SNI, uTLS fingerprint and ALPN are what the
API sees, which is the point of fronting it. canEnableTls kept XDRIVE out
of the TLS-capable networks, so the security picker never offered TLS and
switching a TLS outbound to XDRIVE silently reset it to none.
canEnableOutboundTls admits a TLS-eligible protocol on xdrive and backs
both the picker and the network-change cascade. Inbounds keep the plain
predicate: their tlsSettings carry server certificates, not a client hello.
Also restores the plain oxfmt / oxlint --fix lint-staged commands, reverting
|
||
|
|
1c050f872a |
feat(finalmask): header-custom variables, sudoku UDP mask and noise reset ranges
xray-core 26.6 grew the finalmask surface the editor could not express, and the finalmask watch drops any key without a registered field, so a config written by hand lost these keys on the next save: - header-custom items can capture their bytes into a variable, reuse one or emit a transform expression; a Kind select keeps each item to exactly one kind, as the core requires, and only parsed JSON reaches transform; - TCP header-custom gains the errors group (the reply sent when a client group does not match), UDP header-custom its prefix/standalone mode; - sudoku is offered as a UDP mask, and share links keep it in fm=; - the noise reset is an Int32Range, so it now takes "60-120" as well. The new golden fixture is saved unchanged by the outbound editor and built by xray-core in TestGoldenStreamFixturesBuildInXray. |
||
|
|
ec0a57fc4f |
fix(finalmask): hop client UDP through the udphop mask
xray-core 26.9.9 dropped finalmask.quicParams.udpHop for the client-only
"udphop" UDP mask {mode, interval, remoteIPs, remotePorts}. The panel kept
writing the old key, so hysteria/xhttp outbounds and JSON-subscription
clients silently stopped hopping ports.
An inbound keeps quicParams.udpHop: it is panel metadata advertised as the
link's mport, and a udphop mask on a listener fails with "udphop: client
only". The conversion (maskcompat.UpgradeLegacyUDPHop, intervalRemote mode)
runs only where a client config is produced:
- UDPHopClientMaskFix seeder over hosts, sub-JSON finalmask, cached
outbound-subscription outbounds and template outbounds (never inbounds);
- GetXrayConfig heals template outbounds at build time;
- the JSON subscription upgrades each generated client stream;
- the Go and frontend link importers upgrade a legacy fm= param.
The finalmask editor gains a side prop: the client side lifts the dead key
on mount and offers a udphop mask editor (also for xhttp's HTTP/3 dial);
the inbound side keeps the UDP Hop switch, labelled as advertised.
|
||
|
|
b04039c2bd |
feat(xray): add the MASQUE protocol and transport
xray-core v26.10.10 serves MASQUE: CONNECT-IP over HTTP/3 or HTTP/2, always behind TLS, as an inbound and an outbound protocol riding a dedicated "masque" transport. The panel could not create either. Inbound: MASQUE is a multi-user protocol whose users log in with their email and password over Basic auth. The panel keeps the client's "password" like trojan and GenXrayInboundConfig hands it to the core as "pass", so the full-config path, the live AddInbound path and node pushes all emit what the core reads; one user with an empty pass makes the core refuse the whole inbound. Live user adds build a masque.Account from either key, client-only changes diff per user so a new client does not drop every connected tunnel, and client creation, copy, validation and edits key on the password. The address pool and MTU get form fields, TLS is mandatory, and the port check follows the core's listener choice: HTTP/2 on TCP when the ALPN offers h2, HTTP/3 on UDP when it offers h3 or not h2. Nodes may host MASQUE from v3.9.1, the first panel release that knows to rename the password. MASQUE has no share-link format, so links and Clash skip it; the JSON subscription builds the client's masque outbound with its email and password, and the subscription allowlist now lets a subId see MASQUE inbounds at all. The IP-limit job, client pages and traffic tracking list the protocol. Outbound: the form edits the server, remote DNS, and the transport's host, path, Basic credentials, headers and WARP block (enrolled key, endpoint public key, tunnel addresses), which xray-core refuses next to user/pass. The outbound test probes it through the core, since its default HTTP/3 dial is UDP. |
||
|
|
ca5983595b |
feat(xray): add the XDRIVE transport
xray-core v26.10.10 tunnels a stream through files in shared cloud
storage ("network": "xdrive"): Google Drive, a local folder, or any HTTP
storage API described by a template. The panel could not select it, and
the stream schema rejected a pasted xdrive stream outright.
The inbound and outbound forms now offer XDRIVE with one shared editor:
service, folder, the three Google Drive secrets in the order the core
reads them, a template JSON editor with its {secretN} values, the
optional domain front (the stream-level address/port the storage client
dials), and the core's tuning knobs with their defaults as placeholders.
Switching an inbound to XDRIVE resets security to none, because the
core would otherwise read the inbound's TLS/REALITY as the storage API's
client TLS.
XDRIVE has no share-link format, and a link carrying the storage
secrets would print them into every QR code, so both link generators
emit none for it; the JSON subscription already passes the stream
through intact, and Clash drops the unknown transport.
|
||
|
|
73a028f52d |
feat(xray): update xray-core to v26.10.10 and adapt panel
Move the three Xray binary pins (DockerInit.sh, both release.yml
fetches) to v26.10.10, in lockstep with the xtls/xray-core module that
|
||
|
|
66234315e4 |
fix(panel): default TLS ALPN to http/1.1 for WebSocket transport (#6787)
* fix(panel): default TLS ALPN to http/1.1 for WebSocket transport WebSocket bootstraps over HTTP/1.1: leaving ALPN at the schema default ['h2','http/1.1'] makes the server negotiate h2 and the WS handshake fails. See issue #6782. * style(frontend): keep comments within the two-line limit * style(frontend): keep comments within the two-line limit |
||
|
|
d7da64f2f0 |
fix(qr): hide the QR only for links carrying post-quantum keys
A share link's QR is suppressed only when it carries a post-quantum key
payload too large to scan: an ML-DSA-65 verify key (pqv) or an ML-KEM-768
VLESS-encryption auth key. isPostQuantumLink substring-matched "mlkem768"
anywhere in the URL, so it misfired on:
- every VLESS/Trojan REALITY link, since
|
||
|
|
0054e671f8 |
feat(tuic): implement native in-process Go TUIC v5 server (#6577)
* feat(tuic): implement native in-process Go TUIC v5 server - Implement native TUIC v5 protocol server on pure Go using quic-go - Bridge decrypted TCP/UDP traffic into Xray-core via loopback SOCKS5 inbound - Support full Xray routing rules (geosite/geoip) and cascading outbounds - Implement atomic per-client traffic accounting with TotalGB and ExpiryTime - Add automatic legacy cleanup for older Rust tuic-server binaries, configs, and orphaned processes - Eliminate external Rust tuic-server downloads from install/CI scripts * fix(tuic): address traffic accounting, client reload, and socket lifecycle issues * fix(service): update checkTuicSocksReverseConflict to use bindAddr for listenOverlaps * fix(tuic): resolve traffic double-accounting, UDP fragmentation, and socket lifecycle issues * feat(tuic): complete native Go integration and address audit findings - Integrate an isolated QUIC fork pinned to a specific commit - Preserve original QUIC dependencies for Xray, Hysteria and Gin - Apply BBR, CUBIC and Reno to server connections and exported client profiles - Bridge Xray BBR with correct monotonic time and congestion type conversions - Handle congestion sender recreation after PMTU changes - Update congestion control for new connections without restarting the listener - Preserve existing connections and their selected congestion controller - Apply per-inbound log levels through the shared panel logger - Add lifecycle, authentication and TCP/UDP relay events without exposing secrets - Rate-limit repeated authentication and relay warnings - Support native and QUIC UDP relay modes on the same listener - Recover UDP associations after relay worker failures - Fix TCP relay cancellation, idle shutdown and half-close handling - Close active sessions when client credentials are revoked or disabled - Track traffic by immutable client statistics IDs across email and UUID changes - Prevent ambiguous accounting and duplicate UUIDs within TUIC inbounds - Persist pending traffic in a durable shutdown journal - Replay journal batches transactionally without duplicate accounting - Report server shutdown failures through the shared logger - Preserve legacy flat and nested TUIC settings compatibility - Normalize congestion controller values consistently across backend and frontend - Preserve controller, UDP mode and SNI in client links and subscriptions - Separate client profile options from server settings in the TUIC form - Keep certificate path autofill explicit when changing client SNI - Align UDP packet size validation with protocol limits - Simplify and localize TUIC field hints and certificate autofill messages - Add controller, TCP/UDP, logging and live settings update tests - Add accounting identity, journal replay and shutdown regression tests - Add relay recovery, session revocation and legacy frontend form tests * fix(service): alias the TUIC duplicate-UUID subquery for PostgreSQL < 16 syncInboundClients runs a COUNT(*) FROM (subquery) for every client sync, whatever the protocol. PostgreSQL before 16 rejects a FROM subquery with no alias, so on the distro PostgreSQL install.sh provisions (14 on Ubuntu 22.04, 15 on Debian 12) every client add or edit failed with SQLSTATE 42601. Reproduced against postgres:15 with the new env-gated test. * fix(database): create tuic_traffic_receipts through the model migration AddTuicTrafficBatch issued CREATE TABLE IF NOT EXISTS at runtime, a schema change outside db.go. The table was invisible to allModels and migrationModels, so x-ui migrate-db dropped the receipts and a retained journal could be counted twice after a SQLite to PostgreSQL move. It is now a GORM model in both lists, and the insert uses OnConflict DoNothing. * chore(tuic): skip the ICMP-dependent relay test on Windows, drop dead collectors Go disables SIO_UDP_CONNRESET on Windows, so a dead UDP bridge never fails a read there and TestAudit3UDPAssociationMustRecoverAfterBridgeReadFailure was red on every Windows run. Server.CollectTotalTraffic and Manager.CollectTraffic had no caller. * refactor(tuic): serve TUIC on apernet/quic-go instead of a personal fork The native server depended on github.com/poise52/quic-go, a personal fork of apernet/quic-go patched only to pick the congestion controller before the handshake. That put a second QUIC/TLS stack in the binary that no upstream security fix reaches. apernet/quic-go is already in the graph through xray-core and exposes SetCongestionControl, so BBR is now installed on each accepted connection with Xray's own congestion.UseBBR; the cross-module BBR adapter is gone. apernet ships New Reno as its only built-in sender, so a cubic setting is served as new_reno server-side (clients still get cubic in their profile). The test inspectors now read the sender under congestionMutex, which the post-handshake install writes under. Linux loopback, single stream through Xray: 2428 -> 3383 Mbit/s (bbr). --------- Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
93847dd106 |
fix(sub): keep the spider settings in a reality spiderX seed's query (#6694)
* fix(sub): keep the spider settings in a reality spiderX seed's query xray's REALITY client reads p, c, t, i and r from the spiderX query as its spider's own settings (padding, concurrency, times, interval, return). deriveSpiderX hashes the whole seed into a bare /path per client, so any query set on the inbound was dropped from every share link and JSON subscription, and the spider always ran with defaults. Keep the seed's query after the derived path. The hash input is unchanged, so every existing client's spx stays the same; only seeds that carry a query gain it. The frontend mirror and the cross-language vectors are updated together. * docs(sub): keep the deriveSpiderX comments within two lines Review feedback on #6694: the added lines pushed both doc blocks past the two-line cap. |
||
|
|
ed31ee432c |
feat(inbounds): deploy AmneziaWG, TUIC and MTProto inbounds to nodes
The node gate assumed a node-assigned sidecar row would never converge,
because the master's reconcile loops only read node_id IS NULL rows. But
a pushed row is local on the node's own panel, whose AmneziaWG, TUIC and
mtg loops run it like any other; node-adopted rows of these protocols
already worked. Only creating or cloning them from the master was blocked.
Open the three protocols on both lists and fix what assumed the master's
host for a node row:
- A node older than the release that introduced the protocol (MTProto
v3.5.0, AmneziaWG v3.7.0, TUIC v3.8.0) would hand it to Xray as-is, so
add and protocol-changing update refuse it, and a node that has not
reported its version yet. Dev builds ("dev+<sha>") track main and pass.
- MTProto's routeXrayPort is a loopback port on the host running mtg.
The master no longer allocates one, nor forwards a cloned source's, for
a node row; the node allocates its own and node sync adopts it back.
- AmneziaWG forwardedPorts were checked against the master's inbounds,
web port and id-derived relay ports. A node row is now checked against
its own node's inbounds; the node re-checks what only it knows.
UpdateInbound restores the stored nodeId before that check.
Verified on a docker master+node pair: AWG, routed MTProto and TUIC
created and cloned from the master start on the node (interface, mtg,
tuic-server); an AWG client added and an MTProto client edited on the
master apply on the node; the node-chosen egress port survives edits.
Closes #6306
|
||
|
|
721de5adde |
Fix fragment exports for older Xray clients (#6702)
* Fix fragment exports for older Xray clients * fix(link): tolerate a finalmask without tcp in panel share links withLegacyFragmentRanges called finalmask.tcp.map unguarded, but stored rows reach the link generator unparsed and dropEmptyFinalMask deletes an empty tcp list on save. Any VMess/VLESS/Trojan/SS inbound with only UDP masks or quicParams threw a TypeError in the QR, info and export-links views. The Go counterpart already skipped a missing tcp. Also trims the Go helper's comment to the two-line cap and drops a []string branch no JSON-decoded finalmask can reach. --------- Co-authored-by: Artem K <a.kush@vkteam.ru> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
ce221c33d0 |
fix(sub): carry REALITY ML-KEM hint in VLESS links (#6712)
* fix(sub): carry REALITY ML-KEM hint in VLESS links Keep raw share links in parity with Clash subscriptions for Xray 26.9.8+. Preserve the URI hint through Go and frontend imports, expose it in the outbound editor, and update the documentation tooling. * fix(link): accept REALITY ML-KEM boolean aliases * test(frontend): isolate Happ preset notifications * fix(link): keep the ML-KEM hint out of Xray REALITY settings support-x25519mlkem768 is a Mihomo reality-opts option; xray-core's REALITYConfig (infra/conf/transport_security.go) has no such field and its JSON loader drops unknown keys silently. The PR also stored it as realitySettings.supportX25519Mlkem768 in Xray outbounds (form switch, Go and TS link import, docs outbound builders) and as an inbound settings default that is stripped before Xray and read by no link generator. The outbound switch therefore did nothing, and imported links carried a dead key into the JSON subscription. The share-link hint itself stays: Go, frontend and docs still emit support-x25519mlkem768=true on VLESS REALITY links and drop it on a TLS host override. --------- Co-authored-by: libmur-dev <333915961+libmur-dev@users.noreply.github.com> Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com> |
||
|
|
62423cacd1 |
feat(xray): update xray-core to v26.9.30 and adapt panel
Bump xtls/xray-core to b26a91de4f (v26.9.30) and the three binary pins (DockerInit.sh, release.yml Linux + Windows) in lockstep. No deleted symbols; the sing and sing-shadowsocks indirect deps drop out with the SS2022 rewrite. XDNS finalmask (#6718) replaced its string lists with objects: domains are {name, types, edns0, lenLimit, labelLimit} and client resolvers {type, settings.addr}. The loader no longer parses the old lists, so a single stored xdns mask keeps the whole core from starting. The new leaf package internal/util/maskcompat converts them: "name[:type]" becomes a domain and "name[:type]+udp://addr" a domain plus a udp resolver. A bare name maps to TXT, the type legacy clients queried by default, and each converted domain keeps the 1232-byte EDNS0 the old code always used (without it the server caps answers at 512). It runs from: - the XdnsFinalmaskObjectsFix seeder, over inbound streams, hosts, the xray template, the global sub-JSON mask and cached subscription outbounds; - inbound save (normalizeStreamSettings) and GetXrayConfig, for rows that never went through the seeder; - both link importers, since fm= from an older panel carries the lists. The finalmask form edits the object shape (every key needs a registered field, or the finalmask watch drops it on save) and lifts legacy masks on open. The udp-mask golden fixture moves to the object shape, which TestGoldenStreamFixturesBuildInXray now builds through the core. The wire format changed as well, so pre-upgrade clients need the new core. WireGuard outbound (#6771) dropped settings.domainStrategy and the remoteDNS "local" mode. The endpoint lookup now follows sockopt.domainStrategy and in-tunnel targets the outbound's targetStrategy. The old key is silently ignored, which undid the IPv4-first endpoint lookup the WARP outbound depends on (#5205), and "local" now panics the core at startup because remoteDNS goes through netip.MustParseAddr. The WireguardDomainStrategyFix seeder moves a stored family preference to both keys (a value already set wins) and turns "local" into targetStrategy; the outbound form lifts legacy rows the same way and drops its select, the WARP modal writes the new placement, and the inbound form loses a field the server never read. ValidateOutboundConfig now refuses a non-IP remoteDNS entry, which conf.Build() lets through, on template save and for outbound subscriptions. Noise finalmask items accept type "exp" (#6862), a tag expression. The form offers it for noise items only: header-custom items go through the core's PraseByteSlice, which refuses it. TUN gained autoSystemDnsToGateway (Linux) and autoSystemWfpBlockLeak (Windows). Both pass through the settings schema so a value set in JSON survives the next form save. MASQUE (inbound, outbound, transport) and the XDRIVE transport are new protocols the panel does not offer yet; their new loader refusals only cover configs the panel never generates. The FakeDNS IPv6 pool default, the SS2022 rewrite (same gRPC account; emails are now deduped case-insensitively, as the panel already does), the restored udphop interval default and the rest change no panel-facing config. |
||
|
|
aee45ca3fe |
fix(hosts): advertise Hosts in every WireGuard, AmneziaWG and TUIC config
Invariant: an inbound's enabled Hosts are the endpoints every client config for it advertises, whichever surface renders that config. WireGuard and AmneziaWG broke it. Their raw generators ignored the externalProxy entries Hosts are injected as and always emitted resolveInboundAddress, so the raw subscription, the sub page .conf, the clients links API and "export all links" gave out the panel address while the JSON and Clash formats of the same inbound used the Host. advertisedEndpoints now states the fan-out once for mtproto, wireguard and amneziawg. The browser-built configs had the same gap. The Clients page WireGuard and AmneziaWG config blocks and QR panels, and its TUIC Clash config, used the panel hostname next to server links that already used Hosts; the Inbounds page peer configs, QR and export ignored them too. withMtprotoHostEndpoints becomes withHostEndpoints over a shared hostEndpointsFor mirror of the backend, the tunnel fan-outs render one config per Host, and the clients page waits for the hosts list the way the inbounds page does, so an empty list means "no hosts" rather than "not loaded yet". |
||
|
|
823db05966 |
fix(inbounds): keep the stored client list and enable on inbound save
Invariant: saving an inbound's configuration never changes which clients it holds nor whether it is enabled; both have their own endpoints. The edit modal posts back the clients and the enable flag it loaded when it opened. A client added meanwhile (another admin, the bot, the API, LDAP) was detached and its stats deleted; a client deleted meanwhile came back with its credentials, restoring access that had been revoked; an inbound switched off meanwhile was switched back on. For every save but a master's node-sync push, UpdateInbound now takes the client list and enable from the row it re-reads inside the writer; this replaces the lifecycle-only carry from the previous commit. Client validation (renewal schedule, Hysteria auth, TUIC credentials) moves after that swap so it judges the clients actually saved: a protocol switch keeps the stored clients, and #6268's refusal must apply to them. The edit form no longer loads or sends clients, so neither the JSON editor nor validation sees a copy the server ignores, and the enable switch shows only when adding; the list toggle (/setEnable) covers existing inbounds. Tests that added or re-keyed clients through a panel inbound save pinned the old rule; they now drive the master-push path, where payload clients still apply. |
||
|
|
6f40a75909 |
feat(inbound): excludeFromSub hides links without disabling (#6463)
* feat(inbound): excludeFromSub hides links without disabling Add a per-inbound flag that omits subscription output while keeping the inbound enabled for Xray, auth, and traffic accounting. Fixes #6435. * fix(inbound): excludeFromSub review follow-ups gofumpt model.go, sync docs OpenAPI, keep excludeFromSub master-authored on node mirror, and exercise the legacy add-column migration path in tests. * fix(sub): keep excluded inbounds' clients in the usage header The excludeFromSub filter sat in getInboundsBySubId's SQL, so an excluded inbound's clients never reached seenEmails in the raw, Clash or JSON renderer. A client that lives only on a hidden inbound (one client per inbound sharing a subId) dropped out of the Subscription-Userinfo usage, quota and expiry and out of the info-node state, while the inbound kept serving it and counting its traffic. The query returns every enabled inbound again; each renderer skips an excluded inbound's links but still counts its clients, the same rule the Clash renderer already applies to external links it cannot express. --------- Co-authored-by: mrchatam <mrchatam@users.noreply.github.com> Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com> |
||
|
|
fd7b3559bc |
feat(panel): add sponsor slots fed from sponsors.sanaei.dev
Monthly sponsor placements need to change without cutting a panel release. Panels now read 3X/sponsors.json from the MHSanaei/sponsors repo (GitHub Pages on sponsors.sanaei.dev) and show active sponsors in four slots: an overview banner, a rotating sidebar card (max three), the login page and a new Sponsors page that also lists open placements. An entry shows only while enable is not false and until is in the future; links must be https and logos are png/webp/jpg by name only. The list is cached for an hour and the last good copy survives upstream failures; logos are proxied through /sponsors/logo/:name with failures cached, so CSP stays 'self' and admin browsers never reach a third party. Admins can hide a slot for 24h. Under XUI_DEBUG the panel reads a sibling ../sponsors/3X checkout so edits can be previewed before push. |
||
|
|
14b92fbcff |
fix(nodes): stop flagging a node on the other update channel as outdated
A node's "update available" tag compares its reported panel version with the master's latest, and any non-semver side fell back to string inequality. A dev build reports dev+<sha> (config.GetPanelVersion), so a node moved to the dev channel from a master on the stable channel kept the tag forever; the reverse, a stable node under a master on the dev channel, was flagged too and the tag's default stable update installed nothing new. A dev label and a release tag carry no order, so the comparison now only decides within one channel; dev-to-dev still compares commits, which keeps a node on the current dev-latest commit untagged as config.go intends. |
||
|
|
bc424f0968 |
fix(xray): stop a lone dns qType 0 from matching every query
The core reads a dns rule's qType as a PortList, which drops a bare numeric 0 (infra/conf/common.go: `if number != 0`), and a rule with no qTypes matches every query. A stored `"qType": 0` therefore does not target query type 0: it drops, refuses or hijacks all DNS through that outbound. A qType the panel writes has to be read by the core as exactly the query types it names. Four writers broke that: - DNSOutboundLegacyKeysFix rewrote a lone blockTypes [0] into "qType": 0, so "block type 0" became "block everything" on upgrade. - That seeder shipped in v3.8.0 and is recorded as done, so fixing it does not reach installs that already ran it. DNSOutboundQTypeZeroFix spells any stored numeric qType 0 as "0" once, protocol id matched like the core. - The outbound form adapter turned a typed "0" into the number 0. - The Xray template editor saves raw JSON past that adapter; the save now applies the same rewrite. Each writer is pinned by a test that fails without its part. The rewrite and the repair compare policies as the pinned core builds them, and the repair runs through runSeeders over a database whose legacy-keys seeder already ran, on SQLite and PostgreSQL 16. |
||
|
|
a5a4c9cd83 |
fix(panel): read an outbound protocol id the way the core does (#6522)
* fix(panel): read an outbound protocol id the way the core does
xray-core lowercases a protocol id before it resolves the handler, so a
template that spells the direct outbound "Freedom" is that outbound. The
outbound editor fell through to the vless default and rendered it as an
empty vless server, and the Basics tab did not find it and appended a
second "direct", which the core refuses to load with "existing tag found".
* fix(panel): never leave the direct tag on two outbounds
A "direct" tag held by a non-freedom egress made both Basics-tab setters
push a fresh freedom outbound, and the core refuses a config whose tags
repeat ("existing tag found: direct"). The tag is now checked on its own
before anything is added, matching setDefaultOutboundTag.
* fix(panel): disable the freedom controls when direct is held elsewhere
When a non-freedom outbound holds the "direct" tag, both Basics setters
drop the edit so the core never sees the tag twice, but the Freedom
Strategy select and the Happy Eyeballs switch stayed enabled and snapped
back with no sign of why. isDirectTagTaken now disables both controls in
that state.
The find-or-create-plus-guard was also copied into BasicsTab's happy
eyeballs setter with no test of its own; ensureDirectFreedomOutbound now
owns the lookup, the guard and the creation for both setters, so the
existing helper tests cover that path too.
---------
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
|
||
|
|
826e29e2de |
fix(xray): place the freedom domain strategy where the core reads it (#6515)
* fix(xray): place the freedom domain strategy where the core reads it freedom resolves through the socket layer, so xray-core reads sockopt.domainStrategy and treats both other placements as legacy: it warns on every config load for the outbound-root targetStrategy it migrates itself, and again for the settings-level domainStrategy it deprecates. The panel wrote exactly those two keys from its Freedom Protocol Strategy select, the outbound form card, and the IPv4 routing helper, so any install that had configured a strategy logged a deprecation warning on every start. The strategy now travels in streamSettings.sockopt everywhere the panel emits it: the Basics select, the outbound form (including the JSON tab, which shares the same adapter), the shipped default template, and the IPv4 outbound the routing helper injects. Reading mirrors the loader's own order — root targetStrategy, then the settings keys, then sockopt — so the card keeps showing the value the core would actually run with, and saving drops the legacy keys instead of leaving them behind. A seeder moves the keys for configs already stored in the database, following OutboundRemovedKeysFix. The shared outbound-root Target Strategy field is hidden for freedom, since the core migrates that key into the very sockopt value the card writes and two knobs for one value would race. Tests: placement round-trips and the migration table run through the real vendored core (a captured log handler proves the warning is gone after the rewrite and present before it), and the modal asserts freedom offers a single strategy field. * test(database): seed the template row the seeder test needs A fresh InitDB creates no xrayTemplateConfig row — the panel's setting defaults live in the service layer — so the test has to insert the legacy template itself and then assert the seeder's history gate stops a second pass from rewriting it. * fix(xray): keep one strategy control per outbound, seed the row in tests Review findings: the Transport tab's Sockopts block renders for freedom too, so its Domain Strategy select and the freedom card wrote one sockopt value between them and the card won on save — the field is hidden for freedom now, leaving the card as the single control. The seeder is also pre-marked on a fresh install so it does not run on the second start, and the seeder test seeds the template row itself (a fresh InitDB has none) and asserts the rewrite structurally instead of grepping for a key name that sockopt also uses. |
||
|
|
5ad9df69b9 |
fix(link): restore mKCP seed and headerType on share-link import (#6480)
* fix(link): restore mKCP seed and headerType on share-link import applyTransport / applyTransportParams ignored kcp query params that applyKcpShareParams emits, so re-imported outbounds lost seed and header and could not talk to the inbound. Mirror those fields (plus mtu/tti) into kcpSettings in both Go and TS importers. Fixes #6476 * fix(link): restore mKCP header/seed via finalmask mkcp-legacy * fix(link): split mKCP header and seed into separate masks on import Both importers folded a share link's headerType and seed into one mkcp-legacy mask {header, value}. xray-core's MkcpLegacy.Build ignores value once header is set (and reads it as the fake DNS domain for header=dns), so an imported outbound carried the header mask but no AES-128-GCM seed while the emitting inbound has both, and could not connect — the failure #6476 reports, now for every link carrying both params. Emit one mask per field, seed first: the finalmask array's first item is the innermost layer, which puts the header around the cipher as legacy mKCP did. Also bound mtu/tti to KCPConfig.Build's accepted ranges (mtu >= 21, tti 10..1000, decimal digits only on both importers) so a pasted link cannot fail the whole Xray config load, and look header types up as own properties so a prototype key such as "constructor" is not mapped. --------- Co-authored-by: mrchatam <287639636+mrchatam@users.noreply.github.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
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. |
||
|
|
503b5df4b9 |
fix(link): preserve Shadowsocks TLS query params on import (#6467)
* fix(link): preserve Shadowsocks TLS query params on import Mirror trojan/vless stream parsing so Xray-native type/security/sni/alpn/fp query params on ss:// links survive into streamSettings on both Go and TS importers. Fixes #6094 * fix(link): drop extra blank line so oxfmt passes --------- Co-authored-by: mrchatam <287639636+mrchatam@users.noreply.github.com> |
||
|
|
958d7f138e |
fix(frontend): fold sockopt v6only into V6Only on inbound load (#6453)
Prevents duplicate keys and an unreachable switch when Advanced/API configs store lowercase v6only (#6421). |
||
|
|
51e0afdd90 |
fix(inbounds): allow negative subSortIndex for subscription order (#6465)
* fix(inbounds): allow negative subSortIndex for subscription order Preserve explicitly set negative indices so primary inbounds can sort ahead of the default without renumbering peers; keep 0/omitted → 1. * fix(inbounds): gofumpt model.go and trim subSortIndex comments --------- Co-authored-by: mrchatam <287639636+mrchatam@users.noreply.github.com> |
||
|
|
8082ab4d74 |
feat(clients): show short HWID fingerprint in admin device list (#6464)
* feat(clients): show short HWID fingerprint in admin device list Expose a 12-char prefix of the stored hwid_hash in the admin HWID list API and UI so admins can distinguish devices without querying the database. Full hashes and raw HWIDs remain unexposed. Fixes #6359 * ci: retrigger release matrix after 386 dependency download flake --------- 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> |
||
|
|
d5ab84e8d5 |
feat(amneziawg): add AmneziaWG as an outbound protocol (#6320)
* feat(amneziawg): add AmneziaWG as an outbound protocol - AmneziaWG outbound protocol end-to-end: config schema, socks bridge, netstack, panel UI - Route amneziawg outbounds to HTTP probe in TCP mode (backend + frontend classifiers) with pinning test - Add 2-minute idle read deadline to pumpUDPEgress to reap idle egress sessions - Require SOCKS5 username/password auth on the egress server (reject NO-AUTH with 0xFF) with test - Bound the egress TCP tunnel dial with portForwardDialTimeout (10s), matching portfwd.go - Resolve UDP domain targets off the association's reader loop via deliverUDPDatagram; race-safe getOrDial starts the reply pump at session creation; client passed by value into resolver goroutines (pinned by TestEgressUDPDatagramDomainInterleavedClients) - Reconcile early-returns on an empty desired set and closes the egress listener; EgressBasePort (64900) is reserved against local inbound port conflicts like the internal API port, with pinning tests for both the port reservation (TestCheckPortConflict_EgressPortBlockedLocal) and the Reconcile empty-desired Close/Listen lifecycle (TestOutboundManagerReconcileEmptyDesiredClosesEgress) - Eliminate acceptLoop shutdown race by validating listener != nil and registering to tracked under s.mu before wg.Add; bound pre-auth handshake with deadline (pinned by TestEgressServerCloseDuringConcurrentAccepts) - Support AAAA and dual-stack domain resolution in tunnel DNS resolver with v6 default fallback (DefaultTunnelDNSServerV6); add DNS field to frontend protocol form; avoid unneeded cache flushes on unchanged SetStack ticks * fix(amneziawg): resolve IPv6-only DNS default fallback and validate required keys - Default to IPv6 tunnel DNS on IPv6-only outbounds with blank dns - Require non-empty secretKey and peer publicKey in ValidateAmneziaWGOutbound - Add end-to-end IPv6 tunnel domain resolution test and test empty key rejection - Trim comment blocks exceeding 2 lines across modified files - Fix Storybook test execution on environments with POSIX locale Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: rqzbeh <rqzbeh@users.noreply.github.com> Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |