Commit Graph

3742 Commits

Author SHA1 Message Date
Ali Karami 1edb8b31bc fix(xray): keep balancer overrides across routing reloads (#6779)
* fix(xray): keep balancer overrides across routing reloads

A balancer override set from the panel lived only inside the running
core. Every routing hot-apply (any Xray-settings save touching rules or
balancers) and every node test through an outbound calls
RoutingService.AddRule with shouldAppend=false, which rebuilds all
balancers, so the override vanished silently even though the API docs
promised it lasted until an Xray restart.

The panel now records the overrides it sets, per core process, and
re-applies them after its own routing reloads. Targets are validated
against the running config: the core accepts any tag and then closes
every connection routed to one it lacks, so an unknown target is
refused, and an override whose outbound is removed is cleared instead
of black-holing the balancer. Balancer targets resolve to their _bl_
loopback from the running config rather than the saved template.

Restarting Xray still clears overrides, as documented.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrJrzn9LKaPhMq9h3Fb5TV

* fix(xray): serialize overrides and the node bridge with config applies

Review of #6779 found two races. Neither path took the package lock that
RestartXray holds while it changes the running core:

- An override landing mid hot-apply was checked against the config
  snapshot the apply was replacing, so it could pin a balancer to an
  outbound the core had just removed. OverrideBalancer now takes the lock.
- The node bridge's teardown checked overrides against the snapshot it
  took before the probe, which can run for seconds, so it dropped an
  override set meanwhile onto a newly saved outbound.

The same stale snapshot drove the bridge's routing restore. That restore
silently reverted rule changes saved during a probe. After a restart, it
also pushed the old core's rules and overrides into the new core through
the shared API port. The bridge is now built and torn down under the lock,
which is not held while the probe runs. At teardown it restores the
routing that the core's config holds then, and it leaves alone a core that
was restarted meanwhile.

With every caller under the package lock, balancerOverrideSet drops its
own mutex.
dev-latest
2026-10-12 14:59:26 +02:00
MHSanaei 532392850f fix(ci): drop id-token from the Claude workflows and tighten the review comment
CodeQL alert #121 flags the pr-head checkout in the PR review job. The
checkout is deliberate and nothing in the job executes it, but the job still
carried `id-token: write`, and so did the issue analyst. claude-code-action
only uses OIDC to trade for a Claude GitHub App token when no github_token is
passed - that app token asks for contents, pull_requests and issues write -
or for workload identity, which neither workflow uses. Both pass
github_token, so the permission did nothing for them, yet any code running
in either job could have minted that write token, undoing the review's
read-only split.

Three reviews posted on one morning (#6666, #6675, #6752) also showed rubric
gaps. REVIEW.md said both "open with the verdict" and "lead with No blocking
issues", so the comments opened three ways and two tallies dropped
severities; the opening is now a fixed order - tag line only when blocking,
verdict paragraph, one status line whose tally names all three severities.
#6675 filed a spoofable X-Forwarded-For IP, which that PR first shows to an
admin approving a login, under Design; Design now holds approaches and
trade-offs only, and a concrete consequence the change makes reachable or
worse is a finding. The coverage list ran to some 25 nested lines on #6752;
it is now ten lines at most, one per area, no nested bullets.
2026-10-12 13:39:29 +02:00
Javier Guerrero c321933a54 perf: reduce traffic formatter allocations (#6790)
Co-authored-by: Javier Guerrero <javier.guerrero@github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-12 11:05:40 +02:00
Jay Hemnani d43e0c7ead fix(sub): default VLESS client encryption to none when the inbound has none stored (#6781)
VLESS inbounds whose settings carry no "encryption" key (rows saved by
panels before the field existed, or inbounds added over the API with only
"decryption":"none", as the API docs example does) produced a JSON
subscription outbound with "encryption": "". Xray refuses to load that
config ("VLESS users: please add/set \"encryption\":\"none\" for every
user"), so every VLESS entry of the JSON subscription was unusable. The
share link left the parameter out instead.

Treat a missing or empty value as "none" for both outputs, matching the
frontend inbound schema default and the docs link generator.
2026-10-12 11:03:47 +02:00
mrdarkerr f55938ba0a fix(amneziawg): respect tunnel MTU when writing UDP replies (#6780)
* fix(amneziawg): respect tunnel MTU when writing UDP replies

Send replies through gVisor FindRoute/Route.WritePacket instead of WriteRawPacket so the network endpoints apply the tunnel MTU and fragment oversized IPv4/IPv6 datagrams. Preserve UDP checksums including the zero-complement IPv6 case and validate lengths before narrowing to uint16.

Regression tests inspect and reassemble actual channel-backed TUN output at and above the MTU, verify checksums and exercise concurrent sources.

* docs(review): put the tally before Design and name who can store a trigger
2026-10-12 03:11:09 +02:00
MHSanaei a4699696ef docs(review): put the tally before Design and name who can store a trigger 2026-10-12 03:01:18 +02:00
MHSanaei 5939fda397 fix(ci): let the PR review save its body, and scope the write-check bypass
Run 38187893539 reviewed PR #6784 in full but posted nothing: the model's
Write to /tmp/review.md was refused. Claude Code checks file permissions
against Edit(path) rules only and never consults a Write(path) rule, so the
allow rule was dead - and so were the Write(//home/**) and Write(//opt/**)
deny rules meant to keep $GITHUB_ENV out of reach. All three are Edit rules
now; a local headless run confirms Write(path) is refused, Edit(path) lets
the file through, and an Edit deny still blocks a Write under it.

allowed_non_write_users="*" skipped the action's write-permission check for
every actor and warned each run. Only a pull_request_target from a fork
needs it; an `@claude review` requester is already OWNER/MEMBER/COLLABORATOR
and passes the action's own check.
2026-10-12 02:32:55 +02:00
MHSanaei 9130f6dd75 feat(ci): make the PR review judge the claim and never post code
The review only hunted defects: a correct fix got no word on whether the
reported problem was real or actually solved, and a feature got no design
feedback. The comment now opens with a verdict for every kind of change -
for a fix, whether the failure exists on base, whether the head removes its
root cause (checked against the issue-analyst's analysis) and whether the
test proves it; for a feature, whether it overlaps what the panel already
has, which deployment shapes it misses, and a prose-only Design section;
for refactor, perf, dependency bumps, docs and CI, a question of their own.
The trace scales to the change, so a typo is not traced like a runtime
change. It also searches for duplicate merged or in-flight work, and runs at
high effort since settling whether a bug is real takes tracing base and head.

No code may appear anywhere in the comment, and the model no longer holds a
write token. The workflow is three jobs: `prepare` (ack, pin the head, skip a
reviewed PR), `review` (read-only token; the model writes /tmp/review.md) and
`post` (no model, no checkout), which strips any fenced block, keeps the
Reviewed head marker the skip relies on, and only then posts one plain
comment. Before, `gh api` with a pull-requests: write token could touch any
PR in the repository, `sed -i` could write $GITHUB_ENV, and the inline-comment
tool told the model to use suggestion blocks. Concurrency stays on the review
job: at workflow level any unrelated comment on the PR would replace a
pending review in the queue.

Restores three guards the earlier claude-bot review had: all author-controlled
input, including text inside the checked-out code, is data and a diff adding
text aimed at a reviewer or bot is a HIGH finding; a blocking finding tags the
repository owner; and a diff too large to read whole has a reading order and
must name what went unread.
2026-10-12 02:23:19 +02:00
libmur-dev dbfa369632 fix(frontend): close panel update confirmation before progress (#6766)
* fix(frontend): close panel update confirmation promptly

* chore(ci): rerun flaky checks
2026-10-12 01:32:15 +02:00
MHSanaei 2e8af64745 chore(frontend): update dependencies
Refresh frontend runtime and development dependencies, including Vite, Playwright, TanStack Query, and form and i18n packages.
2026-10-12 01:12:13 +02:00
MHSanaei fd6c5925ce style(sub): match the subscription page to the panel theme
The sub page ran its own violet/cyan palette, an animated aurora
backdrop, a grid overlay and a glass card, so it looked unrelated to
every other panel page. It now uses the panel shell background and card
colors for light, dark and ultra-dark, the panel's antd theme (blue
primary) unmodified, and antd tokens for dividers, tiles and rows.
2026-10-12 01:08:24 +02:00
MHSanaei 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.
2026-10-12 00:49:11 +02:00
MHSanaei 8fe7be943f docs(security): name an email security contact
GitHub private reporting stays the primary channel. The address gives
reporters without GitHub a private route and matches the primary_contact
used for the OSS Scanner enrolment, which checks SECURITY.md for it.
2026-10-11 22:44:39 +02:00
MHSanaei eb498a3c13 chore(security): add OSS Scanner build image and threat model
Anthropic's OSS Scanner builds .oss-scanner/Dockerfile from this repository
and audits the image offline. The image keeps the source, the Go module and
npm caches, the debug panel binary, Xray-core and the sidecars, so tests,
the xray gRPC e2e suite, the master+node e2e and a live panel all run with
the network disabled. threat_model.md lists the trust boundaries, entry
points and the severity rubric the scanner should report against.
2026-10-11 22:29:36 +02:00
MHSanaei ab1c509329 fix(service): keep chunk capacity math from overflowing
chunkStrings/chunkInts sized their result with (len+size-1)/size, so a
size near MaxInt wrapped negative and make panicked with "cap out of
range"; count the chunks with div+remainder instead. NormalizeUDPHop's
len-hops+1 is bounded by len(masks) since hops >= 1, so cap at that.

Resolves CodeQL go/allocation-size-overflow alerts #119 and #120.
2026-10-11 22:10:46 +02:00
MHSanaei b5a04ea95f fix(node-sync): keep shared-email lookups on the open transaction
emailUsedByOtherInbounds and emailsUsedByOtherInbounds always queried
through database.GetDB(), yet five callers run them inside a transaction:
both node traffic sync sweeps (setRemoteTrafficLocked), the inbound edit
stats sync (updateClientTraffics) and both email-drop branches of
UpdateInboundClient. Each call needed a second pool connection while the
transaction held row locks on client_traffics/node_client_traffics. On
PostgreSQL, once subscription fetches (UPDATE client_traffics SET
last_sub_fetch) blocked on those locks filled the rest of the 25-connection
pool, the lookup waited forever, the transaction never ended, and the panel
hung until restart.

Both helpers now take the *gorm.DB to query on; transactional callers pass
tx and the rest pass the pool handle they already hold. The new tests cap
the pool at one connection, so any query that escapes the transaction
blocks; each went red before the fix on SQLite and (node sync) on a live
PostgreSQL 16, and passes after.

Closes #6789
2026-10-11 22:05:24 +02:00
MHSanaei 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.
2026-10-11 21:24:25 +02:00
MHSanaei 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.
2026-10-11 21:20:07 +02:00
MHSanaei b9e1d3afee fix(inbounds): refuse a Shadowsocks-2022 server key xray-core cannot parse
xray-core parses the inbound-level Shadowsocks-2022 key with ParseKey and
refuses one of the wrong size while building the inbound, which keeps the
whole core from starting, not only that inbound. Only client keys were
checked, so an API caller posting a 32-byte key on aes-128 (or keeping a
16-byte key after switching to aes-256/chacha20) saved fine and took
Xray down.

AddInbound and UpdateInbound now reject such a key with a message that
names the method and the size it needs. The check uses the core's own
GetCipherMethod/ParseKey, so a raw non-base64 key the core accepts stays
valid. Startup also replaces a stored wrong-size server key: that inbound
never worked, so no client holds a working copy of it.
2026-10-11 20:42:23 +02:00
MHSanaei b124af2a1b fix(settings): accept five-field crontab for report and LDAP schedules
The panel scheduler runs WithSeconds(), whose parser rejects a standard
five-field crontab such as "30 8 * * *". An admin who typed one into the
Telegram/Discord notify time or the LDAP sync schedule saved it without
error, AddJob then failed at startup, and the job silently never ran.

Admin-typed schedules now go through internal/util/cronspec, which takes
an optional seconds column plus the @descriptors and hands the scheduler
a parsed cron.Schedule. CheckValid rejects an unparseable schedule on
save, and a stored legacy value falls back to the job's default instead
of dropping the job.
2026-10-11 20:37:04 +02:00
MHSanaei 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.
2026-10-11 18:26:45 +02:00
MHSanaei 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.
2026-10-11 18:02:12 +02:00
MHSanaei a8255944bf style: trim MASQUE and XDRIVE comment blocks to two lines
The MASQUE and XDRIVE commits left four comment blocks over the
two-line cap CLAUDE.md sets for committed Go/TS.
2026-10-11 17:00:52 +02:00
MHSanaei c4b9b3f9c7 fix(clients): refuse a ":" email on a MASQUE inbound
MASQUE logs a user in with the email as the Basic-auth user name, so
xray-core refuses the whole inbound (MASQUE: invalid "email") over one
email holding ":", and with it the Xray start. The panel accepts such an
email for every other protocol, so attaching or creating that client on
a MASQUE inbound took every inbound down on the next restart.

syncInboundClients is the write every create, attach, bulk, import and
inbound-edit path shares inside its transaction, before the runtime
push, so the per-protocol check there now refuses the email for MASQUE
next to the existing TUIC identity check, loading the protocol once.
2026-10-11 16:35:20 +02:00
MHSanaei 27e45f3039 fix(sub): keep the udphop mask on every JSON-subscription endpoint
UpgradeLegacyUDPHop deleted udpHop from the quicParams map it was handed
in place. The JSON subscription runs it once per endpoint on a finalmask
that mergeFinalMask (host masks) or withLegacyFragmentRanges cloned only
shallowly, so the first endpoint emptied the inbound's shared quicParams
and every later endpoint got {"quicParams":{}} and no udphop mask: a
hysteria inbound with port hopping and two hosts hopped on the first
host's config only.

The upgrade now replaces quicParams and udp on the finalmask it is given
instead of writing into values a merged finalmask shares, as the
frontend's upgradeLegacyUdpHop already does.
2026-10-11 16:35:16 +02:00
MHSanaei 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
da6872a3: that hook change was not wanted.
2026-10-11 16:05:35 +02:00
MHSanaei ef4bda4e06 feat(inbounds): TUN adapter type and auto-system options
xray-core 26.9 added three TUN settings the inbound form could not set:

- desc, the tunnel type a new Wintun adapter is created with on Windows.
  The settings schema did not list it, so the parse stripped it and a
  value entered as JSON vanished on the next save;
- autoSystemDnsToGateway (Linux), which points systemd-resolved at the
  TUN gateway;
- autoSystemWfpBlockLeak (Windows), whose dns / misconfigtun filters keep
  traffic from leaking past the TUN.

The tooltips name the platform each one acts on and what it needs, since
the core refuses to build or start the TUN otherwise.
2026-10-11 15:40:46 +02:00
MHSanaei b6c859f5b9 feat(xray): edit Lua DNS and routing scripts
xray-core 26.10.10 can hand DNS and routing to Lua: dns.script and
routing.script name a .lua file (absolute, or found next to the Xray
binary) that defines HandleDNSQuery / HandleRoute, and a DNS server's id
is how a DNS script picks it. The keys round-tripped through the JSON
template but had no field.

- DNS: a Lua Script field, and an ID field in the server modal;
- Basics: a Routing Script field beside the routing strategy;
- Routing: a warning above the rules while a script is set, since the
  core then routes only through the script and skips every rule, the
  panel's api rule included.

The descriptions say the script must still send the api inbound to the
api outbound, and that Xray will not start if the file is missing. An
empty field drops the key, because the core rejects an empty path.
2026-10-11 15:37:20 +02:00
MHSanaei 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.
2026-10-11 15:27:19 +02:00
MHSanaei 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.
2026-10-11 11:20:34 +02:00
MHSanaei 00aaef386f fix(inbounds): accept masque in the inbound protocol validator
The inbound add and update handlers bind the body into model.Inbound and
validate Protocol against a oneof list that predates MASQUE, so every
save of a MASQUE inbound from the panel or the API failed validation
even though the rest of the stack (b04039c2) handles the protocol.
Add masque to the list and regenerate the schemas and OpenAPI document
that mirror it.
2026-10-11 10:56:47 +02:00
MHSanaei da6872a3b8 chore(frontend): let lint-staged pass commits that stage only generated files
lint-staged hands every staged src/**/*.ts file to oxfmt and oxlint, but
both ignore src/generated, so a commit whose only TypeScript change is a
regenerated schema (a validator tag edit plus make gen) aborts with
"Expected at least one target file". Tell both tools an empty selection
is not an error.
2026-10-11 10:56:39 +02:00
MHSanaei 04cccef311 feat(warp): register WARP over MASQUE from the WARP modal
xray-core's masque transport reaches Cloudflare WARP with an enrolled
ECDSA P-256 key, the endpoint's public key and the assigned tunnel
addresses, which only a WARP enrollment hands out.

A new regMasque action on /panel/api/xray/warp/:action registers a
separate WARP device and enrolls a secp256r1 key for the masque tunnel
(the API revision and client version the WARP apps enroll MASQUE keys
with). Enrolling replaces a device's WireGuard key, so it never touches
the stored WireGuard registration. It returns the PEM key pair, the
addresses and the endpoint, and the WARP modal turns them into a
warp-masque outbound (HTTP/3, Cloudflare's MASQUE SNI), or refreshes
the one already there.
2026-10-11 10:31:01 +02:00
MHSanaei 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.
2026-10-11 10:28:49 +02:00
MHSanaei 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.
2026-10-11 08:21:08 +02:00
MHSanaei 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
82c77111 already bumped to 701af60772cd.

xdns finalmask (#7090): a domain now takes a "names" list instead of a
single "name", and a resolver is an "addrs" list of udp:// or tcp://
URLs instead of {type, settings:{addr}}. The loader ignores the old
keys rather than failing, so every mask the 26.9.30 seeder wrote would
load with no domain and no resolver and the tunnel would carry nothing.
maskcompat.UpgradeLegacyXdns now lifts both the pre-26.9.30 string lists
and the 26.9.30 objects straight to names/addrs, dropping entries the
old loader refused. A new seeder (XdnsFinalmaskNamesAddrsFix) reruns it
over every stored finalmask, and the save path, GetXrayConfig, both
link importers and the mask editor's mount lift go through it. Template
outbounds now get the build-time heal too: a 26.9.30 template pasted
after the seeder passes the save check, since the core loads that shape
without error. The editor edits names and addrs as tag lists and no
longer requires record types, which the core now defaults (TXT on a
client, A/CNAME/TXT/AAAA on a server). The wire format did not change,
but clients on an older core cannot read the new config shape.

Lua scripts (#6823): dns.script, routing.script and dns servers[].id
are new. The xray page parsed the dns block with a strict schema, so
loading it dropped script and id and the next save deleted them; the
schema now keeps both and the DNS server modal carries the id through
an edit. The hot routing apply built routing in-process, and that build
now resolves the script file from the panel's working directory, so a
script beside the xray binary failed every rule change into a full
restart and broke the node bridge. RoutingService cannot change the
script anyway, so the panel no longer resolves it there.

TLS client roots (#7105): the core now verifies against its bundled
Mozilla roots instead of the OS store unless tlsSettings.useSystemCA is
set. The outbound TLS form gets a switch for it, so an outbound to a
server signed by a privately installed CA can opt back in.
2026-10-11 07:55:12 +02:00
MHSanaei 82c7711189 chore(deps): update Go and dependencies
Raise the Go version to 1.27.2 and refresh Xray, LDAP, and related dependencies.
2026-10-11 07:25:13 +02:00
Leslie Alexander 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
2026-10-11 07:17:05 +02:00
dependabot[bot] 442b7fb163 chore(deps): bump github.com/valyala/fasthttp from 1.74.0 to 1.75.0 (#6767)
Bumps [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp) from 1.74.0 to 1.75.0.
- [Release notes](https://github.com/valyala/fasthttp/releases)
- [Commits](https://github.com/valyala/fasthttp/compare/v1.74.0...v1.75.0)

---
updated-dependencies:
- dependency-name: github.com/valyala/fasthttp
  dependency-version: 1.75.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-08 22:03:22 +02:00
冰 f0016f6cf9 test: stop swapping http.DefaultTransport in the Happ network test (#6742)
* test: stop swapping http.DefaultTransport in the Happ network test

The Happ local-generation test replaced the http.DefaultTransport global to
refuse sockets. resolvePublicIPsInBackground fires a fire-and-forget resolver
(ServerService.GetStatus -> resolvePublicIPs -> getPublicIP), and that
goroutine can outlive the test that scheduled it, so a later test writes the
global while the resolver is reading it: a data race that `make race`
(-shuffle=on) only reports when the status test happens to run first.

Route the lookup transport through an atomic swap point and count in-flight
lookups, so the test stubs its own path and waits for earlier resolvers to
settle before installing it. That also stops another test's dials from
landing in this test's zero-network tally.

Reproduced on the parent commit with -shuffle=6 (read in getPublicIP,
previous write in TestHappGenerateLocallyWithoutNetwork); the same seed is
clean with this change, across repeats and for the whole package under -race.

* test: prove the Happ network guard intercepts panel egress

The PR review pointed out that HappService.Generate never reaches a stub
installed only on the public-IP lookup, so the "zero network attempts"
assertion could not fail. Move the override onto the panel's shared egress
seam — getPublicIP and SettingService.NewProxiedHTTPClient — and gate the
test on a canary request that must be refused and must move the tally, so a
future outbound call from local generation is reported instead of passing
unnoticed.

* test(service): stop the cold-status test leaking a public-IP resolver

The race CI hit (run 37345781756) came from TestCurrentStatusSamplesBeforeFirstTick:
its CurrentStatus call starts resolvePublicIPsInBackground, a goroutine that
makes real internet requests and outlives the test. On a runner without
IPv6 every lookup waits out its 3s timeout, so the goroutine is still
building clients from http.DefaultTransport when the Happ test swaps it.

Pre-settling the IP cache keeps that test from starting the resolver at
all, so no unit test reaches the internet or leaks the goroutine. This
replaces the earlier panelEgressTransport / panelEgressLookups seam, which
added test-only hooks to production code without removing the leak.

Reproduced in golang:1.27-bookworm with egress routed to a blackhole
(HTTPS_PROXY=http://10.255.255.1:9, -race -count=3 on the two tests): the
same server.go:406 race as CI before, clean over -count=5 after.

---------

Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
2026-10-08 22:02:46 +02:00
Jack 881f646d2c fix(clients): balance renewal form layout and mode guidance (#6672)
* fix(clients): balance renewal form layout and mode guidance

Move renewal out of the traffic-reset half-column so its help and preview
no longer leave a tall empty area beside the reset selector. Keep the
mode, active schedule field and renewal limit in equal desktop columns,
stack them on small screens, and preserve the bulk form's stacked layout.
Keep monthly traffic-reset controls together in their own row and allow
the first-cutoff button to wrap without shrinking its click target.

Show fixed-duration guidance only for interval renewal and the month-end
boundary note only for monthly renewal. Synchronize all thirteen locales
and the client docs, remove obsolete zero-value mode-switching tooltips,
and cover the help transitions through the real client form.

Keep preview markup and existing monthly copy intact so the independent
date-display work in #6662 can merge without overlapping these edits.

This is presentation-only: saved values, expiry selection, renewal and
traffic-reset calculations, APIs and database formats are unchanged.

* refactor(clients): drop the renewal CSS that #6662 made redundant

#6662 landed the full-width preview Space and the wrapping first-cutoff
Button as inline styles, so ClientRenewalFields.css and its wrapper div
styled the same two elements a second time; both are removed.

* test(tuic): wait for downstream association activity refresh

The UDP lifecycle regression can receive its QUIC response before the
server's sending goroutine updates the association timestamp. Reading
the protected field once then mistakes a valid scheduling order for a
missing refresh, intermittently failing the race-enabled CI job.

Wait for lastActive to advance under the registry mutex, bounded by a
one-second deadline, before checking idle reaping. Retain the payload,
association ID, idle-survival and shutdown assertions so a missing
activity update still fails the test.

This only changes test synchronization. Production relay behavior,
renewal logic, network timeouts and CI configuration remain unchanged.

---------

Co-authored-by: JacktheRanger <219502738+JacktheRanger@users.noreply.github.com>
Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
2026-10-08 21:59:38 +02:00
MHSanaei 257559c389 fix(update): leave TLS alone during panel updates
The panel's web Update runs update.sh with stdin on /dev/null. On a panel
with no certificate configured, config_after_update opened the SSL menu:
read got EOF, the empty answer selected "Let's Encrypt for IP", x-ui was
stopped and IP-certificate issuance ran, and its failure path rm -rf'd
/root/cert/ip and the acme.sh state, deleting a pair other services may
share. The URL printed afterwards was always https:// even when nothing
was configured. When the public-IP lookup failed, the same run instead
looped forever on an "enter your server IP" prompt it could never answer.

An update now only updates the panel: the SSL setup path and the helpers
only it used are removed from update.sh, the IP lookup falls back without
prompting (it is only used for the printed URL), and the URL reports the
scheme the panel actually serves. TLS stays configurable from the x-ui
menu and install.sh.

Supersedes #6667.
2026-10-07 14:49:23 +02:00
Matt Van Horn f217212247 web: overlay WireGuard peer public key from the inbound (#6751)
The WireGuard branch now copies PublicKey from the matching inbound
settings client. When that entry's KeepAlive is nil the peer keepalive
is cleared, and when it is set the integer is copied into a new pointer.
The dual-tunnel test seeds different keys and keepalives, checks that
the shared row still has the AmneziaWG identity, and expects the emitted
WireGuard peer to use the WireGuard public key and keepalive, including
a case where the WireGuard settings entry has no keepalive and the peer
omits it.

Fixes #6731
2026-10-07 10:55:41 +02:00
Jack 54d1712544 fix(clients): clarify renewal preview times and estimates (#6662)
* fix(clients): clarify renewal preview times and estimates

The preview rendered RFC3339 strings directly and labeled the server's Local
zone without explaining which clock it represented. Its renewal count also
looked like a record of renewals already performed.

Display readable dates with an explicit UTC offset while preserving the
server's wall time, per-date DST offset and fractional cutoff precision.
Explain the panel/server timezone and read-only preview, and label the
renewal count as estimated allowance consumption in all 13 locales.
Let the first-cutoff button wrap on narrow screens and update the English
and Chinese client documentation plus display/form regression coverage.

This changes presentation only: renewal calculations, stored expiry values,
API payloads and first-use behavior remain unchanged.

* test(clients): remove redundant renewal preview copy checks

The locale-only preview case repeated the Local timezone branch without
covering a distinct behavior. Exact textContent comparisons after matching
the same text with getByText also only pinned constant wording, contrary
to the repository's testing guidance.

Remove that duplicate case, its locale setup and the redundant copy
assertions. Keep the UTC, per-boundary DST offset, fractional precision
and original datetime checks, together with the existing real form-save
tests. Production rendering, translations and renewal behavior are unchanged.

---------

Co-authored-by: JacktheRanger <219502738+JacktheRanger@users.noreply.github.com>
2026-10-07 10:55:06 +02:00
libmur-dev b3f1cced70 fix(tuic): preserve IP destinations during sniffing (#6753)
Keep sniffed TLS domains available for routing without replacing the destination address requested by the TUIC client. Add a regression assertion for the generated relay sniffing settings.
2026-10-07 10:53:46 +02:00
MHSanaei c9207b6f3e feat(install): install PostgreSQL 18 from PGDG for new local databases
The local PostgreSQL setup installed whatever the distro metapackage pointed
at, so Ubuntu 22.04 got 14 (EOL Nov 2026), Debian 12 got 15 and EL9 got 13.
Servers ended up on different majors, and a pg_dump backup taken on a newer
server does not restore onto an older one.

New installs now add the official PGDG repository and install
PostgreSQL 18 (apt on Debian/Ubuntu, dnf on EL 8+ with the AppStream module
disabled, postgresql-18 service and /var/lib/pgsql/18/data). Where PGDG has
no build (Ubuntu 20.04, Debian 11, armhf/i386/s390x, EL7, Fedora, Amazon
Linux, older EL point releases) the repository change is rolled back and
the distro package is installed as before. A host that already has a
cluster keeps it, so existing installs are never upgraded.

x-ui.sh resolves the postgresql-18 unit on EL for start/stop/log and also
removes the PGDG packages on purge.

Verified in containers: Ubuntu 22.04/24.04, Debian 12, Rocky 8/9 and
AlmaLinux 10 get 18.6; Ubuntu 20.04 falls back to 12 and Debian 12 armv7 to
15 with apt-get update still clean; Ubuntu 22.04 with an existing 14 cluster
stays on 14. The CI PostgreSQL test set passes against postgres:18.
2026-10-06 14:29:41 +02:00
MHSanaei 6be3c438e1 fix(node): keep client traffic history when a node inbound is replaced
When a node snapshot reports an inbound under a tag that matches neither
the central tag nor an origin/port alias, SetRemoteTraffic adopts it as a
new central inbound and, in the same tick, sweeps the old one. The sweep
deleted the old inbound's client_traffics rows even though the snapshot
still reported those emails under the new inbound. existingEmails was
computed before the sweep, so the rows were not recreated that tick; on
the next tick the inbound was no longer new, the rows came back seeded at
0/0 and the node baseline jumped to the node's totals. Quota accounting
lost every byte the clients had used.

The sweep now moves rows for emails the snapshot still reports to the
inbound now carrying them, instead of deleting them. Only emails the node
no longer reports at all lose their row, mirroring the existing baseline
rule.

The alias check also treated a master-created node inbound that had not
been tag-matched yet (empty origin_node_guid) as foreign, so a renamed tag
on its first sync replaced it instead of aliasing it. An empty origin now
counts as this node.

PostgreSQL lane not run locally; the change is a plain UPDATE through GORM.

Closes #6749
2026-10-06 12:35:59 +02:00
MHSanaei d57dcf824b chore(deps): bump frontend and docs deps, clear npm audit findings
Bump vite, oxlint/oxfmt, msw, jsdom and plugin-react in the frontend, and
fumadocs, lucide-react, oxlint/oxfmt, postcss and pnpm in docs.

npm audit reported seroval <=1.6.2 (critical) via solid-js, which the
react-query devtools pull in pinned to ~1.5.4, and sprintf-js (no patched
release) via remarkable's argparse@1. `npm audit fix --force` "fixed" both
by downgrading swagger-ui-react to 3.23.3, which brings in far worse
advisories. Instead override seroval/seroval-plugins to ^1.6.8 and give
remarkable argparse@2; remarkable only requires argparse from its unused
CLI entry, so the bundled library is unaffected.

pnpm appends an exact-version minimumReleaseAgeExclude entry on every
update of a too-fresh release and never prunes them. Drop the stale ones;
keep only fumadocs 16.16.2 and oxlint/oxfmt, still inside the 1-day cutoff.
2026-10-06 12:09:38 +02:00
MHSanaei 66ef5bbc05 fix(sub): emit host TLS verification at xray level in JSON subscription
The JSON subscription flattens tlsSettings into xray's client shape before
the per-host loop, then applyExternalProxyTLSToStream writes the host's
echConfigList, verifyPeerCertByName, pinnedPeerCertSha256 and
allowInsecure into the panel-only tlsSettings.settings map. Xray ignores
that map, so a host's ECH (and its pins / verify name) never reached the
client and connections through the host failed.

After the host overrides are applied, lift that map through the same
writer tlsData uses for the inbound's own TLS: fields land at the top of
tlsSettings, pins are joined into the comma string xray parses, and
allowInsecure is dropped exactly as it is for the inbound (removed from
xray). The Clash renderer still reads the nested map and is unchanged.

The helper-level subtest that pinned the nested location as the "json"
shape is replaced by an end-to-end GetJson test on a host-backed inbound.

Closes #6743
2026-10-05 22:26:04 +02:00
MHSanaei dd9569478e fix(node): ignore a node's stats for clients detached from the inbound
Invariant: a node snapshot may move a master client's traffic state only
for emails the reporting inbound's settings still list.

A master detach calls the node's /detach endpoint, which keeps the stat
row (keepTraffic), and the node's GetInbounds preloads ClientStats by
inbound_id alone, so the node goes on reporting the detached email under
that inbound. setRemoteTrafficLocked merged every reported entry: it
copied the node's stale total onto the master's per-email row, after
which the matching limits made the node's enable=false look genuine and
latched the client disabled on the master. A master quota edit only
reaches inbounds the client is attached to, so the next sync (every 5s)
wrote the stale quota back.

The same unchecked loop also created a fresh client_traffics row for a
reported email the master no longer had once its tombstone expired.

Snapshot entries are now gated on the inbound's settings.clients. When
the settings cannot be parsed, membership is unknown and every entry is
merged as before.

Closes #6724
2026-10-05 20:37:35 +02:00