fix(waf): exclude internal IPs from automatic IP groups

This commit is contained in:
ryan
2026-10-16 00:02:09 +08:00
parent 0e1ce40cbb
commit 02a2b79c08
4 changed files with 88 additions and 2 deletions
+6
View File
@@ -15,6 +15,12 @@ sidebar: false
>3.5.1 版本解耦了日志存储,ClickHouse 变为可选项,如果想切换数据库, 点击 「任务管理」 -> 「切换日志数据库」任务,按提示迁移数据并切换主库。
>
## [Unreleased]
### 🛠 修复
- 修复 IP 组按规则自动抓取时会收录内网、回环及链路本地等地址的问题;规则测试同步过滤这些地址,历史抓取结果会在下次同步时清理。
## [v3.5.8] - 2026-10-08
### ✨ 新功能
+3 -1
View File
@@ -32,11 +32,13 @@
自动 IP 组先按单个客户端 IP 聚合指标,再对每个 IP 执行规则表达式:
1. Server 读取最近 `lookback` 时长内的请求日志。
2. 按 `remote_addr` 归一化后的 IP 分组。
2. 按 `remote_addr` 归一化后的 IP 分组,跳过 IPv4 私网、IPv6 唯一本地地址(ULA)、回环、链路本地、未指定、组播及广播地址;IPv4 映射 IPv6 地址按对应的 IPv4 地址判断。
3. 为每个 IP 计算请求数、404 数、直连 IP Host 次数等指标。
4. 逐个 IP 执行 `rules[].expr`。
5. 只要某个 IP 命中任意规则,就写入该自动 IP 组的 `IP / IP 段` 列表。
**测试规则**与实际自动抓取使用相同的地址过滤逻辑。自动 IP 组下次同步时,也会清理历史抓取结果中的上述地址,不受存活时间(TTL)限制。手动 IP 组和订阅 IP 组不受此过滤影响。
Host 是否为“通过 IP 访问”按请求日志中的 `Host` 字段判断:如果 Host 是 IPv4 或 IPv6 字面量,例如 `203.0.113.10`、`[2001:db8::10]`、`203.0.113.10:443`,就计入 `ip_host_count`。
## 可用关键字
+5 -1
View File
@@ -19,6 +19,7 @@ import (
"time"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/pkg/geoip/iputil"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/agent"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/websocket"
@@ -211,6 +212,9 @@ func syncIPGroupAutomatic(ctx context.Context, group *model.OpenFlareWAFIPGroup,
activeExtIPs := make([]ipGroupExtIP, 0, len(existingExtIPs))
for _, extIP := range existingExtIPs {
if !iputil.IsPublicString(extIP.IP) {
continue
}
if config.TTL > 0 {
expirationTime := extIP.CapturedAt.Add(time.Duration(config.TTL) * time.Second)
if expirationTime.Before(now) {
@@ -318,7 +322,7 @@ func evaluateParsedIPGroupAutoConfig(ctx context.Context, config ipGroupAutoConf
continue
}
ip, ok := normalizeIPLiteral(item.RemoteAddr)
if !ok {
if !ok || !iputil.IsPublicString(ip) {
continue
}
lastSeen := time.Time{}
@@ -8,6 +8,7 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"slices"
"testing"
"time"
@@ -151,6 +152,79 @@ func TestTestIPGroupAutoConfigReturnsMatchedIPs(t *testing.T) {
assert.Empty(t, want)
}
func TestIPGroupAutomaticExcludesInternalIPs(t *testing.T) {
t.Cleanup(setupIPGroupSyncTestDB(t))
ctx := context.Background()
now := time.Now().UTC()
internalIPs := []string{
"10.0.0.1", "172.16.0.1", "172.31.255.254", "192.168.1.1",
"127.0.0.1", "169.254.1.1", "0.0.0.0", "224.0.0.1", "255.255.255.255",
"fc00::1", "fd12:3456::1", "fe80::1", "::1", "::", "ff02::1",
"::ffff:192.168.1.1", "::ffff:127.0.0.1",
}
for _, ip := range internalIPs {
seedWAFAccessLogs(t, ctx, now, ip, "app.example.com", 1, 0)
}
seedWAFAccessLogs(t, ctx, now, "192.168.1.2:1234", "app.example.com", 1, 0)
seedWAFAccessLogs(t, ctx, now, "[fd00::2]:1234", "app.example.com", 1, 0)
publicIPs := []string{"1.1.1.1", "172.32.0.1", "2606:4700:4700::1111"}
for _, ip := range publicIPs {
seedWAFAccessLogs(t, ctx, now, ip, "app.example.com", 1, 0)
}
autoConfig := json.RawMessage(`{"lookback":"1h","ttl":-1,"rules":[{"expr":"request_count > 0"}]}`)
preview, err := TestIPGroupAutoConfig(ctx, IPGroupAutoTestInput{AutoConfig: autoConfig})
if err != nil {
t.Fatalf("TestIPGroupAutoConfig(%s) error = %v", autoConfig, err)
}
if !slices.Equal(preview.MatchedIPs, publicIPs) || preview.MatchedCount != len(publicIPs) {
t.Errorf("TestIPGroupAutoConfig(%s) matched = %v (count %d), want %v", autoConfig, preview.MatchedIPs, preview.MatchedCount, publicIPs)
}
view, err := CreateIPGroup(ctx, IPGroupInput{
Name: "automatic public IPs", Type: wafIPGroupTypeAutomatic, Enabled: true, AutoConfig: autoConfig,
})
if err != nil {
t.Fatalf("CreateIPGroup(automatic public IPs) error = %v", err)
}
group, err := repository.GetOpenFlareWAFIPGroupByID(ctx, view.ID)
if err != nil {
t.Fatalf("GetOpenFlareWAFIPGroupByID(%d) error = %v", view.ID, err)
}
capturedAt := now.Add(-24 * time.Hour)
extIPs := make([]ipGroupExtIP, 0, len(internalIPs)+1)
for _, ip := range internalIPs {
extIPs = append(extIPs, ipGroupExtIP{IP: ip, CapturedAt: capturedAt})
}
extIPs = append(extIPs, ipGroupExtIP{IP: "8.8.8.8", CapturedAt: capturedAt})
extJSON, err := json.Marshal(extIPs)
if err != nil {
t.Fatalf("json.Marshal(extIPs) error = %v", err)
}
group.ExtIPs = string(extJSON)
if err := repository.UpdateOpenFlareWAFIPGroup(ctx, group); err != nil {
t.Fatalf("UpdateOpenFlareWAFIPGroup(%d) error = %v", group.ID, err)
}
result, err := SyncIPGroup(ctx, group.ID)
if err != nil {
t.Fatalf("SyncIPGroup(%d) error = %v", group.ID, err)
}
wantIPs := append(slices.Clone(publicIPs), "8.8.8.8")
slices.Sort(wantIPs)
if !slices.Equal(result.Group.IPList, wantIPs) || result.IPCount != len(wantIPs) {
t.Errorf("SyncIPGroup(%d) IPs = %v (count %d), want %v", group.ID, result.Group.IPList, result.IPCount, wantIPs)
}
gotExtIPs := make([]string, 0, len(result.Group.ExtIPs))
for _, item := range result.Group.ExtIPs {
gotExtIPs = append(gotExtIPs, item.IP)
}
slices.Sort(gotExtIPs)
if !slices.Equal(gotExtIPs, wantIPs) {
t.Errorf("SyncIPGroup(%d) ExtIPs = %v, want %v", group.ID, gotExtIPs, wantIPs)
}
}
func TestListDueOpenFlareWAFIPGroups(t *testing.T) {
cleanup := setupIPGroupSyncTestDB(t)
defer cleanup()