Files

1675 lines
69 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 main
import (
"SamWaf/cache"
"SamWaf/common/gwebsocket"
"SamWaf/common/tasklog"
"SamWaf/common/wafexec"
"SamWaf/common/zlog"
"SamWaf/enums"
"SamWaf/global"
"SamWaf/globalobj"
"SamWaf/innerbean"
"SamWaf/iplocation"
"SamWaf/model"
"SamWaf/model/wafenginmodel"
"SamWaf/plugin"
"SamWaf/public"
"SamWaf/service/waf_service"
"SamWaf/supervisor"
"SamWaf/utils"
"SamWaf/wafai"
"SamWaf/wafappengine"
"SamWaf/wafconfig"
"SamWaf/wafdb"
"SamWaf/wafdiag"
"SamWaf/wafenginecore"
"SamWaf/wafenginecore/wafcaptcha"
"SamWaf/wafhostguard"
"SamWaf/wafinit"
"SamWaf/wafipban"
"SamWaf/wafipc"
"SamWaf/wafmangeweb"
"SamWaf/wafnotify"
"SamWaf/wafowasp"
"SamWaf/wafqueue"
"SamWaf/wafreg"
"SamWaf/wafsafeclear"
"SamWaf/wafsec"
"SamWaf/wafsnowflake"
"SamWaf/waftask"
"SamWaf/waftunnelengine"
"SamWaf/wafupdate"
"crypto/tls"
"embed"
_ "embed"
"fmt"
"log"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"path/filepath"
"runtime"
"runtime/debug"
"strconv"
"strings"
"sync"
"syscall"
"time"
_ "time/tzdata"
dlp "github.com/bytedance/godlp"
"github.com/kardianos/service"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gorm.io/gorm"
)
//go:embed exedata/ip2region.xdb
var Ip2regionBytes []byte // 当前目录,解析为[]byte类型
// GeoLite2-Country.mmdb 自 1.3.24-beta.6 起不再内嵌,
// 把它编进发行的二进制等同于再分发。需要 GeoLite2 的用户可自行到 MaxMind 官网下载 mmdb
// 放进 data/ 目录,程序会照常加载。IPv6 默认改用 ip2region(Apache-2.0,可自由分发)。
//go:embed exedata/ldpconfig.yml
var ldpConfig string //隐私防护ldp
//go:embed exedata/public_key.pem
var publicKey string //公钥key
//go:embed exedata/owasp
var owaspAssets embed.FS
//go:embed exedata/spiderbot
var spiderBotAssets embed.FS
//go:embed exedata/captcha
var captchaAssets embed.FS
//go:embed exedata/capjs
var capjs embed.FS
//go:embed exedata/httpauth
var httpauth embed.FS
//go:embed exedata/access
var accessAssets embed.FS
// wafSystenService 实现了 service.Service 接口
type wafSystenService struct{}
var webmanager *wafmangeweb.WafWebManager // web管理端
// Start 是服务启动时调用的方法
func (m *wafSystenService) Start(s service.Service) error {
zlog.Info("服务启动形式-----Start")
go m.run()
return nil
}
// Stop 是服务停止时调用的方法
func (m *wafSystenService) Stop(s service.Service) error {
zlog.Info("服务形式的 -----stop")
m.Graceful()
return nil
}
// 守护协程
func NeverExit(name string, f func()) {
defer func() {
if v := recover(); v != nil {
zlog.Error(fmt.Sprintf("协程%s崩溃了,准备重启一个。 : %v, Stack Trace: %s", name, v, debug.Stack()))
if global.GWAF_RELEASE == "false" {
debug.PrintStack()
}
go NeverExit(name, f) // 重启一个同功能协程
}
}()
zlog.Info(name + " start")
f()
}
// run 是服务的主要逻辑
func (m *wafSystenService) run() {
// 先尝试监听端口,检查是否被占用。
// 升级接管(takeover)模式下旧 Worker 仍持有管理端口 :26666,新 Worker 预期与其并存,故跳过该探测。
if !global.GWAF_RUNTIME_IS_TAKEOVER {
listener, err := net.Listen("tcp", ":"+strconv.Itoa(global.GWAF_LOCAL_SERVER_PORT))
if err != nil {
errMsg := fmt.Sprintf("管理界面端口 %d 已被占用,请检查并修改配置(conf/config.yml local_port字段)或关闭占用该端口的程序: %s",
global.GWAF_LOCAL_SERVER_PORT, err.Error())
zlog.Error(errMsg)
panic(errMsg)
}
if listener != nil {
listener.Close()
}
}
// 获取当前执行文件的路径
executablePath, err := os.Executable()
if err != nil {
fmt.Println("Error:", err)
return
}
zlog.Info("执行位置:", executablePath)
global.GWAF_RUNTIME_CURRENT_EXEPATH = executablePath
//初始化步骤[加载ip数据库]
// 创建 IP Location Manager
global.GIPLOCATION_MANAGER = iplocation.NewManager()
// 注册内置数据库,供 manager 在磁盘无文件时兜底加载。
// GeoLite2 已去内嵌,这里只剩 IPv4 的 ip2region 一份兜底。
iplocation.SetBuiltinData(Ip2regionBytes, nil)
// 加载优先级与热重载共用同一份实现(磁盘文件 > 内置数据 > 同类型其它来源降级)。
// IP 库属于「有更好、没有也得能跑」的辅助数据:任何加载失败都只告警,
// 绝不能阻止 WAF 启动——防护能力不依赖地区库。
if err := global.GIPLOCATION_MANAGER.ReloadFromConfig(
filepath.Join(utils.GetCurrentDir(), "data"),
global.GCONFIG_IP_V4_SOURCE, global.GCONFIG_IP_V6_SOURCE,
global.GCONFIG_IP_V4_FORMAT, global.GCONFIG_IP_V6_FORMAT,
); err != nil {
zlog.Warn("IP地理位置数据库加载失败,地区相关功能将不可用: ", err.Error())
}
if st := global.GIPLOCATION_MANAGER.GetStatus(); st != nil {
zlog.Info(fmt.Sprintf("IP库加载完成 IPv4:%s(内置:%v,%d字节) IPv6:%s(内置:%v,%d字节)",
st.IPv4Source, st.IPv4Builtin, st.IPv4FileSize,
st.IPv6Source, st.IPv6Builtin, st.IPv6FileSize))
if st.IPv6FileSize == 0 {
zlog.Warn("IPv6 地区库不可用:IPv6 访客的归属地将显示为未知,且地区类自定义规则对 IPv6 请求不生效。" +
"可在【IP库管理】里在线下载,或自行下载 ip2region_v6.xdb 放入 data/ 目录")
}
}
global.GWAF_DLP_CONFIG = ldpConfig
global.GWAF_REG_PUBLIC_KEY = publicKey
//初始化AI智能检测器,若存在已上传的模型包则加载(失败安全,不影响启动)
global.GWAF_AI_DETECTOR = wafai.NewDetector()
aiModelPath := filepath.Join(utils.GetCurrentDir(), "data", "ai_model", "current.swai")
if _, err := os.Stat(aiModelPath); err == nil {
if manifest, err := global.GWAF_AI_DETECTOR.LoadFromFile(aiModelPath); err != nil {
zlog.Warn("AI模型加载失败,AI检测将不可用: ", err.Error())
} else {
zlog.Info(fmt.Sprintf("AI模型加载成功: version=%s feature=%s type=%s",
manifest.ModelVersion, manifest.FeatureVersion, manifest.ModelType))
}
}
//owasp资源 释放
err = wafinit.CheckAndReleaseDataset(owaspAssets, utils.GetCurrentDir()+"/data/owasp", "owasp")
if err != nil {
zlog.Error("owasp", err.Error())
}
// 验证码资源释放
err = wafinit.CheckAndReleaseDataset(captchaAssets, utils.GetCurrentDir()+"/data/captcha", "captcha")
if err != nil {
zlog.Error("captcha", err.Error())
}
// 验证码工作量证明资源释放
err = wafinit.CheckAndReleaseDataset(capjs, utils.GetCurrentDir()+"/data/capjs", "capjs")
if err != nil {
zlog.Error("capJs", err.Error())
}
// HTTP认证登录页面资源释放
err = wafinit.CheckAndReleaseDataset(httpauth, utils.GetCurrentDir()+"/data/httpauth", "httpauth")
if err != nil {
zlog.Error("httpauth", err.Error())
}
// 统一访问认证(Access 模式)登录页资源释放
err = wafinit.CheckAndReleaseDataset(accessAssets, utils.GetCurrentDir()+"/data/access", "access")
if err != nil {
zlog.Error("access", err.Error())
}
//TODO 准备释放最新spider bot
//初始化cache
{
cacheStore, err := cache.NewCacheStore(global.GCACHE_TYPE, &cache.RedisCacheConfig{
Host: global.GCACHE_REDIS_HOST,
Port: global.GCACHE_REDIS_PORT,
Password: global.GCACHE_REDIS_PASSWORD,
DB: global.GCACHE_REDIS_DB,
PoolSize: global.GCACHE_REDIS_POOL_SIZE,
PoolTimeout: time.Duration(global.GCACHE_REDIS_POOL_TIMEOUT_SEC) * time.Second,
OpTimeout: time.Duration(global.GCACHE_REDIS_OP_TIMEOUT_SEC) * time.Second,
})
if err != nil {
zlog.Error("初始化缓存失败,程序退出,请检查conf/config.yml缓存配置是否正确", "error", err)
os.Exit(1)
}
global.GCACHE_WAFCACHE = cacheStore
// 缓存后端决定了令牌等凭证放在哪里,排障时是第一个要确认的事实,启动就写进日志。
if d, ok := cacheStore.(cache.BackendDescriber); ok {
zlog.Info("缓存后端: " + d.Describe())
} else {
zlog.Info("缓存后端: memory(进程内)")
}
}
//初始化验证码服务
wafcaptcha.InitCaptchaService(global.GCACHE_WAFCACHE)
//初始化锁写不锁度
global.GWAF_MEASURE_PROCESS_DEQUEENGINE = cache.InitWafOnlyLockWrite()
// 创建 Snowflake 实例
global.GWAF_SNOWFLAKE_GEN = wafsnowflake.NewSnowflake(1609459200000, 1, 1) // 设置epoch时间、机器ID和数据中心ID
// 注入 IP 库在线下载上下文:iplocation 不能反向依赖 global/utils(会成环),
// 所以升级源、SSRF 安全客户端、通知回调都从这里传进去。
iplocation.ConfigureUpgrader(iplocation.UpgradeConfig{
UpdateVersionURL: global.GUPDATE_VERSION_URL,
ClientQuery: wafupdate.ClientQuery,
NewClient: utils.SafeHTTPClient,
ValidateURL: utils.IsSafeOutboundURL,
NotifyFunc: func(success bool, msg string) {
if global.GQEQUE_MESSAGE_DB == nil {
return
}
successStr := "false"
if success {
successStr = "true"
}
global.GQEQUE_MESSAGE_DB.Enqueue(innerbean.UpdateResultMessageInfo{
BaseMessageInfo: innerbean.BaseMessageInfo{
OperaType: "IP库下载",
Server: global.GWAF_CUSTOM_SERVER_NAME,
},
Msg: msg,
Success: successStr,
})
},
})
// 创建owasp 管理器(支持热重载)
global.GWAF_OWASP_MANAGER = wafowasp.NewOwaspManager(utils.GetCurrentDir())
global.GWAF_OWASP = global.GWAF_OWASP_MANAGER.Current()
// 注入升级上下文:避免 wafowasp → global 的循环依赖
wafowasp.ConfigureUpgrader(wafowasp.UpgradeConfig{
UpdateVersionURL: global.GUPDATE_VERSION_URL,
ClientQuery: wafupdate.ClientQuery,
NotifyFunc: func(success bool, msg string) {
if global.GQEQUE_MESSAGE_DB == nil {
return
}
successStr := "false"
if success {
successStr = "true"
}
global.GQEQUE_MESSAGE_DB.Enqueue(innerbean.UpdateResultMessageInfo{
BaseMessageInfo: innerbean.BaseMessageInfo{
OperaType: "OWASP 规则升级",
Server: global.GWAF_CUSTOM_SERVER_NAME,
},
Msg: msg,
Success: successStr,
})
},
})
// 初始化ip ban
wafipban.InitIPBanManager(global.GCACHE_WAFCACHE)
// 主机登录失败计数器:与上面共用同一个缓存,但 key 前缀独立。
// 不共用 keyspace 是因为上面那套计数会被自定义规则的 MF.GetIPFailureCount 读取,
// 混入 SSH/RDP 的失败会静默改变用户已有 WAF 规则的语义。
wafipban.InitHostLoginFailureManager(global.GCACHE_WAFCACHE)
// 把事件落库能力注入主机防护引擎(反向注入避免 service 与 wafhostguard 循环依赖)
waf_service.WafHostGuardServiceApp.InitEventSink()
//提前初始化
global.GDATA_CURRENT_LOG_DB_MAP = map[string]*gorm.DB{}
rversion := "初始化系统 编译器版本:" + runtime.Version() + " 程序版本号:" + global.GWAF_RELEASE_VERSION_NAME + "(" + global.GWAF_RELEASE_VERSION + ") 前端版本:" + global.GWAF_WEB_VERSION
if global.GWAF_RELEASE == "false" {
rversion = rversion + " 调试版本"
} else {
rversion = rversion + " 发行版本"
}
if runtime.GOOS == "linux" {
rversion = rversion + " linux"
} else if runtime.GOOS == "windows" {
rversion = rversion + " windows"
if utils.IsSupportedWindows7Version() {
zlog.Info("Now your system is win7 or win2008r2.")
}
if global.GWAF_RUNTIME_WIN7_VERSION == "true" && utils.IsSupportedWindows7Version() == false {
zlog.Error("Now your use is win7 or win2008r2 special version,We recommend you download normal version")
}
} else {
rversion = rversion + " " + runtime.GOOS
}
if global.GWAF_RUNTIME_WIN7_VERSION == "true" {
rversion = rversion + " Win7内核版本"
}
rversion = rversion + " plat:" + runtime.GOOS + "-" + runtime.GOARCH
zlog.Info(rversion)
zlog.Info("OutIp", global.GWAF_RUNTIME_IP)
if global.GWAF_RELEASE == "false" {
global.GUPDATE_VERSION_URL = "http://127.0.0.1:8111/"
}
//初始化每实例静态数据加密密钥(DEK):首启生成 data/.keys/data_key(0600),或用
//config.yml 的 security.data_key_file 指定自管路径。须在数据库初始化之前就绪,
//否则落库敏感字段(Access 密钥/CDN 凭证/2FA 密钥)无法按新格式加解密。
if err := wafsec.InitDataKey(utils.GetCurrentDir(), global.GCONFIG_DATA_KEY_FILE); err != nil {
zlog.Error("初始化数据加密密钥失败,程序退出,请检查 data/.keys 目录权限或 security.data_key_file 配置", "error", err)
os.Exit(1)
}
//初始化每实例传输密钥(X25519):首启生成 data/.keys/comm_key(0600),供管理端 v2 通道
//与客户端协商每会话密钥。失败不退出——传输层还有 legacy 通道可用,管理端不至于打不开。
if err := wafsec.InitCommKey(utils.GetCurrentDir(), global.GCONFIG_COMM_KEY_FILE); err != nil {
zlog.Error("初始化传输密钥失败,管理端将只提供 legacy 传输通道,请检查 data/.keys 目录权限或 security.comm_key_file 配置", "error", err)
}
//初始化本地数据库
if _, err := wafdb.InitCoreDb(""); err != nil {
zlog.Error("初始化核心数据库失败,程序退出,请检查conf/config.yml数据库配置是否正确", "error", err)
os.Exit(1)
}
//存量敏感字段静态加密迁移:把旧格式密文一次性重写为每实例 DEK 密文(幂等,单行失败跳过)。
wafdb.MigrateDataKeyEncryption(global.GWAF_LOCAL_DB)
if _, err := wafdb.InitLogDb(""); err != nil {
zlog.Error("初始化日志数据库失败,程序退出,请检查conf/config.yml数据库配置是否正确", "error", err)
os.Exit(1)
}
if _, err := wafdb.InitStatsDb(""); err != nil {
zlog.Error("初始化统计数据库失败,程序退出,请检查conf/config.yml数据库配置是否正确", "error", err)
os.Exit(1)
}
// 分层改造的边界切换要赶在接流量之前:条件是新表为空,一旦有请求写进来就不再成立
waftask.CutTierBoundaryIfNeeded()
// 全新安装引导:账户表为空(新用户判定)时创建默认管理员并生成随机初始口令。
// 放在启动初始化(核心库就绪后)执行,不再等首次登录才触发,便于新装即时拿到 data/initial_password.txt。
isFreshInstall := false
if cnt, _ := waf_service.WafAccountServiceApp.GetAccountCountApi(); cnt == 0 {
if err := waf_service.WafAccountServiceApp.InitDefaultAccount(); err != nil {
zlog.Error("初始化默认管理员账户失败", "error", err)
} else {
isFreshInstall = true
}
}
//初始化插件系统(从配置文件加载)
if err := plugin.InitPluginSystem(); err != nil {
zlog.Error("初始化插件系统失败", "error", err)
// 插件系统初始化失败不影响主程序启动
}
// WebSocket 在线表必须先于下面的消费协程和管理端建好:消息队列一开始消费就可能广播,
// 管理端一起来就可能有连接注册进来,而这里只是建两个空 map,没有任何外部依赖,放早没有副作用。
global.GWebSocket = gwebsocket.InitWafWebSocket()
//初始化队列引擎
wafqueue.InitDequeEngine()
//启动队列消费
go NeverExit("ProcessCoreDequeEngine", wafqueue.ProcessCoreDequeEngine)
go NeverExit("ProcessMessageDequeEngine", wafqueue.ProcessMessageDequeEngine)
go NeverExit("ProcessStatDequeEngine", wafqueue.ProcessStatDequeEngine)
go NeverExit("ProcessLogDequeEngine", wafqueue.ProcessLogDequeEngine)
//运行诊断:注册队列/缓存/WebSocket 计量器并启动 10s 趋势采样(内存环形缓冲,不落盘)
wafdiag.RegisterBuiltins()
wafdiag.StartSampler()
//初始化一次系统参数信息
waftask.TaskLoadSetting(true)
// 校验"上次运行版本",识别容器重建导致的降级运行(旧程序+新库),仅记录日志不阻断
waftask.CheckVersionDowngrade()
// IP 数据库:配置已从 DB 读出,按持久化的数据源权威重载一次
// (启动早期 manager 用的是默认源,此处才是真正生效的加载点,解决重启后 ipdb 回退到 ip2region 的问题)
if global.GIPLOCATION_MANAGER != nil {
ipDataDir := filepath.Join(utils.GetCurrentDir(), "data")
if err := global.GIPLOCATION_MANAGER.ReloadFromConfig(
ipDataDir,
global.GCONFIG_IP_V4_SOURCE, global.GCONFIG_IP_V6_SOURCE,
global.GCONFIG_IP_V4_FORMAT, global.GCONFIG_IP_V6_FORMAT,
); err != nil {
zlog.Warn("IP 数据库按配置重载失败: ", err)
}
}
// 初始化任务日志管理器(需在 TaskLoadSetting 后执行,以获取正确的 retainDays 配置)
taskLogDir := filepath.Join(utils.GetCurrentDir(), "logs", "task")
tasklog.InitGlobalTaskLogManager(taskLogDir, int(global.GCONFIG_TASK_LOG_RETAIN_DAYS))
// 将 TaskZapCore 挂到全局 zlog,使任务执行期间的 zlog 输出同步写入任务日志文件
zlog.AddCore(tasklog.NewTaskZapCore(zapcore.DebugLevel))
//启动通知相关程序
global.GNOTIFY_KAKFA_SERVICE = wafnotify.InitNotifyKafkaEngine(global.GCONFIG_RECORD_KAFKA_ENABLE, global.GCONFIG_RECORD_KAFKA_URL, global.GCONFIG_RECORD_KAFKA_TOPIC) //kafka
// 日志文件写入
compressFlag := global.GCONFIG_LOG_FILE_WRITE_COMPRESS == 1
global.GNOTIFY_LOG_FILE_WRITER = wafnotify.InitLogFileWriterEngine(
global.GCONFIG_LOG_FILE_WRITE_ENABLE,
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),
compressFlag,
)
//启动waf
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE = &wafenginecore.WafEngine{
//路由表(HostTarget/HostCode/HostTargetNoPort/HostTargetMoreDomain)改为 RCU 快照,
//下方 InitRouting() 初始化为空表;读走 rt(),写走 copy-on-write 助手。
ServerOnline: wafenginmodel.NewSafeServerMap(),
//所有证书情况 对应端口 可能多个端口都是https 443,或者其他非标准端口也要实现https证书
AllCertificate: wafenginecore.AllCertificate{
Mux: sync.Mutex{},
Map: map[string]*tls.Certificate{},
},
EngineCurrentStatus: 0, // 当前waf引擎状态
Sensitive: make([]model.Sensitive, 0),
PluginManager: globalobj.GWAF_RUNTIME_OBJ_PLUGIN_MANAGER, // 设置插件管理器
}
//初始化路由快照(RCU 空表),之后 StartWaf→LoadAllHost 通过 copy-on-write 填充
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.InitRouting()
//注册引擎侧运行诊断计量器(主机/端口/证书/Transport池等对象数)
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.RegisterDiagProvider()
//IP组匹配集必须在引擎接管流量之前发布完成:LoadHost 只预抽出组短码,
//真正的IP集合来自 ipset 全局快照。这里必须同步调用(不能像威胁情报那样 go 出去)——
//威胁情报晚生效只是少拦一点,IP组承载的是用户显式配置的白名单,晚生效就是误拦合法用户。
waf_service.WafIPGroupServiceApp.RebuildAllGroupMatchers()
//统一访问认证配置同理必须在引擎接管流量之前同步发布:
//访问控制晚生效就是一段裸奔窗口,而这个功能的全部意义就是不让人裸奔。
//快照未发布时 accessgate.Get() 返回"全部关闭"的兜底配置(误放行而非误拦),
//所以这一行的作用是把真实策略尽早顶上去。
waf_service.WafAccessConfigServiceApp.PublishConfig()
http.Handle("/", globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE)
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.StartWaf()
//启动隧道
globalobj.GWAF_RUNTIME_OBJ_TUNNEL_ENGINE = waftunnelengine.NewWafTunnelEngine()
//启动应用管理引擎
globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE = wafappengine.NewWafAppEngine()
// 隧道/应用是独占型单例(独占端口/外部进程),不能与旧 Worker 并存。
// takeover 升级模式下延迟到旧 Worker 退出后由 Supervisor 的 ACTIVATE 触发接管(见 activateSingletons)。
if !global.GWAF_RUNTIME_IS_TAKEOVER {
globalobj.GWAF_RUNTIME_OBJ_TUNNEL_ENGINE.StartTunnel()
globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.StartApps()
} else {
zlog.Info("[Worker] takeover 模式:暂不启动隧道/应用,等待旧 Worker 退出后由 Supervisor 的 ACTIVATE 接管")
}
//启动管理界面
webmanager = &wafmangeweb.WafWebManager{LogName: "WebManager"}
go func() {
webmanager.StartLocalServer()
}()
//定时取规则并更新(考虑后期定时拉取公共规则 待定,可能会影响实际生产)
// 创建任务调度器
global.GWAF_LAST_UPDATE_TIME = time.Now()
//开始 需要添加到任务注册器里面的
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry = waftask.NewTaskRegistry()
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_RUNTIME_QPS_CLEAN, waftask.TaskLogQpsClean)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_HOST_QPS_CLEAN, waftask.TaskHostQpsClean)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_SHARE_DB, waftask.TaskShareDbInfo)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_SHARE_DB_CHECK, waftask.TaskShareDbInfo)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_COUNTER, waftask.TaskCounter)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_DELAY_INFO, waftask.TaskDelayInfo)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_LOAD_CONFIG, waftask.TaskLoadSettingCron)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_REFLUSH_WECHAT_ACCESS_TOKEN, waftask.TaskWechatAccessToken)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_DELETE_HISTORY_INFO, waftask.TaskDeleteHistoryInfo)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_DELETE_HISTORY_DOWNLOAD_FILE, waftask.TaskHistoryDownload)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_SSL_ORDER_RENEW, waftask.SSLOrderReload)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_SSL_PATH_LOAD, waftask.SSLReload)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_BATCH, waftask.BatchTask)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_SSL_EXPIRE_CHECK, waftask.SSLExpireCheck)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_MANAGER_CERT_CHECK, waftask.ManagerCertCheck)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_NOTICE, waftask.TaskStatusNotify)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_HEALTH, waftask.TaskHealth)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_CLEAR_CC_WINDOWS, waftask.TaskCC)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_CLEAR_WEBCACHE, waftask.TaskClearWebcache)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_GC, waftask.TaskGC)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_STATS_PUSH, waftask.TaskStatsPush)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_DB_MONITOR, waftask.TaskDatabaseMonitor)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_FIREWALL_CLEAN_EXPIRED, waftask.TaskFirewallCleanExpired)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_STATS_DATA_CLEANUP, waftask.TaskStatsDataCleanup)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_THREAT_IP_SYNC, waftask.TaskThreatIPSync)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_ACCESS_CLEAN, waftask.TaskAccessClean)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_HTTPAUTH_CLEAN, waftask.TaskHttpAuthClean)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_HOSTGUARD_CLEAN_EXPIRED, waftask.TaskHostGuardCleanExpired)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry.RegisterTask(enums.TASK_TRAFFIC_FLUSH, waftask.TaskTrafficFlush)
// 进程启动重放:把各启用威胁情报渠道的快照重新灌入系统 ipset(内存态重启会丢) 并重建 WAF 并集
go waf_service.WafThreatIPServiceApp.RestoreAllOnStartup()
// 启动重放 CDN 回源段中心库(从库内快照恢复内存匹配集,不对外拉取);
// 到期自动拉取由定时任务按各厂商节奏 + AutoFetch 开关处理
go waf_service.WafCDNIPServiceApp.RestoreAllOnStartup()
go waftask.TaskShareDbInfo()
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskScheduler = waftask.NewTaskScheduler(globalobj.GWAF_RUNTIME_OBJ_WAF_TaskRegistry)
taskDbList := waftask.InitTaskDb()
for _, task := range taskDbList {
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskScheduler.ScheduleTask(task.TaskUnit, task.TaskValue, task.TaskAt, task.TaskMethod, task.TaskDaysOfTheWeek)
}
//结束 需要添加到任务注册器里面的
// cron 任务调度同样是单例(避免新旧 Worker 重复执行定时任务);takeover 模式延迟到 ACTIVATE 再启动。
if !global.GWAF_RUNTIME_IS_TAKEOVER {
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskScheduler.Start()
// 日志导出执行器与任务调度同一接管边界启动,蓝绿升级时只有一个 Worker 受理任务。
logExportWorkerLifecycle.start(waf_service.WafLogExportServiceApp.Start)
// 主机登录防护的日志采集同样是独占单例:两个进程同时 tail 同一份 auth.log,
// 同一条失败会被计两次,而 memory 缓存下两边计数器互不可见,阈值等于被腰斩。
wafhostguard.Start()
// 独占单例已在本函数内联启动完毕,这里把 activateOnce 消费掉,
// 使后续任何 ACTIVATE 都成为空操作。Supervisor 重启后"原地认领"存活 Worker 时
// 会补发一次 ACTIVATE(它无从判断该 Worker 当初是否已接管过单例),
// 没有这道守卫就会把隧道/应用/cron 重复启动一遍。
markSingletonsActivated()
}
//脱敏处理初始化
global.GWAF_DLP, _ = dlp.NewEngine("wafDlp")
err = global.GWAF_DLP.ApplyConfig(ldpConfig)
if err != nil {
zlog.Info("ldp init error", err)
} else {
// 注册自定义脱敏规则
global.GWAF_DLP.RegisterMasker("LoginSensitiveInfoMaskRule", func(in string) (string, error) {
// 分割成键值对
pairs := strings.Split(in, "&")
// 遍历每个键值对,对值进行脱敏处理
for i, pair := range pairs {
keyValue := strings.SplitN(pair, "=", 2)
if len(keyValue) != 2 {
continue
}
value := keyValue[1]
if len(value) > 2 {
value = value[:1] + strings.Repeat("*", len(value)-2) + value[len(value)-1:]
}
pairs[i] = keyValue[0] + "=" + value
}
// 将处理后的键值对重新组合成字符串
return "【已脱敏】" + strings.Join(pairs, "&"), nil
})
}
//加载授权信息
verifyResult, info, err := wafreg.VerifyServerRegByDefaultFile()
if verifyResult {
global.GWAF_REG_INFO = info
zlog.Debug("授权信息 调试信息", info)
expiryDay, isExpiry := wafreg.CheckExpiry(info.ExpiryDate)
if isExpiry {
global.GWAF_REG_INFO.IsExpiry = true
zlog.Info("授权信息已经过期")
} else {
global.GWAF_REG_INFO.IsExpiry = false
zlog.Info("授权信息还剩余:" + strconv.Itoa(expiryDay) + "天")
}
} else {
zlog.Info("regInfo", err)
}
if global.GWAF_SECURITY_ENTRY_ENABLE && global.GWAF_SECURITY_ENTRY_PATH != "" {
zlog.Info("SamWaf has started successfully.")
zlog.Info("Security Entry Path Enabled! Your access code is: " + global.GWAF_SECURITY_ENTRY_PATH)
zlog.Info("Access URL: http://127.0.0.1:" + strconv.Itoa(global.GWAF_LOCAL_SERVER_PORT) + "/" + global.GWAF_SECURITY_ENTRY_PATH + "/")
} else {
zlog.Info("SamWaf has started successfully.You can open http://127.0.0.1:" + strconv.Itoa(global.GWAF_LOCAL_SERVER_PORT) + " in your Browser")
}
// 仅全新安装(本次启动创建了默认管理员)时提示随机初始口令位置;按系统语言环境选择中/英文(默认英文,见 utils.IsChineseEnv 跨平台实现)
if isFreshInstall {
if utils.IsChineseEnv() {
zlog.Info("【全新安装】已生成随机初始管理员口令,请查看当前目录文件 data/initial_password.txt ,并在首次登录后立即修改密码")
} else {
zlog.Info("[Fresh Install] A random initial admin password has been generated, please see file: data/initial_password.txt and change it right after first login")
}
}
for {
select {
case msg := <-global.GWAF_CHAN_MSG:
if _, _hostExists := globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.GetHostByCode(msg.HostCode); _hostExists {
switch msg.Type {
case enums.ChanTypeAllowIP:
ipWhiteLists := msg.Content.([]model.IPAllowList)
// 编译与抽取都在 mutator 外面做:它们是纯函数但可能耗时上百 μs,
// 放进 mutator 会拉长路由表写锁的持有时间
ipWhiteIndex := wafenginecore.BuildIPAllowIndex(ipWhiteLists)
ipWhiteGroupCodes := wafenginecore.ExtractAllowGroupCodes(ipWhiteLists)
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.UpdateHost(msg.HostCode, func(h *wafenginmodel.HostSafe) {
h.IPWhiteLists = ipWhiteLists
h.IPWhiteIndex = ipWhiteIndex
h.IPWhiteGroupCodes = ipWhiteGroupCodes
})
zlog.Debug("远程配置", zap.Any("IPWhiteLists", ipWhiteLists))
break
case enums.ChanTypeAllowURL:
urlWhiteLists := msg.Content.([]model.URLAllowList)
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.UpdateHost(msg.HostCode, func(h *wafenginmodel.HostSafe) { h.UrlWhiteLists = urlWhiteLists })
zlog.Debug("远程配置", zap.Any("UrlWhiteLists", urlWhiteLists))
break
case enums.ChanTypeBlockIP:
ipBlockLists := msg.Content.([]model.IPBlockList)
// 同上:编译与抽取放在 mutator 外面,mutator 里只做字段赋值
ipBlockIndex := wafenginecore.BuildIPBlockIndex(ipBlockLists)
ipBlockGroupCodes := wafenginecore.ExtractBlockGroupCodes(ipBlockLists)
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.UpdateHost(msg.HostCode, func(h *wafenginmodel.HostSafe) {
h.IPBlockLists = ipBlockLists
h.IPBlockIndex = ipBlockIndex
h.IPBlockGroupCodes = ipBlockGroupCodes
})
zlog.Debug("远程配置", zap.Any("IPBlockLists", msg))
break
case enums.ChanTypeBlockURL:
urlBlockLists := msg.Content.([]model.URLBlockList)
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.UpdateHost(msg.HostCode, func(h *wafenginmodel.HostSafe) { h.UrlBlockLists = urlBlockLists })
zlog.Debug("远程配置", zap.Any("UrlBlockLists", urlBlockLists))
break
case enums.ChanTypeLdp:
ldpUrlLists := msg.Content.([]model.LDPUrl)
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.UpdateHost(msg.HostCode, func(h *wafenginmodel.HostSafe) { h.LdpUrlLists = ldpUrlLists })
zlog.Debug("远程配置", zap.Any("LdpUrlLists", ldpUrlLists))
break
case enums.ChanTypeRule:
rules := msg.Content.([]model.Rules)
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.UpdateHostRules(msg.HostCode, rules)
zlog.Debug("远程配置", zap.Any("Rule", rules))
break
case enums.ChanTypeAntiCCRule:
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.ApplyCCRules(msg.HostCode)
zlog.Debug("远程配置", zap.Any("AntiCCRule", msg.HostCode))
break
case enums.ChanTypeAnticc:
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.ApplyAntiCCConfig(msg.HostCode, msg.Content.(model.AntiCC))
break
case enums.ChanTypeHttpauth:
httpAuthBases := msg.Content.([]model.HttpAuthBase)
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.UpdateHost(msg.HostCode, func(h *wafenginmodel.HostSafe) { h.HttpAuthBases = httpAuthBases })
zlog.Debug("远程配置", zap.Any("Http Auth", httpAuthBases))
break
case enums.ChanTypeHost:
hosts := msg.Content.([]model.Hosts)
if len(hosts) == 1 {
//情况1,端口是新的,域名也是新的
//情况2,端口不变,域名也不变,就是重新加载数据
//情况3,端口从A切换到B了,域名是旧的 ;端口更改后当前这个端口下没有域名了,应该是关闭了,并移除数据
//情况1
if msg.OldContent == nil {
zlog.Debug("主机处理情况1 端口是新的,域名也是新的")
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.LoadHost(hosts[0])
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.StartAllProxyServer()
} else {
hostsOld := msg.OldContent.(model.Hosts)
if hosts[0].Host == hostsOld.Host && hosts[0].Port == hostsOld.Port {
//情况2:同域名同主端口,重新加载数据
// LoadHost 已做"原子替换"(单次发布,先清旧 key 再装新 key),主端口 key 不会短暂缺失,
// 故不再先 RemoveHost(会发布一张缺该 host 的快照→重载期间 403)。
zlog.Debug("主机处理情况2 端口不变,域名也不变,就是重新加载数据")
//清空已建代理,下次请求按新后端懒重建(LoadBalanceRuntime 自身 Mux 保护)
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.ResetHostProxiesByKey(hosts[0].Host + ":" + strconv.Itoa(hosts[0].Port))
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.LoadHost(hosts[0])
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.StartAllProxyServer()
//副端口可能被删除:关闭已无主机占用的端口(在新路由发布后做,不影响主端口)
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.RemovePortServer()
} else if hosts[0].Host == hostsOld.Host && hosts[0].Port != hostsOld.Port {
//情况3:主端口从 A 切到 B。LoadHost 原子替换会清掉旧端口 key 并装上新端口 key。
zlog.Debug("主机处理情况3 端口从A切换到B了,域名是旧的 ;端口更改后当前这个端口下没有域名了,应该是关闭了,并移除数据")
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.ResetHostProxiesByKey(hostsOld.Host + ":" + strconv.Itoa(hostsOld.Port))
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.LoadHost(hosts[0])
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.StartAllProxyServer()
//旧端口 A 现已无主机占用:关闭其监听
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.RemovePortServer()
}
}
}
break
case enums.ChanTypeDelHost:
host := msg.Content.(model.Hosts)
if host.Id != "" {
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.RemoveHost(host)
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.EnumAllPortProxyServer()
}
break
case enums.ChanTypeLoadBalance:
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.ClearProxy(msg.HostCode)
break
case enums.ChanTypeSSL:
host := msg.Content.(model.Hosts)
zlog.Info(fmt.Sprintf("服务端准备为 %s 主机刷新 SSL证书 ,证书信息:%v", host.Host, utils.PrintSSLCert(host.Certfile)))
// LoadHost 原子替换(单次发布):刷新证书重载期间主端口 key 不缺失,不再先 RemoveHost(避免 403 空档)
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.LoadHost(host)
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.StartAllProxyServer()
break
case enums.ChanTypeBlockingPage:
blockingPage := msg.Content.(map[string]model.BlockingPage)
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.UpdateHost(msg.HostCode, func(h *wafenginmodel.HostSafe) { h.BlockingPage = blockingPage })
zlog.Debug("远程配置", zap.Any("配置自定义拦截界面信息", blockingPage))
break
case enums.ChanTypeCacheRule:
cacheRule := msg.Content.([]model.CacheRule)
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.UpdateHost(msg.HostCode, func(h *wafenginmodel.HostSafe) { h.CacheRule = cacheRule })
zlog.Debug("远程配置", zap.Any("配置缓存规则", cacheRule))
break
case enums.ChanTypeTamperRule:
tamperRules := msg.Content.([]model.TamperRule)
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.UpdateHost(msg.HostCode, func(h *wafenginmodel.HostSafe) { h.TamperRules = tamperRules })
zlog.Debug("远程配置", zap.Any("配置网页防篡改规则", tamperRules))
break
case enums.ChanTypeHostPathRule:
pathRules := msg.Content.([]model.HostPathRule)
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.UpdateHost(msg.HostCode, func(h *wafenginmodel.HostSafe) { h.PathRules = pathRules })
zlog.Debug("远程配置", zap.Any("配置路径路由规则", pathRules))
break
}
//end switch
} else {
//新增
switch msg.Type {
case enums.ChanTypeHost:
hosts := msg.Content.([]model.Hosts)
if len(hosts) == 1 {
hostRunTimeBean := globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.LoadHost(hosts[0])
for _, runTime := range hostRunTimeBean {
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.StartProxyServer(runTime)
}
}
break
}
}
break
case common := <-global.GWAF_CHAN_COMMON_MSG:
if common.Type == enums.ChanComTypeTunnel {
//隧道类型
switch common.OpType {
case enums.OP_TYPE_NEW:
tunnelNew := common.Content.(model.Tunnel)
netRunTimes := globalobj.GWAF_RUNTIME_OBJ_TUNNEL_ENGINE.LoadTunnel(tunnelNew)
for _, netRunTime := range netRunTimes {
globalobj.GWAF_RUNTIME_OBJ_TUNNEL_ENGINE.StartTunnelServer(netRunTime)
}
break
case enums.OP_TYPE_UPDATE:
// 获取修改前的隧道信息
tunnelOld := common.OldContent.(model.Tunnel)
// 获取修改后的隧道信息
tunnelNew := common.Content.(model.Tunnel)
netRunTimes := globalobj.GWAF_RUNTIME_OBJ_TUNNEL_ENGINE.EditTunnel(tunnelOld, tunnelNew)
for _, netRunTime := range netRunTimes {
globalobj.GWAF_RUNTIME_OBJ_TUNNEL_ENGINE.StartTunnelServer(netRunTime)
}
break
case enums.OP_TYPE_DELETE:
tunnelDelete := common.OldContent.(model.Tunnel)
globalobj.GWAF_RUNTIME_OBJ_TUNNEL_ENGINE.RemoveTunnel(tunnelDelete)
break
}
} else if common.Type == enums.ChanComTypeApp {
// 应用管理类型
if globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE != nil {
switch common.OpType {
case enums.OP_TYPE_NEW:
// 新增:如果 AutoStart=1,则启动
appNew := common.Content.(model.WafApp)
if appNew.AutoStart == 1 && appNew.StartStatus == 1 {
if err := globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.StartApp(appNew.Code); err != nil {
zlog.Error("自动启动应用失败", "code", appNew.Code, "error", err.Error())
}
}
case enums.OP_TYPE_UPDATE:
appNew := common.Content.(model.WafApp)
globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.LoadApp(appNew)
case enums.OP_TYPE_DELETE:
appDel := common.OldContent.(model.WafApp)
// 先停止进程(能读到 DB 中的 StopMode/StopCmd/StopTimeout 配置),再删 DB 记录
globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.RemoveApp(appDel.Code)
global.GWAF_LOCAL_DB.Where("id = ?", appDel.Id).Delete(&model.WafApp{})
case enums.OP_TYPE_APP_START:
code := common.Content.(string)
if err := globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.StartApp(code); err != nil {
zlog.Error("启动应用失败", "code", code, "error", err.Error())
}
case enums.OP_TYPE_APP_STOP:
code := common.Content.(string)
if err := globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.StopApp(code); err != nil {
zlog.Error("停止应用失败", "code", code, "error", err.Error())
}
case enums.OP_TYPE_APP_RESTART:
code := common.Content.(string)
if err := globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.RestartApp(code); err != nil {
zlog.Error("重启应用失败", "code", code, "error", err.Error())
}
}
}
}
case engineStatus := <-global.GWAF_CHAN_ENGINE:
if engineStatus == 1 {
// 零空档重载:不再 CloseWaf(关监听器→connection refused 空档)+StartWaf,
// 改为逐 host 原子替换 + 端口差分,业务监听 socket 全程不下线。
zlog.Info("准备零空档重载WAF引擎")
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.ReloadAllHostZeroGap()
// 隧道引擎为独立特性,仍按原逻辑重启(与 HTTP 业务端口无关)
zlog.Info("准备关闭隧道引擎")
globalobj.GWAF_RUNTIME_OBJ_TUNNEL_ENGINE.CloseTunnel()
zlog.Info("准备启动隧道引擎")
globalobj.GWAF_RUNTIME_OBJ_TUNNEL_ENGINE.StartTunnel()
}
break
case <-global.GWAF_CHAN_HTTP3:
// HTTP/3 开关热生效:把所有在线 HTTPS 端口的 QUIC 监听对齐到当前配置。
// 必须 go 出去 —— 停 QUIC 要等在途请求排空(最长 drain_timeout),
// 同步做会把整个主消息循环堵住。
if globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE != nil {
zlog.Info("HTTP/3 配置发生变更,准备重新对齐 QUIC 监听")
go globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.ReconcileHTTP3()
}
break
case host := <-global.GWAF_CHAN_HOST:
// 站点字段热更新(copy-on-write,按 host:port key 定位;全局网站的 key 是「全局网站:0」,同样命中)
// 具体搬哪些字段见 wafenginecore.ApplyHostHotUpdate——新增请求期要读的字段必须在那里登记
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.UpdateHostByKey(host.Host+":"+strconv.Itoa(host.Port), func(h *wafenginmodel.HostSafe) {
wafenginecore.ApplyHostHotUpdate(h, host)
})
zlog.Debug("规则", zap.Any("主机", host))
break
case update := <-global.GWAF_CHAN_UPDATE:
if update == 1 {
// 优雅升级:二进制已被 selfupdate 替换到磁盘。
// 在 Supervisor 模式下,请求 Supervisor 编排滚动升级(起新 Worker→就绪→本 Worker 排空退出),
// 本进程不自行退出,等待 Supervisor 下发 DRAIN,从而实现业务零中断。
if gWorkerCtrl != nil {
zlog.Info("[Worker] 二进制已就绪,请求 Supervisor 发起滚动升级(业务不中断)")
gWorkerCtrl.RequestUpgrade()
break
}
// 兜底:非 Supervisor 托管的独立运行模式,沿用原“停止→拉起新进程→退出”的硬重启。
global.GWAF_RUNTIME_SERVER_TYPE = !service.Interactive()
//需要重新启动
if global.GWAF_RUNTIME_SERVER_TYPE == true {
zlog.Info("服务形式重启")
m.stopSamWaf()
// 只启动一次新进程
cmd := exec.Command(global.GWAF_RUNTIME_CURRENT_EXEPATH, "restart")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
wafexec.FixStdin(cmd)
if err := cmd.Start(); err != nil {
zlog.Error("启动新进程失败:", err)
os.Exit(0)
}
time.Sleep(2 * time.Second)
os.Exit(0)
} else {
m.stopSamWaf()
cmd := exec.Command(global.GWAF_RUNTIME_CURRENT_EXEPATH)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
wafexec.FixStdin(cmd)
if err := cmd.Start(); err != nil {
zlog.Error("启动新进程失败:", err)
os.Exit(0)
}
time.Sleep(2 * time.Second)
os.Exit(0)
}
}
break
case sensitive := <-global.GWAF_CHAN_SENSITIVE:
zlog.Debug("远程配置", sensitive)
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.ReLoadSensitive()
break
case sslOrderChan := <-global.GWAF_CHAN_SSLOrder:
zlog.Debug("ssl证书申请", sslOrderChan)
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.ApplySSLOrder(sslOrderChan.Type, sslOrderChan.Content.(model.SslOrder))
break
case sslExpireCheck := <-global.GWAF_CHAN_SSL_EXPIRE_CHECK:
zlog.Debug("ssl证书到期检测", sslExpireCheck)
waftask.SSLExpireCheck()
break
case syncHostToSslExpire := <-global.GWAF_CHAN_SYNC_HOST_TO_SSL_EXPIRE:
zlog.Debug("同步已存在主机到SSL证书检测任务里", syncHostToSslExpire)
waftask.SyncHostToSslCheck()
break
case taskMethod := <-global.GWAF_CHAN_TASK:
zlog.Debug("需要执行的方法", taskMethod)
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskScheduler.RunManual(taskMethod)
break
case taskReload := <-global.GWAF_CHAN_TASK_RELOAD:
zlog.Info("任务重新加载", "taskName", taskReload.TaskName, "taskMethod", taskReload.TaskMethod)
err := globalobj.GWAF_RUNTIME_OBJ_WAF_TaskScheduler.RescheduleTask(
taskReload.TaskUnit,
taskReload.TaskValue,
taskReload.TaskAt,
taskReload.TaskMethod,
taskReload.TaskDaysOfTheWeek,
)
if err != nil {
zlog.Error("任务重新加载失败", "error", err.Error(), "taskName", taskReload.TaskName)
} else {
zlog.Info("任务重新加载成功", "taskName", taskReload.TaskName)
}
break
case clearCcWindows := <-global.GWAF_CHAN_CLEAR_CC_WINDOWS:
zlog.Debug("定时清空CCwindows", clearCcWindows)
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.ClearCcWindows()
break
case clearCcWindowsIp := <-global.GWAF_CHAN_CLEAR_CC_IP:
zlog.Debug("定时清空CCip", clearCcWindowsIp)
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.ClearCcWindowsForIP(clearCcWindowsIp)
break
}
}
zlog.Info("normal program close")
}
// 停止要提前关闭的 是服务的主要逻辑
func (m *wafSystenService) stopSamWaf() {
zlog.Info("Shutdown SamWaf Engine...")
if globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE != nil {
globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.CloseWaf()
zlog.Info("Shutdown SamWaf Engine finished")
} else {
zlog.Warn("WAF Engine is nil, skipping shutdown")
}
zlog.Info("Shutdown SamWaf Tunnel Engine...")
if globalobj.GWAF_RUNTIME_OBJ_TUNNEL_ENGINE != nil {
globalobj.GWAF_RUNTIME_OBJ_TUNNEL_ENGINE.CloseTunnel()
zlog.Info("Shutdown SamWaf Tunnel Engine finished")
} else {
zlog.Warn("Tunnel Engine is nil, skipping shutdown")
}
zlog.Info("Shutdown SamWaf App Engine...")
if globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE != nil {
globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.StopApps()
zlog.Info("Shutdown SamWaf App Engine finished")
} else {
zlog.Warn("App Engine is nil, skipping shutdown")
}
// 站点流量落库:引擎已停、不会再有新字节进来,这里补最后一次,
// 把内存里没到 30 秒周期的增量写掉(否则每次重启都会丢掉最后不足一个周期的流量)。
zlog.Info("Flush SamWaf Traffic Stats...")
waftask.FlushTrafficStats()
zlog.Info("Flush SamWaf Traffic Stats finished")
zlog.Info("Shutdown SamWaf Queue Processors...")
// 关闭信号通道,通知所有队列处理协程退出
close(global.GWAF_QUEUE_SHUTDOWN_SIGNAL)
// 等待一段时间,让队列处理协程有时间完成当前工作并退出
time.Sleep(500 * time.Millisecond)
zlog.Info("Shutdown SamWaf Queue Processors finished")
// 设置任务停止标志
zlog.Info("Notifying SamWaf Tasks to shutdown...")
global.GWAF_SHUTDOWN_SIGNAL = true
// 给任务一些时间完成当前工作
time.Sleep(200 * time.Millisecond)
zlog.Info("SamWaf Tasks notified")
// 主机登录防护要早于 cron 与关库停:Stop 里要等事件源交还 fd / journalctl 子进程 /
// Windows 事件订阅句柄,还要把缓冲里的事件与待同步的封禁集合落地。
// 停晚了就会出现"旧 Worker 还在读日志、新 Worker 也开始读"的重复计数。
zlog.Info("Shutdown SamWaf HostGuard...")
wafhostguard.Stop()
waf_service.WafHostGuardServiceApp.FlushEvents()
zlog.Info("Shutdown SamWaf HostGuard finished")
zlog.Info("Shutdown SamWaf Cron...")
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskScheduler.Stop()
zlog.Info("Shutdown SamWaf Cron finished")
zlog.Info("Shutdown SamWaf WebManager...")
webmanager.Shutdown()
zlog.Info("Shutdown SamWaf WebManager finished")
zlog.Info("Shutdown SamWaf IPDatabase...")
utils.CloseIPDatabase()
zlog.Info("Shutdown SamWaf IPDatabase finished")
// 关闭插件系统
zlog.Info("Shutdown SamWaf Plugin System...")
if globalobj.GWAF_RUNTIME_OBJ_PLUGIN_MANAGER != nil {
if err := globalobj.GWAF_RUNTIME_OBJ_PLUGIN_MANAGER.Shutdown(); err != nil {
zlog.Error("Shutdown Plugin System failed", zap.Error(err))
} else {
zlog.Info("Shutdown SamWaf Plugin System finished")
}
} else {
zlog.Warn("Plugin Manager is nil, skipping shutdown")
}
}
// supervisorService 是被服务管理器(kardianos)托管的“监护进程”程序。
// 它常驻、永不为升级而退出,负责拉起/监护/滚动升级 Worker 子进程。
type supervisorService struct {
sup *supervisor.Supervisor
}
func (s *supervisorService) Start(svc service.Service) error {
zlog.Info("[Supervisor] 服务启动")
go func() {
if err := s.sup.Run(); err != nil {
zlog.Error("[Supervisor] 运行失败: " + err.Error())
}
}()
return nil
}
func (s *supervisorService) Stop(svc service.Service) error {
zlog.Info("[Supervisor] 服务停止")
s.sup.Shutdown()
return nil
}
// gracefulOnce 保证优雅停止只执行一次,无论由服务 Stop()、系统信号,
// 还是后续 Supervisor 的 DRAIN 指令触发。
var gracefulOnce sync.Once
// workerLogExportLifecycle 将启动与停止串行化,停止后不再接受ACTIVATE启动。
type workerLogExportLifecycle struct {
mu sync.Mutex
stopping bool
}
func (l *workerLogExportLifecycle) start(start func()) {
l.mu.Lock()
defer l.mu.Unlock()
if !l.stopping {
start()
}
}
func (l *workerLogExportLifecycle) stop(stop func()) {
l.mu.Lock()
defer l.mu.Unlock()
if l.stopping {
return
}
l.stopping = true
stop()
}
var logExportWorkerLifecycle workerLogExportLifecycle
// Graceful 优雅停止的统一入口:先排空业务连接(StopAllProxyServer 已支持 Shutdown 排空),
// 回收各引擎/队列/任务资源,最后关闭数据库连接。供信号、服务 Stop 和升级 DRAIN 复用。
func (m *wafSystenService) Graceful() {
gracefulOnce.Do(func() {
zlog.Info("[Graceful] 开始优雅停止:排空业务连接并回收资源 ...")
zlog.Info("Shutdown SamWaf LogExport...")
logExportWorkerLifecycle.stop(waf_service.WafLogExportServiceApp.Stop)
zlog.Info("Shutdown SamWaf LogExport finished")
m.stopSamWaf() // 内部 CloseWaf→StopAllProxyServer 已优雅排空在途连接
wafsafeclear.SafeClear() // 最后关闭数据库连接
zlog.Info("[Graceful] 优雅停止完成")
})
}
// @title SamWaf Open API
// @version 1.0
// @description SamWaf Web Application Firewall 开放平台 API 文档。需要在请求头携带 X-API-Key 进行鉴权。
// @BasePath /api/v1
// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name X-API-Key
func main() {
global.GWAF_WEB_VERSION = public.WebFrontendVersion()
fmt.Println(`
==========================================
SamWaf Web Application Firewall ` + global.GWAF_RELEASE_VERSION + `
Version Name: ` + global.GWAF_RELEASE_VERSION_NAME + `
Web UI Version: ` + global.GWAF_WEB_VERSION + `
==========================================
`)
//加载配置
wafconfig.LoadAndInitConfig()
// 多进程日志隔离:Supervisor 与 Worker 是同一二进制的两个进程,若都写同一个 logs/log.log,
// lumberjack 滚动时 rename 会因文件被另一进程占用而失败(Windows),导致日志再也滚不动。
// 故 Supervisor 角色单独写 supervisor.log;Worker 仍写 log.log(主业务日志名不变)。
// parseWorkerRole 仅扫描 os.Args,不依赖日志/配置,可安全前置调用。
if isWorker, _, _, _ := parseWorkerRole(); !isWorker {
zlog.FileName = "supervisor.log"
}
// 自升级交接助手是与 Supervisor 并存的第三个短命进程,同理单独写一个日志文件,
// 免得三个进程抢同一个 supervisor.log 的滚动 rename。
if len(os.Args) > 1 && os.Args[1] == supervisor.HandoffRestartArg {
zlog.FileName = "handoff.log"
}
//初始化日志
zlog.InitZLog(global.GWAF_LOG_DEBUG_ENABLE, global.GWAF_LOG_OUTPUT_FORMAT)
if v := recover(); v != nil {
zlog.Error("主流程上被异常了")
}
pid := os.Getpid()
zlog.Debug("SamWaf Current PID:" + strconv.Itoa(pid))
// 环境自检:精简版 Windows(如 LTSC 纯净版)可能裁剪/禁用 Null 内核驱动,导致空设备不可用。
// os/exec 在标准流为 nil 时会去打开该设备,缺失则所有子进程(含 Worker)都起不来。
// SamWaf 已通过 wafexec 自动规避,这里只留一条线索:Supervisor 记在 supervisor.log、Worker 记在 log.log。
if !wafexec.NullDeviceAvailable() {
zlog.Warn("[环境] 系统空设备 " + os.DevNull + " 不可用(精简版 Windows 常见:Null 内核驱动被裁剪或禁用)。" +
"SamWaf 已自动规避,不影响运行;如需彻底恢复,请以管理员执行 `sc config Null start=system` 后重启系统。")
}
// 调试标识:响应头标记处理进程,便于验证升级时新旧 Worker 交替。
// 主开关在 config.yml 的 debug_worker_header(已由上方 LoadAndInitConfig 读入,Supervisor/Worker 均生效,
// 服务模式下务必用配置项——SCM/systemd 不会传入交互式 shell 的环境变量)。
// 环境变量 SAMWAF_WORKER_HEADER=1 仅作前台调试的临时叠加开关(只会打开、不会关闭配置已开的开关)。
global.GWAF_WORKER_TAG = "pid=" + strconv.Itoa(pid) + " born=" + time.Now().Format("15:04:05")
if os.Getenv("SAMWAF_WORKER_HEADER") == "1" {
global.GWAF_DEBUG_WORKER_HEADER = true
}
// 角色分流:带 --worker 的进程由 Supervisor 拉起,直接运行业务引擎 + 控制通道客户端。
if workerRole, ctrlAddr, ctrlToken, takeover := parseWorkerRole(); workerRole {
global.GWAF_RUNTIME_IS_TAKEOVER = takeover
//获取外网IP
global.GWAF_RUNTIME_IP = utils.GetExternalIp()
wp := &wafSystenService{}
if ctrlAddr != "" {
startWorkerControl(ctrlAddr, ctrlToken, wp)
}
// Worker 自身的信号处理:收到中断/终止(如前台 Ctrl+C、systemd SIGTERM)时优雅排空再退出。
// 与 Supervisor 下发的 DRAIN/SHUTDOWN 经 gracefulOnce 去重,只执行一次。
go func() {
sc := make(chan os.Signal, 1)
signal.Notify(sc, os.Interrupt, syscall.SIGTERM)
s := <-sc
zlog.Info("[Worker] 收到系统信号优雅停止: " + s.String())
wp.Graceful()
os.Exit(0)
}()
zlog.Info("以 Worker 角色启动 (takeover=" + strconv.FormatBool(takeover) + ")")
wp.run() // 阻塞运行引擎事件循环
return
}
// ===== 以下为 Supervisor 角色(默认,被服务管理器托管,常驻不为升级退出)=====
option := service.KeyValue{}
//windows
//OnFailure:"restart",
if runtime.GOOS == "windows" {
option["OnFailure"] = "restart"
option["OnFailureDelayDuration"] = "1s"
option["OnFailureResetPeriod"] = "10"
} else {
option["Restart"] = "always"
}
// 创建服务对象
svcConfig := &service.Config{
Name: "SamWafService",
DisplayName: "SamWaf Service",
Description: "SamWaf is a Web Application Firewall (WAF) By SamWaf.com",
Option: option,
}
exePath, _ := os.Executable()
dataDir := filepath.Join(utils.GetCurrentDir(), "data")
sup := supervisor.New(supervisor.Options{
ExePath: exePath,
DataDir: dataDir,
DrainTimeout: int(global.GCONFIG_RECORD_DRAIN_TIMEOUT),
// 自升级交接(Windows)靠"退出 + 助手把服务重新 start",前台运行时退出等于停服,
// 故把"是否由服务管理器托管"传下去,由 Supervisor 决定是否交接。
ServiceManaged: !service.Interactive(),
})
prg := &supervisorService{sup: sup}
s, err := service.New(prg, svcConfig)
if err != nil {
log.Fatal(err)
}
// 设置服务控制信号处理程序:收到中断/终止信号时,令 Supervisor 通知各 Worker 优雅退出再停止。
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, os.Kill, syscall.SIGTERM)
go func() {
sig := <-sigChan
zlog.Info("收到系统信号,Supervisor 开始优雅停止: " + sig.String())
sup.Shutdown()
os.Exit(0)
}()
if len(os.Args) > 1 {
command := os.Args[1]
switch command {
case "install", "start", "stop", "uninstall", "restart": // 以服务方式运行
err := service.Control(s, command)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Samwaf has successfully executed the '%s' command.\n", command)
break
case supervisor.HandoffRestartArg:
// 自升级交接助手(Windows,隐藏命令,由 Supervisor 自己拉起)。
// 老 Supervisor 交接前起本进程后就退出了;本进程轮询把服务重新 start 起来,
// 让 Supervisor 换到新二进制。期间 Worker 作为孤儿继续转发业务,不中断。
runHandoffRestart(s)
case "rolling-restart": // 零停机滚动重启:通知运行中的 Supervisor 换 Worker(业务不中断),亦用于测试升级编排
if err := supervisor.TriggerUpgrade(dataDir); err != nil {
fmt.Println("滚动重启触发失败:", err)
os.Exit(1)
}
fmt.Println("已触发零停机滚动重启,请观察日志:新 Worker 就绪后旧 Worker 将优雅排空退出。")
case "rekey": //把遗留(旧格式)敏感字段升级为当前数据密钥(DEK)的新格式,供手动补跑迁移
if err := wafsec.InitDataKey(utils.GetCurrentDir(), global.GCONFIG_DATA_KEY_FILE); err != nil {
fmt.Println("数据密钥初始化失败:", err)
os.Exit(1)
}
if _, err := wafdb.InitCoreDb(""); err != nil {
fmt.Println("核心数据库初始化失败:", err)
os.Exit(1)
}
n := wafdb.RekeyDataKey(global.GWAF_LOCAL_DB)
fmt.Printf("已把 %d 行遗留敏感字段升级为当前数据密钥格式。\n", n)
fmt.Println("注意:本命令不做密钥轮换;请勿在替换 data_key 文件后运行它。")
case "rekey-legacy": //把落库敏感字段重写回旧通讯密钥格式(降级到旧版二进制前执行)
if err := wafsec.InitDataKey(utils.GetCurrentDir(), global.GCONFIG_DATA_KEY_FILE); err != nil {
fmt.Println("数据密钥初始化失败:", err)
os.Exit(1)
}
if _, err := wafdb.InitCoreDb(""); err != nil {
fmt.Println("核心数据库初始化失败:", err)
os.Exit(1)
}
n := wafdb.RekeyToLegacy(global.GWAF_LOCAL_DB)
fmt.Printf("已把 %d 行敏感字段重写回旧格式,现在可以安全降级到旧版本。\n", n)
case "resetpwd": //重制密码
wafdb.InitCoreDb("")
wafdb.ResetAdminPwd()
case "resetotp": //重置安全码
wafdb.InitCoreDb("")
wafdb.ResetAdminOTP()
case "repairdb": //修复数据库
fmt.Println("\n⚠️ 数据库修复工具")
fmt.Println("如果您遇到数据库损坏错误")
fmt.Println("可以使用此工具尝试修复。")
wafdb.RepairAllDatabases("")
case "execsql": //执行SQL语句
fmt.Println("\n💻 SQL 执行工具")
fmt.Println("可以在指定数据库上执行 SQL 语句")
wafdb.ExecuteSQLCommand("")
case "migratedb": // 离线迁移:SQLite→MySQL / SQLite→PostgreSQL / MySQL→PostgreSQL
opts := wafdb.MigrateOptions{}
for _, arg := range os.Args[2:] {
switch {
case arg == "--dry-run":
opts.DryRun = true
case arg == "--force":
opts.Force = true
case strings.HasPrefix(arg, "--from="):
opts.From = strings.TrimPrefix(arg, "--from=")
case strings.HasPrefix(arg, "--to="):
opts.To = strings.TrimPrefix(arg, "--to=")
}
}
if err := wafdb.RunMigrateDB(opts); err != nil {
fmt.Println("迁移失败:", err)
os.Exit(1)
}
case "rollback": //版本回退
fmt.Println("================================================")
fmt.Println(" SamWaf 版本回退工具")
fmt.Println("================================================")
fmt.Printf("当前运行版本: %s\n\n", global.GWAF_RELEASE_VERSION)
list, err := wafupdate.ListBackups()
if err != nil {
fmt.Println("获取备份列表失败:", err)
return
}
if len(list) == 0 {
fmt.Println("没有可用的备份版本,无法回退")
return
}
fmt.Printf("%-4s %-15s %-22s %-10s %s\n", "序号", "版本", "备份时间", "大小(MB)", "备注")
fmt.Println("------------------------------------------------------------------------")
for i, b := range list {
note := ""
if b.Version == global.GWAF_RELEASE_VERSION {
note = "[当前版本]"
}
fmt.Printf("%-4d %-15s %-22s %-10.2f %s\n",
i+1,
b.Version,
b.BackupTime.Format("2006-01-02 15:04:05"),
float64(b.FileSize)/(1024*1024),
note)
}
fmt.Println("------------------------------------------------------------------------")
fmt.Print("\n请输入要回退的序号,或输入 'q' 退出: ")
var input string
fmt.Scanln(&input)
if input == "q" || input == "Q" {
fmt.Println("已退出版本回退工具")
return
}
idx := 0
_, parseErr := fmt.Sscanf(input, "%d", &idx)
if parseErr != nil || idx < 1 || idx > len(list) {
fmt.Printf("无效的序号: %s\n", input)
return
}
target := list[idx-1]
if target.Version == global.GWAF_RELEASE_VERSION {
fmt.Printf("所选版本 %s 与当前运行版本相同,无需回退\n", target.Version)
return
}
fmt.Printf("\n即将回退到: %s(%s)\n", target.Version, target.BackupTime.Format("2006-01-02 15:04:05"))
fmt.Print("确认回退?回退后需要手动重启服务 (y/n): ")
var confirm string
fmt.Scanln(&confirm)
if confirm != "y" && confirm != "Y" {
fmt.Println("已取消")
return
}
fmt.Printf("正在回退到版本 %s...\n", target.Version)
if rollbackErr := wafupdate.RollbackExecutable(target.Version); rollbackErr != nil {
fmt.Println("回退失败:", rollbackErr)
return
}
fmt.Println("回退成功,请重启服务 (samwaf start 或 samwaf restart)")
default:
fmt.Printf("Command '%s' is not recognized.\n", command)
fmt.Println("\n可用命令:")
fmt.Println(" install - 安装为系统服务")
fmt.Println(" start - 启动服务")
fmt.Println(" stop - 停止服务")
fmt.Println(" restart - 重启服务")
fmt.Println(" rolling-restart - 零停机滚动重启(换 Worker,业务不中断)")
fmt.Println(" uninstall - 卸载服务")
fmt.Println(" resetpwd - 重置管理员密码")
fmt.Println(" resetotp - 重置安全码")
fmt.Println(" repairdb - 修复损坏的数据库")
fmt.Println(" execsql - 执行SQL语句(支持SELECT/UPDATE/DELETE等)")
fmt.Println(" migratedb - 离线迁移数据库(不带参数则交互式选择迁移方向)")
fmt.Println(" 支持方向: SQLite→MySQL / SQLite→PostgreSQL / MySQL→PostgreSQL")
fmt.Println(" --from=sqlite|mysql 迁移源(缺省则交互式选择)")
fmt.Println(" --to=mysql|postgres 迁移目标(缺省则交互式选择)")
fmt.Println(" --dry-run 只做预估,不写入数据")
fmt.Println(" --force 目标表已有数据时强制覆盖")
fmt.Println(" rollback - 回退到历史版本 (--list 列出, --version=v1.x.x 指定版本)")
fmt.Println("")
}
return
}
if service.Interactive() {
zlog.Info("main server under service manager")
global.GWAF_RUNTIME_SERVER_TYPE = service.Interactive()
} else {
zlog.Info("main server not under service manager")
global.GWAF_RUNTIME_SERVER_TYPE = service.Interactive()
}
// 以常规方式运行
err = s.Run()
if err != nil {
log.Fatal(err)
}
}
// ===== 优雅升级 Worker 侧控制通道客户端(与 Supervisor 通信) =====
// parseWorkerRole 解析进程角色与控制参数。
// 由 Supervisor 拉起的 Worker 会带 --worker --ctrl-addr=127.0.0.1:port --token=xxx [--takeover]。
func parseWorkerRole() (worker bool, ctrlAddr, token string, takeover bool) {
for _, a := range os.Args[1:] {
switch {
case a == "--worker":
worker = true
case a == "--takeover":
takeover = true
case strings.HasPrefix(a, "--ctrl-addr="):
ctrlAddr = strings.TrimPrefix(a, "--ctrl-addr=")
case strings.HasPrefix(a, "--token="):
token = strings.TrimPrefix(a, "--token=")
}
}
return
}
// workerCtrl 是 Worker 侧的控制通道客户端:连接 Supervisor、上报 HELLO/READY/HEARTBEAT、
// 响应 DRAIN/SHUTDOWN/PING;断线自动重连(支撑 Supervisor 自升级后的“再认领”)。
type workerCtrl struct {
addr string
token string
prg *wafSystenService
mu sync.Mutex
conn *wafipc.Conn
down bool
}
// gWorkerCtrl 当前进程的 Worker 控制客户端(仅 worker 角色下非 nil)。
var gWorkerCtrl *workerCtrl
// runHandoffRestart 是 Supervisor 自升级交接的"重启助手"(Windows 隐藏命令)。
//
// Windows 没有 execve,Supervisor 换二进制只能退出再被拉起来。这里不依赖 SCM 的
// 失败恢复动作(那是服务安装时写入的,老版本装的服务可能压根没配),而是由老
// Supervisor 先起本进程、随后自己干净退出,本进程轮询把服务 start 回来。
//
// 期间 Worker 作为孤儿继续转发业务,新 Supervisor 起来后靠 supervisor.state 里的
// ctrl 端口 + token 把它收编回去(同版本原地认领,不重起 Worker)。
func runHandoffRestart(s service.Service) {
const (
interval = 1 * time.Second
timeout = 60 * time.Second
)
zlog.Info("[交接助手] 等待旧 Supervisor 退出后把服务重新拉起...")
deadline := time.Now().Add(timeout)
sawStopped := false
for time.Now().Before(deadline) {
time.Sleep(interval)
if st, err := s.Status(); err == nil && st == service.StatusRunning {
if sawStopped {
// 服务停过又跑起来了——可能是 SCM 的失败恢复动作先我们一步。目的已达成。
zlog.Info("[交接助手] 服务已重新运行(由服务管理器先行拉起),交接完成")
return
}
// 旧 Supervisor 还没退干净,继续等。
continue
}
sawStopped = true
if err := service.Control(s, "start"); err != nil {
zlog.Debug("[交接助手] start 暂未成功,稍后重试: " + err.Error())
continue
}
zlog.Info("[交接助手] 服务已以新二进制重新启动,交接完成")
return
}
// 拉不起来不代表业务断了:Worker 仍在转发流量,只是暂时没有监护进程。
zlog.Error("[交接助手] " + timeout.String() + " 内未能把服务拉起来。Worker 仍在服务," +
"但监护进程缺席,请人工执行 SamWaf64.exe start")
}
// activateOnce 保证独占单例(应用/隧道/cron)只被接管启动一次。
var activateOnce sync.Once
// activateSingletons 启动独占型单例:隧道、应用、cron 任务调度。
// takeover 升级模式下由 Supervisor 在旧 Worker 退出后经 ACTIVATE 触发(此时旧 Worker 已释放这些资源,避免端口/进程冲突);
// 非 takeover(首次启动/自愈重拉)则在 run() 内联启动,不经过这里。
func activateSingletons() {
activateOnce.Do(func() {
zlog.Info("[Worker] 接管独占单例:启动隧道 / 应用 / 任务调度")
if globalobj.GWAF_RUNTIME_OBJ_TUNNEL_ENGINE != nil {
globalobj.GWAF_RUNTIME_OBJ_TUNNEL_ENGINE.StartTunnel()
}
if globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE != nil {
globalobj.GWAF_RUNTIME_OBJ_APP_ENGINE.StartApps()
}
if globalobj.GWAF_RUNTIME_OBJ_WAF_TaskScheduler != nil {
globalobj.GWAF_RUNTIME_OBJ_WAF_TaskScheduler.Start()
}
// 日志导出执行器同属独占单例:与任务调度一同只在活动 Worker 中运行。
logExportWorkerLifecycle.start(waf_service.WafLogExportServiceApp.Start)
// 主机登录防护的日志采集也必须等旧 Worker 彻底退出后才启动,
// 否则两个进程同时读同一份 auth.log,失败次数被重复计入,阈值等于打了对折。
wafhostguard.Start()
})
}
// markSingletonsActivated 把 activateOnce 消费掉而不做任何事,
// 供"独占单例已在 run() 内联启动"的非 takeover 路径调用,让之后收到的
// ACTIVATE 变成空操作(幂等),避免隧道/应用/cron 被重复启动。
func markSingletonsActivated() {
activateOnce.Do(func() {})
}
// startWorkerControl 启动 Worker 控制通道客户端(后台重连循环)。
func startWorkerControl(addr, token string, prg *wafSystenService) {
gWorkerCtrl = &workerCtrl{addr: addr, token: token, prg: prg}
go gWorkerCtrl.loop()
}
func (wc *workerCtrl) loop() {
for {
if wc.isDown() {
return
}
conn, err := wafipc.Dial(wc.addr)
if err != nil {
time.Sleep(2 * time.Second)
continue
}
wc.setConn(conn)
wc.send(wafipc.Message{
Type: wafipc.MsgHello,
PID: os.Getpid(),
Version: global.GWAF_RELEASE_VERSION,
ProtoVer: wafipc.ProtoVersion,
Token: wc.token,
})
wc.serve(conn) // 阻塞直到连接出错
conn.Close()
wc.setConn(nil)
if wc.isDown() {
return
}
time.Sleep(2 * time.Second) // 重连
}
}
func (wc *workerCtrl) serve(conn *wafipc.Conn) {
stop := make(chan struct{})
defer close(stop)
// 就绪上报:等引擎端口起来后发 READY
go func() {
for i := 0; i < 600; i++ { // 最多约 60s 等引擎对象建立
if globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE != nil {
break
}
select {
case <-stop:
return
case <-time.After(100 * time.Millisecond):
}
}
if globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE != nil {
_ = globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.WaitProxyReady(60 * time.Second)
}
wc.send(wafipc.Message{Type: wafipc.MsgReady, PID: os.Getpid()})
}()
// 心跳:上报当前连接数
go func() {
t := time.NewTicker(3 * time.Second)
defer t.Stop()
for {
select {
case <-stop:
return
case <-t.C:
var conns int64
if globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE != nil {
conns = globalobj.GWAF_RUNTIME_OBJ_WAF_ENGINE.TotalConns()
}
wc.send(wafipc.Message{Type: wafipc.MsgHeartbeat, PID: os.Getpid(), Active: conns})
}
}
}()
// 命令读取
for {
m, err := conn.Recv()
if err != nil {
return
}
switch m.Type {
case wafipc.MsgDrain, wafipc.MsgShutdown:
zlog.Info("[Worker] 收到 " + m.Type + " 指令,开始优雅排空退出")
go wc.gracefulExit()
return
case wafipc.MsgActivate:
zlog.Info("[Worker] 收到 ACTIVATE 指令,接管独占单例(应用/隧道/任务调度)")
go activateSingletons()
case wafipc.MsgPing:
wc.send(wafipc.Message{Type: wafipc.MsgPong, PID: os.Getpid()})
}
}
}
func (wc *workerCtrl) gracefulExit() {
wc.markDown()
wc.prg.Graceful()
wc.send(wafipc.Message{Type: wafipc.MsgGone, PID: os.Getpid()})
time.Sleep(200 * time.Millisecond)
os.Exit(0)
}
// RequestUpgrade 通知 Supervisor 发起滚动升级(二进制已被 selfupdate 替换到磁盘)。
func (wc *workerCtrl) RequestUpgrade() {
wc.send(wafipc.Message{Type: wafipc.MsgRequestUpgrade, PID: os.Getpid()})
}
func (wc *workerCtrl) send(m wafipc.Message) {
wc.mu.Lock()
defer wc.mu.Unlock()
if wc.conn != nil {
_ = wc.conn.Send(m)
}
}
func (wc *workerCtrl) setConn(c *wafipc.Conn) {
wc.mu.Lock()
wc.conn = c
wc.mu.Unlock()
}
func (wc *workerCtrl) isDown() bool {
wc.mu.Lock()
defer wc.mu.Unlock()
return wc.down
}
func (wc *workerCtrl) markDown() {
wc.mu.Lock()
wc.down = true
wc.mu.Unlock()
}