mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-01 03:05:38 +00:00
6 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
effcccceac
|
feat(amneziawg): add native AmneziaWG protocol support (#6105)
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* feat(amneziawg): route a client's traffic through Xray via the Routing page
Every enabled AmneziaWG inbound gets its own Xray TPROXY bridge
automatically, with no toggle to enable first: a loopback
dokodemo-door inbound (sockopt.tproxy) tagged with the AmneziaWG
inbound's own real tag, so it's already selectable in the existing
Routing page's inbound-tag picker — the same trick the mtproto
sidecar's own bridge already relies on (InboundService.GetInboundTags
is a plain, protocol-blind SELECT over every inbound row's tag, no
dedicated UI plumbing needed).
internal/amneziawg's defaultPostUpDown TPROXYs every peer's traffic
into that bridge unconditionally; the bridge's port is derived
deterministically from the inbound's id (EgressPortForInbound) so the
kernel-side reconcile loop and the Xray-config generator never need to
negotiate a runtime value between them.
injectAmneziawgEgress never generates a routing rule itself — whether
a client's traffic goes anywhere beyond Xray's default routing is
entirely up to whatever rules the admin adds through the existing
Routing UI (pick the AmneziaWG inbound's tag as source, optionally a
specific peer's IP via that page's own Source-IP field, and an
outbound), exactly the same workflow as routing any other protocol.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* 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>
* 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.
* 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>
* 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>
* 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>
* fix(amneziawg): don't widen the peer address pool past AmneziaWG's own subnet
Completes Finding 12 from the automated PR review (the serverAddress half
of this finding was already fixed on main and cherry-picked here). This
half is specific to this branch: allocateWireguardAddress's /16
pool-widening fallback is an independent addition from upstream's own
main that this branch inherited during the cherry-pick rebase -- it
doesn't exist on the fork's own main at all, so this fix can't be
cherry-picked the normal way and is committed directly here.
Widening is safe for WireGuard's own Xray-native inbound (AllowedIPs
isn't tied to a strict kernel interface subnet), but AmneziaWG's kernel
interface Address is exactly the configured subnet -- an address
allocated from the containing /16 once the /24 fills up would be
silently unroutable. allocateWireguardAddress now takes an explicit
allowWidening bool: WireGuard's own caller passes true (unchanged
behavior), AmneziaWG's passes false (fails loudly on exhaustion instead).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* 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.
* 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>
* 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>
* fix(amneziawg): allow TPROXY-marked traffic through a default-deny INPUT chain
TPROXY never rewrites a packet's own destination address, only the routing
decision. A default-deny firewall whose INPUT chain sanity-checks "is this
destination actually local" (UFW's ufw-not-local, via addrtype --dst-type
LOCAL, is a concrete example) silently drops the redirected packet before
Xray's socket ever sees it -- RouteThroughXray looked fully configured
(TPROXY rule present and counting, Xray listening with IP_TRANSPARENT set)
yet every peer's traffic vanished with no trace on either side.
Adds an idempotent, never-torn-down "iptables -I INPUT 1 -m mark --mark
<fwmark> -j ACCEPT" alongside the existing shared policy route, so this
works regardless of which firewall manager owns the rest of the INPUT chain.
* fix(frontend): give AmneziaWG the same UDP tag and its own tag color
The Inbounds list only special-cased isWireguard/isHysteria for the "UDP"
network badge, so an AmneziaWG row showed just the bare protocol tag with
no transport badge next to it. Added the missing isAmneziawg flag (mirrors
isWireguard exactly) and wired it into the same branch.
Client-row protocol-color maps in ClientsPage/HostList had no amneziawg
entry, silently falling back to grey -- ClientInfoModal already had
amneziawg: 'yellow' from earlier work, these two just never got it.
* feat(logs): show which AmneziaWG client an access-log line belongs to
The dokodemo-door TPROXY bridge every AmneziaWG peer's traffic is routed
through has no per-user identity, so Xray's own access log never carries an
"email:" token for these lines -- the Access Logs modal showed a blank
Email column for every in-*-udp row, even though every other protocol's
rows show the client normally.
The peer's decapsulated tunnel IP does survive as the log's "from" address,
and that IP deterministically maps to exactly one configured peer. Builds a
"<inbound tag>|<ip>" -> email index from the same AmneziaWG inbounds already
parsed elsewhere (amneziawg.InstanceFromInbound), and fills in Email from it
whenever the raw log line didn't have one.
* fix(amneziawg): enable sniffing on the TPROXY bridge
Domain-based Routing rules could never match RouteThroughXray traffic: an
AmneziaWG peer resolves DNS itself, through the tunnel, before ever sending
a packet, so the decapsulated traffic TPROXY hands to the bridge is already
a bare destination IP with no domain name attached at the network layer.
Every other inbound recovers this via sniffing (confirmed working for the
stock wireguard inbound, which does have it configured); the bridge never
got a sniffing block at all, so only tag/IP/network-based rules could ever
match it -- any domain rule above it in the list was silently unreachable.
* docs: add an AmneziaWG config page and list it as a supported protocol
Closes the PR checklist gap: the feature shipped with zero mention on
the docs site. Mirrors reality.mdx's structure (key settings, setup
steps, config excerpt) and notes the Docker/multi-node/Telegram-bot
caveats the PR itself is honest about not having confirmed.
* fix: address the fresh review round on PR #6105 (8 findings)
1. hostRulesFingerprint didn't account for ForwardedPorts when
RouteThroughXray was off, so re-IPing a peer with port-forwarding
configured left stale DNAT rules pointing at an address the next
peer could be handed.
2. Server/client config values (keys, email, I1) were never validated
for control characters before being written into the generated
.conf; a newline could smuggle a PostUp hook into awg-quick's
parser. Added ValidateConfigValue at save time and a
sanitizeConfigValue backstop at render time.
3. checkForwardedPortsConflict didn't scope to node_id IS NULL, so a
port used only on a different node produced a false collision; also
hoisted the panel-port/inbounds lookup out of the per-client loop
(portConflictContext) so N clients cost one query, not N.
4. PostDown commands were ";"-joined and abort on the first failure;
appendOrTrue makes teardown best-effort so an external firewall
flush can't leave DNAT rules to accumulate across bounces.
5. The "ip rule list | grep -q" existence check could SIGPIPE under
pipefail and re-add a duplicate rule; switched to grep -c >/dev/null.
6. Ported the vpn:// share-link format (base64url of the plain .conf
text, matching the real AmneziaVPN app) onto this branch -- it had
only ever landed on our own fork's main, so this PR branch was still
on the old amneziawg://+query-params scheme our own docs no longer
described. Also corrected the docs' install.sh claim (opt-in/
interactive, not automatic) and stale pre-opt-in comments in
route_egress.go.
7. install.sh: Arch's ndppd install used pacman -Syu (full system
upgrade) instead of -Sy like every other call in the script; and
should_install_amneziawg re-prompted on every `x-ui update` even
when awg was already installed.
8. CollectTraffic could clobber a concurrent restart's freshly-reset
(empty) traffic baseline with stale pre-restart counters, since
getPeerStats runs lock-free; now checks pointer identity before
writing back. sweepOrphansLocked permanently disabled itself on a
transient os.ReadDir failure instead of allowing a retry.
go build/vet/test and frontend typecheck/lint/build/vitest all pass.
* fix(install.sh): check the live sysctl value, not sysctl.conf text
Reviewer feedback (cherts, PR #6105): grepping /etc/sysctl.conf for the
setting name is unreliable -- many distros split sysctl config across
/etc/sysctl.d/*.conf, and /etc/sysctl.conf can be a symlink into that
directory, so the check can miss an already-active setting (harmless
duplicate append) or match a disabled/commented line (forwarding
silently stays off). Query the live value via `sysctl -n` instead,
which is accurate regardless of which file set it. Applied the same
fix to both the IPv6 and IPv4 checks for consistency.
* fix: update inbound_amneziawg.go to the split buildInboundForLocalRuntime
Same fork-only-file blind spot as the one caught on our own main after
the 3.6.0 sync: upstream split buildRuntimeInboundForAPI into
buildInboundForNodePush / buildInboundForLocalRuntime (part of the
node-sync client-deletion fix,
|
||
|
|
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
|
||
|
|
814cda3fb4
|
feat(xray): update xray-core to v26.7.11 and adapt panel
Some checks failed
CI / go-test (push) Waiting to run
CI / postgres-durable-first (push) Waiting to run
CI / codegen (push) Waiting to run
CI / govulncheck (push) Waiting to run
CI / race (push) Waiting to run
CI / fuzz-smoke (push) Waiting to run
CI / golangci (push) Waiting to run
CI / frontend (push) Waiting to run
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
Release 3X-UI / Publish rolling dev release (push) Blocked by required conditions
Docs CI / build (push) Has been cancelled
Docs Deploy (GitHub Pages) / build (push) Has been cancelled
Deploy Smoke Tests / noninteractive-install (ubuntu-24.04-arm) (push) Has been cancelled
Deploy Smoke Tests / noninteractive-install (ubuntu-latest) (push) Has been cancelled
Deploy Smoke Tests / release-tag-install (ubuntu-24.04-arm) (push) Has been cancelled
Deploy Smoke Tests / release-tag-install (ubuntu-latest) (push) Has been cancelled
Docs Deploy (GitHub Pages) / deploy (push) Has been cancelled
Bump xtls/xray-core to 50231eaf (v26.7.11) and the three binary pins (DockerInit.sh, release.yml x2) in lockstep. Adapt the panel to the upstream changes: - Shadowsocks "none"/"plain" and VMess "none"/"zero" were removed from the core. A migration rewrites stored none/plain SS methods to a supported cipher and none/zero VMess security to "auto" (on both the clients column and inbound settings JSON); the SS build-time heal does the same so a row injected after boot cannot brick startup. The removed values are dropped from every frontend option list, schema and adapter, and coerced to "auto" at the Go link/sub/Clash emit sites and both link importers. Fix the CipherType_NONE sentinel that no longer compiles. - Unencrypted vless/trojan outbounds to a public address are now refused by the core. Validate outbounds through the vendored config loader when saving the xray template and when storing/merging outbound subscriptions, so one such outbound cannot keep the core from starting. - New TCP finalmask type "xmc" (Minecraft mimicry): add it to the sub link allowlist, the frontend enum and the FinalMask form (hostname, usernames, required password), and document it. - streamSettings gained a "method" alias for "network"; canonicalize it to "network" at inbound save time and in the form adapters/schema so a method-keyed config keeps its transport. - New root "env" config key is passed through xray.Config, compared in Equals, and forces a restart in the hot diff. - REALITY now defaults minClientVer to 26.3.27; update the form placeholder. |
||
|
|
e5b56c9444 |
fix(xray): reconcile client auto-disable through the API instead of a forced restart
When a client expired or hit its traffic limit, XrayTrafficJob called RestartXray(true), stopping the whole process and dropping every live connection on every inbound (#5712 reported this as XHTTP on 443 dying) — even though disableInvalidClients had already removed the user from the running core over gRPC. The force restart existed only to re-sync the process's config snapshot. Switch the job to a non-forced restart and teach ComputeHotDiff to express a client-only inbound change as per-user AlterInbound operations for vless/vmess/trojan, so the reconcile is a no-op RemoveUser plus a snapshot update rather than a handler swap that would still blip that inbound's listener. Anything beyond the clients list still falls back to handler replacement or a full restart as before. Closes #5712 |
||
|
|
49773c18de
|
fix(xray): force full restart for inbounds with a VLESS reverse client
Some checks are pending
CI / go-test (push) Waiting to run
CI / codegen (push) Waiting to run
CI / govulncheck (push) Waiting to run
CI / race (push) Waiting to run
CI / fuzz-smoke (push) Waiting to run
CI / golangci (push) Waiting to run
CI / frontend (push) Waiting to run
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
Release 3X-UI / Publish rolling dev release (push) Blocked by required conditions
Hot-applying an inbound change swaps it via DelInbound+AddInbound on the running core. That unregisters any client's reverse.tag handler on the xray-core side without closing the bridge's already-established connection, so the reverse tunnel is silently orphaned until someone manually restarts xray. diffInbounds now bails out of the hot-apply path whenever the old or new inbound carries a reverse-tagged client, falling back to a full restart, which actually drops the socket and lets the bridge redial on its own. Also scope the .claude ignore rule to its contents (.claude/*) instead of the whole directory, so individual files under .claude/ can be tracked selectively. |
||
|
|
6b16d8c37a |
feat: apply inbound/outbound/routing changes live via Xray gRPC API
Add a hot-apply layer that computes a diff between the old and new generated config and applies only the changed parts through the Xray gRPC HandlerService and RoutingService, avoiding a full process restart whenever possible. A restart is still performed when sections that have no reload API (log, dns, policy, observatory, ...) actually change. Key additions: - internal/xray/hot_diff.go: ComputeHotDiff with canonical-JSON comparison (sorted keys, null=absent, full number precision) so UI reformatting never triggers a spurious restart - internal/xray/api.go: AddOutbound/DelOutbound, ApplyRoutingConfig, GetBalancerInfo, SetBalancerTarget, TestRoute gRPC wrappers - internal/web/service/xray.go: tryHotApply, ensureAPIServices, GetBalancersStatus, OverrideBalancer, TestRoute service methods - internal/web/controller/xray_setting.go: balancerStatus, balancerOverride, routeTest API endpoints - frontend: BalancersTab live-status/override columns, RouteTester component, Restart button removed (Save now hot-applies) - balancer-helpers.ts: syncObservatories never creates observatory sections for random/roundRobin balancers (no reload API → restart) - i18n: balancerLive/Override/routeTester keys added to all 13 locales |