From e75b025b21f1ec3c7b297f374b427265e66aa9d8 Mon Sep 17 00:00:00 2001 From: yeyunxi Date: Mon, 30 Jun 2025 09:33:02 +0800 Subject: [PATCH 1/3] add mod_unified_waf Signed-off-by: yeyunxi --- bfe_basic/common.go | 20 + bfe_basic/waf_info.go | 61 ++ bfe_modules/bfe_modules.go | 4 + .../mod_unified_waf/alb_waf_instances_load.go | 100 +++ .../alb_waf_instances_load_test.go | 52 ++ bfe_modules/mod_unified_waf/conf_load.go | 105 ++++ bfe_modules/mod_unified_waf/conf_load_test.go | 48 ++ .../mod_unified_waf/mod_unified_waf.go | 483 +++++++++++++++ .../mod_unified_waf/product_param_load.go | 107 ++++ .../product_param_load_test.go | 84 +++ .../mod_unified_waf/product_param_table.go | 62 ++ bfe_modules/mod_unified_waf/states.go | 60 ++ .../testdata/alb_waf_instances.data | 9 + .../testdata/alb_waf_instances_empty.data | 5 + .../testdata/mod_unified_waf.conf | 14 + .../testdata/mod_unified_waf.data | 18 + .../testdata/mod_unified_waf_2.data | 27 + .../testdata/product_param.data | 13 + .../testdata/product_param_1.data | 13 + .../testdata/product_param_2.data | 11 + .../testdata/product_param_empty.data | 5 + bfe_modules/mod_unified_waf/waf_client.go | 584 ++++++++++++++++++ .../mod_unified_waf/waf_client_pool.go | 287 +++++++++ bfe_modules/mod_unified_waf/waf_data_load.go | 185 ++++++ .../mod_unified_waf/waf_data_load_test.go | 70 +++ .../mod_unified_waf/waf_impl/waf_imp_entry.go | 52 ++ bfe_server/reverseproxy.go | 2 +- conf/bfe.conf | 2 + conf/mod_unified_waf/alb_waf_instances.data | 7 + conf/mod_unified_waf/mod_unified_waf.conf | 12 + conf/mod_unified_waf/mod_unified_waf.data | 18 + conf/mod_unified_waf/product_param.data | 10 + docs/mkdocs_zh.yml | 1 + docs/zh_cn/SUMMARY.md | 1 + .../modules/mod_unified_waf/bfe_waf_demo.md | 143 +++++ .../mod_unified_waf/mod_unified_waf.md | 139 +++++ docs/zh_cn/modules/modules.md | 1 + go.mod | 9 +- go.sum | 4 + 39 files changed, 2825 insertions(+), 3 deletions(-) create mode 100644 bfe_basic/waf_info.go create mode 100644 bfe_modules/mod_unified_waf/alb_waf_instances_load.go create mode 100644 bfe_modules/mod_unified_waf/alb_waf_instances_load_test.go create mode 100644 bfe_modules/mod_unified_waf/conf_load.go create mode 100644 bfe_modules/mod_unified_waf/conf_load_test.go create mode 100644 bfe_modules/mod_unified_waf/mod_unified_waf.go create mode 100644 bfe_modules/mod_unified_waf/product_param_load.go create mode 100644 bfe_modules/mod_unified_waf/product_param_load_test.go create mode 100644 bfe_modules/mod_unified_waf/product_param_table.go create mode 100644 bfe_modules/mod_unified_waf/states.go create mode 100644 bfe_modules/mod_unified_waf/testdata/alb_waf_instances.data create mode 100644 bfe_modules/mod_unified_waf/testdata/alb_waf_instances_empty.data create mode 100644 bfe_modules/mod_unified_waf/testdata/mod_unified_waf.conf create mode 100644 bfe_modules/mod_unified_waf/testdata/mod_unified_waf.data create mode 100644 bfe_modules/mod_unified_waf/testdata/mod_unified_waf_2.data create mode 100644 bfe_modules/mod_unified_waf/testdata/product_param.data create mode 100644 bfe_modules/mod_unified_waf/testdata/product_param_1.data create mode 100644 bfe_modules/mod_unified_waf/testdata/product_param_2.data create mode 100644 bfe_modules/mod_unified_waf/testdata/product_param_empty.data create mode 100644 bfe_modules/mod_unified_waf/waf_client.go create mode 100644 bfe_modules/mod_unified_waf/waf_client_pool.go create mode 100644 bfe_modules/mod_unified_waf/waf_data_load.go create mode 100644 bfe_modules/mod_unified_waf/waf_data_load_test.go create mode 100644 bfe_modules/mod_unified_waf/waf_impl/waf_imp_entry.go create mode 100644 conf/mod_unified_waf/alb_waf_instances.data create mode 100644 conf/mod_unified_waf/mod_unified_waf.conf create mode 100644 conf/mod_unified_waf/mod_unified_waf.data create mode 100644 conf/mod_unified_waf/product_param.data create mode 100644 docs/zh_cn/modules/mod_unified_waf/bfe_waf_demo.md create mode 100644 docs/zh_cn/modules/mod_unified_waf/mod_unified_waf.md diff --git a/bfe_basic/common.go b/bfe_basic/common.go index 6a33a079d..342407f4b 100644 --- a/bfe_basic/common.go +++ b/bfe_basic/common.go @@ -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..88cd6ff1b --- /dev/null +++ b/bfe_basic/waf_info.go @@ -0,0 +1,61 @@ +// Copyright (c) 2019 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..4fd4e5142 100644 --- a/bfe_modules/bfe_modules.go +++ b/bfe_modules/bfe_modules.go @@ -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/alb_waf_instances_load.go b/bfe_modules/mod_unified_waf/alb_waf_instances_load.go new file mode 100644 index 000000000..4475bb106 --- /dev/null +++ b/bfe_modules/mod_unified_waf/alb_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 +} + +// alb waf instance config for alb clusters +type ClusterConfigs struct { + WafCluster []WafInstance `json:"WafCluster"` +} + +// global param in config file +type AlbWafInstancesConfFile struct { + Version *string + Config *ClusterConfigs +} + +type AlbWafInstancesConf struct { + Version string `json:"version"` + WafCluster []WafInstance `json:"WafCluster"` +} + +func (cfg *AlbWafInstancesConfFile) 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 AlbWafInstancesLoadAndCheck(filename string) (AlbWafInstancesConf, error) { + var err error + var data AlbWafInstancesConf + + // open the file + file, err := os.Open(filename) + defer file.Close() + if err != nil { + return data, err + } + + // decode the file + decoder := json.NewDecoder(file) + var dataFile AlbWafInstancesConfFile + 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/alb_waf_instances_load_test.go b/bfe_modules/mod_unified_waf/alb_waf_instances_load_test.go new file mode 100644 index 000000000..b9684a32c --- /dev/null +++ b/bfe_modules/mod_unified_waf/alb_waf_instances_load_test.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 mod_unified_waf + +import ( + "fmt" + "testing" +) + +func TestAlbWafInstancesLoadAndCheck_1(t *testing.T) { + albWafInstancesPath := "./testdata/alb_waf_instances.data" + + winsts, err := AlbWafInstancesLoadAndCheck(albWafInstancesPath) + if err != nil { + t.Errorf("AlbWafInstancesLoadAndCheck(): %v", err) + return + } + + if winsts.WafCluster[0].HealthCheckPort != winsts.WafCluster[0].Port { + fmt.Println("=== TestAlbWafInstancesLoadAndCheck_1", 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 TestAlbWafInstancesLoadAndCheck_2(t *testing.T) { + albWafInstancesPath := "./testdata/alb_waf_instances_empty.data" + + _, err := AlbWafInstancesLoadAndCheck(albWafInstancesPath) + if err != nil { + t.Errorf("AlbWafInstancesLoadAndCheck(): %v", err) + return + } +} 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..2debec831 --- /dev/null +++ b/bfe_modules/mod_unified_waf/conf_load.go @@ -0,0 +1,105 @@ +// 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 + // Concurrency int + 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 + AlbWafInstancesPath string // configure path for alb_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 cfg.Basic.Concurrency <= 0 { + // log.Logger.Warn("Basic.Concurrency is : %d, use DEFAULT_CONCURRENCY(%d)", cfg.Basic.Concurrency, DEFAULT_CONCURRENCY) + // cfg.Basic.Concurrency = DEFAULT_CONCURRENCY + // } + 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 AlbWafInstancesPath + if cfg.ConfigPath.AlbWafInstancesPath == "" { + log.Logger.Error("ConfigPath.AlbWafInstancesPath not set") + return errors.New("ConfigPath.AlbWafInstancesPath 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..ea36f12ac --- /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" + albWafInstancesPath := "./testdata/alb_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.AlbWafInstancesPath != albWafInstancesPath { + t.Errorf("AlbWafInstancesPath should be %s not %s", albWafInstancesPath, conf.ConfigPath.AlbWafInstancesPath) + } +} 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..c85736944 --- /dev/null +++ b/bfe_modules/mod_unified_waf/mod_unified_waf.go @@ -0,0 +1,483 @@ +// 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 ( + ModChaitinWaf = "mod_unified_waf" + + NOAH_SD_MOD_WAF = "waf_client" + NOAH_SD_MOD_WAF_DIFF = "waf_client_diff" + NOAH_MOD_WAF_DELAY = "waf_client_delay" + NOAH_MOD_WAF_PEEK_DELAY = "waf_client_delay_peek_body" + NOAH_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 + albWafInstancesPath string // path for alb_waf_instances.data + + monitor *MonitorStates // monitor states + + isNoneWaf bool +} + +func NewModuleWaf() *ModuleWaf { + m := new(ModuleWaf) + m.name = ModChaitinWaf + + 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 +} + +// for alb_waf_instances.data +func (m *ModuleWaf) WafInstancesLoad(path string) error { + data, err := AlbWafInstancesLoadAndCheck(path) + if err != nil { + return err + } + + var wafInstances []WafInstance + wafInstances = data.WafCluster + + if !m.isNoneWaf { + m.wafClientPool.Update(wafInstances, data.Version) + } + + 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.albWafInstancesPath + } + 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.albWafInstancesPath = conf.ConfigPath.AlbWafInstancesPath + + 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..03ec612fd --- /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) + defer file.Close() + if err != nil { + return data, err + } + + // 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..42f3b1c72 --- /dev/null +++ b/bfe_modules/mod_unified_waf/states.go @@ -0,0 +1,60 @@ +// 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" +) + +// key for counter of mod_crypto +type ModuleChaitinWafState 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 moudle state + + underlyingState ModuleChaitinWafState + 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(NOAH_MOD_WAF_DELAY) + m.delayPeekBody.SetKeyPrefix(NOAH_MOD_WAF_PEEK_DELAY) + m.delayCallComp.SetKeyPrefix(NOAH_MOD_WAF_COMP_DELAY) + + m.state.SetKeyPrefix(NOAH_SD_MOD_WAF) + m.stateDiff.SetKeyPrefix(NOAH_SD_MOD_WAF_DIFF) + + m.metrics.Init(&m.underlyingState, ModChaitinWaf, 0) + + return &m +} diff --git a/bfe_modules/mod_unified_waf/testdata/alb_waf_instances.data b/bfe_modules/mod_unified_waf/testdata/alb_waf_instances.data new file mode 100644 index 000000000..3d9ba037f --- /dev/null +++ b/bfe_modules/mod_unified_waf/testdata/alb_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/alb_waf_instances_empty.data b/bfe_modules/mod_unified_waf/testdata/alb_waf_instances_empty.data new file mode 100644 index 000000000..1a4c334b9 --- /dev/null +++ b/bfe_modules/mod_unified_waf/testdata/alb_waf_instances_empty.data @@ -0,0 +1,5 @@ +{ + "Version": "2023-01-19 12:00:10", + "Config": { + } +} 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..d6ea6e3ec --- /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" +AlbWafInstancesPath = "./testdata/alb_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/waf_client.go b/bfe_modules/mod_unified_waf/waf_client.go new file mode 100644 index 000000000..a4154e643 --- /dev/null +++ b/bfe_modules/mod_unified_waf/waf_client.go @@ -0,0 +1,584 @@ +// 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" +) + +const ( + AVAILABLE_THRESHOLD = 20 // default error counter for waf client avaialable +) + +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..2997658ef --- /dev/null +++ b/bfe_modules/mod_unified_waf/waf_client_pool.go @@ -0,0 +1,287 @@ +// 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 + //wafParamVersion string + //wafInstanceVersion string + 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 + //p.wafParamVersion = ver + + 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() + + // try close waf clients + toDelete := []*WafClient{} + for _, client := range p.toDelClients { + 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.toDelClients = toDelete + + // for monitor + p.monitor.state.SetNum(TO_DELETE_CLIENTS, int64(len(toDelete))) + p.monitor.state.SetNum(ACTIVE_CLIENTS, int64(len(p.wafClients))) + + p.lock.Unlock() + } +} + +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() + + // TODO: 这种模式就无法动态修改除weight外的参数 + // 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, version string) { + // 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) + + // update waf instance version + //p.wafInstanceVersion = version +} + +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..6c0f1386c --- /dev/null +++ b/bfe_modules/mod_unified_waf/waf_data_load.go @@ -0,0 +1,185 @@ +// 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 + + DEFAULT_MAX_WAIT_RATE = 0.2 // default client max waiting rate + DEFAULT_HALF_OPEN_THRES_RATE = 0.2 // default client half open thres rate + DEFAULT_CLOSED_THRES_RATE = 0.8 // default client closed thres rate + DEFAULT_SAMPLE_PERCENT = 5.0 // default client sample rate under half-open + DEFAULT_STAT_TOTAL_OP_COUNT = 100 // default client sample rate under half-open +) + +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 + //ConnPoolSize int //connection pool size + 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 + // var data GlobalParamConf + + // open the file + file, err := os.Open(filename) + defer file.Close() + if err != nil { + return nil, err + } + + // 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_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/alb_waf_instances.data b/conf/mod_unified_waf/alb_waf_instances.data new file mode 100644 index 000000000..2265290ca --- /dev/null +++ b/conf/mod_unified_waf/alb_waf_instances.data @@ -0,0 +1,7 @@ +{ + "Version": "2025-06-23 12:00:10", + "Config": { + "WafCluster": [ + ] + } +} 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..c96683750 --- /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" +AlbWafInstancesPath = "../conf/mod_unified_waf/alb_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/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..bd7627cad --- /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/alb_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..d113d48de --- /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.AlbWafInstancesPath | 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" +AlbWafInstancesPath = "../conf/mod_unified_waf/alb_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/alb_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..0be004e2a 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,9 @@ 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 +// replace github.com/bfenetworks/bwi => /root/yingfei/opensouce/bwi +// replace github.com/bfenetworks/bfe-mock-waf => /root/yingfei/opensouce/bfe-mock-waf 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= From 4483d4692feb9872dae0820ce866a4e4179c3f0a Mon Sep 17 00:00:00 2001 From: yeyunxi Date: Mon, 30 Jun 2025 09:33:02 +0800 Subject: [PATCH 2/3] add mod_unified_waf Signed-off-by: yeyunxi --- bfe_basic/common.go | 22 +- bfe_basic/waf_info.go | 61 ++ bfe_modules/bfe_modules.go | 6 +- .../mod_unified_waf/alb_waf_instances_load.go | 100 +++ .../alb_waf_instances_load_test.go | 52 ++ bfe_modules/mod_unified_waf/conf_load.go | 105 ++++ bfe_modules/mod_unified_waf/conf_load_test.go | 48 ++ .../mod_unified_waf/mod_unified_waf.go | 483 +++++++++++++++ .../mod_unified_waf/product_param_load.go | 107 ++++ .../product_param_load_test.go | 84 +++ .../mod_unified_waf/product_param_table.go | 62 ++ bfe_modules/mod_unified_waf/states.go | 60 ++ .../testdata/alb_waf_instances.data | 9 + .../testdata/alb_waf_instances_empty.data | 5 + .../testdata/mod_unified_waf.conf | 14 + .../testdata/mod_unified_waf.data | 18 + .../testdata/mod_unified_waf_2.data | 27 + .../testdata/product_param.data | 13 + .../testdata/product_param_1.data | 13 + .../testdata/product_param_2.data | 11 + .../testdata/product_param_empty.data | 5 + bfe_modules/mod_unified_waf/waf_client.go | 584 ++++++++++++++++++ .../mod_unified_waf/waf_client_pool.go | 287 +++++++++ bfe_modules/mod_unified_waf/waf_data_load.go | 185 ++++++ .../mod_unified_waf/waf_data_load_test.go | 70 +++ .../mod_unified_waf/waf_impl/waf_imp_entry.go | 52 ++ bfe_server/reverseproxy.go | 2 +- conf/bfe.conf | 2 + conf/mod_unified_waf/alb_waf_instances.data | 5 + conf/mod_unified_waf/mod_unified_waf.conf | 12 + conf/mod_unified_waf/mod_unified_waf.data | 18 + conf/mod_unified_waf/product_param.data | 10 + docs/mkdocs_zh.yml | 1 + docs/zh_cn/SUMMARY.md | 1 + .../modules/mod_unified_waf/bfe_waf_demo.md | 143 +++++ .../mod_unified_waf/mod_unified_waf.md | 139 +++++ docs/zh_cn/modules/modules.md | 1 + go.mod | 9 +- go.sum | 4 + 39 files changed, 2825 insertions(+), 5 deletions(-) create mode 100644 bfe_basic/waf_info.go create mode 100644 bfe_modules/mod_unified_waf/alb_waf_instances_load.go create mode 100644 bfe_modules/mod_unified_waf/alb_waf_instances_load_test.go create mode 100644 bfe_modules/mod_unified_waf/conf_load.go create mode 100644 bfe_modules/mod_unified_waf/conf_load_test.go create mode 100644 bfe_modules/mod_unified_waf/mod_unified_waf.go create mode 100644 bfe_modules/mod_unified_waf/product_param_load.go create mode 100644 bfe_modules/mod_unified_waf/product_param_load_test.go create mode 100644 bfe_modules/mod_unified_waf/product_param_table.go create mode 100644 bfe_modules/mod_unified_waf/states.go create mode 100644 bfe_modules/mod_unified_waf/testdata/alb_waf_instances.data create mode 100644 bfe_modules/mod_unified_waf/testdata/alb_waf_instances_empty.data create mode 100644 bfe_modules/mod_unified_waf/testdata/mod_unified_waf.conf create mode 100644 bfe_modules/mod_unified_waf/testdata/mod_unified_waf.data create mode 100644 bfe_modules/mod_unified_waf/testdata/mod_unified_waf_2.data create mode 100644 bfe_modules/mod_unified_waf/testdata/product_param.data create mode 100644 bfe_modules/mod_unified_waf/testdata/product_param_1.data create mode 100644 bfe_modules/mod_unified_waf/testdata/product_param_2.data create mode 100644 bfe_modules/mod_unified_waf/testdata/product_param_empty.data create mode 100644 bfe_modules/mod_unified_waf/waf_client.go create mode 100644 bfe_modules/mod_unified_waf/waf_client_pool.go create mode 100644 bfe_modules/mod_unified_waf/waf_data_load.go create mode 100644 bfe_modules/mod_unified_waf/waf_data_load_test.go create mode 100644 bfe_modules/mod_unified_waf/waf_impl/waf_imp_entry.go create mode 100644 conf/mod_unified_waf/alb_waf_instances.data create mode 100644 conf/mod_unified_waf/mod_unified_waf.conf create mode 100644 conf/mod_unified_waf/mod_unified_waf.data create mode 100644 conf/mod_unified_waf/product_param.data create mode 100644 docs/zh_cn/modules/mod_unified_waf/bfe_waf_demo.md create mode 100644 docs/zh_cn/modules/mod_unified_waf/mod_unified_waf.md 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/alb_waf_instances_load.go b/bfe_modules/mod_unified_waf/alb_waf_instances_load.go new file mode 100644 index 000000000..4475bb106 --- /dev/null +++ b/bfe_modules/mod_unified_waf/alb_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 +} + +// alb waf instance config for alb clusters +type ClusterConfigs struct { + WafCluster []WafInstance `json:"WafCluster"` +} + +// global param in config file +type AlbWafInstancesConfFile struct { + Version *string + Config *ClusterConfigs +} + +type AlbWafInstancesConf struct { + Version string `json:"version"` + WafCluster []WafInstance `json:"WafCluster"` +} + +func (cfg *AlbWafInstancesConfFile) 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 AlbWafInstancesLoadAndCheck(filename string) (AlbWafInstancesConf, error) { + var err error + var data AlbWafInstancesConf + + // open the file + file, err := os.Open(filename) + defer file.Close() + if err != nil { + return data, err + } + + // decode the file + decoder := json.NewDecoder(file) + var dataFile AlbWafInstancesConfFile + 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/alb_waf_instances_load_test.go b/bfe_modules/mod_unified_waf/alb_waf_instances_load_test.go new file mode 100644 index 000000000..b9684a32c --- /dev/null +++ b/bfe_modules/mod_unified_waf/alb_waf_instances_load_test.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 mod_unified_waf + +import ( + "fmt" + "testing" +) + +func TestAlbWafInstancesLoadAndCheck_1(t *testing.T) { + albWafInstancesPath := "./testdata/alb_waf_instances.data" + + winsts, err := AlbWafInstancesLoadAndCheck(albWafInstancesPath) + if err != nil { + t.Errorf("AlbWafInstancesLoadAndCheck(): %v", err) + return + } + + if winsts.WafCluster[0].HealthCheckPort != winsts.WafCluster[0].Port { + fmt.Println("=== TestAlbWafInstancesLoadAndCheck_1", 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 TestAlbWafInstancesLoadAndCheck_2(t *testing.T) { + albWafInstancesPath := "./testdata/alb_waf_instances_empty.data" + + _, err := AlbWafInstancesLoadAndCheck(albWafInstancesPath) + if err != nil { + t.Errorf("AlbWafInstancesLoadAndCheck(): %v", err) + return + } +} 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..2debec831 --- /dev/null +++ b/bfe_modules/mod_unified_waf/conf_load.go @@ -0,0 +1,105 @@ +// 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 + // Concurrency int + 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 + AlbWafInstancesPath string // configure path for alb_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 cfg.Basic.Concurrency <= 0 { + // log.Logger.Warn("Basic.Concurrency is : %d, use DEFAULT_CONCURRENCY(%d)", cfg.Basic.Concurrency, DEFAULT_CONCURRENCY) + // cfg.Basic.Concurrency = DEFAULT_CONCURRENCY + // } + 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 AlbWafInstancesPath + if cfg.ConfigPath.AlbWafInstancesPath == "" { + log.Logger.Error("ConfigPath.AlbWafInstancesPath not set") + return errors.New("ConfigPath.AlbWafInstancesPath 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..ea36f12ac --- /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" + albWafInstancesPath := "./testdata/alb_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.AlbWafInstancesPath != albWafInstancesPath { + t.Errorf("AlbWafInstancesPath should be %s not %s", albWafInstancesPath, conf.ConfigPath.AlbWafInstancesPath) + } +} 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..c85736944 --- /dev/null +++ b/bfe_modules/mod_unified_waf/mod_unified_waf.go @@ -0,0 +1,483 @@ +// 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 ( + ModChaitinWaf = "mod_unified_waf" + + NOAH_SD_MOD_WAF = "waf_client" + NOAH_SD_MOD_WAF_DIFF = "waf_client_diff" + NOAH_MOD_WAF_DELAY = "waf_client_delay" + NOAH_MOD_WAF_PEEK_DELAY = "waf_client_delay_peek_body" + NOAH_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 + albWafInstancesPath string // path for alb_waf_instances.data + + monitor *MonitorStates // monitor states + + isNoneWaf bool +} + +func NewModuleWaf() *ModuleWaf { + m := new(ModuleWaf) + m.name = ModChaitinWaf + + 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 +} + +// for alb_waf_instances.data +func (m *ModuleWaf) WafInstancesLoad(path string) error { + data, err := AlbWafInstancesLoadAndCheck(path) + if err != nil { + return err + } + + var wafInstances []WafInstance + wafInstances = data.WafCluster + + if !m.isNoneWaf { + m.wafClientPool.Update(wafInstances, data.Version) + } + + 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.albWafInstancesPath + } + 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.albWafInstancesPath = conf.ConfigPath.AlbWafInstancesPath + + 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..03ec612fd --- /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) + defer file.Close() + if err != nil { + return data, err + } + + // 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..42f3b1c72 --- /dev/null +++ b/bfe_modules/mod_unified_waf/states.go @@ -0,0 +1,60 @@ +// 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" +) + +// key for counter of mod_crypto +type ModuleChaitinWafState 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 moudle state + + underlyingState ModuleChaitinWafState + 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(NOAH_MOD_WAF_DELAY) + m.delayPeekBody.SetKeyPrefix(NOAH_MOD_WAF_PEEK_DELAY) + m.delayCallComp.SetKeyPrefix(NOAH_MOD_WAF_COMP_DELAY) + + m.state.SetKeyPrefix(NOAH_SD_MOD_WAF) + m.stateDiff.SetKeyPrefix(NOAH_SD_MOD_WAF_DIFF) + + m.metrics.Init(&m.underlyingState, ModChaitinWaf, 0) + + return &m +} diff --git a/bfe_modules/mod_unified_waf/testdata/alb_waf_instances.data b/bfe_modules/mod_unified_waf/testdata/alb_waf_instances.data new file mode 100644 index 000000000..3d9ba037f --- /dev/null +++ b/bfe_modules/mod_unified_waf/testdata/alb_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/alb_waf_instances_empty.data b/bfe_modules/mod_unified_waf/testdata/alb_waf_instances_empty.data new file mode 100644 index 000000000..1a4c334b9 --- /dev/null +++ b/bfe_modules/mod_unified_waf/testdata/alb_waf_instances_empty.data @@ -0,0 +1,5 @@ +{ + "Version": "2023-01-19 12:00:10", + "Config": { + } +} 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..d6ea6e3ec --- /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" +AlbWafInstancesPath = "./testdata/alb_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/waf_client.go b/bfe_modules/mod_unified_waf/waf_client.go new file mode 100644 index 000000000..a4154e643 --- /dev/null +++ b/bfe_modules/mod_unified_waf/waf_client.go @@ -0,0 +1,584 @@ +// 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" +) + +const ( + AVAILABLE_THRESHOLD = 20 // default error counter for waf client avaialable +) + +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..2997658ef --- /dev/null +++ b/bfe_modules/mod_unified_waf/waf_client_pool.go @@ -0,0 +1,287 @@ +// 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 + //wafParamVersion string + //wafInstanceVersion string + 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 + //p.wafParamVersion = ver + + 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() + + // try close waf clients + toDelete := []*WafClient{} + for _, client := range p.toDelClients { + 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.toDelClients = toDelete + + // for monitor + p.monitor.state.SetNum(TO_DELETE_CLIENTS, int64(len(toDelete))) + p.monitor.state.SetNum(ACTIVE_CLIENTS, int64(len(p.wafClients))) + + p.lock.Unlock() + } +} + +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() + + // TODO: 这种模式就无法动态修改除weight外的参数 + // 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, version string) { + // 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) + + // update waf instance version + //p.wafInstanceVersion = version +} + +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..6c0f1386c --- /dev/null +++ b/bfe_modules/mod_unified_waf/waf_data_load.go @@ -0,0 +1,185 @@ +// 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 + + DEFAULT_MAX_WAIT_RATE = 0.2 // default client max waiting rate + DEFAULT_HALF_OPEN_THRES_RATE = 0.2 // default client half open thres rate + DEFAULT_CLOSED_THRES_RATE = 0.8 // default client closed thres rate + DEFAULT_SAMPLE_PERCENT = 5.0 // default client sample rate under half-open + DEFAULT_STAT_TOTAL_OP_COUNT = 100 // default client sample rate under half-open +) + +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 + //ConnPoolSize int //connection pool size + 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 + // var data GlobalParamConf + + // open the file + file, err := os.Open(filename) + defer file.Close() + if err != nil { + return nil, err + } + + // 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_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/alb_waf_instances.data b/conf/mod_unified_waf/alb_waf_instances.data new file mode 100644 index 000000000..f3e11f220 --- /dev/null +++ b/conf/mod_unified_waf/alb_waf_instances.data @@ -0,0 +1,5 @@ +{ + "Version": "2025-06-23 12:00:10", + "Config": { + } +} 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..c96683750 --- /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" +AlbWafInstancesPath = "../conf/mod_unified_waf/alb_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/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..bd7627cad --- /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/alb_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..d113d48de --- /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.AlbWafInstancesPath | 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" +AlbWafInstancesPath = "../conf/mod_unified_waf/alb_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/alb_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..0be004e2a 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,9 @@ 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 +// replace github.com/bfenetworks/bwi => /root/yingfei/opensouce/bwi +// replace github.com/bfenetworks/bfe-mock-waf => /root/yingfei/opensouce/bfe-mock-waf 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= From 2e078cf72f784e32b65ba23738e9de6ea3b444ce Mon Sep 17 00:00:00 2001 From: yeyunxi Date: Fri, 4 Jul 2025 14:30:20 +0800 Subject: [PATCH 3/3] remove comment code; improve naming convention; improve deleteLoop Signed-off-by: yeyunxi --- bfe_modules/mod_unified_waf/conf_load.go | 21 +++++------- bfe_modules/mod_unified_waf/conf_load_test.go | 6 ++-- .../mod_unified_waf/mod_unified_waf.go | 32 +++++++++---------- .../mod_unified_waf/product_param_load.go | 2 +- bfe_modules/mod_unified_waf/states.go | 19 ++++++----- .../testdata/mod_unified_waf.conf | 2 +- ..._waf_instances.data => waf_instances.data} | 0 ...es_empty.data => waf_instances_empty.data} | 0 bfe_modules/mod_unified_waf/waf_client.go | 4 --- .../mod_unified_waf/waf_client_pool.go | 28 ++++++++-------- bfe_modules/mod_unified_waf/waf_data_load.go | 12 ++----- ...nstances_load.go => waf_instances_load.go} | 16 +++++----- ...oad_test.go => waf_instances_load_test.go} | 18 +++++------ conf/mod_unified_waf/mod_unified_waf.conf | 2 +- ..._waf_instances.data => waf_instances.data} | 0 .../modules/mod_unified_waf/bfe_waf_demo.md | 2 +- .../mod_unified_waf/mod_unified_waf.md | 8 ++--- go.mod | 2 -- 18 files changed, 74 insertions(+), 100 deletions(-) rename bfe_modules/mod_unified_waf/testdata/{alb_waf_instances.data => waf_instances.data} (100%) rename bfe_modules/mod_unified_waf/testdata/{alb_waf_instances_empty.data => waf_instances_empty.data} (100%) rename bfe_modules/mod_unified_waf/{alb_waf_instances_load.go => waf_instances_load.go} (86%) rename bfe_modules/mod_unified_waf/{alb_waf_instances_load_test.go => waf_instances_load_test.go} (62%) rename conf/mod_unified_waf/{alb_waf_instances.data => waf_instances.data} (100%) diff --git a/bfe_modules/mod_unified_waf/conf_load.go b/bfe_modules/mod_unified_waf/conf_load.go index 2debec831..ab26b3beb 100644 --- a/bfe_modules/mod_unified_waf/conf_load.go +++ b/bfe_modules/mod_unified_waf/conf_load.go @@ -25,17 +25,16 @@ import ( type ConfBasic struct { WafProductName string - // Concurrency int - ConnPoolSize int + 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 - AlbWafInstancesPath string // configure path for alb_waf_instances.data + 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 { @@ -62,10 +61,6 @@ func ConfLoad(path string, confRoot string) (*ConfModWaf, error) { // check also fix some configure value func (cfg *ConfModWaf) Check(confRoot string) error { - // if cfg.Basic.Concurrency <= 0 { - // log.Logger.Warn("Basic.Concurrency is : %d, use DEFAULT_CONCURRENCY(%d)", cfg.Basic.Concurrency, DEFAULT_CONCURRENCY) - // cfg.Basic.Concurrency = DEFAULT_CONCURRENCY - // } if len(cfg.Basic.WafProductName) <= 0 { cfg.Basic.WafProductName = NoneWafName } @@ -95,10 +90,10 @@ func (cfg *ConfModWaf) Check(confRoot string) error { return errors.New("ConfigPath.ModWafDataPath not set") } - // check conf of AlbWafInstancesPath - if cfg.ConfigPath.AlbWafInstancesPath == "" { - log.Logger.Error("ConfigPath.AlbWafInstancesPath not set") - return errors.New("ConfigPath.AlbWafInstancesPath 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 index ea36f12ac..cb90e79b2 100644 --- a/bfe_modules/mod_unified_waf/conf_load_test.go +++ b/bfe_modules/mod_unified_waf/conf_load_test.go @@ -23,7 +23,7 @@ func TestConfLoad_1(t *testing.T) { confPath := "./testdata/mod_unified_waf.conf" ModWafDataPath := "./testdata/mod_unified_waf.data" productParamPath := "./testdata/product_param.data" - albWafInstancesPath := "./testdata/alb_waf_instances.data" + wafInstancesPath := "./testdata/waf_instances.data" WafProductName := "BFEMockWaf" conf, err := ConfLoad(confPath, "") @@ -42,7 +42,7 @@ func TestConfLoad_1(t *testing.T) { if conf.ConfigPath.ProductParamPath != productParamPath { t.Errorf("ProductParamPath should be %s not %s", productParamPath, conf.ConfigPath.ProductParamPath) } - if conf.ConfigPath.AlbWafInstancesPath != albWafInstancesPath { - t.Errorf("AlbWafInstancesPath should be %s not %s", albWafInstancesPath, conf.ConfigPath.AlbWafInstancesPath) + 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 index c85736944..60689ce80 100644 --- a/bfe_modules/mod_unified_waf/mod_unified_waf.go +++ b/bfe_modules/mod_unified_waf/mod_unified_waf.go @@ -46,13 +46,13 @@ const ( const NoneWafName = "None" const ( - ModChaitinWaf = "mod_unified_waf" + ModUnifiedWaf = "mod_unified_waf" - NOAH_SD_MOD_WAF = "waf_client" - NOAH_SD_MOD_WAF_DIFF = "waf_client_diff" - NOAH_MOD_WAF_DELAY = "waf_client_delay" - NOAH_MOD_WAF_PEEK_DELAY = "waf_client_delay_peek_body" - NOAH_MOD_WAF_COMP_DELAY = "waf_client_delay_call_competition" + 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" @@ -80,9 +80,9 @@ type ModuleWaf struct { prodParams *ProductParamTable wafData *GlobalParamConf - modWafDataPath string // path for mod_unified_waf.data - productParamPath string // path for product_param.data - albWafInstancesPath string // path for alb_waf_instances.data + 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 @@ -91,7 +91,7 @@ type ModuleWaf struct { func NewModuleWaf() *ModuleWaf { m := new(ModuleWaf) - m.name = ModChaitinWaf + m.name = ModUnifiedWaf m.monitor = NewMonitorStates() m.wafClientPool = NewWafClientPool(m.monitor) @@ -239,18 +239,16 @@ func (m *ModuleWaf) WafClientDataLoad(path string) error { return nil } -// for alb_waf_instances.data func (m *ModuleWaf) WafInstancesLoad(path string) error { - data, err := AlbWafInstancesLoadAndCheck(path) + data, err := WafInstancesLoadAndCheck(path) if err != nil { return err } - var wafInstances []WafInstance - wafInstances = data.WafCluster + wafInstances := data.WafCluster if !m.isNoneWaf { - m.wafClientPool.Update(wafInstances, data.Version) + m.wafClientPool.Update(wafInstances) } instData, _ := json.Marshal(wafInstances) @@ -300,7 +298,7 @@ func (m *ModuleWaf) loadWafInstances(query url.Values) error { path := query.Get("path") if path == "" { //use default - path = m.albWafInstancesPath + path = m.wafInstancesPath } err := m.WafInstancesLoad(path) if err != nil { @@ -334,7 +332,7 @@ func (m *ModuleWaf) LoadConfig(confPath string, confRoot string) error { m.modWafDataPath = conf.ConfigPath.ModWafDataPath m.productParamPath = conf.ConfigPath.ProductParamPath - m.albWafInstancesPath = conf.ConfigPath.AlbWafInstancesPath + m.wafInstancesPath = conf.ConfigPath.WafInstancesPath return nil } diff --git a/bfe_modules/mod_unified_waf/product_param_load.go b/bfe_modules/mod_unified_waf/product_param_load.go index 03ec612fd..0d28b96f6 100644 --- a/bfe_modules/mod_unified_waf/product_param_load.go +++ b/bfe_modules/mod_unified_waf/product_param_load.go @@ -79,10 +79,10 @@ func ProductParamLoadAndCheck(filename string) (ProductParamConf, error) { // open the file file, err := os.Open(filename) - defer file.Close() if err != nil { return data, err } + defer file.Close() // decode the file decoder := json.NewDecoder(file) diff --git a/bfe_modules/mod_unified_waf/states.go b/bfe_modules/mod_unified_waf/states.go index 42f3b1c72..d8cb78462 100644 --- a/bfe_modules/mod_unified_waf/states.go +++ b/bfe_modules/mod_unified_waf/states.go @@ -20,8 +20,7 @@ import ( "github.com/baidu/go-lib/web-monitor/module_state2" ) -// key for counter of mod_crypto -type ModuleChaitinWafState struct { +type ModuleWafState struct { } type MonitorStates struct { @@ -29,9 +28,9 @@ type MonitorStates struct { 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 moudle state + stateDiff module_state2.CounterSlice // diff counter of module state - underlyingState ModuleChaitinWafState + underlyingState ModuleWafState metrics metrics.Metrics //moudle state with prometheus format } @@ -47,14 +46,14 @@ func NewMonitorStates() *MonitorStates { m.state.CountersInit(COUNTER_KEYS) m.stateDiff.Init(m.state, DIFF_COUNTER_INTERVAL) - m.delay.SetKeyPrefix(NOAH_MOD_WAF_DELAY) - m.delayPeekBody.SetKeyPrefix(NOAH_MOD_WAF_PEEK_DELAY) - m.delayCallComp.SetKeyPrefix(NOAH_MOD_WAF_COMP_DELAY) + 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(NOAH_SD_MOD_WAF) - m.stateDiff.SetKeyPrefix(NOAH_SD_MOD_WAF_DIFF) + m.state.SetKeyPrefix(KP_SD_MOD_WAF) + m.stateDiff.SetKeyPrefix(KP_SD_MOD_WAF_DIFF) - m.metrics.Init(&m.underlyingState, ModChaitinWaf, 0) + 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 index d6ea6e3ec..617515a70 100644 --- a/bfe_modules/mod_unified_waf/testdata/mod_unified_waf.conf +++ b/bfe_modules/mod_unified_waf/testdata/mod_unified_waf.conf @@ -7,7 +7,7 @@ ConnPoolSize = 8 [ConfigPath] ModWafDataPath = "./testdata/mod_unified_waf.data" ProductParamPath = "./testdata/product_param.data" -AlbWafInstancesPath = "./testdata/alb_waf_instances.data" +WafInstancesPath = "./testdata/waf_instances.data" [Log] OpenDebug = false diff --git a/bfe_modules/mod_unified_waf/testdata/alb_waf_instances.data b/bfe_modules/mod_unified_waf/testdata/waf_instances.data similarity index 100% rename from bfe_modules/mod_unified_waf/testdata/alb_waf_instances.data rename to bfe_modules/mod_unified_waf/testdata/waf_instances.data diff --git a/bfe_modules/mod_unified_waf/testdata/alb_waf_instances_empty.data b/bfe_modules/mod_unified_waf/testdata/waf_instances_empty.data similarity index 100% rename from bfe_modules/mod_unified_waf/testdata/alb_waf_instances_empty.data rename to bfe_modules/mod_unified_waf/testdata/waf_instances_empty.data diff --git a/bfe_modules/mod_unified_waf/waf_client.go b/bfe_modules/mod_unified_waf/waf_client.go index a4154e643..e53e3e897 100644 --- a/bfe_modules/mod_unified_waf/waf_client.go +++ b/bfe_modules/mod_unified_waf/waf_client.go @@ -32,10 +32,6 @@ import ( "github.com/bfenetworks/bwi/bwi" ) -const ( - AVAILABLE_THRESHOLD = 20 // default error counter for waf client avaialable -) - var ( ERR_WAF_FORBIDDEN = errors.New("FORBIDDEN_BY_WAF") // request forbidden by waf ) diff --git a/bfe_modules/mod_unified_waf/waf_client_pool.go b/bfe_modules/mod_unified_waf/waf_client_pool.go index 2997658ef..89306389a 100644 --- a/bfe_modules/mod_unified_waf/waf_client_pool.go +++ b/bfe_modules/mod_unified_waf/waf_client_pool.go @@ -31,9 +31,7 @@ type WafClientPool struct { toDelClients []*WafClient // to be deleted waf clients wafParam GlobalParam - //wafParamVersion string - //wafInstanceVersion string - monitor *MonitorStates // monitor states + monitor *MonitorStates // monitor states lock sync.RWMutex // protect for wafClients and other members updateLock sync.Mutex // protect for Update() @@ -68,7 +66,6 @@ func (p *WafClientPool) UpdateWafParam(data *GlobalParamConf) { p.lock.Lock() p.wafParam = *param - //p.wafParamVersion = ver for _, c := range p.wafClients { c.UpdateWafGlobalParam(param) @@ -85,10 +82,13 @@ func (p *WafClientPool) deleteLoop() { <-t.C p.lock.Lock() + tryDeletes := p.toDelClients + p.toDelClients = nil + p.lock.Unlock() // try close waf clients toDelete := []*WafClient{} - for _, client := range p.toDelClients { + for _, client := range tryDeletes { if err := client.Close(); err != nil { // close failed, still should not delete toDelete = append(toDelete, client) @@ -99,13 +99,15 @@ func (p *WafClientPool) deleteLoop() { } // reset to delete clients - p.toDelClients = toDelete + 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, int64(len(toDelete))) - p.monitor.state.SetNum(ACTIVE_CLIENTS, int64(len(p.wafClients))) - - p.lock.Unlock() + p.monitor.state.SetNum(TO_DELETE_CLIENTS, toDeleteClientCount) + p.monitor.state.SetNum(ACTIVE_CLIENTS, activeClientCount) } } @@ -186,7 +188,6 @@ func (p *WafClientPool) adjustInstances(instanceMap map[string]WafInstance) (map p.lock.RLock() - // TODO: 这种模式就无法动态修改除weight外的参数 // find new added instances for addr, instance := range instanceMap { if client, found := p.wafClients[addr]; !found { @@ -211,7 +212,7 @@ func (p *WafClientPool) adjustInstances(instanceMap map[string]WafInstance) (map return toAdd, toDel } -func (p *WafClientPool) Update(instances []WafInstance, version string) { +func (p *WafClientPool) Update(instances []WafInstance) { // protect from concurrent update p.updateLock.Lock() defer p.updateLock.Unlock() @@ -241,9 +242,6 @@ func (p *WafClientPool) Update(instances []WafInstance, version string) { // delete waf clients p.deleteClients(toDel) - - // update waf instance version - //p.wafInstanceVersion = version } func (p *WafClientPool) Alloc() (*WafClient, error) { diff --git a/bfe_modules/mod_unified_waf/waf_data_load.go b/bfe_modules/mod_unified_waf/waf_data_load.go index 6c0f1386c..fd7e51b28 100644 --- a/bfe_modules/mod_unified_waf/waf_data_load.go +++ b/bfe_modules/mod_unified_waf/waf_data_load.go @@ -26,12 +26,6 @@ import ( const ( DEFAULT_POOL_SIZE = 8 // default waf client connection pool size DEFAULT_CONCURRENCY = 2000 // default waf client concurrency - - DEFAULT_MAX_WAIT_RATE = 0.2 // default client max waiting rate - DEFAULT_HALF_OPEN_THRES_RATE = 0.2 // default client half open thres rate - DEFAULT_CLOSED_THRES_RATE = 0.8 // default client closed thres rate - DEFAULT_SAMPLE_PERCENT = 5.0 // default client sample rate under half-open - DEFAULT_STAT_TOTAL_OP_COUNT = 100 // default client sample rate under half-open ) type HealthCheckerConfFile struct { @@ -44,8 +38,7 @@ type GlobalParamFile 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 + MaxWaitCount int //max wait rate for request waiting for token } WafDetect struct { @@ -154,14 +147,13 @@ func (cfg *GlobalParamConfFile) cvtToConf() (*GlobalParamConf, error) { // reload_trigger adaptor interface func WafDataParamLoadAndCheck(filename string) (*GlobalParamConf, error) { var err error - // var data GlobalParamConf // open the file file, err := os.Open(filename) - defer file.Close() if err != nil { return nil, err } + defer file.Close() // decode the file decoder := json.NewDecoder(file) diff --git a/bfe_modules/mod_unified_waf/alb_waf_instances_load.go b/bfe_modules/mod_unified_waf/waf_instances_load.go similarity index 86% rename from bfe_modules/mod_unified_waf/alb_waf_instances_load.go rename to bfe_modules/mod_unified_waf/waf_instances_load.go index 4475bb106..52bdfee54 100644 --- a/bfe_modules/mod_unified_waf/alb_waf_instances_load.go +++ b/bfe_modules/mod_unified_waf/waf_instances_load.go @@ -28,23 +28,23 @@ type WafInstance struct { HealthCheckPort int } -// alb waf instance config for alb clusters +// waf instance config for waf clusters type ClusterConfigs struct { WafCluster []WafInstance `json:"WafCluster"` } // global param in config file -type AlbWafInstancesConfFile struct { +type WafInstancesConfFile struct { Version *string Config *ClusterConfigs } -type AlbWafInstancesConf struct { +type WafInstancesConf struct { Version string `json:"version"` WafCluster []WafInstance `json:"WafCluster"` } -func (cfg *AlbWafInstancesConfFile) Check() error { +func (cfg *WafInstancesConfFile) Check() error { if err := bfe_util.CheckNilField(*cfg, false); err != nil { return err } @@ -67,20 +67,20 @@ func (cfg *AlbWafInstancesConfFile) Check() error { } // reload_trigger adaptor interface -func AlbWafInstancesLoadAndCheck(filename string) (AlbWafInstancesConf, error) { +func WafInstancesLoadAndCheck(filename string) (WafInstancesConf, error) { var err error - var data AlbWafInstancesConf + var data WafInstancesConf // open the file file, err := os.Open(filename) - defer file.Close() if err != nil { return data, err } + defer file.Close() // decode the file decoder := json.NewDecoder(file) - var dataFile AlbWafInstancesConfFile + var dataFile WafInstancesConfFile err = decoder.Decode(&dataFile) if err != nil { return data, err diff --git a/bfe_modules/mod_unified_waf/alb_waf_instances_load_test.go b/bfe_modules/mod_unified_waf/waf_instances_load_test.go similarity index 62% rename from bfe_modules/mod_unified_waf/alb_waf_instances_load_test.go rename to bfe_modules/mod_unified_waf/waf_instances_load_test.go index b9684a32c..27b996d04 100644 --- a/bfe_modules/mod_unified_waf/alb_waf_instances_load_test.go +++ b/bfe_modules/mod_unified_waf/waf_instances_load_test.go @@ -15,21 +15,19 @@ package mod_unified_waf import ( - "fmt" "testing" ) -func TestAlbWafInstancesLoadAndCheck_1(t *testing.T) { - albWafInstancesPath := "./testdata/alb_waf_instances.data" +func TestWafInstancesLoadAndCheck_1(t *testing.T) { + wafInstancesPath := "./testdata/waf_instances.data" - winsts, err := AlbWafInstancesLoadAndCheck(albWafInstancesPath) + winsts, err := WafInstancesLoadAndCheck(wafInstancesPath) if err != nil { - t.Errorf("AlbWafInstancesLoadAndCheck(): %v", err) + t.Errorf("WafInstancesLoadAndCheck(): %v", err) return } if winsts.WafCluster[0].HealthCheckPort != winsts.WafCluster[0].Port { - fmt.Println("=== TestAlbWafInstancesLoadAndCheck_1", winsts.WafCluster[0].HealthCheckPort, winsts.WafCluster[0].Port) t.Errorf("winsts.WafCluster[0].HealthCheckPort != winsts.WafCluster[0].Port") return } @@ -41,12 +39,12 @@ func TestAlbWafInstancesLoadAndCheck_1(t *testing.T) { } -func TestAlbWafInstancesLoadAndCheck_2(t *testing.T) { - albWafInstancesPath := "./testdata/alb_waf_instances_empty.data" +func TestWafInstancesLoadAndCheck_2(t *testing.T) { + wafInstancesPath := "./testdata/waf_instances_empty.data" - _, err := AlbWafInstancesLoadAndCheck(albWafInstancesPath) + _, err := WafInstancesLoadAndCheck(wafInstancesPath) if err != nil { - t.Errorf("AlbWafInstancesLoadAndCheck(): %v", err) + t.Errorf("TestWafInstancesLoadAndCheck_2(): %v", err) return } } diff --git a/conf/mod_unified_waf/mod_unified_waf.conf b/conf/mod_unified_waf/mod_unified_waf.conf index c96683750..0301c39fb 100644 --- a/conf/mod_unified_waf/mod_unified_waf.conf +++ b/conf/mod_unified_waf/mod_unified_waf.conf @@ -6,7 +6,7 @@ ConnPoolSize = 8 [ConfigPath] ModWafDataPath = "../conf/mod_unified_waf/mod_unified_waf.data" ProductParamPath = "../conf/mod_unified_waf/product_param.data" -AlbWafInstancesPath = "../conf/mod_unified_waf/alb_waf_instances.data" +WafInstancesPath = "../conf/mod_unified_waf/waf_instances.data" [Log] OpenDebug = false diff --git a/conf/mod_unified_waf/alb_waf_instances.data b/conf/mod_unified_waf/waf_instances.data similarity index 100% rename from conf/mod_unified_waf/alb_waf_instances.data rename to conf/mod_unified_waf/waf_instances.data 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 index bd7627cad..e8baf7401 100644 --- a/docs/zh_cn/modules/mod_unified_waf/bfe_waf_demo.md +++ b/docs/zh_cn/modules/mod_unified_waf/bfe_waf_demo.md @@ -96,7 +96,7 @@ WafProductName = BFEMockWaf #### 修改WAF RS 实例 ``` -#cat ../conf/mod_unified_waf/alb_waf_instances.data +#cat ../conf/mod_unified_waf/waf_instances.data { "Version": "2023-01-19 12:00:10", "Config": { 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 index d113d48de..b4ba12a27 100644 --- a/docs/zh_cn/modules/mod_unified_waf/mod_unified_waf.md +++ b/docs/zh_cn/modules/mod_unified_waf/mod_unified_waf.md @@ -2,7 +2,7 @@ ## 模块简介 -Bfe 支持在 http request 的处理流程中引入统一的第三方WAF支持。 +BFE 支持在 http request 的处理流程中引入统一的第三方WAF支持。 ## 基础配置 @@ -16,7 +16,7 @@ Bfe 支持在 http request 的处理流程中引入统一的第三方WAF支持 | Basic.ConnPoolSize | String
与WAF server 的连接池大小 | | ConfigPath.ModWafDataPath | String
WAF访问的具体参数配置 | | ConfigPath.ProductParamPath | String
WAF访问的产品线配置 | -| ConfigPath.AlbWafInstancesPath | String
WAF RS实例池的配置 | +| ConfigPath.WafInstancesPath | String
WAF RS实例池的配置 | | Log.OpenDebug | Boolean
是否开启 debug 日志
默认值False | ### 配置示例 @@ -30,7 +30,7 @@ ConnPoolSize = 8 [ConfigPath] ModWafDataPath = "../conf/mod_unified_waf/mod_unified_waf.data" ProductParamPath = "../conf/mod_unified_waf/product_param.data" -AlbWafInstancesPath = "../conf/mod_unified_waf/alb_waf_instances.data" +WafInstancesPath = "../conf/mod_unified_waf/waf_instances.data" [Log] OpenDebug = false @@ -111,7 +111,7 @@ OpenDebug = false ## WAF RS实例池配置 -配置文件: conf/mod_unified_waf/alb_waf_instances.data +配置文件: conf/mod_unified_waf/waf_instances.data ### 配置描述 diff --git a/go.mod b/go.mod index 0be004e2a..8c9e57825 100644 --- a/go.mod +++ b/go.mod @@ -73,5 +73,3 @@ require ( ) // replace github.com/bfenetworks/proxy-wasm-go-host => ../proxy-wasm-go-host -// replace github.com/bfenetworks/bwi => /root/yingfei/opensouce/bwi -// replace github.com/bfenetworks/bfe-mock-waf => /root/yingfei/opensouce/bfe-mock-waf