Commit Graph

3737 Commits

Author SHA1 Message Date
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
MHSanaei 49fbcdc09c fix(frontend): wrap overview modal action buttons on long labels
The Geodata Auto-Update actions row is a non-wrapping flex row with no
gap. Its three buttons fit in English, but the longer Russian, Ukrainian
and Turkish labels make the row wider than the default-width modal, so
the buttons spill out of it. The row now wraps with an 8px gap.

.actions-row is a global class defined identically in VersionModal.css
and PanelUpdateModal.css, so both copies change to keep the cascade
order irrelevant. Verified in Chromium with a temporary Storybook story
rendering VersionModal in ru-RU: the first button sat 88px outside the
modal before the change and inside it after.

Closes #6737
2026-10-05 20:20:52 +02:00
Egor aacfaebab8 fix(tuic): client speed display and certificate button layout (#6723)
* fix(tuic): restore client speed and certificate layout

* docs(tuic): clarify native runtime and protocol behavior

* fix(websocket): preserve traffic updates from independent sources

* fix(docs): sync websocket traffic schema and drop restating TUIC tests

docs/public/openapi.json still described the old traffic event, without
clientTrafficSource/clientTrafficIntervalMs or the TUIC oneOf branch, so
the docs site showed a payload the panel no longer sends. No check
covers that copy.

The TUIC certificate layout test and the TUIC speed payload test only
read back the literals the code writes, so neither could fail on a real
regression. Both are removed, along with the className that existed only
for the layout test.

---------

Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
2026-10-05 19:46:46 +02:00
冰 e897b0957a fix(sub): append host serverDescription to hysteria links (#6740)
* fix(sub): append host serverDescription to hysteria links

A host's Description reached vless/trojan/ss through buildEndpointLinks, but
genHysteriaLink renders the fragment in its own externalProxy loop and never
added the suffix, so Happ fell back to its "Hysteria | hysteria | TLS" caption
for every Hysteria server on a host that also serves VLESS (#6738).

Reuse appendHappServerDescription with the description the endpoint map already
carries, so no key lookup is duplicated and a host with no description emits the
same bytes as before. genTuicLink (service.go:912) has the same gap; left alone
to keep this diff to the reported protocol.

Regression test is red without the fix for both hysteria:// and hysteria2://.

* chore(sub): trim the hysteria serverDescription regression test

The no-description test passed with and without the #6738 fix: the empty
description branch is already pinned by TestAppendHappServerDescription, so
it certified nothing about this change. Also cut the remaining test's comment
block to the two-line limit CLAUDE.md sets.

---------

Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
2026-10-05 19:05:13 +02:00
Chester Fishmans b42a1c0ba1 fix(systemd): harden shipped x-ui unit files (#6718)
* fix(systemd): harden shipped x-ui unit files

The units ran the panel as root with no sandboxing: systemd-analyze
security rates them 9.6 UNSAFE.

Add NoNewPrivileges, ProtectSystem=full with ReadWritePaths for the
default XUI_DB_FOLDER/XUI_BIN_FOLDER/XUI_LOG_FOLDER stores, kernel and
clock protections, UMask=0077, RestrictAddressFamilies, a
CapabilityBoundingSet with NET_ADMIN/NET_BIND_SERVICE/NET_RAW and
SystemCallFilter=@system-service.

PrivateTmp is deliberately omitted: the web updater hands a path inside
the system temp directory to a systemd-run transient unit, which does not
share the service's private tmpfs. ProtectHome stays read-only because
installs keep TLS certificates under the root home directory.

Fixes #6605

* fix(systemd): ship ReadWriteDirectories= alias for systemd < 231

ReadWritePaths= only exists since systemd 231; install.sh still supports
CentOS 7 (systemd 219), where the directive is ignored and ProtectSystem=full
would leave the panel state directory read-only, breaking its database.

* fix(systemd): keep root's DAC bits and regenerate the write paths

Two follow-ups to the hardening, both reported by review on #6718.

CAP_DAC_OVERRIDE and CAP_DAC_READ_SEARCH were dropped from the bounding set.
Root holds them normally, and a bounding set is subtracted from root too: the
panel could no longer read a private key it does not own (a Caddy-issued
certificate under its own state dir, an acme.sh home, any 0600 file owned by
another account). That fails quietly for TLS -- the panel listener logs the
tls.LoadX509KeyPair error and keeps serving plain HTTP, and Xray inbounds using
that key stop -- so both bits stay.

ProtectSystem=full plus a hard-coded ReadWritePaths list broke installs whose
XUI_DB_FOLDER/XUI_LOG_FOLDER/XUI_BIN_FOLDER live outside the defaults, and the
workaround of editing the unit did not survive an update, because install.sh and
update.sh reinstall the unit from the release tarball. The folders actually in
use are now resolved from the same env file the unit passes to the panel and
regenerated into x-ui.service.d/10-xui-write-paths.conf on every install and
update, so a relocated store stays writable and the list is not reset. The unit
keeps the plain-install defaults plus XUI_SERVICE, which the in-panel updater
needs when systemd-run is unavailable and it falls back to a child process that
inherits this sandbox while update.sh lands the unit again. Uninstall removes
the drop-in with the unit.

* fix(systemd): keep the seccomp whitelist off old systemd, tighten the rest

Review of the previous head found that SystemCallFilter=@system-service plus
SystemCallErrorNumber=EPERM is a hard regression on the platforms this PR means
to keep working. @-named filter groups exist from systemd 239 on, and older
systemd does not ignore an unknown group name: on <231 the name fails to resolve
and the filter stays the built-in whitelist of execve/exit/exit_group/
rt_sigreturn/sigreturn, on 231..238 it degrades to @default. Either way the panel
then gets EPERM on read/openat/mmap/clone and cannot start -- a CentOS 7 or
Ubuntu 18.04 install would come up dead after this update. The two directives now
live in the generated drop-in and are written only when "systemctl --version"
reports 239 or newer, so old hosts keep the rest of the hardening and simply go
without seccomp.

The same review listed three more items, all addressed here:

- a comment claiming ProtectSystem=full "keeps everything outside /var, /run and
  the listed ReadWritePaths read-only" -- that is `strict`; `full` locks down
  /usr, /boot, /efi and /etc;
- /etc/systemd/system was granted writable for the in-panel updater's fallback,
  but that fallback cannot work under this sandbox at all: update.sh also stages
  the release archive beside the main folder, replaces /usr/bin/x-ui and calls
  the package manager. The entry is gone and update.sh now stops up front with
  one clear message when the directories it needs are read-only, instead of
  failing halfway with "Failed to download x-ui";
- CAP_DAC_READ_SEARCH is redundant next to CAP_DAC_OVERRIDE, so the bounding set
  keeps just the latter.

Relocating a store by editing the env file alone is documented in the unit and
in the generated drop-in: the drop-in is only written by install/update, so one
of those has to be re-run afterwards.

Verified with a local harness (9 checks: plain defaults, relocated store read
from the env file, the same list produced by update.sh, duplicate collapse,
seccomp present at systemd 249 and absent at 238, read-only guard) and bash -n
on install.sh, update.sh, x-ui.sh. systemd-analyze is not available here, so the
unit files themselves are unverified by a parser.

* fix(systemd): actually wire the read-only guard, drop the superseded drop-in

Re-review of the previous head caught two leftovers from that commit:

- require_writable_update_paths was defined but never called, so the guard the
  unit comments, the commit message and the PR comment promise did not exist at
  all. It is now called at the top level, before install_base, i.e. before
  anything with a side effect: a sandboxed fallback run stops with one clear
  message instead of failing halfway, which on a relocated main folder meant the
  old install removed and the service folder rewritten before dying on /usr/bin.
- the drop-in this branch replaced (10-xui-write-paths.conf) is no longer written
  or referenced, but nothing removed it either. Whoever installed the build that
  wrote it keeps its wider list, including the writable service folder, until it
  is deleted by hand. Both generators now remove it.

Harness extended to 11 checks: the superseded file is gone after a run, the guard
is actually called, plus the previous nine (defaults, env-file relocation, same
list from update.sh, dedupe, seccomp at 249 / absent at 238, read-only guard) and
bash -n on the three scripts.

* fix(systemd): correct two comments and keep spaces out of the path list

Second-opinion review of the previous head (two models, both asked to state
platforms and versions) produced three actionable items: a wrong comment kept
from the earlier commits, a wrong generalisation about the filter groups, and a
path-list case that would leave the panel unable to start.

- the ProtectSystem= comment claimed strict leaves /var and /run writable. It
  does not: strict mounts the whole hierarchy read-only and only the kernel API
  filesystems stay as they are. The sentence was already wrong before this
  branch and moving it to ProtectSystem=full did not fix it.
- "the @-named filter groups need systemd >= 239" is the wrong generalisation:
  named groups exist since 231, it is @system-service that arrived in 239. The
  unit files, both script comments and the drop-in body now name the group.
- a folder containing whitespace (XUI_DB_FOLDER="/srv/panel data") was written
  into ReadWritePaths= verbatim. That directive is a whitespace-separated list,
  so the entry splits into "-/srv/panel" and "data", and systemd rejects the
  whole drop-in: the panel then does not start at all. Such folders are left
  out and reported to the operator instead; the other paths are still written.

Harness extended with three checks for the whitespace case (folder left out,
remaining paths intact, warning emitted) and the duplicate-store case now reads
its own env file instead of the previous one, so it tests what it claims.
14 checks plus bash -n on the three scripts, all passing.

* fix(systemd): act on the independent review of the drop-in generator

A read-only review of the branch head (another model, given the diff and the
sources, asked to cite only verified lines) confirmed the earlier work and
turned up four items that are fixed here:

- a folder name carrying a literal % went into ReadWritePaths= as it was, and
  systemd expands %-specifiers in unit files: with XUI_DB_FOLDER=/srv/x%-ui the
  entry no longer named the directory the panel writes to and the panel could
  not write its database. The path is now emitted as %%; the duplicate check
  keeps comparing the unescaped value.
- systemd older than 229/242/244 does not know NoNewPrivileges, ProtectClock,
  ProtectHostname and ProtectKernelLogs. It logs them and carries on, so
  CentOS 7 (systemd 219, which install.sh explicitly supports) runs with less
  hardening than the unit lists. install.sh and update.sh now print which
  protections need a newer systemd, which ones still apply, and that upgrading
  systemd is what changes it.
- the updater's writability guard asked [[ -w ]] about the parent directories.
  It creates and removes a probe file instead, so an immutable attribute or a
  full filesystem is caught as well (a read-only mount was already caught).
- the generator's comment claimed to resolve the folders the service actually
  uses, while the shipped unit hard-codes WorkingDirectory= and ExecStart= under
  /usr/local/x-ui. The comment now states what XUI_MAIN_FOLDER really feeds --
  the location install.sh/update.sh install into and the base for a relative
  XUI_BIN_FOLDER -- and that a relocated main folder needs the unit edited too.

Rejected from the same review, with the evidence: that [[ -w ]] cannot see a
read-only mount (access(W_OK)/faccessat consults __mnt_is_readonly before the
mode bits), and that the /etc ReadWritePaths entry is an exception granted for
/etc rather than a default store already in the list.

Harness extended: 19 checks (escaped %, the old-systemd note, whitespace and
duplicate folders, seccomp gating, the read-only guard) plus bash -n on the
three scripts, all passing.

* fix(systemd): name the hardening old systemd really ignores

The old-systemd note fired only below 239 and listed wrong versions:
RHEL 8 (239) and Debian 10 (241) silently lose ProtectHostname and
RestrictSUIDSGID (242), ProtectKernelLogs (244) and ProtectClock (245)
with no note, while CentOS 7 was told NoNewPrivileges (187) and
ProtectHome=read-only (214) were not applied although both are. The
note is now built from a directive/version table taken from
systemd.exec(5) and lists only what the running systemd lacks.

Also drop the removal of 10-xui-write-paths.conf: only an intermediate
commit of this branch wrote that file, no release ever shipped it.

---------

Co-authored-by: Кот <kot@zeroclaw.local>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-10-05 18:47:10 +02:00
Mr. Nickson a8d65a55b0 fix(update): run the database migration before starting the service (#6729)
update_x-ui() started x-ui.service and then called config_after_update right
away, which runs `x-ui setting -show true` and `x-ui migrate`. The service and
the CLI each run InitDB(), and with it every schema migration, on the same
database at the same time. On an upgrade that adds schema, the loser exits
with an error. Upgrading 3.8.5 to 3.9.0 stopped the service with
"duplicate column name: exclude_from_sub", and only Restart=on-failure
brought it back 5 s later.

Tolerating the duplicate-column error in the column helpers is not enough.
The same race also hits the tables new in 3.9.0: concurrent InitDB fails with
"table `node_pending_resets` already exists" and
"table `tuic_traffic_receipts` already exists". The cause is two processes
migrating at once, so the fix is to stop that from happening during update.

Run `x-ui migrate` to completion before the service is started, on both the
systemd and the OpenRC path. This mirrors install.sh, whose
config_after_install already migrates before the first start. The service and
the follow-up CLI calls then find the schema current, and their InitDB has
nothing to change.

Refs #6728
2026-10-05 16:48:50 +02:00