mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-16 15:17:14 +00:00
7550072c7b318189caa5fa25a8b5eb584e7cbec0
3330 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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.
|
||
|
|
12b002b090 |
chore: point install/update/self-update at this fork instead of upstream
install.sh, update.sh, and x-ui.sh all hardcoded MHSanaei/3x-ui as the release/raw-file source (release tarball downloads, the self-update script fetch, service unit fallback downloads). Left as-is, merging AmneziaWG into main and cutting a release here would still silently install upstream's stock binary — none of our work would ever reach a machine that runs the curl-from-GitHub one-liner. Repointed all of it at Kuzz007/3x-ui, plus two more functional (not just doc/attribution) spots the same grep turned up: the panel's own in-app update checker (internal/web/service/panel/panel.go's panelUpdaterURL and the GitHub releases API calls in fetchPanelRelease) and deploy/cloud-init/cloud-init.yaml's auto-provisioning curl line. Left README/docs/CONTRIBUTING/FUNDING attribution to the upstream project untouched — this is still a fork of MHSanaei's work, that credit stays. Third-party dependency downloads in .github/workflows/release.yml (Xray-core, geoip/geosite data, mtg-multi) intentionally still point at their own respective upstream projects, not this fork. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
0f7329c3ce |
fix(ci): repair the Claude bot and narrow what it can reach
Three problems, all in .github/workflows/claude-bot.yml. It was silently dead. No comment had been posted since 2026-07-20 while every run reported success: roughly twenty issues and pull requests each burned 18-56 turns and up to $2.59, ended with permission denials, and published nothing. Comment bodies are markdown, markdown is full of backticks, and inside a quoted `--body "..."` backticks are command substitution, so the write was rejected and a failed triage looked exactly like a clean one. The body now goes to /tmp through Write and out through --body-file, in every branch of both jobs, and each job re-reads the thread afterwards so a rejected write fails loudly instead of reporting success. The run transcript is kept as an artifact. It could reach much further than it claimed. Both jobs that any GitHub user can trigger declared themselves READ-ONLY in prose while holding Bash(gh:*), which is not a GitHub-scoped allowlist: `gh alias set --shell` runs its argument through sh -c and `gh extension install` fetches and executes code, both as single commands whose first token is gh. That is a general shell on a runner holding CLAUDE_CODE_OAUTH_TOKEN, which does not expire with the job. `gh api` accepted any method, issues: write is repo-scoped rather than issue-scoped, and `gh pr review --approve`, `gh pr close` and `gh pr checkout` were forbidden in prose only. Those two jobs now list the subcommands they actually run. The untrusted title and body are fenced in tags carrying github.run_id, unguessable at the time the issue is written, and the invariants an allowlist cannot express - one issue number, labels and title only, /tmp as the sole writable path, never $GITHUB_ENV - are stated explicitly. Both checkouts get persist-credentials: false. handle-pr-fix and mention keep their wildcards: only owners, members and collaborators can trigger them, and narrowing the maintainer's own path risks more than it protects. Its review hid findings and its triage quoted stale facts. "Prefer a few high-signal findings over many low-value ones" is read literally by Opus - it finds the bug, judges it below the stated bar and says nothing - while the Severity and Confidence tiers already existed to do that filtering. The review also never said that the working directory is the base revision, so it could assert that a case was unhandled in code the pull request had already rewritten, and label it confirmed, on an outside contributor's first patch. Four CLAUDE.md conventions were missing, each a guaranteed miss: openapigen's StructAllow allowlist, the layering rules including the runtime.Runtime dispatch requirement that silently breaks multi-node when bypassed, the assertion standard, and golden share-link fixtures regenerated to turn a red test green. On the triage side the invalid and duplicate branches were gated three times over and so never fired, leaving spam to collect a full investigation and a courteous reply; /etc/default/x-ui was given as the env file when it is distro-dependent, making the PostgreSQL migration advice a silent no-op on RHEL and Arch; an env list labelled "full" omitted XUI_PORT and the XUI_TUNNEL_HEALTH_* family; XTLS was offered as a security option the panel does not have. docs/architecture.md was invisible to both prompts despite being maintained and already in the checkout. From the bot's own output: it published a trigger only the maintainer can use, retitled issues without saying so, asked for screenshots it cannot open, and once invented a reason for a number it had miscounted. All four jobs move to Opus 5, at xhigh effort rather than max - the recommended tier for agentic work, and one below the overthinking that max invites on routine triage. |
||
|
|
d3db90c8f1 |
Merge branch 'feat/amneziawg'
Native AmneziaWG protocol support (backend + frontend) for this fork. See the three merged commits for details. |
||
|
|
266f40d79f |
feat(amneziawg): complete frontend parity for the Inbounds list page
The Clients page (form, CRUD, QR/config) already worked from the prior commit; this closes the remaining gap on the Inbounds side and in a couple of protocol allowlists that a plain search for existing wireguard/mtproto handling turned up. lib/xray/inbound-link.ts gets amneziawg-specific link/config builders (genAmneziaWGLink/genAmneziaWGConfig, plus the *s fan-out variants) mirroring the wireguard ones — AmneziaWG has no legacy peers-array to fall back to, so these read settings.clients directly and add the obfuscation lines every client must share with the server. Wired into genInboundLinks generically, and into three consumers that call the wireguard builders directly rather than through that dispatcher: QrCodeModal, InboundInfoModal, and InboundsPage's bulk export. ClientInfoModal, ClientBulkAddModal, and the bulk attach/detach modals each had their own protocol allowlist that needed amneziawg added alongside wireguard/mtproto. Two real gaps surfaced by grepping every remaining 'wireguard' / Protocols.WIREGUARD hit in frontend/src rather than trusting the checklist was exhaustive: - useInbounds.ts's TRACKED_PROTOCOLS gates the deactive/depleted/ expiring/online client counts shown per inbound on the list page; without amneziawg those counts would silently read zero. - inbound-tag.ts is an explicit client-side mirror of the Go backend's port_conflict.go (the file says so itself: "Keep in sync"). It still only special-cased wireguard for UDP, so an amneziawg inbound would have fallen through to the TCP default and disagreed with the backend's own port-conflict math. Also finishes translating the AmneziaWG UI strings into the 11 locale files that were still falling back to English (ar-EG, es-ES, fa-IR, id-ID, ja-JP, pt-BR, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW), matching en-US/ru-RU key-for-key (26 new keys, verified by count in every file). Not run anywhere: npm run typecheck / build. This machine has neither Node nor npm, so nothing here has compiled — reviewed by hand plus brace/paren balance checks and cross-referencing the generated Zod/TS types. Treat this as needing a real typecheck before shipping. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
29557e2153 |
fix(sub): gate the VLESS flow in JSON subscriptions like raw and Clash links
genVless emitted client.Flow unconditionally, while the raw link (service.go:806) and the Clash proxy (clash_service.go:251) both gate it behind vlessFlowAllowed. A flow_override left on client_inbounds after its inbound moved to a transport Vision cannot use -- ws, grpc, httpupgrade -- therefore survived only into the JSON subscription, handing that client an outbound xray-core rejects while its other two formats were correct. Apply the same gate at the call site, reading the network from the per-host stream so a host that rewrites the transport is judged on what it actually emits. Verified by seeding a flow_override on a ws+tls inbound: before, raw and Clash dropped the flow and JSON kept it. |
||
|
|
0b60154383 |
fix(docs): force transitive sharp up to patched 0.35.3
sharp <0.35.0 inherits four libvips CVEs (GHSA-f88m-g3jw-g9cj). It comes in as an optional dependency of next, which still declares ^0.34.5 on its current release, so only an override reaches the fixed line. Brings libvips 8.18.3 via @img/sharp-libvips-* 1.3.2. |
||
|
|
c3967e57dc |
perf(clients): take one email snapshot per client fan-out, not one per inbound (#6091)
Create and Attach called the exported AddInboundClient once per target inbound, and that wrapper passes a nil email→subId map, so every iteration re-ran getAllEmailSubIDs -- a JSON_EACH expansion over the settings blob of every inbound in the panel. Adding one client to 24 inbounds on a panel with ~300 users meant 24 full expansions of ~7k rows to answer the same question. Hoist the snapshot above the loop and call the unexported addInboundClient with it, exactly as BulkAttach (client_bulk.go:63) and BulkCreate (client_bulk.go:1151) already do. The snapshot goes stale from the second inbound onward, but the identity being added is the same on every iteration, so its own entry can only ever match itself -- checkEmailsExistForClients accepts an email whose stored subId equals the incoming one, and an absent entry is accepted too. This is the database half of #6091. The dominant cost there is the other half -- one synchronous 10s-capped node round-trip per remote inbound, which multiplies again on chained nodes -- and that needs the push batched per node rather than per inbound; left for a separate change. |
||
|
|
19082fdfe9 |
feat(amneziawg): add frontend support and fix a Go->Zod generator gap
Wires the amneziawg protocol through the panel UI the same way every
other protocol is registered: a Zod settings schema (nested
{server, clients}, matching the Go JSON exactly), the protocol enum,
the inbound-form's per-protocol fields component and its
tab-visibility allowlist, the default-settings factory, the client
schema dispatcher, and the sniffing-capability exclusion (no Xray
inbound exists for amneziawg, same as mtproto).
Client key/allowedIPs fields are reused rather than duplicated: since
AmneziaWG clients are wire-identical to WireGuard clients (same
model.Client fields), ClientFormModal renders one shared field block
for both, switching only the visible label by which protocol is
active. The private-key input also gets a live public-key sync via a
new useEffect, because unlike WireGuard's Xray-native inbound (which
re-derives its public key at runtime and never stores one),
AmneziaWG's server.publicKey is a real persisted field the Go backend
reads directly — free-typing a new private key without this would
silently save a mismatched keypair.
Adds a downloadable per-client .conf (amneziawgConfig.ts, mirroring
wireguardConfig.ts) with the obfuscation lines, and an
InboundOption.AwgServer field on the Go side so the config builder
gets the full server block in one round trip.
Along the way, running tools/openapigen surfaced a real bug: it
doesn't flatten anonymously-embedded Go structs the way encoding/json
does, so ServerSettings embedding Obfuscation20 produced a Zod schema
with a nested `obfuscation20` key that never matches the real wire
JSON. Fixed by un-embedding (flat fields + an accessor method) and
registering internal/amneziawg in the generator's own package list,
which had been silently emitting a dangling schema reference.
English and Russian translations are complete; the other 10 locale
files still fall back to English for the new keys.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
83cc545953 |
feat(amneziawg): add native AmneziaWG protocol backend
AmneziaWG (WireGuard plus DPI-resistant obfuscation) needs no Docker here — it runs as a genuine kernel interface via awg-quick/awg, managed the same way internal/mtproto manages mtg: one Inbound row is one desired Instance, and a Manager reconciles running interfaces toward the database every 10s (internal/web/job/amneziawg_job.go) plus immediately after a client edit (applyLocalAmneziaWG). Clients reuse model.Client verbatim (the same PrivateKey/PublicKey/ PreSharedKey/AllowedIPs fields WireGuard already uses), so bulk operations, the QR/share-link modal and subscriptions come from the shared inbound infrastructure instead of a parallel implementation. internal/amneziawg owns the obfuscation param generator/validator (ported from coinman-dev/3ax-ui, upgraded to AmneziaWG 2.0's S3/S4 padding and I1 signature packet) and the exec wrapper around awg-quick/awg, with fingerprint-based reconcile (noop / reload-via- syncconf / full restart) mirroring mtproto.Manager so a same-protocol edit doesn't force an unnecessary interface bounce that would drop every peer's connection. Frontend and install.sh's DKMS/awg-tools setup are tracked separately; this is backend-only. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
aa60d54ea5 |
fix(wireguard): widen the client address pool past a full /24 (#6089)
allocateWireguardAddress scanned exactly one /24, so a WireGuard inbound was hard-capped at 254 clients with no way out -- the pool is not configurable anywhere in the UI or API. Fill the inbound's own /24 first, then widen to the enclosing /16 instead of failing. A wireguard inbound carries no interface subnet and xray routes purely by each peer's allowedIPs, so nothing constrains the wider address. Capped at /16 to keep the worst-case scan bounded; IPv4 only. |
||
|
|
a652cb8cea |
fix(clients): keep a client editable when its subId is already shared (#6065)
The subId collision check in Update ran on every save, unlike the email
check above it. Because Update defaults an omitted subId to the stored
one, any client already sharing a subId was rejected on every later edit
-- even a pure totalGB or expiry change that never mentions subId.
Gate the check on an actual change. Pre-existing duplicates are reachable
because SyncInbound has no such check, and
|
||
|
|
cd674c8d4f |
feat(sub): expose live online status and add ?format=info endpoint
Custom subscription templates only received the lastOnline timestamp, so template authors had to fake an online indicator by comparing it against the current time, and the page was a one-shot server render with no way to refresh usage without reloading the whole HTML. The template context (and window.__SUB_PAGE_DATA__) now carries isOnline, computed from the panel's own online-client tracking (local xray plus remote nodes) at render time. The subscription URL also answers ?format=info with the page view-model as JSON — minus the links, with emails deduplicated — so templates can poll live status cheaply. The shared view-model construction moved into buildSubPageData/subPageContext so the HTML page, the SPA payload and the info JSON cannot drift apart. Also documents the previously injected but undocumented announce template variable. |
||
|
|
b319dd0c3a |
fix(panel): align telegram icon with its label in home card actions
The .tg-icon override (display: inline-block; vertical-align: -2px) defeated the default .anticon flex centering that every other card action icon relies on, so the icon rendered ~2px below the @XrayUI text. Dropping the override lets AntD center it like its neighbors. |
||
|
|
8ef2eec3d1 |
fix(hosts): assign group ids to imported hosts and repair empty ones
Host rows created from a legacy streamSettings.externalProxy during inbound import got an empty group_id, and the one-time HostGroupIds seeder had already been gated off, so the UI rendered them under a synthetic fallback_<id> group the update/delete API could not resolve, failing every edit with "host group not found". Assign a real group id in externalProxyEntryToHost at creation, and replace the seeder with backfillEmptyHostGroupIds, an idempotent startup repair that runs on every boot so rows from older builds and restored backups are healed too. Also rename the leaked internal error "host group not found" to "host not found" since groups are not a user-facing concept. |
||
|
|
941c6116a9 |
chore(openapi): regenerate schemas with int64 formats on node fields
Output of make gen: the generator now stamps format int64 on the node status schema's 64-bit integer fields (timestamps, net counters, uptime), syncing the committed OpenAPI doc and generated schemas with the Go structs. |
||
|
|
c77608bc47 |
fix(nodes): make node API tokens write-only (#5613)
* fix(nodes): make node API tokens write-only * fix(nodes): keep token optional on edit for write-only API tokens NodeView no longer returns apiToken, so the edit form must consume hasApiToken and not require re-entering the token. Relaxes the form validation on edit, adds a keep-current placeholder, and adds the i18n key to all 13 locales. |
||
|
|
892c06c8bc |
Bug-label issue sweep: 16 fixes (#6083)
* fix(xray): block private-range egress in default freedom finalRules (#6037)
With domainStrategy AsIs the router never resolves domains, so a domain
with a private A record (e.g. 127-0-0-1.nip.io) sails past the
geoip:private routing block and freedom's allow-all finalRules let it
reach loopback services such as the xray gRPC API and metrics listener.
Prepend a block rule for geoip:private to the default template and add
the FreedomFinalRulesPrivateEgressBlock seeder so existing installs
still carrying the stock allow-only (or legacy private-only-allow)
finalRules are upgraded in place; customized rules are left untouched.
* fix(sub): version-gate unencrypted-outbound drops in outbound subscriptions (#6033)
Commit
|
||
|
|
16b9b3ce1c |
chore(deps): bump docs and frontend dependencies
Routine minor/patch updates: Next.js 16.2.11 + eslint-config-next, fumadocs, React 19.2.8, Storybook 10.5.3, and assorted tooling. Docs stays on ESLint 9 (^9.39.5): eslint-config-next pulls in eslint-plugin-react 7.37.5, whose newest release still calls the context.getFilename API that ESLint 10 removed, so eslint crashes on every file under ESLint 10. The frontend workspace already ran ESLint 10 without eslint-plugin-react and is unaffected. |