One mtg-multi process serves every client of an MTProto inbound from a
single TOML file and refuses the whole file over one value it cannot
parse. InstanceFromInbound already guarded the ad tag for that reason
but copied secret, quota and expiry as stored, so a malformed secret, a
year-10000 expiry or a near-MaxInt64 quota on one client took the
sidecar down for everyone on the inbound.
A client whose secret mtg cannot parse is left out with a warning, and
quota and expiry are capped at what mtg accepts.
UpdateInboundClient carried the stored forwardedPorts forward whenever
the incoming value was empty, and Client.ForwardedPorts was a plain
omitempty string, so clearing the field in the client editor, as its
hint says to, reported success while the settings kept the old ports
and the host kept relaying them to the client's device.
The field is now a pointer, as KeepAlive is: nil (not sent) keeps the
stored spec and "" withdraws it. The client record follows the live
spec on every update, and a one-time seeder fills records that still
hold their creation value from the inbound settings, so the editor no
longer posts an empty field that would drop forwards added later.
The one-time ClientsTable migration builds client records from each
inbound's settings with a plain Create, and gorm's default:true on
ClientRecord.Enable drops an explicit false from that insert. Every
client an operator had disabled before v3.1.0 was therefore stored
enabled on an in-place upgrade or a restored backup, and Xray, which now
reads that column, let the customer back in.
The seeder restates enable=false after Create, as the service paths do
since #6478, and a legacy client whose settings carry no switch stays
enabled, as releases before 3.1.0 served it.
The subscription routes run the HWID gate before they look up the
subId, and the gate read "no device limit" and "no such subscription"
alike, so a request naming any path segment with an X-HWID header was
answered 404 yet stored that segment, up to about 1 MiB, in
client_hwids for good. Anyone reaching the subscription port could fill
the disk until the panel could no longer save anything.
An unlimited subscription is tracked only while an enabled client owns
its subId, and a one-time seeder drops the rows already stored for
subIds no client owns.
The groups page lists clients.group_name as stored, while every group
action trims the name it is given. A name with surrounding white space,
saved by a node snapshot or by a client edit, was listed as a separate
group, and deleting, renaming or resetting that row acted on the real
group of the trimmed name instead.
Group names are trimmed wherever a client record is built or updated,
and a one-time seeder folds stored names onto their trimmed form.
A node snapshot was merged into the master's client records by email, and
the only ownership check filtered one derived copy of it: the node's
settings were still stored and re-synced later, its stats still merged,
and a record linked to the master as well as the node took every value
the node reported, including credentials rendered into the master's own
Xray config.
Each snapshot now passes one gate before anything is written. A record
also linked to the master or another node keeps the master's values
apart from per-link fields and the lifecycle the merge already settles;
a record linked only elsewhere, a detached record, a padded or
case-variant email and a new client holding another client's subId are
dropped from the stored settings as well. A soft-orphaned record can be
taken back only by the node whose merge orphaned it, which the new
clients.sync_orphaned_by column records. A node tunnel no longer owns
the peer keys of a client a master-local tunnel also serves.
A node-sync token, or a master enrolled with the mTLS certificate, may
push inbounds, and the node stored them as already validated. An
inbound is effectively config for a core running as root, so a push
could serve the host's files (hysteria file masquerade), relay to its
own services or the core API (masquerade proxy, fallback dest, REALITY
target), write files (masterKeyLog), use local or templated XDRIVE
storage, fetch URLs (ECH, realm masks), or run tun/http/mixed inbounds.
A push now refuses those fields unless the node already stores the same
value, which keeps rows configured by the node's own admin syncing:
relay targets must be public or another of this node's inbounds, and
only node-eligible protocols are accepted. Every caller is refused env:
references, which the core expands from the panel's environment, and
the inbound listen address is JSON-encoded into the config instead of
quoted by hand.
The session cookie key is generated once and kept forever. Keys stored by
older releases came from a weaker generator, and a stored value cannot be
told apart from a current one, so drop it once through a seeder and let
GetSecret store a fresh crypto/rand key on the next start. Admins log in
again once after upgrading; fresh installs record the seeder up front.
The access-log scanner and the cross-node merge stored the same IP list in
opposite orders, so neither saw a row as unchanged, and the merge re-read
every preloaded row and re-upserted every attribution row each sync cycle.
All writers now share one canonical encoder (timestamp, then IP ascending),
the first CAS is seeded from the preload, and identical attribution rows are
skipped. A create race between the scanner and node sync now merges into the
winning row instead of dropping the loser's IPs, which on PostgreSQL also
aborted the scanner's whole scan transaction.
Closes#6927
* perf(traffic): skip unchanged global writes between heartbeats
Persist directional usage changes immediately, including lower resets, while
refreshing identical global totals only at an inclusive one-hour heartbeat.
Use the GORM clock consistently for the existing 24-hour freshness window and
preserve local-only snapshots, quota enforcement, and downstream forwarding.
Fixes#6928
* docs(traffic): describe persisted global heartbeat timestamps
Replace the stale every-push model comment with the change-or-hourly-heartbeat
rule. Keep the local-counter separation, delta accounting, MAX fold and reset
semantics explicit without treating UpdatedAt as a last-push timestamp.
Address the nonblocking review feedback on PR #6931 without rewriting
published history.
Refs #6928
Rotating SNIs meant one host entry per domain. A host now takes a pool of
SNIs: with more than one, every generated config gets a random entry,
or, with the shared switch on, the whole host group uses one pick per
subscription fetch. Raw, JSON and Clash output, the panel's link preview
and node adoption all carry the pool.
sni stays the pool's first entry, so a rolled-back binary and API callers
that only know the single field keep working; an sni outside the pool
becomes its primary. A host with one SNI renders exactly as before.
In an LXC container with LXCFS the Overview showed the container's RAM
beside the host's swap, the host's CPU topology and a CPU percentage from
the host's view. Swap and uptime now come from LXCFS's virtualised files
and CPU from the container's cgroup v2 counter, normalised by the visible
vCPUs and any quota; container CPU and swap history is kept apart from
the earlier host samples.
A value the reader cannot measure (the first CPU sample, or a layout it
cannot verify such as cgroup v1) is reported as unavailable instead of 0:
the dashboard, the node list, node CPU history and the Telegram and
Discord uptime show a dash, and nodes keep the flags in a new
unavailable_metrics column.
Deleting the only host a client was bound to blanked that client on every
inbound. Once the host row was gone, the binding could no longer say which
inbound it guarded, so it failed closed everywhere: a client on inbounds A
and B, bound to one host on A, lost B's link the moment that host was
deleted, and nothing in the panel hinted at it.
Every host delete (host groups, the externalProxy sync, inbound deletion,
the depleted-inbound sweep and the orphan prune) now goes through
database.DeleteHosts, which keeps each deleted host's inbound in
host_tombstones in the same transaction. A binding to a deleted host closes
only that inbound; ids that resolve to no host at all still fail closed
everywhere. The Postgres Host step now runs the deletion cases as well.
Host rules applied per inbound only, so every client of an inbound got the same
subscription endpoints (#6618). A client can now be bound to one or more host
rules, and its raw, JSON and Clash subscriptions render only those hosts, in
either subscription order; an unbound client is unchanged. A binding whose hosts
are deleted, disabled or excluded from the format fails closed: the client gets
no links there but stays in traffic accounting. Host edits keep row ids, so
bindings survive them; nil leaves a stored binding alone and an empty list
clears it.
AmneziaWG clients accept a randomised PersistentKeepalive range such as 15-25,
which makes keepalive timing harder to fingerprint; the panel only stored a
single number. A client's keepalive may now be a range, validated the way
AmneziaWG parses it. The downloadable .conf and vpn:// profile carry the range,
Clash/Mihomo output gets its lower bound, and plain WireGuard stays scalar-only.
The range lives in an additive wg_keep_alive_range column, so the integer
column and an older panel binary keep working after a rollback.
The per-host Mihomo X25519 switch has been stored since managed hosts landed,
but no renderer read it, while every REALITY proxy was forced to
support-x25519mlkem768: true. On some networks DPI drops the second segment of
an ML-KEM ClientHello and blackholes the server for that client. A host with the
switch on now gets support-x25519mlkem768: false and no ML-KEM hint in its vless
link; switches saved while they had no effect are cleared once, so upgrading
does not silently change existing subscriptions.
A REALITY host's fingerprint now reaches the Clash and JSON outputs, and every
host endpoint gets its own deep copy of the stream, so one host's blank SNI,
fingerprint, Host header or path no longer inherits another host's value.
Operators who front a server with an IP range (a CDN or anycast prefix) had to
list each address as its own host. A host address may now be a CIDR, and every
subscription fetch expands it into up to randomCount (1–100, default 1) unique
random host IPs from the range, IPv4 and IPv6 alike, skipping the network and
broadcast addresses. The panel's own previews, QR codes and tunnel config
exports (WireGuard, AmneziaWG, TUIC, MTProto) expand it the same way, and the
externalProxy importer writes the model's default count so the re-sync still
recognises untouched imported hosts.
Every bearer-authenticated request, including each node-sync call, loaded all
enabled api_tokens rows and compared each stored SHA-256 hash in constant time.
Tokens are stored hashed, so the database can compare hashes directly: a
timing leak there reveals only a stored hash, which cannot authenticate. The
lookup is now one indexed seek on idx_api_tokens_token, which AutoMigrate
creates from the model tag on new and existing databases; scope and expiry
are still checked afterwards.
One lost heartbeat on a lossy link flipped a node offline and sent node.down,
and the next probe sent node.up, so a flaky node flooded Telegram, email and
Discord with alert pairs.
A new nodeDownThreshold setting (default 1, today's behaviour) works like
outboundDownThreshold: node.down fires once the failed-check streak reaches it,
once per outage, and node.up only after a node.down was sent. The announcement
is persisted in nodes.down_notified, so a restart inside the threshold window
neither drops the outage's node.down nor re-announces nodes already reported
offline; nodes offline at upgrade are seeded as announced.
Subscriptions build an inbound's links from its Host rows, and those rows are
imported from streamSettings.externalProxy only when the inbound is added. An
update that moved the external proxy returned 200, but every subscription kept
handing out the old address, port and security.
An update now re-syncs only the externalProxy entries it changed onto hosts that
are still exactly as imported. Unchanged entries are set aside first, so a host
the admin edited, disabled, deleted or shared is never re-imported next to the
admin's version, and a stream without the externalProxy key leaves hosts alone.
The first user is now created when the server starts, with a random
username and password printed once to its log (docker logs, the journal),
instead of a fixed pair seeded by InitDB. CLI calls that initialize the
database are often piped away, as the Docker entrypoint's port lookup is, so
only the server's own log reliably reaches the operator.
`x-ui setting -show` keeps reporting hasDefaultCredential: true until a user
exists, so install.sh still sets its own credentials on a fresh install.
Existing databases are not changed. Verified on a built binary: fresh
setting -show, install.sh-style credential set, first start, login and
restart. The PostgreSQL lane was not run locally; the change adds no query.
* fix(node-sync): do not latch node disable when master allows (#6870)
nodeDisableIsStale trusted any node-reported enable=false whose limits matched
the master's, treating it as a genuine verdict and latching it on the master.
But matching limits no longer prove the verdict: a single stray false
(a depleted snapshot the master later re-enabled, a stale
disableInvalidClients hit, or a push that lagged a quota reset) leaves the
client stuck disabled on the node, and every master tick flips the client's
state again. This is the loop reported in #6870.
Make nodeDisableIsStale a single rule: stale iff the master's own deadline
and quota still permit the client. A genuine combined-quota or expiry
disable (masterLimitsAllowClient == false) still latches, preserving #4917.
Mirror the same rule in ClientTrafficEnableMergeExpr so the SQL UPDATE and
the Go in-memory replay cannot drift.
* fix(node-sync): keep node usage over quota latching a node disable
Dropping the matching-limits latch (#6870) let the master re-enable a
client the node had genuinely counted past its quota whenever the
master's own counters lagged, e.g. after a seeded-at-zero adoption,
reopening #4917. A node's enable=false now stays latched when the
node's own usage already spends the master's quota; otherwise the
master's limits decide, so a stray disable no longer loops.
Restores TestNodeQuotaDisable_SameExpiryStillLatches as it was on main.
---------
Co-authored-by: Alireza Ahmadi <alireza7@gmail.com>
* feat(auth): add node-admin API token scope
Keep ordinary node-sync credentials unable to self-update a remote panel while allowing explicitly elevated node administrators to inherit synchronization access and invoke updatePanel.
* style(docs): format API scope table
* fix(auth): preserve mTLS node-sync semantics
Allow only an explicit node-admin bearer to elevate a verified mTLS request. Existing admin, monitor, and node-sync bearer values continue to use the historical node-sync identity on mTLS connections.
* chore(ci): rerun flaky checks
---------
Co-authored-by: libmur-dev <333915961+libmur-dev@users.noreply.github.com>
A host's verify_peer_cert_by_name must only hold a server name an
operator typed. The column started life as a boolean, and the migration
that was meant to discard those values runs after AutoMigrate, which has
already cast the column to text: SQLite's table rebuild stores 0/1 as
"0"/"1", and Postgres' USING col::text stores "false"/"true". The
migration then sees a text column and leaves the values alone.
Since 3.10.0 the JSON subscription lifts host TLS verification fields to
the level xray reads, so that leftover "0" is now sent as the cert name to
verify and clients such as Happ refuse the server certificate.
A one-shot HostVerifyPeerCertByNameBoolFix seeder blanks exactly the four
cast values on existing databases; real names are untouched. The
Postgres variant joins the CI schema job, since that cast only happens
there.
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.
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.
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.
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.
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.
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.
A SQL dump replay only has to rebuild the tables of the database it
restores into. Run it on a single connection whose attached-database
limit is zero, so the script cannot open or create any other file.
* feat(tuic): implement native in-process Go TUIC v5 server
- Implement native TUIC v5 protocol server on pure Go using quic-go
- Bridge decrypted TCP/UDP traffic into Xray-core via loopback SOCKS5 inbound
- Support full Xray routing rules (geosite/geoip) and cascading outbounds
- Implement atomic per-client traffic accounting with TotalGB and ExpiryTime
- Add automatic legacy cleanup for older Rust tuic-server binaries, configs, and orphaned processes
- Eliminate external Rust tuic-server downloads from install/CI scripts
* fix(tuic): address traffic accounting, client reload, and socket lifecycle issues
* fix(service): update checkTuicSocksReverseConflict to use bindAddr for listenOverlaps
* fix(tuic): resolve traffic double-accounting, UDP fragmentation, and socket lifecycle issues
* feat(tuic): complete native Go integration and address audit findings
- Integrate an isolated QUIC fork pinned to a specific commit
- Preserve original QUIC dependencies for Xray, Hysteria and Gin
- Apply BBR, CUBIC and Reno to server connections and exported client profiles
- Bridge Xray BBR with correct monotonic time and congestion type conversions
- Handle congestion sender recreation after PMTU changes
- Update congestion control for new connections without restarting the listener
- Preserve existing connections and their selected congestion controller
- Apply per-inbound log levels through the shared panel logger
- Add lifecycle, authentication and TCP/UDP relay events without exposing secrets
- Rate-limit repeated authentication and relay warnings
- Support native and QUIC UDP relay modes on the same listener
- Recover UDP associations after relay worker failures
- Fix TCP relay cancellation, idle shutdown and half-close handling
- Close active sessions when client credentials are revoked or disabled
- Track traffic by immutable client statistics IDs across email and UUID changes
- Prevent ambiguous accounting and duplicate UUIDs within TUIC inbounds
- Persist pending traffic in a durable shutdown journal
- Replay journal batches transactionally without duplicate accounting
- Report server shutdown failures through the shared logger
- Preserve legacy flat and nested TUIC settings compatibility
- Normalize congestion controller values consistently across backend and frontend
- Preserve controller, UDP mode and SNI in client links and subscriptions
- Separate client profile options from server settings in the TUIC form
- Keep certificate path autofill explicit when changing client SNI
- Align UDP packet size validation with protocol limits
- Simplify and localize TUIC field hints and certificate autofill messages
- Add controller, TCP/UDP, logging and live settings update tests
- Add accounting identity, journal replay and shutdown regression tests
- Add relay recovery, session revocation and legacy frontend form tests
* fix(service): alias the TUIC duplicate-UUID subquery for PostgreSQL < 16
syncInboundClients runs a COUNT(*) FROM (subquery) for every client sync,
whatever the protocol. PostgreSQL before 16 rejects a FROM subquery with
no alias, so on the distro PostgreSQL install.sh provisions (14 on Ubuntu
22.04, 15 on Debian 12) every client add or edit failed with SQLSTATE
42601. Reproduced against postgres:15 with the new env-gated test.
* fix(database): create tuic_traffic_receipts through the model migration
AddTuicTrafficBatch issued CREATE TABLE IF NOT EXISTS at runtime, a schema
change outside db.go. The table was invisible to allModels and
migrationModels, so x-ui migrate-db dropped the receipts and a retained
journal could be counted twice after a SQLite to PostgreSQL move. It is
now a GORM model in both lists, and the insert uses OnConflict DoNothing.
* chore(tuic): skip the ICMP-dependent relay test on Windows, drop dead collectors
Go disables SIO_UDP_CONNRESET on Windows, so a dead UDP bridge never fails
a read there and TestAudit3UDPAssociationMustRecoverAfterBridgeReadFailure
was red on every Windows run. Server.CollectTotalTraffic and
Manager.CollectTraffic had no caller.
* refactor(tuic): serve TUIC on apernet/quic-go instead of a personal fork
The native server depended on github.com/poise52/quic-go, a personal fork
of apernet/quic-go patched only to pick the congestion controller before
the handshake. That put a second QUIC/TLS stack in the binary that no
upstream security fix reaches. apernet/quic-go is already in the graph
through xray-core and exposes SetCongestionControl, so BBR is now installed
on each accepted connection with Xray's own congestion.UseBBR; the
cross-module BBR adapter is gone.
apernet ships New Reno as its only built-in sender, so a cubic setting is
served as new_reno server-side (clients still get cubic in their profile).
The test inspectors now read the sender under congestionMutex, which the
post-handshake install writes under.
Linux loopback, single stream through Xray: 2428 -> 3383 Mbit/s (bbr).
---------
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
Bump xtls/xray-core to b26a91de4f (v26.9.30) and the three binary pins
(DockerInit.sh, release.yml Linux + Windows) in lockstep. No deleted
symbols; the sing and sing-shadowsocks indirect deps drop out with the
SS2022 rewrite.
XDNS finalmask (#6718) replaced its string lists with objects: domains
are {name, types, edns0, lenLimit, labelLimit} and client resolvers
{type, settings.addr}. The loader no longer parses the old lists, so a
single stored xdns mask keeps the whole core from starting. The new leaf
package internal/util/maskcompat converts them: "name[:type]" becomes a
domain and "name[:type]+udp://addr" a domain plus a udp resolver. A bare
name maps to TXT, the type legacy clients queried by default, and each
converted domain keeps the 1232-byte EDNS0 the old code always used
(without it the server caps answers at 512). It runs from:
- the XdnsFinalmaskObjectsFix seeder, over inbound streams, hosts, the
xray template, the global sub-JSON mask and cached subscription
outbounds;
- inbound save (normalizeStreamSettings) and GetXrayConfig, for rows
that never went through the seeder;
- both link importers, since fm= from an older panel carries the lists.
The finalmask form edits the object shape (every key needs a registered
field, or the finalmask watch drops it on save) and lifts legacy masks
on open. The udp-mask golden fixture moves to the object shape, which
TestGoldenStreamFixturesBuildInXray now builds through the core. The
wire format changed as well, so pre-upgrade clients need the new core.
WireGuard outbound (#6771) dropped settings.domainStrategy and the
remoteDNS "local" mode. The endpoint lookup now follows
sockopt.domainStrategy and in-tunnel targets the outbound's
targetStrategy. The old key is silently ignored, which undid the
IPv4-first endpoint lookup the WARP outbound depends on (#5205), and
"local" now panics the core at startup because remoteDNS goes through
netip.MustParseAddr. The WireguardDomainStrategyFix seeder moves a stored
family preference to both keys (a value already set wins) and turns
"local" into targetStrategy; the outbound form lifts legacy rows the same
way and drops its select, the WARP modal writes the new placement, and
the inbound form loses a field the server never read.
ValidateOutboundConfig now refuses a non-IP remoteDNS entry, which
conf.Build() lets through, on template save and for outbound
subscriptions.
Noise finalmask items accept type "exp" (#6862), a tag expression. The
form offers it for noise items only: header-custom items go through the
core's PraseByteSlice, which refuses it.
TUN gained autoSystemDnsToGateway (Linux) and autoSystemWfpBlockLeak
(Windows). Both pass through the settings schema so a value set in JSON
survives the next form save.
MASQUE (inbound, outbound, transport) and the XDRIVE transport are new
protocols the panel does not offer yet; their new loader refusals only
cover configs the panel never generates. The FakeDNS IPv6 pool default,
the SS2022 rewrite (same gRPC account; emails are now deduped
case-insensitively, as the panel already does), the restored udphop
interval default and the rest change no panel-facing config.
Invariant: a client traffic reset zeroes every counter that enforces the
client's quota - the master's, each hosting node's, and the local MTProto
sidecar's. A node cuts a client on its own local counters, and the master
adopts that verdict when both judged the same limits (#4917).
Node counters: ResetClientTraffic tried the node once and dropped a
failure ("nothing replays a reset"); BulkResetTraffic,
ResetAllClientTraffics and ClientService.ResetAllTraffics never told the
node at all. The node kept its pre-reset usage, switched the client off
again on its next tick, and the master latched that - a client shown at
zero usage stayed disabled.
Every reset path now queues a node_pending_resets row per hosting node in
its own transaction. It is delivered right after commit (per-client
endpoint up to the push threshold, bulkResetTraffic above) and replayed by
the node sync ahead of its snapshot, and dropped only once the node
accepted it. While one is owed, the merge takes only that client's usage
from the node, not its enable or limits. Deliveries to a node are
serialized so a reset is not sent twice.
Sidecar quota: BulkResetTraffic, ClientService.ResetAllTraffics and
auto-renew zeroed the panel counters but not mtg's own quota counter, so
the sidecar kept refusing a renewed or reset MTProto client. They now
reset it too, scoped to the affected clients.
Panels up to v2.x stored a client's tgId as a string, "" when unset. The
startup migration heals copies already in the database, but an exported
inbound imported into a current panel goes straight to AddInbound, whose
typed client parse failed with "cannot unmarshal string into Go struct
field .0.tgId of type int64", so every such import was refused.
AddInbound now runs the legacy normalizer the clients-table seeder already
used (moved from database to model so both share it) over the incoming
settings before parsing them. String numbers become integers and empty ones
are dropped, and the settings are stored in that shape. The same applies to
/inbounds/add callers still sending the old types.
The seeder change is a pure move; the PostgreSQL lane was not run (no
Docker on this host) and is left to CI.
Closes#6663
InitDB assigned the new pool over the old one without closing it. The
panel's own restore flows call CloseDB first, but any other re-init
leaked the replaced pool and its handle on the database file. On Windows
that handle blocks deleting the file, which is why the four GetApiToken
CLI tests failed their t.TempDir cleanup there: dbtest.InitDB opened the
store, then GetApiToken's own InitDB replaced it. InitDB now closes the
previous pool itself; sql.DB.Close is idempotent, so the restore flows
behave as before.
* feat(inbound): excludeFromSub hides links without disabling
Add a per-inbound flag that omits subscription output while keeping the
inbound enabled for Xray, auth, and traffic accounting. Fixes#6435.
* fix(inbound): excludeFromSub review follow-ups
gofumpt model.go, sync docs OpenAPI, keep excludeFromSub master-authored
on node mirror, and exercise the legacy add-column migration path in tests.
* fix(sub): keep excluded inbounds' clients in the usage header
The excludeFromSub filter sat in getInboundsBySubId's SQL, so an excluded
inbound's clients never reached seenEmails in the raw, Clash or JSON
renderer. A client that lives only on a hidden inbound (one client per
inbound sharing a subId) dropped out of the Subscription-Userinfo usage,
quota and expiry and out of the info-node state, while the inbound kept
serving it and counting its traffic.
The query returns every enabled inbound again; each renderer skips an
excluded inbound's links but still counts its clients, the same rule the
Clash renderer already applies to external links it cannot express.
---------
Co-authored-by: mrchatam <mrchatam@users.noreply.github.com>
Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
Most tests opened a throwaway panel DB with database.InitDB, which runs the
full AutoMigrate + seed on an empty file every time: ~230ms, and ~850ms under
-race because GORM's reflection-heavy migration is what the detector slows
most. internal/web/service does this in ~550 of its 830 tests, so the CI race
job spent ~10 of its ~14.6 minutes re-migrating empty databases.
internal/database/dbtest.InitDB migrates once per test process, then hands
each test its own copy of that file (~130ms under -race) and registers the
CloseDB cleanup. The copy then goes through InitDB like a panel restart, so
every test still starts from the state a fresh install has. Tests that reopen
an existing file, migrate a hand-built legacy DB or target Postgres keep
calling database.InitDB.
Locally under -race: internal/web/service 626s (last CI run) -> 114s,
internal/sub 246s -> 35s.
* feat(clients): add calendar weekly renewal and schedule previews
Expose fixed-day, calendar-weekly, calendar-monthly, and disabled renewal
through one shared selector in individual and bulk client forms. Store the
weekly weekday separately (Monday 1 through Sunday 7) and use panel-local
calendar dates rather than a fixed 168-hour duration. Resolve skipped or
repeated midnights to the first valid instant of the selected date, and skip
an entirely nonexistent calendar date rather than changing the weekday.
Reuse the existing renewal writer and share its boundary alignment and
per-period catch-up calculation with an authenticated, read-only preview.
Keep monthly precedence for legacy records, fixed-day interval semantics,
maximum renewal allowances, first-use durations, and operator-disabled
settings unchanged. Selecting a mode does not rewrite an existing cutoff;
an unset calendar cutoff requires an explicit action to choose the first.
The last-valid-second preview uses the stored exclusive expiry, even when
the billing calculation aligns a legacy last-second cutoff up to midnight.
Carry weekly schedules through client persistence, paging, enable toggles,
inbound settings, and node traffic reconciliation. Migrate missing or nullable
weekday columns to disabled by default without altering existing limits, and
include the new isolated-schema PostgreSQL regression in the live CI gate.
Regenerate API contracts and reference documentation, add lifecycle and form
regressions, and document timezone, quota-reset, and upgrade considerations.
All participating nodes must be upgraded before weekly mode is enabled;
older binaries ignore the new field. Independent periodic traffic resets and
the optional month-end subscription-header display are not changed.
* fix(clients): validate renewal schedules across inbound write paths
Reject conflicting weekly/interval/monthly schedules and out-of-range
weekdays on inbound creation and edits, legacy one-client apply paths,
record/link synchronization, and traffic metadata writes. Validate imported
traffic snapshots as well, before any inbound or client is persisted, so
an inbound API cannot create a client that the clients page cannot toggle.
Merge a weekly-related schedule as one timestamp-selected tuple rather
than filling its zero fields from another renewal mode. Preserve empty
migration snapshots and the existing non-weekly monthly/interval merge
semantics. Renewal caps, counters, credentials, and deadlines are unchanged.
Add regressions for nine write paths, unchanged records and runtime calls
after rejection, valid inbound clients remaining editable, and duplicate
record merges between individually valid renewal modes.
* docs(clients): clarify depleted-client deletion risks on downgrade
Explain in English and Chinese that older versions not only stop weekly
renewal: their depleted-client cleanup can delete a weekly-only client once
its expiry or quota is exhausted. This is conditional on cleanup, not an
automatic deletion caused by downgrade itself.
Recommend backing up and converting weekly schedules to a mode supported
by every participating version before rollback, and avoiding cleanup while
mixed versions or unconverted clients remain. Merely disabling weekly
renewal does not restore the old binary's missing purge protection.
* fix(clients): bound weekly renewal date searches
Limit the search for a valid weekly calendar date to eight candidates so
an unusual timezone cannot monopolize the single traffic writer. Exhaustion
returns the original instant, allowing the existing catch-up forward-progress
guard to stop without advancing expiry, consuming an allowance, resetting
traffic, or falling back to a fixed-duration schedule that can drift.
Reject a non-future calendar suggestion in the read-only preview instead of
offering an immediately expired initial cutoff. Also report failed weekly
catch-up as a search error when allowances remain, not as cap exhaustion.
Existing preview errors use the form's current warning; no API schema or
locale changes are needed.
Exercise exhaustion with a synthetic valid TZif containing twelve skipped
Sundays. This fault-injection case was red without the bound; it is not a
claim that a production IANA timezone was observed hanging. Keep the Havana
and Apia regressions for real skipped/repeated midnights and absent dates.
* fix(tests): isolate weekly renewal preview timezone
Stop the weekly search regression from replacing process-global time.Local.
CI caught that assignment and its cleanup racing with background timer reads
through time.Now, even though the top-level tests do not use t.Parallel.
Pass the timezone and current instant into the unchanged preview calculation.
The public service still validates the request and resolves the panel timezone;
API responses, renewal accounting, and persisted client data are unchanged.
Use fixed dates for both suggestion and catch-up exhaustion, removing the
test's dependency on today's date and its unnecessary database setup. Keep a
bounded-lifetime background clock reader to expose future global-timezone
mutations under the existing race gate rather than disabling that check.
* ci: retrigger PR checks
Create an empty commit to request a fresh pull-request CI run after release dependency downloads failed with network errors.
No source, dependency, or workflow changes are included. Retry the existing checks without bypassing them.
* ci: retry PR checks and record deferred download hardening
Request another CI run after the amd64 release job compiled successfully but failed during dependency fetching with exit code 4 (network failure).
Record possible follow-up improvements for the Linux release fetch helper:
- Print each download URL and destination, and preserve error details.
- Reuse the existing curl configuration with up to five retries; add connection and per-attempt timeouts and a bounded retry window.
- Download to a temporary file and promote it to the final filename only after a successful, non-empty transfer. Keep the job failing if downloads ultimately fail.
- Validate successful downloads, recovery after a temporary failure, and correct failure after persistent errors before shipping such a change.
These improvements are intentionally deferred, not implemented or tested by this commit. This commit is empty: renewal logic, dependencies, workflow configuration, check requirements, and TLS verification remain unchanged.
---------
Co-authored-by: JacktheRanger <219502738+JacktheRanger@users.noreply.github.com>
The inbound TLS form offered cipherSuites as a closed single-choice list,
but xray reads the value as a colon-separated list and accepts any name Go
knows, so several suites or one missing from the list could not be set.
Both the inbound and the new host field now use a tag picker that keeps
the stored value as the colon-joined string xray expects; old single
values open unchanged.
A host's cipher suites replace the inbound's in the JSON subscription
stream, and a blank field inherits them. Share links and Clash carry no
cipher suite parameter, so their output is unchanged.
The core reads a dns rule's qType as a PortList, which drops a bare numeric
0 (infra/conf/common.go: `if number != 0`), and a rule with no qTypes
matches every query. A stored `"qType": 0` therefore does not target query
type 0: it drops, refuses or hijacks all DNS through that outbound.
A qType the panel writes has to be read by the core as exactly the query
types it names. Four writers broke that:
- DNSOutboundLegacyKeysFix rewrote a lone blockTypes [0] into "qType": 0,
so "block type 0" became "block everything" on upgrade.
- That seeder shipped in v3.8.0 and is recorded as done, so fixing it does
not reach installs that already ran it. DNSOutboundQTypeZeroFix spells
any stored numeric qType 0 as "0" once, protocol id matched like the core.
- The outbound form adapter turned a typed "0" into the number 0.
- The Xray template editor saves raw JSON past that adapter; the save now
applies the same rewrite.
Each writer is pinned by a test that fails without its part. The rewrite
and the repair compare policies as the pinned core builds them, and the
repair runs through runSeeders over a database whose legacy-keys seeder
already ran, on SQLite and PostgreSQL 16.
* fix(xray): read an outbound protocol id the way the core does
xray-core lowercases a protocol id before it looks up the handler
(infra/conf/loader.go: `id = strings.ToLower(id)`), so a template that
spells the direct outbound "Freedom" runs as freedom. The three config
rewriters compared the id case-sensitively, so such an outbound was
skipped while the seed was recorded as applied: the refused
sockopt.addressPortStrategy stayed and xray-core refused to start.
* fix(xray): match an outbound protocol id case-insensitively
xray-core lowercases a protocol id before looking up its handler, so a
template that spells the direct outbound "Freedom" runs as freedom while
this rewriter skipped it and left the deprecated placement in place.
* fix(xray): re-run the freedom finalRules rewrite where its seeder was gated
Both finalRules seeders recorded their rows before the predicate could see
an outbound spelled "Freedom", and a recorded row is never re-run, so the
corrected predicates alone left the #6037 private-egress hardening
unapplied on every panel that had already run them. The new one-shot
seeder replays both rewrites, and only when the config actually carries a
differently spelled freedom outbound, so stock lowercase configs stay
byte-identical.
* fix(xray): migrate the dns outbound off its legacy nonIPQuery and blockTypes
xray-core logs both keys as deprecated on every config load, and refuses them
outright next to rules. The panel's own dns outbound card wrote them with
defaults until it switched that card to rules, so a panel that ever had one
keeps warning at every start, and the card no longer reads them back — saving
that outbound from the current UI silently dropped the policy.
The seeder converts them into the three rules the core's legacy builder
produced, in its order, then drops the keys.
* fix(xray): read a dns outbound's null keys and protocol id like the core
Two details the seeder got wrong, both found reviewing the diff against the
pinned loader. A JSON null is a present key with a nil value here but a nil
pointer there, so `nonIPQuery: null` was rewritten into a reject policy the
core never built, and `rules: null` hid the legacy pair that the core does
still read — dropping the operator's policy on upgrade. And the core
lowercases the protocol id before dispatching, so `"protocol": "DNS"` was
never migrated and kept warning.
* fix(xray): place the freedom domain strategy where the core reads it
freedom resolves through the socket layer, so xray-core reads
sockopt.domainStrategy and treats both other placements as legacy: it warns on
every config load for the outbound-root targetStrategy it migrates itself, and
again for the settings-level domainStrategy it deprecates. The panel wrote
exactly those two keys from its Freedom Protocol Strategy select, the outbound
form card, and the IPv4 routing helper, so any install that had configured a
strategy logged a deprecation warning on every start.
The strategy now travels in streamSettings.sockopt everywhere the panel emits
it: the Basics select, the outbound form (including the JSON tab, which shares
the same adapter), the shipped default template, and the IPv4 outbound the
routing helper injects. Reading mirrors the loader's own order — root
targetStrategy, then the settings keys, then sockopt — so the card keeps showing
the value the core would actually run with, and saving drops the legacy keys
instead of leaving them behind.
A seeder moves the keys for configs already stored in the database, following
OutboundRemovedKeysFix. The shared outbound-root Target Strategy field is hidden
for freedom, since the core migrates that key into the very sockopt value the
card writes and two knobs for one value would race.
Tests: placement round-trips and the migration table run through the real
vendored core (a captured log handler proves the warning is gone after the
rewrite and present before it), and the modal asserts freedom offers a single
strategy field.
* test(database): seed the template row the seeder test needs
A fresh InitDB creates no xrayTemplateConfig row — the panel's setting defaults
live in the service layer — so the test has to insert the legacy template itself
and then assert the seeder's history gate stops a second pass from rewriting it.
* fix(xray): keep one strategy control per outbound, seed the row in tests
Review findings: the Transport tab's Sockopts block renders for freedom too, so
its Domain Strategy select and the freedom card wrote one sockopt value between
them and the card won on save — the field is hidden for freedom now, leaving the
card as the single control. The seeder is also pre-marked on a fresh install so
it does not run on the second start, and the seeder test seeds the template row
itself (a fresh InitDB has none) and asserts the rewrite structurally instead of
grepping for a key name that sockopt also uses.
Saving the node form writes the grown selection and marks the node dirty
in one transaction. On the next tick ReconcileNode runs before the
snapshot merge, and its delete sweep treats a selected tag with no
central row as "deleted on the master" — so an inbound the operator just
ticked in the picker (or every unselected one, when switching the node
to "all") is deleted from the node before the import that would have
created its row ever runs.
Nothing on disk separates "pending import" from "deleted while the node
was unreachable", but the pre-adoption guard already expresses the
former: while inbounds_adopted_at is zero the sweep waits for a clean
sync to adopt. A save that grows the managed set now zeroes it again,
and the same clean sync re-stamps it, so the offline-delete sweep is
only deferred by one successful sync, not disabled.
The trade: an inbound deleted on the master while the node was
unreachable is re-imported instead of swept if the operator grows the
node's selection during that same outage. That is visible and
recoverable, where the previous behaviour destroyed a live inbound.
Closes#6329
* fix(inbounds): allow negative subSortIndex for subscription order
Preserve explicitly set negative indices so primary inbounds can sort
ahead of the default without renumbering peers; keep 0/omitted → 1.
* fix(inbounds): gofumpt model.go and trim subSortIndex comments
---------
Co-authored-by: mrchatam <287639636+mrchatam@users.noreply.github.com>
* Feat(tuic): Implement native TUIC v5 protocol support via Rust sidecar daemon
- Add internal/tuic package for official tuic-server sidecar lifecycle management, configuration generation, and graceful process control
- Bridge decrypted TUIC QUIC traffic into loopback Xray SOCKS5 inbounds (63200+id) for traffic accounting, statistics, and routing rules
- Implement periodic reconciliation job (cadence @every 10s) and immediate runtime synchronization on inbound/client mutations
- Add TUIC inbound & multi-user client settings (UUID + Password authentication) in Web UI with SNI auto-fill and panel certificate loader
- Integrate tuic:// subscription links and Clash.Meta (Mihomo) proxy generation for TUIC
- Update install.sh to automatically download and install official tuic-server release for x86_64, aarch64, and armv7
- Add full localization for TUIC protocol across all 13 supported languages
* Feat(install): Support custom repository and branch in install and update scripts
* Ci(release): Enable publish-dev for feature branch and workflow dispatch
* Feat(sub): Add TUIC to subscription resolution and client QR config generator
- Add 'tuic' to getInboundsBySubId SQL allowlist to resolve TUIC inbounds in subscriptions and sub links
- Enhance buildTuicProxy in Clash subscription generator with robust host and credentials resolution
- Add tuicConfig.ts to generate standalone Clash/Mihomo YAML configuration
- Add dedicated TUIC Config tab in ClientQrModal with QR code and .yaml download button
- Add localization keys for TUIC config across all 13 supported languages
* Fix(tuic): Exclude TUIC from native Xray inbounds and strip udp_relay_mode from server config
- Exclude model.TUIC from native Xray inbounds in GetXrayConfig to prevent Xray startup failure
- Remove udp_relay_mode from tuic-server JSON configuration builder
- Update install.sh to install tuic-server binary to both xui_folder/bin and /usr/local/bin
* Fix(install): Fallback to dev-latest when releases/latest is not present on fork
* Feat(tuic): Add real-time online status and LastOnline tracking for TUIC clients
- Track client activity by mapping client UUID in tuic-server logs to email
- Integrate TUIC active clients into XrayTrafficJob to refresh local online clients
- Bump LastOnline timestamp in database and broadcast live online status over WebSocket
* Feat(tuic): Implement real-time traffic statistics and live speed reporting for TUIC
- Collect precise I/O traffic deltas for tuic-server child processes via /proc/<pid>/io
- Aggregate and attribute TUIC traffic deltas per client in tuic Manager
- Integrate TUIC traffic deltas into XrayTrafficJob to update database and broadcast live speed
* Feat(tuic): Finalize TUIC v5 integration with 1:1 traffic counting and orphan process cleanup
- Use exact 1:1 byte delta accounting from /proc/<pid>/io
- Add killStrayTuicProcesses to terminate orphan sidecars on panel startup
- Fully integrate TUIC with subscriptions, live speed meter, and all 13 locales
* Feat(frontend): Polish TUIC UI, support bulk operations, and update translations
- Align TUIC inbound certificate form with standard 3X-UI layout (Set Default Cert, Clear)
- Remove extra subtitle hint text from TUIC inbound form fields
- Support TUIC in client bulk attach/detach and bulk add modals
- Add TUIC badge color to client info modal, clients table, and host list
- Update password tooltip across all 13 locales to include TUIC
- Remove obsolete dead translation keys across all 13 locales
* Chore(ci): Finalize TUIC v5 bundling across release workflow, Docker, and scripts
* Feat(openapi): Update OpenAPI generator and schemas for TUIC types
* Fix(backend): Address core review findings for TUIC types, port checks, and xray bridge
* Refactor(traffic): Isolate proc reading with build tags and decouple TUIC metering into TuicJob
* Feat(client): Add TuicServer to InboundOption, fix config export and clean share links
* Fix(frontend): Register TUIC in multi-user helpers, tracked protocols, and tag derivation
* Chore(openapi): Re-generate OpenAPI specification and sync Zod schemas
* Chore(scripts): Add Alpine musl binaries, 386 and Windows packaging, and anchor pkill
* Fix(review): Remove stale import, correct binary names, switch to musl, and drop unreachable relay gate
* Feat(frontend): Show share link in Inbound Info and display UDP tag for TUIC
* Docs: Add TUIC v5 configuration guide and link specifications
* Docs(tuic): Correct Clash Meta configuration parameter to reduce-rtt
* Fix(tuic): Generate client credentials on copy, enforce ID/password validation, and add i386 to DockerInit
* Fix(tuic): drop unused relay, fix traffic accounting, and honor host endpoints
- Drop unused loopback SOCKS relay and eliminate port collision with AmneziaWG
- Correct inbound traffic calculation without double-counting
- Drop heuristic client traffic division while retaining online tracking
- Support externalProxy host fan-out and conditional parameters in share links
- Scope orphan process termination to managed config directory
* Fix(tuic): enforce client quotas, decouple Xray restart, and sync openapi schemas
- Regenerate OpenAPI, Zod schemas, and TypeScript types without route_through_xray
- Populate clientTraffics in TuicJob to enforce client quotas and first-use expiry
- Split process I/O delta into up and down in Process.CollectTraffic
- Remove SetNeedRestart from updateTuicInbound to prevent Xray session drops
- Use InstanceFromInbound for default ALPN and UDP relay mode in tuic:// share links
- Support allow_insecure on externalProxy host endpoints without parameter collision
* Fix(tuic): attribute client traffic only on single-user inbounds and sync link defaults
- Attribute I/O deltas to the client only when the inbound has exactly one configured client, avoiding false billing and disablings on multi-user inbounds
- Aggregate client traffic by email in TuicJob so clients on multiple inbounds don't lose deltas
- Match frontend genTuicLink defaults for alpn and udp_relay_mode with backend subscription links
* Fix(tuic): gate client traffic by total sidecar clients and require client email
* Fix(tuic): enforce inbound-only traffic limits and disable client totalGB
* fix(tuic): restore delayed start, remove client totalGB rejection, and document linux-only limits
* fix(tuic): anchor pkill, fix io baseline/split, escape yaml, and deduplicate start errors
* fix(tuic): prevent traffic double-counting, ensure info log level for delayed start, and broaden pkill matching
* fix(tuic): address review round 11 findings
- internal/sub/json_service: skip tuic protocol in json subscription to prevent direct routing leak
- internal/sub/clash_service: honor externalProxy/host row allowInsecure, sni, and alpn in buildTuicProxy
- internal/web/runtime: decouple tuic inbound add/delete from xray restart
- internal/tuic/config: restore user log-level options (warn, error) without forced info clamp
- frontend/src/lib/xray/inbound-link: fix duplicate remark suffix and apply externalProxy TLS overrides
- frontend/src/schemas/protocols/stream/external-proxy: propagate allowInsecure through host mapping
- tests: add coverage for json sub skip, clash proxy overrides, and link generation
* fix(tuic): meter inbound traffic through a UDP relay and bracket IPv6 binds
Review repairs on the TUIC v5 sidecar integration:
- Inbound traffic was read from the sidecar's /proc/<pid>/io rchar, but
the kernel only counts read()/write() there and tuic-server moves its
sockets with recvfrom/recvmmsg/sendmmsg/sendto, so an inbound's up/down
stayed at 0 forever and inbound total limits never tripped (measured:
12 MiB relayed, rchar delta 0). The panel now owns the inbound's public
UDP port with a small relay and runs tuic-server behind it on a loopback
port, counting up/down exactly on every OS. tuic-server therefore logs
127.0.0.1 as every client's address; per-client attribution stays
unsupported since QUIC is opaque.
- Instance.BindTo formatted an IPv6 listen address as ":::8443", which
tuic-server rejects with "invalid socket address syntax", so an inbound
listening on "::" or any IPv6 literal never started. It now uses
net.JoinHostPort; IPv4 output is unchanged.
- The log level is passed to the sidecar as chosen. Online status,
last-online and delayed start are read from its Info lines, so the Log
Level field now says that Warn and Error switch them off for the
inbound, and the docs say the same.
- Drop two frontend tests that only exercised a getter and a set lookup,
and strip the trailing blank line that made gofumpt fail on two of the
new Go test files.
* fix(tuic): harden tag updates, runtime routing, and relay stability
---------
Co-authored-by: poise52 <equipoise52@gmail.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
Some subscription providers require a client-specific User-Agent before returning outbound links. Persist an optional value per subscription and use it for refreshes and previews while preserving the existing default for blank values.
* feat(clients): let admins set PersistentKeepalive on tunnel clients
model.Client already carries KeepAlive, and every AmneziaWG/WireGuard client
config emitter already writes PersistentKeepalive when it is above zero -- but
nothing in the UI could set it, so it stayed 0 and the line was never emitted.
Without it a peer that goes quiet has nothing to trigger a handshake: WireGuard
only initiates when it has data to send. An idle client stays disconnected
after any interruption -- a NAT mapping timing out, a device sleeping, the
panel restarting -- until the user generates traffic themselves.
New clients default to 25, the conventional value, which also keeps the NAT
mapping open. Existing clients keep whatever they have, and 0 remains valid and
means "do not send keepalives".
* fix(clients): let an explicit 0 actually disable PersistentKeepalive
Addresses review feedback on the previous commit.
UpdateInboundClient carries a stored keepalive forward whenever the incoming
one is zero, so the settings JSON and the running peer survive a metadata-only
edit that omits the field. That was a 0 -> 0 no-op while no UI could set a
nonzero value. Now that the client form can, the carry-forward became reachable
in the other direction: a client created at the form's default of 25 could
never be returned to 0, and the hint text shipped to all 13 locales -- "0
disables it" -- described something the backend silently refused. The save even
reported success, because a settings blob that came back byte-identical skips
the transaction entirely.
The zero value cannot carry that distinction, so model.Client.KeepAlive becomes
*int: nil means the field was never sent, &0 means "send no keepalives". The
pointer survives the internal marshal in ClientService.Update, which is where an
explicit 0 was being erased by omitempty before UpdateInboundClient ever saw it.
ClientRecord.KeepAlive stays a plain int -- it is the stored column, where
"unset" has no meaning -- and the conversions bridge the two.
Two tests, both red before this change in the direction they cover: an explicit
0 must reach wg_keep_alive, and an update that omits the field must still leave
a stored 25 alone.
Also adds the output transform every other numeric field in the client form
already has, so a cleared box sends 0 rather than null.
* fix(clients): repair the keepalive pointer conversion after the main merge
Merging main brought buildAmneziaWGProxy (#6326) in beside the
Client.KeepAlive int -> *int change without reconciling the new call site,
so internal/sub stopped compiling and took every package importing it with
it. The two sides touched different lines, so git merged them without a
conflict -- the green `make verify` on 112b19a8 predates the break.
ToClient also wrapped a stored 0 in a pointer, so omitempty stopped
omitting: a VLESS client's settings JSON gained "keepAlive": 0 on the
attach and bulk-attach paths, and that JSON reaches xray-core verbatim
through GenXrayInboundConfig. wg_keep_alive cannot tell "off" from "never
set", so a stored 0 now stays nil.
Also copies the regenerated openapi.json over the docs mirror, which
nothing in CI checks, and trims two comment blocks to the two-line cap.
---------
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
* fix(mtproto): use hosts for public share links
Generate MTProto subscription, client, copy, QR, and export links from managed Hosts so reverse-proxied public ports are advertised correctly. Migrate the redundant legacy custom share address into a Host and keep old imports compatible.
Closes#5126.
* fix(mtproto): keep host share links lossless and consistent
Address review on the MTProto hosts share-link change.
The migration no longer drops a legacy custom share address: an unrelated
(or disabled) Host stopped suppressing it, so only a Host already advertising
the same address does. An imported address now clears the same validation the
strict normalizer applies to every other protocol before it becomes a Host.
Panel and subscription agree on the endpoint a Host advertises: a portless host
string inherits the inbound port rather than the group's, and a port-only host
inherits the inbound address instead of emitting server=%3A8443.
LinksForClient prefers host endpoints for every protocol, the way getSubs and
inboundLinks already do, so the client-links API no longer ignores managed
hosts.
* fix(mtproto): migrate legacy share address past unusable hosts
The seeder skipped the conversion whenever any Host already carried the
address, including one that is disabled or excludes the raw sub type.
hostEndpoints drops those, so nothing advertised the address afterwards and
the marker committed with no way back. The duplicate check now mirrors that
same predicate.
UpdateInbound cleared a legacy MTProto shareAddr without the Host conversion
AddInbound runs, so re-applying an inbound definition through the API dropped
the public address silently. Both paths share one capture helper now.
Refresh the generated clients API reference for the summary reworded in the
previous commit.
* fix(inbounds): wait for the hosts list before building mtproto links
The page destructured only `hosts` from useHostsQuery, and that list reads
empty both while /panel/api/hosts/list is in flight and after it fails.
withMtprotoHostEndpoints then returns the inbound untouched, so Copy, QR and
Export advertise the internal listen port — the endpoint this branch exists to
replace. It is worse than not fixing it: the seeder has already moved a legacy
custom share address into a Host, so the fallback is the panel's own hostname
instead of the operator's address, and the Go generators reading the same rows
from the DB stay correct, so the two disagree for one inbound.
Fold the query into the page's existing readiness gate, the same way
useInbounds and HostsPage already consume that hook, so an empty list means
"no hosts" rather than "not loaded yet". The error branch fires only when
nothing is cached, so a refetch failing on window focus does not blank a page
whose host rows are still perfectly usable.
---------
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>