mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-27 12:16:41 +08:00
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:
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -612,6 +613,29 @@ func TestInjectAmneziawgnetSocks_CreatesRelayTaggedWithInboundsOwnTag(t *testing
|
||||
}
|
||||
}
|
||||
|
||||
// Without routeOnly the sniffed SNI replaces the dial target, so Telegram's
|
||||
// FakeTLS to 194.221.250.50 (SNI www.google.com) lands on real Google.
|
||||
func TestInjectAmneziawgnetSocks_SniffingRouteOnly(t *testing.T) {
|
||||
cfg := egressTestConfig()
|
||||
injectAmneziawgnetSocks(cfg, []*model.Inbound{amneziawgInbound(7, "awg-7", []model.Client{
|
||||
{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}},
|
||||
})})
|
||||
var sniffing struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
DestOverride []string `json:"destOverride"`
|
||||
RouteOnly bool `json:"routeOnly"`
|
||||
}
|
||||
if err := json.Unmarshal(cfg.InboundConfigs[1].Sniffing, &sniffing); err != nil {
|
||||
t.Fatalf("relay inbound must carry a sniffing block, got %q: %v", cfg.InboundConfigs[1].Sniffing, err)
|
||||
}
|
||||
if !sniffing.Enabled || !sniffing.RouteOnly {
|
||||
t.Fatalf("sniffing must be enabled with routeOnly, got %+v", sniffing)
|
||||
}
|
||||
if want := []string{"http", "tls", "quic", "fakedns"}; !slices.Equal(sniffing.DestOverride, want) {
|
||||
t.Fatalf("destOverride = %v, want %v", sniffing.DestOverride, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectAmneziawgnetSocks_MultipleInboundsEachGetOwnRelay(t *testing.T) {
|
||||
cfg := egressTestConfig()
|
||||
inbound1 := amneziawgInbound(1, "awg-1", []model.Client{
|
||||
@@ -757,6 +781,7 @@ type v6EgressRouting struct {
|
||||
Rules []struct {
|
||||
InboundTag []string `json:"inboundTag"`
|
||||
User []string `json:"user"`
|
||||
IP []string `json:"ip"`
|
||||
OutboundTag string `json:"outboundTag"`
|
||||
Type string `json:"type"`
|
||||
} `json:"rules"`
|
||||
@@ -815,6 +840,10 @@ func TestInjectAmneziawgV6Egress_CreatesOutboundAndRuleForV6Peer(t *testing.T) {
|
||||
len(rule.InboundTag) != 1 || rule.InboundTag[0] != "awg-7" {
|
||||
t.Fatalf("rule must match this peer's email and inbound tag, got %+v", rule)
|
||||
}
|
||||
// A v6 sendThrough cannot dial an IPv4 target, so only v6 destinations may take this outbound.
|
||||
if !slices.Equal(rule.IP, []string{"::/0"}) {
|
||||
t.Fatalf("rule must be limited to IPv6 destinations, got ip %v", rule.IP)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectAmneziawgV6Egress_SkipsPeerWithoutV6Address(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user