mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-20 18:11:00 +00:00
b51f09768b
IsBlockedIP leaned entirely on Go's net.IP predicates, which judge an address by its own range only. 6to4 (2002::/16), NAT64 (64:ff9b::/96 and 64:ff9b:1::/48) and Teredo (2001::/32) each tunnel an arbitrary IPv4 destination inside an IPv6 address, so all five predicates returned false for e.g. 64:ff9b::7f00:1 and the SSRF guard waved it through. CGNAT (100.64.0.0/10) and the deprecated site-local block were unclassified for the same reason. Reported as GHSA-cfpf-wmjp-gh6c. Reaching the embedded IPv4 needs a 6to4 tunnel, NAT64 gateway or Teredo client on the host, none of which exist by default, so this is hardening rather than a live path off a stock install. The guard backs outbound subscription fetches, node sync, reality scan, the tgbot API URL and the xray setting test URL, which is reason enough to close the gap. The deprecated and local-use prefixes are blocked outright since nothing public routes through them. The NAT64 well-known prefix is judged by the IPv4 it embeds instead: on a DNS64 network every public IPv4 host resolves into it, so blocking it wholesale would break legitimate fetches.
141 lines
4.1 KiB
Go
141 lines
4.1 KiB
Go
package netsafe
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestIsBlockedIP(t *testing.T) {
|
|
cases := []struct {
|
|
ip string
|
|
want bool
|
|
}{
|
|
{"127.0.0.1", true},
|
|
{"::1", true},
|
|
{"10.0.0.5", true},
|
|
{"172.16.0.1", true},
|
|
{"192.168.1.1", true},
|
|
{"169.254.0.1", true},
|
|
{"0.0.0.0", true},
|
|
{"::", true},
|
|
{"8.8.8.8", false},
|
|
{"1.1.1.1", false},
|
|
{"2606:4700:4700::1111", false},
|
|
// IPv6 transition prefixes tunnel an arbitrary IPv4 destination that
|
|
// Go's net.IP predicates do not see through (GHSA-cfpf-wmjp-gh6c).
|
|
{"2002:7f00:0001::1", true}, // 6to4 -> 127.0.0.1
|
|
{"2002:a9fe:a9fe::1", true}, // 6to4 -> 169.254.169.254
|
|
{"64:ff9b::7f00:1", true}, // NAT64 well-known -> 127.0.0.1
|
|
{"64:ff9b::a9fe:a9fe", true}, // NAT64 well-known -> 169.254.169.254
|
|
{"64:ff9b:1::a9fe:a9fe", true}, // NAT64 local-use
|
|
{"2001:0:dead:beef::80ff:fffe", true}, // Teredo -> 127.0.0.1
|
|
{"100.64.0.1", true}, // CGNAT
|
|
{"::ffff:100.64.0.1", true}, // CGNAT via 4-in-6
|
|
{"fec0::1", true}, // site-local
|
|
{"64:ff9b::8.8.8.8", false}, // NAT64 to a public host stays reachable
|
|
{"2001:db8::1", false}, // documentation prefix is not Teredo
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.ip, func(t *testing.T) {
|
|
ip := net.ParseIP(c.ip)
|
|
if ip == nil {
|
|
t.Fatalf("could not parse %q", c.ip)
|
|
}
|
|
if got := IsBlockedIP(ip); got != c.want {
|
|
t.Fatalf("IsBlockedIP(%s) = %v, want %v", c.ip, got, c.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAllowPrivateFromContext_Default(t *testing.T) {
|
|
if AllowPrivateFromContext(context.Background()) {
|
|
t.Fatal("default context should report AllowPrivate=false")
|
|
}
|
|
}
|
|
|
|
func TestAllowPrivateFromContext_RoundTrip(t *testing.T) {
|
|
ctx := ContextWithAllowPrivate(context.Background(), true)
|
|
if !AllowPrivateFromContext(ctx) {
|
|
t.Fatal("expected AllowPrivate=true after ContextWithAllowPrivate(true)")
|
|
}
|
|
ctx = ContextWithAllowPrivate(ctx, false)
|
|
if AllowPrivateFromContext(ctx) {
|
|
t.Fatal("expected AllowPrivate=false after overriding with false")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeHost_Valid(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
want string
|
|
}{
|
|
{"example.com", "example.com"},
|
|
{" example.com ", "example.com"},
|
|
{"a.b.c.example.com", "a.b.c.example.com"},
|
|
{"10.0.0.1", "10.0.0.1"},
|
|
{"[2606:4700:4700::1111]", "2606:4700:4700::1111"},
|
|
{"2606:4700:4700::1111", "2606:4700:4700::1111"},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.in, func(t *testing.T) {
|
|
got, err := NormalizeHost(c.in)
|
|
if err != nil {
|
|
t.Fatalf("NormalizeHost(%q) returned error: %v", c.in, err)
|
|
}
|
|
if !strings.EqualFold(got, c.want) {
|
|
t.Fatalf("NormalizeHost(%q) = %q, want %q", c.in, got, c.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNormalizeHost_Invalid(t *testing.T) {
|
|
cases := []string{
|
|
"",
|
|
" ",
|
|
"-leading-dash.com",
|
|
"trailing-dash-.com",
|
|
"bad host with spaces",
|
|
"under_score.example.com",
|
|
"exa$mple.com",
|
|
strings.Repeat("a", 254),
|
|
}
|
|
for _, in := range cases {
|
|
t.Run(in, func(t *testing.T) {
|
|
if _, err := NormalizeHost(in); err == nil {
|
|
t.Fatalf("NormalizeHost(%q) expected error, got nil", in)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSSRFGuardedDialContext_BlocksLiteralPrivateIP(t *testing.T) {
|
|
_, err := SSRFGuardedDialContext(context.Background(), "tcp", "127.0.0.1:1")
|
|
if err == nil {
|
|
t.Fatal("expected dial to 127.0.0.1 to be blocked")
|
|
}
|
|
if !strings.Contains(err.Error(), "blocked") {
|
|
t.Fatalf("expected 'blocked' in error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSSRFGuardedDialContext_AllowPrivateBypassesGuard(t *testing.T) {
|
|
ctx := ContextWithAllowPrivate(context.Background(), true)
|
|
_, err := SSRFGuardedDialContext(ctx, "tcp", "127.0.0.1:1")
|
|
if err == nil {
|
|
t.Fatal("dial to a closed loopback port should still fail at the connect step")
|
|
}
|
|
if strings.Contains(err.Error(), "blocked private/internal address") {
|
|
t.Fatalf("expected guard to be bypassed when AllowPrivate=true, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSSRFGuardedDialContext_BadAddress(t *testing.T) {
|
|
if _, err := SSRFGuardedDialContext(context.Background(), "tcp", "no-port"); err == nil {
|
|
t.Fatal("expected error for address without port")
|
|
}
|
|
}
|