mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-11-12 21:44:22 +08:00
fix(waf): exclude internal IPs from automatic IP groups
This commit is contained in:
@@ -15,6 +15,12 @@ sidebar: false
|
|||||||
>3.5.1 版本解耦了日志存储,ClickHouse 变为可选项,如果想切换数据库, 点击 「任务管理」 -> 「切换日志数据库」任务,按提示迁移数据并切换主库。
|
>3.5.1 版本解耦了日志存储,ClickHouse 变为可选项,如果想切换数据库, 点击 「任务管理」 -> 「切换日志数据库」任务,按提示迁移数据并切换主库。
|
||||||
>
|
>
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### 🛠 修复
|
||||||
|
|
||||||
|
- 修复 IP 组按规则自动抓取时会收录内网、回环及链路本地等地址的问题;规则测试同步过滤这些地址,历史抓取结果会在下次同步时清理。
|
||||||
|
|
||||||
## [v3.5.8] - 2026-10-08
|
## [v3.5.8] - 2026-10-08
|
||||||
|
|
||||||
### ✨ 新功能
|
### ✨ 新功能
|
||||||
|
|||||||
@@ -32,11 +32,13 @@
|
|||||||
自动 IP 组先按单个客户端 IP 聚合指标,再对每个 IP 执行规则表达式:
|
自动 IP 组先按单个客户端 IP 聚合指标,再对每个 IP 执行规则表达式:
|
||||||
|
|
||||||
1. Server 读取最近 `lookback` 时长内的请求日志。
|
1. Server 读取最近 `lookback` 时长内的请求日志。
|
||||||
2. 按 `remote_addr` 归一化后的 IP 分组。
|
2. 按 `remote_addr` 归一化后的 IP 分组,跳过 IPv4 私网、IPv6 唯一本地地址(ULA)、回环、链路本地、未指定、组播及广播地址;IPv4 映射 IPv6 地址按对应的 IPv4 地址判断。
|
||||||
3. 为每个 IP 计算请求数、404 数、直连 IP Host 次数等指标。
|
3. 为每个 IP 计算请求数、404 数、直连 IP Host 次数等指标。
|
||||||
4. 逐个 IP 执行 `rules[].expr`。
|
4. 逐个 IP 执行 `rules[].expr`。
|
||||||
5. 只要某个 IP 命中任意规则,就写入该自动 IP 组的 `IP / IP 段` 列表。
|
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`。
|
Host 是否为“通过 IP 访问”按请求日志中的 `Host` 字段判断:如果 Host 是 IPv4 或 IPv6 字面量,例如 `203.0.113.10`、`[2001:db8::10]`、`203.0.113.10:443`,就计入 `ip_host_count`。
|
||||||
|
|
||||||
## 可用关键字
|
## 可用关键字
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/repository"
|
"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/agent"
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/websocket"
|
"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))
|
activeExtIPs := make([]ipGroupExtIP, 0, len(existingExtIPs))
|
||||||
for _, extIP := range existingExtIPs {
|
for _, extIP := range existingExtIPs {
|
||||||
|
if !iputil.IsPublicString(extIP.IP) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if config.TTL > 0 {
|
if config.TTL > 0 {
|
||||||
expirationTime := extIP.CapturedAt.Add(time.Duration(config.TTL) * time.Second)
|
expirationTime := extIP.CapturedAt.Add(time.Duration(config.TTL) * time.Second)
|
||||||
if expirationTime.Before(now) {
|
if expirationTime.Before(now) {
|
||||||
@@ -318,7 +322,7 @@ func evaluateParsedIPGroupAutoConfig(ctx context.Context, config ipGroupAutoConf
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ip, ok := normalizeIPLiteral(item.RemoteAddr)
|
ip, ok := normalizeIPLiteral(item.RemoteAddr)
|
||||||
if !ok {
|
if !ok || !iputil.IsPublicString(ip) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
lastSeen := time.Time{}
|
lastSeen := time.Time{}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -151,6 +152,79 @@ func TestTestIPGroupAutoConfigReturnsMatchedIPs(t *testing.T) {
|
|||||||
assert.Empty(t, want)
|
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) {
|
func TestListDueOpenFlareWAFIPGroups(t *testing.T) {
|
||||||
cleanup := setupIPGroupSyncTestDB(t)
|
cleanup := setupIPGroupSyncTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|||||||
Reference in New Issue
Block a user