fix(amneziawg): sniff the relay with routeOnly; scope the v6 egress to IPv6 (#6654)

* fix(amneziawg): sniff the relay with routeOnly

The embedded AmneziaWG relay sniffed without routeOnly, so a sniffed SNI
replaced the dial target. Telegram's FakeTLS recovery dials
194.221.250.50:443 with SNI www.google.com; the rewrite sent it to real
Google and the client looped on "TLS hash mismatch", stuck on "Connecting".

Sniffing here exists only so domain routing rules can match; routeOnly
keeps that and dials the IP the peer resolved. Fake-pool targets are
still rewritten (the dispatcher ignores routeOnly for fakedns).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(amneziawg): send only IPv6 targets through the peer's v6 egress

The per-peer IPv6 egress rule matched every flow of that peer, but its
freedom outbound binds a v6 sendThrough and cannot dial an IPv4 target, so
IPv4 DNS and any other unsniffed IPv4 traffic of such a peer failed.
Sniffed TLS/HTTP only worked because the sniffed domain replaced the IP;
with routeOnly on the relay that no longer happens.

Limiting the rule to ::/0 keeps the peer's IPv6 identity for IPv6 targets
and lets IPv4 targets take the regular outbound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Kirill Rudenko <rudenko@npp-energy.ru>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
Kirill Rudenko
2026-09-26 23:04:08 +03:00
committed by GitHub
parent 6ac0c88084
commit 788b76c544
2 changed files with 34 additions and 12 deletions
+5 -12
View File
@@ -727,17 +727,9 @@ func injectMtprotoEgress(cfg *xray.Config, inbound *model.Inbound) {
})
}
// amneziawgEgressSniffingSettings matches this fork's normal per-inbound
// default (see default.json's "mixed" inbound). Without this, domain-based
// Routing rules can never match this relay: the peer resolved DNS
// itself, through the tunnel, before ever sending a packet — by the time the
// embedded forwarder recovers the decapsulated traffic, the destination is
// already a bare IP, with no domain name attached at the network layer at
// all. Sniffing recovers it from the payload itself (TLS SNI / HTTP Host /
// QUIC) the same way it already does for every other inbound; without it,
// only tag/IP/network-based rules can ever match this traffic, and any
// domain rule above it in the list is silently unreachable.
const amneziawgEgressSniffingSettings = `{"enabled":true,"destOverride":["http","tls","quic","fakedns"]}`
// Peers resolve DNS inside the tunnel, so domain rules match only via sniffing; routeOnly
// keeps the dial on the peer's IP, else Telegram's FakeTLS (IP + foreign SNI) breaks.
const amneziawgEgressSniffingSettings = `{"enabled":true,"destOverride":["http","tls","quic","fakedns"],"routeOnly":true}`
// injectAmneziawgnetSocks gives every enabled AmneziaWG inbound with at
// least one qualifying peer its own loopback SOCKS5 inbound for the
@@ -814,7 +806,7 @@ func amneziawgV6EgressTag(inboundID int, email string) string {
// injectAmneziawgV6Egress gives every enabled, non-node-hosted AmneziaWG
// peer with an IPv6 AllowedIPs entry its own single-purpose freedom
// outbound, bound via sendThrough to that exact address, plus a routing
// rule sending only that peer's own traffic through it — restoring the
// rule sending only that peer's IPv6-destined traffic through it — restoring the
// per-client public IPv6 identity the hard cutover temporarily dropped
// (Phase 3.5 of the migration plan). Scoped to outbound source identity
// only: it depends on internal/amneziawgnet's own alias mechanism actually
@@ -916,6 +908,7 @@ func injectAmneziawgV6Egress(cfg *xray.Config, inbounds []*model.Inbound) {
"type": "field",
"inboundTag": []any{inbound.Tag},
"user": []any{p.Email},
"ip": []any{"::/0"},
"outboundTag": tag,
})
}