Commit Graph

3348 Commits

Author SHA1 Message Date
Kuzz007 037ca7330f fix(install): make AmneziaWG the default, opt-out instead of opt-in
AmneziaWG is this fork's whole reason for existing, so requiring an
explicit flag/prompt answer to actually get it on every fresh install,
migration, or update was the wrong default -- confirmed today by a real
migration where it silently stayed uninstalled (interactive default was
"no", and non-interactive/unattended installs skipped it outright with
nothing to answer the prompt).

should_install_amneziawg now defaults to yes in both the interactive
prompt and the non-interactive/unattended path; XUI_INSTALL_AMNEZIAWG
still works as an explicit override in either direction (true/false),
so anyone who genuinely doesn't want the DKMS module + host-wide IP
forwarding can still opt out.

Also fixed the printed opt-out hint to show the actual working syntax
for setting the variable before a later re-run -- `VAR=val curl ... |
bash` only exports VAR into curl's environment, not bash's, since
they're separate processes joined by a pipe. Hit this exact gotcha
live: XUI_INSTALL_AMNEZIAWG=true prefixed onto the curl command was
silently ignored.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 14:33:32 +03:00
Kuzz007 d7a922dcf3 fix(frontend): point remaining GitHub repo links at this fork
The sidebar's version badge and the subscription-templates docs link
still hardcoded MHSanaei/3x-ui. install.sh and every README already
point at Kuzz007/3x-ui; these were the last two functional (non-credit)
repo links left pointing upstream. DONATE_URL/DOCS_URL intentionally
left alone -- those point at the original author's own donation page
and docs site, which is deliberate per this fork's existing convention
of crediting/linking upstream for general (non-fork-specific) info.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 14:25:57 +03:00
Kuzz007 ee47aa2608 fix(install): preserve custom bin/ files (e.g. hand-added geoip) across updates
Every reinstall/update wipes /usr/local/x-ui/ wholesale and re-extracts
the release tarball, which only ships known assets (xray/mtg binaries,
the bundled geoip*/geosite*.dat sets). A user-reported real incident:
a hand-placed custom geoip file referenced from a routing rule via
"ext:<file>:<code>" got silently deleted on update, and Xray refused
to start at all afterward ("failed to open <file>: no such file or
directory"), taking down every inbound until the file was manually
restored from the user's own backup.

install_x-ui now backs up the old bin/ before the wipe and restores,
after extraction, only the files the fresh release doesn't provide --
bundled assets still get the newer per-release copy, nothing custom
silently disappears. Verified in isolation: standard files (geoip.dat,
the xray binary) end up as the fresh release's copy; a custom file
absent from the release survives untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 13:46:19 +03:00
Kuzz007 196ec792d4 ci: remove the Docs Deploy (GitHub Pages) workflow
GitHub Pages was never enabled in this fork's repo settings, and the
workflow's own comment ties it to docs.sanaei.dev, upstream's custom
domain, not something this fork has. Failed both times it ever ran
(including once before today, unrelated to any of this session's
other changes) with "Ensure GitHub Pages has been enabled". Docs CI
(docs-ci.yml, lint/build-check only, no deploy) is untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 01:23:33 +03:00
Kuzz007 ea00e69d4d chore: remove Docker support entirely from this fork
Never used by this fork's own distribution (install.sh/x-ui.sh is the
only supported install path), AmneziaWG structurally can't run in the
Alpine-based image anyway, and Docker Hub publishing was failing on
every release for lack of configured credentials. Removed Dockerfile,
docker-compose.yml, DockerEntrypoint.sh, DockerInit.sh, .dockerignore,
and the docker.yml CI workflow; dropped the now-dead "Docker" README
subsection (all 7 languages), the "Docker image"/"Docker Compose"
options from the issue/PR templates, and corrected claude-bot.yml's
now-stale references to the deleted files, the never-actually-ours
ghcr.io/mhsanaei/3x-ui image, and (caught in passing) an already-stale
claim that Windows is a supported platform.

Left untouched: generic container-runtime adaptations that apply
regardless of image source (x-ui.sh's running-in-docker detection,
the virtual-interface-name filters, the DNS-over-container-network
note) and deploy/test/smoke-noninteractive.sh, which uses Docker only
as its own test sandbox, not as something this repo ships.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 01:13:04 +03:00
Kuzz007 ea43571037 chore(release): bump version to 3.5.0-awg.3
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 00:55:33 +03:00
Kuzz007 f1f68e6653 ci: trigger CI on workflow-file changes too
The push/pull_request paths filter didn't include .github/workflows/**,
so the previous commit (fixing the Audit step) never actually ran
through CI -- editing the workflow itself silently skipped the trigger.
A broken workflow-syntax change could merge untested the same way.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 00:35:50 +03:00
Kuzz007 422e193b21 fix(ci): actually make the Audit step green, not just documented as accepted
npm audit --audit-level=high doesn't read code comments -- it was still
exiting 1 on the one remaining, already-investigated-as-unfixable advisory
(eslint-plugin-jsx-a11y's own pinned minimatch, GHSA-mh99-v99m-4gvg), so
every run on main has actually been red despite the prior commit's comment
calling it "known-accepted". That advisory lives entirely in
devDependencies (lint tooling, never shipped), so --omit=dev is the
correct signal for "doesn't apply to what we ship" -- verified locally,
0 vulnerabilities. Runtime dependencies are still audited at high severity.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 00:30:17 +03:00
Kuzz007 70d80fbe8d fix(amneziawg): use the real vpn:// share-link scheme
The AmneziaWG share link (both the panel's per-client copy-link/QR and
the subscription endpoint) used an invented amneziawg://user@host:port
URI the real AmneziaVPN app can't parse -- it only recognizes its own
vpn:// scheme. Reverse-engineered the real app's import path (reading
amnezia-vpn/amnezia-client's own source) and confirmed it just needs
base64url(no padding) of a plain AmneziaWG .conf text -- no JSON schema
or qCompress framing to replicate, since qUncompress falls back to the
raw bytes for plain text and the parser reads a flat "Key = Value" bag
regardless of section. Both link generators now wrap the same .conf
text their own "download config" feature already produces correctly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 00:16:00 +03:00
Kuzz007 3760e01806 fix(clients): remove the redundant download button on the SUB link row
Downloading the raw subscription URL as a .txt file duplicates the
adjacent copy button for the vast majority of actual usage, and the
row already has a real, useful download further down (the per-client
config, e.g. AmneziaWG's own .conf). Left the JSON/Clash rows' download
buttons alone -- only the plain SUB row's was removed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 23:52:12 +03:00
Kuzz007 032dbabc39 fix(deps): migrate off abandoned react-router-dom, fix eslint's own brace-expansion
Two real, forward-compatible fixes for npm audit's high-severity
advisories (not the downgrades npm audit fix --force offers):

- react-router (GHSA-qwww-vcr4-c8h2, RSC CSRF bypass): react-router-dom
  is frozen at 7.18.1, pinning the vulnerable react-router@7.18.1 -- no
  newer react-router-dom release exists pointing at the fixed line.
  react-router itself has shipped the real fix at 8.3.0. Migrated the 9
  files importing from react-router-dom (all using plain
  createBrowserRouter/RouterProvider/useLocation/useNavigate/Outlet, no
  RSC anywhere) to import from react-router directly instead.

- brace-expansion/minimatch (GHSA-mh99-v99m-4gvg): fixed for eslint's
  own dependency chain (minimatch@10.2.5, used by eslint itself,
  storybook, typescript-eslint, swagger-client) via a scoped
  "minimatch@^10" override forcing brace-expansion to the now-published
  5.0.8 patch -- within the range minimatch@10.2.5 already declares
  wanting (^5.0.5), so this isn't a version-pin workaround, just
  unblocking a patch release npm's resolver hadn't picked up.

One advisory remains genuinely unfixable from our side:
eslint-plugin-jsx-a11y pins minimatch@^3.1.2 (old major, never
patched); forcing it to the 10.x line via override breaks npm's own
dependency-tree validation (a real incompatibility, not just an npm
quirk), so this needs an eslint-plugin-jsx-a11y release bumping its own
minimatch. Lint-time only, no untrusted input reaches it -- ci.yml's
Audit step comment updated to reflect the new, smaller remaining scope.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 23:31:33 +03:00
Kuzz007 f70002d9a6 Revert "fix(routing): insert new rules after api instead of appending at the end"
This reverts commit 638458bd. Reordering new rules to the top by
creation-time was a blunt heuristic: it fixed a rule silently shadowed
by an unrestricted catch-all above it, but breaks the opposite,
equally valid setup -- e.g. a pre-existing "youtube -> interfaceA" rule
(no inboundTag restriction, meant to apply everywhere) followed by a
new "amneziawg -> interfaceB" rule. Under the reverted logic the newer,
broader-matching amneziawg rule would jump above the youtube rule and
shadow it for amneziawg-sourced traffic -- the exact same class of bug
this was meant to fix, just aimed the other way.

Xray's rule order encodes real admin intent (which rule should win for
overlapping traffic) that creation time can't safely stand in for.
Back to plain append-at-the-end; admins reorder manually with the
existing moveUp/moveDown controls, same as upstream's own behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 19:14:56 +03:00
Kuzz007 638458bdea fix(routing): insert new rules after api instead of appending at the end
A new routing rule created through the Routing tab was always appended
to the end of the list. Xray matches rules top-to-bottom, first hit
wins, so any pre-existing broader rule (e.g. a block rule with no
inboundTag restriction) silently shadows a newly created, more specific
rule forever -- it looks saved and enabled but never actually fires.

Root-caused this from a report that an AmneziaWG inbound routed through
a real outbound lost all connectivity while routing it "direct" worked
fine: the AmneziaWG bridge's own TPROXY/interface code turned out to be
entirely fine (tunnel stayed up, client stayed online) -- the new rule
was just sitting below existing RU-IP/bittorrent/domain block rules
with no inbound restriction, so those matched first. Confirmed directly:
manually dragging the rule to sit right after the pinned api rule fixed
it. This isn't AmneziaWG-specific -- any protocol's newly added rule can
be shadowed the same way.

New rules now insert right after the pinned api rule (or at the very
top if it's absent) instead of at the end, so a newly created rule takes
effect by default; drag it lower afterward if a lower priority is
actually wanted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 19:06:27 +03:00
Kuzz007 1aa81428b8 fix(traffic): show live Speed for AmneziaWG and MTProto inbounds/clients
The Speed column showed "--" for AmneziaWG (and MTProto, which has the
identical gap) even while cumulative traffic totals were correct.
XrayTrafficJob drives live speed by querying xray-core's own stats API
and broadcasting the delta over websocket -- but AmneziaWG/MTProto never
run inside xray-core's own runtime inbounds, so they're invisible to
that API. Their own jobs already compute the same per-poll delta shape
(that's what keeps cumulative totals correct) but never broadcast it.

Reusing the existing "traffics"/"clientTraffics" broadcast would have
two real bugs: the frontend's existing scope/replace logic would let
each side clobber the other's speed on its next unrelated tick, and the
websocket hub's per-message-type throttle is keyed only by message type,
not caller -- since both sidecar jobs run on identical "@every 10s"
grids registered milliseconds apart, one would silently lose almost
every broadcast if both protocols were ever configured together.

Fixed with a small unthrottled broadcast path (both sidecar jobs are
already self-rate-limited by their own cron cadence) and protocol-
namespaced wire keys, tracked in their own frontend state and merged
into the existing inboundSpeed/clientSpeed only at read time -- so every
existing consumer needs zero changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 18:27:23 +03:00
Kuzz007 0436ac641f docs: add an "Other changes in this fork" section to all READMEs
The fork's README only ever documented AmneziaWG as "what's different
here" -- the new geosite/geoip routing-rule autocomplete had nowhere
appropriate to go. Added a dedicated, appendable section for smaller
fork-specific improvements beyond AmneziaWG, and described the new
autocomplete feature there in all 7 languages. Future fork-specific
features should get a short entry here too, in every README.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 17:33:48 +03:00
Kuzz007 2736f9beb3 feat(xray): autocomplete geosite/geoip categories in the routing rule editor
Writing a routing rule today means remembering exact geosite:/geoip:/
ext:file:code syntax by hand, with no way to discover what categories
actually exist in the .dat files sitting in the bin folder -- including
custom ones like geosite_roscom.dat added via the Geodata auto-update
feature. The Domain/IP fields in the rule editor now suggest categories
as you type (e.g. "you" -> "geosite:youtube"), built live from whatever
.dat files are actually on disk, while still accepting any free-typed
value exactly as before.

Backend: GET /panel/api/xray/getGeodataCategories scans the bin folder,
parses matched geosite*/geoip*.dat files via xray-core's own exported
protobuf types, and formats each category as the exact rule syntax
xray-core's parser accepts -- geosite:/geoip: for the default files,
ext:<file>:<code> for anything else (there's no shorthand for custom
files). Cached in memory keyed by each file's (name, size, modTime) so
a request-time scan is cheap until a file actually changes.

Frontend: the Domain/IP inputs become Select "tags" fields fed by a new
useGeodataCategories() query hook, with an explicit substring filter so
"you" matches "geosite:youtube" (not a prefix). The array<->CSV-string
adapter lives entirely at the FormField transform boundary, so the
underlying form schema and saved rule shape are unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 17:13:07 +03:00
Kuzz007 c282c17b5a chore(release): bump version to 3.5.0-awg.2
The published v3.5.0-awg.1 release was built before 814369da, so it still
ships the version-comparison bug that commit fixed. Cutting a new tag from
the corrected tree so the panel's own self-check is actually accurate for
anyone who updates.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 12:44:47 +03:00
Kuzz007 814369da38 fix(panel): recognize this fork's -awg.N tags in version comparison
internal/config/version was never bumped past 3.5.0 when v3.5.0-awg.1 was
tagged, so a freshly-updated panel kept reporting its own version as the
plain upstream base. On top of that, parseVersionParts (Go and its
TypeScript mirror) required exactly 3 dot-separated numeric parts, so it
rejected the -awg.N suffix entirely and fell back to a raw string
inequality that reports "update available" any time the strings merely
differ -- which they always do here, even when already on the latest tag.

Bump the embedded version to 3.5.0-awg.1 and extend both parsers to treat
"-awg.N" as an optional 4th, lower-priority component (defaulting to 0 for
a plain tag), so e.g. 3.5.0-awg.2 > 3.5.0-awg.1 > 3.5.0 and a matching tag
compares equal instead of always looking outdated.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 12:42:20 +03:00
Kuzz007 0372515aa2 docs(ci): document the 2 known-accepted npm audit advisories
Neither is exploitable in this app (react-router CVE is RSC-only, we use
createBrowserRouter; jsx-a11y's brace-expansion chain only runs against our
own lint globs), and npm's suggested fixes are both downgrades with no real
forward patch published yet -- left as-is rather than trading a working
version for one that doesn't fix anything reachable here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 12:42:02 +03:00
Kuzz007 972514f279 docs: add the stable install command to Quick Start in all READMEs
v3.5.0-awg.1 is now tagged and promoted to Latest, so the plain
no-argument install.sh command resolves to something real for the first
time. Restructured Quick Start in all 7 READMEs to lead with it, followed
by the explicit-version and dev-channel variants (matching upstream's own
three-command Quick Start layout), replacing the now-outdated "this fork
only ever publishes dev-latest" note.
2026-07-26 12:03:43 +03:00
Kuzz007 1ed9cd8ea1 fix(inbound): enforce node-eligibility server-side, not just in the UI
Investigated multi-node interaction with AmneziaWG: the master's own
reconcile (DesiredAmneziaWGInstances) and Xray config generation
(injectAmneziawgEgress, the GenXrayInboundConfig protocol skip) all
correctly filter on NodeID IS NULL, so a node-assigned AmneziaWG (or
MTProto) inbound would never be managed by the master. But nothing
stopped one from being created that way: NODE_ELIGIBLE_PROTOCOLS
(frontend/src/pages/inbounds/form/InboundFormModal.tsx) only hides the
node picker client-side -- a direct API call could set nodeId on an
AmneziaWG inbound, which every node then reconciles as an ordinary
local inbound (nodes run the identical binary, full cron suite
included), leaving it running unmanaged and untracked by the master's
own AmneziaWG bookkeeping.

Added isNodeEligibleProtocol (inbound_protocol.go), mirroring the
frontend's allowlist, and enforced it in both AddInbound (the actually
exploitable path -- nodeId comes straight from the request) and
UpdateInbound (defense in depth; NodeID is already restored from the
stored row there before this check, so it mainly guards against a
protocol change on an existing node-hosted inbound).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 11:42:24 +03:00
Kuzz007 ba1a33f307 fix(sub): include amneziawg inbounds in subscription links
getInboundsBySubId's SQL protocol allowlist never had 'amneziawg' added,
so every AmneziaWG client was silently excluded from all three
subscription formats (plain/individual links, JSON, Clash) and from the
Telegram bot's QR/individual-link buttons, which fetch through the same
path. genAmneziaWGLink itself was already fully implemented and already
wired into GetLink's dispatch switch -- it just never got a chance to
run. Same bug shape as the earlier TRACKED_PROTOCOLS frontend gap: a
hardcoded protocol list one entry short.

Found while investigating whether the Telegram bot needed AmneziaWG-
specific client-management code -- it doesn't (the bot itself is fully
protocol-agnostic), but this is the actual root cause of "can't share
an AmneziaWG client's config via the bot."

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 11:42:11 +03:00
Kuzz007 79cfcb9966 docs(docker): note that AmneziaWG doesn't work in this image
Investigated: the image is Alpine-based, and AmneziaWG's own packaging
(DKMS module + amneziawg-tools) doesn't target Alpine/musl at all --
unlike the Debian/Ubuntu/Fedora/Arch paths install.sh already handles,
there's no package to apk add even with full host network/capabilities.
The panel already degrades gracefully (IsAwgInstalled() logs one warning
instead of retrying forever), so no code change is needed -- just made
the reason explicit at the point where a user would reach for cap_add/
network_mode to try to work around it.
2026-07-26 11:27:55 +03:00
Kuzz007 df6d2f7652 fix(amneziawg): resolve 4 Low findings from the automated PR review
- manager.go: serverAddress assumed subnetIp always ends in ".0"; a
  base like "10.8.1.5" was used verbatim as the server's own address,
  eventually colliding with peer allocation (which starts at .2
  upward). Now derives the first host of the actual subnetIp/subnetCidr
  network via netip, matching serverAddressV6's own approach. A /32
  base (no host bits at all) is still used as-is. (Finding 12, partial
  -- the /16 pool-widening half of this finding only exists on the
  upstream-pr/amneziawg branch's merged client_wireguard.go, not here;
  handled separately on that branch.)

- manager.go: ensureLocked carried the previous per-peer traffic
  counters (`last`) forward even through a full restart, but
  awg-quick down+up resets the kernel's own counters to zero -- the
  next CollectTraffic computed a large negative delta (clamped to 0),
  silently discarding real traffic. Extracted the decision into
  nextTrafficBaseline: only a reload (syncconf) preserves the
  baseline. (Finding 13)

- portfwd.go: exported ForwardedPortsInclude; inbound_amneziawg.go's
  new checkForwardedPortsConflict uses it to reject, at save time, a
  client's forwardedPorts that would DNAT the panel's own port or
  another enabled inbound's port to the tunnel client --
  portForwardLines has no destination restriction, so this collision
  was previously silent. Wired into both the single-client update path
  and the add-client path (client_inbound_apply.go), plus
  normalizeAmneziaWGSettings for the whole-inbound save path. (Finding 14)

- inbound.go: InboundOption.AwgServer sent the whole ServerSettings
  struct including PrivateKey to GetInboundOptions callers -- a
  shared, admin-wide dropdown-filling endpoint the frontend's own
  AwgServerOptionSchema never reads that field from. Redacted it
  before assigning. (Finding 11)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 11:16:39 +03:00
Kuzz007 71dc453970 feat(amneziawg): make the Xray TPROXY bridge a per-inbound opt-in
Addresses Finding 10 from the automated PR review: an always-on TPROXY
bridge makes every AmneziaWG tunnel hard-depend on Xray being up (all
traffic, including DNS, drops whenever Xray restarts), and forces a full
awg-quick down+up bounce on any client add/remove/re-IP, permanently
losing the syncconf fast path.

Adds ServerSettings.RouteThroughXray (off by default):

- defaultPostUpDown only emits the TPROXY/policy-route rules when it's
  on; a plain AmneziaWG tunnel now has zero Xray dependency out of the
  box.
- structuralFingerprint covers it (toggling it changes whether PostUp/
  PostDown contain any TPROXY rules at all -- structural, not a
  per-peer host-rule). hostRulesFingerprint's IPv4 tracking is now
  itself conditional on RouteThroughXray (and IPv6 tracking on
  IPv6Enabled), so an instance that never uses either keeps the
  syncconf fast path for a plain peer re-IP.
- injectAmneziawgEgress only creates a bridge for inbounds that opted
  in; checkAmneziawgEgressConflict (the Finding-7 fix) now parses each
  candidate through InstanceFromInbound so a non-routed inbound's port
  is correctly never treated as reserved.
- New inbound-level Switch in the AmneziaWG form; the actual outbound
  decision is still made entirely through the panel's stock Routing
  page, same as before -- only whether the bridge exists at all is now
  a choice.

Translation keys added to all 13 locales in the same commit this time,
not backfilled later (see Finding 9's lesson).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 00:39:32 +03:00
Kuzz007 c41f97cf86 fix(amneziawg): resolve 7 Medium findings from the automated PR review
Each is independently reproducible; fixed together since one review pass
found all of them.

- manager.go: the shared "ip rule add fwmark" policy route had no
  existence check, so it duplicated in "ip rule show" on every interface
  bounce (which hostRulesFingerprint forces on any client add/remove/
  re-IP). Now checked via "ip rule list | grep -q ..." first. (Finding 2)

- params.go: ExternalInterface, IPv6ExternalInterface, and subnetIp/
  subnetCidr are interpolated unescaped into a shell-executed PostUp/
  PostDown line, but only obfuscation and the IPv6 subnet were validated
  before save. Added ValidateInterfaceName (a strict charset+length
  pattern) and ValidateSubnetIPv4 (netip.ParsePrefix), wired into
  normalizeAmneziaWGSettings. (Finding 3)

- amneziawg_job.go: IsAwgInstalled() existed but nothing ever called it,
  so a host without awg/awg-quick (the Docker image, RHEL, Arch, a failed
  install.sh PPA step) logged a reconcile failure every 10s forever. Now
  checked once an inbound actually needs it, warning once instead of
  spamming. (Finding 4)

- client_inbound_apply.go: the WireGuard/AmneziaWG credential
  carry-forward (added so a metadata-only client edit doesn't rotate
  keys) never covered ForwardedPorts, so a partial edit -- an API call or
  Telegram-bot toggle that omits the field -- silently wiped a client's
  port-forwarding spec. Carried forward and written back the same way the
  key fields already are. (Finding 5)

- manager.go: hostRulesFingerprint keyed each peer on its IPv4 address
  only, and structuralFingerprint omitted IPv6Enabled/IPv6ExternalInterface
  entirely, so an IPv6-only change could pick the syncconf reload path
  (which never re-runs PostUp, leaving a stale NDP-proxy entry) or be a
  complete no-op. Both fingerprints now cover the IPv6 fields. (Finding 6)

- port_conflict.go: the AmneziaWG egress bridge (injectAmneziawgEgress)
  binds 127.0.0.1:63100+id with no collision check anywhere, since it
  isn't a database row the ordinary port-conflict query can see -- same
  blind spot the reserved Xray API port already has its own check for.
  Added the equivalent check for the AmneziaWG bridge port. (Finding 7)

- install.sh: install_amneziawg ran unconditionally for every install/
  update, building a DKMS kernel module and enabling host-wide IPv4/IPv6
  forwarding whether or not the feature is ever used. Gated behind a new
  should_install_amneziawg (XUI_INSTALL_AMNEZIAWG=true/false, or an
  interactive y/N prompt defaulting to no). Also replaced the deprecated
  apt-key adv with a dedicated keyring + signed-by= on the Debian branch,
  and guarded its sources.list appends against duplication on a retried
  install. (Finding 8)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 00:16:20 +03:00
Kuzz007 f5543047b7 fix(amneziawg): recover orphaned interfaces after an ungraceful exit
Two gaps left an AmneziaWG interface stuck outside the manager's
control after a crash (kill -9/OOM/panic skips StopAll):

- ensureRestart's teardown was gated on the in-memory `exists` map,
  which is always empty on a fresh process, so a survived interface
  never got interfaceDown before interfaceUp tried `ip link add`
  against a name the kernel already had — failing forever and never
  populating m.ifaces, so traffic accounting silently stopped and the
  inbound could never be removed. Gate on isInterfaceUp instead, which
  checks real kernel state rather than this process's own bookkeeping.

- An inbound deleted from the database entirely while the panel was
  down has no entry in `desired` ever again, so it never reaches the
  per-id cleanup loop in Reconcile (which only walks m.ifaces). Add a
  one-time sweepOrphansLocked scan of configDir, mirroring
  mtproto.Manager.sweepOrphansLocked, that tears down and removes any
  leftover interface/config not in the current desired set.

Found by the automated review on MHSanaei/3x-ui#6105 (Finding 1).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 23:55:11 +03:00
Kuzz007 65513b4854 docs: rewrite all README translations for the AmneziaWG fork
Fa/Ar/Zh/Es/Tr READMEs still described upstream verbatim (generic 3X-UI,
no AmneziaWG section, upstream install command, Windows listed as
supported, Contributing/donation/stargazers sections). Rewrite each to
match the already fork-ified README.md/README.ru_RU.md: fork framing,
the "What's different: AmneziaWG" section, dev-latest install command,
Windows removed from supported platforms, leaner developer-notes/credit
sections. Also restore the 7-language switcher line on all files now
that they're consistent again.
2026-07-25 23:45:07 +03:00
Kuzz007 510b43f32d i18n(amneziawg): backfill IPv6/obfuscation/port-forwarding keys in 11 locales
Only en-US/ru-RU ever got these 9 keys as each AmneziaWG feature landed
(the regenerate-obfuscation button, then Phase 2a's IPv6 fields, then
Phase 2b's per-client ForwardedPorts) — the other 11 locale files were
never backfilled, so i18next has been silently falling back to English
for all of them since Phase 1. Cosmetic-only (never broke anything),
but now closed for every shipped locale.
2026-07-25 23:27:57 +03:00
Sanaei 5accd8a611 fix(ci): stop the conflict job trusting the branch it is merging
A second audit of the hardened workflow found the "no shell at all"
claim in resolve-conflicts was still false, by two routes that live
outside this file.

The job runs the model in the workspace right after `gh pr checkout`,
so for a fork pull request the working directory is attacker-controlled.
claude-code-action writes `enableAllProjectMcpServers = true` into
~/.claude/settings.json before starting Claude Code
(base-action/src/setup-claude-code-settings.ts), and the CLI honours a
project `.mcp.json` unless `strictMcpConfig` is set, which the action
never sets. A contributor branch carrying an `.mcp.json` therefore got
its command spawned at session start, with --allowedTools gating tool
calls but not server startup. The same tree also supplied CLAUDE.md and
.claude/ as project instructions. The job now passes
`--strict-mcp-config` and `--setting-sources user`, so nothing in the
merged tree configures the session.

The second route was `Edit` with no path scope, the only unscoped file
grant left. Editing `.git/config` to set `core.fsmonitor` or a
`credential.helper` gets a command run by the next step's git calls,
which hold CLAUDE_BOT_PAT, and the stray-file guard could never see it
because `git diff --name-only` lists tracked paths only. The merge step
now emits one `Edit(//<workspace>/<file>)` rule per conflicted path and
the model gets exactly those plus /tmp, with `.git/**` denied outright
and Bash, WebFetch, WebSearch and Task denied by name. Hooks are
disabled for the run (`core.hooksPath=/dev/null`, `commit --no-verify`).

Conflict handling gets three real gaps closed: modify/delete, rename and
both-added conflicts (git status DD/AU/UD/DU/AA/UA) leave no markers, so
they used to sail through the marker check and get committed unresolved
- they are now detected up front and handed back untouched; the marker
scan covers `=======` and `|||||||`, not just the outer pair; and after
staging, `git diff --diff-filter=U` must come back empty or nothing is
committed. A `=======` markdown underline of exactly seven characters in
a conflicted file will now hand the merge back rather than commit it,
which is the safe direction.

Smaller things the audit was right about:
- the mutating gh rules are prefix rules, so `Bash(gh issue close:*)`
  reached every issue in the repository. They now carry the triggering
  number: `Bash(gh issue close ${{ github.event.issue.number }}:*)`.
- `Write(//tmp/**)` is granted alongside `Edit(//tmp/**)`: the docs say a
  Write(path) rule is never matched by the file checks, so the Edit rule
  is what authorises it, but the tool has to be listed to exist at all.
  Without this the model could not create /tmp/comment.md.
- the mention prompt lost its thread context when it moved to agent mode
  and referred to "<number>" literally; it now gets repo, number, title
  and whether the thread is a pull request.
- `git log`/`git show` are gone from mention: `--output=<file>` makes
  them a file-write primitive.
- `@claude resolve pr conflicts` on a plain issue matched no job at all.
- the commit step gated on `skip != 'true'`, so it also ran when the
  merge step died before writing any output; it now needs `skip ==
  'false'`.
- bot-authored pull requests (dependabot opens three ecosystems' worth)
  no longer start a review run that the action refuses to serve.
- resolve-conflicts drops to `contents: read`, since the push is the
  PAT's job, and fails with a comment when that PAT is missing.
2026-07-25 22:27:09 +02:00
Sanaei f46b1726cf fix(ci): close the write paths an audit found still open in the bot
Making the jobs read-only in the previous commit was not enough: two of
the mechanisms that grant write access were invisible in the workflow
file itself.

Every job now passes a `prompt:` input. Without one, claude-code-action
picks tag mode for a mention, and src/modes/tag/index.ts then appends
`--permission-mode acceptEdits`, its own allowedTools including
`Bash(git commit:*)` and a push wrapper, and calls setupBranch. So the
mention job could edit files and commit them no matter what its own
allowedTools said, and its system prompt claiming otherwise was simply
wrong. A `prompt:` selects agent mode, which adds nothing. It also
removes tag mode's hidden requirement that the comment contain the
trigger phrase, which would have made resolve-conflicts a no-op for a
comment that said only "resolve pr conflicts".

resolve-conflicts no longer hands git to the model. `Bash(git:*)` is a
prefix rule, so it permitted `git push origin HEAD:main`, `--force`,
`git remote set-url`, and shell execution through `git config alias.x
'!sh -c ...'` - the action ships scripts/git-push.sh precisely because
`git push:*` allows `--receive-pack='sh -c ...'`. The job now splits in
three: a step checks out the PR branch, merges the base and collects the
conflicted paths; the model gets Read/Glob/Grep/Edit and no shell at all;
a final step verifies and pushes. That step refuses to commit if a
conflict marker survives, if the model wrote /tmp/ABORT, or if anything
outside the conflicted set was touched, and it stages those paths
individually instead of `git add -A`. The PAT is now written to the push
URL only in that last step, after the model's session has ended, instead
of sitting in .git/config while untrusted branch content is read.

The bare `Write` grant in the three answering jobs becomes
`Edit(//tmp/**)`, since only prose kept it out of the checkout and out of
$GITHUB_ACTION_PATH, whose scripts run after the model step. Each prompt
now says to fall back to an inline --body if the write is refused, so a
denied write cannot silently cost a reply. mention gains the transcript
upload and the no-reply guard the other jobs already have, keyed to the
triggering comment's timestamp.

Restores the header note about the 21000-character expression cap, with
the current block sizes.
2026-07-25 22:03:10 +02:00
Sanaei acbb879f80 refactor(ci): make the bot read-only except for PR conflict resolution
The bot is meant to investigate and explain, not to write code. It could
do considerably more than that: handle-pr-fix applied fixes and pushed
them to any trusted author's PR, an @claude mention on a pull request
could edit files, and an @claude mention on an issue opened a pull
request against main. All of it is gone.

Now every job that answers automatically runs with a contents: read
token, so pushing is impossible rather than merely forbidden:

- handle-pr-fix is deleted. handle-pr-review takes every pull request
  instead of only the ones from outside contributors, and it comments.
- mention drops contents: write, the push-URL routing step, and the
  Edit tool. Its Bash allowlist is now an explicit read-only set - the
  gh subcommands it needs plus git log/show/diff/blame - so gh api,
  gh pr merge and gh pr create are no longer reachable. Asked for a
  fix, it now writes the change out in full instead of applying it.

One narrow exception replaces all of that: resolve-conflicts. It runs
only when the repository owner comments "resolve pr conflicts" on a
pull request, and it may merge the base branch into that PR's head
branch and resolve the conflicts, nothing else. It keeps both sides of
every conflict, takes the base version of generated artifacts it cannot
regenerate here, and aborts the merge rather than guess when a hunk
needs a human. It never force-pushes, merges, or closes.

Also removes the pull-request-opening step whose guard never worked:
gh api prints the 404 body on stdout, so `ahead=$(gh api ... || echo 0)`
became `{"message":"Not Found",...}0`, never equal to "0", and every
reply-only mention run ended red on `gh pr create`. Uploads the
handle-pr-review transcript the way handle-issue already does, so a run
that dies inside the sandbox leaves evidence.
2026-07-25 21:39:28 +02:00
Kuzz007 db8253421a refactor(amneziawg): route via Xray through the stock Routing page, not custom toggles
Simplifies RouteViaXray after realizing the panel already has everything
needed: the Routing page already lets an admin pick a source inbound tag
and a target outbound (plus, if they want it, a specific source IP) for
any protocol. Bolting a parallel routeThroughXray/routeOutboundTag pair
onto both the client and inbound forms duplicated that mechanism instead
of using it.

Removed entirely: Client/ClientRecord/ServerSettings/Peer's
RouteThroughXray + RouteOutboundTag fields, the effective-routing OR/
fallback logic in InstanceFromInbound, and the Switch+Select UI on both
forms. Nothing configures "route via Xray" as a setting anymore.

In its place, every enabled AmneziaWG inbound now gets its own Xray
TPROXY bridge unconditionally, by default, no toggle:

- internal/amneziawg: every peer's traffic is always TPROXY'd into that
  instance's own bridge (defaultPostUpDown, port derived from the
  inbound's id via EgressPortForInbound so the kernel side and the
  Xray-config side never need to negotiate a runtime value). Since the
  TPROXY rule is now tied to a peer's mere presence rather than an
  opt-in flag, hostRulesFingerprint now covers every peer unconditionally
  (add/remove/re-IP forces a restart, the same way ForwardedPorts always
  did) instead of skipping peers with nothing to opt into.
- internal/web/service/xray.go's injectAmneziawgEgress creates one
  dokodemo-door bridge per qualifying inbound, tagged with that inbound's
  own real tag — the same trick injectMtprotoEgress already uses (reusing
  a real inbound's tag), which is why it's already selectable in the
  panel's Routing page: InboundService.GetInboundTags() is a plain,
  protocol-blind SELECT over every inbound row's tag, no dedicated UI
  plumbing needed. The function never generates a routing rule itself
  anymore — where (if anywhere) that traffic goes is entirely up to
  whatever rules the admin adds through the existing Routing UI.

Frontend: no new UI at all. Tests rewritten to match — one bridge per
inbound with its own tag/port, no rule generation, no opt-in gating.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 22:37:08 +03:00
Sanaei 1358f65bec fix(ci): unbreak the issue-triage bot, which answered nothing
Since 2026-07-20 every `issues` run reported success while posting no
comment at all - #6094 through #6103 carry zero replies. The cause is
the sandbox, not the prompt or the model.

`handle-issue` and `handle-pr-review` pass allowed_non_write_users,
which is what lets the bot run for reporters who have no write access.
claude-code-action reacts to that input by turning subprocess isolation
on and installing bubblewrap, and that sandbox cannot start on the
runner: every Bash call dies during setup, before the command itself
runs, with

    bwrap: Can't create file at /home/.mcp.json: Permission denied

`gh` is reachable only through Bash, so the triage investigated the
issue, wrote its reply to /tmp/comment.md, and could never post it.
The action itself did not crash, so the job stayed green.

Opt both jobs out with CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=0. The scrub is
a best-effort wipe of secrets from subprocess environments, not an
access control; what actually bounds these jobs is unchanged - a
contents: read token that cannot push, and a Bash allowlist holding
only specific `gh issue`, `gh label`, `gh search` and `gh release`
subcommands. Code changes stay confined to handle-pr-fix and mention,
which only trusted actors and the owner can trigger.

Add a step to each job that fails the run when no bot comment landed on
the issue or pull request, so the next silent breakage shows up red
instead of green, and lower retention-days to the repository maximum of
7 so the artifact upload stops warning.
2026-07-25 20:14:53 +02:00
Kuzz007 909feefd1d fix(amneziawg): make RouteViaXray an inbound-level option too
RouteThroughXray/RouteOutboundTag were client-only, but the more common
case is "route this whole AmneziaWG server's traffic through Xray", not
configuring every peer individually. Add the same pair to ServerSettings
(inbound-level) while keeping the per-client fields as an override —
matching how ExternalInterface/IPv6Enabled already work at the server
level next to per-client settings like ForwardedPorts.

Effective per-peer decision (computed once, in InstanceFromInbound, not
duplicated at each consumer):
  - routed = client.RouteThroughXray || server.RouteThroughXray
  - outbound tag = client's own if set, else the server's default

This means a peer can be routed by the inbound-wide default with no
config of its own, opt in on its own even when the default is off, or
keep the default's on/off but pick a different outbound than everyone
else.

internal/web/service/xray.go's injectAmneziawgEgress now calls
amneziawg.InstanceFromInbound instead of re-parsing InboundSettings and
reading model.Client fields directly — the same effective-routing
computation the kernel-side TPROXY rules use, so the two independent
reconcile loops (Xray-config generation and the AWG manager) can never
quietly disagree about which peers are actually routed.

Frontend: Switch + conditional outbound Select added to the AWG inbound
form (mirroring the client-form version and mtproto's own UI), plus the
inbound-defaults.ts default-object fix that's bitten this project's CI
before (Phase 2a) whenever ServerSettings gains a new required-shaped
field.

Test fixtures in xray_config_inject_test.go needed a real Server block
and PublicKey once injectAmneziawgEgress started requiring a usable
InstanceFromInbound result — both were implicit fixture gaps, not
behavior the old tests were actually asserting.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 19:01:31 +03:00
Kuzz007 d1b77b2aa4 feat(amneziawg): Phase 2c — RouteViaXray (TPROXY into Xray)
Per-client toggle (RouteThroughXray + RouteOutboundTag) that TPROXYs a
peer's traffic into Xray instead of NAT'ing it straight out the host's
network interface, so it can egress through any configured Xray
outbound (or balancer) — a VLESS/proxy chain, WARP, etc.

Discovered mid-design that internal/mtproto already solved the "let a
native sidecar's traffic egress through Xray" problem once, via
routeThroughXray/routeXrayPort/outboundTag + injectMtprotoEgress: a
loopback bridge inbound plus a routing rule. AmneziaWG can't reuse it
directly — mtg is a userspace process that dials *out* through a local
SOCKS proxy, while AmneziaWG is a kernel tunnel interface with no
process of its own to redirect. The Xray-side shape carries over
almost exactly, the kernel-side plumbing is new:

- internal/amneziawg/route_egress.go: EgressPort/EgressTag/EgressFwmark/
  EgressTable are one shared constant set, not one bridge per peer.
  Every routed peer, across every AmneziaWG instance, TPROXYs into the
  *same* loopback dokodemo-door bridge; the per-peer distinction happens
  downstream, in Xray's own router, matched against each peer's
  TPROXY-preserved source IP (Xray's field-rule `source` matcher — a
  capability the router already had). This avoids two independent
  reconcile loops (the AWG manager and the Xray-config generator) ever
  having to agree on a dynamically-picked port for each peer.
- manager.go's defaultPostUpDown emits a per-peer mangle-table TPROXY
  rule (matched by tunnel source IP) for each opted-in peer, plus the
  fwmark->table->local-everywhere policy route TPROXY needs to deliver
  those packets to the bridge. That policy route is system-wide, not
  interface-specific, so — like the existing IPv6-forwarding sysctl —
  it's added idempotently and never torn down in PostDown; a second
  AmneziaWG instance with its own routed peers must find it already in
  place, not race to remove what the first still needs.
- The existing portForwardFingerprint became hostRulesFingerprint,
  covering both ForwardedPorts and RouteThroughXray/RouteOutboundTag:
  both only ever take effect through PostUp/PostDown, which `awg
  syncconf` never re-runs, so either one changing must force the same
  full interface bounce.
- internal/web/service/xray.go's new injectAmneziawgEgress mirrors
  injectMtprotoEgress/injectPanelEgress's safety rules, adapted for one
  bridge serving many peers: an invalid or missing outbound target
  skips only that one peer's rule (not the whole bridge, since other
  peers may still need it), while the bridge itself is skipped
  entirely when nothing needs it or its tag is already taken by a real
  inbound.

Frontend: a Switch + conditional outbound Select on the client form
(showAmneziawg only), mirroring mtproto's own routeThroughXray UI and
reusing its useOutboundTags hook. install.sh now modprobes the
mainline TPROXY modules (xt_TPROXY, nf_tproxy_ipv4/ipv6) alongside the
existing AmneziaWG setup — ordinary upstream kernel modules, no
DKMS/PPA needed unlike the AmneziaWG module itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 18:34:37 +03:00
Kuzz007 69de904bf6 feat(amneziawg): Phase 2b — per-client port-forwarding
Admins can now set a per-client ForwardedPorts string (e.g. "80, 443,
8000-8100") that gets DNAT'd + FORWARD'd to that peer's tunnel address
via iptables rules in PostUp/PostDown, ported and simplified from
coinman-dev/3ax-ui's shared/portfwd.

Two decisions worth flagging for future readers:

- The iptables --comment tag on each rule is awg-fwd-<fnv32a(email)>,
  not the raw client email. Email is admin/API-supplied free text that
  ends up embedded in a shell-executed PostUp/PostDown line; a hash
  can never carry a shell metacharacter through where raw
  interpolation could.
- The reconcile manager gained a third fingerprint (portFwdFP, next to
  the existing structural/peers ones). `awg syncconf` only touches the
  WireGuard peer table — it never re-applies PostUp/PostDown iptables
  rules — so a port-forward-only change has to force a full
  awg-quick down+up bounce, same as a structural change, rather than
  the lighter sync a plain peer add/remove can use.

Also fixes a real pre-existing bug found while wiring up IPv6 client
allocation in the previous commit's spirit: allocateWireguardAddress
always suffixed "/32" regardless of address family, which produced
invalid host bits for IPv6 (needs "/128").

ForwardedPorts flows through model.Client -> model.ClientRecord
(gorm column wg_forwarded_ports, auto-migrated) -> ToRecord/ToClient/
MergeClientRecord, mirroring the awgServer field's earlier lesson
that new fields need checking against a second, hand-maintained
persistence-layer struct.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 17:54:05 +03:00
Kuzz007 9eaae5fd6a fix(amneziawg): fill in IPv6 fields missed by the Phase 2a commit
Two real gaps the CI caught (both new fields, both my miss):

- inbound-defaults.ts's createDefaultAmneziawgInboundSettings() built a
  server object literal predating ipv6Enabled/ipv6Subnet/
  ipv6ExternalInterface — AmneziawgServer's inferred type now requires
  them (zod .default() fields are non-optional post-parse), so this
  didn't typecheck at all.
- openapi.json's ipv6Enabled property was missing the description the
  real generator attaches (the Go doc comment covering all three IPv6
  fields is attached to the first one) — a one-line diff, but git
  diff --exit-code doesn't care how small.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 17:29:30 +03:00
Kuzz007 ef13e8567e feat(amneziawg): Phase 2a — IPv6 support + NDP proxy
Adds native dual-stack IPv6 to AmneziaWG inbounds, ported from
coinman-dev/3ax-ui's approach:

- ServerSettings gets ipv6Enabled/ipv6Subnet/ipv6ExternalInterface;
  Instance carries the server's own IPv6 address (first host of the
  subnet) alongside its IPv4 one.
- defaultAmneziaWGClients allocates an IPv6 host address per client
  (second AllowedIPs entry) when the server has IPv6 enabled, reusing
  allocateWireguardAddress — which needed a real fix along the way: it
  always suffixed "/32" regardless of address family, which is wrong
  for an IPv6 host address (needs /128). Now family-aware.
- generateServerConfig's PostUp/PostDown gains IPv6 forward-accept
  rules, proxy_ndp sysctl, and one `ip -6 neigh add/del proxy` entry per
  enabled peer with an IPv6 address — the lightweight per-client
  method, not the ndppd-daemon whole-subnet method (not worth the
  config-file-management complexity at this scale; ndppd itself is
  still installed by install.sh in case that changes later).
- ValidateIPv6Subnet rejects a malformed subnet before save.
- Frontend: ipv6Enabled/ipv6Subnet/ipv6ExternalInterface fields on the
  AmneziaWG inbound form, EN+RU translations, openapi.json/generated/*
  regenerated (the latter via `go run ./tools/openapigen`, pure Go).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 17:25:58 +03:00
Kuzz007 a76f0ab233 fix(ci): drop the now-impossible .zip glob from the dev-latest upload
Removing build-windows (previous commit) means dev-artifacts/*.zip
never matches anything — gh release upload treats an unmatched glob as
fatal, so every dev-latest publish since has failed outright (verified:
this run's publish-dev job errored "no matches found for
`dev-artifacts/*.zip`" on all 5 retries). Only the *.tar.gz glob remains.

Also manually deleted the stale x-ui-windows-amd64.zip asset from the
current dev-latest release — it would never be refreshed again
otherwise, and silently going stale forever is worse than not being
there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 17:08:25 +03:00
Sanaei f4e79e70ea chore: refresh dependencies, fix Linux tool tasks, modernize Go idioms
Frontend deps: @hookform/resolvers 5.4.0 -> 5.4.3 and react-hook-form
7.82.0 -> 7.83.0. The @typeschema/valibot override is what makes this
installable at all. Resolvers 5.4.3 re-declares 25 optional peers for its
validator matrix, and npm resolves them into the ideal tree even though none
are used here; two of them contradict, since resolvers wants valibot ^1 while
@typeschema/main -> @typeschema/valibot pins valibot ^0.39. Both target the
same node_modules/valibot, so a plain npm update dies with ERESOLVE. The
override settles that one edge and nothing extra lands in node_modules.

Backend deps: telego 1.10.0 -> 1.11.1 (Telegram Bot API v10.2, additive
only), klauspost/compress 1.19.1, plus the indirect bumps that came with them.

VS Code tasks: the golangci-lint and modernize tasks assumed Windows PATH
semantics, where PATH is a persistent user variable that every process
inherits, so ~/go/bin was always visible. On Linux that directory is exported
from ~/.bashrc, which the non-interactive `bash -c` behind a task never
sources, and both tasks failed with exit 127. Adds linux/osx option blocks
that prepend the Go bin directories and leaves the Windows path untouched,
plus tasks to install the two tools; those are split because go install
rejects packages from different modules in one invocation.

Go sources: modernize -fix output, covering range-over-int, slices.Backward,
maps.Copy, strings.CutPrefix and strings.SplitSeq. Behaviour is unchanged.
2026-07-25 16:08:09 +02:00
Kuzz007 ad7361549a ci: drop the Windows build from release.yml
AmneziaWG (awg-quick, the DKMS kernel module) and this whole panel's
real deployment target here are Linux servers/routers — the Windows
job (MSYS2 + CGO cross-build, its own Xray/mtg-multi asset download
step) never served a purpose for this fork and just spent CI minutes
producing a binary nobody uses. Removed build-windows entirely and
dropped it from publish-dev's needs.

Also updated both READMEs' Supported Platforms line to match — this
fork's CI no longer produces a Windows binary, upstream's still does.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 17:01:18 +03:00
Kuzz007 9581c34529 docs(readme): rewrite README.md/README.ru_RU.md for this fork
Was still MHSanaei/3x-ui's own upstream README verbatim — wrong install
URL (mhsanaei/master, no AmneziaWG mention), badges pointing at a repo
this fork doesn't publish releases/downloads on, stargazer chart that
would just show a near-flat personal-fork history.

Rewritten to lead with what's actually different here (native
AmneziaWG: no Docker, DKMS-installed by install.sh, full 2.0 obfuscation
params), point Quick Start at Kuzz007/3x-ui's dev-latest channel, note
where AmneziaWG needs Linux/Secure-Boot-off/native-not-Docker, and credit
the base project + the two AmneziaWG references this fork's
implementation was ported from (PR #6086, coinman-dev/3ax-ui).

The other 5 language READMEs (fa_IR/ar_EG/zh_CN/es_ES/tr_TR) still
describe upstream verbatim and are no longer linked from the language
switcher here — left as a follow-up rather than guessing translations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 11:08:05 +03:00
Kuzz007 7619c61863 fix(amneziawg): resolve all 3 real CI failures (typecheck/lint/codegen)
Found by checking the fork's Actions tab after the last two pushes —
the release build passed (it doesn't run these checks) but the
separate CI workflow caught three real issues:

- golangci-lint (noctx): every internal/amneziawg/manager.go exec.Command
  call is now exec.CommandContext with a 30s timeout, so a hung
  awg-quick/awg invocation can't block the reconcile job indefinitely
  (mirrors internal/mtproto/process.go's own CommandContext usage).
- tsc --noEmit: frontend/src/schemas/client.ts's hand-maintained
  InboundOptionSchema (used by the useClients hook, separate from the
  auto-generated one in generated/) never got an awgServer field added
  when the AmneziaWG frontend work was done — every read of
  inbound.awgServer.* in amneziawgConfig.ts was typing as {}. Added
  AwgServerOptionSchema, nested (not flattened like wg*) to match what
  amneziawgConfig.ts already expects. Also guarded server.publicKey in
  inbound-link.ts's genAmneziaWGLink against the schema's optional type.
- codegen staleness: frontend/public/openapi.json is produced by a Node
  script (gen:api) this machine can't run; hand-applied the exact diff
  the CI failure log already showed (amneziawg protocol enum entry,
  ServerSettings schema, InboundOption.awgServer, one example payload),
  verified as valid JSON.

Also confirmed independently by this run: install_amneziawg (previous
commit) installed and loaded the DKMS module successfully on both amd64
and arm64 CI runners. The two "Deploy Smoke Tests" failures are
unrelated to this change — this fork has only ever published the
dev-latest pre-release, and GitHub's /releases/latest API deliberately
excludes pre-releases, so the smoke test's no-argument install path
(which resolves "latest") has nothing to find. Not a regression; needs
an actual tagged release whenever that's wanted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 10:49:02 +03:00
Kuzz007 6053ebedfd feat(install): auto-install the AmneziaWG DKMS module + amneziawg-tools
Ports install_amneziawg from coinman-dev/3ax-ui's install.sh, adapted to
this script's broader distro coverage and NONINTERACTIVE convention:

- Ubuntu/Debian/Armbian: ppa:amnezia/ppa (primary, tested path), with a
  reachability pre-check for the Launchpad PPA host — often blocked by
  hosting providers, especially Russian VPS — so a flaky network skips
  the feature instead of hanging apt through several retries.
- Fedora/RHEL-family, Arch/Manjaro/Parch: best-effort fallback to plain
  wireguard-tools (+ AUR amneziawg-dkms via yay/paru when available),
  with a manual-install pointer.
- Everything else: manual-install pointer only.

Also installs ndppd and persists IPv4/IPv6 forwarding (for the future
IPv6/NDP phase, not yet wired into the panel) and adds a Secure Boot
warning at the end of the run, since a DKMS-built module is unsigned
and won't load while it's enabled — a common trap on cloud VPS images.

Never fatal: the panel installs and runs fine either way, an AmneziaWG
inbound just won't bring up its tunnel until the module is present.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 10:28:36 +03:00
Kuzz007 5617cdcf31 feat(amneziawg): add a button to randomize obfuscation parameters
Mirrors the existing key-regenerate button next to the private key
field. Client-side randomization matches the ranges/constraints of
GenerateObfuscation20's "default" preset (internal/amneziawg/params.go)
closely enough for a form suggestion — the user can still hand-edit any
field afterward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 10:20:52 +03:00
Sanaei edb487a005 chore(deps): migrate to react-router 8 and refresh frontend dependencies
react-router-dom 7 is superseded by react-router 8, which folds the DOM
bindings back into the core package. RouterProvider now comes from
`react-router/dom`, while the hooks and `createBrowserRouter` move to
`react-router`. Updates the nine importing modules and the router line in
docs/architecture.md to match.

Also refreshes antd, react-i18next, storybook, eslint, lint-staged and
playwright to current patch/minor releases, and restores alphabetical order
in devDependencies for the @vitest/browser-playwright and playwright entries.

Bumps brace-expansion to 5.0.8, the only release outside the affected range
of GHSA-mh99-v99m-4gvg (unbounded expansion length causing an OOM crash).
`npm audit fix` could not apply this on its own: the lockfile pinned 5.0.7
and npm will not re-resolve a transitive-only dependency in place, so the
entry was updated directly and reinstalled.
2026-07-25 02:07:15 +02:00
Kuzz007 f075cd75d6 docs(install): note that AmneziaWG kernel module install is still manual
Tracked separately (not yet ported into this script) — see
coinman-dev/3ax-ui's install_amneziawg for the reference approach
(ppa:amnezia/ppa). Also serves as a real, path-filter-matching change
to get the previous empty commit's CI trigger to actually fire —
release.yml's push trigger is paths-scoped and an empty commit changes
no files, so it never matched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 02:33:51 +03:00
Kuzz007 83a68634bb chore: trigger CI to publish the dev-latest release
Empty commit — no code change. Actions on this fork needed a manual
wake-up (workflow_dispatch) before push-triggered runs would fire at
all; that manual run already validated the build (all 8 platforms
succeeded) but couldn't publish dev-latest since that job is gated on
event_name == 'push'. This commit exists purely to produce that push
event now that Actions are confirmed working.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 02:32:37 +03:00
Sanaei 35cf6be6f9 fix(ci): keep the triage prompt under the 21000-char expression cap
The previous commit pushed handle-issue's prompt to 21587 characters and
GitHub stopped parsing the file: "(Line: 39, Col: 19): Exceeded max
expression length 21000". Because the prompt interpolates ${{ }}, GitHub
treats the whole block scalar as a single expression, and the cap applies
per expression. The failure mode is quiet and total - no job fails,
the workflow itself disappears, its registered name reverts from "Claude
Bot" to the file path, and the only signal is a run attributed to the
push with no jobs in it.

Drop the hand-written stack description, repository map and runtime-fact
list from that prompt and point at CLAUDE.md and docs/architecture.md
instead. Both are maintained, both are already in the checkout, and the
copy in the prompt had drifted from them anyway - it still described the
mtg worker, omitted internal/tunnelmonitor/ and memory.high, and filed
internal/web/runtime/ under "wiring". Only the support-facing facts that
live in neither file are kept: the install one-liner, the random initial
credentials, the distro-dependent env file, the Docker image and the
capabilities fail2ban needs.

handle-issue is now 15069 characters, and a header comment records the
limit so the next edit does not rediscover it in production.
2026-07-25 01:31:40 +02:00