mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-06 18:27:14 +00:00
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.
This commit is contained in:
@@ -734,9 +734,27 @@ func defaultPostUpDown(inst Instance, ext string) (postUp, postDown string) {
|
||||
// re-runs on any client add/remove/re-IP — without the existence
|
||||
// check below, "ip rule show" would accumulate one duplicate entry
|
||||
// per bounce forever.
|
||||
//
|
||||
// TPROXY never rewrites the packet's own destination address — only
|
||||
// the routing decision changes, via the fwmark+table trick above — so
|
||||
// by the time this packet reaches the host's own INPUT chain, its
|
||||
// destination still looks like some remote address (e.g. 8.8.8.8),
|
||||
// never this host's own. A default-deny firewall whose INPUT chain
|
||||
// sanity-checks "is this destination actually local" (UFW's
|
||||
// ufw-not-local, using addrtype --dst-type LOCAL, is exactly this) can
|
||||
// never see it as legitimate and silently drops it before Xray's
|
||||
// socket ever sees a single byte — TPROXY's own counters keep
|
||||
// incrementing the whole time, making this look like a Xray-side bug
|
||||
// even though Xray never gets the chance to fail. The fix is the same
|
||||
// shape as the policy route above: an idempotent, never-torn-down,
|
||||
// system-wide accept for this fwmark, inserted at the very front of
|
||||
// the base INPUT chain so it runs before any such sanity check,
|
||||
// regardless of which firewall manager (ufw, firewalld, bare
|
||||
// iptables) owns the rest of that chain.
|
||||
up = append(up,
|
||||
fmt.Sprintf("ip rule list | grep -q 'fwmark %#x lookup %d' || ip rule add fwmark %#x lookup %d", EgressFwmark, EgressTable, EgressFwmark, EgressTable),
|
||||
fmt.Sprintf("ip route replace local 0.0.0.0/0 dev lo table %d", EgressTable),
|
||||
fmt.Sprintf("iptables -C INPUT -m mark --mark %#x -j ACCEPT 2>/dev/null || iptables -I INPUT 1 -m mark --mark %#x -j ACCEPT", EgressFwmark, EgressFwmark),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,6 +401,35 @@ func TestDefaultPostUpDownEmitsTproxyForEveryPeerWhenRouteThroughXrayOn(t *testi
|
||||
}
|
||||
}
|
||||
|
||||
// TPROXY never rewrites a packet's own destination address -- only the
|
||||
// routing decision changes -- so a default-deny INPUT chain that sanity-checks
|
||||
// "is this destination actually local" (e.g. UFW's ufw-not-local, via
|
||||
// addrtype --dst-type LOCAL) drops it before Xray's socket ever sees it, even
|
||||
// though TPROXY's own mangle-table counters keep incrementing the whole time.
|
||||
// This was a real, hard-to-diagnose production outage: RouteThroughXray
|
||||
// looked fully configured (TPROXY rule present, Xray socket listening with
|
||||
// IP_TRANSPARENT set) yet every peer's traffic silently vanished.
|
||||
func TestDefaultPostUpDownAddsInputAcceptForFwmarkWhenRouteThroughXrayOn(t *testing.T) {
|
||||
inst := baseInstance() // two peers, a@x and b@x
|
||||
inst.RouteThroughXray = true
|
||||
up, down := defaultPostUpDown(inst, "eth0")
|
||||
|
||||
wantCheck := fmt.Sprintf("iptables -C INPUT -m mark --mark %#x -j ACCEPT", EgressFwmark)
|
||||
wantInsert := fmt.Sprintf("iptables -I INPUT 1 -m mark --mark %#x -j ACCEPT", EgressFwmark)
|
||||
if !strings.Contains(up, wantCheck) || !strings.Contains(up, wantInsert) {
|
||||
t.Errorf("expected an idempotent INPUT accept for the shared fwmark in PostUp, got:\n%s", up)
|
||||
}
|
||||
if strings.Contains(down, "-m mark --mark") {
|
||||
t.Error("the shared INPUT accept must never be removed in PostDown -- other instances may still need it, same as the policy route")
|
||||
}
|
||||
|
||||
none := Instance{Id: 2, InterfaceName: "awg2", RouteThroughXray: true} // no peers at all
|
||||
upNone, _ := defaultPostUpDown(none, "eth0")
|
||||
if strings.Contains(upNone, "-m mark --mark") {
|
||||
t.Errorf("an instance with no peers must not emit the INPUT accept either, got:\n%s", upNone)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateServerConfigContainsExpectedLines(t *testing.T) {
|
||||
inst := baseInstance()
|
||||
inst.ExternalInterface = "eth0"
|
||||
|
||||
Reference in New Issue
Block a user