mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-08 19: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
|
// re-runs on any client add/remove/re-IP — without the existence
|
||||||
// check below, "ip rule show" would accumulate one duplicate entry
|
// check below, "ip rule show" would accumulate one duplicate entry
|
||||||
// per bounce forever.
|
// 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,
|
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 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("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) {
|
func TestGenerateServerConfigContainsExpectedLines(t *testing.T) {
|
||||||
inst := baseInstance()
|
inst := baseInstance()
|
||||||
inst.ExternalInterface = "eth0"
|
inst.ExternalInterface = "eth0"
|
||||||
|
|||||||
Reference in New Issue
Block a user