Files
3x-ui/internal/amneziawg/route_egress.go
T
Kuzz007 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.
2026-07-29 01:21:03 +03:00

84 lines
3.7 KiB
Go

package amneziawg
import (
"fmt"
"hash/fnv"
)
// EgressBasePort is the first loopback port used for an AmneziaWG inbound's
// own Xray TPROXY bridge. The bridge is opt-in per inbound, gated on
// Instance.RouteThroughXray (off by default): only when it's on does
// defaultPostUpDown's TPROXY rules redirect a peer's traffic there, and only
// then does internal/web/service's injectAmneziawgEgress create the matching
// dokodemo-door inbound, tagged with the AmneziaWG inbound's own real tag so
// it's already selectable in the panel's stock Routing page (the same
// mechanism that already makes an mtproto inbound's own bridge routable
// there — see injectMtprotoEgress). A plain AmneziaWG tunnel with routing
// left off never depends on Xray being up at all. Whether — and where —
// routed traffic actually goes anywhere beyond Xray's default routing is
// entirely up to whatever rules the admin adds on that page; this package
// and injectAmneziawgEgress never generate a routing rule themselves.
//
// EgressPortForInbound derives each inbound's own port deterministically
// from its id, so the two independent reconcile loops (this package's
// PostUp generator and the Xray-config generator, in a different package)
// never have to agree on a runtime-negotiated value.
const EgressBasePort = 63100
// EgressPortForInbound returns the loopback port of one AmneziaWG inbound's
// own Xray TPROXY bridge.
func EgressPortForInbound(inboundID int) int {
return EgressBasePort + inboundID
}
// EgressFwmark and EgressTable are the fwmark and policy-routing table
// TPROXY needs to deliver a peer's packets to a local socket even though
// their destination is never one of this host's own addresses. Shared by
// every AmneziaWG instance's bridge — only the port differs per instance.
// Chosen to be distinctive; if either happens to collide with something else
// already using fwmarks/routing tables on the host, change the values here —
// nothing outside this package and its own PostUp/PostDown output depends on
// the actual numbers.
const (
EgressFwmark = 0x2377
EgressTable = 87
)
// routeEgressComment returns a short, shell-safe iptables comment tag for one
// peer's TPROXY rule, so PostDown removes exactly what PostUp added
// regardless of ordering. Derived from a hash of the peer's email for the
// same reason portForwardComment is: email is admin/API-supplied free text
// that ends up embedded in a shell-executed PostUp/PostDown line, and a hash
// can never carry a shell metacharacter through.
func routeEgressComment(email string) string {
if email == "" {
return "awg-route"
}
h := fnv.New32a()
_, _ = h.Write([]byte(email))
return fmt.Sprintf("awg-route-%08x", h.Sum32())
}
// routeEgressLines returns the PostUp ("-A") or PostDown ("-D") mangle-table
// TPROXY lines that redirect one peer's traffic — matched by its tunnel
// source IP, arriving on tunIface — into that instance's own Xray bridge on
// port. Both TCP and UDP are covered since every peer's whole traffic is
// meant to reach the bridge, not just a specific protocol or port; which
// outbound (if any) it then takes is entirely up to the admin's own Routing
// rules. Returns nil when clientIP is empty.
func routeEgressLines(action, tunIface, clientIP, email string, port int) []string {
clientIP = stripCIDRMask(clientIP)
if clientIP == "" {
return nil
}
comment := routeEgressComment(email)
lines := make([]string, 0, 2)
for _, proto := range []string{"tcp", "udp"} {
lines = append(lines, fmt.Sprintf(
"iptables -t mangle %s PREROUTING -i %s -s %s -p %s -m comment --comment %s -j TPROXY --on-port %d --on-ip 127.0.0.1 --tproxy-mark %#x/%#x",
action, tunIface, clientIP, proto, comment, port, EgressFwmark, EgressFwmark,
))
}
return lines
}