mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-27 09:42:10 +03:00
fix(amneziawg): stop AAAA fallback on v4-only tunnels and expose I2–I5 (#6611)
* fix(amneziawg): stop AAAA fallback on v4-only tunnels and expose I2–I5 Gate tunnel DNS queries to address families the device can actually dial, reject undialable literal IPs early, and surface I2–I5 on the outbound form. Fixes #6570 * ci: retrigger frontend after npm registry maintenance The frontend job failed solely on `npm audit` while registry.npmjs.org returned 503 (Service Under Maintenance). Lint, typecheck, vitest, vite build, and storybook all passed. Local `npm audit --omit=dev --audit-level=high` now reports 0 vulnerabilities. * style(amneziawg): keep the tunnel DNS family comments to two lines CLAUDE.md caps a comment block at two lines. --------- Co-authored-by: mrchatam <mrchatam@users.noreply.github.com> Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
@@ -79,6 +79,10 @@ export default function AmneziawgFields() {
|
||||
<ObfText name="h3" label={t('pages.xray.amneziawg.h3')} placeholder="1700-2400" />
|
||||
<ObfText name="h4" label={t('pages.xray.amneziawg.h4')} placeholder="2500-3200" />
|
||||
<ObfText name="i1" label={t('pages.xray.amneziawg.i1')} placeholder="<r 64>" />
|
||||
<ObfText name="i2" label={t('pages.xray.amneziawg.i2')} placeholder="<r 64>" />
|
||||
<ObfText name="i3" label={t('pages.xray.amneziawg.i3')} placeholder="<r 64>" />
|
||||
<ObfText name="i4" label={t('pages.xray.amneziawg.i4')} placeholder="<r 64>" />
|
||||
<ObfText name="i5" label={t('pages.xray.amneziawg.i5')} placeholder="<r 64>" />
|
||||
<ObfText
|
||||
name="contentPaddingAddition"
|
||||
label={t('pages.xray.amneziawg.contentPaddingAddition')}
|
||||
|
||||
@@ -135,21 +135,42 @@ func flushTunnelDNSCacheForTag(tag string) {
|
||||
}
|
||||
}
|
||||
|
||||
// exchangeTunnelDNSWithFallback queries A and/or AAAA depending on the local
|
||||
// address families configured on the device stack.
|
||||
func exchangeTunnelDNSWithFallback(ctx context.Context, conn *gonet.UDPConn, addrs []netip.Addr, host string) (netip.Addr, error) {
|
||||
// dnsQueryTypesFor asks only for families the tunnel can dial, so a v4-only tunnel
|
||||
// never caches an unroutable AAAA answer (#6570). No addresses keeps A then AAAA.
|
||||
func dnsQueryTypesFor(addrs []netip.Addr) []dnsmessage.Type {
|
||||
hasV4 := deviceHasV4(addrs)
|
||||
hasV6 := deviceHasV6(addrs)
|
||||
|
||||
// If the tunnel is IPv6-only, query AAAA first; else query A first.
|
||||
types := []dnsmessage.Type{dnsmessage.TypeA, dnsmessage.TypeAAAA}
|
||||
if hasV6 && !hasV4 {
|
||||
types = []dnsmessage.Type{dnsmessage.TypeAAAA, dnsmessage.TypeA}
|
||||
switch {
|
||||
case hasV4 && hasV6:
|
||||
return []dnsmessage.Type{dnsmessage.TypeA, dnsmessage.TypeAAAA}
|
||||
case hasV6:
|
||||
return []dnsmessage.Type{dnsmessage.TypeAAAA}
|
||||
case hasV4:
|
||||
return []dnsmessage.Type{dnsmessage.TypeA}
|
||||
default:
|
||||
return []dnsmessage.Type{dnsmessage.TypeA, dnsmessage.TypeAAAA}
|
||||
}
|
||||
}
|
||||
|
||||
// tunnelSupportsAddr reports whether the device stack has a local address in
|
||||
// the same family as ip (IPv4-mapped IPv6 counts as IPv4).
|
||||
func tunnelSupportsAddr(addrs []netip.Addr, ip netip.Addr) bool {
|
||||
if !ip.IsValid() {
|
||||
return false
|
||||
}
|
||||
if ip.Is4() || ip.Is4In6() {
|
||||
return deviceHasV4(addrs)
|
||||
}
|
||||
return deviceHasV6(addrs)
|
||||
}
|
||||
|
||||
// exchangeTunnelDNSWithFallback returns the first answer among the families the
|
||||
// device stack can route.
|
||||
func exchangeTunnelDNSWithFallback(ctx context.Context, conn *gonet.UDPConn, addrs []netip.Addr, host string) (netip.Addr, error) {
|
||||
types := dnsQueryTypesFor(addrs)
|
||||
|
||||
var firstErr error
|
||||
for _, qType := range types {
|
||||
// Skip AAAA if device has no IPv6 capability and has IPv4, unless A failed.
|
||||
addr, err := exchangeTunnelDNSQuery(ctx, conn, host, qType)
|
||||
if err == nil {
|
||||
return addr, nil
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package amneziawgnet
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
)
|
||||
|
||||
func TestDNSQueryTypesFor(t *testing.T) {
|
||||
v4 := netip.MustParseAddr("10.8.0.2")
|
||||
v6 := netip.MustParseAddr("2001:db8::2")
|
||||
mapped := netip.MustParseAddr("::ffff:10.8.0.2")
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
addrs []netip.Addr
|
||||
want []dnsmessage.Type
|
||||
}{
|
||||
{
|
||||
name: "v4-only",
|
||||
addrs: []netip.Addr{v4},
|
||||
want: []dnsmessage.Type{dnsmessage.TypeA},
|
||||
},
|
||||
{
|
||||
name: "v6-only",
|
||||
addrs: []netip.Addr{v6},
|
||||
want: []dnsmessage.Type{dnsmessage.TypeAAAA},
|
||||
},
|
||||
{
|
||||
name: "dual-stack prefers A then AAAA",
|
||||
addrs: []netip.Addr{v4, v6},
|
||||
want: []dnsmessage.Type{dnsmessage.TypeA, dnsmessage.TypeAAAA},
|
||||
},
|
||||
{
|
||||
name: "empty falls back to A then AAAA",
|
||||
addrs: nil,
|
||||
want: []dnsmessage.Type{dnsmessage.TypeA, dnsmessage.TypeAAAA},
|
||||
},
|
||||
{
|
||||
name: "v4-mapped alone is not dual-stack",
|
||||
addrs: []netip.Addr{mapped},
|
||||
want: []dnsmessage.Type{dnsmessage.TypeA, dnsmessage.TypeAAAA},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := dnsQueryTypesFor(tc.addrs)
|
||||
if len(got) != len(tc.want) {
|
||||
t.Fatalf("dnsQueryTypesFor(%v) = %v, want %v", tc.addrs, got, tc.want)
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tc.want[i] {
|
||||
t.Fatalf("dnsQueryTypesFor(%v) = %v, want %v", tc.addrs, got, tc.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTunnelSupportsAddr(t *testing.T) {
|
||||
v4 := netip.MustParseAddr("10.8.0.2")
|
||||
v6 := netip.MustParseAddr("2001:db8::2")
|
||||
dest4 := netip.MustParseAddr("8.8.8.8")
|
||||
dest6 := netip.MustParseAddr("2001:4860:4860::8888")
|
||||
mappedDest := netip.MustParseAddr("::ffff:8.8.8.8")
|
||||
|
||||
if !tunnelSupportsAddr([]netip.Addr{v4}, dest4) {
|
||||
t.Error("v4 tunnel should dial IPv4")
|
||||
}
|
||||
if tunnelSupportsAddr([]netip.Addr{v4}, dest6) {
|
||||
t.Error("v4-only tunnel must not dial IPv6")
|
||||
}
|
||||
if !tunnelSupportsAddr([]netip.Addr{v6}, dest6) {
|
||||
t.Error("v6 tunnel should dial IPv6")
|
||||
}
|
||||
if tunnelSupportsAddr([]netip.Addr{v6}, dest4) {
|
||||
t.Error("v6-only tunnel must not dial IPv4")
|
||||
}
|
||||
if !tunnelSupportsAddr([]netip.Addr{v4, v6}, dest4) || !tunnelSupportsAddr([]netip.Addr{v4, v6}, dest6) {
|
||||
t.Error("dual-stack tunnel should dial both families")
|
||||
}
|
||||
if !tunnelSupportsAddr([]netip.Addr{v4}, mappedDest) {
|
||||
t.Error("v4 tunnel should treat IPv4-mapped destinations as IPv4")
|
||||
}
|
||||
if tunnelSupportsAddr(nil, dest4) {
|
||||
t.Error("empty address list should not claim support")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSocksTargetResolveTunnelVia_RejectsWrongFamilyLiteral(t *testing.T) {
|
||||
dev := &Device{localAddrs: []netip.Addr{netip.MustParseAddr("10.8.0.2")}}
|
||||
target := socksTarget{ip: netip.MustParseAddr("2001:4860:4860::8888"), port: 443}
|
||||
_, err := target.resolveTunnelVia("", "awg", dev)
|
||||
if err == nil {
|
||||
t.Fatal("expected error dialing IPv6 literal on v4-only tunnel")
|
||||
}
|
||||
|
||||
okTarget := socksTarget{ip: netip.MustParseAddr("8.8.8.8"), port: 443}
|
||||
got, err := okTarget.resolveTunnelVia("", "awg", dev)
|
||||
if err != nil {
|
||||
t.Fatalf("v4 literal on v4 tunnel: %v", err)
|
||||
}
|
||||
if got.String() != "8.8.8.8:443" {
|
||||
t.Fatalf("got %s", got)
|
||||
}
|
||||
}
|
||||
@@ -360,6 +360,9 @@ func (t socksTarget) String() string {
|
||||
// Domain targets resolve via the tunnel; reply-side helper must not be used here.
|
||||
func (t socksTarget) resolveTunnelVia(dnsServer, tag string, dev *Device) (netip.AddrPort, error) {
|
||||
if t.ip.IsValid() {
|
||||
if !tunnelSupportsAddr(dev.LocalAddresses(), t.ip) {
|
||||
return netip.AddrPort{}, fmt.Errorf("tunnel has no route for %s (device addresses %v)", t.ip, dev.LocalAddresses())
|
||||
}
|
||||
return netip.AddrPortFrom(t.ip, t.port), nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), tunnelResolveTimeout)
|
||||
@@ -523,6 +526,10 @@ func (s *udpEgressSessions) getOrDial(dev *Device, tag string, udpConn *net.UDPC
|
||||
if sess, ok := s.m[dst]; ok {
|
||||
return sess
|
||||
}
|
||||
if !tunnelSupportsAddr(dev.LocalAddresses(), dst.Addr()) {
|
||||
logger.Warningf("amneziawgnet: egress %q: dial udp %s: tunnel has no route for address family (device addresses %v)", tag, dst, dev.LocalAddresses())
|
||||
return nil
|
||||
}
|
||||
raddr := tcpip.FullAddress{
|
||||
NIC: 1,
|
||||
Addr: tcpip.AddrFromSlice(dst.Addr().AsSlice()),
|
||||
|
||||
Reference in New Issue
Block a user