[优化] 添加 WAF IP 组功能,包括 CRUD 接口和前端页面支持

This commit is contained in:
ryan
2026-06-01 08:46:24 +08:00
parent d5f46138d5
commit 95d7045b4a
32 changed files with 2214 additions and 44 deletions
+1
View File
@@ -46,6 +46,7 @@ func registeredModels() []any {
&AcmeAccount{},
&DnsAccount{},
&WAFRuleGroup{},
&WAFIPGroup{},
&WAFRuleGroupBinding{},
}
}
+35
View File
@@ -184,6 +184,41 @@ func TestRegisterShardingAutoMigratesShardTables(t *testing.T) {
}
}
func TestUpgradeDatabaseSchemaV15ToV16AddsWAFIPGroups(t *testing.T) {
db := openBareTestSQLiteDB(t, "v16.db")
if err := registerSharding(db, "sqlite"); err != nil {
t.Fatalf("register sharding: %v", err)
}
if err := autoMigrateSchemaMetadata(db); err != nil {
t.Fatalf("auto migrate schema metadata: %v", err)
}
if err := applyCurrentSchema(db, "sqlite"); err != nil {
t.Fatalf("apply current schema: %v", err)
}
if err := ensureDefaultWAFRuleGroup(db); err != nil {
t.Fatalf("ensure default waf rule group: %v", err)
}
if err := saveDatabaseSchemaVersion(db, 15); err != nil {
t.Fatalf("save schema version: %v", err)
}
if err := upgradeDatabaseSchema(db, "sqlite", 15); err != nil {
t.Fatalf("upgrade schema: %v", err)
}
if !db.Migrator().HasTable(&WAFIPGroup{}) {
t.Fatal("expected waf_ip_groups table")
}
if !db.Migrator().HasColumn(&WAFRuleGroup{}, "ip_whitelist_groups") {
t.Fatal("expected waf_rule_groups.ip_whitelist_groups column")
}
version, ok, err := loadDatabaseSchemaVersion(db)
if err != nil {
t.Fatalf("load schema version: %v", err)
}
if !ok || version != currentDatabaseSchemaVersion {
t.Fatalf("unexpected schema version: got %d ok=%v want %d", version, ok, currentDatabaseSchemaVersion)
}
}
func TestMigrateObservabilityLegacyColumnsBackfillsHealthEventMetadata(t *testing.T) {
db := openTestSQLiteDB(t, "legacy-health-events.db")
-3
View File
@@ -1,6 +1,3 @@
// v16 升级内容:新增 Tunnel 隧道表,为 nodes 增加 TunnelRelay 相关字段,为 proxy_routes 增加隧道上游支持字段。
// 背景说明:引入 Tunnel 组件后,边缘节点可通过隧道中继回源,需要区分节点类型(edge_node/relay_node)、记录中继绑定端口,
// 并在代理路由中支持 tunnel 上游类型以替代直连回源,同时需要独立的 tunnels 表管理隧道实例。
package migrate
import (
+55
View File
@@ -0,0 +1,55 @@
package migrate
import (
"fmt"
"gorm.io/gorm"
)
type wafIPGroupV17 struct{}
type wafRuleGroupV17 struct {
IPWhitelistGroups string `gorm:"column:ip_whitelist_groups;type:text;not null;default:'[]'"`
IPBlacklistGroups string `gorm:"column:ip_blacklist_groups;type:text;not null;default:'[]'"`
}
func init() {
Register(V17())
}
func V17() Migration {
return Migration{
FromVersion: 16,
ToVersion: 17,
Migrate: migrateV17,
Validate: validateV17,
}
}
func (wafIPGroupV17) TableName() string {
return "waf_ip_groups"
}
func (wafRuleGroupV17) TableName() string {
return "waf_rule_groups"
}
func migrateV17(ctx Context, db *gorm.DB, backend string) error {
return ctx.ApplyCurrentSchema(db, backend)
}
func validateV17(ctx Context, db *gorm.DB, backend string) error {
if err := ctx.ValidateDatabaseSchemaVersion(db, backend, 16); err != nil {
return err
}
if db == nil || !db.Migrator().HasTable(&wafIPGroupV17{}) {
return fmt.Errorf("table waf_ip_groups is missing")
}
if !db.Migrator().HasColumn(&wafRuleGroupV17{}, "ip_whitelist_groups") {
return fmt.Errorf("column waf_rule_groups.ip_whitelist_groups is missing")
}
if !db.Migrator().HasColumn(&wafRuleGroupV17{}, "ip_blacklist_groups") {
return fmt.Errorf("column waf_rule_groups.ip_blacklist_groups is missing")
}
return nil
}
+20
View File
@@ -78,6 +78,8 @@ func (databaseSchemaMigrationContext) ValidateDatabaseSchemaVersion(db *gorm.DB,
return validateDatabaseSchemaV15(db, backend)
case 16:
return validateDatabaseSchemaV16(db, backend)
case 17:
return validateDatabaseSchemaV17(db, backend)
default:
return fmt.Errorf("database schema validation for v%d is not defined", version)
}
@@ -1097,6 +1099,8 @@ func ensureDefaultWAFRuleGroup(db *gorm.DB) error {
BlockStatusCode: 418,
IPWhitelist: "[]",
IPBlacklist: "[]",
IPWhitelistGroups: "[]",
IPBlacklistGroups: "[]",
CountryWhitelist: "[]",
CountryBlacklist: "[]",
RegionWhitelist: "[]",
@@ -1170,6 +1174,22 @@ func validateDatabaseSchemaV16(db *gorm.DB, backend string) error {
return nil
}
func validateDatabaseSchemaV17(db *gorm.DB, backend string) error {
if err := validateDatabaseSchemaV16(db, backend); err != nil {
return err
}
if !db.Migrator().HasTable(&WAFIPGroup{}) {
return fmt.Errorf("table waf_ip_groups is missing")
}
if !db.Migrator().HasColumn(&WAFRuleGroup{}, "ip_whitelist_groups") {
return fmt.Errorf("column waf_rule_groups.ip_whitelist_groups is missing")
}
if !db.Migrator().HasColumn(&WAFRuleGroup{}, "ip_blacklist_groups") {
return fmt.Errorf("column waf_rule_groups.ip_blacklist_groups is missing")
}
return nil
}
func databaseSchemaMigrations() []databaseSchemaMigration {
ctx := databaseSchemaMigrationContext{}
migrations := []databaseSchemaMigration{}
+90
View File
@@ -11,6 +11,8 @@ type WAFRuleGroup struct {
BlockResponseBody string `json:"block_response_body" gorm:"type:text;not null;default:''"`
IPWhitelist string `json:"ip_whitelist" gorm:"type:text;not null;default:'[]'"`
IPBlacklist string `json:"ip_blacklist" gorm:"type:text;not null;default:'[]'"`
IPWhitelistGroups string `json:"ip_whitelist_group_ids" gorm:"type:text;not null;default:'[]'"`
IPBlacklistGroups string `json:"ip_blacklist_group_ids" gorm:"type:text;not null;default:'[]'"`
CountryWhitelist string `json:"country_whitelist" gorm:"type:text;not null;default:'[]'"`
CountryBlacklist string `json:"country_blacklist" gorm:"type:text;not null;default:'[]'"`
RegionWhitelist string `json:"region_whitelist" gorm:"type:text;not null;default:'[]'"`
@@ -22,6 +24,26 @@ type WAFRuleGroup struct {
UpdatedAt time.Time `json:"updated_at"`
}
type WAFIPGroup struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"size:255;not null"`
Type string `json:"type" gorm:"size:32;not null;index"`
Enabled bool `json:"enabled" gorm:"not null;default:true"`
IPList string `json:"ip_list" gorm:"type:text;not null;default:'[]'"`
AutoConfig string `json:"auto_config" gorm:"type:text;not null;default:'{}'"`
SubscriptionURL string `json:"subscription_url" gorm:"size:2048;not null;default:''"`
SubscriptionFormat string `json:"subscription_format" gorm:"size:32;not null;default:'text'"`
SubscriptionMappingRule string `json:"subscription_mapping_rule" gorm:"size:255;not null;default:''"`
SyncIntervalMinutes int `json:"sync_interval_minutes" gorm:"not null;default:1440"`
LastSyncedAt *time.Time `json:"last_synced_at"`
NextSyncAt *time.Time `json:"next_sync_at" gorm:"index"`
LastSyncStatus string `json:"last_sync_status" gorm:"size:32;not null;default:''"`
LastSyncMessage string `json:"last_sync_message" gorm:"type:text;not null;default:''"`
Remark string `json:"remark" gorm:"size:255"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type WAFRuleGroupBinding struct {
ID uint `json:"id" gorm:"primaryKey"`
RuleGroupID uint `json:"rule_group_id" gorm:"not null;uniqueIndex:idx_waf_group_route"`
@@ -60,6 +82,8 @@ func (group *WAFRuleGroup) Update() error {
"block_response_body": group.BlockResponseBody,
"ip_whitelist": group.IPWhitelist,
"ip_blacklist": group.IPBlacklist,
"ip_whitelist_groups": group.IPWhitelistGroups,
"ip_blacklist_groups": group.IPBlacklistGroups,
"country_whitelist": group.CountryWhitelist,
"country_blacklist": group.CountryBlacklist,
"region_whitelist": group.RegionWhitelist,
@@ -73,3 +97,69 @@ func (group *WAFRuleGroup) Update() error {
func (group *WAFRuleGroup) Delete() error {
return DB.Delete(group).Error
}
func ListWAFIPGroups() ([]*WAFIPGroup, error) {
var groups []*WAFIPGroup
err := DB.Order("type asc").Order("id asc").Find(&groups).Error
return groups, err
}
func GetWAFIPGroupByID(id uint) (*WAFIPGroup, error) {
group := &WAFIPGroup{}
err := DB.First(group, id).Error
return group, err
}
func ListWAFIPGroupsByIDs(ids []uint) ([]*WAFIPGroup, error) {
if len(ids) == 0 {
return []*WAFIPGroup{}, nil
}
var groups []*WAFIPGroup
err := DB.Where("id IN ?", ids).Order("id asc").Find(&groups).Error
return groups, err
}
func ListDueSubscriptionWAFIPGroups(now time.Time) ([]*WAFIPGroup, error) {
var groups []*WAFIPGroup
err := DB.Where("type = ? AND enabled = ? AND subscription_url <> '' AND (next_sync_at IS NULL OR next_sync_at <= ?)", "subscription", true, now).
Order("id asc").
Find(&groups).Error
return groups, err
}
func (group *WAFIPGroup) Insert() error {
return DB.Create(group).Error
}
func (group *WAFIPGroup) Update() error {
return DB.Model(&WAFIPGroup{}).Where("id = ?", group.ID).Updates(map[string]any{
"name": group.Name,
"type": group.Type,
"enabled": group.Enabled,
"ip_list": group.IPList,
"auto_config": group.AutoConfig,
"subscription_url": group.SubscriptionURL,
"subscription_format": group.SubscriptionFormat,
"subscription_mapping_rule": group.SubscriptionMappingRule,
"sync_interval_minutes": group.SyncIntervalMinutes,
"next_sync_at": group.NextSyncAt,
"last_sync_status": group.LastSyncStatus,
"last_sync_message": group.LastSyncMessage,
"remark": group.Remark,
}).Error
}
func (group *WAFIPGroup) UpdateSyncResult() error {
return DB.Model(&WAFIPGroup{}).Where("id = ?", group.ID).Updates(map[string]any{
"ip_list": group.IPList,
"last_synced_at": group.LastSyncedAt,
"next_sync_at": group.NextSyncAt,
"last_sync_status": group.LastSyncStatus,
"last_sync_message": group.LastSyncMessage,
"subscription_format": group.SubscriptionFormat,
}).Error
}
func (group *WAFIPGroup) Delete() error {
return DB.Delete(group).Error
}