txlyre

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

  • 43e64993fc fix(amneziawg): refuse a row's own relay port and keep a disabled row's slot reserved (#6544) * fix(amneziawg): refuse a WireGuard port that is the row's own relay port All three relay checks filter themselves out of the candidates with id != ignoreId, so nothing ever compared an AmneziaWG row's own WireGuard listen port with the relay port its own id derives. Saving a row on that exact port left the embedded device (UDP on the inbound's listen address, amneziawgnet/device.go:137) and its injected relay (TCP and UDP on 127.0.0.1, amneziawgnet/relay.go:47-61) bound to the same UDP port, so whichever loses the race dies -- and when the relay loses it, Xray refuses the whole config and takes every other protocol on the host with it. The first AmneziaWG inbound on port 65101 was enough to reach it: id 1 derives exactly that port. The row now states the rule its three siblings do: it owns the slot its id derives. A node-hosted row still keeps its own port, since it binds no relay on this host. TestAddInbound_AmneziawgRefusesItsOwnRelayPort and TestUpdateInbound_AmneziawgRefusesItsOwnRelayPort fail without this -- both were watched red first -- and pin the two separate call sites, AddInbound's post-Save block and checkPortConflictTx's ignoreId > 0 block. * fix(amneziawg): keep a disabled row's relay port reserved for port forwards loadPortConflictContext filtered its query with enable = true, so a client's ForwardedPorts spec could claim the relay port a disabled AmneziaWG row's id derives. That row's relay appears with its first client -- a path that runs no port check -- and when the relay then loses the loopback bind race to the forward listener, Xray refuses the whole config instead of losing one forward (#6542 review, arrived with #6540). The context now loads every local row and gates only the ordinary-port compare on enable, which is what a disabled row's own port is worth: free. Its relay slot is not free, which is the rule #6540 already states for the other two guards. TestCheckForwardedPortsConflict_DisabledAmneziawgRelayPortIsReserved fails without this -- watched red first -- and passes with it, while TestCheckForwardedPortsConflict_IgnoresDisabledInboundPort keeps proving that a disabled inbound's own port stays available. * fix(amneziawg): re-run the forward guard once a new row has its own ports normalizeAmneziaWGSettings validates every client's ForwardedPorts before the row is saved, and loadPortConflictContext then reads the database -- so the new AmneziaWG row is never a candidate for itself. A client could forward exactly the relay port the row's own id derives, or its own WireGuard listen port, and the create was accepted: at runtime the panel's wildcard forward listener and Xray's 127.0.0.1 relay race for the same port, and a lost relay bind makes Xray refuse the whole generated config (#6544 review, pre-existing). The post-Save block is the only place the id is known, so it re-runs the guard there. Both callers now share amneziaWGForwardedPortsConflict, so the collision message lives in one place instead of two. TestAddInbound_AmneziawgRefusesAClientForwardingItsOwnRelayPort fails without this -- watched red first -- and passes with it. * fix(amneziawg): stop blocking stored forward specs on a disabled row's slot Round 2 flagged this PR's widening as the one MEDIUM it introduced, and the code confirms it: UpdateInboundClient carries a stored ForwardedPorts spec forward for a partial edit (client_inbound_apply.go:763-765) and re-validates it (:772 and :909), so after an in-place upgrade an edit that never submitted the field -- a bot enable/expiry toggle -- is refused over a slot the operator did not touch, for a relay injectAmneziawgnetSocks does not emit while the row is disabled. The inbound-save path re-validates every stored spec the same way. The trade does not pay for itself: the slot this reserves is claimable only by a spec an operator authors onto 65101-65535, while the cost lands on unrelated operations. The precise fix -- refuse a newly claimed spec rather than a stored one, and check the enable transition in SetInboundEnable, where the conflict is actually created -- is larger than the hole, so the slot goes back to a documented pre-existing item with its own follow-up. The create-path re-run added in 80eb5712 is unaffected: it reads the settings submitted in the same request, so it never refuses a stored value, and its test still passes.
  • d52b598abf fix(amneziawg): reserve the relay port before an AmneziaWG inbound has a peer (#6542) * test(amneziawg): pin that a peerless inbound still owns its relay port checkAmneziawgnetSocksConflict skips a candidate whose settings yield no qualifying peer, and normalizeAmneziaWGSettings writes Clients: [] for a fresh AmneziaWG inbound -- so a newly created row reserves nothing, an ordinary inbound can take its derived port, and adding that row's first client then puts two inbounds on 127.0.0.1:65101. The client paths run no port check. Expected red on this head; the fix follows. * fix(amneziawg): reserve the relay port before the first peer is added checkAmneziawgnetSocksConflict skipped a candidate whose settings yield no qualifying peer (amneziawg.InstanceFromInbound), and normalizeAmneziaWGSettings writes Clients: [] for a fresh AmneziaWG inbound. A newly created row therefore reserved nothing, an ordinary inbound could be saved onto the port that row derives, and adding its first client generated the relay next to it: two inbounds on 127.0.0.1:65101, which makes Xray refuse the whole config and take every other protocol on the host down with it. Nothing re-checked it later either -- only AddInbound and UpdateInbound run checkPortConflictTx, and the client paths that create the first peer run no port check at all. Ownership now follows the row, so the check states the same rule as its two siblings, which key on protocol and node_id IS NULL alone. The amneziawg import goes with the guard. TestCheckPortConflict_AmneziawgnetSocksRelayReservedBeforeTheFirstPeer fails without this, on a test-only head whose go-test run failed on exactly that test, and passes with it. * docs(amneziawg): stop the forward check's doc block claiming every row gets a relay Round-1 LOW: the block's justification clause read "every one of them gets a relay inbound", which is false for exactly the rows this change newly reserves for -- injectAmneziawgnetSocks skips a row with no peer email, and that is the row whose port must stay reserved. A reader following the cross-reference landed on the guard this branch removes and read it as the rule. Replaced by the two facts that are true, which also brings the block under CLAUDE.md's two-line cap instead of twelve lines over it. The peerless reason stays where it is load-bearing, in the two-line comment above the candidate loop.
  • 2d8d304850 fix(amneziawg): stop a disabled inbound's relay slot from being taken (#6540) * test(amneziawg): pin that a disabled row still owns its relay slot checkAmneziawgnetSocksConflict filters enable = true, so a disabled AmneziaWG row is not a candidate when an ordinary inbound's configured port is validated. SetInboundEnable then flips the column with no port check, so enabling that row later puts a second inbound on 127.0.0.1:65101 and Xray refuses the whole config. Expected red on this head; the fix follows. * fix(amneziawg): count a disabled inbound as owning its relay slot The forward port check filtered its candidates with enable = true, so a disabled AmneziaWG row was invisible when an ordinary inbound's configured port was validated. Nothing else covered the gap: the relay is not a database row, and SetInboundEnable flips the column with no port check, so re-enabling that row put a second inbound on 127.0.0.1:65101 and made Xray refuse its whole config, taking every other protocol on the host down with it. A row owns the slot its id derives for as long as the row exists, which is the rule the reverse-direction check already follows. TestCheckPortConflict_ DisabledAmneziawgStillOwnsItsRelaySlot fails without this, on a test-only head whose go-test run failed on exactly that test, and passes with it. * test(amneziawg): drop the disabled-row case that asserts the reversed rule TestCheckPortConflict_AmneziawgnetSocksRelayIgnoredWhenDisabled stated, in its name and its doc comment, that a disabled AmneziaWG inbound's port must not block anything -- the rule the parent commit reverses. It also never reached the predicate it named: its fixture seeds Settings: {}, which amneziawg.InstanceFromInbound rejects on parsed.Server == nil one statement before the enable column is read, so it passed with or without the filter. Leaving it would document both rules for the same operator state with nothing failing to flag the contradiction. The rule this PR pins is covered for real by TestCheckPortConflict_DisabledAmneziawgStillOwnsItsRelaySlot, whose fixture carries a qualifying server block and an enabled peer.
  • a036ddd66f fix(amneziawg): wrap the relay port window instead of refusing ids past it (#6539) * fix(amneziawg): wrap the relay port window instead of refusing ids past it An AmneziaWG inbound's loopback relay port is SOCKSBasePort + row id, and AddInbound refused any id that pushed it past 65535. The inbounds table is AUTOINCREMENT, so an id is never reused and the counter is only reset when the table empties: the 435-port window was a lifetime budget, and a database that had ever created more inbounds could never create another AmneziaWG one -- the reporter's counter sits at 70350, so the protocol never worked there at all (#6537). Ids now wrap into the same 435 ports, which leaves every id up to 435 with the exact port it had, so no existing row, relay or generated config moves. Wrapping makes the id -> port map non-injective, and nothing compared two derived relay ports before -- two relays on one port would leave Xray with a duplicate listen and refuse to start, taking the whole panel's proxy down. checkAmneziawgnetSocksRelayCollision now refuses a create or an edit whose derived port another local AmneziaWG row already owns, disabled rows included: a row owns its slot for good, and enabling it later re-runs no port check. * test(amneziawg): give each relay-window fixture its own client email Every fixture built the same client email, and an email is unique across the whole panel, so AddInbound refused the second create with "Duplicate email" before either new guard ran -- CI exercised neither the wrap nor the collision refusal. Each fixture now derives its email from its own tag, which is what the tag already exists for. * fix(amneziawg): say relay port in the relay conflict message A refusal that named the port of the automatic loopback relay read as if the named inbound listened on an unrelated port -- its own port is the WireGuard one. portConflictDetail now carries Relay, and both messages that report a derived relay port say "relay port N"; messages that report a configured port render byte-for-byte as before. * test(amneziawg): pin that a node-assigned inbound owns no relay slot A row adopted from a node carries a NodeID and the protocol it arrived with (inbound_node.go:737), yet injectAmneziawgnetSocks skips it, so it binds no loopback relay. The gate this PR added to checkPortConflictTx never looked at NodeID, so editing such a row can be refused for a slot it does not own. Expected red on this head; the fix follows. * fix(amneziawg): skip the relay guards for node-assigned inbounds Round-2 review finding: the gate this PR added to checkPortConflictTx keyed on inbound.Protocol alone, so it also ran for a row adopted from a node. Such a row carries a NodeID and gets no loopback relay -- injectAmneziawgnetSocks skips it and the desired-instance query is node_id IS NULL -- so it owns no slot and can collide with nothing, yet editing it was refused with "relay port N ... already used by inbound '<local>'", naming a port the edited row never binds. Wrapping made this visible: before it, an adopted id above 435 derived a port above 65535 that no row could hold, so the pre-existing reverse check under the same gate could not fire. Both call sites now require NodeID == nil, matching the local-only predicate the forward check already used. TestCheckPortConflict_NodeAssignedAmneziawgOwnsNoRelaySlot fails without this, with the exact false refusal, and passes with it.
  • 78ab7a9246 fix(amneziawg): read the outbound pseudo-protocol id like the core (#6531) * fix(amneziawg): read the outbound pseudo-protocol id like the core IsAmneziaWGOutbound compared the id exactly while every reader around it does not: the probe lane already reads the same id with strings.EqualFold (outbound/probe_http.go, pinned by TestBuildBatchTestConfigReadsTheProtocolIDLikeTheCore), and the core lowercases a protocol id before it resolves the handler. A template entry spelled "AmneziaWG" therefore stayed unbridged in two paths. transformAmneziaWGOutbounds skipped it and handed the raw pseudo-protocol to the core, which answers "unknown config id: amneziawg" -- Xray then fails to start, since bridging is what makes that entry a socks outbound. The amneziawg job skipped it too, so the reconcile loop never created the instance and the outbound silently carried no tunnel. The exact comparison also made the save path answer two ways for one spelling: CheckXrayConfig routed the exact match to the panel's own validator and the case variant to the core's, so the operator was told the core does not know a protocol the panel implements (probe output, before: `xray core rejects outbound "t1": infra/conf: unknown config id: amneziawg` for "AmneziaWG" and `amneziawg outbound "t1": privateKey is required` for "amneziawg"; after: the panel's own message for both). Reachable only from a template that did not come through the panel's save, which rejects the case variant today -- a restored backup, a direct DB edit, a scripted template, or a legacy DB. That is the same class of data the UppercaseFreedomFinalRulesFix seeder exists to repair, so the panel already treats non-lowercase protocol ids as real operator input. strings.EqualFold is the whole change; the package already imports strings. * style(service): trim the amneziawg outbound test comment to two lines The review flagged the three-line block: CLAUDE.md caps a committed Go comment block at two lines and the test name already carries the what. The remaining two lines keep the why — the core folds the id's case before resolving it, so a mixed-case spelling must bridge here too.
  • View comparison for these 5 commits »

4 hours ago

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

4 hours ago

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

4 hours ago

txlyre synced commits to master at txlyre/dtlspipe from mirror

4 hours ago

txlyre pushed to master at txlyre/jk

8 hours ago

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

20 hours ago

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

20 hours ago

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

  • a810f497e6 fix(xray): read the last two inboundTag protocol ids like the core (#6530) The core lowercases an outbound's protocol id before it resolves the handler, so an outbound spelled "Loopback" still is the loopback outbound. Both readers that keep a loopback outbound's inboundTag in step with the inbound it names compared the id exactly, so such an outbound was skipped: renaming or deleting that inbound left settings.inboundTag pointing at a tag that no longer exists, and traffic returning through the loopback outbound arrives under a tag no routing rule can match (infra/conf/loopback.go:15 carries the tag, proxy/loopback/loopback.go:43 uses it as the inbound identity). The probe lane's "nothing to test here" gate had the same exact comparison, so a "Freedom"/"Blackhole" outbound reported the vaguer "No testable endpoint" where the canonical spelling reports "Outbound has no testable endpoint" — the two spellings took different paths to the same rejection. Both readers now compare case-insensitively; the outbound package reuses its existing equalsAnyFold helper rather than adding a second one. The service reads the config template an operator edits, so a case variant is reachable there; server.go's GetDefaultLogOutboundTags scans the embedded config.json instead, whose protocols are canonical by construction, so it is left as is and no test can tell a case-insensitive read there from an exact one.
  • 837addf66e v3.8.0
  • 840a40edcd chore(deps): update frontend and Go deps Update Ant Design, React i18n, Zod, testing utilities, Oxc tooling, GORM Postgres, Pion transport, and sing dependencies to their latest specified versions.
  • c0271e231d fix(panel): read the outbound protocol id in the Outbounds row like the core (#6528) * fix(panel): read the outbound protocol id in the address column like the core outboundAddresses switched on the raw id, so a row the core runs normally but spelled "VMess", "Trojan" or "WireGuard" fell through to default and rendered an empty Address column in the outbounds table, the card view and the subscription table -- a populated server that looks absent, which is what sends an operator to recreate a correct outbound. The id is folded once before the switch, the way isUdpOutbound already folds the transport name. * fix(panel): fill the outbound address column from one protocol-id rule outboundAddresses folded the id inline while isUntestable, two functions below it in the same file, reads it through isOutboundProtocol — so the "the core lowercases the id" rule lived in two places and two tests. It now routes through the shared helper, which keeps the rule with the module that owns it. Two further gaps in the same switch, reported in the same review: hysteria and amneziawg are both selectable in the outbound form but had no case, so a canonically spelled row rendered a blank Address cell that case folding could not reach; and the VLESS branch returned a bare ":" for a row whose servers sit in vnext, which this change newly reached for a "VLESS" spelling. Tests: the hysteria/amneziawg cases and the bare-separator case are red on the pre-fix switch. * fix(panel): read the vnext shape of a vless outbound in the address column The vless branch read only the flat settings.address/port, so a row whose servers sit in vnext — the shape the probe's extractor reads first (internal/web/service/outbound/outbound.go:259-269) — rendered a bare ":" separator, or nothing at all before this branch folded the id. It now reads vnext first and falls back to the flat pair, the order the extractor uses, which also makes it agree with what a probe of that row would say. Test: "reads the vnext server of a vless row" is red on the pre-fix branch. * fix(panel): read the protocol id of the outbound stream tags like the core The identity cell gated the network and security tags on an exact-match includes() over four ids, so the same "VMess" row whose address this branch now shows still rendered without its ws/tls tags — the row was half-readable. It now asks the shared isOutboundProtocol, the rule every other reader on the page uses. Test: "renders the stream tags and the address of a VMess row" is red without this change (['VMess'] vs ['VMess','ws','tls']).
  • efcf152950 fix(outbound): read the probe testability gate's ids like the core (#6527) A direct, DNS, loopback or blackhole outbound is not a proxy, so the probe must reject it instead of measuring the panel host's own reachability. The gate compared the protocol id exactly while the core lowercases it in LoadWithID before resolving the handler, so "Freedom" and "DNS" were not recognised: the HTTP probe ran through the direct outbound and returned Success=true with a full egress block, and the row's Test button stayed enabled because isUntestable compared exactly as well. The operator reads the panel host's own country and delay as a working tunnel. The batch gate now folds the id once before its switch, and isUntestable goes through the shared isOutboundProtocol helper.
  • View comparison for these 10 commits »

20 hours ago

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

  • 39ce7cbc22 fix(xray): migrate the dns outbound off its legacy nonIPQuery and blockTypes (#6519) * 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.
  • c90996eda3 feat(sub): add opt-in month-end expiry presentation (#6517) Offer monthly calendar subscriptions an explicit last-valid-second display without moving their real billing boundary or spending renewal allowances. Keep the option off by default and limit conversion to a shared fixed day-1 midnight cutoff at an actual month transition in the panel timezone. Use the authoritative client calendar mode when aggregating node traffic, and share the header formatter across raw, JSON, and Clash exports. Expose the setting in the existing settings API/UI, regenerate its schemas, and document that clients may report expiry one second early or format the date differently in another timezone. Add HTTP, settings, and DST coverage. Stored deadlines, access enforcement, info/remark expiry values, and renewal accounting remain unchanged. Refs: #6516 Co-authored-by: JacktheRanger <219502738+[email protected]>
  • 826e29e2de fix(xray): place the freedom domain strategy where the core reads it (#6515) * fix(xray): place the freedom domain strategy where the core reads it freedom resolves through the socket layer, so xray-core reads sockopt.domainStrategy and treats both other placements as legacy: it warns on every config load for the outbound-root targetStrategy it migrates itself, and again for the settings-level domainStrategy it deprecates. The panel wrote exactly those two keys from its Freedom Protocol Strategy select, the outbound form card, and the IPv4 routing helper, so any install that had configured a strategy logged a deprecation warning on every start. The strategy now travels in streamSettings.sockopt everywhere the panel emits it: the Basics select, the outbound form (including the JSON tab, which shares the same adapter), the shipped default template, and the IPv4 outbound the routing helper injects. Reading mirrors the loader's own order — root targetStrategy, then the settings keys, then sockopt — so the card keeps showing the value the core would actually run with, and saving drops the legacy keys instead of leaving them behind. A seeder moves the keys for configs already stored in the database, following OutboundRemovedKeysFix. The shared outbound-root Target Strategy field is hidden for freedom, since the core migrates that key into the very sockopt value the card writes and two knobs for one value would race. Tests: placement round-trips and the migration table run through the real vendored core (a captured log handler proves the warning is gone after the rewrite and present before it), and the modal asserts freedom offers a single strategy field. * test(database): seed the template row the seeder test needs A fresh InitDB creates no xrayTemplateConfig row — the panel's setting defaults live in the service layer — so the test has to insert the legacy template itself and then assert the seeder's history gate stops a second pass from rewriting it. * fix(xray): keep one strategy control per outbound, seed the row in tests Review findings: the Transport tab's Sockopts block renders for freedom too, so its Domain Strategy select and the freedom card wrote one sockopt value between them and the card won on save — the field is hidden for freedom now, leaving the card as the single control. The seeder is also pre-marked on a fresh install so it does not run on the second start, and the seeder test seeds the template row itself (a fresh InitDB has none) and asserts the rewrite structurally instead of grepping for a key name that sockopt also uses.
  • 032ddcb29f fix(nodetoken): make the corrupt-ciphertext test corrupt deterministically (#6520) * fix(nodetoken): make the corrupt-ciphertext test corrupt deterministically The test replaced the last two characters of the base64 body with "AA", which can decode to the very same bytes: the body is RawURLEncoding of a 31-byte blob, so the final character carries only 2 significant bits and the decoded value is unchanged whenever the tag's last byte is 0x00. Measured over 50000 encryptions, 170 of those edits corrupted nothing — about one run in three hundred fails for a reason that has nothing to do with the codec. Flipping a bit of the decoded blob always changes the ciphertext, so the test now pins the fallback behavior instead of the encoder's tail padding. * style(nodetoken): trim the helper comment to the two-line limit CLAUDE.md caps a committed Go comment block at two lines; the why fits.
  • a09e136001 docs: add Discord bot to READMEs, architecture, operations guides, and locales (#6513) * docs: add Discord bot to READMEs, architecture, operations guides, and locales * docs: address review feedback on Discord bot formatting, backup commands, and architecture * docs(discord): fix Persian typo and literal arrows on fa/zh bot pages Senior review of #6513, two LOW findings in the two new pages: - fa/operations/discord-bot.mdx:30 spelled "developers" with Cyrillic "де" in place of Persian "ده", rendering a mixed-script word. - Both pages copied `$\rightarrow$` from the en page. The docs site has no math plugin (nothing in source.config.ts, no remark-math installed), so the built HTML shows the literal string "$\rightarrow$" in every menu path. Replaced with a Unicode arrow on fa and zh; en and ru have carried the same since #6486 and are left for a separate change. --------- Co-authored-by: Sanaei <[email protected]>
  • View comparison for these 6 commits »

1 day ago

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

  • cfd4f64a79 fix(amneziawg): bound the SOCKS5 UDP associate exchange newSocks5UDPSession only bounded the dial. The greeting, auth and UDP ASSOCIATE reads on the control connection had no deadline, and they run inline in UDPRelay.Handle -- on the peer's receive goroutine that delivers decrypted packets into gVisor. A SOCKS5 server the kernel accepts for but that never answers (a wedged Xray: its listen backlog still completes TCP handshakes) therefore parked that goroutine, and every later packet from the peer behind it, TCP included, for as long as Xray stayed wedged. One deadline now covers the dial plus the whole exchange and is cleared once the association is up, since after that the control connection is only held open. The test drives the exchange against a listener that is never accepted, which is exactly the hung-server shape. This was the last of the three defects confirmed on the issue: the header protection key that could not be cleared went with cfd596a4, the missing PersistentKeepalive with 8f162994, and the manager lock inversion the same thread flagged with e95fe80f. The session death the issue was opened for is not a panel defect. The reporter's own capture on the host NIC shows the client's packets stop reaching the VPS after the first burst, the server never sees a second handshake initiation from it, and nothing the panel sends is outside what a stock amneziawg-go 3.1 server sends (the Apple client embeds the same library build). That is a client- or path-side stop, which no server-side change can address. Closes #6323
  • 22346eef78 fix(node): import a newly selected node inbound instead of sweeping it 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
  • 5ad9df69b9 fix(link): restore mKCP seed and headerType on share-link import (#6480) * fix(link): restore mKCP seed and headerType on share-link import applyTransport / applyTransportParams ignored kcp query params that applyKcpShareParams emits, so re-imported outbounds lost seed and header and could not talk to the inbound. Mirror those fields (plus mtu/tti) into kcpSettings in both Go and TS importers. Fixes #6476 * fix(link): restore mKCP header/seed via finalmask mkcp-legacy * fix(link): split mKCP header and seed into separate masks on import Both importers folded a share link's headerType and seed into one mkcp-legacy mask {header, value}. xray-core's MkcpLegacy.Build ignores value once header is set (and reads it as the fake DNS domain for header=dns), so an imported outbound carried the header mask but no AES-128-GCM seed while the emitting inbound has both, and could not connect — the failure #6476 reports, now for every link carrying both params. Emit one mask per field, seed first: the finalmask array's first item is the innermost layer, which puts the header around the cipher as legacy mKCP did. Also bound mtu/tti to KCPConfig.Build's accepted ranges (mtu >= 21, tti 10..1000, decimal digits only on both importers) so a pasted link cannot fail the whole Xray config load, and look header types up as own properties so a prototype key such as "constructor" is not mapped. --------- Co-authored-by: mrchatam <287639636+[email protected]> Co-authored-by: Sanaei <[email protected]>
  • 939c470698 feat(inbounds): show linked host remarks in inbound list (#6468) * feat(inbounds): show linked host remarks in inbound list Join Host Group remarks from the existing hosts list onto each inbound row client-side so multiple endpoints (IPv4/IPv6/CDN) are visible without opening the inbound. Truncate long lists with a tooltip for the full set. Fixes #6026 * fix(inbounds): skip disabled host groups in inbound list remarks buildHostRemarksByInboundId joined every host group from /hosts/list onto its inbounds, so a group toggled off on the Hosts page still read as a live endpoint in the inbound remark cell and matched the search box. A disabled group serves nothing: internal/sub/host_sub.go filters it out of subscription output and withMtprotoHostEndpoints skips it for MTProto share links. Skip it here the same way, and drop the unread `truncated` field from formatHostRemarksLabel. --------- Co-authored-by: mrchatam <287639636+[email protected]> Co-authored-by: Sanaei <[email protected]>
  • 4760ccaba0 fix(logs): standardize logs (#6484) * fix(logs): standardize login and logout logs * fix(logs): log the real username on login lines The four login log lines logged safeUser, the HTML-escaped copy kept for the Telegram and email notifiers, so an account named o"reilly<1> showed up as o&#34;reilly&lt;1&gt; on login but o\"reilly<1> on logout. %q already neutralises control characters, so the log now carries form.Username and safeUser feeds only the notifiers. Resolves the pre-existing LOW left on PR #6484. TestLoginLogsRealUsername drives the success, plain-failure, blocking and refused paths over HTTP and fails on the escaped value. Refs #6483 --------- Co-authored-by: Mapioe <[email protected]> Co-authored-by: Sanaei <[email protected]>
  • View comparison for these 6 commits »

1 day ago

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

  • d0ad773edf fix(clients): preserve traffic reset schedule when toggling enable (#6502) Carry the hydrated reset cycle and day into the enable update payload so toggling a client does not normalize its schedule to never. Cover both toggle directions with a regression test.
  • d600de2c2e feat(geodata): add standard source presets (#6504) * feat(geodata): add standard source presets Expose the existing geofile allowlist to the Geodata editor so administrators can configure the supported scheduled downloads without copying URLs manually Tested with npm run test, npm run lint, npm run typecheck, npm run format:check, and go test ./internal/web/service ./internal/web/controller -run '^(TestStandardGeodataSources|TestGeodata)' -count=1 Assisted-by: OpenCode:openai/gpt-5.6-terra (mostly) * fix(geodata): preserve custom source entries Add missing standard sources instead of replacing existing custom entries. Assisted-by: OpenCode:openai/gpt-5.6-terra (mostly)
  • ff1a6c3caf fix(sub): drop external Clash shadowsocks nodes the panel cannot express (#6508) * fix(sub): gate external Clash shadowsocks links like the inbound path clashProxyFromExternal returned as soon as it had built the ss proxy, so an ss:// link skipped applyTransport/applySecurity: a node whose tcp/http obfuscation Clash cannot express was emitted anyway (mihomo then opens a plain shadowsocks stream at a server that requires the header, and the node silently never connects), and security=tls was silently stripped. The inbound path runs both helpers for every protocol, so the two Clash importers disagreed about the same node. * fix(sub): count a dropped external link in the quota header The client email that feeds AggregateTrafficByEmails was recorded only when a proxy came out of the link, so a node Clash cannot represent also vanished from the Subscription-Userinfo header of every other node in the same subscription — the header reported another client's numbers as the whole subscription's. The inactive-link branch already counted an email without a proxy; make that unconditional so the header describes the subscribers, not the representable subset of their nodes. * docs(sub): describe clashProxyFromExternal by what it does, not by protocol The protocol list in the doc comment went stale the moment the shadowsocks branch stopped returning early, and it restated what the switch already says.
  • 8fc4fc0bf8 fix(link): rebuild shadowsocks tcp/http obfuscation on import (#6505) * fix(link): rebuild shadowsocks tcp/http obfuscation on import genShadowsocksLink encodes tcp/http obfuscation only as the SIP002 plugin=obfs-local;obfs=http;obfs-host=... parameter, deleting type, headerType, path and host in the process, because SIP002 clients ignore those and read `plugin` alone. ParseLink read none of them, so importing a link the panel had just exported produced a plain tcp outbound with header.type none: the obfuscation the inbound requires was gone, and the client could not connect to the very inbound the link came from. The plugin is now mapped back onto the header it stands for. Credentials and every other parameter are untouched, and other plugin values are left as they were because Xray has no equivalent for them. * fix(link): map the SIP002 plugin in both importers The panel parses share links twice: link.ParseLink in Go, which the external subscriptions use, and parseShadowsocksLink in outbound-link-parser.ts, which the Add Outbound button calls. Mapping the plugin in Go alone left the UI path still saving header.type none for a link the panel had exported itself, so one panel answered the same link with two different outbounds. The unencoded plugin=obfs-local;obfs=http;... form maps as well now: stdlib drops any query pair whose value holds a literal semicolon, and that is the shape clients which skip percent-encoding emit, so the raw query is read as a fallback when the parsed parameter is missing.
  • f3dba07e13 fix(link): read the vmess certificate checks on import (#6507) * fix(link): read the vmess certificate checks on import applyVmessTLSParams writes ech, vcn and pcs into the vmess share object, but parseVmess only read sni, fp and alpn back. Importing a link the panel had just exported therefore dropped all three: no pinned certificate, no verify-by-name, no ECH. On a server whose certificate is only trusted through a pin, the imported outbound falls back to public-CA verification against the system roots and cannot connect to the inbound the link came from. The url-param protocols already read the same three in applySecurity, and the core takes pinnedPeerCertSha256 as one joined string there, so the vmess path now fills them the same way. * fix(frontend): read the vmess certificate checks on import The panel parses share links twice: link.ParseLink in Go and parseVmessLink in outbound-link-parser.ts, which is what the Add Outbound button calls. Reading ech, vcn and pcs in Go alone would have made the two sides disagree on one link, leaving the UI path — the one an operator uses by hand — still dropping the pin the panel had just exported.
  • View comparison for these 13 commits »

1 day ago

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

  • cba8f0672f feat(sub): refine Happ routing presets, serverDescription escaping, and auto-detect placement (#6488) * feat(sub): refine Happ routing presets, serverDescription escaping, and auto-detect placement * fix(sub): address PR review findings on routing parity, agent regex, and i18n
  • c7518c4038 fix(tgbot): send the admin traffic reports as one message (#6490) Reset all traffics and the sorted usage report replied with one Telegram message per client. A panel with a few hundred clients therefore fired a burst of sendMessage calls that trips Telegram's per-chat rate limit and throttles the bot for every user, not just the admin who tapped. Both reports are now assembled into one string and handed to SendMsgToTgbot, which already pages long messages. Two details the batching would otherwise lose: the reset report still answers (with the reply keyboard removed) when the panel has no clients, and both reports are HTML-escaped as a whole, because a single stray "<" in a remark or an email now costs the ~15-client page it lands on instead of one client's message.
  • 6a5b4fab6a feat(happ): generate Crypt5 subscription links locally (#6494) * feat(clients): add stateless Happ link generator Generate Happ provider links from the current effective subscription source without caching results. Reject unsafe provider responses and redact failure diagnostics. * fix(clients): reject duplicate Happ provider fields Parse Happ provider objects token by token so duplicate supported keys cannot be silently overwritten by encoding/json. * feat(clients): expose on-demand Happ link API Expose a no-store client endpoint backed by the Happ link generator and keep its generated OpenAPI contract synchronized. * fix(openapi): exclude service interfaces from generated types Keep dependency-injection interfaces out of the frontend API surface while preserving allowed response schemas. * feat(clients): add stateless Happ QR presentation Generate Happ links only for the active modal scope and retire late responses so Standard remains immediately available. Add focused component coverage and localized retry guidance across every locale. * fix(clients): cover overlapping Happ generations Prove the cancellation cleanup is required by resolving a retired request while its replacement remains pending. Also wait for Regenerate to leave loading state before exercising the existing action. * fix(clients): harden Happ link handling Validate generated responses before rendering and hide actions during unresolved requests. Strengthen route, redirect, timeout, and lint regression coverage with mutation-sensitive tests. * fix(clients): gate Happ link generation behind operator opt-in - add a fail-closed happLinkEnable setting - enforce the gate before and after provider requests - add locked Happ QR state with privacy disclosure and settings link - cover backend, frontend, settings, and i18n regressions * fix(frontend): guard oversized Happ QR codes Keep valid long crypt5 links copyable while suppressing QR rendering and image actions above the encoder's UTF-8 byte limit. Add localized guidance and boundary coverage. * fix(clients): log the sanitized transport error for Happ link failures Every fail() call in HappService.Generate passed a string literal as the detail, so the sanitizer written for provider errors only ever saw constants, and an operator following the QR modal's "check Logs" hint found nothing beyond reason=transport. Transport and body-read errors now flow through sanitizeHappDetail, which also redacts cookie/session pairs. Drop TestHappLinkEnableDefaultsOffWithoutPersistingRow: it pinned a getter and its constant default, which the Generate gate test already drives. * fix(frontend): size the Happ QR cap to level L and keep the QR modal mounted on close HAPP_QR_MAX_BYTES was the level-M capacity (2331) while QrPanel encodes at errorLevel "L", whose version-40 byte-mode capacity is 2953, so valid links between 2332 and 2953 bytes lost their QR. The cap now matches the encoder and a test renders the real QrPanel at the boundary. Keying the modal content on `open` remounted it on every close, which cut the Modal's exit transition and made the openSubId sync unreachable, so `loading` never turned on for the subLinks fetch and a client without a subscription link flashed noLinks on reopen. `open` leaves the key and the sync block now also resets the Happ state. * chore(clients): request Happ crypt5 links from api-v3 crypto.happ.su serves api-v2.php and api-v3.php side by side. Probed with the same payloads, both take {"url"} over a JSON POST, answer {"encrypted_link":"happ://crypt5/..."} of identical length with the same crypt5 key marker, and fail the same way: 400 "No url provided.", 500 "Invalid URL format.", 405 on GET. Happ's own generator page is branded "URL Encryption v3", so the panel follows it. The parser and the link validator are unchanged. * feat: add local generation of encrypted Happ links - Implemented functionality to generate encrypted Happ links locally without network dependency. - Added validation for URL length and format to ensure compliance with processing limits. - Introduced new error handling for invalid URLs and control characters. - Updated translations for various languages to reflect changes in Happ link generation. - Created unit tests to validate the encryption process and ensure session keys and nonces are unique. * fix(frontend): match the tuic memo deps to the non-optional subSettings The Happ branch reads subSettings non-optionally in ClientQrModalContent (happLinkEnable and the WireGuard/AmneziaWG publicHost memos), so React Compiler infers subSettings.publicHost. The TUIC memo merged in from main still listed subSettings?.publicHost, which fails oxlint's preserve-manual-memoization rule and makes the compiler skip optimizing the component. make verify stopped at lint-fe on the branch head. * chore(happ): trim the pinned-key provenance comment to two lines CLAUDE.md caps a comment block at two lines. The bare URL line repeated the repository and file the next line already names, so it is folded into that line (review LOW on happ_crypto.go). --------- Co-authored-by: Sanaei <[email protected]>
  • c3b08b6d9f fix(tgbot): guard the mock Telegram server's call counts staleButtonServer increments its per-method map from the HTTP handler, which httptest runs on one goroutine per connection. #6491's TestAdminListReadersShareTheWriterLock is the first test to reach it from several goroutines at once, so CI's race job flagged the helper's map rather than the code under test.
  • b98f947efe fix(tgbot): answer only the link callbacks that match nothing #6493 was written against the if/else chain where every served link action returned early, so its trailing answer ran only for an unrouted payload. #6489 had already turned that chain into a switch that falls through, so after the merge every served link tap also got an error toast, while an unknown payload still returned from the !ok branch unanswered. Move the answer into the !ok branch, the one place nothing matched. This turns TestClientLinkCallbackServesOwnClient and TestUnroutableCallbackIsAnswered green again on main's go-test job.
  • View comparison for these 10 commits »

2 days ago

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

  • 72df05a403 fix(hosts): keep TLS override fields visible when Security is same (#6452) When Security is same, Fingerprint/SNI and TLS extras stayed live in form state but were hidden, so stale values could not be cleared (#6444).
  • 503b5df4b9 fix(link): preserve Shadowsocks TLS query params on import (#6467) * fix(link): preserve Shadowsocks TLS query params on import Mirror trojan/vless stream parsing so Xray-native type/security/sni/alpn/fp query params on ss:// links survive into streamSettings on both Go and TS importers. Fixes #6094 * fix(link): drop extra blank line so oxfmt passes --------- Co-authored-by: mrchatam <287639636+[email protected]>
  • bdd351bd15 fix(api): return 401 for invalid Bearer token instead of 404 (#6459) When Authorization: Bearer is present but does not match (or is disabled), respond with 401 Unauthorized so script authors can distinguish auth failure from a wrong webBasePath. Requests with no Authorization header still get 404 masking; wrong base paths continue to 404 via NoRoute. Fixes #6255 Co-authored-by: mrchatam <[email protected]>
  • 958d7f138e fix(frontend): fold sockopt v6only into V6Only on inbound load (#6453) Prevents duplicate keys and an unreachable switch when Advanced/API configs store lowercase v6only (#6421).
  • 6a159683d5 docs: update star history badges Replaced the legacy starchart.cc widget with Star History chart and badge embeds in the main README and all localized variants. This keeps the star visual consistent and adds ranked/trending badges for easier repository context.
  • View comparison for these 21 commits »

3 days ago

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

  • 0a2cd789ba fix(sub): enable ML-KEM for Mihomo REALITY subscriptions (#6451)
  • 9f07951ba7 feat(outbounds): support custom subscription user agents (#6398) 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.
  • View comparison for these 2 commits »

3 days ago

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

  • 89ee1242bd feat(sub): add read-only HWID device-slot status endpoint (#6380) * feat(sub): add read-only HWID device-slot status endpoint Closes #6357 A client with an HWID limit had no way to tell a subscriber how many device slots were left: /{subPath}/{subId} only exposes the gate as a boolean through X-Hwid-* headers on a 404, and ?format=info carries no limitHwid or registered count. Every "why can't I connect on my new phone" case therefore had to be answered by the operator by hand. GET /{subPath}/{subId}/hwid-status now returns the aggregate counters: {"active":true,"limit":2,"registered":1,"remaining":1,"full":false} - SELECT-only. It never registers an hwid, never touches last_seen and never calls the enforcement path, so asking about a slot cannot spend one. - Counters only: no hwid value or hash, no email, no device metadata, no IP, no User-Agent, and none of the X-Hwid-* gate headers. - The subscription id is already the bearer secret for /{subPath}/{subId}, so no admin token and no new auth mechanism. - Unknown and disabled subscriptions both answer a bare 404, with identical status, headers and body, so the route cannot be used to probe which subscription ids exist. - No HWID limit configured returns {"active":false,"limit":0,...}. - No schema change and no migration. Scoped to enabled clients exactly like effectiveHwidLimitForSubID, so the reported limit is always the limit the gate enforces on a shared sub_id, and remaining clamps at zero when the effective limit drops below the number of registered devices. A separate route leaves /{subPath}/{subId}, ?format=info and the JSON/Clash routes byte-for-byte unchanged. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> * fix(sub): document hwid-status as the bare object it returns The OpenAPI operation for GET /{subPath}/{subId}/hwid-status inherited the {success,msg,obj} panel envelope from build-openapi.mjs's default 200 response, while the handler writes the HwidSlotStatus struct bare. A client generated from the spec would read `obj` and never find the counters, and the description prose contradicted the schema with a hand-written example. HwidSlotStatus now sits in openapigen's StructAllow with example: tags, the entry references the generated schema through a `responses` block, and build-openapi.mjs attaches the generated example to any `responses` entry that $refs a generated schema, so no example is hand-written. The HEAD variant the controller registers is documented like its siblings, and the summary follows the "path prefix is configured by subPath" wording now that fresh panels randomise the prefix. Regenerated frontend/public/openapi.json, docs/public/openapi.json and the subscription-server MDX. openapi-runtime-contracts.test.ts pins the bare schema, the generated example and the HEAD operation. --------- Co-authored-by: Claude Opus 5 (1M context) <[email protected]> Co-authored-by: Sanaei <[email protected]>

4 days ago

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

  • 8f162994ef feat(clients): let admins set PersistentKeepalive on tunnel clients (#6377) * 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 <[email protected]>
  • 64b6e43e2b feat(sub): add legacy Clash subscription endpoint (#6338) * feat(sub): add legacy Clash subscription endpoint * fix(deps): update js-yaml to patched release Raise the Swagger UI js-yaml override to 4.3.2 and refresh the lockfile to resolve GHSA-2883-xcg3-v3hh without changing Swagger UI. * fix(sub): preserve client detection and normalize legacy cipher Keep the original Clash/Mihomo auto-detection default so existing subscription URLs continue returning YAML. Normalize the panel-supported chacha20-poly1305 alias when generating legacy Clash profiles, and cover both regressions through HTTP endpoint tests. * refactor(sub): drop an unreachable guard and make the alias test assert Review of the legacy Clash subscription endpoint left three LOW findings, all introduced by the change: - The comment above the routing merge ran to three lines, over CLAUDE.md's two-line cap. - validateClashRouteGraph on the legacy path could never fail: the legacy branch skips the routing merge, so it validated the literal config built a few lines above against itself. Dead code that reads as a guard. - TestClashAliasesSkipConfiguredPathConflicts asserted nothing — it could only fail on an escaping gin panic, so a regression that registered the alias handler on the configured path went unnoticed. It now drives each collision through the router and asserts which format answers each path. --------- Co-authored-by: Sanaei <[email protected]>
  • 3f1e52f09e refactor(panel): drop two duplicated helpers SettingService.GetDefaultJSONConfig was a byte-identical copy of GetDefaultXrayConfig with no callers anywhere in the tree. amneziawgnet.normalizeDNSServer re-implemented the exported amneziawg.NormalizeDNSServer line for line, in a file that already imports that package for EffectiveMTU three lines above it. Its two callers now use the exported one, so the bare-IP-to-host:port rule has a single definition.
  • 0fbdf0f9bf fix(ui): keep the empty-group placeholder legible in dark mode The "no group" em dash in the clients table and in two client-picker modals was drawn with an inline color: rgba(0,0,0,0.45). The panel renders every page under antd's darkAlgorithm as well, so on a dark container that near-black placeholder is effectively invisible. All three now use Typography.Text type="secondary", the idiom the rest of the panel already uses for muted text — GroupAddClientsModal itself uses it 58 lines further down. Mid-greys such as #888 elsewhere in the panel stay legible in both themes and are deliberately left alone.
  • 02f2a63c53 refactor(tgbot): extract the shared numeric keypad builder Six callback flows — limit_traffic, reset_exp, ip_limit and their add_client_* counterparts — each carried the same 28-line inline keyboard: cancel, confirm, the 1-9 grid, clear, 0 and backspace. Only the callback prefix, the threaded email argument, the cancel target and the confirm label key ever differed, and the labels had already drifted apart between otherwise identical flows. numericKeypad now builds that grid from a numericKeypadSpec, cutting 168 lines to 6. The callback data is unchanged: every one of the 84 strings and all 6 label keys were diffed against the pre-refactor router and are byte-identical, which matters because encodeQuery hashes any query over 64 chars and buttons already sitting in a user's chat carry these strings.
  • View comparison for these 6 commits »

4 days ago

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

  • fc08b53395 feat(ui): add global command palette (Ctrl+K) for fast navigation and search (#6352) * feat(ui): add global command palette (Ctrl+K) for fast navigation and search * fix(ui): address review feedback for shortcut listener, i18n parity, and search deep links * fix(ui): resolve search routing, translation keys, and palette state reset * fix(ui): improve command palette styling and sidebar transitions * fix(ui): address review feedback for typecheck, codegen, debouncing, and state reset * fix(ui): resolve effect state update warning and debounce reset in command palette * fix(ui): address review feedback for stale client search results and theme action * style(ui): apply oxfmt formatting to command palette and tests * fix(deps): update js-yaml override to resolve audit advisory * docs(api): sync the docs OpenAPI copy with the new InboundOption fields Adding Network/Security to InboundOption regenerated frontend/public/openapi.json, but docs/public/openapi.json is a hand-kept copy of that file and nothing checks it: make verify never reaches docs/, and docs-ci.yml fires only on docs/**. The two files were byte-identical on main and had diverged here, so the published API reference described a response shape the panel no longer returns. Regenerating the MDX under docs/content/docs/en/reference/api/ produced no change — the schema is read from the JSON at render time. * fix(ui): unnest the command palette row control and label its shortcut The palette row was a <button> wrapping the copy-subscription <button>. Nested interactive content is invalid HTML and React 19 logs two errors for it on every client result. The row is now a role="button" div using activateOnKey, the pattern the rest of the panel already uses, with line-height pinned so dropping the UA button style does not grow every row. Its keydown handler ignores events bubbling from the nested button: activateOnKey preventDefaults Enter, which would otherwise cancel the browser's Enter-to-click on the copy button and navigate instead. The sidebar chip hardcoded the Mac glyph while the handler accepts Ctrl as well, so Linux and Windows operators were shown a key they do not have; it now picks the modifier from the platform. Also restores the comment on ClientsPage's debouncedSearch that the deep-link change removed — the code it explains is unchanged.
  • 2dd903ea8e feat(sub): bake Happ/INCY routing profiles into the JSON subscription (#6402) * feat(sub): parse generic Happ/INCY routing payloads for the JSON subscription Accepts the routing-rules format emitted for Happ and INCY (inline JSON, happ:// or incy:// deeplink, or a remote https:// URL resolved through the existing remote routing cache). The JSON subscription will bake these rules into its documents so header-ignoring clients still get routing. * feat(sub): bake Happ/INCY routing profiles into JSON subscription documents When subJsonRoutingRules is set, every emitted document (per-inbound and balancer alike) carries the profile's dns and routing rules baked in, so header-ignoring clients like Happ and INCY still get routing; the legacy simple-rules merge only applies when no profile is set. The balancer document builder keeps rewriting proxy-tag rules to the balancer. * feat(sub): add the subJsonRoutingRules setting Plumbed from the settings store through the subscription server into SubJsonService, so admins can set a routing profile once and every JSON subscription document carries it. * chore(api): regenerate OpenAPI artifacts for subJsonRoutingRules * feat(web): routing profile editor for the JSON subscription A textarea inside the JSON card accepts the routing profile (inline JSON, happ/incy deeplink, or https URL) with a remote-source badge; the badge helper moves to a shared module. Keys added to all 13 locales. * fix(sub): warm and lazily resolve the baked JSON routing source The routing profile was resolved once at service construction: a remote URL that was cold at that moment baked default routing forever, and the cron job never warmed it. The job now warms the subJsonRoutingRules URL, and the profile resolves per request with an in-memory memo (a failed resolve is not cached), so a warmed cache takes effect without a restart. * feat(sub): fall back to the JSON routing profile for the Routing header Happ and INCY download the geo files a routing profile references through the Routing response header. When the Happ header setting was blank the header stayed unset, and clients fetched no geo files even though a JSON routing profile was configured. A blank setting now falls back to the JSON profile: happ/incy deeplinks pass through, inline JSON and remote URLs are normalized to a happ:// deeplink; an unusable or oversized value leaves the header unset. Locale captions mention the fallback. * fix(sub): pass routingRules arg at call sites added by main Main gained four NewSubJsonService call sites after this branch forked; update them to the five-arg signature so internal/sub builds again. * fix(sub): address code review findings on the baked JSON routing The memoised baked template never invalidated, so an edited remote profile kept serving the superseded dns/routing subtrees until a panel restart; bakedTemplate now re-resolves the spec per request and rebuilds only when the payload actually changed (regression-tested). subJsonRoutingRules shared the happ persistence row with subRoutingRules, so only the last-written setting survived a restart; it now resolves under its own jsonhapp kind with the same validation and size caps. The setting also joins validateSettingsURLs, so remote values are canonicalised and bad URLs are rejected on save. Also: drop the unreachable half of the remote-source guard, cut the overlong comment blocks to the two-line convention, and deduplicate remoteSourceBadge in the General tab. Merges upstream/main (call sites for the widened NewSubJsonService signature). * style(sub): gofumpt the json_routing imports * fix(sub): accept happ add/ deeplinks and bound the routing warning The baked-JSON routing parser only recognised happ://routing/onadd/, but normalizeHappRouting treats happ://routing/add/ as an equally valid routing deeplink. An operator pasting the add/ form got the Routing header set, so the panel looked configured, while every JSON subscription document silently carried the default routing instead of their profile. resolveJsonRoutingSpec logged one warning per call and bakedTemplate calls it once per emitted document, so a single fetch of an unusable profile wrote one identical warning per document. On the public subscription server that floods the 10240-entry buffer the panel's log view reads, evicting real entries. Log only when the message changes, and reset on a successful resolve so a profile that recovers and fails again is still reported. Also resolve the template once in buildBalancerConfig: two resolves could straddle a profile refresh and pair one revision's dns with the other's routing.
  • ed5465d0f2 feat(clients): support setting HWID limit and MTProto ad-tag in bulk adjust (#6399) * feat(clients): support setting HWID limit and MTProto ad-tag in bulk adjust Add HWID device limit and Telegram MTProto sponsor channel (ad-tag) support to the bulk client adjustment flow in both the panel API and frontend ClientBulkAdjustModal. Co-Authored-By: Claude Code <[email protected]> * fix(clients): gate adTag to MTProto inbounds and avoid inbound rewrite for limitHwid Co-Authored-By: Claude Code <[email protected]> * fix(clients): stamp updated_at only on the clients a bulk adjust changed The updated_at write was gated on hasInboundChanges, which accumulates over the whole inbound instead of describing the client in hand. Once any client in the settings array changed, every client after it was re-stamped as well, so whether an untouched client kept its own updated_at depended on its position in the array. That field feeds node-snapshot conflict resolution, where a spurious bump lets a stale snapshot value win over the stored record. Track the change per client and fold it into the inbound-level flag where the stamp is written, so the early return still skips a save whose settings JSON would be unchanged. Also condenses the BulkAdjust doc comment back to the two-line maximum. * docs(api): regenerate the bulkAdjust reference for limitHwid and adTag frontend/public/openapi.json was copied to docs/public/, but pnpm gen:api was never re-run, so the API reference page's heading, anchor id and search index still described bulkAdjust without limitHwid or adTag. docs-ci.yml fires only on docs/**, and that path had been touched, so nothing flagged the stale MDX. The externalLinks hunks are the generator rewrapping lines main had left stale, not a content change. * fix(i18n): stop enumerating fields in the bulk-adjust empty-form message bulkAdjustNothing listed the fields the form accepts, so it went stale every time one was added: only en-US ever gained "flow", leaving the other twelve locales describing days and traffic alone, and limitHwid and adTag would have repeated that. Say that one field is required instead of naming which, so the message cannot drift again.
  • 1456658028 feat(sub): add Happ client integration, routing presets, and app management (#6434) * feat(sub): add Happ client integration, routing presets, and app management Implement comprehensive Happ proxy client integration according to official developer specifications. - Fix header emission on disabled routing and hidden settings to send explicit '0' headers rather than omitting, allowing Happ clients to reset cached settings. - Add support for 'happ://routing/off' deeplink in routing validation. - Preserve '?serverDescription=' query parameters in link fragments without escaping to support Happ server subtitles across VMess, VLESS, Trojan and SS. - Add Happ application management headers: ProviderID, New-Url, Fallback-Url, Sub-Info banners, Sub-Expire notifications, No-Limit mode, hardware ID enforcement, TUN modes/types, route exclusions, APNS exclusions, and per-app proxy settings. - Add curated routing presets (Iran Bypass, China Direct, AdBlock, Global) and interactive visual rule generator in frontend settings. - Synchronize all 13 translation locales with native Persian, Russian, and Chinese translations. * fix(sub): keep Happ header overrides behind the auto-detect opt-in The Routing-Enable/Hide-Settings off values were emitted on the User-Agent alone, so every panel that upgraded would push "Routing-Enable: 0" — documented by happ.su as disabling routing globally — to every Happ client without the operator enabling anything. They now ride subHappAutoDetect like every other Happ header. Two further mismatches against the vendor spec: - serverDescription was written as a key of the VMess base64 JSON object. happ.su documents it as a "#Title?serverDescription=<base64>" link parameter or a JSON "meta" entry, so the caption never reached Happ while every other VMess consumer received an unknown key. Dropped rather than moved: emitting the documented form is unsafe here because our own parser base64-decodes the whole VMess body (internal/util/link/outbound.go). - The TUN Mode dropdown stored the literal "default", forwarded as "Tun-Mode: default", where happ.su documents system|gvisor only. It now stores the unset value so no header is sent. TUN Type "default" is a documented value and is unchanged. Each fix carries a test that fails without it.
  • d5ab84e8d5 feat(amneziawg): add AmneziaWG as an outbound protocol (#6320) * feat(amneziawg): add AmneziaWG as an outbound protocol - AmneziaWG outbound protocol end-to-end: config schema, socks bridge, netstack, panel UI - Route amneziawg outbounds to HTTP probe in TCP mode (backend + frontend classifiers) with pinning test - Add 2-minute idle read deadline to pumpUDPEgress to reap idle egress sessions - Require SOCKS5 username/password auth on the egress server (reject NO-AUTH with 0xFF) with test - Bound the egress TCP tunnel dial with portForwardDialTimeout (10s), matching portfwd.go - Resolve UDP domain targets off the association's reader loop via deliverUDPDatagram; race-safe getOrDial starts the reply pump at session creation; client passed by value into resolver goroutines (pinned by TestEgressUDPDatagramDomainInterleavedClients) - Reconcile early-returns on an empty desired set and closes the egress listener; EgressBasePort (64900) is reserved against local inbound port conflicts like the internal API port, with pinning tests for both the port reservation (TestCheckPortConflict_EgressPortBlockedLocal) and the Reconcile empty-desired Close/Listen lifecycle (TestOutboundManagerReconcileEmptyDesiredClosesEgress) - Eliminate acceptLoop shutdown race by validating listener != nil and registering to tracked under s.mu before wg.Add; bound pre-auth handshake with deadline (pinned by TestEgressServerCloseDuringConcurrentAccepts) - Support AAAA and dual-stack domain resolution in tunnel DNS resolver with v6 default fallback (DefaultTunnelDNSServerV6); add DNS field to frontend protocol form; avoid unneeded cache flushes on unchanged SetStack ticks * fix(amneziawg): resolve IPv6-only DNS default fallback and validate required keys - Default to IPv6 tunnel DNS on IPv6-only outbounds with blank dns - Require non-empty secretKey and peer publicKey in ValidateAmneziaWGOutbound - Add end-to-end IPv6 tunnel domain resolution test and test empty key rejection - Trim comment blocks exceeding 2 lines across modified files - Fix Storybook test execution on environments with POSIX locale Co-Authored-By: Claude Code <[email protected]> --------- Co-authored-by: rqzbeh <[email protected]> Co-authored-by: Claude Code <[email protected]> Co-authored-by: Sanaei <[email protected]>
  • View comparison for these 6 commits »

4 days ago

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

  • 8076d5edfa chore(frontend): bump React and Zod deps Update frontend dependencies to React/ReactDOM 19.3.0, Zod 4.6.1, and matching React type packages. Also replace Storybook addon-vitest override placeholders with explicit Vitest/browser-playwright versions to keep dependency resolution stable.
  • 87420e3bb1 fix(tgbot): close stale-inbound TOCTOU and contain handler panics (#6442) Tapping an old get_clients_for_* inline keyboard re-fetched the inbound after the keyboard lookup, discarding the error; if the row vanished between the two reads, the second GetInbound returned nil and inbound.Remark panicked. The callback handler runs on a bare goroutine with no recover(), so that panic killed the whole panel process. Fetch the inbound once in a shared chooseInboundClient helper that answers an error callback on a missing row, and pass the row down to getInboundClientsFor instead of re-reading the DB, removing the between-reads window. Route all three OnReceive handler paths through a recover() barrier so no handler panic can take down the process, and log the GetInbound failure instead of silently swallowing it.
  • 33e6c2ec0c chore(deps): raise the swagger-ui-react js-yaml override to 4.3.2
  • View comparison for these 3 commits »

5 days ago

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.

6 days 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 »

6 days ago