mirror of
https://gitee.com/samwaf/SamWaf.git
synced 2026-11-02 19:21:44 +08:00
315 lines
9.6 KiB
Go
315 lines
9.6 KiB
Go
package ipset
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"math/rand"
|
|
"net"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestParsePattern_Valid(t *testing.T) {
|
|
cases := []struct {
|
|
raw string
|
|
wantKind PatternKind
|
|
wantWidth int
|
|
wantPrefix int
|
|
}{
|
|
{"1.2.3.4", KindSingle, 4, 32},
|
|
{"2001:db8::1", KindSingle, 16, 128},
|
|
{"1.2.3.0/24", KindCIDR, 4, 24},
|
|
{"2001:db8::/32", KindCIDR, 16, 32},
|
|
{"10.10.*.*", KindWildcard, 4, 16}, // 尾部通配 → 可降级 CIDR
|
|
{"10.*.*.*", KindWildcard, 4, 8}, // 等价 10.0.0.0/8
|
|
{"10.*.1.*", KindWildcard, 4, -1}, // 掩码不连续 → 线性表
|
|
{"*.*.*.*", KindWildcard, 4, 0}, // 全通配:语法合法,写入侧另行拒绝
|
|
{"192.168.1.*", KindWildcard, 4, 24}, //
|
|
{"2001:db8:*:*:*:*:*:*", KindWildcard, 16, 32}, //
|
|
{"1.2.3.4-1.2.3.99", KindRange, 4, -1}, //
|
|
{"2001:db8::1-2001:db8::ff", KindRange, 16, -1},
|
|
{"1.2.3.4-1.2.3.4", KindSingle, 4, 32}, // 起止相同 → 退化单 IP
|
|
{" 1.2.3.4 ", KindSingle, 4, 32}, // 两端空白
|
|
}
|
|
for _, c := range cases {
|
|
p, err := ParsePattern(c.raw)
|
|
if err != nil {
|
|
t.Errorf("ParsePattern(%q) 意外报错: %v", c.raw, err)
|
|
continue
|
|
}
|
|
if p.Kind != c.wantKind || p.Width != c.wantWidth || p.Prefix != c.wantPrefix {
|
|
t.Errorf("ParsePattern(%q) = kind=%d width=%d prefix=%d, want kind=%d width=%d prefix=%d",
|
|
c.raw, p.Kind, p.Width, p.Prefix, c.wantKind, c.wantWidth, c.wantPrefix)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParsePattern_Invalid(t *testing.T) {
|
|
cases := []string{
|
|
"", // 空
|
|
" ", // 全空白
|
|
"not-an-ip", // 含 '-' 走区间分支,两端都不是 IP
|
|
"5.6.7.0/33", // 非法掩码
|
|
"2001:db8::*", // :: 与 * 混用
|
|
"2001:db8:*:*", // IPv6 通配段数不足
|
|
"2001:db8:*:*:*:*:*:*:*", // IPv6 通配段数超出
|
|
"::ffff:10.10.*.*", // 内嵌 IPv4 + ::
|
|
"10.*.*.*/8", // 通配符带掩码
|
|
"10.1*.0.0", // 段内部分通配
|
|
"10.10.*", // IPv4 通配段数不足
|
|
"10.10.*.*.*", // IPv4 通配段数超出
|
|
"10.256.*.*", // 段超出 0-255
|
|
"10.010.*.*", // 前导零
|
|
"1.2.3.4-::1", // 跨族区间
|
|
"1.2.3.*-1.2.3.9", // 区间内含通配符
|
|
"1.2.3.0/24-1.2.3.9", // 区间内含掩码
|
|
"-1.2.3.4", // 区间缺起点
|
|
"1.2.3.4-", // 区间缺终点
|
|
}
|
|
for _, raw := range cases {
|
|
if _, err := ParsePattern(raw); err == nil {
|
|
t.Errorf("ParsePattern(%q) 应当报错,但通过了", raw)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 区间起止颠倒:严格解析必须报错(挡住新数据),宽容解析自动交换(存量数据仍生效)
|
|
func TestParsePattern_RangeReversed(t *testing.T) {
|
|
const raw = "1.2.3.99-1.2.3.4"
|
|
|
|
_, err := ParsePattern(raw)
|
|
if err == nil {
|
|
t.Fatal("ParsePattern 对颠倒区间应当报错")
|
|
}
|
|
if !errors.Is(err, ErrRangeReversed) {
|
|
t.Fatalf("错误应当可被 errors.Is(ErrRangeReversed) 识别,实际: %v", err)
|
|
}
|
|
|
|
p, err := ParsePatternLenient(raw)
|
|
if err != nil {
|
|
t.Fatalf("ParsePatternLenient 应当自动交换后成功,实际: %v", err)
|
|
}
|
|
if p.Kind != KindRange {
|
|
t.Fatalf("交换后应为区间,实际 kind=%d", p.Kind)
|
|
}
|
|
|
|
// MatchSet.Add 走宽容解析,颠倒的存量数据必须继续生效
|
|
m := BuildMatchSet([]string{raw})
|
|
if !m.Contains(net.ParseIP("1.2.3.50")) {
|
|
t.Error("颠倒区间经自动交换后应当命中区间内地址")
|
|
}
|
|
if m.Contains(net.ParseIP("1.2.3.200")) {
|
|
t.Error("颠倒区间不应命中区间外地址")
|
|
}
|
|
}
|
|
|
|
// 连续掩码通配符与等价 CIDR 必须语义完全一致
|
|
func TestWildcardCIDREquivalence(t *testing.T) {
|
|
pairs := []struct{ wildcard, cidr string }{
|
|
{"10.*.*.*", "10.0.0.0/8"},
|
|
{"10.10.*.*", "10.10.0.0/16"},
|
|
{"192.168.1.*", "192.168.1.0/24"},
|
|
{"2001:db8:*:*:*:*:*:*", "2001:db8::/32"},
|
|
}
|
|
rnd := rand.New(rand.NewSource(20260803))
|
|
for _, pair := range pairs {
|
|
mw := BuildMatchSet([]string{pair.wildcard})
|
|
mc := BuildMatchSet([]string{pair.cidr})
|
|
for i := 0; i < 200; i++ {
|
|
var ip net.IP
|
|
if strings.Contains(pair.cidr, ":") {
|
|
b := make([]byte, 16)
|
|
rnd.Read(b)
|
|
if i%2 == 0 { // 一半构造成命中前缀
|
|
b[0], b[1], b[2], b[3] = 0x20, 0x01, 0x0d, 0xb8
|
|
}
|
|
ip = net.IP(b)
|
|
} else {
|
|
b := make([]byte, 4)
|
|
rnd.Read(b)
|
|
if i%2 == 0 { // 一半构造成命中前缀
|
|
switch pair.cidr {
|
|
case "192.168.1.0/24":
|
|
b[0], b[1], b[2] = 192, 168, 1
|
|
case "10.10.0.0/16":
|
|
b[0], b[1] = 10, 10
|
|
default:
|
|
b[0] = 10
|
|
}
|
|
}
|
|
ip = net.IP(b)
|
|
}
|
|
if mw.Contains(ip) != mc.Contains(ip) {
|
|
t.Fatalf("%s 与 %s 对 %s 判定不一致: %v vs %v",
|
|
pair.wildcard, pair.cidr, ip, mw.Contains(ip), mc.Contains(ip))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 区间→CIDR 分解必须与朴素的字节比较判定逐地址一致,且条数在可证明上界内
|
|
func TestRangeToPrefixes_Exact(t *testing.T) {
|
|
cases := []struct{ start, end string }{
|
|
{"1.2.3.4", "1.2.3.99"},
|
|
{"10.0.0.0", "10.0.1.255"},
|
|
{"192.168.0.1", "192.168.0.1"},
|
|
{"0.0.0.0", "255.255.255.255"},
|
|
{"1.2.3.255", "1.2.4.0"},
|
|
}
|
|
for _, c := range cases {
|
|
start := net.ParseIP(c.start).To4()
|
|
end := net.ParseIP(c.end).To4()
|
|
prefixes := rangeToPrefixes(start, end)
|
|
if len(prefixes) == 0 {
|
|
t.Fatalf("[%s,%s] 分解结果为空", c.start, c.end)
|
|
}
|
|
if len(prefixes) > 62 {
|
|
t.Errorf("[%s,%s] 分解出 %d 条,超过 IPv4 上界 62", c.start, c.end, len(prefixes))
|
|
}
|
|
|
|
trie := newCIDRTrie()
|
|
for _, pe := range prefixes {
|
|
trie.insert(pe.Net, pe.Bits)
|
|
}
|
|
// 在区间内外各抽样验证;小区间直接全量枚举
|
|
for i := 0; i < 3000; i++ {
|
|
b := make([]byte, 4)
|
|
rand.New(rand.NewSource(int64(i) * 7919)).Read(b)
|
|
want := bytes.Compare(b, start) >= 0 && bytes.Compare(b, end) <= 0
|
|
if got := trie.contains(b, 32); got != want {
|
|
t.Fatalf("[%s,%s] 对 %v 判定错误: got=%v want=%v", c.start, c.end, net.IP(b), got, want)
|
|
}
|
|
}
|
|
// 边界四点必须精确
|
|
for _, probe := range []struct {
|
|
ip []byte
|
|
want bool
|
|
}{
|
|
{start, true},
|
|
{end, true},
|
|
{decOne(start), false},
|
|
{incOne(end), false},
|
|
} {
|
|
if probe.ip == nil {
|
|
continue
|
|
}
|
|
if got := trie.contains(probe.ip, 32); got != probe.want {
|
|
t.Errorf("[%s,%s] 边界 %v 判定错误: got=%v want=%v", c.start, c.end, net.IP(probe.ip), got, probe.want)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRangeToPrefixes_V6Bound(t *testing.T) {
|
|
start := net.ParseIP("::1").To16()
|
|
end := net.ParseIP("ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe").To16()
|
|
prefixes := rangeToPrefixes(start, end)
|
|
if len(prefixes) == 0 {
|
|
t.Fatal("v6 极端区间分解结果为空")
|
|
}
|
|
if len(prefixes) > 254 {
|
|
t.Errorf("v6 分解出 %d 条,超过上界 254", len(prefixes))
|
|
}
|
|
trie := newCIDRTrie()
|
|
for _, pe := range prefixes {
|
|
trie.insert(pe.Net, pe.Bits)
|
|
}
|
|
if !trie.contains(net.ParseIP("2001:db8::1").To16(), 128) {
|
|
t.Error("区间内的 v6 地址应当命中")
|
|
}
|
|
if trie.contains(net.ParseIP("::").To16(), 128) {
|
|
t.Error("区间外的 :: 不应命中")
|
|
}
|
|
}
|
|
|
|
func TestPattern_Match_FamilyIsolation(t *testing.T) {
|
|
p4, _ := ParsePattern("10.10.*.*")
|
|
if p4.Match(net.ParseIP("2001:db8::1")) {
|
|
t.Error("v4 通配符不应命中 v6 地址")
|
|
}
|
|
p6, _ := ParsePattern("2001:db8:*:*:*:*:*:*")
|
|
if p6.Match(net.ParseIP("10.10.1.1")) {
|
|
t.Error("v6 通配符不应命中 v4 地址")
|
|
}
|
|
if p4.Match(nil) {
|
|
t.Error("nil ip 应当返回 false")
|
|
}
|
|
}
|
|
|
|
// 「会匹配全部地址」的隐晦写法必须能被识别出来:
|
|
// 白名单误写成 *.*.*.* 等于全站不设防,黑名单误写成它等于封禁所有人。
|
|
func TestIsCatchAllWildcard(t *testing.T) {
|
|
cases := map[string]bool{
|
|
"*.*.*.*": true,
|
|
"*:*:*:*:*:*:*:*": true,
|
|
"0.0.0.0-255.255.255.255": true, // 全空间区间,与全通配同等风险
|
|
"::-ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff": true,
|
|
"10.*.*.*": false,
|
|
"0.0.0.0/0": false, // 显式 CIDR:用户明确表达了全匹配,不拦
|
|
"::/0": false,
|
|
"1.2.3.4-1.2.3.99": false,
|
|
"0.0.0.0-0.0.0.255": false,
|
|
"not-an-ip": false,
|
|
"10.10.*.*": false,
|
|
}
|
|
for raw, want := range cases {
|
|
if got := IsCatchAllWildcard(raw); got != want {
|
|
t.Errorf("IsCatchAllWildcard(%q) = %v, want %v", raw, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMatchPatternCached(t *testing.T) {
|
|
cases := []struct {
|
|
ip, pattern string
|
|
want bool
|
|
}{
|
|
{"1.2.3.4", "1.2.3.4", true},
|
|
{"1.2.3.5", "1.2.3.4", false},
|
|
{"10.0.5.9", "10.0.0.0/8", true},
|
|
{"11.0.5.9", "10.0.0.0/8", false},
|
|
{"10.10.7.7", "10.10.*.*", true},
|
|
{"10.11.7.7", "10.10.*.*", false},
|
|
{"10.5.1.9", "10.*.1.*", true},
|
|
{"10.5.2.9", "10.*.1.*", false},
|
|
{"1.2.3.50", "1.2.3.4-1.2.3.99", true},
|
|
{"1.2.3.100", "1.2.3.4-1.2.3.99", false},
|
|
{"2001:db8::1", "2001:db8:*:*:*:*:*:*", true},
|
|
{"2001:db9::1", "2001:db8:*:*:*:*:*:*", false},
|
|
{"1.2.3.4", "垃圾模式", false}, // 非法 pattern 恒 false
|
|
{"垃圾IP", "1.2.3.4", false}, // 非法 ip 恒 false
|
|
}
|
|
for _, c := range cases {
|
|
// 连续调两次,验证缓存路径与首次解析路径结果一致
|
|
for i := 0; i < 2; i++ {
|
|
if got := MatchPatternStrCached(c.ip, c.pattern); got != c.want {
|
|
t.Errorf("MatchPatternStrCached(%q,%q) 第%d次 = %v, want %v", c.ip, c.pattern, i+1, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func decOne(b []byte) []byte {
|
|
out := append([]byte(nil), b...)
|
|
for i := len(out) - 1; i >= 0; i-- {
|
|
if out[i] > 0 {
|
|
out[i]--
|
|
return out
|
|
}
|
|
out[i] = 0xff
|
|
}
|
|
return nil // 全 0,无前驱
|
|
}
|
|
|
|
func incOne(b []byte) []byte {
|
|
out := append([]byte(nil), b...)
|
|
for i := len(out) - 1; i >= 0; i-- {
|
|
if out[i] < 0xff {
|
|
out[i]++
|
|
return out
|
|
}
|
|
out[i] = 0
|
|
}
|
|
return nil // 全 F,无后继
|
|
}
|