Files
3x-ui/internal/amneziawg/route_egress.go
T
Kuzz007 db8253421a refactor(amneziawg): route via Xray through the stock Routing page, not custom toggles
Simplifies RouteViaXray after realizing the panel already has everything
needed: the Routing page already lets an admin pick a source inbound tag
and a target outbound (plus, if they want it, a specific source IP) for
any protocol. Bolting a parallel routeThroughXray/routeOutboundTag pair
onto both the client and inbound forms duplicated that mechanism instead
of using it.

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

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

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

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

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 22:37:08 +03:00

83 lines
3.6 KiB
Go

package amneziawg
import (
"fmt"
"hash/fnv"
)
// EgressBasePort is the first loopback port used for an AmneziaWG inbound's
// own Xray TPROXY bridge. Every enabled inbound gets one bridge, always
// present by default (no opt-in flag): defaultPostUpDown's TPROXY rules
// redirect every peer's traffic there unconditionally, and
// internal/web/service's injectAmneziawgEgress creates 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). Whether — and where — that 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
}