mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-24 23:05:01 +03:00
40cea0c2fe
Operators who front a server with an IP range (a CDN or anycast prefix) had to list each address as its own host. A host address may now be a CIDR, and every subscription fetch expands it into up to randomCount (1–100, default 1) unique random host IPs from the range, IPv4 and IPv6 alike, skipping the network and broadcast addresses. The panel's own previews, QR codes and tunnel config exports (WireGuard, AmneziaWG, TUIC, MTProto) expand it the same way, and the externalProxy importer writes the model's default count so the re-sync still recognises untouched imported hosts.
176 lines
4.5 KiB
Go
176 lines
4.5 KiB
Go
package sub
|
|
|
|
import (
|
|
"net"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestIsCIDR(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
want bool
|
|
}{
|
|
{input: "192.168.1.0/24", want: true},
|
|
{input: "10.0.0.0/8", want: true},
|
|
{input: "[192.168.1.0/24]", want: true},
|
|
{input: "2a01:4f8:1c1b:729::/64", want: true},
|
|
{input: "[2a01:4f8:1c1b:729::/64]", want: true},
|
|
{input: "[2001:db8::]/64", want: true},
|
|
{input: "fe80::/10", want: true},
|
|
{input: "example.com", want: false},
|
|
{input: "192.168.1.1", want: false},
|
|
{input: "2001:db8::1", want: false},
|
|
{input: "", want: false},
|
|
{input: "invalid/33", want: false},
|
|
{input: "999.999.999.999/24", want: false},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.input, func(t *testing.T) {
|
|
if got := IsCIDR(tc.input); got != tc.want {
|
|
t.Fatalf("IsCIDR(%q) = %v, want %v", tc.input, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRandomIPsFromCIDR_IPv4(t *testing.T) {
|
|
t.Run("single host /32", func(t *testing.T) {
|
|
ips, err := RandomIPsFromCIDR("192.0.2.1/32", 3)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(ips) != 1 || ips[0] != "192.0.2.1" {
|
|
t.Fatalf("RandomIPsFromCIDR(/32) = %v, want [192.0.2.1]", ips)
|
|
}
|
|
})
|
|
|
|
t.Run("small subnet /30 caps count", func(t *testing.T) {
|
|
ips, err := RandomIPsFromCIDR("198.51.100.0/30", 5)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(ips) != 2 {
|
|
t.Fatalf("len(ips) = %d, want 2", len(ips))
|
|
}
|
|
for _, ipStr := range ips {
|
|
if ipStr == "198.51.100.0" || ipStr == "198.51.100.3" {
|
|
t.Fatalf("got network or broadcast address: %s", ipStr)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("subnet /24 generates unique IPs within range", func(t *testing.T) {
|
|
cidr := "203.0.113.0/24"
|
|
_, ipNet, err := net.ParseCIDR(cidr)
|
|
if err != nil {
|
|
t.Fatalf("parse cidr: %v", err)
|
|
}
|
|
|
|
ips, err := RandomIPsFromCIDR(cidr, 10)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(ips) != 10 {
|
|
t.Fatalf("len(ips) = %d, want 10", len(ips))
|
|
}
|
|
|
|
seen := make(map[string]struct{}, len(ips))
|
|
for _, ipStr := range ips {
|
|
if _, dup := seen[ipStr]; dup {
|
|
t.Fatalf("duplicate IP emitted: %s", ipStr)
|
|
}
|
|
seen[ipStr] = struct{}{}
|
|
|
|
parsed := net.ParseIP(ipStr)
|
|
if parsed == nil || !ipNet.Contains(parsed) {
|
|
t.Fatalf("IP %s outside expected subnet %s", ipStr, cidr)
|
|
}
|
|
if strings.HasSuffix(ipStr, ".0") || strings.HasSuffix(ipStr, ".255") {
|
|
t.Fatalf("unusable host IP picked: %s", ipStr)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRandomIPsFromCIDR_IPv6(t *testing.T) {
|
|
t.Run("single host /128", func(t *testing.T) {
|
|
ips, err := RandomIPsFromCIDR("2001:db8::1/128", 4)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(ips) != 1 || ips[0] != "2001:db8::1" {
|
|
t.Fatalf("RandomIPsFromCIDR(/128) = %v, want [2001:db8::1]", ips)
|
|
}
|
|
})
|
|
|
|
t.Run("standard /64 datacenter block", func(t *testing.T) {
|
|
cidr := "2a01:4f8:1c1b:729::/64"
|
|
_, ipNet, err := net.ParseCIDR(cidr)
|
|
if err != nil {
|
|
t.Fatalf("parse cidr: %v", err)
|
|
}
|
|
|
|
ips, err := RandomIPsFromCIDR(cidr, 10)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(ips) != 10 {
|
|
t.Fatalf("len(ips) = %d, want 10", len(ips))
|
|
}
|
|
|
|
seen := make(map[string]struct{}, len(ips))
|
|
for _, ipStr := range ips {
|
|
if _, dup := seen[ipStr]; dup {
|
|
t.Fatalf("duplicate IP emitted: %s", ipStr)
|
|
}
|
|
seen[ipStr] = struct{}{}
|
|
|
|
parsed := net.ParseIP(ipStr)
|
|
if parsed == nil || !ipNet.Contains(parsed) {
|
|
t.Fatalf("IP %s outside expected subnet %s", ipStr, cidr)
|
|
}
|
|
if ipStr == "2a01:4f8:1c1b:729::" {
|
|
t.Fatalf("subnet-router anycast address was emitted")
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("bracketed cidr notation", func(t *testing.T) {
|
|
ips, err := RandomIPsFromCIDR("[2a01:4f8:1c1b:729::/64]", 3)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(ips) != 3 {
|
|
t.Fatalf("len(ips) = %d, want 3", len(ips))
|
|
}
|
|
})
|
|
|
|
t.Run("bracketed host form /64", func(t *testing.T) {
|
|
ips, err := RandomIPsFromCIDR("[2001:db8::]/64", 3)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(ips) != 3 {
|
|
t.Fatalf("len(ips) = %d, want 3", len(ips))
|
|
}
|
|
})
|
|
|
|
t.Run("unbounded count clamped to MaxRandomCIDRCount", func(t *testing.T) {
|
|
ips, err := RandomIPsFromCIDR("2001:db8::/64", 1000)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(ips) != MaxRandomCIDRCount {
|
|
t.Fatalf("len(ips) = %d, want %d", len(ips), MaxRandomCIDRCount)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRandomIPsFromCIDR_Invalid(t *testing.T) {
|
|
if _, err := RandomIPsFromCIDR("example.com", 1); err == nil {
|
|
t.Fatalf("RandomIPsFromCIDR(domain) expected error, got nil")
|
|
}
|
|
}
|