diff --git a/bfe_basic/common.go b/bfe_basic/common.go
index 6a33a079d..744885bc7 100644
--- a/bfe_basic/common.go
+++ b/bfe_basic/common.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2019 The BFE Authors.
+// Copyright (c) 2019 - 2025 The BFE Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -15,6 +15,10 @@
package bfe_basic
import (
+ "io/ioutil"
+ "strconv"
+ "strings"
+
"github.com/bfenetworks/bfe/bfe_http"
"github.com/bfenetworks/bfe/bfe_route/bfe_cluster"
)
@@ -80,6 +84,22 @@ func CreateInternalResp(request *Request, code int) *bfe_http.Response {
return res
}
+func CreateSpecifiedContentResp(request *Request, responseCode int, contentType string, content string) *bfe_http.Response {
+ resp := new(bfe_http.Response)
+ resp.StatusCode = responseCode
+
+ resp.Header = make(bfe_http.Header)
+ resp.Header.Set("Server", "bfe")
+
+ if len(contentType) != 0 {
+ resp.Header.Set("Content-Type", contentType)
+ }
+ resp.Header.Set("Content-Length", strconv.Itoa(len(content)))
+ resp.Body = ioutil.NopCloser(strings.NewReader(content))
+
+ return resp
+}
+
// ServerDataConfInterface is an interface used for lookup config for each request
type ServerDataConfInterface interface {
ClusterTableLookup(clusterName string) (*bfe_cluster.BfeCluster, error)
diff --git a/bfe_basic/waf_info.go b/bfe_basic/waf_info.go
new file mode 100644
index 000000000..77ae6b5bb
--- /dev/null
+++ b/bfe_basic/waf_info.go
@@ -0,0 +1,61 @@
+// Copyright (c) 2025 The BFE Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// basic waf info
+
+package bfe_basic
+
+const (
+ REQ_CHECK_ONLY = "CheckOnly"
+ REQ_NO_CHECK = "NoCheck"
+ REQ_FORBIDDEN = "Forbidden"
+ REQ_OK = "WaitResponse.Pass.Ok"
+ REQ_TIMEOUT = "WaitResponse.Pass.Timeout"
+ REQ_OTHER = "WaitResponse.Pass.Other"
+ NET_ERR = "Net.Error" // net error between go-bfe and waf-server
+)
+
+const (
+ WAF_NO_CHECK = 0 // no check for request
+ WAF_CHECKONLY = 1 // check only; from mod_waf_client, not used now
+ WAF_FORBIDDEN = 2 // check and forbidden
+ WAF_PASS = 3 // check and pass
+ WAF_DEGRADE = 4 // check, but pass with degraded
+ WAF_TIMEOUT = 5 // check, but pass with timeout
+ WAF_ERROR = 6 // check and pass with error happened
+)
+
+const (
+ REQ_CTX_WAF_INFO = "waf_client.waf_info"
+)
+
+// support old waf info struct
+type WafInfo struct {
+ WafSpentTime int64 // in ms.
+ WafStatus int // waf status, see bfe proto file for detail
+ WafRuleName string // not used
+}
+
+func GetWafInfo(req *Request) *WafInfo {
+ var info *WafInfo
+
+ val := req.GetContext(REQ_CTX_WAF_INFO)
+ if val != nil {
+ info = val.(*WafInfo)
+ } else {
+ info = new(WafInfo)
+ req.SetContext(REQ_CTX_WAF_INFO, info)
+ }
+ return info
+}
diff --git a/bfe_modules/bfe_modules.go b/bfe_modules/bfe_modules.go
index 56017ca5f..16bb4614b 100644
--- a/bfe_modules/bfe_modules.go
+++ b/bfe_modules/bfe_modules.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2019 The BFE Authors.
+// Copyright (c) 2019 - 2025 The BFE Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -42,6 +42,7 @@ import (
"github.com/bfenetworks/bfe/bfe_modules/mod_tcp_keepalive"
"github.com/bfenetworks/bfe/bfe_modules/mod_trace"
"github.com/bfenetworks/bfe/bfe_modules/mod_trust_clientip"
+ "github.com/bfenetworks/bfe/bfe_modules/mod_unified_waf"
"github.com/bfenetworks/bfe/bfe_modules/mod_userid"
"github.com/bfenetworks/bfe/bfe_modules/mod_waf"
"github.com/bfenetworks/bfe/bfe_modules/mod_wasmplugin"
@@ -135,6 +136,9 @@ var moduleList = []bfe_module.BfeModule{
// mod_wasm
mod_wasmplugin.NewModuleWasm(),
+
+ // mod_unified_waf
+ mod_unified_waf.NewModuleWaf(),
}
// init modules list
diff --git a/bfe_modules/mod_unified_waf/conf_load.go b/bfe_modules/mod_unified_waf/conf_load.go
new file mode 100644
index 000000000..ab26b3beb
--- /dev/null
+++ b/bfe_modules/mod_unified_waf/conf_load.go
@@ -0,0 +1,100 @@
+// Copyright (c) 2025 The BFE Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mod_unified_waf
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/baidu/go-lib/log"
+ "github.com/bfenetworks/bfe/bfe_modules/mod_unified_waf/waf_impl"
+ gcfg "gopkg.in/gcfg.v1"
+)
+
+type ConfBasic struct {
+ WafProductName string
+ ConnPoolSize int
+}
+
+type ConfModWaf struct {
+ Basic ConfBasic
+
+ ConfigPath struct {
+ ModWafDataPath string // configure path for mod_unified_waf.data
+ ProductParamPath string // configure path for product_param.data
+ WafInstancesPath string // configure path for waf_instances.data
+ }
+
+ Log struct {
+ OpenDebug bool
+ }
+}
+
+func ConfLoad(path string, confRoot string) (*ConfModWaf, error) {
+ var err error
+ var cfg ConfModWaf
+
+ // read config from file
+ if err = gcfg.ReadFileInto(&cfg, path); err != nil {
+ return &cfg, err
+ }
+ // check conf of mod_waf_client
+ err = cfg.Check(confRoot)
+ if err != nil {
+ return &cfg, err
+ }
+
+ return &cfg, nil
+}
+
+// check also fix some configure value
+func (cfg *ConfModWaf) Check(confRoot string) error {
+ if len(cfg.Basic.WafProductName) <= 0 {
+ cfg.Basic.WafProductName = NoneWafName
+ }
+
+ if len(cfg.Basic.WafProductName) > 0 {
+ twafName := cfg.Basic.WafProductName
+ if (twafName != NoneWafName) && !waf_impl.CheckWafSupport(twafName) {
+ err := fmt.Errorf("Basic.WafProductName:%s is illgal", cfg.Basic.WafProductName)
+ return err
+ }
+ }
+
+ if cfg.Basic.ConnPoolSize <= 0 {
+ log.Logger.Warn("Basic.ConnPoolSize is : %d, use DEFAULT_POOL_SIZE(%d)", cfg.Basic.ConnPoolSize, DEFAULT_POOL_SIZE)
+ cfg.Basic.ConnPoolSize = DEFAULT_POOL_SIZE
+ }
+
+ // check conf of ProductParamPath
+ if cfg.ConfigPath.ProductParamPath == "" {
+ log.Logger.Error("ConfigPath.ProductParamPath not set")
+ return errors.New("ConfigPath.ProductParamPath not set")
+ }
+
+ // check conf of ModWafDataPath
+ if cfg.ConfigPath.ModWafDataPath == "" {
+ log.Logger.Error("ConfigPath.ModWafDataPath not set")
+ return errors.New("ConfigPath.ModWafDataPath not set")
+ }
+
+ // check conf of WafInstancesPath
+ if cfg.ConfigPath.WafInstancesPath == "" {
+ log.Logger.Error("ConfigPath.WafInstancesPath not set")
+ return errors.New("ConfigPath.WafInstancesPath not set")
+ }
+
+ return nil
+}
diff --git a/bfe_modules/mod_unified_waf/conf_load_test.go b/bfe_modules/mod_unified_waf/conf_load_test.go
new file mode 100644
index 000000000..cb90e79b2
--- /dev/null
+++ b/bfe_modules/mod_unified_waf/conf_load_test.go
@@ -0,0 +1,48 @@
+// Copyright (c) 2025 The BFE Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mod_unified_waf
+
+import (
+ "testing"
+)
+
+// config path is not empty, correct
+func TestConfLoad_1(t *testing.T) {
+ confPath := "./testdata/mod_unified_waf.conf"
+ ModWafDataPath := "./testdata/mod_unified_waf.data"
+ productParamPath := "./testdata/product_param.data"
+ wafInstancesPath := "./testdata/waf_instances.data"
+ WafProductName := "BFEMockWaf"
+
+ conf, err := ConfLoad(confPath, "")
+ if err != nil {
+ t.Errorf("ConfLoad(): %v", err)
+ return
+ }
+
+ if conf.Basic.WafProductName != WafProductName {
+ t.Errorf("WafProductName should be %s not %s", WafProductName, conf.Basic.WafProductName)
+ }
+
+ if conf.ConfigPath.ModWafDataPath != ModWafDataPath {
+ t.Errorf("ModWafDataPath should be %s not %s", ModWafDataPath, conf.ConfigPath.ModWafDataPath)
+ }
+ if conf.ConfigPath.ProductParamPath != productParamPath {
+ t.Errorf("ProductParamPath should be %s not %s", productParamPath, conf.ConfigPath.ProductParamPath)
+ }
+ if conf.ConfigPath.WafInstancesPath != wafInstancesPath {
+ t.Errorf("AlbWafInstancesPath should be %s not %s", wafInstancesPath, conf.ConfigPath.WafInstancesPath)
+ }
+}
diff --git a/bfe_modules/mod_unified_waf/mod_unified_waf.go b/bfe_modules/mod_unified_waf/mod_unified_waf.go
new file mode 100644
index 000000000..60689ce80
--- /dev/null
+++ b/bfe_modules/mod_unified_waf/mod_unified_waf.go
@@ -0,0 +1,481 @@
+// Copyright (c) 2025 The BFE Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mod_unified_waf
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "time"
+
+ "github.com/baidu/go-lib/log"
+ "github.com/baidu/go-lib/web-monitor/delay_counter"
+ "github.com/baidu/go-lib/web-monitor/module_state2"
+ "github.com/baidu/go-lib/web-monitor/web_monitor"
+ "github.com/bfenetworks/bfe/bfe_basic"
+ "github.com/bfenetworks/bfe/bfe_http"
+ "github.com/bfenetworks/bfe/bfe_module"
+)
+
+// delay_counter.DelayRecent parameters
+const (
+ DELAY_STAT_INTERVAL = 20 // delay stat interval
+ DELAY_BUCKET_SIZE = 1 // delay bucket size
+ DELAY_BUCKET_NUM = 20 // delay bucket num
+)
+
+const (
+ DIFF_COUNTER_INTERVAL = 20
+)
+
+const NoneWafName = "None"
+
+const (
+ ModUnifiedWaf = "mod_unified_waf"
+
+ KP_SD_MOD_WAF = "waf_client"
+ KP_SD_MOD_WAF_DIFF = "waf_client_diff"
+ KP_MOD_WAF_DELAY = "waf_client_delay"
+ KP_MOD_WAF_PEEK_DELAY = "waf_client_delay_peek_body"
+ KP_MOD_WAF_COMP_DELAY = "waf_client_delay_call_competition"
+
+ TO_DELETE_CLIENTS = "waf_client.to_delete_clients"
+ ACTIVE_CLIENTS = "waf_client.active_clients"
+ DELETED_CLIENTS = "waf_client.deleted_clients"
+ ADDED_CLIENTS = "waf_client.added_clients"
+)
+
+var COUNTER_KEYS = []string{
+ bfe_basic.REQ_NO_CHECK,
+ bfe_basic.REQ_FORBIDDEN,
+ bfe_basic.REQ_OK,
+ bfe_basic.REQ_TIMEOUT,
+ bfe_basic.REQ_OTHER,
+ bfe_basic.NET_ERR,
+}
+
+var (
+ openDebug = false
+)
+
+type ModuleWaf struct {
+ name string // name of module
+ conf *ConfModWaf
+ wafClientPool *WafClientPool
+ prodParams *ProductParamTable
+ wafData *GlobalParamConf
+
+ modWafDataPath string // path for mod_unified_waf.data
+ productParamPath string // path for product_param.data
+ wafInstancesPath string // path for waf_instances.data
+
+ monitor *MonitorStates // monitor states
+
+ isNoneWaf bool
+}
+
+func NewModuleWaf() *ModuleWaf {
+ m := new(ModuleWaf)
+ m.name = ModUnifiedWaf
+
+ m.monitor = NewMonitorStates()
+ m.wafClientPool = NewWafClientPool(m.monitor)
+ m.prodParams = NewProductParamTable()
+
+ return m
+}
+
+func (m *ModuleWaf) Name() string {
+ return m.name
+}
+
+func (m *ModuleWaf) Init(cbs *bfe_module.BfeCallbacks, whs *web_monitor.WebHandlers, cr string) error {
+ var err error
+
+ // parse config
+ confPath := bfe_module.ModConfPath(cr, m.name)
+ if err = m.LoadConfig(confPath, cr); err != nil {
+ return fmt.Errorf("%s.Init(): ParseConfig %s", m.name, err.Error())
+ }
+
+ m.monitor.state.Set("WafProductName", m.conf.Basic.WafProductName)
+ if m.conf.Basic.WafProductName == NoneWafName {
+ m.isNoneWaf = true
+ } else {
+ m.isNoneWaf = false
+ }
+ if m.isNoneWaf {
+ log.Logger.Info("WafProductName is None.")
+ }
+
+ // set debug switch
+ openDebug = m.conf.Log.OpenDebug
+ if openDebug {
+ log.Logger.Debug("mod_unified_waf openDebug")
+ }
+
+ if !m.isNoneWaf {
+ err = m.wafClientPool.SetConfBasic(m.conf.Basic)
+ if err != nil {
+ // log.Logger.Error("failed to SetConfBasic: %s", err.Error())
+ return err
+ }
+ }
+
+ // load configs
+ err = m.loadWafData(nil)
+ if err != nil {
+ return fmt.Errorf("%s.Init(): loadWafData(): %s", m.name, err.Error())
+ }
+
+ err = m.loadWafInstances(nil)
+ if err != nil {
+ return fmt.Errorf("%s.Init(): loadWafInstances(): %s", m.name, err.Error())
+ }
+
+ err = m.loadProductParam(nil)
+ if err != nil {
+ return fmt.Errorf("%s.Init(): loadProductParam(): %s", m.name, err.Error())
+ }
+
+ if !m.isNoneWaf {
+ // register handler
+ err = cbs.AddFilter(bfe_module.HandleAfterLocation, m.wafHandler) // for after location
+ if err != nil {
+ return fmt.Errorf("%s.Init(): AddFilter(m.wafHandler): %s", m.name, err.Error())
+ }
+ }
+
+ // register web handlers for reload
+ err = web_monitor.RegisterHandlers(whs, web_monitor.WebHandleReload, m.reloadHandlers())
+ if err != nil {
+ return fmt.Errorf("%s.Init(): RegisterHandlers(m.reloadHandlers): %s", m.name, err.Error())
+ }
+
+ // register web handlers for monitor
+ err = web_monitor.RegisterHandlers(whs, web_monitor.WebHandleMonitor, m.monitorHandlers())
+ if err != nil {
+ return fmt.Errorf("%s.Init(): RegisterHandlers(m.monitorHandlers): %s", m.name, err.Error())
+ }
+
+ return nil
+}
+
+func (m *ModuleWaf) getState() *module_state2.StateData {
+
+ res := m.monitor.state.GetAll()
+
+ return res
+}
+
+func (m *ModuleWaf) getStateDiff() *module_state2.CounterDiff {
+ stateDiff := m.monitor.stateDiff.Get()
+ return &stateDiff
+}
+
+func (m *ModuleWaf) getMetricsState(params map[string][]string) ([]byte, error) {
+ s := m.monitor.metrics.GetAll()
+ return s.Format(params)
+}
+
+// register web monitor handlers
+func (m *ModuleWaf) monitorHandlers() map[string]interface{} {
+ handlers := map[string]interface{}{
+ m.name: web_monitor.CreateStateDataHandler(m.getState),
+ m.name + ".diff": web_monitor.CreateCounterDiffHandler(m.getStateDiff),
+ m.name + ".delay": m.monitor.delay.FormatOutput,
+ m.name + ".delay_peek_body": m.monitor.delayPeekBody.FormatOutput,
+ m.name + ".delay_call_competition": m.monitor.delayCallComp.FormatOutput,
+ m.name + ".mstate": m.getMetricsState,
+ }
+
+ return handlers
+}
+
+// register web reload handlers
+func (m *ModuleWaf) reloadHandlers() map[string]interface{} {
+ handlers := map[string]interface{}{
+ m.name + ".product_parameter": m.loadProductParam,
+ m.name + ".waf_data": m.loadWafData,
+ m.name + ".waf_instances": m.loadWafInstances,
+ }
+
+ return handlers
+}
+
+// for mod_unified_waf.data
+func (m *ModuleWaf) WafClientDataLoad(path string) error {
+ data, err := WafDataParamLoadAndCheck(path)
+ if err != nil {
+ return err
+ }
+ m.wafData = data
+
+ if !m.isNoneWaf {
+ m.wafClientPool.UpdateWafParam(data)
+ }
+
+ ver := data.Version
+ param := &data.Config
+ bdata, _ := json.Marshal(param)
+ m.monitor.state.Set("GlobalParam", string(bdata))
+ m.monitor.state.Set("GlobalParam.Version", ver)
+
+ return nil
+}
+
+func (m *ModuleWaf) WafInstancesLoad(path string) error {
+ data, err := WafInstancesLoadAndCheck(path)
+ if err != nil {
+ return err
+ }
+
+ wafInstances := data.WafCluster
+
+ if !m.isNoneWaf {
+ m.wafClientPool.Update(wafInstances)
+ }
+
+ instData, _ := json.Marshal(wafInstances)
+ m.monitor.state.Set("WafInstances", string(instData))
+ m.monitor.state.Set("WafInstances.Version", data.Version)
+
+ return nil
+}
+
+// for product_param.data
+func (m *ModuleWaf) ProductParamLoad(path string) error {
+ data, err := ProductParamLoadAndCheck(path)
+ if err != nil {
+ return err
+ }
+
+ m.prodParams.Update(data.Config, data.Version)
+
+ conf, _ := json.Marshal(data.Config)
+ m.monitor.state.Set("ProductParam", string(conf))
+ m.monitor.state.Set("ProductParam.Version", data.Version)
+
+ return nil
+}
+
+// loadWafData is a registered reload callback
+// params:
+// - query: url query, query["path"] is the file need to load
+// if query["path"] is not set, use default path
+func (m *ModuleWaf) loadWafData(query url.Values) error {
+ // get file path
+ path := query.Get("path")
+ if path == "" {
+ //use default
+ path = m.modWafDataPath
+ }
+ err := m.WafClientDataLoad(path)
+ return err
+}
+
+// loadWafInstances is a registered reload callback
+// params:
+// - query: url query, query["path"] is the file need to load
+// if query["path"] is not set, use default path
+func (m *ModuleWaf) loadWafInstances(query url.Values) error {
+ // get file path
+ path := query.Get("path")
+ if path == "" {
+ //use default
+ path = m.wafInstancesPath
+ }
+ err := m.WafInstancesLoad(path)
+ if err != nil {
+ log.Logger.Warn("loadWafInstances(): %s", err.Error())
+ }
+ return err
+}
+
+// loadProductParam is a registered reload callback
+// params:
+// - query: url query, query["path"] is the file need to load
+// if query["path"] is not set, use default path
+func (m *ModuleWaf) loadProductParam(query url.Values) error {
+ // get file path
+ path := query.Get("path")
+ if path == "" {
+ //use default
+ path = m.productParamPath
+ }
+ err := m.ProductParamLoad(path)
+ return err
+}
+
+// load configure from conf file
+func (m *ModuleWaf) LoadConfig(confPath string, confRoot string) error {
+ conf, err := ConfLoad(confPath, confRoot)
+ if err != nil {
+ return fmt.Errorf("%s conf load error %s", m.name, err.Error())
+ }
+ m.conf = conf
+
+ m.modWafDataPath = conf.ConfigPath.ModWafDataPath
+ m.productParamPath = conf.ConfigPath.ProductParamPath
+ m.wafInstancesPath = conf.ConfigPath.WafInstancesPath
+
+ return nil
+}
+
+func (m *ModuleWaf) getRequestWafParam(req *bfe_basic.Request) *WafParam {
+ return m.prodParams.GetRequestWafParam(req)
+}
+
+// module call backs
+// handler for finish http request
+func (m *ModuleWaf) wafHandler(req *bfe_basic.Request) (int, *bfe_http.Response) {
+ conf := m.getRequestWafParam(req)
+ // no waf check
+ if conf == nil {
+ if openDebug {
+ log.Logger.Debug("product %s has no waf config", req.Route.Product)
+ }
+
+ m.monitor.state.Inc(bfe_basic.REQ_NO_CHECK, 1)
+ setWafStatus(req, int(bfe_basic.WAF_NO_CHECK))
+ return bfe_module.BfeHandlerGoOn, nil
+ }
+ // if wafClientPool is not created. this should never happen.
+ if m.wafClientPool == nil {
+ log.Logger.Warn("wafClientPool is nil")
+ m.monitor.state.Inc(bfe_basic.REQ_NO_CHECK, 1)
+ setWafStatus(req, int(bfe_basic.WAF_NO_CHECK))
+ return bfe_module.BfeHandlerGoOn, nil
+ }
+
+ // convert request
+ wafReq, err := m.genWafRequest(req, conf, &m.monitor.delayPeekBody)
+ if err != nil {
+ log.Logger.Error("genWafRequest(): %s", err.Error())
+ m.monitor.state.Inc(bfe_basic.REQ_NO_CHECK, 1)
+ setWafStatus(req, int(bfe_basic.WAF_NO_CHECK))
+ return bfe_module.BfeHandlerGoOn, nil
+ }
+
+ // get a waf client object
+ wafClient, err := m.wafClientPool.Alloc()
+ if err != nil {
+ // only if all waf-instance is not usable
+ log.Logger.Warn("m.wafClientPool.Alloc() failed: %s", err.Error())
+ m.monitor.state.Inc(bfe_basic.REQ_NO_CHECK, 1)
+ setWafStatus(req, int(bfe_basic.WAF_NO_CHECK))
+ return bfe_module.BfeHandlerGoOn, nil
+ }
+ defer m.wafClientPool.Release(wafClient)
+
+ // call waf-server
+ block, eventId := wafClient.Detect(req, wafReq, conf)
+ if block {
+ return bfe_module.BfeHandlerFinish, GenForbiddenHttpResponse(req, eventId)
+ }
+
+ return bfe_module.BfeHandlerGoOn, nil
+}
+
+// generate request for remote call
+func (m *ModuleWaf) genWafRequest(req *bfe_basic.Request, param *WafParam, delayPeekBody *delay_counter.DelayRecent) (*http.Request, error) {
+ httpRequest := req.HttpRequest
+ wafRequest, err := http.NewRequest(req.HttpRequest.Method, httpRequest.URL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ // copy request data
+ wafRequest.Method = httpRequest.Method
+ wafRequest.URL = httpRequest.URL
+ wafRequest.Proto = httpRequest.Proto
+ wafRequest.ProtoMajor = httpRequest.ProtoMajor
+ wafRequest.ProtoMinor = httpRequest.ProtoMinor
+ //copy httpRequest.Header
+ wafRequest.Header = generateHeaders(httpRequest.Header)
+ wafRequest.TransferEncoding = httpRequest.TransferEncoding
+ wafRequest.Host = httpRequest.Host
+ wafRequest.Form = httpRequest.Form
+ wafRequest.PostForm = httpRequest.PostForm
+ wafRequest.MultipartForm = httpRequest.MultipartForm
+ //copy httpRequest.Trailer
+ wafRequest.Trailer = generateHeaders(httpRequest.Trailer)
+ wafRequest.RemoteAddr = httpRequest.RemoteAddr
+ wafRequest.RequestURI = httpRequest.RequestURI
+
+ // make empty body
+ wafRequest.Body = ioutil.NopCloser(bytes.NewReader([]byte{}))
+ wafRequest.ContentLength = 0
+ wafRequest.Header.Set("Content-Length", fmt.Sprintf("%d", wafRequest.ContentLength))
+
+ // copy body if needed
+ var peekN int64 = 0
+ if param.SendBody && checkBodyWithHttpMethod(httpRequest.Method) && httpRequest.ContentLength > 0 {
+ // set when request is not chunk (ContentLength > 0) and method is POST/PUT/PATCH
+ peekN = httpRequest.ContentLength
+ if peekN > int64(param.SendBodySize) {
+ peekN = int64(param.SendBodySize)
+ }
+ }
+
+ if peekN <= 0 {
+ return wafRequest, nil
+ }
+
+ var wafBodySize int64
+ if p, ok := httpRequest.Body.(Peeker); ok {
+ t := time.Now()
+ b, err := p.Peek(int(peekN))
+ if err == nil {
+ // set body
+ wafRequest.Body = ioutil.NopCloser(bytes.NewReader(b))
+ wafBodySize = int64(len(b))
+ wafRequest.ContentLength = wafBodySize
+ if openDebug {
+ log.Logger.Info("mod_unified_waf Peek succ, %d, contentlen:%d", peekN, wafBodySize)
+ }
+ } else {
+ log.Logger.Info("mod_unified_waf genWafRequest():peekN:%d, contentlen:%d, peek body err %s", peekN, httpRequest.ContentLength, err)
+ }
+
+ delayPeekBody.AddBySub(t, time.Now())
+ } else {
+ log.Logger.Info("mod_unified_waf genWafRequest(): do not have Peeker")
+ }
+
+ wafRequest.Header.Set("Content-Length", fmt.Sprintf("%d", wafRequest.ContentLength))
+
+ return wafRequest, nil
+}
+
+type wafForbiddenInfo struct {
+ EventId string `json:"event_id"`
+}
+
+func GenForbiddenHttpResponse(req *bfe_basic.Request, eventId string) *bfe_http.Response {
+ tmp := &wafForbiddenInfo{}
+ tmp.EventId = eventId
+ bodystr := ""
+ if bodybytes, err := json.Marshal(tmp); err == nil {
+ bodystr = string(bodybytes)
+ }
+
+ ret := bfe_basic.CreateSpecifiedContentResp(req, bfe_http.StatusOK, "application/json", bodystr)
+
+ return ret
+}
diff --git a/bfe_modules/mod_unified_waf/product_param_load.go b/bfe_modules/mod_unified_waf/product_param_load.go
new file mode 100644
index 000000000..0d28b96f6
--- /dev/null
+++ b/bfe_modules/mod_unified_waf/product_param_load.go
@@ -0,0 +1,107 @@
+// Copyright (c) 2025 The BFE Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mod_unified_waf
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+
+ "github.com/bfenetworks/bfe/bfe_util"
+)
+
+// product waf parameters
+type WafParam struct {
+ SendBody bool // is need to send http body
+ SendBodySize int // send how many bytes of body
+}
+
+// each product's waf param
+// key is product name
+type ProductParams map[string]WafParam
+
+// product parameters in config file
+type ProductParamConfFile struct {
+ Version *string // version string
+ Config *ProductParams // product param
+}
+
+type ProductParamConf struct {
+ Version string
+ Config ProductParams
+}
+
+func (cfg *ProductParamConfFile) Check() error {
+ if err := bfe_util.CheckNilField(*cfg, false); err != nil {
+ return err
+ }
+
+ if cfg.Config != nil {
+ // check ProductWafFile
+ for product, param := range *cfg.Config {
+ if err := param.Check(); err != nil {
+ return fmt.Errorf("%s: %s", product, err.Error())
+ }
+ }
+ }
+
+ return nil
+}
+
+func (p *WafParam) Check() error {
+ if p.SendBodySize < 0 {
+ return fmt.Errorf("SendBodySize should >= 0")
+ }
+
+ if p.SendBody && p.SendBodySize <= 0 {
+ return fmt.Errorf("SendBody and SendBodySize should > 0")
+ }
+
+ return nil
+}
+
+// reload_trigger adaptor interface
+func ProductParamLoadAndCheck(filename string) (ProductParamConf, error) {
+ var err error
+ var data ProductParamConf
+
+ // open the file
+ file, err := os.Open(filename)
+ if err != nil {
+ return data, err
+ }
+ defer file.Close()
+
+ // decode the file
+ decoder := json.NewDecoder(file)
+ var dataFile ProductParamConfFile
+ err = decoder.Decode(&dataFile)
+ if err != nil {
+ return data, err
+ }
+
+ // check config
+ if err := dataFile.Check(); err != nil {
+ return data, err
+ }
+
+ // convert config
+ data.Version = *dataFile.Version
+ if dataFile.Config != nil {
+ data.Config = *dataFile.Config
+ }
+
+ return data, nil
+}
diff --git a/bfe_modules/mod_unified_waf/product_param_load_test.go b/bfe_modules/mod_unified_waf/product_param_load_test.go
new file mode 100644
index 000000000..5b56b3fc9
--- /dev/null
+++ b/bfe_modules/mod_unified_waf/product_param_load_test.go
@@ -0,0 +1,84 @@
+// Copyright (c) 2025 The BFE Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mod_unified_waf
+
+import (
+ "testing"
+)
+
+func TestProductParamLoadAndCheck_1(t *testing.T) {
+ productParamPath := "./testdata/product_param.data"
+
+ conf, err := ProductParamLoadAndCheck(productParamPath)
+ if err != nil {
+ t.Errorf("ProductParamLoadAndCheck(): %v", err)
+ return
+ }
+
+ p1, found := conf.Config["ProductA"]
+ if !found {
+ t.Errorf("ProductParamLoadAndCheck(): ProductA is not found")
+ return
+ }
+ if p1.SendBody != false && p1.SendBodySize != 0 {
+ t.Errorf("ProductParamLoadAndCheck(): ProductA param err: %v", p1)
+ return
+ }
+
+ p2, found := conf.Config["ProductB"]
+ if !found {
+ t.Errorf("ProductParamLoadAndCheck(): ProductB is not found")
+ return
+ }
+ if p2.SendBody != true && p2.SendBodySize != 4096 {
+ t.Errorf("ProductParamLoadAndCheck(): ProductB param err: %v", p2)
+ return
+ }
+}
+
+func TestProductParamLoadAndCheck_2(t *testing.T) {
+ productParamPath := "./testdata/product_param_1.data"
+
+ _, err := ProductParamLoadAndCheck(productParamPath)
+ if err == nil {
+ t.Errorf("ProductParamLoadAndCheck() should return error")
+ return
+ }
+}
+
+func TestProductParamLoadAndCheck_3(t *testing.T) {
+ productParamPath := "./testdata/product_param_2.data"
+
+ cfg, err := ProductParamLoadAndCheck(productParamPath)
+ if err != nil {
+ t.Errorf("ProductParamLoadAndCheck(): %v", err)
+ return
+ }
+
+ if cfg.Config["ProductA"].SendBody != false && cfg.Config["ProductA"].SendBodySize != 0 {
+ t.Errorf("ProductA: %v", cfg.Config["ProductA"])
+ return
+ }
+}
+
+func TestProductParamLoadAndCheck_4(t *testing.T) {
+ productParamPath := "./testdata/product_param_empty.data"
+
+ _, err := ProductParamLoadAndCheck(productParamPath)
+ if err != nil {
+ t.Errorf("ProductParamLoadAndCheck() should not return error:%v", err)
+ return
+ }
+}
diff --git a/bfe_modules/mod_unified_waf/product_param_table.go b/bfe_modules/mod_unified_waf/product_param_table.go
new file mode 100644
index 000000000..553123f6a
--- /dev/null
+++ b/bfe_modules/mod_unified_waf/product_param_table.go
@@ -0,0 +1,62 @@
+// Copyright (c) 2025 The BFE Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mod_unified_waf
+
+import (
+ "sync"
+
+ "github.com/bfenetworks/bfe/bfe_basic"
+)
+
+type ProductParamTable struct {
+ lock sync.RWMutex
+ prodParam *ProductParams
+ version string
+}
+
+func NewProductParamTable() *ProductParamTable {
+ t := new(ProductParamTable)
+ t.prodParam = &ProductParams{}
+
+ return t
+}
+
+func (t *ProductParamTable) Update(param ProductParams, ver string) {
+ t.lock.Lock()
+ t.prodParam = ¶m
+ t.version = ver
+ t.lock.Unlock()
+}
+
+func (t *ProductParamTable) GetRequestWafParam(req *bfe_basic.Request) *WafParam {
+ t.lock.RLock()
+ table := t.prodParam
+ t.lock.RUnlock()
+
+ productName := req.Route.Product
+ if param, ok := (*table)[productName]; ok {
+ return ¶m
+ }
+
+ return nil
+}
+
+func (t *ProductParamTable) Version() string {
+ t.lock.RLock()
+ version := t.version
+ t.lock.RUnlock()
+
+ return version
+}
diff --git a/bfe_modules/mod_unified_waf/states.go b/bfe_modules/mod_unified_waf/states.go
new file mode 100644
index 000000000..d8cb78462
--- /dev/null
+++ b/bfe_modules/mod_unified_waf/states.go
@@ -0,0 +1,59 @@
+// Copyright (c) 2025 The BFE Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mod_unified_waf
+
+import (
+ "github.com/baidu/go-lib/web-monitor/delay_counter"
+ "github.com/baidu/go-lib/web-monitor/metrics"
+ "github.com/baidu/go-lib/web-monitor/module_state2"
+)
+
+type ModuleWafState struct {
+}
+
+type MonitorStates struct {
+ delay delay_counter.DelayRecent // delay counter for request of wait response type
+ delayPeekBody delay_counter.DelayRecent // delay counter for peek http body
+ delayCallComp delay_counter.DelayRecent // delay counter for concurrency call competition
+ state *module_state2.State // module state
+ stateDiff module_state2.CounterSlice // diff counter of module state
+
+ underlyingState ModuleWafState
+ metrics metrics.Metrics //moudle state with prometheus format
+
+}
+
+func NewMonitorStates() *MonitorStates {
+ m := MonitorStates{}
+ m.delay.Init(DELAY_STAT_INTERVAL, DELAY_BUCKET_SIZE, DELAY_BUCKET_NUM)
+ m.delayPeekBody.Init(DELAY_STAT_INTERVAL, DELAY_BUCKET_SIZE, DELAY_BUCKET_NUM)
+ m.delayCallComp.Init(DELAY_STAT_INTERVAL, DELAY_BUCKET_SIZE, DELAY_BUCKET_NUM)
+
+ m.state = new(module_state2.State)
+ m.state.Init()
+ m.state.CountersInit(COUNTER_KEYS)
+ m.stateDiff.Init(m.state, DIFF_COUNTER_INTERVAL)
+
+ m.delay.SetKeyPrefix(KP_MOD_WAF_DELAY)
+ m.delayPeekBody.SetKeyPrefix(KP_MOD_WAF_PEEK_DELAY)
+ m.delayCallComp.SetKeyPrefix(KP_MOD_WAF_COMP_DELAY)
+
+ m.state.SetKeyPrefix(KP_SD_MOD_WAF)
+ m.stateDiff.SetKeyPrefix(KP_SD_MOD_WAF_DIFF)
+
+ m.metrics.Init(&m.underlyingState, ModUnifiedWaf, 0)
+
+ return &m
+}
diff --git a/bfe_modules/mod_unified_waf/testdata/mod_unified_waf.conf b/bfe_modules/mod_unified_waf/testdata/mod_unified_waf.conf
new file mode 100644
index 000000000..617515a70
--- /dev/null
+++ b/bfe_modules/mod_unified_waf/testdata/mod_unified_waf.conf
@@ -0,0 +1,14 @@
+[Basic]
+#None, BFEMockWaf
+WafProductName = BFEMockWaf
+# Concurrency = 2000
+ConnPoolSize = 8
+
+[ConfigPath]
+ModWafDataPath = "./testdata/mod_unified_waf.data"
+ProductParamPath = "./testdata/product_param.data"
+WafInstancesPath = "./testdata/waf_instances.data"
+
+[Log]
+OpenDebug = false
+
diff --git a/bfe_modules/mod_unified_waf/testdata/mod_unified_waf.data b/bfe_modules/mod_unified_waf/testdata/mod_unified_waf.data
new file mode 100644
index 000000000..8d1f58aa1
--- /dev/null
+++ b/bfe_modules/mod_unified_waf/testdata/mod_unified_waf.data
@@ -0,0 +1,18 @@
+{
+ "Version": "2023-01-19 12:00:10",
+ "Config": {
+ "WafClient": {
+ "ConnectTimeout": 30,
+ "Concurrency": 10,
+ "MaxWaitCount": 10
+ },
+ "WafDetect": {
+ "RetryMax": 2,
+ "ReqTimeout": 50
+ },
+ "HealthChecker": {
+ "UnavailableFailedThres": 20,
+ "HealthCheckInterval": 1000
+ }
+ }
+}
\ No newline at end of file
diff --git a/bfe_modules/mod_unified_waf/testdata/mod_unified_waf_2.data b/bfe_modules/mod_unified_waf/testdata/mod_unified_waf_2.data
new file mode 100644
index 000000000..069402235
--- /dev/null
+++ b/bfe_modules/mod_unified_waf/testdata/mod_unified_waf_2.data
@@ -0,0 +1,27 @@
+{
+ "Version": "2023-01-19 12:00:10",
+ "Config": {
+ "WafClient": {
+ "ConnectTimeout": 30,
+ "Concurrency": 10,
+ "MaxWaitCount": 10
+ },
+ "WafDetect": {
+ "RetryMax": 2,
+ "ReqTimeout": 50
+ },
+ "MaxWafMem": 1024,
+ "HealthChecker": {
+ "UnavailableFailedThres": 20,
+ "HealthCheckInterval": 1000
+ },
+ "CircuitBreaker": {
+ "Enable": true,
+ "OpenThres": 0.2,
+ "DurationOpen2HalfOpen": 0,
+ "ClosedThres": 0.8,
+ "SamplePercent": 5.0,
+ "StatTotalOpCount": 100
+ }
+ }
+}
\ No newline at end of file
diff --git a/bfe_modules/mod_unified_waf/testdata/product_param.data b/bfe_modules/mod_unified_waf/testdata/product_param.data
new file mode 100644
index 000000000..a8e55067f
--- /dev/null
+++ b/bfe_modules/mod_unified_waf/testdata/product_param.data
@@ -0,0 +1,13 @@
+{
+ "Version": "2023-01-19 12:00:10",
+ "Config": {
+ "ProductA": {
+ "SendBody": false,
+ "SendBodySize": 0
+ },
+ "ProductB": {
+ "SendBody": true,
+ "SendBodySize": 4096
+ }
+ }
+}
\ No newline at end of file
diff --git a/bfe_modules/mod_unified_waf/testdata/product_param_1.data b/bfe_modules/mod_unified_waf/testdata/product_param_1.data
new file mode 100644
index 000000000..60622ea19
--- /dev/null
+++ b/bfe_modules/mod_unified_waf/testdata/product_param_1.data
@@ -0,0 +1,13 @@
+{
+ "Version": "2023-01-19 12:00:10",
+ "Config": {
+ "ProductA": {
+ "SendBody": false,
+ "SendBodySize": 0
+ },
+ "ProductB": {
+ "SendBody": true,
+ "SendBodySize": 0
+ }
+ }
+}
\ No newline at end of file
diff --git a/bfe_modules/mod_unified_waf/testdata/product_param_2.data b/bfe_modules/mod_unified_waf/testdata/product_param_2.data
new file mode 100644
index 000000000..9eaf14f29
--- /dev/null
+++ b/bfe_modules/mod_unified_waf/testdata/product_param_2.data
@@ -0,0 +1,11 @@
+{
+ "Version": "2023-01-19 12:00:10",
+ "Config": {
+ "ProductA": {
+ },
+ "ProductB": {
+ "SendBody": true,
+ "SendBodySize": 1024
+ }
+ }
+}
\ No newline at end of file
diff --git a/bfe_modules/mod_unified_waf/testdata/product_param_empty.data b/bfe_modules/mod_unified_waf/testdata/product_param_empty.data
new file mode 100644
index 000000000..ba7b38cb0
--- /dev/null
+++ b/bfe_modules/mod_unified_waf/testdata/product_param_empty.data
@@ -0,0 +1,5 @@
+{
+ "Version": "2023-01-19 12:00:10",
+ "Config": {
+ }
+}
\ No newline at end of file
diff --git a/bfe_modules/mod_unified_waf/testdata/waf_instances.data b/bfe_modules/mod_unified_waf/testdata/waf_instances.data
new file mode 100644
index 000000000..3d9ba037f
--- /dev/null
+++ b/bfe_modules/mod_unified_waf/testdata/waf_instances.data
@@ -0,0 +1,9 @@
+{
+ "Version": "2023-01-19 12:00:10",
+ "Config": {
+ "WafCluster": [
+ {"IpAddr": "10.10.10.1", "Port": 80},
+ {"IpAddr": "10.10.10.2", "Port": 80, "HealthCheckPort": 5001}
+ ]
+ }
+}
diff --git a/bfe_modules/mod_unified_waf/testdata/waf_instances_empty.data b/bfe_modules/mod_unified_waf/testdata/waf_instances_empty.data
new file mode 100644
index 000000000..1a4c334b9
--- /dev/null
+++ b/bfe_modules/mod_unified_waf/testdata/waf_instances_empty.data
@@ -0,0 +1,5 @@
+{
+ "Version": "2023-01-19 12:00:10",
+ "Config": {
+ }
+}
diff --git a/bfe_modules/mod_unified_waf/waf_client.go b/bfe_modules/mod_unified_waf/waf_client.go
new file mode 100644
index 000000000..e53e3e897
--- /dev/null
+++ b/bfe_modules/mod_unified_waf/waf_client.go
@@ -0,0 +1,580 @@
+// Copyright (c) 2025 The BFE Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mod_unified_waf
+
+import (
+ "errors"
+ "fmt"
+ "net"
+ "net/http"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/baidu/go-lib/gotrack"
+ "github.com/baidu/go-lib/log"
+
+ "github.com/bfenetworks/bfe/bfe_basic"
+ "github.com/bfenetworks/bfe/bfe_http"
+ "github.com/bfenetworks/bfe/bfe_modules/mod_unified_waf/waf_impl"
+ "github.com/bfenetworks/bwi/bwi"
+)
+
+var (
+ ERR_WAF_FORBIDDEN = errors.New("FORBIDDEN_BY_WAF") // request forbidden by waf
+)
+
+type WafDetectResult struct {
+ Result bwi.WafResult
+ Error error
+}
+
+func (obj *WafDetectResult) getWafEventId() string {
+ if obj.Result == nil {
+ return ""
+ }
+ return obj.Result.GetEventId()
+}
+
+func (obj *WafDetectResult) Passed() bool {
+ return obj.Result != nil && obj.Result.GetResultFlag() == bwi.WAF_RESULT_PASS
+}
+
+func (obj *WafDetectResult) Blocked() bool {
+ return obj.Result != nil && obj.Result.GetResultFlag() == bwi.WAF_RESULT_BLOCK
+}
+
+type HealthCheckerConf struct {
+ UnavailableFailedThres int64 //unavailable failed threshold
+ HealthCheckInterval int64 //health check interval(ms)
+}
+
+type WafClient struct {
+ wafEntries *waf_impl.WafImplMethodBundle
+ client bwi.WafServer // chang-ting sdk client
+ serverAddress string // waf server instance address
+ serverIP string
+ hcPort atomic.Uint32
+
+ connectTimeout time.Duration // connection timeout
+ // reqTimeout time.Duration // detection timeout for a request
+ // retryMax int // detection retry for a request
+ globalWafParam *GlobalParam
+
+ concurrency int // how many concurrency goroutine call waf-server
+ concurrencyChan chan int // concurrency pool
+
+ monitor *MonitorStates
+
+ refCount int // reference counter
+ toDelete bool // if toDelete set true, it indicates current waf client is going to be deleted
+
+ lock sync.RWMutex
+
+ errCounter atomic.Int64 // gosnserver err counter
+ available atomic.Bool // if errCounter >= AVAILABLE_THRESHOLD, let available = false
+ maxWaitCount atomic.Int64
+ curWaitCount atomic.Int64
+
+ HCConf HealthCheckerConf
+
+ exitCh chan struct{}
+}
+
+type Peeker interface {
+ Peek(n int) ([]byte, error)
+}
+
+func NewWafClient(wafEntries *waf_impl.WafImplMethodBundle, addr string, instConf *WafInstance, wafParam *GlobalParam, poolSize int, m *MonitorStates) (*WafClient, error) {
+ connectTimeout := time.Duration(wafParam.WafClient.ConnectTimeout * int(time.Millisecond))
+
+ c := new(WafClient)
+ c.monitor = m
+ c.serverAddress = addr
+ c.serverIP = instConf.IpAddr
+ c.UpdateInstanceConf(instConf)
+
+ c.connectTimeout = connectTimeout
+ c.wafEntries = wafEntries
+ c.client = c.wafEntries.NewWafServerWithPoolSize(func() (net.Conn, error) {
+ conn, err := net.DialTimeout("tcp", c.serverAddress, c.connectTimeout)
+ if err != nil {
+ c.monitor.state.Inc(bfe_basic.NET_ERR, 1)
+ }
+ return conn, err
+ }, poolSize)
+
+ c.globalWafParam = wafParam
+
+ c.concurrency = wafParam.WafClient.Concurrency
+ c.concurrencyChan = make(chan int, c.concurrency)
+ for i := 0; i < c.concurrency; i++ {
+ c.concurrencyChan <- 1
+ }
+ log.Logger.Info("Set waf client: %s concurrency = %d", c.serverAddress, c.concurrency)
+
+ c.errCounter.Store(0)
+ c.available.Store(true)
+
+ c.updateHCConf(&wafParam.HealthChecker)
+
+ c.curWaitCount.Store(0)
+ c.maxWaitCount.Store(int64(wafParam.WafClient.MaxWaitCount))
+
+ c.monitor.state.Set("waf_client_available_"+c.serverAddress, "true")
+
+ c.exitCh = make(chan struct{}, 1)
+ go c.checkWafServer() // start health check task
+
+ return c, nil
+}
+
+func (c *WafClient) Detect(req *bfe_basic.Request, wafReq *http.Request, param *WafParam) (bool, string) {
+ c.monitor.state.Inc("waf_client_detect_"+c.serverAddress, 1)
+
+ reqTimeout, retryMax := c.GetDetectParam(wafReq.ContentLength)
+ var startTime, endTime, finTime time.Time
+ startTime = time.Now()
+ finTime = startTime.Add(reqTimeout)
+
+ if c.curWaitCount.Load() > c.maxWaitCount.Load() {
+ c.monitor.state.Inc(bfe_basic.REQ_NO_CHECK, 1)
+ c.monitor.state.Inc("waf_client_detect_closed_skip_"+c.serverAddress, 1)
+ setWafStatus(req, (int)(bfe_basic.WAF_NO_CHECK))
+ if openDebug {
+ log.Logger.Debug("waf instance is closed, but skip, instance = %s, logid = %s",
+ c.serverAddress, req.LogId)
+ }
+ return false, ""
+ }
+
+ isGetToken := c.getToken(req, reqTimeout, req.LogId)
+ endTime = time.Now()
+ if isGetToken {
+ c.monitor.delayCallComp.AddBySub(startTime, endTime)
+ } else {
+ c.monitor.delayCallComp.AddBySub(startTime, endTime)
+ c.monitor.state.Inc(bfe_basic.REQ_TIMEOUT, 1)
+ c.monitor.state.Inc("waf_client_detect_concurrency_timout_"+c.serverAddress, 1)
+ setWafStatus(req, (int)(bfe_basic.WAF_TIMEOUT))
+ setWafSpentTime(req, startTime, endTime)
+
+ if openDebug {
+ log.Logger.Debug("time out for concurrency control, logid = %s, start = %d, end = %d",
+ req.LogId, startTime.UnixNano(), endTime.UnixNano())
+ }
+ return false, ""
+ }
+
+ // get remaining time
+ diff := finTime.Sub(endTime)
+ if diff <= 0 {
+ diff = time.Duration(1 * time.Millisecond)
+ }
+
+ leftTimer := time.NewTicker(diff)
+ defer leftTimer.Stop()
+
+ // call waf server
+ done := make(chan *WafDetectResult, 1)
+ go c.detect(wafReq, done, retryMax, req.LogId)
+
+ // wait result
+ select {
+ case res := <-done:
+ if res.Error != nil {
+ endTime = time.Now()
+ c.monitor.delay.AddBySub(startTime, endTime)
+ c.monitor.state.Inc(bfe_basic.REQ_OTHER, 1)
+ c.monitor.state.Inc("waf_client_detect_other_"+c.serverAddress, 1)
+ setWafSpentTime(req, startTime, endTime)
+ setWafStatus(req, int(bfe_basic.WAF_ERROR))
+
+ // pass, go on
+ log.Logger.Warn("waf-server detect pass with error: %s, logid = %s, start = %d, end = %d",
+ res.Error.Error(), req.LogId, startTime.UnixNano(), endTime.UnixNano())
+
+ return false, ""
+ }
+
+ if res.Blocked() {
+ endTime = time.Now()
+ c.monitor.delay.AddBySub(startTime, endTime)
+ c.monitor.state.Inc(bfe_basic.REQ_FORBIDDEN, 1)
+ c.monitor.state.Inc("waf_client_detect_forbidden_"+c.serverAddress, 1)
+ setWafSpentTime(req, startTime, endTime)
+ setWafStatus(req, int(bfe_basic.WAF_FORBIDDEN))
+ setWafRuleName(req, "-")
+ req.ErrCode = ERR_WAF_FORBIDDEN
+
+ if openDebug {
+ log.Logger.Debug("waf-server detect block, logid = %s, start = %d, end = %d",
+ req.LogId, startTime.UnixNano(), endTime.UnixNano())
+ }
+ return true, res.getWafEventId()
+ }
+
+ // res.Result.Passed
+ endTime = time.Now()
+ c.monitor.delay.AddBySub(startTime, endTime)
+ c.monitor.state.Inc(bfe_basic.REQ_OK, 1)
+ c.monitor.state.Inc("waf_client_detect_ok_"+c.serverAddress, 1)
+ setWafSpentTime(req, startTime, endTime)
+ setWafStatus(req, int(bfe_basic.WAF_PASS))
+
+ // pass, go on
+ if openDebug {
+ log.Logger.Debug("waf-server detect pass, logid = %s, start = %d, end = %d",
+ req.LogId, startTime.UnixNano(), endTime.UnixNano())
+ }
+ return false, ""
+
+ case <-leftTimer.C: // use time.Ticker instead of time.After()
+ endTime = time.Now()
+ c.monitor.delay.AddBySub(startTime, endTime)
+ c.monitor.state.Inc(bfe_basic.REQ_TIMEOUT, 1)
+ c.monitor.state.Inc("waf_client_detect_timeout_"+c.serverAddress, 1)
+ setWafStatus(req, (int)(bfe_basic.WAF_TIMEOUT))
+ setWafSpentTime(req, startTime, endTime)
+
+ if openDebug {
+ log.Logger.Debug("time out for waiting waf-server, logid = %s, start = %d, end = %d",
+ req.LogId, startTime.UnixNano(), endTime.UnixNano())
+ }
+ }
+
+ return false, ""
+}
+
+func (c *WafClient) getToken(req *bfe_basic.Request, reqTimeout time.Duration, logId string) bool {
+ // concurrency control: concurrencyChan is used as a pool
+ ok := false
+
+ c.curWaitCount.Add(1)
+ defer c.curWaitCount.Add(-1)
+
+ ticker := time.NewTicker(reqTimeout)
+ defer ticker.Stop()
+
+ select {
+ case <-c.concurrencyChan:
+ ok = true
+ case <-ticker.C: // use time.Ticker instead of time.After()
+ ok = false
+ }
+
+ if ok && openDebug {
+ log.Logger.Debug("get concurrencyChan, logid = %s", logId)
+ }
+
+ return ok
+}
+
+func (c *WafClient) getHcServerStr() string {
+ addr := fmt.Sprintf("%s:%d", c.serverIP, c.hcPort.Load())
+ return addr
+}
+
+func (c *WafClient) detect(req *http.Request, done chan *WafDetectResult, retryMax int, logId string) {
+ var res bwi.WafResult
+ var err error
+ defer func() {
+ // release
+ c.concurrencyChan <- 1
+ if openDebug {
+ log.Logger.Debug("release concurrencyChan, logid = %s", logId)
+ }
+ if err := recover(); err != nil {
+ log.Logger.Warn("waf client detect panic, logid = %s. err:%v\n%s", logId, err, gotrack.CurrentStackTrace(0))
+ }
+ }()
+
+ //set circuit status in use side
+ maxRunCount := retryMax + 1
+ for i := 0; i < maxRunCount; i++ {
+ res, err = c.client.DetectRequest(req, logId)
+ if err == nil {
+ c.instanceHeathJudge(true, false)
+ break
+ }
+
+ if openDebug {
+ log.Logger.Debug("c.client.Detect(dc) failed: %s, retry = %d, logid = %s", err.Error(), i, logId)
+ }
+ c.instanceHeathJudge(false, false)
+ }
+
+ // send result
+ done <- &WafDetectResult{Result: res, Error: err}
+ if openDebug {
+ log.Logger.Debug("waf detect done, logid = %s", logId)
+ }
+}
+
+func (c *WafClient) IsAvailable() bool {
+ return c.available.Load()
+}
+
+func (c *WafClient) instanceHeathJudge(isOpSucc bool, isHcOp bool) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+
+ if isOpSucc {
+ c.errCounter.Store(0)
+ c.available.Store(true)
+ } else {
+ c.errCounter.Add(1)
+ // set only once
+
+ if c.errCounter.Load() > atomic.LoadInt64(&c.HCConf.UnavailableFailedThres) && c.available.Load() {
+ c.available.Store(false)
+ log.Logger.Info("Waf client: %s available set to false", c.serverAddress)
+ }
+ }
+
+ if c.available.Load() {
+ c.monitor.state.Set("waf_client_available_"+c.serverAddress, "true")
+ } else {
+ c.monitor.state.Set("waf_client_available_"+c.serverAddress, "false")
+ }
+}
+
+// check waf server health
+func doCheck(wafEntries *waf_impl.WafImplMethodBundle, addr string) bool {
+ defer func() {
+ if err := recover(); err != nil {
+ log.Logger.Warn("waf client:%s doCheck: panic serving :%v\n%s",
+ addr, err, gotrack.CurrentStackTrace(0))
+ }
+ }()
+
+ log.Logger.Info("doCheck(): start check: %s", addr)
+
+ // connect to waf server
+ conn, err := net.DialTimeout("tcp", addr, time.Second)
+ if err != nil {
+ log.Logger.Info("doCheck(): DialTimeout(): %s", err)
+ return false
+ }
+
+ // using DoHeartbeat() as headlth checking
+ err = wafEntries.HealthCheck(conn)
+ if err != nil {
+ log.Logger.Info("doCheck(): DoHeartbeat(): %s", err)
+
+ err := conn.Close()
+ if err != nil {
+ log.Logger.Warn("doCheck(): Heart beat conn.Close(): %s", err)
+ }
+ return false
+ }
+
+ // if conn close failed, still has some problems.
+ err = conn.Close()
+ if err != nil {
+ log.Logger.Warn("doCheck(): Heart beat conn.Close(): %s", err)
+ return false
+ }
+
+ return true
+}
+
+func (c *WafClient) checkWafServer() {
+ keySuccess := fmt.Sprintf("waf_client_check_success_%s", c.serverAddress)
+ keyFailed := fmt.Sprintf("waf_client_check_failed_%s", c.serverAddress)
+
+ for {
+ interval := time.Duration(atomic.LoadInt64(&c.HCConf.HealthCheckInterval)) * time.Millisecond
+ select {
+ // check waf server every second
+ case <-time.After(interval):
+ success := doCheck(c.wafEntries, c.getHcServerStr())
+ //success := true
+ c.instanceHeathJudge(success, true)
+
+ if success {
+ log.Logger.Debug("checkWafServer(): %s doCheck() success", c.serverAddress)
+ c.monitor.state.Inc(keySuccess, 1)
+ } else {
+ log.Logger.Info("checkWafServer(): %s doCheck() failed", c.serverAddress)
+ c.monitor.state.Inc(keyFailed, 1)
+ }
+ case <-c.exitCh:
+ log.Logger.Info("checkWafServer(): %s get exit signal", c.serverAddress)
+ return
+ }
+ }
+}
+
+// generate request for remote call
+func generateHeaders(headers bfe_http.Header) http.Header {
+ newHeaders := http.Header{}
+ for k, v := range headers {
+ newHeaders[k] = v
+ }
+
+ return newHeaders
+}
+
+// body data with http method: POST/PUT/PATCH will be checked
+func checkBodyWithHttpMethod(method string) bool {
+ switch method {
+ case http.MethodPost:
+ return true
+ case http.MethodPatch:
+ return true
+ case http.MethodPut:
+ return true
+ }
+
+ return false
+}
+
+// set waf spent time
+func setWafSpentTime(req *bfe_basic.Request, start time.Time, end time.Time) {
+ info := bfe_basic.GetWafInfo(req)
+ info.WafSpentTime = end.Sub(start).Nanoseconds() / 1000000
+}
+
+// set waf status
+func setWafStatus(req *bfe_basic.Request, status int) {
+ info := bfe_basic.GetWafInfo(req)
+ info.WafStatus = status
+}
+
+// set waf rule
+func setWafRuleName(req *bfe_basic.Request, ruleName string) {
+ info := bfe_basic.GetWafInfo(req)
+ info.WafRuleName = ruleName
+}
+
+func (c *WafClient) WafServerAddress() string {
+ return c.serverAddress
+}
+
+func (c *WafClient) UpdateInstanceConf(instConf *WafInstance) {
+ c.lock.Lock()
+ c.hcPort.Store(uint32(instConf.HealthCheckPort))
+ c.lock.Unlock()
+}
+
+func (c *WafClient) UpdateWafGlobalParam(wafGlobalParam *GlobalParam) {
+ c.lock.Lock()
+ c.globalWafParam = wafGlobalParam
+ c.lock.Unlock()
+
+ t := time.Duration(wafGlobalParam.WafClient.ConnectTimeout * int(time.Millisecond))
+ c.updateConnTimeout(t, int64(wafGlobalParam.WafClient.MaxWaitCount))
+
+ c.updateHCConf(&wafGlobalParam.HealthChecker)
+}
+
+func (c *WafClient) updateConnTimeout(timeout time.Duration, maxWaitCount int64) {
+ c.lock.Lock()
+ if c.connectTimeout != timeout {
+ c.connectTimeout = timeout
+
+ // reset socket factory
+ c.client.UpdateSockFactory(func() (net.Conn, error) {
+ conn, err := net.DialTimeout("tcp", c.serverAddress, c.connectTimeout)
+ if err != nil {
+ c.monitor.state.Inc(bfe_basic.NET_ERR, 1)
+ }
+ return conn, err
+ })
+ }
+ c.lock.Unlock()
+
+ c.maxWaitCount.Store(maxWaitCount)
+}
+
+func (c *WafClient) updateHCConf(hcconff *HealthCheckerConf) {
+ c.lock.Lock()
+ c.HCConf.HealthCheckInterval = hcconff.HealthCheckInterval
+ c.HCConf.UnavailableFailedThres = hcconff.UnavailableFailedThres
+ c.lock.Unlock()
+}
+
+func (c *WafClient) GetDetectParam(bodySize int64) (time.Duration, int) {
+ c.lock.Lock()
+ timeout := time.Duration(c.globalWafParam.GetReqTimeout(int(bodySize)) * int(time.Millisecond))
+ retryMax := c.globalWafParam.WafDetect.RetryMax
+ c.lock.Unlock()
+
+ return timeout, retryMax
+}
+
+func (c *WafClient) GetRefCount() int {
+ c.lock.RLock()
+ counter := c.refCount
+ c.lock.RUnlock()
+
+ return counter
+}
+
+func (c *WafClient) AddRefCount() {
+ c.lock.Lock()
+ c.refCount = c.refCount + 1
+ c.lock.Unlock()
+}
+
+func (c *WafClient) DecRefCount() {
+ c.lock.Lock()
+
+ c.refCount = c.refCount - 1
+ if c.refCount < 0 {
+ c.refCount = 0
+ log.Logger.Warn("WafClient ref counter error: refCount < 0")
+ }
+
+ c.lock.Unlock()
+}
+
+func (c *WafClient) SetDeleteTag() {
+ c.lock.Lock()
+ c.toDelete = true
+ c.available.Store(false)
+ c.monitor.state.Set("waf_client_available_"+c.serverAddress+"_delete_tag", "true")
+ c.lock.Unlock()
+}
+
+func (c *WafClient) WillBeDeleted() bool {
+ c.lock.RLock()
+ toDelete := c.toDelete
+ c.lock.RUnlock()
+
+ return toDelete
+}
+
+func (c *WafClient) Close() error {
+ del := c.WillBeDeleted()
+ rc := c.GetRefCount()
+
+ if del && rc == 0 {
+ c.client.Close()
+
+ // tell checkWafServer() to exit
+ c.exitCh <- struct{}{}
+
+ log.Logger.Info("Waf client: %s close", c.serverAddress)
+ c.monitor.state.Delete("waf_client_available_" + c.serverAddress)
+ c.monitor.state.Delete("waf_client_available_" + c.serverAddress + "_delete_tag")
+ c.monitor.state.Inc(DELETED_CLIENTS, 1)
+
+ return nil
+ }
+
+ return fmt.Errorf("WafClient.Close(): toDelete = %v, RefCounter = %d", del, rc)
+}
diff --git a/bfe_modules/mod_unified_waf/waf_client_pool.go b/bfe_modules/mod_unified_waf/waf_client_pool.go
new file mode 100644
index 000000000..89306389a
--- /dev/null
+++ b/bfe_modules/mod_unified_waf/waf_client_pool.go
@@ -0,0 +1,285 @@
+// Copyright (c) 2025 The BFE Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mod_unified_waf
+
+import (
+ "fmt"
+ "sync"
+ "time"
+
+ "github.com/baidu/go-lib/log"
+ "github.com/bfenetworks/bfe/bfe_modules/mod_unified_waf/waf_impl"
+)
+
+type WafClientPool struct {
+ confBasic ConfBasic
+ wafEntries *waf_impl.WafImplMethodBundle
+
+ wafClients map[string]*WafClient // current working waf clients
+ toDelClients []*WafClient // to be deleted waf clients
+
+ wafParam GlobalParam
+ monitor *MonitorStates // monitor states
+
+ lock sync.RWMutex // protect for wafClients and other members
+ updateLock sync.Mutex // protect for Update()
+ curIdx int
+}
+
+func NewWafClientPool(m *MonitorStates) *WafClientPool {
+ p := WafClientPool{}
+ p.wafClients = map[string]*WafClient{}
+ p.toDelClients = []*WafClient{}
+
+ p.monitor = m
+ go p.deleteLoop()
+
+ return &p
+}
+
+func (p *WafClientPool) SetConfBasic(confBasic ConfBasic) error {
+ var err error
+ p.confBasic = confBasic
+
+ p.wafEntries, err = waf_impl.WafFactory(confBasic.WafProductName)
+ if err != nil || p.wafEntries == nil {
+ err := fmt.Errorf("illegal WafProductName:%s", confBasic.WafProductName)
+ return err
+ }
+ return nil
+}
+
+func (p *WafClientPool) UpdateWafParam(data *GlobalParamConf) {
+ param := &data.Config
+
+ p.lock.Lock()
+ p.wafParam = *param
+
+ for _, c := range p.wafClients {
+ c.UpdateWafGlobalParam(param)
+ }
+ p.lock.Unlock()
+}
+
+func (p *WafClientPool) deleteLoop() {
+ t := time.NewTicker(time.Second * 1)
+ defer t.Stop()
+
+ for {
+ // wait for ticker
+ <-t.C
+
+ p.lock.Lock()
+ tryDeletes := p.toDelClients
+ p.toDelClients = nil
+ p.lock.Unlock()
+
+ // try close waf clients
+ toDelete := []*WafClient{}
+ for _, client := range tryDeletes {
+ if err := client.Close(); err != nil {
+ // close failed, still should not delete
+ toDelete = append(toDelete, client)
+ } else {
+ // client is closed
+ log.Logger.Info("Waf client: %s is deleted.", client.serverAddress)
+ }
+ }
+
+ // reset to delete clients
+ p.lock.Lock()
+ p.toDelClients = append(p.toDelClients, toDelete...)
+ activeClientCount := int64(len(p.wafClients))
+ toDeleteClientCount := int64(len(p.toDelClients))
+ p.lock.Unlock()
+
+ // for monitor
+ p.monitor.state.SetNum(TO_DELETE_CLIENTS, toDeleteClientCount)
+ p.monitor.state.SetNum(ACTIVE_CLIENTS, activeClientCount)
+ }
+}
+
+func (p *WafClientPool) createClients(wafInstances map[string]WafInstance) map[string]*WafClient {
+ clients := map[string]*WafClient{}
+
+ for addr, wafInstance := range wafInstances {
+ // new waf client has net.DialTimeout() call
+ client, err := NewWafClient(p.wafEntries, addr, &wafInstance, &p.wafParam, p.confBasic.ConnPoolSize, p.monitor)
+ if err != nil {
+ log.Logger.Error("NewWafClient(): %s", err.Error())
+ }
+ clients[addr] = client
+
+ log.Logger.Info("create waf client for %s", addr)
+
+ }
+
+ return clients
+}
+
+func (p *WafClientPool) addClients(clients map[string]*WafClient) {
+ addedClients := []*WafClient{}
+
+ p.lock.Lock()
+
+ for addr, client := range clients {
+ // check duplication; this should never happen.
+ if _, found := p.wafClients[addr]; found {
+ log.Logger.Warn("duplication waf client")
+
+ // move to delete pool
+ p.deleteClient(client)
+ continue
+ }
+
+ p.wafClients[addr] = client
+ addedClients = append(addedClients, client)
+ }
+
+ p.lock.Unlock()
+
+ // for logging and monitor
+ p.monitor.state.Inc(ADDED_CLIENTS, len(addedClients))
+ for _, client := range addedClients {
+ log.Logger.Info("Add waf client: %s", client.serverAddress)
+ }
+}
+
+func (p *WafClientPool) deleteClient(client *WafClient) {
+ client.SetDeleteTag()
+ p.toDelClients = append(p.toDelClients, client)
+
+ log.Logger.Info("Waf client: %s move to delete pool", client.serverAddress)
+}
+
+func (p *WafClientPool) deleteClients(toDel map[string]*WafClient) {
+ p.lock.Lock()
+
+ for addr, client := range toDel {
+ // remove from p.wafClients
+ delete(p.wafClients, addr)
+
+ // move to delete pool
+ p.deleteClient(client)
+ }
+
+ p.lock.Unlock()
+}
+
+// adjustInstances():
+// 1, add new waf instances
+// 2, remove to delete waf instances
+// 3, change weight of waf instance
+func (p *WafClientPool) adjustInstances(instanceMap map[string]WafInstance) (map[string]WafInstance, map[string]*WafClient) {
+ toAdd := map[string]WafInstance{}
+ toDel := map[string]*WafClient{}
+
+ p.lock.RLock()
+
+ // find new added instances
+ for addr, instance := range instanceMap {
+ if client, found := p.wafClients[addr]; !found {
+ // new added waf instance
+ toAdd[addr] = instance
+ } else {
+ // old waf instance, reset weight
+ client.UpdateInstanceConf(&instance)
+ }
+ }
+
+ // to delete instances
+ for addr, client := range p.wafClients {
+ if _, found := instanceMap[addr]; !found {
+ // add to toDel list
+ toDel[addr] = client
+ }
+ }
+
+ p.lock.RUnlock()
+
+ return toAdd, toDel
+}
+
+func (p *WafClientPool) Update(instances []WafInstance) {
+ // protect from concurrent update
+ p.updateLock.Lock()
+ defer p.updateLock.Unlock()
+
+ // check empty config
+ if len(instances) == 0 {
+ log.Logger.Warn("get empty waf instances, will remove all existed instances.")
+ }
+
+ // make instance map
+ // Note: if there are some duplication instances, only one instance will be used.
+ instanceMap := map[string]WafInstance{}
+ for _, instance := range instances {
+ addr := fmt.Sprintf("%s:%d", instance.IpAddr, instance.Port)
+ instanceMap[addr] = instance
+ }
+
+ // adjust instances:
+ // 1, find new added instances
+ // 2, find to delete instances
+ // 3, reset instance weight
+ toAdd, toDel := p.adjustInstances(instanceMap)
+
+ // add new waf clients
+ clients := p.createClients(toAdd)
+ p.addClients(clients)
+
+ // delete waf clients
+ p.deleteClients(toDel)
+}
+
+func (p *WafClientPool) Alloc() (*WafClient, error) {
+ var client *WafClient
+ var err error
+
+ p.lock.Lock()
+ client, err = p.rrBalance(p.wafClients)
+ p.lock.Unlock()
+
+ if err == nil {
+ client.AddRefCount()
+ }
+ return client, err
+}
+
+func (p *WafClientPool) Release(client *WafClient) {
+ client.DecRefCount()
+}
+
+func (p *WafClientPool) rrBalance(backs map[string]*WafClient) (*WafClient, error) {
+ var best *WafClient
+ var keys []string
+ for key, client := range backs {
+ // skip unavaliable backend
+ if !client.IsAvailable() {
+ continue
+ }
+ keys = append(keys, key)
+ }
+ if len(keys) <= 0 {
+ return nil, fmt.Errorf("no available waf instance")
+ }
+ if p.curIdx >= len(keys) {
+ p.curIdx = 0
+ }
+ best = backs[keys[p.curIdx]]
+ p.curIdx = p.curIdx + 1
+
+ return best, nil
+}
diff --git a/bfe_modules/mod_unified_waf/waf_data_load.go b/bfe_modules/mod_unified_waf/waf_data_load.go
new file mode 100644
index 000000000..fd7e51b28
--- /dev/null
+++ b/bfe_modules/mod_unified_waf/waf_data_load.go
@@ -0,0 +1,177 @@
+// Copyright (c) 2025 The BFE Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mod_unified_waf
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+
+ "github.com/baidu/go-lib/log"
+ "github.com/bfenetworks/bfe/bfe_util"
+)
+
+const (
+ DEFAULT_POOL_SIZE = 8 // default waf client connection pool size
+ DEFAULT_CONCURRENCY = 2000 // default waf client concurrency
+)
+
+type HealthCheckerConfFile struct {
+ UnavailableFailedThres int64 //unavailable failed threshold
+ HealthCheckInterval int64 //health check interval(ms)
+}
+
+// global param for mod_unified_waf
+type GlobalParamFile struct {
+ WafClient struct {
+ ConnectTimeout int // connect timeout for waf client
+ Concurrency int // how many concurrency call for one waf client
+ MaxWaitCount int //max wait rate for request waiting for token
+ }
+
+ WafDetect struct {
+ ReqTimeout int // total timeout for a request detecting
+ RetryMax int // max retry number in each request detecting
+ }
+
+ HealthChecker HealthCheckerConfFile
+}
+
+// global param in config file
+type GlobalParamConfFile struct {
+ Version *string // version string
+ Config *GlobalParamFile // global param for mod_unified_waf
+}
+
+// global param for mod_unified_waf
+type GlobalParam struct {
+ WafClient struct {
+ ConnectTimeout int // connect timeout for waf client
+ Concurrency int // how many concurrency call for one waf client
+ //ConnPoolSize int //connection pool size
+ MaxWaitCount int //max wait rate for request waiting for token
+ }
+
+ WafDetect struct {
+ RetryMax int // max retry number in each request detecting
+ ReqTimeout int // total timeout for a request detecting
+ }
+
+ HealthChecker HealthCheckerConf
+}
+
+func (p *GlobalParam) GetReqTimeout(bodySize int) int {
+ return p.WafDetect.ReqTimeout
+}
+
+type GlobalParamConf struct {
+ Version string
+ Config GlobalParam
+}
+
+func (cfg *GlobalParamConfFile) Check() error {
+ if err := bfe_util.CheckNilField(*cfg, false); err != nil {
+ return err
+ }
+
+ if err := cfg.Config.Check(); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (p *GlobalParamFile) Check() error {
+ if p.WafClient.ConnectTimeout <= 0 {
+ return fmt.Errorf("WafClient.ConnectTimeout > 0")
+ }
+
+ if p.WafClient.Concurrency <= 0 {
+ p.WafClient.Concurrency = DEFAULT_CONCURRENCY
+ log.Logger.Warn("Concurrency is : %d, use DEFAULT_CONCURRENCY(%d)", p.WafClient.Concurrency, DEFAULT_CONCURRENCY)
+ }
+
+ if p.HealthChecker.UnavailableFailedThres <= 0 {
+ return fmt.Errorf("WafClient.HealthChecker.UnavailableFailedThres <= 0")
+ }
+
+ if p.HealthChecker.HealthCheckInterval <= 0 {
+ return fmt.Errorf("WafClient.HealthChecker.HealthCheckInterval <= 0")
+ }
+
+ if p.WafClient.MaxWaitCount <= 0 {
+ return fmt.Errorf("WafClient.MaxWaitCount <= 0")
+ }
+
+ if p.WafDetect.RetryMax < 0 {
+ return fmt.Errorf("WafDetect.RetryMax < 0")
+ }
+
+ if p.WafDetect.ReqTimeout <= 0 {
+ return fmt.Errorf("WafDetect.ReqTimeout <= 0")
+ }
+
+ return nil
+}
+
+func (cfg *GlobalParamConfFile) cvtToConf() (*GlobalParamConf, error) {
+ var data GlobalParamConf
+ data.Version = *cfg.Version
+
+ //data.Config = *dataFile.Config
+ data.Config.WafClient.MaxWaitCount = cfg.Config.WafClient.MaxWaitCount
+ data.Config.WafClient.ConnectTimeout = cfg.Config.WafClient.ConnectTimeout
+ data.Config.WafClient.Concurrency = cfg.Config.WafClient.Concurrency
+
+ data.Config.WafDetect.RetryMax = cfg.Config.WafDetect.RetryMax
+ data.Config.WafDetect.ReqTimeout = cfg.Config.WafDetect.ReqTimeout
+
+ data.Config.HealthChecker.HealthCheckInterval = cfg.Config.HealthChecker.HealthCheckInterval
+ data.Config.HealthChecker.UnavailableFailedThres = cfg.Config.HealthChecker.UnavailableFailedThres
+
+ return &data, nil
+}
+
+// reload_trigger adaptor interface
+func WafDataParamLoadAndCheck(filename string) (*GlobalParamConf, error) {
+ var err error
+
+ // open the file
+ file, err := os.Open(filename)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+
+ // decode the file
+ decoder := json.NewDecoder(file)
+ var dataFile GlobalParamConfFile
+ err = decoder.Decode(&dataFile)
+ if err != nil {
+ return nil, err
+ }
+
+ // check config
+ if err := dataFile.Check(); err != nil {
+ return nil, err
+ }
+
+ // convert config
+ tdata, err := dataFile.cvtToConf()
+ if err != nil {
+ return nil, err
+ }
+ return tdata, nil
+}
diff --git a/bfe_modules/mod_unified_waf/waf_data_load_test.go b/bfe_modules/mod_unified_waf/waf_data_load_test.go
new file mode 100644
index 000000000..0928a046e
--- /dev/null
+++ b/bfe_modules/mod_unified_waf/waf_data_load_test.go
@@ -0,0 +1,70 @@
+// Copyright (c) 2025 The BFE Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mod_unified_waf
+
+import (
+ "testing"
+)
+
+func TestWafDataParamLoadAndCheck_1(t *testing.T) {
+ ModWafDataPath := "./testdata/mod_unified_waf.data"
+
+ conf, err := WafDataParamLoadAndCheck(ModWafDataPath)
+ if err != nil {
+ t.Errorf("WafDataParamLoadAndCheck(): %v", err)
+ return
+ }
+
+ if conf.Config.WafClient.ConnectTimeout != 30 {
+ t.Errorf("WafClient.ConnectTimeout != 30")
+ return
+ }
+ if conf.Config.WafDetect.RetryMax != 2 {
+ t.Errorf("WafDetect.RetryMax != 2")
+ return
+ }
+ if conf.Config.WafDetect.ReqTimeout != 50 {
+ t.Errorf("WafDetect.ReqTimeout != 50")
+ return
+ }
+}
+
+func TestWafDataParamLoadAndCheck_2(t *testing.T) {
+ ModWafDataPath := "./testdata/mod_unified_waf_2.data"
+
+ conf, err := WafDataParamLoadAndCheck(ModWafDataPath)
+ if err != nil {
+ t.Errorf("WafDataParamLoadAndCheck(): %v", err)
+ return
+ }
+
+ if conf.Config.WafClient.ConnectTimeout != 30 {
+ t.Errorf("WafClient.ConnectTimeout != 30")
+ return
+ }
+ if conf.Config.WafDetect.RetryMax != 2 {
+ t.Errorf("WafDetect.RetryMax != 2")
+ return
+ }
+ if conf.Config.WafDetect.ReqTimeout != 50 {
+ t.Errorf("WafDetect.ReqTimeout != 50")
+ return
+ }
+ reqt := conf.Config.GetReqTimeout(100)
+ if reqt != 50 {
+ t.Errorf("conf.Config.GetReqTimeout(100) != 50, actual:%d", reqt)
+ return
+ }
+}
diff --git a/bfe_modules/mod_unified_waf/waf_impl/waf_imp_entry.go b/bfe_modules/mod_unified_waf/waf_impl/waf_imp_entry.go
new file mode 100644
index 000000000..11755cd63
--- /dev/null
+++ b/bfe_modules/mod_unified_waf/waf_impl/waf_imp_entry.go
@@ -0,0 +1,52 @@
+// Copyright (c) 2025 The BFE Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package waf_impl
+
+import (
+ "fmt"
+ "net"
+
+ mockWafSDK "github.com/bfenetworks/bfe-mock-waf/waf-bfe-sdk"
+ bwi "github.com/bfenetworks/bwi/bwi"
+)
+
+type WafImplMethodBundle struct {
+ NewWafServerWithPoolSize func(socketFactory func() (net.Conn, error), poolSize int) bwi.WafServer
+ HealthCheck func(conn net.Conn) error
+}
+
+var wafImplDict = map[string]*WafImplMethodBundle{
+ //BFEMockWaf
+ "BFEMockWaf": &WafImplMethodBundle{
+ NewWafServerWithPoolSize: mockWafSDK.NewWafServerWithPoolSize,
+ HealthCheck: mockWafSDK.HealthCheck,
+ },
+ //AnHengWaf
+ //ChaiTinWaf
+}
+
+func CheckWafSupport(wafName string) bool {
+ _, ok := wafImplDict[wafName]
+ return ok
+}
+
+func WafFactory(wafName string) (*WafImplMethodBundle, error) {
+ bundle, ok := wafImplDict[wafName]
+ if !ok {
+ return nil, fmt.Errorf("don't support %s", wafName)
+ }
+
+ return bundle, nil
+}
diff --git a/bfe_modules/mod_unified_waf/waf_instances_load.go b/bfe_modules/mod_unified_waf/waf_instances_load.go
new file mode 100644
index 000000000..52bdfee54
--- /dev/null
+++ b/bfe_modules/mod_unified_waf/waf_instances_load.go
@@ -0,0 +1,100 @@
+// Copyright (c) 2025 The BFE Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mod_unified_waf
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+
+ "github.com/bfenetworks/bfe/bfe_util"
+)
+
+type WafInstance struct {
+ IpAddr string
+ Port int
+ HealthCheckPort int
+}
+
+// waf instance config for waf clusters
+type ClusterConfigs struct {
+ WafCluster []WafInstance `json:"WafCluster"`
+}
+
+// global param in config file
+type WafInstancesConfFile struct {
+ Version *string
+ Config *ClusterConfigs
+}
+
+type WafInstancesConf struct {
+ Version string `json:"version"`
+ WafCluster []WafInstance `json:"WafCluster"`
+}
+
+func (cfg *WafInstancesConfFile) Check() error {
+ if err := bfe_util.CheckNilField(*cfg, false); err != nil {
+ return err
+ }
+
+ if cfg.Config.WafCluster != nil {
+ for idx, instance := range cfg.Config.WafCluster {
+ if instance.Port <= 0 {
+ return fmt.Errorf("illegal waf instance Port, idx:%d", idx)
+ }
+ if cfg.Config.WafCluster[idx].HealthCheckPort <= 0 {
+ cfg.Config.WafCluster[idx].HealthCheckPort = instance.Port
+ }
+ }
+ if len(cfg.Config.WafCluster) <= 0 {
+ return fmt.Errorf("WafCluster is empty")
+ }
+ }
+
+ return nil
+}
+
+// reload_trigger adaptor interface
+func WafInstancesLoadAndCheck(filename string) (WafInstancesConf, error) {
+ var err error
+ var data WafInstancesConf
+
+ // open the file
+ file, err := os.Open(filename)
+ if err != nil {
+ return data, err
+ }
+ defer file.Close()
+
+ // decode the file
+ decoder := json.NewDecoder(file)
+ var dataFile WafInstancesConfFile
+ err = decoder.Decode(&dataFile)
+ if err != nil {
+ return data, err
+ }
+
+ // check config
+ if err := dataFile.Check(); err != nil {
+ return data, err
+ }
+
+ // convert config
+ data.Version = *dataFile.Version
+ if dataFile.Config.WafCluster != nil {
+ data.WafCluster = dataFile.Config.WafCluster
+ }
+ return data, nil
+}
diff --git a/bfe_modules/mod_unified_waf/waf_instances_load_test.go b/bfe_modules/mod_unified_waf/waf_instances_load_test.go
new file mode 100644
index 000000000..27b996d04
--- /dev/null
+++ b/bfe_modules/mod_unified_waf/waf_instances_load_test.go
@@ -0,0 +1,50 @@
+// Copyright (c) 2025 The BFE Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mod_unified_waf
+
+import (
+ "testing"
+)
+
+func TestWafInstancesLoadAndCheck_1(t *testing.T) {
+ wafInstancesPath := "./testdata/waf_instances.data"
+
+ winsts, err := WafInstancesLoadAndCheck(wafInstancesPath)
+ if err != nil {
+ t.Errorf("WafInstancesLoadAndCheck(): %v", err)
+ return
+ }
+
+ if winsts.WafCluster[0].HealthCheckPort != winsts.WafCluster[0].Port {
+ t.Errorf("winsts.WafCluster[0].HealthCheckPort != winsts.WafCluster[0].Port")
+ return
+ }
+
+ if winsts.WafCluster[1].HealthCheckPort != 5001 {
+ t.Errorf("winsts.WafCluster[1].HealthCheckPort != 5001")
+ return
+ }
+
+}
+
+func TestWafInstancesLoadAndCheck_2(t *testing.T) {
+ wafInstancesPath := "./testdata/waf_instances_empty.data"
+
+ _, err := WafInstancesLoadAndCheck(wafInstancesPath)
+ if err != nil {
+ t.Errorf("TestWafInstancesLoadAndCheck_2(): %v", err)
+ return
+ }
+}
diff --git a/bfe_server/reverseproxy.go b/bfe_server/reverseproxy.go
index 12685ed3a..ace59b86c 100644
--- a/bfe_server/reverseproxy.go
+++ b/bfe_server/reverseproxy.go
@@ -764,7 +764,7 @@ func (p *ReverseProxy) ServeHTTP(rw bfe_http.ResponseWriter, basicReq *bfe_basic
// close the connection after response
action = closeAfterReply
basicReq.BfeStatusCode = bfe_http.StatusInternalServerError
- return
+ goto send_response
case bfe_module.BfeHandlerRedirect:
// make redirect
Redirect(rw, req, basicReq.Redirect.Url, basicReq.Redirect.Code, basicReq.Redirect.Header)
diff --git a/conf/bfe.conf b/conf/bfe.conf
index 9a4d043b2..8a6fd8620 100644
--- a/conf/bfe.conf
+++ b/conf/bfe.conf
@@ -63,6 +63,8 @@ Modules = mod_prison
# Modules = mod_cors
Modules = mod_wasm
+Modules = mod_unified_waf
+
# interval for get diff of proxy-state
MonitorInterval = 20
diff --git a/conf/mod_unified_waf/mod_unified_waf.conf b/conf/mod_unified_waf/mod_unified_waf.conf
new file mode 100644
index 000000000..0301c39fb
--- /dev/null
+++ b/conf/mod_unified_waf/mod_unified_waf.conf
@@ -0,0 +1,12 @@
+[Basic]
+#candidates: None, BFEMockWaf
+WafProductName = None
+ConnPoolSize = 8
+
+[ConfigPath]
+ModWafDataPath = "../conf/mod_unified_waf/mod_unified_waf.data"
+ProductParamPath = "../conf/mod_unified_waf/product_param.data"
+WafInstancesPath = "../conf/mod_unified_waf/waf_instances.data"
+
+[Log]
+OpenDebug = false
diff --git a/conf/mod_unified_waf/mod_unified_waf.data b/conf/mod_unified_waf/mod_unified_waf.data
new file mode 100644
index 000000000..e99651110
--- /dev/null
+++ b/conf/mod_unified_waf/mod_unified_waf.data
@@ -0,0 +1,18 @@
+{
+ "Version": "2025-06-23 12:00:10",
+ "Config": {
+ "WafClient": {
+ "ConnectTimeout": 30,
+ "Concurrency": 2000,
+ "MaxWaitCount": 400
+ },
+ "WafDetect": {
+ "RetryMax": 2,
+ "ReqTimeout": 40
+ },
+ "HealthChecker": {
+ "UnavailableFailedThres": 20,
+ "HealthCheckInterval": 10000
+ }
+ }
+}
\ No newline at end of file
diff --git a/conf/mod_unified_waf/product_param.data b/conf/mod_unified_waf/product_param.data
new file mode 100644
index 000000000..12bd74e03
--- /dev/null
+++ b/conf/mod_unified_waf/product_param.data
@@ -0,0 +1,10 @@
+{
+ "Version": "2023-01-19 12:00:10",
+ "Config": {
+ "example_product": {
+ "SendBody": true,
+ "SendBodySize": 1024
+ }
+ }
+}
+
diff --git a/conf/mod_unified_waf/waf_instances.data b/conf/mod_unified_waf/waf_instances.data
new file mode 100644
index 000000000..f3e11f220
--- /dev/null
+++ b/conf/mod_unified_waf/waf_instances.data
@@ -0,0 +1,5 @@
+{
+ "Version": "2025-06-23 12:00:10",
+ "Config": {
+ }
+}
diff --git a/docs/mkdocs_zh.yml b/docs/mkdocs_zh.yml
index 7988e56f4..e7e01af8f 100644
--- a/docs/mkdocs_zh.yml
+++ b/docs/mkdocs_zh.yml
@@ -134,6 +134,7 @@ nav:
- 'mod_trust_clientip': 'modules/mod_trust_clientip/mod_trust_clientip.md'
- 'mod_userid': 'modules/mod_userid/mod_userid.md'
- 'mod_wasmplugin': 'modules/mod_wasmplugin/mod_wasmplugin.md'
+ - 'mod_unified_waf': 'modules/mod_unified_waf/mod_unified_waf.md'
- '运维管理':
- '命令行工具及参数': 'operation/command.md'
- '环境变量说明': 'operation/env_var.md'
diff --git a/docs/zh_cn/SUMMARY.md b/docs/zh_cn/SUMMARY.md
index 792126aeb..cb759ba49 100644
--- a/docs/zh_cn/SUMMARY.md
+++ b/docs/zh_cn/SUMMARY.md
@@ -65,6 +65,7 @@
* [mod_userid](modules/mod_userid/mod_userid.md)
* [mod_secure_link](modules/mod_secure_link/mod_secure_link.md)
* [mod_wasmplugin](modules/mod_wasmplugin/mod_wasmplugin.md)
+ * [mod_unified_waf](modules/mod_unified_waf/mod_unified_waf.md)
* 运维管理
* [命令行工具及参数](operation/command.md)
* [环境变量说明](operation/env_var.md)
diff --git a/docs/zh_cn/modules/mod_unified_waf/bfe_waf_demo.md b/docs/zh_cn/modules/mod_unified_waf/bfe_waf_demo.md
new file mode 100644
index 000000000..e8baf7401
--- /dev/null
+++ b/docs/zh_cn/modules/mod_unified_waf/bfe_waf_demo.md
@@ -0,0 +1,143 @@
+# BFE WAF Usage
+本文演示如何使用bfe waf.
+
+## 介绍
+BFE 通过BWI支持统一的第三方WAF 接入。
+关于BWI(BFE WAF Interface), 参考[BFE WAF Interface](https://github.com/bfenetworks/bwi)。
+关于BFE Mock WAF Server,参考[BFE Mock WAF Server](https://github.com/bfenetworks/bfe-mock-waf)。
+本文使用BFE Mock WAF Server演示BFE WAF模块的使用。
+
+## 前置准备
+本文会使用默认的bfe中的配置。
+包含:
+- host:example.org
+- product: example_product
+- cluster: cluster_example
+- subcluster: example.bfe.bj
+- RS: 127.0.0.1:8181
+
+### 启动BFE RS
+这里使用如下构造的简化http server
+#python3 simple_http_server.py 8181
+
+```
+# cat simple_http_server.py
+import http.server
+import socketserver
+import sys
+
+port = int(sys.argv[1])
+
+class MyHttpRequestHandler(http.server.SimpleHTTPRequestHandler):
+ def do_POST(self):
+ return self.do_GET()
+with socketserver.TCPServer(("", port), MyHttpRequestHandler) as httpd:
+ print("Http Server Serving at port", port)
+ httpd.serve_forever()
+```
+
+
+### 启动WAF Server
+这里我们使用BFE Mock WAF Server,参考[BFE Mock WAF Server](https://github.com/bfenetworks/bfe-mock-waf)。
+BFE默认集成了BFE Mock WAF Server。
+
+切换到BFE Mock WAF Server的工作路径
+#go run waf_server_demo.go
+WAF HTTP server listening port:8899
+
+## BFE配置修改
+
+切换到BFE的工作路径(bin目录)。
+
+### 打开mod_unified_waf 模块
+确认模块mod_unified_waf打开了
+```
+#cat ../conf/bfe.conf
+...
+Modules = mod_unified_waf
+...
+```
+
+### 修改使用的WAF产品
+
+#### 把WafProductName改为BFEMockWaf
+```
+#cat ../conf/mod_unified_waf/mod_unified_waf.conf
+
+[Basic]
+#candidates: None, BFEMockWaf
+WafProductName = BFEMockWaf
+```
+
+#### 确认mod_unified_waf参数
+
+```
+#cat ../conf/mod_unified_waf/mod_unified_waf.data
+{
+ "Version": "2025-06-23 12:00:10",
+ "Config": {
+ "WafClient": {
+ "ConnectTimeout": 30,
+ "Concurrency": 2000,
+ "MaxWaitCount": 400
+ },
+ "WafDetect": {
+ "RetryMax": 2,
+ "ReqTimeout": 40
+ },
+ "HealthChecker": {
+ "UnavailableFailedThres": 20,
+ "HealthCheckInterval": 10000
+ }
+ }
+}
+```
+
+
+#### 修改WAF RS 实例
+```
+#cat ../conf/mod_unified_waf/waf_instances.data
+{
+ "Version": "2023-01-19 12:00:10",
+ "Config": {
+ "WafCluster": [
+ {"IpAddr": "127.0.0.1", "Port": 8899, "HealthCheckPort": 8899}
+ ]
+
+ }
+}
+```
+
+#### 修改产品线检测参数
+```
+#cat ../conf/mod_unified_waf/product_param.data
+{
+ "Version": "2023-01-19 12:00:10",
+ "Config": {
+ "example_product": {
+ "SendBody": true,
+ "SendBodySize": 1024
+ }
+
+ }
+}
+```
+注:这里最多只检测http req body的前面1024个字节。
+
+
+## 启动BFE
+#./bfe -d -c ../conf -l ../log
+
+注:BFE 运行在 172.18.55.230 的机器上。下面会用到这个IP地址。
+
+## 客户端访问
+
+## curl访问
+使用http GET
+#curl -v -H "HOST:example.org" http://172.18.55.230:8080
+
+使用http POST
+#curl -v -X POST -H "HOST:example.org" http://172.18.55.230:8080 -d @waf-body1023.data
+注: waf-body1023.data是一个1023个字节的数据文件
+
+
diff --git a/docs/zh_cn/modules/mod_unified_waf/mod_unified_waf.md b/docs/zh_cn/modules/mod_unified_waf/mod_unified_waf.md
new file mode 100644
index 000000000..b4ba12a27
--- /dev/null
+++ b/docs/zh_cn/modules/mod_unified_waf/mod_unified_waf.md
@@ -0,0 +1,139 @@
+# mod_unified_waf
+
+## 模块简介
+
+BFE 支持在 http request 的处理流程中引入统一的第三方WAF支持。
+
+## 基础配置
+
+### 配置描述
+
+模块配置文件: conf/mod_unified_waf/mod_unified_waf.conf
+
+| 配置项 | 描述 |
+| ---------------------| ------------------------------------------- |
+| Basic.WafProductName | String
第三方WAF产品的名字,默认提供None、BFEMockWaf两个候选。默认值为None |
+| Basic.ConnPoolSize | String
与WAF server 的连接池大小 |
+| ConfigPath.ModWafDataPath | String
WAF访问的具体参数配置 |
+| ConfigPath.ProductParamPath | String
WAF访问的产品线配置 |
+| ConfigPath.WafInstancesPath | String
WAF RS实例池的配置 |
+| Log.OpenDebug | Boolean
是否开启 debug 日志
默认值False |
+
+### 配置示例
+
+```ini
+[Basic]
+#candidates: None, BFEMockWaf
+WafProductName = None
+ConnPoolSize = 8
+
+[ConfigPath]
+ModWafDataPath = "../conf/mod_unified_waf/mod_unified_waf.data"
+ProductParamPath = "../conf/mod_unified_waf/product_param.data"
+WafInstancesPath = "../conf/mod_unified_waf/waf_instances.data"
+
+[Log]
+OpenDebug = false
+```
+
+## WAF访问具体参数配置
+配置文件: conf/mod_unified_waf/mod_unified_waf.data
+### 配置描述
+
+| 配置项 | 描述 |
+| ------- | -------------------------------------------------------------- |
+| Version | String
配置文件版本 |
+| Config | Object
具体参数配置 |
+| Config.WafClient | Object
WAF Client参数配置 |
+| Config.WafClient.ConnectTimeout | int
连接 WAF RS的超时时间|
+| Config.WafClient.Concurrency | int
访问 WAF RS的并发度|
+| Config.WafClient.MaxWaitCount | int
访问 WAF RS的等待请求数|
+| Config.WafDetect | Object
WAF 检测参数配置 |
+| Config.WafDetect.RetryMax | int
访问 WAF RS的重试次数 |
+| Config.WafDetect.ReqTimeout | int
访问 WAF RS的超时时间|
+| Config.HealthChecker | Object
WAF RS 健康检查参数配置 |
+| Config.HealthChecker.UnavailableFailedThres | int
WAF RS健康检测时,RS不可访问的连续失败次数阈值 |
+| Config.HealthChecker.HealthCheckInterval | int
WAF RS健康检测的间隔(ms) |
+
+
+### 配置示例
+
+```json
+{
+ "Version": "2025-06-23 12:00:10",
+ "Config": {
+ "WafClient": {
+ "ConnectTimeout": 30,
+ "Concurrency": 2000,
+ "MaxWaitCount": 400
+ },
+ "WafDetect": {
+ "RetryMax": 2,
+ "ReqTimeout": 40
+ },
+ "HealthChecker": {
+ "UnavailableFailedThres": 20,
+ "HealthCheckInterval": 1000
+ }
+ }
+}
+```
+
+## WAF访问产品线配置
+配置文件: conf/mod_unified_waf/product_param.data
+
+### 配置描述
+
+| 配置项 | 描述 |
+| ------- | -------------------------------------------------------------- |
+| Version | String
配置文件版本 |
+| Config | Object
具体参数配置 |
+| Config{k} | Object
具体产品线的名字 |
+| Config{v} | Object
具体产品线的配置 |
+| Config{v}.SendBody | Object
WAF 检测时,是否发送body |
+| Config{v}.SendBodySize | Object
WAF 检测时,发送body的最大size(byte) |
+
+
+### 配置示例
+
+```json
+{
+ "Version": "2023-01-19 12:00:10",
+ "Config": {
+ "example_product": {
+ "SendBody": true,
+ "SendBodySize": 1024
+ }
+
+ }
+}
+```
+
+
+## WAF RS实例池配置
+配置文件: conf/mod_unified_waf/waf_instances.data
+
+### 配置描述
+
+| 配置项 | 描述 |
+| ------- | -------------------------------------------------------------- |
+| Version | String
配置文件版本 |
+| Config | Object
具体配置信息,目前只有一个WafCluster|
+| Config.WafCluster | Object
WafCluster RS 具体配置 |
+| Config.WafCluster[].IpAddr | String
WAF RS IP |
+| Config.WafCluster[].Port | String
WAF RS 攻击检测端口 |
+| Config.WafCluster[].HealthCheckPort | String
WAF RS 健康检测端口 |
+
+### 配置示例
+
+```json
+{
+ "Version": "2023-01-19 12:00:10",
+ "Config": {
+ "WafCluster": [
+ {"IpAddr": "127.0.0.1", "Port": 8899, "HealthCheckPort": 8899}
+ ]
+
+ }
+}
+```
\ No newline at end of file
diff --git a/docs/zh_cn/modules/modules.md b/docs/zh_cn/modules/modules.md
index c55b02c7e..cbf089dc6 100644
--- a/docs/zh_cn/modules/modules.md
+++ b/docs/zh_cn/modules/modules.md
@@ -20,3 +20,4 @@
- [mod_trace](mod_trace/mod_trace.md)
- [mod_trust_clientip](mod_trust_clientip/mod_trust_clientip.md)
- [mod_userid](mod_userid/mod_userid.md)
+- [mod_unified_waf](mod_unified_waf/mod_unified_waf.md)
diff --git a/go.mod b/go.mod
index b429b1969..8c9e57825 100644
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,8 @@
module github.com/bfenetworks/bfe
-go 1.21
+go 1.22
-toolchain go1.22.2
+toolchain go1.22.9
require (
github.com/abbot/go-http-auth v0.4.1-0.20181019201920-860ed7f246ff
@@ -47,6 +47,8 @@ require (
require (
github.com/HdrHistogram/hdrhistogram-go v1.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
+ github.com/bfenetworks/bfe-mock-waf v0.1.0
+ github.com/bfenetworks/bwi v0.1.2
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/elastic/go-sysinfo v1.1.1 // indirect
github.com/elastic/go-windows v1.0.0 // indirect
@@ -67,6 +69,7 @@ require (
google.golang.org/grpc v1.56.3 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect
+
)
// replace github.com/bfenetworks/proxy-wasm-go-host => ../proxy-wasm-go-host
diff --git a/go.sum b/go.sum
index 4032ecd31..9dda25134 100644
--- a/go.sum
+++ b/go.sum
@@ -16,6 +16,10 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/baidu/go-lib v0.0.0-20200819072111-21df249f5e6a h1:m/u39GNhkoUSC9WxTuM5hWShEqEfVioeXDiqiQd6tKg=
github.com/baidu/go-lib v0.0.0-20200819072111-21df249f5e6a/go.mod h1:FneHDqz3wLeDGdWfRyW4CzBbCwaqesLGIFb09N80/ww=
+github.com/bfenetworks/bfe-mock-waf v0.1.0 h1:dTd540S3nv6qNlG6lLC0F8qx3gyo5WB4wnci3+d4J78=
+github.com/bfenetworks/bfe-mock-waf v0.1.0/go.mod h1:MWZHbihiRQXpoUCvY1l18s2bfOBWx4N4pghxBt+xUv0=
+github.com/bfenetworks/bwi v0.1.2 h1:3AcCzUjyzKm+FeLgTIVg58u+SUdEVZdJbQM58Ezwjcg=
+github.com/bfenetworks/bwi v0.1.2/go.mod h1:zCRIdSw521zVnNCM73qw/lZ9UknbRux9rk6UQvBJgMA=
github.com/bfenetworks/proxy-wasm-go-host v0.0.0-20241202144118-62704e5df808 h1:v0ckUMaZJFe8XvoM9x3kn+lDtMfI9EvpFadiOiV/s8A=
github.com/bfenetworks/proxy-wasm-go-host v0.0.0-20241202144118-62704e5df808/go.mod h1:VG3ZZ8Zg7dYkla2hHy9UsX0GLl/dgJYP4IxuPvoq+/U=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=