txlyre

txlyre synced commits to main at txlyre/3x-ui from mirror

  • 6ee74f2032 fix(amneziawgnet): wait for the client netstack goroutines before closing its device TestPortForwardRoundTripTCPAndUDP flakes in the race job: closing the test's client WireGuard device races the goroutines still writing into its netstack. amneziawg-go's device.Close() calls tun.Close() before it stops the routine draining the tun, and netTun.Close() closes the unbuffered incomingPacket channel that WriteNotify sends on. A goroutine still inside a netstack write when the deferred clientDev.Close() runs therefore closes and sends on the same channel -- reported as a data race, and on a bad interleaving a "send on closed channel" panic. The TCP echo listener, its per-connection copies and the UDP echo all write into clientNet, and teardown only closed the two listeners before the device: nothing waited for the goroutines themselves. A WaitGroup deferred right after clientDev.Close() supplies the missing edge, since LIFO then puts the wait between the listener closes and the device close. Confirmed by flooding the existing UDP echo goroutine under GOMAXPROCS=1 and 2, which failed 3/6 and 2/6 runs with the stack CI reported and 0/12 with the fix.

10 hours ago

txlyre synced commits to main at txlyre/3x-ui from mirror

  • d0edbcec81 feat(xray): update xray-core to v26.9.9 and follow the udpHop move Bump xtls/xray-core to 52a412d9e2f5 (v26.9.9) and the three binary pins in DockerInit.sh and release.yml in lockstep. Upstream moved UDP port hopping out of finalmask.quicParams.udpHop and into a standalone "udphop" UDP mask with a different shape (mode / interval / remotePorts / remoteIPs). The old key is gone from QuicParams, and since the config loader ignores unknown fields it is now silently dropped rather than rejected — port hopping just stops. The panel adapts where that key was live: - Both link importers rebuilt quicParams.udpHop from the standard mport param, so an imported hysteria2 link produced an outbound that no longer hops. They now emit a udphop mask in intervalremote mode, which is what the old key did. The mode is required: UDPHop.Build() rejects an empty or unknown one. - validFinalMaskUDPTypes and UdpMaskTypeSchema learn "udphop", otherwise the Go link generator strips the mask from every link and sub, and Zod strips it on the next form round trip. - mport generation (Go and frontend) reads the mask first and keeps reading the legacy key, so inbounds stored before the upgrade still advertise their range. On an inbound the old key was always inert — only hysteria's dialer consumed it — so nothing regresses server-side and no migration is needed. udphop stays out of the mask dropdown on purpose: it is client-only in core, which refuses to wrap a server socket, and that form is shared with the inbound editor.
  • cfd596a489 fix(amneziawg): let a cleared header protection key reach a running device amneziawg-go reads an absent UAPI line as "keep the current value", and addressFingerprint keys only on the addresses and MTU, so an obfuscation-only edit reconfigures in place rather than rebuilding. Clearing headerProtectionKey therefore never took effect: the device kept protecting headers with the old key. The stale key also keeps the S1-S4 minimum in force, so lowering S3/S4 in the same edit made every later IpcSet fail with -22 — after replace_peers had already dropped the peers. Send the all-zero key when the field is empty, which is how the UAPI expresses "disabled"; an empty value would be rejected, since it decodes to zero bytes.
  • efc603f59c fix(settings): show the SMTP failure reason instead of a raw i18n key classifySMTPError returned keys already carrying "pages.settings.", while the four keys TestConnection returns directly do not, and the alert renders every Message under that one prefix. Any classified failure therefore looked up pages.settings.pages.settings.smtpErrorAuth, which does not exist, so the panel printed the key instead of "Authentication failed — check username and password". The unknown case was worse: it appended the raw error to the key, and its own text interpolated {{ .Error }}, Go template syntax the frontend's i18next never fills. Return the keys unprefixed like the rest, and point the unknown case at the panel log, which already carries the underlying error.
  • c392f367e1 fix(dns): stop offering a port field that DoH entries discard Xray ignores port for DoH/DoHL/DoQL, so valuesToWire deliberately stores none for an encrypted address and a non-standard port has to go inside the URL. The form kept offering the field anyway, pre-filled with the 53 from its own defaults: a port typed there was dropped on save and redrawn as 53 on reopen, which reads as the panel losing the value. Render the port field only where it is actually stored. DoT keeps it, since tls:// is not an encrypted-address scheme for this purpose. Closes #6403
  • 705b291d34 fix(amneziawg): stop losing an inbound and its server keys on the API path Two saves that the panel UI never makes, but the documented REST API does. A client whose allowedIPs normalized to empty passed validation, then InstanceFromInbound skipped the peer and dropped the whole instance when it was the only one. Nothing logged it, so an enabled inbound simply never opened its socket. Refuse an enabled peer with no address, naming the client, the way the injection and collision checks already do. The server keypair was regenerated whenever a payload omitted privateKey, which invalidates every client config already distributed, and a payload carrying only privateKey left publicKey empty so rendered configs got a blank "PublicKey =". An omitted key now means unchanged: the stored pair is carried forward, a half-supplied pair has its public half derived, and generation is reserved for an inbound that has no stored keys at all. UpdateInbound loads the stored row before normalizing so those keys are available. Closes #6407
  • View comparison for these 11 commits »

18 hours ago

txlyre synced commits to main at txlyre/3x-ui from mirror

  • acf3603dc8 refactor(ci): review pull requests with one senior-engineer role The review job ran the official code-review plugin, which fans a pull request out to five Sonnet reviewers plus a Haiku scorer per finding and drops everything scored under 80, and the briefing file spent most of its lines overriding that plugin. Both are gone: the job hands one Senior Software Engineer prompt to the action inline, the way the issue analyst does, and denies the Agent tool so the single role is mechanical rather than a request. REVIEW.md moves from the emoji markers to CRITICAL/HIGH/MEDIUM/LOW with a pre-existing qualifier. The uncapped rule is scoped to findings the pull request introduced or worsened so it cannot collide with the cap of three pre-existing ones. "A finding is a report, not a patch" stays as it was. Workflow housekeeping: GH_TOKEN, REPO and PR live in the job env instead of six step copies; the skip gate is per pull request, so a head pushed after the automatic review is reviewed only on @claude review; the comment counters sum gh's per-page jq output, which read "0\n0" as a review on a pull request with more than 100 comments; --max-turns rises to 300 because every read now costs the single agent a turn instead of a subagent.
  • 47d2303334 fix(inbounds): reject missing TLS certificates before saving (#6429) An inbound could be saved with security "tls" and a certificate row carrying neither a file path nor inline content. Nothing rejected it, so the row reached xray-core, whose readFileOrString fails with "both file and bytes are empty" and takes the whole config build down with it — every other inbound included. Validate the credentials on both sides of the wire. validateInboundTLSCertificates follows xray's file-over-inline precedence, requires a private key for every non-verify certificate and insists on at least one server certificate, so a verify-only CA list no longer passes as a server config. The inbound form's Zod schema enforces the same rules per field and serializes only the editor mode the operator actually used, and a failed save jumps to the Security tab naming the certificate row that broke. On update the guard is scoped to a real TLS edit. A row already stored incomplete is grandfathered: it stays editable, and only a save that breaks a previously valid block is refused. A sub-node stores whatever the master pushes, and Remote.UpdateInbound falls back to AddInbound when the node does not yet hold the tag, so a grandfathered row could otherwise never be deployed or re-seeded — the rejection is swallowed to a logger.Debug line and the node stays on a stale config while the panel shows the client as cut off. The controller now marks a node-sync request (mTLS or a node-sync token) on a per-request copy of InboundService, and the guard steps aside for it on both add and update: the row was judged where the operator acted, and a node that refuses it only falls out of sync. Operator and admin-token saves are held to the guard as before. The security union is parameterised on its tlsSettings branch instead of copied, and tlsCertUsesFiles is the one file-vs-inline inference shared by the form schema and the adapter, so the mode the editor opens in and the pair of fields the save serializes cannot drift apart.
  • 9f76a66dcf feat(sub): add dummy info node and status configs for subscriptions (#6412) * feat(settings): add subInfoNodeEnable and status template settings * feat(sub): add dummy info node and status configs for raw links * feat(sub): support dummy info node in clash and json subscriptions * feat(ui): add subscription info node switch and status templates to settings * style: apply gofumpt formatting * fix(sub): address review feedback on subscription info node - Restore GetSubs contract to avoid unintended remark expansions on non-subscription-body calls. - Exclude dummy info node from Clash PROXY select group when active server nodes exist. - Track hasEnabledClient and set traffic.Enable in JSON and Clash paths so status tokens evaluate correctly. - Deterministically sort client emails across subscriptions before selecting primaryEmail. - Consolidate duplicated info-node evaluation logic into resolveInfoNodeRemark helper. - Remove redundant pure-getter test from setting_sub_info_node_test.go. Co-Authored-By: Claude Code <[email protected]> --------- Co-authored-by: Claude Code <[email protected]>
  • b8597314f8 docs(api): mark collection responses nullable (#6430) * docs(api): mark collection responses nullable Describe allLinks and panel log response objects as nullable string arrays so generated clients accept the existing nil-slice wire format. Pin both schemas with buildSpec regression assertions and regenerate the OpenAPI copies. * docs(api): include nullable Xray log responses Allow generated response arrays to opt into nullability while retaining their schema references and Go-derived examples. Apply this to Xray logs, whose nil slices already serialize as null, and pin the schema and example through buildSpec.
  • 3cd3836d77 fix(amneziawg): account for S4 junk in the default tunnel MTU (#6376) * fix(amneziawg): account for S4 junk in the default tunnel MTU amneziawg prepends S4 random bytes to every transport packet (device.NewOutboundElement) and, unlike content padding and random trailers, never clamps them against the tunnel MTU. A full-size packet therefore lands on the wire at MTU + 60 + S4 bytes: 20 IPv4 + 8 UDP + S4 + 16 transport header + 16 poly1305 tag. With the 1420 default that overflows a 1500-byte link once S4 exceeds 20, and GenerateObfuscation31 draws S4 from 12..27 inclusive -- so roughly 44% of newly created inbounds fragment every full-size packet they send. Measured on a live pair of interfaces, predicted against observed: MTU 1380 S4 12 -> 1452 on the wire (fits) MTU 1420 S4 12 -> 1492 (fits) MTU 1420 S4 20 -> 1500 (exactly at the limit) MTU 1420 S4 21 -> 1501 (fragments) MTU 1420 S4 27 -> 1507 (fragments) EffectiveMTU now subtracts S4 from the default; an explicit MTU is untouched. Client configs carry the same number. They previously omitted the MTU line whenever the server had no explicit value, which left the client on its own 1420 default and fragmented the client-to-server direction even after the server side was fixed -- silently, and only in one direction. All three emitters (the Go subscription text and the two TypeScript ones) now agree, which is what the existing parity test exists to protect. * fix(amneziawg): rebuild the device when S4 changes the derived MTU Addresses review feedback on the previous commit. Deriving the default MTU from S4 made a construction-time-only property depend on a hot-reloadable input, but addressFingerprint -- ensureLocked's only rebuild trigger -- still hashed the raw inst.MTU. S4 is a UAPI field, so an S4-only edit took the in-place IpcSet branch and the gVisor netstack kept the MTU derived from the old S4 while all three client emitters already advertised the new one. Every panel-created inbound leaves mtu unset, so that was the normal case, not an edge one: with S4 raised far enough the fragmentation this fix exists to remove came straight back, and stayed until a panel restart or an unrelated address edit. Folding EffectiveMTU into the fingerprint fixes it. An explicit MTU still takes the in-place branch on an S4 edit, since it does not move the interface MTU. Also trims four comment blocks to the 2-line cap in CLAUDE.md, and points NewDevice's doc comment at EffectiveMTU instead of the deleted defaultMTU.
  • View comparison for these 9 commits »

1 day ago

txlyre synced commits to main at txlyre/3x-ui from mirror

  • a5e68f410f perf(node): bound the per-client node push and fan out the traffic reset An operator with several nodes reported that editing a client or resetting its traffic takes more than ten seconds on the master. Measured against real Remote HTTP (fake node servers, one client per node), the healthy case is already fast — 3 nodes: update 51ms, delete 51ms; 5 nodes: 102ms / 103ms — but two things were not: - ResetTrafficByEmail still walked its inbounds one node round-trip after another: 152ms at 3 nodes, 253ms at 5, linear in node count. - Every per-client op blocked on the SLOWEST node's push. With one node answering in 3s, update/delete/reset all took 3003ms regardless of node count. A node that answers the 4s heartbeat probe but hangs on the push stays "online", so every edit waited on it up to remoteHTTPTimeout — the ten seconds in the report. More nodes only raise the odds one is sick. The push is an immediacy optimisation, not the source of truth: every one of these ops calls MarkNodeDirtyTx inside the transaction that commits the change, before it pushes, and the node reconcile job converges a dirty node on its next 5s tick by re-sending the inbound whose fingerprint was not advanced. So bound the synchronous push with nodeClientPushTimeout = 4s — the budget the heartbeat and traffic-sync jobs already treat as "responsive" — at the eight node-branch push sites. A node that does not answer in time is left dirty and converged a few seconds later instead of stalling the request; the tag-cache list fetch inside resolveRemoteID shares the same budget. Once one push in a batch has timed out, the rest of that inbound's batch now stops pushing too, as AddInboundClient already did: the node is dirty and one reconcile converges the whole inbound. Deleting three clients on one hung node went from 30.08s (three remote timeouts) to 4.06s; at the 32-client push threshold that is 128s of deadlines saved per inbound. Fan the reset out through fanoutInboundApplies like the other client ops. Its node propagation is still attempted whatever the node's status flag says, as before, because nothing replays a traffic reset — the reconcile pushes inbound config, not counters — so a node still serving after being marked offline must receive it now or never. Trade-offs stated plainly: a node that would have answered in 4–10s now falls to the reconcile's full-inbound push, which on the node is a delete+add of the inbound and drops its sessions there — the same fallback a failed 10s push already used, now reached sooner. The reset stays best-effort with no retry path, which predates this change. The response still reports success while a timed-out node catches up; the pending-node badge is keyed off node status by design, so only the warning log records it. Tests: a barrier test that a sequential reset cannot satisfy; two tests against a real runtime.Remote and an httptest node that hangs on the push, pinning that an edit returns at the deadline (exactly one push reached the node, the node is left dirty) and that a bulk delete stops after its first timed-out push. All red without the change; the two hung-node tests pay their 4s deadline on every run.

2 days ago

txlyre synced commits to main at txlyre/3x-ui from mirror

  • 81611a6a76 fix(node): flag every hosting node before a client edit applies A client edit fans out one transaction per inbound, and each one renames the single shared clients row but calls MarkNodeDirtyTx for only its OWN node. So between the first and the last commit the record already carries the new email while every other node hosting that client is still config_dirty = false. setRemoteTrafficLocked gates the snapshot merge on that flag, so a merge landing in the gap is accepted, sees a pre-rename snapshot, finds no record for the old email and inserts one through syncInboundClients' CreateInBatches — the only place in the panel that creates a client record. The ghost is never in any later merge's perInboundOld, so markSyncOrphan never fires and ReapSyncOrphans never collects it: the operator is left with a permanent second client under the old name. The same stale merge reverts an expiry-only edit instead of duplicating it. Mark every node hosting the client dirty in one serialized write before the fanout starts, so a merge queued behind it skips the node instead of merging a half-applied edit. The nodes were going to be marked by their own applies anyway; doing it up front only moves it earlier, and a client on local-only inbounds never reaches the writer at all. The set is the client's FULL attachment list, taken before the inboundIds filter narrows it: the rename rewrites the one shared record, so an inbound the filter excluded goes stale too. Two tests, both red without the change. The first pins the ordering rather than the end state — it reads the watched node's flag from inside another inbound's push, so moving the marking after the fanout turns it red. The second pins that the filtered path still covers the excluded node. This narrows the window rather than closing it everywhere. A reconcile tick can still clear the flag mid-fanout, and on a filtered edit the excluded inbound keeps the old email in its settings for good, so its next merge duplicates again. The case-drift path — a node reporting another case of a known email — is untouched and still duplicates.
  • 832423d543 fix(clients): flag the restart a partly-applied edit or delete still needs 63b46cd6 made a multi-inbound client op apply its inbounds concurrently and stop aborting at the first failure, so an error can now come back together with needRestart=true: the inbounds that succeeded committed real changes and their Xray still needs the restart. That commit taught the two callers it converted — create and attach — to read the flag before the error check. d34ec97f then routed Update, Delete, Detach and DeleteByEmail's record-less fallback through the same fanout but touched no caller, so on a master with several nodes a partly-applied edit or delete returned (true, err) into a handler that returned on err first. Xray was never flagged for the work that landed and notifyClientsChanged never fired, so the running config kept serving the pre-edit client set and every open panel showed stale rows until something else happened to trigger a restart. Read needRestart before the error check in update, delete and detach, and broadcast on needRestart || err == nil — the same shape create and attach have had since 63b46cd6. The predicate is a strict superset of the old err == nil, and needRestart is only ever assigned after a runSerializedTx commit, so it firing genuinely means something landed. Same one-line move in the LDAP sync job's detach loop, which discarded the flag on its continue. The three handlers are pinned by a new controller test each: one client on two inbounds, the second one's settings JSON corrupted so the op commits on one and fails on the other, asserting both the success:false response and the restart flag. All three fail without the change. The API docs for update, del and detach now describe the partial-application contract, as add and attach already did. Detach ends at the fanout so every one of its errors carries the inbound prefix; update and delete write the client record afterwards, and a failure there is reported without one.
  • View comparison for these 2 commits »

2 days ago

txlyre synced commits to main at txlyre/3x-ui from mirror

  • 33058c8eed fix(tgbot): use a token telego accepts in the edit-message tests telego.NewBot validates the token against `^\d+:[\w-]{35}$` before any option is applied, so the "test-token" literal in the two not-modified tests failed with "telego: invalid token format" and the go-test and race jobs went red on every run since #6340. Use a placeholder token that matches the format; the tests now reach the mock API server, pass with the guard in place and fail without it.

3 days ago

txlyre synced commits to main at txlyre/3x-ui from mirror

  • 8e13f8b172 fix(tgbot): suppress 'message not modified' warnings in Telegram edit calls (#6340) When users click Refresh buttons in the Telegram bot (usage_refresh, client_refresh, ips_refresh, onlines_refresh), editMessageText and editMessageReplyMarkup are always called even when the content has not changed. Telegram returns a 400 "message is not modified" error which was logged as Warning, cluttering the logs on every refresh click. Add isTelegramNotModifiedError helper that detects this specific Telegram API error and logs it at Debug level instead of Warning.
  • fc05249e0c fix(geofile): verify downloaded geo databases against published digests (#6404) * fix(geofile): verify downloaded geo databases against published digests UpdateGeofile wrote whatever the three upstreams returned straight into the Xray asset folder with no integrity check. Xray parses these databases when it builds its routing matchers, so a corrupted or substituted file takes the core down at its next start. The panel already does this for the other artifact it downloads: installXray checks the release archive against the SHA-256 published in its .dgst sidecar. The geo databases were the one download that skipped it, even though all three upstreams publish a <asset>.sha256sum beside every .dat. Fetch that sidecar, compare it against the bytes that actually arrived, and stage every file in a temporary folder first, so one bad database installs nothing rather than leaving the core running databases from two releases. Match the digest line by base name rather than by the path it records. Loyalsoldier and runetfreedom write "<hash> geoip.dat" while chocolate4u writes "<hash> release/geoip.dat" -- the path from its own build -- so `sha256sum --check` semantics fail on a perfectly good download. Also skip the Xray restart when every upstream answered 304. The conditional GET was already there, but the restart ran unconditionally and dropped every client connection on a refresh that changed nothing. Assisted-by: Claude Code:claude-opus-5 (mostly) * fix(geofile): pin the release and scope atomicity to one upstream Four corrections to the digest verification, all from review. Pin the release. The asset and its .sha256sum were fetched as two independent requests to releases/latest/download/, so GitHub re-resolved "latest" between them. These upstreams publish several times a day -- 202609022346, 202609030908 and 202609031849 are three tags from one day -- so a release landing mid-batch had release N+1's digest checked against release N's bytes, reporting a healthy upstream as "corrupted or tampered with". Resolve the tag once per upstream from the redirect GitHub already returns, then fetch body and digest from it. Modeling the entry as repo + asset rather than an opaque URL is what makes that possible. Scope atomicity to one upstream. A single failure discarded every verified download, so one transient 5xx from one of three independent repositories threw away four good files and re-downloaded tens of MB on the next attempt. The integrity argument holds for a geoip/geosite pair out of one release; across repositories it buys nothing. Each upstream now installs or aborts on its own and errors are collected, as the code did before this feature. Make the all-or-none test deterministic. It ranged a map, so when the corrupt entry came first the run returned before the good file was ever requested and the assertions held trivially -- a coin flip that would also pass against an implementation installing each file as it verified. Iteration is sorted now, and the test asserts the good file was actually downloaded first. Assert which error. The error table checked only that err != nil, so its two branches could swallow each other's cases; each row now pins the message. Also trims three comment blocks to the two-line limit. Assisted-by: Claude Code:claude-opus-5 (mostly)
  • 4e355edc15 fix(sub): skip AmneziaWG JSON entries (#6420)
  • 0f6e1ae8d7 fix(sub): bind JSON local inbounds to 127.0.0.1 and keep mux.cool off Vision outbounds (#6418) * fix(sub): bind JSON local inbounds to 127.0.0.1 and keep mux.cool off Vision outbounds The JSON subscription's local SOCKS/HTTP inbounds had no listen address, so every client that runs the profile verbatim bound an unauthenticated proxy on 0.0.0.0, and iOS packet-tunnel clients could not reach it at all (Happ iOS: CONNECTED with zero traffic, same symptom as #6379 — on the same device the mixed inbound also worked once bound to 127.0.0.1). Bind both to loopback, which is what every client's own generated config does. The global subJsonMux was also applied to VLESS outbounds carrying xtls-rprx-vision. XTLS flows do not support mux.cool: Xray answers the mux handshake with "common/mux: unexpected network TCP" and the tunnel passes nothing, on every platform (verified with Happ iOS/Android/macOS, V2Box iOS and desktop Xray 26.6.27 against a 3x-ui 3.7.0 box with per-client traffic counters). Skip the mux block whenever the outbound carries a flow. Refs #6379 * fix(sub): keep XUDP settings when disabling TCP mux on Vision outbounds Clearing the whole mux object also dropped xudpConcurrency, xudpProxyUDP443 and any per-host muxParams override. Xray reads those only under mux.enabled, so set concurrency to -1 instead: TCP mux.cool (which XTLS flows reject) is off, XUDP and the UDP/443 policy stay. The test now decodes each outbound into a fresh map. --------- Co-authored-by: Farhan Zare <[email protected]>
  • View comparison for these 4 commits »

3 days ago

txlyre synced commits to main at txlyre/3x-ui from mirror

  • ed6bc1d898 docs(api): align OpenAPI with runtime contracts (#6409) Document the cookie-authenticated WebSocket upgrade and its emitted envelopes without exporting pseudo-paths. Align REST response schemas, paged-client filters, and subscription HEAD operations with their runtime implementations, then regenerate frontend and docs artifacts.
  • 3b5273b1d6 fix(amneziawg): reject obfuscation values amneziawg-go's own UAPI rejects ValidateObfuscation exists, by its own doc comment, so that a bad manual entry cannot break the embedded device's IpcSet. It was not covering enough to do that. Auditing the panel against amneziawg-go v3.1.20260828's full UAPI surface turned up two holes, both confirmed by driving the values through a real IpcSet: S1 = 70000 upstream parses s1-s4 as uint16 S2 = 70000 (device/uapi.go) Jc = -1 jc/jmin/jmax are uint32, so no negatives Jmin/Jmax = -5/-1 Jc = 5000000000 and nothing wider than uint32 I1 = <rand 100> newObfChain hard-fails on an unknown tag I1 = <r 100 ... and on a missing '>' I1 = <> ... and on an empty one All eight passed validation and were then rejected by the device. Only S3 and S4 were bounded, which is why the asymmetry went unnoticed. The inbound saves, the reconcile fails on every tick, and the interface never comes up with a single log line to say so. Bound the five numeric fields to the widths upstream actually parses, and check the I1-I5 chain's <tag value> structure against a tag set mirroring upstream's own obfBuilders map. Each tag's value grammar stays amneziawg-go's to enforce -- that is eight builders across several files, and duplicating them here would drift. So <r abc> still reaches IpcSet, now as the only remaining class rather than one of four. Mirror the same bounds in the Zod schema, next to the max() that s3 and s4 already carried, so the form rejects the value instead of the save doing it. TestValidatedObfuscationAlwaysApplies pins the contract itself: whatever ValidateObfuscation accepts, a real amneziawg-go device must accept too. It covers the specs the new grammar check deliberately allows, not just the ones it rejects, so the allowlist cannot quietly become stricter than upstream. The rest of the audit found no gaps: all 17 settable device keys reach buildUAPIConfig, ServerSettings, the Zod schema and all three .conf emitters. fwmark and persistent_keepalive_interval remain unemitted, both deliberately -- the panel models no fwmark anywhere, and keepAlive is carried client-side where WireGuard puts it.
  • be5ee3e0e1 fix(amneziawg): three defects in the embedded relay's connection handling Half-close. Both TCP relays -- RelayTCP into Xray's SOCKS5 inbound and relayTCPForward into a peer's tunnel address -- waited on a single `done` receive and then closed both sides. A client that finished sending and shut down its write side therefore had the connection torn down before the response came back. pipeBothWays now runs both directions to completion and propagates the half-close via CloseWrite (which *net.TCPConn and *gonet.TCPConn both implement), falling back to a full Close for anything that does not. Waiting for both directions reintroduces the risk the old single-receive was implicitly avoiding: a peer that vanishes mid-transfer would pin the pair forever. guardedReader bounds that, but as an idle window rather than a total one -- the deadline is re-armed on every read once armed -- so a slow transfer is never cut, while a silent peer is. Two minutes matches the idle window UDPRelay.pump and portForwardUDPIdleTimeout already use. UDP session retirement. pump's teardown deleted the map entry by key alone, so a session that lost a create race evicted whichever session currently held that source, orphaning a live flow. It now retires only its own entry, and Handle keeps the already-published session when it loses the race. The map is keyed on netip.AddrPort rather than src.String(), matching udpForwardListener next door and dropping one allocation per relayed datagram. SOCKS5 reply decoding. bytesReader had a value receiver, so each Read restarted at the head of the slice, and receive never advanced past a domain-form address because its switch only handled ATYP 0x01 and 0x04 -- a 0x03 reply decoded to a wrong source, port and payload. splitSocks5Addr replaces it: all three address forms, length-checked at every step, with the domain form accepting only a literal. Resolving there would have put a blocking DNS lookup on the receive path, and a datagram's own source is an address already. Unreachable against Xray's own inbound, which always answers with an IP, so this is a latent-bug fix rather than an observed one.
  • 24cb6bfe1f perf(amneziawg): return gVisor's pooled buffers on the embedded data path Every packet crossing the embedded AmneziaWG interface allocated instead of reusing gVisor's pools, in both directions. stackTun.Write injected each decrypted packet and never called DecRef, so the packet buffer and its chunk were never returned; stackTun.Read copied each view out and never released it. gVisor's own link endpoints settle the ownership question -- loopback.go and sharedmem.go both DecRef immediately after DeliverNetworkPacket, because the injector owns the buffer. AttachUDPHandler compounded it by cloning a packet buffer it then dropped on the floor, on top of a Data().AsRange().ToSlice() that already returns an owned copy, so the clone bought nothing and stranded a pooled buffer plus a cloned view per datagram. Measured with the benchmarks added here: stackTunWrite (upload) 794ns -> 107ns 4 -> 0 allocs stackTunRead (download) 707ns -> 129ns 3 -> 0 allocs UDP datagram, end to end 2.69us -> 1.58us 8 -> 2 allocs The remaining UDP allocation is the ToSlice copy itself. Through a real handshaked tunnel -- both devices in one process over loopback, so ChaCha20-Poly1305 and the UDP syscalls dominate -- it is worth -48% bytes/op and -33% allocs/op, and about +4.8% throughput in each direction (n=18, p<=0.01). On a small VPS, where the allocation pressure is not spread over 24 idle cores, the throughput share should be larger; that part is reasoning, not something measured here. The three regression tests assert allocations per packet rather than timing, since the defect is the pool miss, not the nanoseconds. Thresholds leave room for the extra allocation -race adds.
  • d34ec97f62 perf(node): push a client edit to every node at once, not one after another Editing, deleting or detaching a client on a master with several nodes took one node round-trip per node, added end to end. Create and Attach already fanned their per-inbound applies out through fanoutInboundClientAdds, but Update, Delete, Detach and DeleteByEmail's record-less fallback still walked their inbounds in a plain sequential loop, and each iteration blocks on a node RPC (10s timeout, more when a node is slow or has just gone unreachable and the heartbeat has not marked it offline yet). Measured with a node runtime injecting 100ms per RPC, before: nodes=1 create=101ms update=101ms delete=101ms nodes=3 create=102ms update=303ms delete=302ms nodes=5 create=202ms update=504ms delete=504ms after, all three track create: nodes=3 create=102ms update=102ms delete=101ms nodes=5 create=203ms update=203ms delete=203ms Generalize the existing helper into fanoutInboundApplies over an inboundApply list and route the four remaining loops through it, so they inherit the same concurrency cap, per-inbound panic recovery and joined errors. Each caller still builds its payloads sequentially first: fillProtocolDefaults mints the shared credentials on the first inbound and every later one reuses them, so that order has to stay deterministic. Only the applies overlap; their DB work still serializes through the single traffic writer, and the per-inbound mutation lock is unchanged, which is exactly what Create has relied on. Behaviour change: one failing inbound no longer aborts the remaining ones, matching what Create already does. The error still names each failed inbound and the record-level writes are still skipped when any inbound failed. The snapshot merge on the same serialized writer was measured as a second suspect and cleared: ~43ms per node at 500 clients, an order of magnitude below the RPC serialization.
  • View comparison for these 13 commits »

5 days ago

txlyre synced commits to main at txlyre/3x-ui from mirror

  • 63b46cd612 perf(clients): apply a multi-inbound client create concurrently Creating or attaching a client across N inbounds called AddInboundClient once per inbound, strictly one after another. When those inbounds live on different nodes each call is a full node round-trip bounded by the 10s remote timeout, so the request cost the SUM of every node's latency: two nodes felt instant, three took ~13s and timed out bot callers, which is how it surfaced as "two out of four account creations fail". Split the per-inbound preparation from the apply. Preparation stays ordered and single-threaded because fillProtocolDefaults mints the shared credentials on the first inbound and every later one reuses them; the applies then run concurrently, capped at inboundFanoutConcurrency. A 4-node create measured 1.205s -> 0.307s with peak overlap 1 -> 4. Consequences of no longer aborting at the first failing inbound: - Every apply error is tagged with its inbound and the failures are joined, so all of them reach the caller instead of just the first. - The fanout goroutines recover their own panics. Off the request goroutine gin's Recovery no longer covers them, and an unrecovered panic would kill the panel rather than fail one inbound. - A partly-applied call commits clients on the inbounds that succeeded, so the controller and the LDAP job now read needRestart before the error check; otherwise Xray was never flagged for the work that landed. - limitHwid is applied only when every inbound succeeded. Applying it after a failure rewrites limit_hwid and trims the registered devices of an email that already existed, which is silent data loss on an operation the panel reported as failed. Update the API docs for the new partial-application contract and the inbound-tagged error strings.

5 days ago

txlyre synced commits to main at txlyre/3x-ui from mirror

  • 2ddcf53020 Feature/fix external subscription client expiry (#6333) * fix(sub): honor client expiry for external links * fix(ui): show client expiry on external links * fix(sub): address external expiry review
  • 13e87a18c8 chore(ci): give the race job a 25m test timeout The race job failed with "panic: test timed out after 10m0s" in internal/web/service (FAIL at 600.106s) while every other package passed and the non-race go-test job ran the same package in 57s. Nothing hung. The race detector costs this repo ~8.5-10x (internal/database 8.4s -> 73s, internal/sub 17.8s -> 149s), and internal/web/service has 671 tests, ~40 of which each pay a full InitDB + AutoMigrate. That puts it right on go test's 10-minute default per-package timeout: the last four race jobs finished in 10m10s-10m28s before this one crossed the line. Pass -timeout 25m in ci.yml and `make race` so the largest package has real headroom while a genuine deadlock is still bounded. Verified locally: ok internal/web/service 265.425s, 658 tests, no data races.
  • bd1c27b03d fix(amneziawg): H1-H4 generator + queue-depth throughput fixes (#6330) * fix(amneziawg): stop H1-H4 generator misclassifying transport packets Both the Go generator and its frontend mirror picked a random *range* per H1-H4 field with only a minimum width enforced (no maximum). amneziawg-go's packet classifier only ever compares a fixed-size ciphertext prefix against these bounds, so a wide range buys no DPI resistance -- the boundaries themselves are never observable on the wire. It does cost real throughput: with randomTrailers on (the default here), the handshake-size checks relax from == to >, so a wide H-range misclassifies a proportional fraction of ordinary transport packets as handshakes and silently drops them (amnezia-vpn/amneziawg-go#183). A single value per field is strictly safer than any range, with no obfuscation trade-off. Live-tested: narrowing H1-H4 alone took AmneziaWG upload from 2-3 Mbit/s to 200+ Mbit/s on one box, and ~20 Mbit/s to 120-156 Mbit/s on another, single-variable, no other change. * fix(amneziawgnet): raise tunQueueDepth to absorb slow-start bursts 1024 was sized for a single-connection buffering problem (the gVisor-to-amneziawg-go TUN handoff channel needing slack for the download direction). tcpip.Stack.Stats() during a real many-connection download (20-28 concurrent TCP flows, e.g. a segmented speed test) showed SlowStartRetransmits jump by ~770 in a single second the moment CurrentEstablished crossed ~20 -- consistent with many connections' simultaneous slow-start growth briefly exceeding 1024 outstanding packets and gVisor treating the resulting silent drops as real network loss. * fix(amneziawg): trim comment blocks to the repo's 2-line cap Review feedback: four comment blocks in the previous commits exceeded CLAUDE.md's 2-line-per-block hard rule (up to 13 lines). Trimmed each to the one non-obvious fact plus the amneziawg-go#183 reference; the fuller rationale already lives in the commit message. Also refreshed the stale H1-H4 range example in docs/content/docs/en/config/amneziawg.mdx to match the new single-value generator output.
  • 0ff3c23948 fix(api-docs): generate request bodies for all encodings (#6296) * fix(api-docs): generate request bodies for all encodings The OpenAPI generator only recognized generic body parameters, so JSON, form, and multipart declarations disappeared into empty application/json objects. Generate the declared media type and schema, preserve optionality and conditional requirements, and encode repeated form arrays the way Gin expects. Correct the request metadata exposed by the complete schemas and keep the panel and docs specifications synchronized. * fix(api-docs): align alternative request schemas Keep non-empty constraints on the selected request-body alternative without rejecting empty values for the alternatives that panel requests also include. Allow null client IP lists because model serialization emits them while cleared rows await pruning. * fix(api-docs): send object urlencoded fields as JSON, document the inbound update body Four defects the request-body rework exposed or left behind: - An object-typed field in an x-www-form-urlencoded body got no encoding entry, so OpenAPI 3.0 serialized it form-style. Swagger "Try it out" and generated clients sent memberWeights=3&memberWeights=0.2 to /panel/api/sub-balancers, and parseSubBalancerForm json.Unmarshals the raw field, so every such call failed with "invalid memberWeights". Emit encoding.<name>.contentType = application/json instead. - bodyRequiredOneOf names were never checked against the declared body params: a typo emitted an anyOf branch requiring a property that does not exist — unsatisfiable — and make gen still passed. Throw now, and extend the requestSchema guard to reject bodyRequiredOneOf as well. - /panel/api/inbounds/update/:id advertised no request body although its own summary says the shape mirrors /add and updateInbound binds one. Both entries now share an inboundBody const so they cannot drift. - The mixed-locations error was the only buildOperation throw without the method and path, aborting make gen without naming the offender. Regenerated frontend/public/openapi.json and copied it to docs/public/openapi.json. No MDX regeneration: no summary changed. --------- Co-authored-by: Sanaei <[email protected]>
  • f294e1806d feat(release): publish SHA-256 sums and verify them in install.sh/update.sh (#6393) * feat(release): publish SHA-256 sums and verify them in install.sh/update.sh The installer and updater fetched the release archive and extracted it after checking only that the file is not empty, and the release workflow published no checksums. TLS protects the transport, not the bytes: a truncated or swapped asset, a bad mirror or a TLS-terminating proxy was installed as root. #5396 added this verification for the Xray archive; the panel's own archive was the remaining unverified download. Publish <asset>.sha256 next to every release archive (Linux and Windows) and verify it before extracting. A mismatch aborts the install; a missing sidecar, which every release before this change has, only warns, so installing older tags keeps working. Assisted-by: Claude Code:claude-fable-5-1 * fix(install): fail closed when the checksum sidecar cannot be fetched Review follow-up. Any curl failure on the sidecar (5xx, reset, DNS) was treated as "no checksum published", so whoever can swap the archive could also drop the 90-byte sidecar request and skip the check. Only a 404, which every release before the sidecar existed returns, is still tolerated with a warning; every other outcome aborts and removes the downloaded archive. Assisted-by: Claude Code:claude-fable-5-1 * fix(install): restore the closing brace lost in the main merge
  • View comparison for these 21 commits »

5 days ago

txlyre synced commits to main at txlyre/3x-ui from mirror

  • 65b9bfed8b fix(ci): stop the review bot handing over fixes in prose The `suggestion` blocks stopped once the briefing moved into its own file, but the carve-out that survived — "one clause naming where the fix belongs" — was being stretched from a location into an instruction. #6397 dictated what to write in a comment and which existing test to copy; #6394 named the fix outright. The clause now permits a file, a function, a symbol or a layer and nothing about what happens there, and closes the stretch three ways: prose is a patch the moment a verb describes the change, so is holding up an existing symbol as the model to copy, and a clause the maintainer could apply as written is the fix however it is punctuated. Three rules the rubric was missing, none of which existed anywhere. A 🔴 or 🟡 says in one clause what the change did to the code it is about, the way a 🟣 already says it predates it — otherwise nothing in the comment shows the marker was earned. A claim about a caller or a callee needs that file read: the dispatch-rule violation this repo cares most about sits a frame outside the diff, and the skill is told to avoid reading past the changes. And nothing pads the comment. The briefing's one named override aimed at a step that does not exist. The plugin the job loads defines no `--comment` flag and mentions suggestions nowhere, so `max --comment <target>` is inert trailing text. Replaced with the six overrides that are real: the skill calls pre-existing issues and unmodified lines false positives, drops every finding its confidence pass scores under 80 and then posts nothing at all (a nitpick scores 50, so that filter empties all five nit slots), says to avoid emojis against a severity system that is three of them, mandates a "Found N issues" format, and forbids reading build signal.

6 days ago

txlyre synced commits to main at txlyre/3x-ui from mirror

  • 38dd9bcc70 Bump Go dependency versions Refresh the Go module set in go.mod and go.sum to newer patch/minor releases, including xray-related dependencies, gRPC, WireGuard, and supporting indirect libraries. This keeps the project aligned with upstream fixes and compatibility updates without changing application code.
  • e264ea89c1 chore(deps): bump docs and frontend deps Update dependency versions across `docs` and `frontend`, including Next/Fumadocs packages in docs and Ant Design, React Query, Storybook, and related tooling in frontend. Also updates lint/format tool versions (`oxlint`, `oxfmt`), bumps docs `pnpm` package manager version, and refreshes workspace release-age exclusions for the newly upgraded docs packages.
  • ac193cd9d3 refactor(ci): split the issue analyst out and brief the review job from a file The issue analyst moves verbatim from claude-bot.yml into its own claude-issue-analyst.yml, so claude-bot.yml now holds only the pull-request side: review, @claude mentions and conflict resolution. The review job's briefing was a single 2,600-character quoted string inside claude_args, unreadable and unreviewable. It now lives in .github/claude/review-job.md, assembled at run time with a "This run" section that hands the reviewer the pinned head SHA, the pull request and the exact check-runs command, and reaches the CLI through --append-system-prompt-file. The agent-mode action sets no system-prompt append of its own, so the file flag cannot collide with one. Findings no longer carry the fix: REVIEW.md and the brief both forbid suggestion blocks, patches and replacement snippets, overriding the code-review skill's --comment step, which attaches a committable suggestion to any small fix. A finding states what is wrong, where, what triggers it and what breaks; the maintainer decides the change.
  • c62ee0bbd8 fix(outbound): test VLESS vnext endpoints (#6358) Co-authored-by: sanmaxdev <[email protected]>
  • 8abe87b625 fix(outbounds): preserve stable subscription tags (#6345) An inserted link could claim a previous positional tag before the existing identity that owned it was processed. The owner was then suffixed and the swapped mapping persisted across refreshes. Reserve tags for identities still present in the batch so positional fallback, fresh allocation, and collision suffixes cannot take them.
  • View comparison for these 11 commits »

6 days ago

txlyre synced commits to v1.8.13 at txlyre/dtlspipe from mirror

1 week ago

txlyre synced new reference v1.8.13 to txlyre/dtlspipe from mirror

1 week ago

txlyre synced commits to master at txlyre/dtlspipe from mirror

1 week ago

txlyre synced commits to v1.8.12 at txlyre/dtlspipe from mirror

1 week ago

txlyre synced new reference v1.8.12 to txlyre/dtlspipe from mirror

1 week ago

txlyre synced commits to master at txlyre/dtlspipe from mirror

1 week ago

txlyre synced commits to v3.7.0 at txlyre/3x-ui from mirror

2 weeks ago

txlyre synced new reference v3.7.0 to txlyre/3x-ui from mirror

2 weeks ago