Files

952 lines
56 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package waftask
import (
"SamWaf/common/tasklog"
"SamWaf/common/zlog"
"SamWaf/global"
"SamWaf/iplocation"
"SamWaf/model"
"SamWaf/model/request"
"SamWaf/service/waf_service"
"SamWaf/utils"
"SamWaf/wafenginecore"
"SamWaf/wafhostguard"
"SamWaf/wafipban"
"SamWaf/wafnotify/logfilewriter"
"SamWaf/wafowasp"
"fmt"
"strconv"
"strings"
"sync"
"golang.org/x/mod/semver"
)
// syncLogFileWriterConfig 将最新的全局配置同步到 LogFileWriter 实例
func syncLogFileWriterConfig() {
if global.GNOTIFY_LOG_FILE_WRITER == nil {
return
}
notifier := global.GNOTIFY_LOG_FILE_WRITER.GetNotifier()
if notifier == nil {
return
}
if writer, ok := notifier.(*logfilewriter.LogFileWriter); ok {
writer.UpdateConfig(
global.GCONFIG_LOG_FILE_WRITE_PATH,
global.GCONFIG_LOG_FILE_WRITE_FORMAT,
global.GCONFIG_LOG_FILE_WRITE_CUSTOM_TPL,
global.GCONFIG_LOG_FILE_WRITE_MAX_SIZE,
int(global.GCONFIG_LOG_FILE_WRITE_MAX_BACKUPS),
int(global.GCONFIG_LOG_FILE_WRITE_MAX_DAYS),
global.GCONFIG_LOG_FILE_WRITE_COMPRESS == 1,
)
}
}
// tokenExpireUnlimitedMinutes 令牌"不限制有效期"时实际采用的上限:1 年。
// 不做真正的永不过期:缓存 TTL 是 time.Duration(int64 纳秒),约 292 年就会溢出成负数,
// 令牌反而当场失效;而且永不过期的会话没有任何自然收敛点,泄露后只能靠人工清理。
// 1 年足够覆盖"我不想管这个"的诉求。
const tokenExpireUnlimitedMinutes int64 = 365 * 24 * 60
// normalizeTokenExpireMinutes 归一化令牌有效期(分钟):
//
// <= 0 用户表达的是"不管控有效期",按 1 年封顶
// > 1 年 同样收敛到 1 年,防止 time.Duration 溢出把令牌变成立即过期
// 其他 原样返回
//
// 归一化放在写全局变量的地方,保证 GCONFIG_RECORD_TOKEN_EXPIRE_MINTUTES 永远是正数,
// 所有取用点(登录写缓存、中间件续期、令牌重载)无需各自防御。
func normalizeTokenExpireMinutes(v int64) int64 {
if v <= 0 || v > tokenExpireUnlimitedMinutes {
return tokenExpireUnlimitedMinutes
}
return v
}
func setConfigIntValue(name string, value int64, change int) {
// 更新全局配置值
switch name {
case "record_max_req_body_length":
global.GCONFIG_RECORD_MAX_BODY_LENGTH = value
break
case "record_max_res_body_length":
global.GCONFIG_RECORD_MAX_RES_BODY_LENGTH = value
break
case "record_resp":
global.GCONFIG_RECORD_RESP = value
break
case "delete_history_log_day":
global.GDATA_DELETE_INTERVAL = value
break
case "log_db_size":
global.GDATA_SHARE_DB_SIZE = value
break
case "db_file_size":
global.GDATA_SHARE_DB_FILE_SIZE = value
break
case "access_log_retention_days":
if value < 1 {
value = 1
}
global.GDATA_ACCESS_LOG_RETENTION_DAYS = value
break
case "analysis_scan_path_threshold":
if value < 1 {
value = 1
}
global.GDATA_ANALYSIS_SCAN_PATH_THRESHOLD = value
break
case "analysis_ua_threshold":
if value < 1 {
value = 1
}
global.GDATA_ANALYSIS_UA_THRESHOLD = value
break
case "auto_load_ssl_file":
global.GCONFIG_RECORD_AUTO_LOAD_SSL = value
break
case "kafka_enable":
if global.GCONFIG_RECORD_KAFKA_ENABLE != value && global.GNOTIFY_KAKFA_SERVICE != nil {
global.GNOTIFY_KAKFA_SERVICE.ChangeEnable(value)
}
global.GCONFIG_RECORD_KAFKA_ENABLE = value
break
case "redirect_https_code":
global.GCONFIG_RECORD_REDIRECT_HTTPS_CODE = value
break
case "proxy_loop_max_hop":
global.GCONFIG_RECORD_PROXY_LOOP_MAX_HOP = value
break
case "login_max_error_time":
global.GCONFIG_RECORD_LOGIN_MAX_ERROR_TIME = value
break
case "login_limit_mintutes":
global.GCONFIG_RECORD_LOGIN_LIMIT_MINTUTES = value
break
case "access_enable":
global.GCONFIG_ACCESS_ENABLE = value
// 总开关是运行时快照的一部分,改完必须重新发布,否则要等下次配置保存才生效。
// 这里也是"改错了能自救"的关键路径:管理端把开关拨回 0,最迟一个刷新周期就恢复。
waf_service.WafAccessConfigServiceApp.PublishConfig()
break
case "access_audit_retain_days":
global.GCONFIG_ACCESS_AUDIT_RETAIN_DAYS = value
break
case "enable_owasp":
global.GCONFIG_RECORD_ENABLE_OWASP = value
break
case "ai_enable":
global.GCONFIG_AI_ENABLE = value
break
case "rule_chain_mode":
if value != 1 {
value = 0
}
global.GCONFIG_RULE_CHAIN_MODE = value
break
case "owasp_block_threshold":
if value <= 0 {
value = 7
}
global.GCONFIG_OWASP_BLOCK_THRESHOLD = value
break
case "enable_http_80":
global.GCONFIG_RECORD_ENABLE_HTTP_80 = value
break
case "sslorder_expire_day":
global.GCONFIG_RECORD_SSLOrder_EXPIRE_DAY = value
break
case "ssl_ip_expire_day":
global.GCONFIG_RECORD_SSL_IP_EXPIRE_DAY = value
break
case "ssl_expire_auto_sync_host":
if value != 0 {
value = 1
}
global.GCONFIG_SSL_EXPIRE_AUTO_SYNC_HOST = value
break
case "connect_time_out":
global.GCONFIG_RECORD_CONNECT_TIME_OUT = value
break
case "keepalive_time_out":
global.GCONFIG_RECORD_KEEPALIVE_TIME_OUT = value
break
case "record_all_src_byte_info":
global.GCONFIG_RECORD_ALL_SRC_BYTE_INFO = value
break
case "token_expire_time":
// 用户填 0/负数表示不限制有效期,统一归一化后再落到全局变量
global.GCONFIG_RECORD_TOKEN_EXPIRE_MINTUTES = normalizeTokenExpireMinutes(value)
break
case "spider_deny":
global.GCONFIG_RECORD_SPIDER_DENY = value
break
case "enable_debug":
global.GCONFIG_RECORD_DEBUG_ENABLE = value
break
case "dns_timeout":
global.GWAF_RUNTIME_DNS_TIMEOUT = value
break
case "hide_server_header":
global.GCONFIG_RECORD_HIDE_SERVER_HEADER = value
break
case "force_bind_2fa":
global.GCONFIG_RECORD_FORCE_BIND_2FA = value
break
case "fake_spider_captcha":
global.GCONFIG_RECORD_FAKE_SPIDER_CAPTCHA = value
break
case "sslhttp_check":
global.GCONFIG_RECORD_SSLHTTP_CHECK = value
break
case "enable_https_redirect":
global.GCONFIG_ENABLE_HTTPS_REDIRECT = value
break
case "ipprobe_enable":
if value != 1 {
wafenginecore.ClearAllIPProbeSamples() //关闭时把已采到的样本一并清掉,不留残留
}
global.GCONFIG_IPPROBE_ENABLE = value
break
case "allow_container_selfupdate":
global.GCONFIG_ALLOW_CONTAINER_SELFUPDATE = value
break
case "enable_device_fingerprint":
global.GCONFIG_ENABLE_DEVICE_FINGERPRINT = value
break
case "enable_strict_ip_binding":
global.GCONFIG_ENABLE_STRICT_IP_BINDING = value
break
case "batch_insert":
global.GDATA_BATCH_INSERT = value
break
case "log_persist_enable":
global.GCONFIG_LOG_PERSIST_ENABLED = value
break
case "enable_proxy_protocol":
global.GCONFIG_ENABLE_PROXY_PROTOCOL = value
break
case "enable_system_stats_push":
global.GCONFIG_ENABLE_SYSTEM_STATS_PUSH = value
break
case "ip_tag_db":
if value != 0 {
value = 1
}
changed := global.GDATA_IP_TAG_DB != value
global.GDATA_IP_TAG_DB = value
if changed {
// 归属换了:把另一个库里的历史标签并过来,否则界面按新库查,看起来像"切一下历史就没了"。
// 合并可能要搬几十万行,放后台跑,界面用 GDATA_IP_TAG_MERGING 显示"合并中"。
go waf_service.MergeIPTagsInto(value)
}
break
case "ip_failure_ban_enabled":
global.GCONFIG_IP_FAILURE_BAN_ENABLED = value
break
case "ip_failure_ban_lock_time":
global.GCONFIG_IP_FAILURE_BAN_LOCK_TIME = value
break
case "rule_ban_mode":
global.GCONFIG_RULE_BAN_MODE = value
break
case "host_guard_enabled":
global.GCONFIG_HOST_GUARD_ENABLED = value
// 开关切换不需要重启进程:0→1 启动日志采集,1→0 停止采集并释放 fd/子进程/系统句柄。
// 这也是四层自救里的 L1——把开关拨回 0,最迟一个配置刷新周期就不再产生新封禁。
wafhostguard.Reload()
break
case "host_guard_find_time":
global.GCONFIG_HOST_GUARD_FIND_TIME = value
break
case "host_guard_max_retry":
global.GCONFIG_HOST_GUARD_MAX_RETRY = value
break
case "host_guard_offender_reset_day":
global.GCONFIG_HOST_GUARD_OFFENDER_RESET_DAY = value
break
case "host_guard_count_soft_fail":
global.GCONFIG_HOST_GUARD_COUNT_SOFT_FAIL = value
break
case "host_guard_auto_lan":
global.GCONFIG_HOST_GUARD_AUTO_LAN = value
wafhostguard.InvalidateWhitelist()
// 内网段豁免同时是威胁情报误报排除集的来源,开关一变要重新落地。
// 只在真的变了时做:启动加载(change=0)时排除集本来就会按需构建,
// 这里再触发一次对账纯属浪费,还可能赶在防火墙引擎就绪之前。
if change == 1 {
waf_service.WafThreatIPExcludeServiceApp.NotifySourceChanged()
}
break
case "host_guard_debounce_sec":
global.GCONFIG_HOST_GUARD_DEBOUNCE_SEC = value
break
case "host_guard_max_ban_entries":
global.GCONFIG_HOST_GUARD_MAX_BAN_ENTRIES = value
break
case "host_guard_ban_rate_limit":
global.GCONFIG_HOST_GUARD_BAN_RATE_LIMIT = value
break
case "host_guard_subnet_aggregate":
global.GCONFIG_HOST_GUARD_SUBNET_AGGREGATE = value
break
case "host_guard_subnet_threshold":
global.GCONFIG_HOST_GUARD_SUBNET_THRESHOLD = value
break
case "host_guard_notify":
global.GCONFIG_HOST_GUARD_NOTIFY = value
break
case "host_conn_enabled":
global.GCONFIG_HOST_CONN_ENABLED = value
break
case "host_conn_cache_sec":
global.GCONFIG_HOST_CONN_CACHE_SEC = value
break
case "check_beta_version":
global.GCONFIG_CHECK_BETA_VERSION = value
break
case "http3":
global.GCONFIG_ENABLE_HTTP3 = value
if change == 1 {
// 光改全局变量不会让任何端口去监听 UDP:h3 实例只在 StartProxyServer 里建,
// 而已在监听的端口(Status==0)会被直接跳过,所以必须显式让引擎重新对齐(issue #916)。
// 先赋值再投递,避免消费方读到旧值。
global.NotifyHTTP3ConfigChanged()
}
case "record_log_desensitize":
global.GCONFIG_RECORD_LOG_DESENSITIZE = value
case "pwd_min_length":
global.GCONFIG_PWD_MIN_LENGTH = value
case "pwd_require_upper":
global.GCONFIG_PWD_REQUIRE_UPPER = value
case "pwd_require_lower":
global.GCONFIG_PWD_REQUIRE_LOWER = value
case "pwd_require_digit":
global.GCONFIG_PWD_REQUIRE_DIGIT = value
case "pwd_require_special":
global.GCONFIG_PWD_REQUIRE_SPECIAL = value
case "pwd_expire_days":
global.GCONFIG_PWD_EXPIRE_DAYS = value
case "pwd_history_count":
global.GCONFIG_PWD_HISTORY_COUNT = value
case "pwd_force_change_default":
global.GCONFIG_PWD_FORCE_CHANGE_DEFAULT = value
case "log_file_write_enable":
if global.GCONFIG_LOG_FILE_WRITE_ENABLE != value && global.GNOTIFY_LOG_FILE_WRITER != nil {
global.GNOTIFY_LOG_FILE_WRITER.ChangeEnable(value)
}
global.GCONFIG_LOG_FILE_WRITE_ENABLE = value
case "log_file_write_max_size":
global.GCONFIG_LOG_FILE_WRITE_MAX_SIZE = value
syncLogFileWriterConfig()
case "log_file_write_max_backups":
global.GCONFIG_LOG_FILE_WRITE_MAX_BACKUPS = value
syncLogFileWriterConfig()
case "log_file_write_max_days":
global.GCONFIG_LOG_FILE_WRITE_MAX_DAYS = value
syncLogFileWriterConfig()
case "log_file_write_compress":
global.GCONFIG_LOG_FILE_WRITE_COMPRESS = value
syncLogFileWriterConfig()
case "open_platform_enabled":
global.GCONFIG_OPEN_PLATFORM_ENABLED = value
case "task_log_retain_days":
global.GCONFIG_TASK_LOG_RETAIN_DAYS = value
// 动态更新任务日志管理器的保留天数
if tasklog.GlobalTaskLogManager != nil {
tasklog.GlobalTaskLogManager.UpdateRetainDays(int(value))
}
case "http3_bbr":
global.GCONFIG_ENABLE_HTTP3_BBR = value
if change == 1 {
// 拥塞算法变了需要重建 QUIC 监听,同样要通知引擎
global.NotifyHTTP3ConfigChanged()
}
default:
zlog.Warn("Unknown config item:", name)
}
}
func setConfigStringValue(name string, value string, change int) {
// 更新全局配置值
switch name {
case "dns_server":
global.GWAF_RUNTIME_DNS_SERVER = value
break
case "record_log_type":
global.GWAF_RUNTIME_RECORD_LOG_TYPE = value
break
case "access_log_mode":
if value != "off" && value != "sample" {
value = "db"
}
global.GDATA_ACCESS_LOG_MODE = value
break
case "attack_tag_exclude":
global.GCONFIG_ATTACK_TAG_EXCLUDE = value
break
case "exclude_ip_log":
global.GCONFIG_EXCLUDE_IP_LOG = value
// 同步编译全局排除快照,保存即生效,无需重启
wafenginecore.SetGlobalIPLogExclude(value)
break
case "gwaf_proxy_header":
global.GCONFIG_RECORD_PROXY_HEADER = value
break
case "gwaf_manage_proxy_header":
global.GCONFIG_MANAGE_PROXY_HEADER = value
break
case "owasp_mode":
switch value {
case "On", "DetectionOnly", "Off":
global.GCONFIG_OWASP_MODE = value
case "":
global.GCONFIG_OWASP_MODE = "On"
default:
zlog.Warn("invalid owasp_mode value, fallback to On", value)
global.GCONFIG_OWASP_MODE = "On"
}
// 同步到 wafowasp 热路径,使 DetectionOnly "本该拦截" 的 INFO 日志能按当前模式生效
wafowasp.SetEngineMode(global.GCONFIG_OWASP_MODE)
break
case "ai_mode":
switch value {
case "observe", "block":
global.GCONFIG_AI_MODE = value
default:
zlog.Warn("invalid ai_mode value, fallback to observe", value)
global.GCONFIG_AI_MODE = "observe"
}
break
case "body_detect_mode":
switch value {
case "observe", "block", "off":
global.GCONFIG_BODY_DETECT_MODE = value
default:
zlog.Warn("invalid body_detect_mode value, fallback to observe", value)
global.GCONFIG_BODY_DETECT_MODE = "observe"
}
break
case "body_detect_field_exclude":
global.GCONFIG_BODY_DETECT_FIELD_EXCLUDE = value
wafenginecore.SetBodyDetectFieldExclude(value) // 同步解析成 RCU 排除集
break
case "kafka_url":
global.GCONFIG_RECORD_KAFKA_URL = value
break
case "kafka_topic":
global.GCONFIG_RECORD_KAFKA_TOPIC = value
break
case "debug_pwd":
global.GCONFIG_RECORD_DEBUG_PWD = value
break
case "gpt_url":
global.GCONFIG_RECORD_GPT_URL = value
break
case "gpt_token":
global.GCONFIG_RECORD_GPT_TOKEN = value
break
case "gpt_model":
global.GCONFIG_RECORD_GPT_MODEL = value
break
case "ssl_min_version":
global.GCONFIG_RECORD_SSLMinVerson = value
break
case "ssl_max_version":
global.GCONFIG_RECORD_SSLMaxVerson = value
break
case "ssl_ip_cert_ip":
global.GCONFIG_RECORD_SSL_IP_CERT_IP = value
break
case "ip_failure_status_codes":
global.GCONFIG_IP_FAILURE_STATUS_CODES = value
// 重新加载状态码配置
wafipban.GetIPFailureManager().ReloadStatusCodes()
break
case "host_guard_mode":
if value != "block" {
value = "observe"
}
global.GCONFIG_HOST_GUARD_MODE = value
break
case "host_guard_whitelist":
global.GCONFIG_HOST_GUARD_WHITELIST = value
// 白名单是防误封的主力,改完必须立刻重建,不能等下一个刷新周期
wafhostguard.InvalidateWhitelist()
// 同一份白名单也用来把"自己人"从威胁情报里剔出去,改完要重新落地
if change == 1 {
waf_service.WafThreatIPExcludeServiceApp.NotifySourceChanged()
}
break
case "host_guard_log_paths":
global.GCONFIG_HOST_GUARD_LOG_PATHS = value
// 日志路径变了要换事件源,重启采集
wafhostguard.Reload()
break
case "host_guard_ssh_ports":
global.GCONFIG_HOST_GUARD_SSH_PORTS = value
wafhostguard.InvalidatePorts()
// 端口变了,端口级封禁的规则也要跟着换
wafhostguard.GetBanExecutor().ApplyPortScope()
break
case "host_guard_rdp_ports":
global.GCONFIG_HOST_GUARD_RDP_PORTS = value
wafhostguard.InvalidatePorts()
wafhostguard.GetBanExecutor().ApplyPortScope()
break
case "host_guard_port_scope":
if value != "detected" {
value = "all"
}
global.GCONFIG_HOST_GUARD_PORT_SCOPE = value
// 封禁范围要立刻重建引用规则,否则要等下次重启才生效
wafhostguard.GetBanExecutor().ApplyPortScope()
break
case "host_guard_exec_mode":
if value != "ipset" && value != "rule" {
value = "auto"
}
global.GCONFIG_HOST_GUARD_EXEC_MODE = value
break
case "zerossl_access_key":
global.GCONFIG_ZEROSSL_ACCESS_KEY = value
break
case "zerossl_eab_kid":
global.GCONFIG_ZEROSSL_EAB_KID = value
break
case "zerossl_eab_hmac_key":
global.GCONFIG_ZEROSSL_EAB_HMAC_KEY = value
break
case "log_file_write_path":
global.GCONFIG_LOG_FILE_WRITE_PATH = value
syncLogFileWriterConfig()
case "log_file_write_format":
global.GCONFIG_LOG_FILE_WRITE_FORMAT = value
syncLogFileWriterConfig()
case "log_file_write_custom_tpl":
global.GCONFIG_LOG_FILE_WRITE_CUSTOM_TPL = value
syncLogFileWriterConfig()
case "ip_v4_source":
global.GCONFIG_IP_V4_SOURCE = value
if change == 1 {
zlog.Info("IPv4 数据源配置已更改为: ", value)
}
case "ip_v6_source":
global.GCONFIG_IP_V6_SOURCE = value
if change == 1 {
zlog.Info("IPv6 数据源配置已更改为: ", value)
}
case "ip_v4_format":
global.GCONFIG_IP_V4_FORMAT = value
if change == 1 {
// 重新加载 IPv4 数据库
if global.GIPLOCATION_MANAGER != nil {
global.GIPLOCATION_MANAGER.SetV4Format(iplocation.DBFormat(value))
zlog.Info("IPv4 数据格式配置已更改为: ", value)
}
}
case "ip_v6_format":
global.GCONFIG_IP_V6_FORMAT = value
if change == 1 {
// 重新加载 IPv6 数据库
if global.GIPLOCATION_MANAGER != nil {
global.GIPLOCATION_MANAGER.SetV6Format(iplocation.DBFormat(value))
zlog.Info("IPv6 数据格式配置已更改为: ", value)
}
}
default:
zlog.Warn("Unknown config item:", name)
}
}
// legacyMigrateOnce 保证旧默认值迁移在一个进程内只执行一次
var legacyMigrateOnce sync.Once
// defaultTokenExpireMinutes 令牌有效期的出厂默认值(分钟),与 global/config.go 的初始值保持一致。
// 单独取常量而不是读全局变量:全局变量在迁移执行时可能已被库里的旧值覆盖成 5,
// 那样迁移就成了"5 改成 5"的空操作。
const defaultTokenExpireMinutes int64 = 30
// syncConfigMeta 把配置项的说明性字段(分类/类型/可选项/备注)同步成代码里的最新文案。
// 只在确实不一致时写库,且绝不触碰 Value —— 用户设过的值不受影响。
// 存在的意义:存量库里的说明是首次写入时的老文案,页面会一直显示过时描述。
func syncConfigMeta(configItem model.SystemConfig, itemClass string, itemType string, options string, remarks string) {
if configItem.Remarks == remarks && configItem.Options == options &&
configItem.ItemClass == itemClass && configItem.ItemType == itemType {
return
}
if err := wafSystemConfigService.SyncMetaByItemApi(configItem.Item, itemClass, itemType, options, remarks); err != nil {
zlog.Debug("同步配置项说明失败", configItem.Item, err.Error())
}
}
// migrateLegacyIntConfig 一次性把"仍停留在旧默认值"的存量配置迁移到新默认值。
// 仅当库里的值恰好等于 legacyValue 时才迁移;用户自己改过的值一律不动。
// 迁移后同步更新 configMap,避免紧接着的 updateConfigIntItem 又把全局变量按旧值写回去。
func migrateLegacyIntConfig(itemName string, legacyValue int64, newValue int64, reason string, configMap map[string]model.SystemConfig) {
configItem, exists := configMap[itemName]
if !exists || configItem.Id == "" {
return // 新装:直接走默认值创建,无需迁移
}
value, err := strconv.ParseInt(configItem.Value, 10, 0)
if err != nil || value != legacyValue {
return
}
newStr := strconv.FormatInt(newValue, 10)
if err := wafSystemConfigService.ModifyByItemApi(request.WafSystemConfigEditByItemReq{
Item: itemName,
Value: newStr,
}); err != nil {
zlog.Warn("迁移旧默认配置失败", itemName, err.Error())
return
}
configItem.Value = newStr
configMap[itemName] = configItem
zlog.Info(fmt.Sprintf("配置项 %s 仍为旧默认值 %d,已自动升级为 %d(%s)", itemName, legacyValue, newValue, reason))
}
func updateConfigIntItem(initLoad bool, itemClass string, itemName string, defaultValue int64, remarks string, itemType string, options string, configMap map[string]model.SystemConfig) {
configItem, exists := configMap[itemName]
if exists && configItem.Id != "" {
syncConfigMeta(configItem, itemClass, itemType, options, remarks)
value, err := strconv.ParseInt(configItem.Value, 10, 0)
if err == nil && defaultValue != value {
setConfigIntValue(itemName, value, 1)
} else if err == nil && initLoad == true {
setConfigIntValue(itemName, value, 0)
}
} else {
wafSystemConfigAddReq := request.WafSystemConfigAddReq{
ItemClass: itemClass,
Item: itemName,
Value: strconv.FormatInt(defaultValue, 10),
Remarks: remarks,
ItemType: itemType,
Options: options,
}
wafSystemConfigService.AddApi(wafSystemConfigAddReq)
}
}
func updateConfigStringItem(initLoad bool, itemClass string, itemName string, defaultValue string, remarks string, itemType string, options string, configMap map[string]model.SystemConfig) {
configItem, exists := configMap[itemName]
if exists && configItem.Id != "" {
syncConfigMeta(configItem, itemClass, itemType, options, remarks)
if defaultValue != configItem.Value {
setConfigStringValue(itemName, configItem.Value, 1)
} else if initLoad == true {
setConfigStringValue(itemName, configItem.Value, 0)
}
} else {
wafSystemConfigAddReq := request.WafSystemConfigAddReq{
ItemClass: itemClass,
Item: itemName,
Value: defaultValue,
Remarks: remarks,
ItemType: itemType,
Options: options,
}
wafSystemConfigService.AddApi(wafSystemConfigAddReq)
}
}
// TaskLoadSettingCron 不是初始化加载
func TaskLoadSettingCron() {
TaskLoadSetting(false)
}
// CheckVersionDowngrade 维护两个版本水位并做降级校验,顺带生成本次的「升级须知」。
//
// 场景:容器环境点了应用内升级后,新版本会把数据库迁移到新 schema,但程序文件在镜像可写层,
// 容器一旦重建就回退成镜像自带的旧版本,而数据库回不去 —— 形成"旧程序 + 新库"。
// 这种状态过去是完全静默的,只能靠"任务方法 xxx 未找到"这类间接报错才能猜出来(issue #938)。
// 这里只记日志、不阻断启动。
//
// 为什么要两个配置项:
// - last_run_version 上次运行的版本,**跟着当前版本走(会下调)**,用于算升级须知的版本区间
// - max_run_version 曾经运行过的最高版本,**只升不降**,用于降级判定
//
// 降级判定必须用后者。若沿用 last_run_version,判完降级后它会被改写成当前这个较低的版本,
// 等于自己把证据抹掉:重启一次警告就永远消失,而"旧程序 + 新库"的真实状态还在。
// 容器反复重建恰恰是这个警告要防的场景,只警告第一次等于没警告。
//
// 升级须知刻意写在这个函数里、而不是另找地方再查一次 last_run_version:
// 本函数读完就会把库里的值改写成当前版本,一旦写回,旧版本号就再也拿不到了。
func CheckVersionDowngrade() {
current := strings.TrimSpace(global.GWAF_RELEASE_VERSION)
if current == "" {
return
}
item := wafSystemConfigService.GetDetailByItem(versionRecordItem)
last := ""
if item.Id != "" {
last = strings.TrimSpace(item.Value)
}
maxItem := wafSystemConfigService.GetDetailByItem(maxVersionRecordItem)
maxSeen := ""
if maxItem.Id != "" {
maxSeen = strings.TrimSpace(maxItem.Value)
}
// 老库没有 max_run_version 这一行,用 last_run_version 当初值:
// 升级到本版本之前跑过的最高版本,最好的已知近似就是上次运行的那个。
if maxSeen == "" {
maxSeen = last
}
if maxSeen != "" && semver.IsValid(maxSeen) && semver.IsValid(current) && semver.Compare(maxSeen, current) > 0 {
zlog.Error(fmt.Sprintf("检测到降级运行:数据库记录曾经运行过的最高版本是 %v,当前程序版本是 %v。"+
"若为容器部署,通常是应用内升级后容器被重建、程序回退到镜像自带版本所致;"+
"数据库已按新版本迁移且无法回退,请把镜像更新到 %v 或更高版本后重建容器", maxSeen, current, maxSeen))
}
// 生成本次升级须知(内置清单 + 版本区间)。必须赶在下面写回 last_run_version 之前。
wafUpgradeNoticeSvc.Generate(last, current, maxSeen)
// 版本水位只升不降:降级运行时保留历史最高版本,警告才不会因为一次重启就消失
if maxSeen == "" || (semver.IsValid(current) && (!semver.IsValid(maxSeen) || semver.Compare(current, maxSeen) > 0)) {
writeVersionRecord(maxItem, maxVersionRecordItem, current,
"曾经成功启动过的最高程序版本(只升不降),用于识别\"程序被降级、数据库却已按新版本迁移\"的不一致状态,请勿手工修改")
}
// 记录本次版本(只在变化时写库)
if last == current {
return
}
writeVersionRecord(item, versionRecordItem, current,
"上次成功启动的程序版本,用于计算升级须知的版本区间,请勿手工修改")
}
// writeVersionRecord 写入/更新一个版本水位配置项。失败只记日志,不影响启动。
func writeVersionRecord(existing model.SystemConfig, item, value, remarks string) {
if existing.Id != "" {
if err := wafSystemConfigService.ModifyByItemApi(request.WafSystemConfigEditByItemReq{
Item: item,
Value: value,
}); err != nil {
zlog.Debug("更新版本记录失败", item, err.Error())
}
return
}
if err := wafSystemConfigService.AddApi(request.WafSystemConfigAddReq{
ItemClass: "system",
Item: item,
Value: value,
Remarks: remarks,
ItemType: "string",
}); err != nil {
zlog.Debug("写入版本记录失败", item, err.Error())
}
}
// versionRecordItem 记录"上次运行版本"的配置项名(跟着当前版本走,可下调)
const versionRecordItem = "last_run_version"
// maxVersionRecordItem 记录"曾经运行过的最高版本"的配置项名(只升不降,降级判定用它)
const maxVersionRecordItem = "max_run_version"
// TaskLoadSetting 加载配置数据
//
// initLoad true 是初始化加载,false不是初始化加载
func TaskLoadSetting(initLoad bool) {
zlog.Debug("TaskLoadSetting")
// 一次性批量查询所有配置项
configMap := wafSystemConfigService.GetAllConfigs()
// 存量实例的旧默认值升级:令牌有效期长期没有页面入口(issue #930),库里若还是 5 分钟
// 必然是老默认值而非用户选择,5 分钟同时充当空闲与绝对超时会导致"页面开着不动就掉线"。
// 只在进程启动后的第一次加载执行:配置页每保存一次都会调 TaskLoadSetting(true),
// 若不加守卫,用户特意填回 5 分钟会被反复改成 30,等于夺走用户的选择权。
legacyMigrateOnce.Do(func() {
migrateLegacyIntConfig("token_expire_time", 5, defaultTokenExpireMinutes,
"旧默认值5分钟过短,易造成反复登录", configMap)
})
updateConfigIntItem(initLoad, "system", "record_max_req_body_length", global.GCONFIG_RECORD_MAX_BODY_LENGTH, "记录请求最大报文", "int", "", configMap)
updateConfigIntItem(initLoad, "system", "record_max_res_body_length", global.GCONFIG_RECORD_MAX_RES_BODY_LENGTH, "如果可以记录,满足最大响应报文大小才记录", "int", "", configMap)
updateConfigIntItem(initLoad, "system", "record_resp", global.GCONFIG_RECORD_RESP, "是否记录响应报文", "int", "", configMap)
updateConfigIntItem(initLoad, "system", "delete_history_log_day", global.GDATA_DELETE_INTERVAL, "删除多少天前的日志数据(单位:天)", "int", "", configMap)
updateConfigIntItem(initLoad, "system", "log_db_size", global.GDATA_SHARE_DB_SIZE, "日志归档最大记录数量", "int", "", configMap)
updateConfigIntItem(initLoad, "system", "db_file_size", global.GDATA_SHARE_DB_FILE_SIZE, "日志归档最大文件大小(MB)", "int", "", configMap)
updateConfigIntItem(initLoad, "system", "auto_load_ssl_file", global.GCONFIG_RECORD_AUTO_LOAD_SSL, "是否每天凌晨3点自动加载ssl证书", "int", "", configMap)
updateConfigStringItem(initLoad, "system", "dns_server", global.GWAF_RUNTIME_DNS_SERVER, "DNS服务器", "options", "119.29.29.29|腾讯DNS,8.8.8.8|谷歌DNS", configMap)
updateConfigIntItem(initLoad, "system", "dns_timeout", global.GWAF_RUNTIME_DNS_TIMEOUT, "DNS 查询超时时间 单位毫秒", "int", "", configMap)
updateConfigStringItem(initLoad, "system", "record_log_type", global.GWAF_RUNTIME_RECORD_LOG_TYPE, "日志记录类型", "options", "all|全部,abnormal|非正常", configMap)
updateConfigStringItem(initLoad, "system", "access_log_mode", global.GDATA_ACCESS_LOG_MODE, "访问日志窄行档位:db=全部请求入库(默认,保留期见下项);sample=安全事件+采样入库;off=只留安全事件(高流量推荐,但会失去CC阈值推荐/AI训练负样本/异常IP的正常行为回溯)。db/sample 档均按站点每天采样 500 条正常请求报文供 AI 训练", "options", "db|全部入库,sample|采样入库,off|仅安全事件", configMap)
updateConfigIntItem(initLoad, "system", "access_log_retention_days", global.GDATA_ACCESS_LOG_RETENTION_DAYS, "访问日志窄行保留天数(默认30)。安全事件仍按「日志保留天数」走;本项直接决定CC阈值推荐能回看多少天", "int", "", configMap)
updateConfigIntItem(initLoad, "system", "analysis_scan_path_threshold", global.GDATA_ANALYSIS_SCAN_PATH_THRESHOLD, "来源分析:一天摸过多少个不同路径模板判为「疑似扫目录」(默认20)。只影响界面提示与筛选,不改拦截行为", "int", "", configMap)
updateConfigIntItem(initLoad, "system", "analysis_ua_threshold", global.GDATA_ANALYSIS_UA_THRESHOLD, "来源分析:一天用过多少种UA指纹判为「疑似换UA试探」(默认5)。只影响界面提示与筛选,不改拦截行为", "int", "", configMap)
updateConfigStringItem(initLoad, "system", "gwaf_proxy_header", global.GCONFIG_RECORD_PROXY_HEADER, "获取访客IP头信息(按照顺序)比如:X-Forwarded-For,X-Real-IP ,留空则提取的是直接访客IP", "string", "", configMap)
updateConfigStringItem(initLoad, "system", "gwaf_manage_proxy_header", global.GCONFIG_MANAGE_PROXY_HEADER, "管理端获取客户端IP头信息(按优先级逗号分隔,如 X-Forwarded-For,X-Real-IP,CF-Connecting-IP),留空则直接取网络IP。安全起见需配合 conf/config.yml 的 security.manage_trusted_proxies:仅当直连来源属可信代理时才采信此头(容器/内网部署可直接填 private)", "string", "", configMap)
updateConfigIntItem(initLoad, "kafka", "kafka_enable", global.GCONFIG_RECORD_KAFKA_ENABLE, "kafka 是否激活", "int", "", configMap)
updateConfigStringItem(initLoad, "kafka", "kafka_url", global.GCONFIG_RECORD_KAFKA_URL, "kafka url地址", "string", "", configMap)
updateConfigStringItem(initLoad, "kafka", "kafka_topic", global.GCONFIG_RECORD_KAFKA_TOPIC, "kafka topic", "string", "", configMap)
updateConfigIntItem(initLoad, "system", "redirect_https_code", global.GCONFIG_RECORD_REDIRECT_HTTPS_CODE, "80重定向https时候跳转代码", "int", "", configMap)
updateConfigIntItem(initLoad, "system", "enable_https_redirect", global.GCONFIG_ENABLE_HTTPS_REDIRECT, "是否启用HTTPS重定向服务器(0关闭 1开启)", "int", "", configMap)
updateConfigIntItem(initLoad, "system", "proxy_loop_max_hop", global.GCONFIG_RECORD_PROXY_LOOP_MAX_HOP, "反向代理最大跳数,请求经过SamWaf转发累计超过此值判定为环路并拦截(防后端回指WAF造成死循环),默认10,设为0关闭检测", "int", "", configMap)
updateConfigIntItem(initLoad, "system", "login_max_error_time", global.GCONFIG_RECORD_LOGIN_MAX_ERROR_TIME, "登录周期里错误最大次数 请大于0 ", "int", "", configMap)
updateConfigIntItem(initLoad, "system", "login_limit_mintutes", global.GCONFIG_RECORD_LOGIN_LIMIT_MINTUTES, "登录错误记录周期 单位分钟数,默认1分钟", "int", "", configMap)
updateConfigIntItem(initLoad, "system", "enable_owasp", global.GCONFIG_RECORD_ENABLE_OWASP, "启动OWASP数据检测(1启动 0关闭)", "int", "", configMap)
updateConfigIntItem(initLoad, "system", "ipprobe_enable", global.GCONFIG_IPPROBE_ENABLE, "真实IP来源探针(1开启 0关闭,默认关闭)。开启后记录各站点最近到达的请求头(脱敏,仅内存,每站每秒最多1条),供站点「真实IP来源」处排查CDN送来的是哪个头;排查完建议关闭", "int", "", configMap)
updateConfigIntItem(initLoad, "system", "ai_enable", global.GCONFIG_AI_ENABLE, "启动AI智能检测总开关(1启动 0关闭,需先在AI模型管理上传模型包并在站点开启)", "int", "", configMap)
updateConfigStringItem(initLoad, "system", "ai_mode", global.GCONFIG_AI_MODE, "AI检测工作模式:observe(仅记录/观察) block(达拦截阈值则拦截)", "options", "observe|仅记录,block|拦截", configMap)
updateConfigStringItem(initLoad, "system", "body_detect_mode", global.GCONFIG_BODY_DETECT_MODE, "请求体深度检测模式(formValue/JSON 逐值 XSS/SQLi/RCE):observe 观察(命中只记录不拦,默认) block 拦截(命中即拦) off 关闭。默认 observe 避免误伤正常 JSON/表单流量,观察日志确认无误后再切 block;查询串检测不受影响始终拦截", "options", "observe|观察(只记录),block|拦截,off|关闭", configMap)
updateConfigStringItem(initLoad, "system", "body_detect_field_exclude", global.GCONFIG_BODY_DETECT_FIELD_EXCLUDE, "请求体深检字段排除清单(逗号分隔字段名,不区分大小写)。这些字段名下的表单值/JSON值(匹配直接父键名)跳过 XSS/SQLi/RCE 深检,用于放行已知携带富文本/HTML/代码的合法字段(如 content,html,markdown),压 block 模式误报。看观察日志里是哪些字段误报再填", "string", "", configMap)
updateConfigIntItem(initLoad, "system", "rule_chain_mode", global.GCONFIG_RULE_CHAIN_MODE, "自定义规则在检测链中的位置(0默认:排在CC之后;1规则优先:排在黑名单之后、Bot/SQLI/XSS等之前,此时规则的放行动作才能跳过这些检测)", "int", "", configMap)
updateConfigIntItem(initLoad, "access", "access_enable", global.GCONFIG_ACCESS_ENABLE, "统一访问认证总开关:开启后所有被WAF代理的站点默认都需先登录才能访问(可在站点里单独强制开/关,也可配置免认证路径与IP组)。请先在【统一访问认证-访问账号】里建好账号再开启", "options", "0|关闭,1|开启", configMap)
updateConfigIntItem(initLoad, "access", "access_audit_retain_days", global.GCONFIG_ACCESS_AUDIT_RETAIN_DAYS, "统一访问认证审计日志保留天数,默认90天", "int", "", configMap)
updateConfigIntItem(initLoad, "ssl", "enable_http_80", global.GCONFIG_RECORD_ENABLE_HTTP_80, "启动80端口服务(为自动申请证书使用 HTTP文件验证类型需要,DNS验证不需要)", "int", "", configMap)
updateConfigIntItem(initLoad, "ssl", "sslorder_expire_day", global.GCONFIG_RECORD_SSLOrder_EXPIRE_DAY, "自动续期检测小于多少天开始发起自动申请 默认30天", "int", "", configMap)
updateConfigStringItem(initLoad, "ssl", "ssl_ip_cert_ip", global.GCONFIG_RECORD_SSL_IP_CERT_IP, "获取IP证书时的IP地址(为IP证书申请使用,留空则不使用)", "string", "", configMap)
updateConfigIntItem(initLoad, "ssl", "ssl_ip_expire_day", global.GCONFIG_RECORD_SSL_IP_EXPIRE_DAY, "IP证书自动续期检测小于多少天开始发起自动申请 默认3天", "int", "", configMap)
updateConfigIntItem(initLoad, "ssl", "ssl_expire_auto_sync_host", global.GCONFIG_SSL_EXPIRE_AUTO_SYNC_HOST, "SSL证书过期检测前是否自动同步已配置的SSL主机到检测列表(1同步 0不同步,默认1;关闭后过期检测只检测列表里已有的域名,手动点【同步主机】仍可用)", "options", "0|不同步,1|同步", configMap)
updateConfigIntItem(initLoad, "ssl", "sslhttp_check", global.GCONFIG_RECORD_SSLHTTP_CHECK, "证书文件验证:本地挑战文件始终优先使用;本项仅控制【本地无挑战文件】且后端对 .well-known 返回非404/301/302 时是否写告警日志(1告警 0不告警),不影响能否签发", "int", "", configMap)
updateConfigStringItem(initLoad, "ssl", "ssl_min_version", global.GCONFIG_RECORD_SSLMinVerson, "SSL最低版本(支持TLS 1.0,TLS 1.1,TLS 1.2,TLS 1.3),修改后重启一下", "options", "TLS 1.0|TLS 1.0,TLS 1.1|TLS 1.1,TLS 1.2|TLS 1.2,TLS 1.3|TLS 1.3", configMap)
updateConfigStringItem(initLoad, "ssl", "ssl_max_version", global.GCONFIG_RECORD_SSLMaxVerson, "SSL最大版本(支持TLS 1.0,TLS 1.1,TLS 1.2,TLS 1.3),修改后重启一下", "options", "TLS 1.0|TLS 1.0,TLS 1.1|TLS 1.1,TLS 1.2|TLS 1.2,TLS 1.3|TLS 1.3", configMap)
updateConfigIntItem(initLoad, "network", "connect_time_out", global.GCONFIG_RECORD_CONNECT_TIME_OUT, "连接超时(默认30s)", "int", "", configMap)
updateConfigIntItem(initLoad, "network", "keepalive_time_out", global.GCONFIG_RECORD_KEEPALIVE_TIME_OUT, "保持活动超时(默认30s)", "int", "", configMap)
updateConfigIntItem(initLoad, "network", "http3", global.GCONFIG_ENABLE_HTTP3, "是否启用HTTP/3(QUIC)。生效前提:①站点必须开启SSL(HTTP/3 只跑在 TLS 上,非HTTPS端口不会监听UDP);②必须在防火墙/安全组放行对应的【UDP】端口(如 443/udp)。开启后立即生效、无需重启;浏览器通过响应头 Alt-Svc 升级到 h3", "options", "0|关闭,1|开启", configMap)
updateConfigIntItem(initLoad, "network", "http3_bbr", global.GCONFIG_ENABLE_HTTP3_BBR, "HTTP/3 拥塞控制算法:0=NewReno(默认) 1=BBR。仅在已启用 HTTP/3 时有意义,修改后会自动重建 QUIC 监听", "options", "0|NewReno,1|BBR", configMap)
updateConfigIntItem(initLoad, "system", "record_all_src_byte_info", global.GCONFIG_RECORD_ALL_SRC_BYTE_INFO, "启动记录原始请求BODY报文(1启动 0关闭)", "int", "", configMap)
updateConfigIntItem(initLoad, "system", "record_log_desensitize", global.GCONFIG_RECORD_LOG_DESENSITIZE, "请求记录是否进行脱敏处理(1开启脱敏 0关闭脱敏)", "options", "0|关闭脱敏,1|开启脱敏", configMap)
updateConfigIntItem(initLoad, "system", "token_expire_time", global.GCONFIG_RECORD_TOKEN_EXPIRE_MINTUTES, "管理平台令牌有效期(空闲多久未操作即失效,单位分钟;每次访问会自动续期。默认30分钟;填 0 或负数表示不限制有效期,实际按 1 年封顶。过短会导致频繁重新登录)", "int", "", configMap)
// 口令复杂度策略
updateConfigIntItem(initLoad, "password", "pwd_min_length", global.GCONFIG_PWD_MIN_LENGTH, "密码最小长度(默认8)", "int", "", configMap)
updateConfigIntItem(initLoad, "password", "pwd_require_upper", global.GCONFIG_PWD_REQUIRE_UPPER, "是否要求包含大写字母", "options", "0|不要求,1|要求", configMap)
updateConfigIntItem(initLoad, "password", "pwd_require_lower", global.GCONFIG_PWD_REQUIRE_LOWER, "是否要求包含小写字母", "options", "0|不要求,1|要求", configMap)
updateConfigIntItem(initLoad, "password", "pwd_require_digit", global.GCONFIG_PWD_REQUIRE_DIGIT, "是否要求包含数字", "options", "0|不要求,1|要求", configMap)
updateConfigIntItem(initLoad, "password", "pwd_require_special", global.GCONFIG_PWD_REQUIRE_SPECIAL, "是否要求包含特殊字符", "options", "0|不要求,1|要求", configMap)
updateConfigIntItem(initLoad, "password", "pwd_expire_days", global.GCONFIG_PWD_EXPIRE_DAYS, "密码有效期天数(0=不限,到期登录时提示强制更换)", "int", "", configMap)
updateConfigIntItem(initLoad, "password", "pwd_history_count", global.GCONFIG_PWD_HISTORY_COUNT, "历史密码防重用个数(0=不启用)", "int", "", configMap)
updateConfigIntItem(initLoad, "password", "pwd_force_change_default", global.GCONFIG_PWD_FORCE_CHANGE_DEFAULT, "默认密码/被重置后是否强制改密", "options", "0|否,1|强制", configMap)
updateConfigIntItem(initLoad, "system", "spider_deny", global.GCONFIG_RECORD_SPIDER_DENY, "爬虫禁止访问开关 默认 0 只检测不阻止访问 1 检测并阻止访问)", "int", "", configMap)
updateConfigIntItem(initLoad, "debug", "enable_debug", global.GCONFIG_RECORD_DEBUG_ENABLE, "调试开关 默认关闭", "int", "", configMap)
updateConfigStringItem(initLoad, "debug", "debug_pwd", global.GCONFIG_RECORD_DEBUG_PWD, "调试密码 如果未空则不需要密码", "string", "", configMap)
updateConfigStringItem(initLoad, "gpt", "gpt_url", global.GCONFIG_RECORD_GPT_URL, "GPT远程地址 默认:DeepSeek ,符合ChatGpt或者使用one-api封装好的接口都可以", "string", "", configMap)
updateConfigStringItem(initLoad, "gpt", "gpt_token", global.GCONFIG_RECORD_GPT_TOKEN, "GPT远程授权密钥", "string", "", configMap)
updateConfigStringItem(initLoad, "gpt", "gpt_model", global.GCONFIG_RECORD_GPT_MODEL, "GPT模型名称", "string", "", configMap)
updateConfigIntItem(initLoad, "security", "hide_server_header", global.GCONFIG_RECORD_HIDE_SERVER_HEADER, "是否隐藏Server响应头(1隐藏 0不隐藏)", "int", "", configMap)
updateConfigIntItem(initLoad, "security", "force_bind_2fa", global.GCONFIG_RECORD_FORCE_BIND_2FA, "是否强制绑定双因素认证(1强制 0不强制)", "options", "0|不强制,1|强制", configMap)
updateConfigIntItem(initLoad, "system", "fake_spider_captcha", global.GCONFIG_RECORD_FAKE_SPIDER_CAPTCHA, "伪爬虫进行图形挑战开关 0 放过 1 显示图形验证码", "int", "", configMap)
updateConfigIntItem(initLoad, "system", "enable_proxy_protocol", global.GCONFIG_ENABLE_PROXY_PROTOCOL, "是否启用Proxy Protocol(1启用 0禁用)", "options", "0|禁用,1|启用", configMap)
updateConfigIntItem(initLoad, "system", "enable_system_stats_push", global.GCONFIG_ENABLE_SYSTEM_STATS_PUSH, "是否启用系统统计数据推送(1启用 0禁用)", "options", "0|禁用,1|启用", configMap)
// 指纹认证相关配置
updateConfigIntItem(initLoad, "system", "allow_container_selfupdate", global.GCONFIG_ALLOW_CONTAINER_SELFUPDATE, "容器(Docker等)环境是否允许应用内升级(0拦截 1允许,默认拦截)。容器里的程序文件在镜像可写层,应用内升级只在本次容器生命周期有效,容器重建后会回退成镜像自带的旧版本,而数据库已被新版本迁移且无法回退,会造成\"旧程序+新库\"的不一致。容器环境请改用更新镜像的方式升级;确实把程序文件挂到卷里的用户可置1自行放行", "options", "0|拦截,1|允许", configMap)
updateConfigIntItem(initLoad, "security", "enable_device_fingerprint", global.GCONFIG_ENABLE_DEVICE_FINGERPRINT, "是否启用设备指纹认证(1启用 0禁用,默认禁用)。指纹由浏览器 UA、Accept-Language、Accept-Encoding 计算;若管理端处于反向代理/CDN 之后,这些请求头可能被改写,开启后会被判为设备不匹配而频繁掉线,请确认链路稳定后再启用", "options", "0|禁用,1|启用", configMap)
updateConfigIntItem(initLoad, "security", "enable_strict_ip_binding", global.GCONFIG_ENABLE_STRICT_IP_BINDING, "是否启用严格IP绑定(1启用 0禁用;启用后令牌绑定登录时的真实IP,IP变化需重新登录;反向代理后请先配置可信代理网段,否则按代理IP判定)", "options", "0|禁用,1|启用", configMap)
//数据库相关
updateConfigIntItem(initLoad, "database", "batch_insert", global.GDATA_BATCH_INSERT, "数据库批量插入数量", "int", "", configMap)
updateConfigIntItem(initLoad, "database", "log_persist_enable", global.GCONFIG_LOG_PERSIST_ENABLED, "是否开启日志持久化(1开启 0关闭)", "options", "0|关闭,1|开启", configMap)
updateConfigIntItem(initLoad, "database", "ip_tag_db", global.GDATA_IP_TAG_DB, "IP Tag 存放位置 0 是主库 1是读取 stat库", "int", "", configMap)
updateConfigStringItem(initLoad, "system", "attack_tag_exclude", global.GCONFIG_ATTACK_TAG_EXCLUDE, "风险日志不算风险的标签(逗号分隔,如ACME证书校验),这些标签不出现在规则筛选里也不计入阻止数量;正常 始终排除", "string", "", configMap)
updateConfigStringItem(initLoad, "system", "exclude_ip_log", global.GCONFIG_EXCLUDE_IP_LOG, "全局排除记录日志的IP(所有站点生效):单IP/CIDR/通配符/区间,或 group:组短码 引用IP组;逗号或换行分隔,# 开头为注释。只静音正常请求,安全事件照常记录;站点级清单在网站编辑里", "string", "", configMap)
// IP失败封禁相关配置
updateConfigStringItem(initLoad, "security", "ip_failure_status_codes", global.GCONFIG_IP_FAILURE_STATUS_CODES, "失败状态码配置,支持多个用|分隔,也支持正则表达式,例如:401|403|404|444|429|503 或 ^4[0-9]{2}$", "string", "", configMap)
updateConfigIntItem(initLoad, "security", "ip_failure_ban_enabled", global.GCONFIG_IP_FAILURE_BAN_ENABLED, "是否启用IP失败封禁(1启用 0禁用)", "options", "0|禁用,1|启用", configMap)
updateConfigIntItem(initLoad, "security", "ip_failure_ban_lock_time", global.GCONFIG_IP_FAILURE_BAN_LOCK_TIME, "IP失败封禁锁定时间(单位:分钟,默认10分钟)", "int", "", configMap)
// 规则封禁相关配置(自定义规则 RF.Ban(分钟) 命中后临时封禁客户端IP)
updateConfigIntItem(initLoad, "security", "rule_ban_mode", global.GCONFIG_RULE_BAN_MODE, "规则封禁执行动作(0返回拦截页 1直接断开连接)", "options", "0|拦截页,1|直接断开", configMap)
// 主机远程登录爆破防护(SSH/RDP)。封禁时长不在这里配,由「封禁阶梯」表接管(5分→15分→60分→1天→永久)
updateConfigIntItem(initLoad, "hostguard", "host_guard_enabled", global.GCONFIG_HOST_GUARD_ENABLED, "主机远程登录爆破防护总开关(保护SamWaf所在机器自身的SSH/RDP。启用前请先确认白名单已包含你的管理IP,否则可能把自己锁在门外)", "options", "0|禁用,1|启用", configMap)
updateConfigStringItem(initLoad, "hostguard", "host_guard_mode", global.GCONFIG_HOST_GUARD_MODE, "工作模式:observe=只记录不封禁(建议先跑一周确认无误封),block=达到阈值即调用系统防火墙封禁", "options", "observe|观察模式,block|封禁模式", configMap)
updateConfigIntItem(initLoad, "hostguard", "host_guard_find_time", global.GCONFIG_HOST_GUARD_FIND_TIME, "失败统计窗口(单位:分钟,默认10分钟)", "int", "", configMap)
updateConfigIntItem(initLoad, "hostguard", "host_guard_max_retry", global.GCONFIG_HOST_GUARD_MAX_RETRY, "统计窗口内失败次数达到该值即触发处置(默认8次)", "int", "", configMap)
updateConfigIntItem(initLoad, "hostguard", "host_guard_offender_reset_day", global.GCONFIG_HOST_GUARD_OFFENDER_RESET_DAY, "累犯记忆期(单位:天,默认7天):超过这么久没再犯,下次封禁从第1级重新开始", "int", "", configMap)
updateConfigIntItem(initLoad, "hostguard", "host_guard_count_soft_fail", global.GCONFIG_HOST_GUARD_COUNT_SOFT_FAIL, "软失败是否计入阈值(preauth断连/用户名枚举/PAM失败行)。前者由端口扫描与健康探针大量产生,后两者会与密码失败行成对出现导致阈值腰斩,默认不计入", "options", "0|不计入,1|计入", configMap)
updateConfigStringItem(initLoad, "hostguard", "host_guard_whitelist", global.GCONFIG_HOST_GUARD_WHITELIST, "永不封禁的IP/网段白名单,逗号分隔,支持单IP/CIDR/通配符/区间(如 1.2.3.4,10.0.0.0/8,192.168.1.*)", "string", "", configMap)
updateConfigIntItem(initLoad, "hostguard", "host_guard_auto_lan", global.GCONFIG_HOST_GUARD_AUTO_LAN, "是否自动豁免本机所有网卡IP、环回地址与常见内网段(10/8、172.16/12、192.168/16、fc00::/7、100.64/10、169.254/16)", "options", "0|不豁免,1|自动豁免", configMap)
updateConfigStringItem(initLoad, "hostguard", "host_guard_log_paths", global.GCONFIG_HOST_GUARD_LOG_PATHS, "自定义系统认证日志路径(逗号分隔),留空则自动探测 /var/log/secure、/var/log/auth.log,都没有则用 journalctl。容器部署请把宿主机日志只读挂载进来并在此指定路径", "string", "", configMap)
updateConfigStringItem(initLoad, "hostguard", "host_guard_ssh_ports", global.GCONFIG_HOST_GUARD_SSH_PORTS, "SSH实际监听端口(逗号分隔),留空自动发现。注意:爆破检测本身不依赖端口,改过默认端口也能正常工作,此项仅用于端口级封禁与连接看板高亮", "string", "", configMap)
updateConfigStringItem(initLoad, "hostguard", "host_guard_rdp_ports", global.GCONFIG_HOST_GUARD_RDP_PORTS, "RDP实际监听端口(逗号分隔),留空自动发现", "string", "", configMap)
updateConfigStringItem(initLoad, "hostguard", "host_guard_port_scope", global.GCONFIG_HOST_GUARD_PORT_SCOPE, "封禁范围:all=封全端口(更安全),detected=只封SSH/RDP端口(误封时杀伤面更小)", "options", "all|全端口,detected|仅SSH/RDP端口", configMap)
updateConfigStringItem(initLoad, "hostguard", "host_guard_exec_mode", global.GCONFIG_HOST_GUARD_EXEC_MODE, "封禁执行方式:auto=平台自适应(推荐),ipset=强制走集合,rule=强制逐条防火墙规则(调试用)", "options", "auto|自动,ipset|集合,rule|逐条规则", configMap)
updateConfigIntItem(initLoad, "hostguard", "host_guard_debounce_sec", global.GCONFIG_HOST_GUARD_DEBOUNCE_SEC, "Windows封禁同步去抖窗口(单位:秒,默认5)。Windows无ipset只能全量重建规则,去抖可避免频繁封禁时反复重建;该值也是封禁生效的最大延迟;Linux/macOS走增量不受此影响", "int", "", configMap)
updateConfigIntItem(initLoad, "hostguard", "host_guard_max_ban_entries", global.GCONFIG_HOST_GUARD_MAX_BAN_ENTRIES, "封禁集合容量上限(默认10000)。超限时优先淘汰剩余时间最短的临时封禁,永久封禁不淘汰", "int", "", configMap)
updateConfigIntItem(initLoad, "hostguard", "host_guard_ban_rate_limit", global.GCONFIG_HOST_GUARD_BAN_RATE_LIMIT, "每分钟最多新增封禁数(默认200),防止分布式爆破把封禁集合瞬间打满", "int", "", configMap)
updateConfigIntItem(initLoad, "hostguard", "host_guard_subnet_aggregate", global.GCONFIG_HOST_GUARD_SUBNET_AGGREGATE, "网段聚合封禁:同一/24网段内被封IP数达到阈值时,升级为封禁整个网段。对僵尸网络有效但误伤面大(可能封掉整个机房/运营商段),默认关闭", "options", "0|禁用,1|启用", configMap)
updateConfigIntItem(initLoad, "hostguard", "host_guard_subnet_threshold", global.GCONFIG_HOST_GUARD_SUBNET_THRESHOLD, "网段聚合触发阈值(同一/24内被封IP数,默认10)", "int", "", configMap)
updateConfigIntItem(initLoad, "hostguard", "host_guard_notify", global.GCONFIG_HOST_GUARD_NOTIFY, "触发封禁时是否发送通知(复用「IP封禁」消息类型,可在通知订阅里配置渠道与频控)", "options", "0|不通知,1|通知", configMap)
// 远程连接看板
updateConfigIntItem(initLoad, "hostguard", "host_conn_enabled", global.GCONFIG_HOST_CONN_ENABLED, "远程连接看板开关(展示当前连接到本机的所有TCP连接)", "options", "0|禁用,1|启用", configMap)
updateConfigIntItem(initLoad, "hostguard", "host_conn_cache_sec", global.GCONFIG_HOST_CONN_CACHE_SEC, "连接快照缓存秒数(默认3秒)。Linux下采集需要遍历/proc建立inode到进程的映射,连接数上万时开销明显,建议不低于3秒", "int", "", configMap)
// 版本更新相关配置
updateConfigIntItem(initLoad, "system", "check_beta_version", global.GCONFIG_CHECK_BETA_VERSION, "是否检测beta版本更新(1启用 0禁用)", "options", "0|禁用,1|启用", configMap)
// ZeroSSL 相关配置
updateConfigStringItem(initLoad, "ssl", "zerossl_access_key", global.GCONFIG_ZEROSSL_ACCESS_KEY, "zerossl访问key", "string", "", configMap)
updateConfigStringItem(initLoad, "ssl", "zerossl_eab_kid", global.GCONFIG_ZEROSSL_EAB_KID, "zerossl eab_kid", "string", "", configMap)
updateConfigStringItem(initLoad, "ssl", "zerossl_eab_hmac_key", global.GCONFIG_ZEROSSL_EAB_HMAC_KEY, "zerossl eab_hmac_key", "string", "", configMap)
// IP数据库配置
updateConfigStringItem(initLoad, "ip_database", "ip_v4_source",
global.GCONFIG_IP_V4_SOURCE, "IPv4数据库来源",
"options", "ip2region|ip2region,geolite2|GeoLite2", configMap)
updateConfigStringItem(initLoad, "ip_database", "ip_v6_source",
global.GCONFIG_IP_V6_SOURCE, "IPv6数据库来源",
"options", "ip2region|ip2region,geolite2|GeoLite2", configMap)
updateConfigStringItem(initLoad, "ip_database", "ip_v4_format",
global.GCONFIG_IP_V4_FORMAT, "IPv4 xdb字段格式",
"options", "legacy|老版本,opensource|开源版,full|满载版,standard|标准版,compact|精简版", configMap)
updateConfigStringItem(initLoad, "ip_database", "ip_v6_format",
global.GCONFIG_IP_V6_FORMAT, "IPv6 xdb字段格式(仅ip2region时有效)",
"options", "legacy|老版本,opensource|开源版,full|满载版,standard|标准版,compact|精简版", configMap)
// 日志文件写入相关配置
updateConfigIntItem(initLoad, "logfile", "log_file_write_enable", global.GCONFIG_LOG_FILE_WRITE_ENABLE, "日志文件写入开关(0关闭 1开启)", "options", "0|关闭,1|开启", configMap)
updateConfigStringItem(initLoad, "logfile", "log_file_write_path", global.GCONFIG_LOG_FILE_WRITE_PATH, "日志文件输出路径", "string", "", configMap)
updateConfigStringItem(initLoad, "logfile", "log_file_write_format", global.GCONFIG_LOG_FILE_WRITE_FORMAT, "日志格式(nginx/apache/custom)", "options", "nginx|Nginx Combined,apache|Apache Combined,custom|自定义格式", configMap)
updateConfigStringItem(initLoad, "logfile", "log_file_write_custom_tpl", global.GCONFIG_LOG_FILE_WRITE_CUSTOM_TPL, "自定义日志格式模板", "string", "", configMap)
updateConfigIntItem(initLoad, "logfile", "log_file_write_max_size", global.GCONFIG_LOG_FILE_WRITE_MAX_SIZE, "单个日志文件最大大小(MB)", "int", "", configMap)
updateConfigIntItem(initLoad, "logfile", "log_file_write_max_backups", global.GCONFIG_LOG_FILE_WRITE_MAX_BACKUPS, "保留的历史文件数量", "int", "", configMap)
updateConfigIntItem(initLoad, "logfile", "log_file_write_max_days", global.GCONFIG_LOG_FILE_WRITE_MAX_DAYS, "保留天数", "int", "", configMap)
updateConfigIntItem(initLoad, "logfile", "log_file_write_compress", global.GCONFIG_LOG_FILE_WRITE_COMPRESS, "是否压缩历史文件(0关闭 1开启)", "options", "0|关闭,1|开启", configMap)
// 开放平台配置
updateConfigIntItem(initLoad, "openplatform", "open_platform_enabled", global.GCONFIG_OPEN_PLATFORM_ENABLED, "开放平台开关(1启用 0禁用)", "options", "0|禁用,1|启用", configMap)
// 任务日志配置
updateConfigIntItem(initLoad, "task", "task_log_retain_days", global.GCONFIG_TASK_LOG_RETAIN_DAYS, "任务日志保留天数(默认30天)", "int", "", configMap)
// 安全提示(升级感知):配了管理端代理头但未设可信代理网段 → 代理头被忽略、客户端IP取网络层IP。
// 提醒反向代理后的部署在 conf/config.yml 配置 security.manage_trusted_proxies,
// 以免 IP白名单/登录锁定误按代理IP生效(该项放 config.yml 便于被白名单挡住时改文件+重启自救)。
if initLoad && global.GCONFIG_MANAGE_PROXY_HEADER != "" && global.GCONFIG_MANAGE_TRUSTED_PROXIES == "" {
zlog.Warn("管理端已配置代理头(gwaf_manage_proxy_header)但未设可信代理网段:出于安全,代理头将被忽略、按网络层IP识别客户端。若本机在反向代理之后,请在 conf/config.yml 填写 security.manage_trusted_proxies(如 10.0.0.0/8,容器部署可填 private)后重启")
}
// 可信代理网段过宽 → 无法据此判定代理头里哪个 IP 是客户端,代理头不予采信。
if initLoad {
if broad, entry := utils.ManageTrustedProxiesHasOverBroad(global.GCONFIG_MANAGE_TRUSTED_PROXIES); broad {
zlog.Warn("管理端可信代理网段包含过宽条目(" + entry + "):这种网段无法用来判定代理头里的哪个IP是客户端,代理头将不被采信、仍按网络层IP识别客户端(管理端IP白名单/登录失败锁定/令牌IP绑定也按它判定)。请在 conf/config.yml 把 security.manage_trusted_proxies 改成上游代理自身的地址,容器部署可填 private")
}
}
// 把落在另一个库里的 IP 标签收回当前归属。按内容判断、源库空即返回,
// 所以更早版本切换归属时留下的历史也能在这里被收回来,重复跑无副作用。
// 合并前先清存量「正常」行(分层后不再记它):那是表里的大头,清完合并要搬的行数能少几个数量级。
if initLoad {
go func() {
waf_service.CleanLegacyBenignIPTags()
waf_service.MergeIPTagsInto(global.GDATA_IP_TAG_DB)
}()
}
}