mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-17 15:47:14 +00:00
d163e6ac2da9ee5da9420a18453d7d307bfc44a0
11 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d3da7abdf0 |
fix: port the PR #6105 review-round fixes into this fork's own AmneziaWG code
Same 8 findings fixed on upstream-pr/amneziawg, ported here since this fork's internal/amneziawg + related web/service files predate that PR branch's own fix-up commits: 1. hostRulesFingerprint now folds in a peer's IPv4 whenever ForwardedPorts is set, not only when RouteThroughXray is on, so a re-IP forces the bounce needed to move the DNAT rule too. 2. ValidateConfigValue (new, params.go) rejects control characters in server/client keys, email and I1 at save time; sanitizeConfigValue strips them defensively at .conf-render time. 3. checkForwardedPortsConflict now scopes to node_id IS NULL and takes a pre-loaded portConflictContext (loadPortConflictContext), so a port used only on another node isn't a false collision and an inbound with N clients costs one query instead of N. 4. PostDown commands are now best-effort (appendOrTrue) so an external firewall flush can't abort the rest of the teardown chain. 5. The "ip rule list | grep -q" existence check now uses grep -c >/dev/null, avoiding a pipefail/SIGPIPE false negative that could re-add a duplicate rule. 6. route_egress.go's stale "always present, no opt-in" comment corrected to describe the real RouteThroughXray-gated behavior. (This fork's genAmneziaWGLink already emits vpn://, and there's no upstream-facing docs page here, so neither needed the PR branch's Finding 6 docs/link-format changes.) 7. install.sh: Arch's ndppd install uses pacman -Sy, not -Syu, matching every other pacman call in the script; should_install_amneziawg short-circuits to yes when awg is already installed, so `x-ui update` doesn't re-prompt -- this fork's own opt-out-by-default philosophy for should_install_amneziawg is unchanged, only the redundant-reprompt behavior is fixed. 8. CollectTraffic checks pointer identity before writing back a traffic-counter baseline, so a concurrent restart's freshly-reset (empty) baseline can't be clobbered by stale pre-restart counters. sweepOrphansLocked no longer permanently disables itself on a transient os.ReadDir failure. go build/vet/test and frontend typecheck/lint/build/vitest all pass. |
||
|
|
fef9a4b20a |
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |