mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-25 04:17:15 +00:00
fix(panel): stop one poisoned DNS answer from blocking outbound tests
SanitizePublicHTTPURL rejected a hostname as soon as any single resolved
address was blocked, so a resolver returning a bogon AAAA for the test URL
host (e.g. 2001::1 for www.google.com, inside the Teredo range blocked
since b51f0976) failed the outbound Check button outright — including TCP
mode, which never uses the test URL. Mirror SSRFGuardedDialContext instead:
one usable address is enough, because the guarded dialer skips blocked
answers at connect time; a hostname with nothing usable is still refused.
Closes #6290
This commit is contained in:
@@ -72,15 +72,21 @@ func rejectPrivateHost(ctx context.Context, hostname string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot resolve host %s: %w", hostname, err)
|
return fmt.Errorf("cannot resolve host %s: %w", hostname, err)
|
||||||
}
|
}
|
||||||
|
return rejectAllBlockedIPs(hostname, ips)
|
||||||
|
}
|
||||||
|
|
||||||
|
// One usable address is enough — SSRFGuardedDialContext skips blocked ones at
|
||||||
|
// dial time, so a poisoned AAAA answer must not veto a healthy hostname.
|
||||||
|
func rejectAllBlockedIPs(hostname string, ips []net.IPAddr) error {
|
||||||
if len(ips) == 0 {
|
if len(ips) == 0 {
|
||||||
return fmt.Errorf("host %s has no IP addresses", hostname)
|
return fmt.Errorf("host %s has no IP addresses", hostname)
|
||||||
}
|
}
|
||||||
for _, ipAddr := range ips {
|
for _, ipAddr := range ips {
|
||||||
if isBlockedIP(ipAddr.IP) {
|
if !isBlockedIP(ipAddr.IP) {
|
||||||
return fmt.Errorf("host %s resolves to blocked private/internal address %s", hostname, ipAddr.IP.String())
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return fmt.Errorf("host %s resolves to blocked private/internal address %s", hostname, ips[0].IP.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func isBlockedIP(ip net.IP) bool {
|
func isBlockedIP(ip net.IP) bool {
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRejectAllBlockedIPsNeedsOnlyOneUsableAddress(t *testing.T) {
|
||||||
|
teredo := net.IPAddr{IP: net.ParseIP("2001::1")}
|
||||||
|
public4 := net.IPAddr{IP: net.ParseIP("142.250.74.36")}
|
||||||
|
private4 := net.IPAddr{IP: net.ParseIP("10.0.0.1")}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
ips []net.IPAddr
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{"poisoned AAAA next to healthy A", []net.IPAddr{teredo, public4}, ""},
|
||||||
|
{"all blocked", []net.IPAddr{teredo, private4}, "host h.example resolves to blocked private/internal address 2001::1"},
|
||||||
|
{"no addresses", nil, "host h.example has no IP addresses"},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := rejectAllBlockedIPs("h.example", tc.ips)
|
||||||
|
if tc.wantErr == "" {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("rejectAllBlockedIPs() = %v, want nil", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err == nil || err.Error() != tc.wantErr {
|
||||||
|
t.Fatalf("rejectAllBlockedIPs() = %v, want %q", err, tc.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user