├── changelog ├── doc ├── waf-1.jpg └── go-fast-waf-1.png ├── internal ├── rules │ ├── writelist.json │ ├── blacklist.json │ ├── cc-1.json │ ├── path_though.json │ ├── disclosure.json │ ├── xss-1.json │ ├── xss-3.json │ ├── scan.json │ └── xss-2.json ├── share │ ├── waf_proto.go │ ├── util.go │ ├── config.go │ ├── msgtrace.go │ └── waf_proto_easyjson.go ├── gate │ ├── rtt.go │ ├── waf_check.go │ ├── waf_proxy.go │ ├── http_proxy.go │ └── router.go └── server │ ├── rule_iplist.go │ ├── rule_cache.go │ ├── rule_cc.go │ ├── rule_regexp.go │ └── rule_parse.go ├── waf_server.toml.template ├── go.mod ├── Makefile ├── error.html ├── .gitignore ├── SECURITY.md ├── go.sum ├── waf_gate.toml.template ├── cmd ├── waf-server │ └── main.go └── waf-gate │ └── main.go ├── .github └── workflows │ └── codeql-analysis.yml ├── README.md └── README_en.md /changelog: -------------------------------------------------------------------------------- 1 | V0.2 2 | TODO: 3 | 1. waf-gate可以启动多个http和Https应用; -------------------------------------------------------------------------------- /doc/waf-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kumustone/go-fast-waf/HEAD/doc/waf-1.jpg -------------------------------------------------------------------------------- /doc/go-fast-waf-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kumustone/go-fast-waf/HEAD/doc/go-fast-waf-1.png -------------------------------------------------------------------------------- /internal/rules/writelist.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "IpWhiteList", 3 | "status": "valid", 4 | "rule_name": "IpWhiteList-0", 5 | "ip_list": [ 6 | "3.3.3.3" 7 | ] 8 | } -------------------------------------------------------------------------------- /internal/rules/blacklist.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "IpBlackList", 3 | "status": "valid", 4 | "rule_name": "IpBlackList-0", 5 | "ip_list": [ 6 | "1.1.1.1", 7 | "2.2.2.2" 8 | ] 9 | } -------------------------------------------------------------------------------- /waf_server.toml.template: -------------------------------------------------------------------------------- 1 | [server] 2 | # WafServer Listen Address 3 | WafServerAddress = "127.0.0.1:8000" 4 | 5 | # WafServer API Address 6 | HttpAPIAddress = "127.0.0.1:8001" 7 | 8 | 9 | -------------------------------------------------------------------------------- /internal/rules/cc-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "CC", 3 | "status": "valid", 4 | "rule_name": "cc-1", 5 | "desc": "反CC/爬虫", 6 | "cc_rule": { 7 | "host": "xxxx.com", 8 | "interval": 60, 9 | "count": 100, 10 | "forbid_time": 3600, 11 | "key": "ip" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /internal/rules/path_though.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Group", 3 | "status": "valid", 4 | "rule_name": "路径穿越", 5 | "desc": "路径穿越", 6 | "group_rule": [ 7 | { 8 | "field": "Url", 9 | "op": "is", 10 | "empty": false, 11 | "val": "(\\.\\.\\/){2,}" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go-fast-waf 2 | 3 | go 1.21.5 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.3.2 7 | github.com/kumustone/tcpstream v1.0.2 8 | github.com/mailru/easyjson v0.7.7 9 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 10 | ) 11 | 12 | require github.com/josharian/intern v1.0.0 // indirect 13 | -------------------------------------------------------------------------------- /internal/rules/disclosure.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Group", 3 | "status": "valid", 4 | "rule_name": "信息泄露", 5 | "desc": "信息泄露", 6 | "group_rule": [ 7 | { 8 | "field": "Url", 9 | "op": "is", 10 | "empty": false, 11 | "val": "(\\/web-inf\\/|\\/\\.git\\/config|\\/\\.svn\\/|\\.war($|\\?|;))" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /internal/rules/xss-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Group", 3 | "status": "valid", 4 | "rule_name": "xss-1", 5 | "desc": "this is a test rule", 6 | "group_rule": [ 7 | { 8 | "field": "Url", 9 | "op": "is", 10 | "empty": false, 11 | "val": "\\(?\\s*\\b(alert|prompt|confirm|console\\.log)\\s*\\)?\\s*(\\(|`)" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /internal/rules/xss-3.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Group", 3 | "status": "valid", 4 | "rule_name": "xss-3", 5 | "desc": "this is a test rule", 6 | "group_rule": [ 7 | { 8 | "field": "Url", 9 | "op": "is", 10 | "empty": false, 11 | "val": "(<\\s?\\/?\\b(script|iframe)\\b(\\s|\\+|>){0,5})|(<\\s*embed(\\/|\\s)*src\\s*=)" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all waf-gate waf-server clean 2 | 3 | BIN_DIR := $(shell pwd)/bin 4 | 5 | all: waf-gate waf-server 6 | 7 | waf-gate: 8 | @mkdir -p $(BIN_DIR) 9 | @cd cmd/waf-gate && go build -o $(BIN_DIR)/waf-gate 10 | 11 | waf-server: 12 | @mkdir -p $(BIN_DIR) 13 | @cd cmd/waf-server && go build -o $(BIN_DIR)/waf-server 14 | 15 | clean: 16 | @rm -rf $(BIN_DIR) 17 | -------------------------------------------------------------------------------- /internal/rules/scan.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Group", 3 | "status": "valid", 4 | "rule_name": "scanner", 5 | "desc": "扫描器", 6 | "group_rule": [ 7 | { 8 | "field": "User-Agent", 9 | "op": "is", 10 | "empty": false, 11 | "val": "\\b(nmap|w3af|sqlmap|acunetix|nessus|webvulnscan|masscan|ce\\.baidu\\.com|WPScan|xmlrpc exploit|wordpress hash grabber|whatweb|w3af|havij|masscan|pangolin|security scan)" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Error 5 | 12 | 13 | 14 |

An error occurred.

15 |

Sorry, the page you are looking for is currently unavailable.
16 | Please try again later.

17 | 18 | -------------------------------------------------------------------------------- /internal/rules/xss-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Group", 3 | "status": "valid", 4 | "rule_name": "xss-2", 5 | "desc": "this is a test rule", 6 | "group_rule": [ 7 | { 8 | "field": "Url", 9 | "op": "is", 10 | "empty": false, 11 | "val": "\\b(onmouseenter|onmouseover|onmousemove|onmousedown|onmouseleave|onclick|onload|onerror|onmousewheel|onscroll|onbeforecopy|onbeforecut|onstart|oncontextmenu|oncopy|onfocus|autofocus)\\s*?=" 12 | } 13 | ] 14 | } 15 | 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # bin file 3 | # Ignore waf-server and waf-gate executables 4 | waf-server* 5 | waf-gate* 6 | 7 | # Created by .ignore support plugin (hsz.mobi) 8 | # Compiled binary files 9 | *.exe 10 | *.dll 11 | *.so 12 | *.dylib 13 | 14 | # Executables 15 | bin/ 16 | out/ 17 | 18 | # Go specific 19 | # Ignore the go binary 20 | *.test 21 | *.prof 22 | 23 | # Ignore the output of go test 24 | *.out 25 | 26 | # Ignore the vendor directory 27 | vendor/ 28 | 29 | # Ignore go modules cache 30 | /go.sum 31 | 32 | # IDE-specific files 33 | .idea/ 34 | .vscode/ 35 | *.swp 36 | *.swo 37 | *.swn 38 | *.swm 39 | *.swpx 40 | *.swx 41 | -------------------------------------------------------------------------------- /internal/share/waf_proto.go: -------------------------------------------------------------------------------- 1 | package share 2 | 3 | const ( 4 | WAF_PASS = iota 5 | WAF_INTERCEPT 6 | WAF_SLIDER 7 | WAF_INTERNAL_TIMEOUT 8 | WAF_INTERNAL_SEND_ERR 9 | WAF_INTERNAL_RECV_ERR 10 | WAF_INTERNAL_REQUEST_INVALID 11 | ) 12 | 13 | //easyjson:json 14 | type WafHttpRequest struct { 15 | Mark string 16 | Method string 17 | Scheme string 18 | Url string 19 | Proto string 20 | Host string 21 | RemoteAddr string 22 | ContentLength uint64 23 | Header map[string][]string 24 | Body []byte 25 | } 26 | 27 | //easyjson:json 28 | type WafProxyResp struct { 29 | RetCode int 30 | RuleName string 31 | Desc string 32 | } 33 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /internal/gate/rtt.go: -------------------------------------------------------------------------------- 1 | package gate 2 | 3 | import ( 4 | "sync/atomic" 5 | ) 6 | 7 | /* 8 | RTT round-trip time 9 | */ 10 | 11 | var ( 12 | rtt RTT 13 | ) 14 | 15 | // RTT统计的基础类 16 | type RTT struct { 17 | name string // 统计类目名称 18 | count uint64 // 总个数 19 | total uint64 // 总耗时 20 | } 21 | 22 | func (r *RTT) Add(time uint64) { 23 | atomic.AddUint64(&r.total, time) 24 | atomic.AddUint64(&r.count, 1) 25 | } 26 | 27 | func (r *RTT) GetAverageRT() uint64 { 28 | average := uint64(0) 29 | total := atomic.SwapUint64(&r.total, 0) 30 | count := atomic.SwapUint64(&r.count, 0) 31 | 32 | if count > 0 { 33 | average = total / count 34 | } else { 35 | average = 0 36 | } 37 | 38 | return average 39 | } 40 | -------------------------------------------------------------------------------- /internal/share/util.go: -------------------------------------------------------------------------------- 1 | package share 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "os" 7 | "runtime/debug" 8 | "time" 9 | ) 10 | 11 | // 获取当前的微秒时间 12 | func GetMicroTime() uint64 { 13 | return uint64(time.Now().UnixNano()) / uint64(time.Microsecond) 14 | } 15 | 16 | func Now() int { 17 | return int(time.Now().Unix()) 18 | } 19 | 20 | func PanicRecovery(quit bool) { 21 | var err error 22 | if r := recover(); r != nil { 23 | switch x := r.(type) { 24 | case string: 25 | err = errors.New(x) 26 | break 27 | case error: 28 | err = x 29 | break 30 | default: 31 | err = errors.New("Unknown panic") 32 | break 33 | } 34 | debug.PrintStack() 35 | log.Println("Panic :", err.Error()) 36 | 37 | if quit { 38 | os.Exit(101) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= 2 | github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 3 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 4 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 5 | github.com/kumustone/tcpstream v1.0.2 h1:xch69oMpHgBdzUoUw3huQgqQfDCOWW7dqEJp1jjMQTI= 6 | github.com/kumustone/tcpstream v1.0.2/go.mod h1:1bGLZSjyzROSkm3F/Ns8UAfe1tQqp1B7U/srX+Pg2eQ= 7 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 8 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 9 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= 10 | gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 11 | -------------------------------------------------------------------------------- /waf_gate.toml.template: -------------------------------------------------------------------------------- 1 | [gate] 2 | # Gate HTTP Listen Address 3 | GateHttpAddress = "0.0.0.0:80" 4 | 5 | # Gate HTTPS Listen Addresses 6 | StartHttps = true 7 | GateHttpsAddress = "0.0.0.0:443" 8 | 9 | # Different keys for multiple subdomains 10 | CertKeyList = [ 11 | ["A.xxx.com", "A.pem", "A.key"], 12 | ["B.xxx.com", "B.pem", "B.key"] 13 | ] 14 | 15 | CertFile = "xxxxxxx.pem" 16 | KeyFile = "xxxxx.key" 17 | 18 | # Gat API Service 19 | GateAPIAddress = "0.0.0.0:2081" 20 | 21 | # Upstream Address, RoundRobin 22 | UpstreamList = [ 23 | "1.1.1.1:80" 24 | ] 25 | 26 | # WAF detection items 27 | [wafrpc] 28 | 29 | # Detection switch: if false, all content is forwarded directly to upstream without sending to waf-server for monitoring; 30 | # true: process according to the Checklist rules; 31 | CheckSwitch = true 32 | 33 | [wafrpc.CheckList] 34 | # Hosts to be checked 35 | Include = ["xxxxxxx"] 36 | 37 | # Excluded Hosts 38 | Exclude = [] 39 | 40 | # Whether to check hosts other than those in Include and Exclude 41 | CheckDefault = true 42 | 43 | [wafrpc.ServerAddr] 44 | # List of waf-server addresses; if there are multiple waf-servers, requests are load-balanced in a round-robin manner 45 | Address = ["127.0.0.1:8000"] 46 | -------------------------------------------------------------------------------- /internal/server/rule_iplist.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | . "go-fast-waf/internal/share" 5 | "sync" 6 | ) 7 | 8 | type IPList struct { 9 | set map[string]bool 10 | name string 11 | desc string 12 | mutex sync.RWMutex 13 | } 14 | 15 | func NewIPList(n string, d string) *IPList { 16 | return &IPList{ 17 | name: n, 18 | desc: d, 19 | set: make(map[string]bool), 20 | } 21 | } 22 | 23 | func (l *IPList) HandleRule(r *JSONRule) { 24 | for _, i := range r.IPList { 25 | if r.Status == "valid" { 26 | l.Add(i) 27 | } 28 | } 29 | } 30 | 31 | func (l *IPList) CleanRules() { 32 | l.mutex.Lock() 33 | l.set = make(map[string]bool) 34 | l.mutex.Unlock() 35 | } 36 | 37 | func (l *IPList) CheckRequest(req *WafHttpRequest) *WafProxyResp { 38 | ip := req.RemoteAddr 39 | if l.Contains(ip) { 40 | 41 | resp := &WafProxyResp{ 42 | RetCode: WAF_INTERCEPT, 43 | RuleName: l.name, 44 | Desc: l.desc, 45 | } 46 | 47 | if l.name == "IPWhiteList" { 48 | resp.RetCode = WAF_PASS 49 | } 50 | 51 | return resp 52 | } 53 | 54 | return SuccessResp 55 | } 56 | 57 | func (l *IPList) Add(k string) { 58 | l.mutex.Lock() 59 | l.set[k] = true 60 | l.mutex.Unlock() 61 | } 62 | 63 | func (l *IPList) Remove(k string) { 64 | l.mutex.Lock() 65 | delete(l.set, k) 66 | l.mutex.Unlock() 67 | } 68 | 69 | func (l *IPList) Contains(k string) bool { 70 | l.mutex.RLock() 71 | _, ok := l.set[k] 72 | l.mutex.RUnlock() 73 | return ok 74 | } 75 | -------------------------------------------------------------------------------- /internal/gate/waf_check.go: -------------------------------------------------------------------------------- 1 | package gate 2 | 3 | import ( 4 | "bytes" 5 | . "go-fast-waf/internal/share" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | func cloneHeader(h http.Header) http.Header { 14 | h2 := make(http.Header, len(h)) 15 | for k, vv := range h { 16 | vv2 := make([]string, len(vv)) 17 | copy(vv2, vv) 18 | h2[k] = vv2 19 | } 20 | return h2 21 | } 22 | 23 | const MaxLimitBody int64 = 100 * 1024 24 | 25 | func GetBody(req *http.Request) []byte { 26 | if req.ContentLength > MaxLimitBody || req.ContentLength <= 0 { 27 | return []byte("") 28 | } 29 | 30 | var originBody []byte 31 | defer req.Body.Close() 32 | if body, err := ioutil.ReadAll(req.Body); err != nil { 33 | log.Println("Get body fail : ", err.Error()) 34 | return body 35 | } else { 36 | originBody = make([]byte, req.ContentLength) 37 | copy(originBody, body) 38 | req.Body = ioutil.NopCloser(bytes.NewReader(body)) 39 | return originBody 40 | } 41 | } 42 | 43 | func Check(req *http.Request) *WafProxyResp { 44 | wafReq := &WafHttpRequest{ 45 | Mark: req.Host, 46 | Method: req.Method, 47 | Scheme: req.URL.Scheme, 48 | Url: req.RequestURI, 49 | Proto: req.Proto, 50 | Host: req.Host, 51 | RemoteAddr: req.RemoteAddr, 52 | ContentLength: uint64(req.ContentLength), 53 | Header: cloneHeader(req.Header), 54 | Body: GetBody(req), 55 | } 56 | 57 | //只保留IP即可 58 | if s := strings.Split(req.RemoteAddr, ":"); len(s) > 0 { 59 | wafReq.RemoteAddr = s[0] 60 | } 61 | 62 | resp, err := WafCheck(wafReq, time.Duration(20*time.Millisecond)) 63 | if err != nil { 64 | //log.Println("waf check : ", err.Error()) 65 | } 66 | return resp 67 | } 68 | -------------------------------------------------------------------------------- /internal/gate/waf_proxy.go: -------------------------------------------------------------------------------- 1 | package gate 2 | 3 | import ( 4 | "errors" 5 | "github.com/kumustone/tcpstream" 6 | . "go-fast-waf/internal/share" 7 | "time" 8 | ) 9 | 10 | var routerServer = NewRouter() 11 | 12 | func handleServerNotify(n AddrNotify) { 13 | for _, addr := range n.Address { 14 | if n.Action == WAF_SERVER_ADD { 15 | routerServer.Add(&RouterItem{ 16 | Key: addr, 17 | Value: tcpstream.NewSyncClient(addr), 18 | }) 19 | } else if n.Action == WAF_SERVER_REMOVE { 20 | routerServer.Remove(addr) 21 | } 22 | } 23 | } 24 | 25 | func WaitServerNotify() { 26 | go func() { 27 | for { 28 | select { 29 | case n := <-ServerNotify: 30 | handleServerNotify(n) 31 | } 32 | } 33 | }() 34 | } 35 | 36 | func WafCheck(request *WafHttpRequest, timeout time.Duration) (*WafProxyResp, error) { 37 | if !NeedCheck(request.Mark) { 38 | return nil, errors.New("Need no check") 39 | } 40 | 41 | buffer, err := request.MarshalJSON() 42 | if err != nil { 43 | return nil, errors.New(" request MarshalJSON fail") 44 | } 45 | 46 | var conn *tcpstream.SyncClient 47 | for i := 0; i < int(routerServer.Size()); i++ { 48 | if r := routerServer.Select(); r == nil { 49 | break 50 | } else { 51 | if r.Value.(*tcpstream.SyncClient).Stream.State == tcpstream.CONN_STATE_ESTAB { 52 | conn = r.Value.(*tcpstream.SyncClient) 53 | } 54 | r = nil 55 | } 56 | } 57 | 58 | if conn == nil { 59 | return nil, errors.New("No tcpstream available ") 60 | } 61 | 62 | respMsg, err := conn.Call(&tcpstream.Message{Body: buffer}, time.Duration(time.Millisecond*200)) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | resp := &WafProxyResp{} 68 | if err := resp.UnmarshalJSON(respMsg.Body); err != nil { 69 | return nil, err 70 | } 71 | 72 | return resp, nil 73 | } 74 | -------------------------------------------------------------------------------- /internal/gate/http_proxy.go: -------------------------------------------------------------------------------- 1 | package gate 2 | 3 | import ( 4 | . "go-fast-waf/internal/share" 5 | "log" 6 | "net" 7 | "net/http" 8 | "net/http/httputil" 9 | "net/url" 10 | "os" 11 | "time" 12 | ) 13 | 14 | var UpStream = NewRouter() 15 | 16 | var httpReverse = NewMultipleHostReverseProxy() 17 | 18 | func NewMultipleHostReverseProxy() *httputil.ReverseProxy { 19 | debugLog := log.New(os.Stdout, "[Debug]", log.Ldate|log.Ltime|log.Llongfile) 20 | 21 | return &httputil.ReverseProxy{ 22 | ErrorLog: debugLog, 23 | 24 | //Modify request 25 | Director: func(req *http.Request) { 26 | req.URL.Scheme = "http" 27 | req.URL.Host = UpStream.Select().Key 28 | //log.Println("upstream host ", req.URL.Host) 29 | }, 30 | 31 | //Modify response 32 | ModifyResponse: func(resp *http.Response) error { 33 | return nil 34 | }, 35 | 36 | Transport: &http.Transport{ 37 | Proxy: func(req *http.Request) (*url.URL, error) { 38 | return http.ProxyFromEnvironment(req) 39 | }, 40 | 41 | Dial: func(network, addr string) (net.Conn, error) { 42 | conn, err := (&net.Dialer{ 43 | Timeout: 30 * time.Second, 44 | KeepAlive: 30 * time.Second, 45 | }).Dial(network, addr) 46 | if err != nil { 47 | println("Error during DIAL:", err.Error()) 48 | } 49 | return conn, err 50 | }, 51 | 52 | //must config MaxIdleConnsPerHost: connect: can't assign requested address 53 | MaxIdleConnsPerHost: 512, 54 | TLSHandshakeTimeout: 300 * time.Second, 55 | IdleConnTimeout: 120 * time.Second, 56 | }, 57 | } 58 | } 59 | 60 | type HttpHandler struct{} 61 | 62 | var ( 63 | WafHandler = &HttpHandler{} 64 | ) 65 | 66 | func (h *HttpHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { 67 | ret := Check(req) 68 | if ret != nil && ret.RetCode == WAF_INTERCEPT { 69 | log.Printf("Intercept : Rule %s %s %s\n", ret.RuleName, ret.Desc, req) 70 | resp.WriteHeader(405) 71 | return 72 | } 73 | 74 | //可以在这里加一些包头给后端做业务处理 75 | req.Header.Set("x-waf-scheme", req.URL.Scheme) 76 | httpReverse.ServeHTTP(resp, req) 77 | } 78 | -------------------------------------------------------------------------------- /cmd/waf-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "github.com/BurntSushi/toml" 6 | "github.com/kumustone/tcpstream" 7 | . "go-fast-waf/internal/server" 8 | . "go-fast-waf/internal/share" 9 | lumberjack "gopkg.in/natefinch/lumberjack.v2" 10 | "log" 11 | ) 12 | 13 | const WafMsgVersion uint8 = 1 14 | 15 | type Server struct { 16 | WafServerAddress string 17 | HttpAPIAddress string 18 | } 19 | 20 | type WafServerConf struct { 21 | Server Server 22 | } 23 | 24 | var ( 25 | confFile = flag.String("c", "waf_server.conf", "Config file") 26 | logPath = flag.String("l", "./log", " log path") 27 | rulePath = flag.String("r", "./rules", " rule path") 28 | ) 29 | 30 | func main() { 31 | flag.Parse() 32 | 33 | c := WafServerConf{} 34 | 35 | if _, err := toml.DecodeFile(*confFile, &c); err != nil { 36 | log.Fatal("Can not decode config file ", err.Error()) 37 | return 38 | } 39 | 40 | defer PanicRecovery(true) 41 | log.SetOutput(&lumberjack.Logger{ 42 | Filename: *logPath + "/waf_server.log", 43 | MaxSize: 10, 44 | MaxBackups: 10, 45 | MaxAge: 30, 46 | }) 47 | 48 | if err := InitRulePath(*rulePath); err != nil { 49 | log.Fatal("InitRulePath : ", err.Error()) 50 | } 51 | 52 | log.Println("waf-server listen at : ", c.Server.WafServerAddress) 53 | 54 | if err := tcpstream.NewTCPServer(c.Server.WafServerAddress, &ServerHandler{}).Serve(); err != nil { 55 | log.Println("server : ", err.Error()) 56 | } 57 | 58 | select {} 59 | } 60 | 61 | type ServerHandler struct{} 62 | 63 | func (*ServerHandler) OnData(conn *tcpstream.TcpStream, msg *tcpstream.Message) error { 64 | request := &WafHttpRequest{} 65 | if err := request.UnmarshalJSON(msg.Body); err != nil { 66 | return err 67 | } 68 | 69 | var resp *WafProxyResp 70 | for _, c := range CheckList { 71 | resp = c.CheckRequest(request) 72 | if resp.RuleName != "" { 73 | break 74 | } 75 | } 76 | 77 | body, _ := resp.MarshalJSON() 78 | respMsg := tcpstream.Message{ 79 | Header: tcpstream.ProtoHeader{ 80 | Seq: msg.Header.Seq, 81 | }, 82 | Body: body, 83 | } 84 | 85 | return conn.Write(&respMsg) 86 | } 87 | 88 | func (*ServerHandler) OnConn(conn *tcpstream.TcpStream) { 89 | 90 | } 91 | 92 | func (*ServerHandler) OnDisConn(conn *tcpstream.TcpStream) { 93 | 94 | } 95 | -------------------------------------------------------------------------------- /internal/gate/router.go: -------------------------------------------------------------------------------- 1 | package gate 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | "time" 7 | ) 8 | 9 | // 目前只支持轮询模式,一般后面是NGINX web服务; 10 | // 一般网关不能重启,支持对后端数据的热加载; 11 | type RouterItem struct { 12 | Key string 13 | Value interface{} 14 | } 15 | 16 | type Notify struct { 17 | Items []*RouterItem 18 | Action int 19 | } 20 | 21 | var ( 22 | Upstream = NewRouter() 23 | ) 24 | 25 | type Router struct { 26 | notify chan Notify 27 | pool []*RouterItem 28 | stop chan struct{} 29 | counter int32 30 | current uint32 //轮询算法使用 31 | mutex sync.RWMutex 32 | timeout time.Duration 33 | } 34 | 35 | func NewRouter() *Router { 36 | return &Router{ 37 | notify: make(chan Notify, 128), 38 | stop: make(chan struct{}), 39 | } 40 | } 41 | 42 | func (r *Router) WaitNotify() { 43 | go func() { 44 | for { 45 | select { 46 | case n := <-r.notify: 47 | for _, item := range n.Items { 48 | if n.Action == 0 { 49 | r.Add(item) 50 | } else if n.Action == 1 { 51 | r.Remove(item.Key) 52 | } 53 | } 54 | case <-r.stop: 55 | return 56 | } 57 | } 58 | }() 59 | } 60 | 61 | func (r *Router) Size() int32 { 62 | return atomic.LoadInt32(&r.counter) 63 | } 64 | 65 | func (r *Router) addSize(d int32) int32 { 66 | return atomic.AddInt32(&r.counter, d) 67 | } 68 | 69 | // 做健康监测使用 70 | func (r *Router) Remove(key string) { 71 | exist := false 72 | index := 0 73 | r.mutex.Lock() 74 | defer r.mutex.Unlock() 75 | 76 | for i, value := range r.pool { 77 | if value.Key == key { 78 | exist = true 79 | index = i 80 | break 81 | } 82 | } 83 | if exist { 84 | r.pool = append(r.pool[:index], r.pool[index+1:]...) 85 | r.addSize(-1) 86 | } 87 | 88 | } 89 | 90 | func (r *Router) Add(item *RouterItem) bool { 91 | r.mutex.Lock() 92 | defer r.mutex.Unlock() 93 | 94 | exist := false 95 | for _, value := range r.pool { 96 | if value.Key == item.Key { 97 | exist = true 98 | return false 99 | } 100 | } 101 | 102 | if !exist { 103 | r.pool = append(r.pool, item) 104 | r.addSize(1) 105 | } 106 | return true 107 | } 108 | 109 | func (r *Router) Select() *RouterItem { 110 | r.mutex.RLock() 111 | defer r.mutex.RUnlock() 112 | 113 | if r.Size() <= 0 { 114 | return nil 115 | } 116 | 117 | //类型是uint32, 如果int32发生越界 118 | r.current++ 119 | index := r.current % uint32(r.Size()) 120 | 121 | return r.pool[index] 122 | } 123 | -------------------------------------------------------------------------------- /internal/server/rule_cache.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "go-fast-waf/internal/share" 5 | "log" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | // BlackInfo 命中多条规则,那么以最长的时间为准; 11 | type BlackInfo struct { 12 | Host string 13 | Key string 14 | AddTime int 15 | EndTime int 16 | } 17 | 18 | type BlackMap struct { 19 | sessions map[string]*BlackInfo 20 | } 21 | 22 | // CacheBlackList 第一层以Host作为区分,第二层的Key值可能是IP或是UID 23 | type CacheBlackList struct { 24 | sync.RWMutex 25 | blackMaps map[string]BlackMap 26 | current uint64 27 | } 28 | 29 | func NewCacheBlackList() *CacheBlackList { 30 | manager := &CacheBlackList{ 31 | blackMaps: make(map[string]BlackMap), 32 | } 33 | 34 | go manager.CleanLoop() 35 | return manager 36 | } 37 | 38 | func (c *CacheBlackList) CheckRequest(req *share.WafHttpRequest) *share.WafProxyResp { 39 | if b := c.Match(req.Mark, req.RemoteAddr); b != nil { 40 | return &share.WafProxyResp{ 41 | RetCode: share.WAF_INTERCEPT, 42 | RuleName: "CacheBlackList", 43 | Desc: "缓存黑名单", 44 | } 45 | } 46 | return SuccessResp 47 | } 48 | 49 | func (c *CacheBlackList) HandleRule(j *JSONRule) { 50 | 51 | } 52 | 53 | func (c *CacheBlackList) CleanRules() { 54 | 55 | } 56 | 57 | func (c *CacheBlackList) Add(info *BlackInfo) { 58 | c.Lock() 59 | defer c.Unlock() 60 | 61 | blackMap, exist := c.blackMaps[info.Host] 62 | if !exist { 63 | blackMap = BlackMap{ 64 | sessions: make(map[string]*BlackInfo), 65 | } 66 | c.blackMaps[info.Host] = blackMap 67 | } 68 | 69 | //不管有没有这个IP的blackInfo,都将它覆盖掉; 70 | blackMap.sessions[info.Key] = info 71 | } 72 | 73 | func (c *CacheBlackList) Remove(host string, key string) { 74 | c.Lock() 75 | defer c.Unlock() 76 | blackMap, exist := c.blackMaps[host] 77 | if !exist { 78 | return 79 | } 80 | delete(blackMap.sessions, key) 81 | } 82 | 83 | func (c *CacheBlackList) Match(host string, key string) *BlackInfo { 84 | c.RLock() 85 | defer c.RUnlock() 86 | 87 | blackMap, exist := c.blackMaps[host] 88 | if !exist { 89 | return nil 90 | } 91 | 92 | if b, exist := blackMap.sessions[key]; exist { 93 | return b 94 | } 95 | 96 | return nil 97 | } 98 | 99 | func (c *CacheBlackList) CleanLoop() { 100 | for { 101 | c.Lock() 102 | now := share.Now() 103 | for key, value := range c.blackMaps { 104 | for key1, value1 := range value.sessions { 105 | if value1.EndTime <= now { 106 | log.Printf("AntiCC Remove timeout key : %s:%s\n", key, key1) 107 | delete(value.sessions, key1) 108 | } 109 | } 110 | } 111 | c.Unlock() 112 | time.Sleep(time.Second * 1) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /internal/share/config.go: -------------------------------------------------------------------------------- 1 | package share 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // WafCheckList 通过URL的关键字来检查哪些需要上传检测,哪些不需要 8 | type WafCheckList struct { 9 | // 检测项 10 | Include []string 11 | // 排除项 12 | Exclude []string 13 | // 默认url是否检测 14 | CheckDefault bool 15 | } 16 | 17 | // WafServerAddr 要连接的waf-server的地址 18 | type WafServerAddr struct { 19 | Address []string 20 | } 21 | 22 | const ( 23 | WAF_SERVER_ADD = iota 24 | WAF_SERVER_REMOVE 25 | ) 26 | 27 | type AddrNotify struct { 28 | Address []string 29 | Action int 30 | } 31 | 32 | var ( 33 | ServerNotify = make(chan AddrNotify, 128) 34 | ) 35 | 36 | type Config struct { 37 | CheckSwitch bool 38 | CheckList WafCheckList 39 | ServerAddr WafServerAddr 40 | } 41 | 42 | var ( 43 | config Config 44 | chkInclude map[string]bool 45 | chkExclude map[string]bool 46 | mutex sync.RWMutex 47 | ) 48 | 49 | // InitConfig 初始化或者发生动态的改变都调用这个接口 50 | func InitConfig(c Config) { 51 | mutex.Lock() 52 | defer mutex.Unlock() 53 | 54 | server := config.ServerAddr.Address 55 | 56 | config = c 57 | chkInclude = make(map[string]bool) 58 | chkExclude = make(map[string]bool) 59 | for _, item := range config.CheckList.Include { 60 | chkInclude[item] = true 61 | } 62 | 63 | for _, item := range config.CheckList.Exclude { 64 | chkExclude[item] = true 65 | } 66 | 67 | add, remove := diffSlice(server, config.ServerAddr.Address) 68 | 69 | ServerNotify <- AddrNotify{ 70 | Address: add, 71 | Action: WAF_SERVER_ADD, 72 | } 73 | 74 | ServerNotify <- AddrNotify{ 75 | Address: remove, 76 | Action: WAF_SERVER_REMOVE, 77 | } 78 | } 79 | 80 | // NeedCheck 判断一个标记是否需要经过waf检查 81 | func NeedCheck(mark string) bool { 82 | mutex.RLock() 83 | defer mutex.RUnlock() 84 | 85 | if !config.CheckSwitch { 86 | return false 87 | } 88 | 89 | if _, ok := chkInclude[mark]; ok { 90 | return true 91 | } 92 | 93 | if _, ok := chkExclude[mark]; ok { 94 | return false 95 | } 96 | return config.CheckList.CheckDefault 97 | } 98 | 99 | // 比较两个数组的异同点 100 | func diffSlice(oldArrays []string, newArrays []string) (add []string, remove []string) { 101 | for _, itemOld := range oldArrays { 102 | exist := false 103 | for _, itemNew := range newArrays { 104 | if itemOld == itemNew { 105 | exist = true 106 | break 107 | } 108 | } 109 | //已经删除的 110 | if exist == false { 111 | remove = append(remove, itemOld) 112 | } 113 | } 114 | 115 | for _, itemNew := range newArrays { 116 | exist := false 117 | for _, itemOld := range oldArrays { 118 | if itemNew == itemOld { 119 | exist = true 120 | break 121 | } 122 | } 123 | 124 | if exist == false { 125 | add = append(add, itemNew) 126 | } 127 | } 128 | 129 | return add, remove 130 | } 131 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '34 15 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /internal/share/msgtrace.go: -------------------------------------------------------------------------------- 1 | package share 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | /* 10 | 做调试使用,对整个数据流进行打点,以便找到可能的阻塞点; 11 | */ 12 | 13 | // 跟踪msg打点 14 | type timeStamp struct { 15 | time uint64 // 打点时间,单位是us 16 | name string // 打点名称 17 | } 18 | 19 | func newTimeStamp(name string) *timeStamp { 20 | return &timeStamp{ 21 | time: uint64(time.Now().UnixNano()) / uint64(time.Microsecond), 22 | name: name, 23 | } 24 | } 25 | 26 | type MsgTrace struct { 27 | trace []*timeStamp // 流经的每个地点的时间打标; 28 | } 29 | 30 | func (mt *MsgTrace) mark(name string) { 31 | mt.trace = append(mt.trace, &timeStamp{ 32 | time: uint64(time.Now().UnixNano()) / uint64(time.Microsecond), 33 | name: name, 34 | }) 35 | } 36 | 37 | func NewMsgTrace() *MsgTrace { 38 | mt := &MsgTrace{} 39 | mt.mark("beginning") 40 | return mt 41 | } 42 | 43 | // 设置消息打点, 单独跟踪一条消息,默认为是线程安全的 44 | func (mt *MsgTrace) MarkTimeStamp(name string) { 45 | mt.trace = append(mt.trace, &timeStamp{ 46 | time: uint64(time.Now().UnixNano()) / uint64(time.Microsecond), 47 | name: name, 48 | }) 49 | } 50 | 51 | // 打点时间输出 52 | func (mt *MsgTrace) OutputString() string { 53 | if len(mt.trace) == 0 { 54 | return "null" 55 | } 56 | 57 | var output string 58 | for index, value := range mt.trace { 59 | if index == 0 { 60 | continue 61 | } 62 | output = output + fmt.Sprintf("->%s(%dus)", value.name, int64(value.time)-int64(mt.trace[0].time)) 63 | } 64 | return output 65 | } 66 | 67 | const sessionMapNum = 8 68 | 69 | // key: requestid, value: 同步等待返回的wait 70 | type requestMap struct { 71 | rwmutex sync.RWMutex 72 | sessions map[uint64]*MsgTrace 73 | } 74 | 75 | type MsgTraceCache struct { 76 | sessionMaps [sessionMapNum]requestMap 77 | //disposeFlag bool 78 | //disposeOnce sync.Once 79 | //disposeWait sync.WaitGroup 80 | current uint64 81 | } 82 | 83 | func NewMsgTraceCache() *MsgTraceCache { 84 | manager := &MsgTraceCache{} 85 | for i := 0; i < sessionMapNum; i++ { 86 | manager.sessionMaps[i].sessions = make(map[uint64]*MsgTrace) 87 | } 88 | return manager 89 | } 90 | 91 | var G_msg_trace = NewMsgTraceCache() 92 | 93 | func (m *MsgTraceCache) Cache(request_id uint64, mt *MsgTrace) { 94 | smap := &m.sessionMaps[request_id%sessionMapNum] 95 | smap.rwmutex.Lock() 96 | smap.sessions[request_id] = mt 97 | smap.rwmutex.Unlock() 98 | } 99 | 100 | func (m *MsgTraceCache) Get(request_id uint64) *MsgTrace { 101 | smap := &m.sessionMaps[request_id%sessionMapNum] 102 | 103 | smap.rwmutex.RLock() 104 | defer smap.rwmutex.RUnlock() 105 | 106 | mt, exist := smap.sessions[request_id] 107 | if exist { 108 | return mt 109 | } 110 | return nil 111 | } 112 | 113 | func (m *MsgTraceCache) Remove(request_id uint64) { 114 | smap := &m.sessionMaps[request_id%sessionMapNum] 115 | smap.rwmutex.Lock() 116 | defer smap.rwmutex.Unlock() 117 | _, exist := smap.sessions[request_id] 118 | if exist { 119 | //直接把channel关闭掉 120 | delete(smap.sessions, request_id) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /cmd/waf-gate/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "github.com/BurntSushi/toml" 9 | . "go-fast-waf/internal/gate" 10 | . "go-fast-waf/internal/share" 11 | lumberjack "gopkg.in/natefinch/lumberjack.v2" 12 | "log" 13 | "net/http" 14 | _ "net/http/pprof" 15 | "time" 16 | ) 17 | 18 | type GateConfig struct { 19 | GateHttpAddress string 20 | StartHttps bool 21 | CertKeyList [][]string 22 | GateHttpsAddress string 23 | GateAPIAddress string 24 | UpstreamList []string 25 | } 26 | 27 | type WafGateConfig struct { 28 | Gate GateConfig 29 | WAFRPC Config 30 | } 31 | 32 | var ( 33 | confFile = flag.String("c", "./waf_gate.conf", "Config file") 34 | logPath = flag.String("l", "./log", " log path") 35 | ) 36 | 37 | func main() { 38 | flag.Parse() 39 | 40 | c := WafGateConfig{} 41 | if _, err := toml.DecodeFile(*confFile, &c); err != nil { 42 | log.Fatal("Can not decode config file ", err.Error()) 43 | return 44 | } 45 | 46 | log.Println(c) 47 | 48 | defer PanicRecovery(true) 49 | 50 | log.SetOutput(&lumberjack.Logger{ 51 | Filename: *logPath + "/waf_gate.log", 52 | MaxSize: 1, 53 | MaxBackups: 10, 54 | MaxAge: 30, 55 | }) 56 | 57 | //go pprof检测 58 | go func() { 59 | log.Println(http.ListenAndServe("0.0.0.0:60060", nil)) 60 | }() 61 | 62 | InitConfig(c.WAFRPC) 63 | WaitServerNotify() 64 | 65 | for _, it := range c.Gate.UpstreamList { 66 | UpStream.Add(&RouterItem{ 67 | Key: it, 68 | }) 69 | } 70 | 71 | UpStream.WaitNotify() 72 | server := &http.Server{ 73 | Addr: c.Gate.GateHttpAddress, 74 | IdleTimeout: 3 * time.Minute, 75 | ReadTimeout: 5 * time.Minute, 76 | WriteTimeout: 5 * time.Minute, 77 | MaxHeaderBytes: 20 * 1024 * 1024, 78 | Handler: WafHandler, 79 | } 80 | 81 | go func() { 82 | err := server.ListenAndServe() 83 | if err != nil { 84 | fmt.Println("Listen and serve error ", err.Error()) 85 | } 86 | }() 87 | 88 | if c.Gate.StartHttps { 89 | 90 | caData := make(map[string][]string) 91 | for _, item := range c.Gate.CertKeyList { 92 | if len(item) != 3 { 93 | continue 94 | } 95 | caData[item[0]] = []string{item[1], item[2]} 96 | } 97 | 98 | cfg := &tls.Config{ 99 | GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { 100 | data, ok := caData[info.ServerName] 101 | if !ok { 102 | return nil, errors.New("Cert Key is not exist") 103 | } 104 | cert, err := tls.LoadX509KeyPair(data[0], data[1]) 105 | if err != nil { 106 | return nil, err 107 | } 108 | return &cert, nil 109 | }, 110 | } 111 | 112 | server := http.Server{ 113 | IdleTimeout: 3 * time.Minute, 114 | ReadTimeout: 5 * time.Minute, 115 | WriteTimeout: 5 * time.Minute, 116 | MaxHeaderBytes: 20 * 1024 * 1024, 117 | Handler: WafHandler, 118 | TLSConfig: cfg, 119 | } 120 | fmt.Println("Https start at ", c.Gate.GateHttpsAddress) 121 | log.Fatal(server.ListenAndServeTLS("", "")) 122 | } 123 | 124 | select {} 125 | } 126 | -------------------------------------------------------------------------------- /internal/server/rule_cc.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | . "go-fast-waf/internal/share" 5 | "log" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | type CCStat struct { 11 | count int 12 | } 13 | 14 | // CCRule 单个CCRules 15 | type CCRule struct { 16 | Host string 17 | Interval int 18 | Count int 19 | ForbidTime int 20 | Key string 21 | 22 | ttl int 23 | //到时间后,把整个statics清理掉; 24 | statics map[string]*CCStat 25 | mutex sync.RWMutex 26 | } 27 | 28 | type CCServe struct { 29 | mutex sync.RWMutex 30 | rules map[string][]*CCRule 31 | } 32 | 33 | func NewCCRule(j *JsonCCRule) *CCRule { 34 | return &CCRule{ 35 | Host: j.Host, 36 | Interval: j.InterVal, 37 | Count: j.Count, 38 | ForbidTime: j.ForbidTime, 39 | Key: j.Key, 40 | ttl: Now() + j.InterVal, 41 | statics: make(map[string]*CCStat), 42 | } 43 | } 44 | 45 | func (r *CCRule) OnReq(req *WafHttpRequest) { 46 | r.mutex.Lock() 47 | defer r.mutex.Unlock() 48 | 49 | //目前Key只支持IP 50 | key := req.RemoteAddr 51 | value, exist := r.statics[key] 52 | if !exist { 53 | r.statics[key] = &CCStat{ 54 | count: 1, 55 | } 56 | 57 | } else { 58 | value.count = value.count + 1 59 | if value.count >= r.Count { 60 | log.Printf("AntiCC add to blacklist: %s:%s\n ", r.Host, key) 61 | CBlackList.Add(&BlackInfo{ 62 | Host: r.Host, 63 | Key: key, 64 | AddTime: Now(), 65 | EndTime: Now() + r.ForbidTime, 66 | }) 67 | delete(r.statics, key) 68 | } 69 | } 70 | return 71 | } 72 | 73 | func (r *CCRule) CleanUp(now int) { 74 | if now < r.ttl { 75 | return 76 | } 77 | r.ttl = now + r.Interval 78 | 79 | r.mutex.Lock() 80 | r.statics = make(map[string]*CCStat) 81 | r.mutex.Unlock() 82 | } 83 | 84 | func NewCCServe() *CCServe { 85 | c := &CCServe{ 86 | rules: make(map[string][]*CCRule), 87 | } 88 | 89 | go c.CleanLoop() 90 | return c 91 | } 92 | 93 | func (c *CCServe) HandleRule(j *JSONRule) { 94 | if len(j.CCRule.Host) == 0 { 95 | return 96 | } 97 | if j.Status != "valid" { 98 | return 99 | } 100 | 101 | c.mutex.Lock() 102 | value, exist := c.rules[j.CCRule.Host] 103 | if exist == false { 104 | var ruleList []*CCRule 105 | cr := NewCCRule(&(j.CCRule)) 106 | ruleList = append(ruleList, cr) 107 | c.rules[j.CCRule.Host] = ruleList 108 | } else { 109 | cr := NewCCRule(&(j.CCRule)) 110 | value = append(value, cr) 111 | c.rules[j.CCRule.Host] = value 112 | } 113 | c.mutex.Unlock() 114 | 115 | log.Println("add rule ", *j) 116 | } 117 | 118 | func (c *CCServe) CleanRules() { 119 | c.mutex.Lock() 120 | c.rules = make(map[string][]*CCRule) 121 | c.mutex.Unlock() 122 | } 123 | 124 | func (c *CCServe) CleanLoop() { 125 | c.mutex.RLock() 126 | now := Now() 127 | for _, rules := range c.rules { 128 | for _, spiderRule := range rules { 129 | spiderRule.CleanUp(now) 130 | } 131 | } 132 | c.mutex.RUnlock() 133 | 134 | time.AfterFunc(time.Second, c.CleanLoop) 135 | } 136 | 137 | func (c *CCServe) Add(j *JsonCCRule) { 138 | if len(j.Host) == 0 { 139 | return 140 | } 141 | 142 | c.mutex.Lock() 143 | value, exist := c.rules[j.Host] 144 | if exist == false { 145 | var ruleList []*CCRule 146 | cr := NewCCRule(j) 147 | ruleList = append(ruleList, cr) 148 | c.rules[j.Host] = ruleList 149 | } else { 150 | cr := NewCCRule(j) 151 | value = append(value, cr) 152 | c.rules[j.Host] = value 153 | } 154 | c.mutex.Unlock() 155 | 156 | log.Println("AntiCC add rule ", *j) 157 | } 158 | 159 | func (c *CCServe) CheckRequest(req *WafHttpRequest) *WafProxyResp { 160 | c.mutex.RLock() 161 | defer c.mutex.RUnlock() 162 | 163 | host := req.Host 164 | 165 | hostRules, exist := c.rules[host] 166 | if exist { 167 | for _, rule := range hostRules { 168 | rule.OnReq(req) 169 | } 170 | } 171 | 172 | return SuccessResp 173 | } 174 | -------------------------------------------------------------------------------- /internal/server/rule_regexp.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | . "go-fast-waf/internal/share" 5 | "log" 6 | "regexp" 7 | "sync" 8 | ) 9 | 10 | type RuleItem struct { 11 | JsonGroupRule 12 | reg *regexp.Regexp 13 | } 14 | 15 | type Rule struct { 16 | Type string 17 | Status string 18 | RuleName string 19 | Desc string 20 | Rule []*RuleItem 21 | } 22 | 23 | type RuleList struct { 24 | Rules []*Rule 25 | mutex sync.RWMutex 26 | } 27 | 28 | func NewRuleList() *RuleList { 29 | return &RuleList{} 30 | } 31 | 32 | func (r *RuleList) HandleRule(j *JSONRule) { 33 | if j.Status == "invalid" { 34 | r.Remove(j.RuleName) 35 | return 36 | } 37 | 38 | if j.Status == "valid" { 39 | rule := &Rule{ 40 | Type: j.Type, 41 | Status: j.Status, 42 | RuleName: j.RuleName, 43 | Desc: j.Desc, 44 | } 45 | 46 | for _, item := range j.Rule { 47 | ruleItem := &RuleItem{ 48 | JsonGroupRule: item, 49 | } 50 | var err error 51 | ruleItem.reg, err = regexp.Compile(item.Val) 52 | if err != nil { 53 | log.Printf("Error compiling regex for rule %s: %v", j.RuleName, err) 54 | continue 55 | } 56 | rule.Rule = append(rule.Rule, ruleItem) 57 | } 58 | 59 | log.Printf("RuleList adding rule: %v", rule) 60 | r.Add(rule) 61 | } 62 | } 63 | 64 | func (r *RuleList) CleanRules() { 65 | r.mutex.Lock() 66 | r.Rules = r.Rules[:0:0] 67 | r.mutex.Unlock() 68 | } 69 | 70 | func (r *RuleList) CheckRequest(req *WafHttpRequest) *WafProxyResp { 71 | r.mutex.RLock() 72 | 73 | for _, item := range r.Rules { 74 | if shoot, resp := item.CheckRequest(req); shoot { 75 | r.mutex.RUnlock() 76 | return resp 77 | } 78 | } 79 | 80 | r.mutex.RUnlock() 81 | return SuccessResp 82 | } 83 | 84 | // 查询name的规则是否存在 85 | func (r *RuleList) Exist(name string) bool { 86 | r.mutex.RLock() 87 | defer r.mutex.RUnlock() 88 | 89 | for _, item := range r.Rules { 90 | if item.RuleName == name { 91 | return true 92 | } 93 | } 94 | 95 | return false 96 | } 97 | 98 | func (r *RuleList) Add(rule *Rule) { 99 | r.mutex.Lock() 100 | r.Rules = append(r.Rules, rule) 101 | r.mutex.Unlock() 102 | 103 | return 104 | } 105 | 106 | func (r *RuleList) Remove(name string) { 107 | r.mutex.Lock() 108 | defer r.mutex.Unlock() 109 | 110 | for index, item := range r.Rules { 111 | if item.RuleName == name { 112 | r.Rules = append(r.Rules[:index], r.Rules[index+1:]...) 113 | return 114 | } 115 | } 116 | return 117 | } 118 | 119 | func (r *Rule) CheckRequest(req *WafHttpRequest) (bool, *WafProxyResp) { 120 | //必须所有RuleItem都满足,才算命中这一条规则 121 | for _, item := range r.Rule { 122 | if !item.CheckRequest(req) { 123 | return false, SuccessResp 124 | } 125 | } 126 | 127 | log.Println(*req, " shoot ", *r) 128 | 129 | return true, &WafProxyResp{ 130 | RetCode: WAF_INTERCEPT, 131 | RuleName: r.RuleName, 132 | Desc: r.Desc, 133 | } 134 | } 135 | 136 | func GetFieldFromReq(req *WafHttpRequest, field string) string { 137 | switch field { 138 | case "Host": 139 | return req.Host 140 | case "Referer": 141 | if len(req.Header[field]) > 0 { 142 | return req.Header[field][0] 143 | } 144 | case "Url": 145 | return req.Url 146 | case "User-Agent": 147 | if len(req.Header[field]) > 0 { 148 | return req.Header[field][0] 149 | } 150 | case "Content-Type": 151 | if len(req.Header[field]) > 0 { 152 | return req.Header[field][0] 153 | } 154 | } 155 | return "" 156 | } 157 | 158 | // 对正则进行一次预编译 159 | func (r *RuleItem) CompileReg() (err error) { 160 | r.reg, err = regexp.Compile(r.Val) 161 | return 162 | } 163 | 164 | func (r *RuleItem) CheckRequest(req *WafHttpRequest) bool { 165 | Val := GetFieldFromReq(req, r.Field) 166 | if r.Empty { 167 | return Val == "" 168 | } 169 | 170 | shoot := len(r.reg.FindString(Val)) > 0 171 | 172 | if r.Op == "is" { 173 | return shoot 174 | } else { 175 | return !shoot 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-fast-waf 2 | 3 | [English](README_en.md) 4 | 5 | **轻量级,低延迟,高性能,易于配置,网关和Waf解耦的风险检测服务.** 6 | 7 | ![](https://github.com/kumustone/go-fast-waf/blob/main/doc/go-fast-waf-1.png) 8 | 9 | ## 组件介绍 10 | 1. waf-gate:一个基于 Go 语言编写的极简 HTTP/HTTPS 反向代理。 11 | 2. waf-server:负责执行Waf检测,黑白名单,访问控制,反爬等风险检测任务执行。 12 | 13 | **为什么 waf-gate和waf-server 分离** 14 | 15 | 基于业务场景的考虑前提,即优先保证网关稳定性,高性能和低延迟, waf检测次之。 16 | 1. 网关稳定性风险解耦, 主要是稳定性风险和延迟风险。 17 | * waf-server出现崩溃时,waf-gate和waf-server长连接断开。数据不再发送waf-server检测; 18 | * waf-server出现延迟,waf-gate的client超时机制会保证会在最大超时时间内(默认20ms)把数据转发到upstream server上,不影响业务的正常运行。 19 | * waf-server通过会有计算量大操作和入库等行为,如果waf-server和waf-gate是同一个工程,部署在同一台机器上存在资源征用的问题。响应网关的响应速度。 20 | 2. 方便分布式部署扩展,可以部署1对多部署,弥补某些复杂检测业务的waf性能不足。 21 | 22 | **waf-gate** 23 | 24 | 1. **轻量级**:waf-gate 是一个非常轻量级的 HTTP 代理。它在 Go 的反向代理库基础上进行了极少量的修改和功能扩展。最大程度保证waf-gate运行的稳定性。 25 | 2. **路由功能**:如果您的网站部署很简单,没有复杂的业务路由,waf-gate 可以直接将请求分发给下游的业务服务器。但如果路由较为复杂,waf-gate 会将请求转发给另一个代理(例如 NGINX)进行进一步的路由。无论哪种情况,waf-gate 对整个业务链条来说都是完全透明的。 26 | 3. **Web风险检测** : waf-gate 将请求转发给 waf-server 进行检测,然后 waf-server 将检测结果返回给 waf-gate,以便拦截或允许请求继续。 27 | 28 | **waf-server** 29 | 30 | 1. **通信方式**:waf-gate 与 waf-server 之间通过长连接的 TCP 通信,使用 tcpstream 库实现。 31 | 2. **多连接支持**:waf-gate 与 waf-server 之间建立多个连接,采用轮询策略将请求分发给 waf-server 进行检测。 32 | 3. **超时处理**:waf-gate 支持检测超时设置,如果网络或其他异常导致 waf-server 未能及时响应,waf-gate 会自动放行请求。 33 | 4. **自动重连**:如果当前没有可用的 waf-server 响应,waf-gate 会自动放行所有请求。 34 | 35 | 36 | **waf_gate与waf_server的网络通信**,是通过[tcpstream]()的库来实现的 37 | 38 | - waf_gate与waf_server之间通过tcp长链连接; 39 | - waf_gate与waf_server之间采用多对多连接,gate通过轮询策略发送给server检测; 40 | - waf_gate支持检测超时设置,如果网络或者其他异常导致请求没有及时回复,那么waf_gate自动放过; 41 | - waf_gate启动自动重连功能,如果没有当前没有可用的waf-server请求,那么放过所有请求; 42 | 43 | ## 规则功能 44 | 45 | - 支持IP黑白名单; 46 | 47 | - 基于Host,Referer,Url,User-Agent,Content-Type 正则表达式的组合规则,各个字段支持为空; 48 | 49 | - rule规则通过JSON格式配置,方便后续通过接口规则做扩展,和后续进行GUI开发; 50 | 51 | 比如拦截请求: Host: www.xxx.com; Refer 为空; User-Agent中包nmap关键字; 52 | 53 | ## 1. 安装 54 | 55 | ``` 56 | git clone https://github.com/kumustone/waf.git 57 | make # make完成后会在bin目录下面生成waf-gate, waf-server两个文件。 58 | ``` 59 | 60 | ## 2. 运行 61 | 62 | > waf-gate -c /YourConfigPath/waf_gate.conf -l /YourLogPath/log 63 | 64 | > waf-server -c /YouConfigPaht/waf_server.conf -l /YourLogPath/log -r /YourRuleDir 65 | 66 | ## 3. 配置说明 67 | 68 | waf-gate 69 | 70 | ```go 71 | [gate] 72 | #Gate Http Listen Address 73 | GateHttpAddress = "0.0.0.0:80" 74 | 75 | # Gate Https Listen Addresses 76 | StartHttps = true 77 | GateHttpsAddress = "0.0.0.0:443" 78 | 79 | # 多个二级域名使用不同的key 80 | CertKeyList = [ 81 | [ 82 | "A.xxx.com", 83 | "A.pem", 84 | "A.key" 85 | ], 86 | [ 87 | "B.xxx.com", 88 | "B.pem", 89 | "B.key" 90 | ] 91 | ] 92 | 93 | CertFile = "xxxxxxx.pem" 94 | KeyFile = "xxxxx.key" 95 | 96 | # Gat API Service 97 | GateAPIAddress = "0.0.0.0:2081" 98 | 99 | # Upstream Address, RoundBin 100 | UpstreamList = [ 101 | "1.1.1.1:80" 102 | ] 103 | 104 | # Waf检测项 105 | [wafrpc] 106 | 107 | # 检测开关,如果为false,所有的内容都不发送waf-server监测,直接转发给upstream处理; 108 | # true: 按照Checklist规则处理; 109 | CheckSwitch = true 110 | [wafrpc.CheckList] 111 | # 需要检测项Host 112 | Include = ["xxxxxxx"] 113 | 114 | # 排除检测项Host 115 | Exclude = [] 116 | 117 | # Include 和 Exclude以外的Host,是否检测; 118 | CheckDefault = true 119 | [wafrpc.ServerAddr] 120 | # waf-server的地址列表;如果是多个,如果有多个waf-server按照轮询策略转发请求; 121 | Address = ["127.0.0.1:8000"] 122 | 123 | ``` 124 | 125 | waf-server 配置 126 | 127 | ```go 128 | [server] 129 | # WafServer的监听地址 130 | WafServerAddress = "127.0.0.1:8000" 131 | 132 | # WafServer接口地址 133 | HttpAPIAddress = "127.0.0.1:8001" 134 | 135 | ``` 136 | 137 | ## 4. 规则文件说明 138 | 139 | ip 黑名单的例子 140 | ``` 141 | { 142 | "rules": [ 143 | { 144 | "type": "IpBlackList", 145 | "action": "add", 146 | "rule_name": "IpBlackList-0", 147 | "iplist": [ 148 | "1.1.1.1", 149 | "2.2.2.2" 150 | ] 151 | } 152 | ] 153 | } 154 | ``` 155 | 156 | 正则规则的例子url 满足正则:\\(?\\s*\\b(alert|prompt|confirm|console\\.log)\\s*\\)?\\s*(\\(|`) 都会被拦截; 157 | 158 | ``` 159 | { 160 | "rules": [ 161 | { 162 | "type": "Group", 163 | "action": "add", 164 | "rule_name": "xss-1", 165 | "desc": "this is a test rule", 166 | "group_rule": [ 167 | { 168 | "field": "Url", 169 | "op": "is", 170 | "empty": false, 171 | "val": "\\(?\\s*\\b(alert|prompt|confirm|console\\.log)\\s*\\)?\\s*(\\(|`)" 172 | } 173 | ] 174 | } 175 | ] 176 | } 177 | ``` 178 | 179 | 每一个规则文件里面包含的都是一份完整的json,不同的规则文件,可以叠加使用; 180 | 181 | 目前支持的规则还比较少,欢迎添加一些规则; 182 | 183 | -------------------------------------------------------------------------------- /internal/server/rule_parse.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/json" 5 | . "go-fast-waf/internal/share" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "regexp" 10 | "strings" 11 | ) 12 | 13 | var ( 14 | supportField = []string{ 15 | "Host", 16 | "Referer", 17 | "Url", 18 | "User-Agent", 19 | "Content-Type", 20 | } 21 | ) 22 | 23 | var ( 24 | SuccessResp = &WafProxyResp{ 25 | RetCode: WAF_PASS, 26 | } 27 | ) 28 | 29 | var ( 30 | IPBlackList = NewIPList("IPBlackList", "") 31 | IPWriteList = NewIPList("IPWhiteList", "") 32 | GroupRule = NewRuleList() 33 | CBlackList = NewCacheBlackList() 34 | AntiCC = NewCCServe() 35 | 36 | CheckList = []RuleCheckHandler{ 37 | IPWriteList, 38 | IPBlackList, 39 | GroupRule, 40 | CBlackList, 41 | AntiCC, 42 | } 43 | ) 44 | 45 | type JsonGroupRule struct { 46 | Field string `json:"field"` 47 | Op string `json:"op"` 48 | Empty bool `json:"empty"` 49 | Val string `json:"val"` 50 | } 51 | 52 | type JsonCCRule struct { 53 | Host string `json:"host"` 54 | InterVal int `json:"interval"` 55 | Count int `json:"count"` 56 | ForbidTime int `json:"forbid_time"` 57 | Key string `json:"key"` 58 | } 59 | 60 | type JSONRule struct { 61 | Type string `json:"type"` 62 | Status string `json:"status"` 63 | RuleName string `json:"rule_name"` 64 | Desc string `json:"desc,omitempty"` 65 | IPList []string `json:"ip_list,omitempty"` 66 | Rule []JsonGroupRule `json:"group_rule,omitempty"` 67 | CCRule JsonCCRule `json:"cc_rule,omitempty"` 68 | } 69 | 70 | type RuleCheckHandler interface { 71 | CheckRequest(req *WafHttpRequest) *WafProxyResp 72 | CleanRules() 73 | HandleRule(j *JSONRule) 74 | } 75 | 76 | func validIP4(ipAddress string) bool { 77 | ipAddress = strings.Trim(ipAddress, " ") 78 | 79 | re, _ := regexp.Compile(`^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$`) 80 | if re.MatchString(ipAddress) { 81 | return true 82 | } 83 | return false 84 | } 85 | 86 | func validField(field string) bool { 87 | for _, item := range supportField { 88 | if item == field { 89 | return true 90 | } 91 | } 92 | return false 93 | } 94 | 95 | func handleJsonFile(file string) error { 96 | log.Println("handle rule file :", file) 97 | bs, err := ioutil.ReadFile(file) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | var r JSONRule 103 | if err := json.Unmarshal(bs, &r); err != nil { 104 | return err 105 | } 106 | return HandleRule(&r) 107 | } 108 | 109 | func InitRulePath(path string) error { 110 | log.Println("InitRulePath:", path) 111 | 112 | if f, err := os.Stat(path); err != nil { 113 | return err 114 | } else { 115 | if f.IsDir() { 116 | files, _ := ioutil.ReadDir(path) 117 | for _, ff := range files { 118 | if !ff.IsDir() && strings.HasSuffix(ff.Name(), "json") { 119 | if err := handleJsonFile(path + "/" + ff.Name()); err != nil { 120 | return err 121 | } 122 | } 123 | } 124 | } else { 125 | return handleJsonFile(path) 126 | } 127 | } 128 | return nil 129 | } 130 | 131 | func HandleRule(j *JSONRule) error { 132 | switch j.Type { 133 | case "IpBlackList": 134 | IPBlackList.HandleRule(j) 135 | case "IpWhiteList": 136 | IPWriteList.HandleRule(j) 137 | case "Group": 138 | GroupRule.HandleRule(j) 139 | case "CC": 140 | AntiCC.HandleRule(j) 141 | default: 142 | log.Fatal("unknown Rule type ", j.Type) 143 | } 144 | 145 | return nil 146 | } 147 | 148 | //func CheckRule(j *JSONRule) error { 149 | // //检查ruleName是否已经存在 150 | // //检查action : add/remove 151 | // //IPList中的内容是否全部为IP,或者IP段? 152 | // //检查groupRule每一个field字段是否支持 153 | // 154 | // if j.Status != "invalid" && j.Status != "valid" { 155 | // return errors.New("exist invalid action, check your rule config") 156 | // } 157 | // 158 | // if j.Type != "IpBlackList" && j.Type != "IpWhiteList" && j.Type != "Group" { 159 | // return errors.New(fmt.Sprint("type", j.Type, " is not invalid")) 160 | // } 161 | // 162 | // for _, ip := range j.IPList { 163 | // if !validIP4(ip) { 164 | // return errors.New(fmt.Sprint("ip ", ip, " is Invalid")) 165 | // } 166 | // } 167 | // 168 | // for _, rule := range j.Rule { 169 | // if !validField(rule.Field) { 170 | // return errors.New(fmt.Sprint("field ", rule.Field, " is not support")) 171 | // } 172 | // 173 | // if rule.Op != "is" && rule.Op != "not" { 174 | // return errors.New(fmt.Sprint("op ", rule.Op, " is invalid")) 175 | // } 176 | // 177 | // //data, err := base64.StdEncoding.DecodeString(rule.Val) 178 | // //if err != nil { 179 | // // return errors.New(fmt.Sprint("Val", rule.Val, " can not base64 Decode ", err.Error())) 180 | // //} 181 | // 182 | // if _, err := regexp.Compile(string(rule.Val)); err != nil { 183 | // return errors.New(fmt.Sprint("Val ", rule.Val, " can not ruleExp Compile ", err.Error())) 184 | // } 185 | // } 186 | // return nil 187 | //} 188 | -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 | # waf 2 | [简体中文](README.md) 3 | 4 | A lightweight Waf detection tool; 5 | 6 | Based on Go language, it consists of a gateway waf-gate with Http/Https reverse proxy and a waf-server that performs detection tasks; 7 | 8 | ![](https://github.com/deph) 9 | 10 | 11 | 12 | waf_gate is a very lightweight httpproxy reverse proxy, which does a very small amount of encapsulation and functionality addition on the basis of go's own reverseproxy library, with very little performance loss. waf_gate itself has a simple routing distribution function, if the website deployment is very simple, there is no business routing, you can directly distribute to the next level of business server through waf-gate. If the routing is more complex, then waf_gate directly forwards to the next level of proxy (such as NGINX) for routing distribution; in this process, waf_gate is completely transparent to the entire business chain. 13 | 14 | waf detection is done by forwarding waf_gate to waf_server, and then waf_server returns the detection result to waf_gate to do interception or release operation. The main reasons for not doing rules directly on waf_gate are based on the following considerations: 15 | 16 | 1. If the detection rules are too complex, especially in the case of containing a lot of regular expressions, CPU time consumption will be higher, affecting waf_gate's forwarding time, thus increasing the business time of the entire link; 17 | 2. waf detection may cache a lot of data, resulting in large memory, GC time is too long; 18 | 3. Some rules require data from multiple httpproxies to be aggregated and then processed; this way httpProxy is powerless; 19 | 4. In the actual application scenario, request and response data may need to be stored; 20 | 21 | 22 | 23 | waf_gate's supported features: 24 | 25 | - Lightweight, performance, RT loss is very small; 26 | - Support rewriting request header, response header, response tail; 27 | 28 | 29 | 30 | **waf_gate and waf_server network communication**, is implemented by [tcpstream](https://github.com/deph) library 31 | 32 | - waf_gate and waf_server are connected by tcp long chain; 33 | - waf_gate and waf_server use multiple-to-multiple connections, gate sends to server detection by polling strategy; 34 | - waf_gate supports detection timeout setting, if the network or other abnormality causes the request to not reply in time, then waf_gate automatically let go; 35 | - waf_gate starts automatic reconnection function, if there is no currently available waf-server request, then let go of all requests; 36 | 37 | **Rule function** 38 | 39 | - Support IP black and white list; 40 | 41 | - Based on Host,Referer,Url,User-Agent,Content-Type regular expression combination rules, each field supports empty; 42 | 43 | - rule rules are configured by JSON format, which is convenient for subsequent expansion of interface rules and GUI development; 44 | 45 | For example, intercept request: Host: www.xxx.com; Refer is empty; User-Agent contains nmap keyword; 46 | 47 | ## 1. Installation 48 | 49 | ``` 50 | git clone https://github.com/kumustone/waf.git 51 | cd waf 52 | ./build.sh 53 | 54 | ``` 55 | 56 | 57 | After installation, two bin files waf-gate waf-server will be generated 58 | 59 | ## 2. Run 60 | 61 | > waf-gate -c /YourConfigPath/waf_gate.conf -l /YourLogPath/log 62 | 63 | > waf-server -c /YouConfigPaht/waf_server.conf -l /YourLogPath/log -r /YourRuleDir 64 | 65 | ## 3. Configuration instructions 66 | 67 | waf-gate 68 | 69 | ```go 70 | [gate] 71 | #Gate Http Listen Address 72 | GateHttpAddress = "0.0.0.0:80" 73 | 74 | # Gate Https Listen Addresses 75 | StartHttps = true 76 | GateHttpsAddress = "0.0.0.0:443" 77 | 78 | # Multiple secondary domains use different keys 79 | CertKeyList = [ 80 | [ 81 | "A.xxx.com", 82 | "A.pem", 83 | "A.key" 84 | ], 85 | [ 86 | "B.xxx.com", 87 | "B.pem", 88 | "B.key" 89 | ] 90 | ] 91 | 92 | CertFile = "xxxxxxx.pem" 93 | KeyFile = "xxxxx.key" 94 | 95 | # Gat API Service 96 | GateAPIAddress = "0.0.0.0:2081" 97 | 98 | # Upstream Address, RoundBin 99 | UpstreamList = [ 100 | "1.1.1.1:80" 101 | ] 102 | 103 | # Waf detection item 104 | [wafrpc] 105 | 106 | # Detection switch, if false, all content is not sent to waf-server detection, directly forwarded to upstream processing; 107 | # true: Process according to Checklist rules; 108 | CheckSwitch = true 109 | [wafrpc.CheckList] 110 | # Host to be detected 111 | Include = ["xxxxxxx"] 112 | 113 | # Exclude detection item Host 114 | Exclude = [] 115 | 116 | # Whether to detect Host outside Include and Exclude; 117 | CheckDefault = true 118 | [wafrpc.ServerAddr] 119 | # waf-server address list; if there are multiple, if there are multiple waf-server, forward requests by polling strategy; 120 | Address = ["127.0.0.1:8000"] 121 | 122 | # WafServer interface address 123 | HttpAPIAddress = "127.0.0.1:8001" 124 | ``` 125 | 126 | ## 4. Rule file description 127 | 128 | ip blacklist example 129 | 130 | ``` 131 | { 132 | "rules": [ 133 | { 134 | "type": "IpBlackList", 135 | "action": "add", 136 | "rule_name": "IpBlackList-0", 137 | "iplist": [ 138 | "1.1.1.1", 139 | "2.2.2.2" 140 | ] 141 | } 142 | ] 143 | } 144 | 145 | ``` 146 | 147 | regular rule example url meets regular: \(?\s*\b(alert|prompt|confirm|console\.log)\s*\)?\s*(\(|`) will be intercepted; 148 | 149 | ``` 150 | { 151 | "rules": [ 152 | { 153 | "type": "Group", 154 | "action": "add", 155 | "rule_name": "xss-1", 156 | "desc": "this is a test rule", 157 | "group_rule": [ 158 | { 159 | "field": "Url", 160 | "op": "is", 161 | "empty": false, 162 | "val": "\\(?\\s*\\b(alert|prompt|confirm|console\\.log)\\s*\\)?\\s*(\\(|`)" 163 | } 164 | ] 165 | } 166 | ] 167 | } 168 | 169 | ``` 170 | 171 | Each rule file contains a complete json, different rule files can be superimposed; 172 | The rules currently supported are relatively few, welcome to add some rules; 173 | 174 | 175 | -------------------------------------------------------------------------------- /internal/share/waf_proto_easyjson.go: -------------------------------------------------------------------------------- 1 | // Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. 2 | 3 | package share 4 | 5 | import ( 6 | "encoding/json" 7 | "github.com/mailru/easyjson" 8 | "github.com/mailru/easyjson/jlexer" 9 | "github.com/mailru/easyjson/jwriter" 10 | ) 11 | 12 | // suppress unused package warning 13 | var ( 14 | _ *json.RawMessage 15 | _ *jlexer.Lexer 16 | _ *jwriter.Writer 17 | _ easyjson.Marshaler 18 | ) 19 | 20 | func easyjsonCf9917fDecodeWafRpc(in *jlexer.Lexer, out *WafProxyResp) { 21 | isTopLevel := in.IsStart() 22 | if in.IsNull() { 23 | if isTopLevel { 24 | in.Consumed() 25 | } 26 | in.Skip() 27 | return 28 | } 29 | in.Delim('{') 30 | for !in.IsDelim('}') { 31 | key := in.UnsafeString() 32 | in.WantColon() 33 | if in.IsNull() { 34 | in.Skip() 35 | in.WantComma() 36 | continue 37 | } 38 | switch key { 39 | case "RetCode": 40 | out.RetCode = int(in.Int()) 41 | case "RuleName": 42 | out.RuleName = string(in.String()) 43 | case "Desc": 44 | out.Desc = string(in.String()) 45 | default: 46 | in.SkipRecursive() 47 | } 48 | in.WantComma() 49 | } 50 | in.Delim('}') 51 | if isTopLevel { 52 | in.Consumed() 53 | } 54 | } 55 | func easyjsonCf9917fEncodeWafRpc(out *jwriter.Writer, in WafProxyResp) { 56 | out.RawByte('{') 57 | first := true 58 | _ = first 59 | { 60 | const prefix string = ",\"RetCode\":" 61 | if first { 62 | first = false 63 | out.RawString(prefix[1:]) 64 | } else { 65 | out.RawString(prefix) 66 | } 67 | out.Int(int(in.RetCode)) 68 | } 69 | { 70 | const prefix string = ",\"RuleName\":" 71 | if first { 72 | first = false 73 | out.RawString(prefix[1:]) 74 | } else { 75 | out.RawString(prefix) 76 | } 77 | out.String(string(in.RuleName)) 78 | } 79 | { 80 | const prefix string = ",\"Desc\":" 81 | if first { 82 | first = false 83 | out.RawString(prefix[1:]) 84 | } else { 85 | out.RawString(prefix) 86 | } 87 | out.String(string(in.Desc)) 88 | } 89 | out.RawByte('}') 90 | } 91 | 92 | // MarshalJSON supports json.Marshaler interface 93 | func (v WafProxyResp) MarshalJSON() ([]byte, error) { 94 | w := jwriter.Writer{} 95 | easyjsonCf9917fEncodeWafRpc(&w, v) 96 | return w.Buffer.BuildBytes(), w.Error 97 | } 98 | 99 | // MarshalEasyJSON supports easyjson.Marshaler interface 100 | func (v WafProxyResp) MarshalEasyJSON(w *jwriter.Writer) { 101 | easyjsonCf9917fEncodeWafRpc(w, v) 102 | } 103 | 104 | // UnmarshalJSON supports json.Unmarshaler interface 105 | func (v *WafProxyResp) UnmarshalJSON(data []byte) error { 106 | r := jlexer.Lexer{Data: data} 107 | easyjsonCf9917fDecodeWafRpc(&r, v) 108 | return r.Error() 109 | } 110 | 111 | // UnmarshalEasyJSON supports easyjson.Unmarshaler interface 112 | func (v *WafProxyResp) UnmarshalEasyJSON(l *jlexer.Lexer) { 113 | easyjsonCf9917fDecodeWafRpc(l, v) 114 | } 115 | func easyjsonCf9917fDecodeWafRpc1(in *jlexer.Lexer, out *WafHttpRequest) { 116 | isTopLevel := in.IsStart() 117 | if in.IsNull() { 118 | if isTopLevel { 119 | in.Consumed() 120 | } 121 | in.Skip() 122 | return 123 | } 124 | in.Delim('{') 125 | for !in.IsDelim('}') { 126 | key := in.UnsafeString() 127 | in.WantColon() 128 | if in.IsNull() { 129 | in.Skip() 130 | in.WantComma() 131 | continue 132 | } 133 | switch key { 134 | case "Mark": 135 | out.Mark = string(in.String()) 136 | case "Method": 137 | out.Method = string(in.String()) 138 | case "Scheme": 139 | out.Scheme = string(in.String()) 140 | case "Url": 141 | out.Url = string(in.String()) 142 | case "Proto": 143 | out.Proto = string(in.String()) 144 | case "Host": 145 | out.Host = string(in.String()) 146 | case "RemoteAddr": 147 | out.RemoteAddr = string(in.String()) 148 | case "ContentLength": 149 | out.ContentLength = uint64(in.Uint64()) 150 | case "Header": 151 | if in.IsNull() { 152 | in.Skip() 153 | } else { 154 | in.Delim('{') 155 | if !in.IsDelim('}') { 156 | out.Header = make(map[string][]string) 157 | } else { 158 | out.Header = nil 159 | } 160 | for !in.IsDelim('}') { 161 | key := string(in.String()) 162 | in.WantColon() 163 | var v1 []string 164 | if in.IsNull() { 165 | in.Skip() 166 | v1 = nil 167 | } else { 168 | in.Delim('[') 169 | if v1 == nil { 170 | if !in.IsDelim(']') { 171 | v1 = make([]string, 0, 4) 172 | } else { 173 | v1 = []string{} 174 | } 175 | } else { 176 | v1 = (v1)[:0] 177 | } 178 | for !in.IsDelim(']') { 179 | var v2 string 180 | v2 = string(in.String()) 181 | v1 = append(v1, v2) 182 | in.WantComma() 183 | } 184 | in.Delim(']') 185 | } 186 | (out.Header)[key] = v1 187 | in.WantComma() 188 | } 189 | in.Delim('}') 190 | } 191 | case "Body": 192 | if in.IsNull() { 193 | in.Skip() 194 | out.Body = nil 195 | } else { 196 | out.Body = in.Bytes() 197 | } 198 | default: 199 | in.SkipRecursive() 200 | } 201 | in.WantComma() 202 | } 203 | in.Delim('}') 204 | if isTopLevel { 205 | in.Consumed() 206 | } 207 | } 208 | func easyjsonCf9917fEncodeWafRpc1(out *jwriter.Writer, in WafHttpRequest) { 209 | out.RawByte('{') 210 | first := true 211 | _ = first 212 | { 213 | const prefix string = ",\"Mark\":" 214 | if first { 215 | first = false 216 | out.RawString(prefix[1:]) 217 | } else { 218 | out.RawString(prefix) 219 | } 220 | out.String(string(in.Mark)) 221 | } 222 | { 223 | const prefix string = ",\"Method\":" 224 | if first { 225 | first = false 226 | out.RawString(prefix[1:]) 227 | } else { 228 | out.RawString(prefix) 229 | } 230 | out.String(string(in.Method)) 231 | } 232 | { 233 | const prefix string = ",\"Scheme\":" 234 | if first { 235 | first = false 236 | out.RawString(prefix[1:]) 237 | } else { 238 | out.RawString(prefix) 239 | } 240 | out.String(string(in.Scheme)) 241 | } 242 | { 243 | const prefix string = ",\"Url\":" 244 | if first { 245 | first = false 246 | out.RawString(prefix[1:]) 247 | } else { 248 | out.RawString(prefix) 249 | } 250 | out.String(string(in.Url)) 251 | } 252 | { 253 | const prefix string = ",\"Proto\":" 254 | if first { 255 | first = false 256 | out.RawString(prefix[1:]) 257 | } else { 258 | out.RawString(prefix) 259 | } 260 | out.String(string(in.Proto)) 261 | } 262 | { 263 | const prefix string = ",\"Host\":" 264 | if first { 265 | first = false 266 | out.RawString(prefix[1:]) 267 | } else { 268 | out.RawString(prefix) 269 | } 270 | out.String(string(in.Host)) 271 | } 272 | { 273 | const prefix string = ",\"RemoteAddr\":" 274 | if first { 275 | first = false 276 | out.RawString(prefix[1:]) 277 | } else { 278 | out.RawString(prefix) 279 | } 280 | out.String(string(in.RemoteAddr)) 281 | } 282 | { 283 | const prefix string = ",\"ContentLength\":" 284 | if first { 285 | first = false 286 | out.RawString(prefix[1:]) 287 | } else { 288 | out.RawString(prefix) 289 | } 290 | out.Uint64(uint64(in.ContentLength)) 291 | } 292 | { 293 | const prefix string = ",\"Header\":" 294 | if first { 295 | first = false 296 | out.RawString(prefix[1:]) 297 | } else { 298 | out.RawString(prefix) 299 | } 300 | if in.Header == nil && (out.Flags&jwriter.NilMapAsEmpty) == 0 { 301 | out.RawString(`null`) 302 | } else { 303 | out.RawByte('{') 304 | v4First := true 305 | for v4Name, v4Value := range in.Header { 306 | if v4First { 307 | v4First = false 308 | } else { 309 | out.RawByte(',') 310 | } 311 | out.String(string(v4Name)) 312 | out.RawByte(':') 313 | if v4Value == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { 314 | out.RawString("null") 315 | } else { 316 | out.RawByte('[') 317 | for v5, v6 := range v4Value { 318 | if v5 > 0 { 319 | out.RawByte(',') 320 | } 321 | out.String(string(v6)) 322 | } 323 | out.RawByte(']') 324 | } 325 | } 326 | out.RawByte('}') 327 | } 328 | } 329 | { 330 | const prefix string = ",\"Body\":" 331 | if first { 332 | first = false 333 | out.RawString(prefix[1:]) 334 | } else { 335 | out.RawString(prefix) 336 | } 337 | out.Base64Bytes(in.Body) 338 | } 339 | out.RawByte('}') 340 | } 341 | 342 | // MarshalJSON supports json.Marshaler interface 343 | func (v WafHttpRequest) MarshalJSON() ([]byte, error) { 344 | w := jwriter.Writer{} 345 | easyjsonCf9917fEncodeWafRpc1(&w, v) 346 | return w.Buffer.BuildBytes(), w.Error 347 | } 348 | 349 | // MarshalEasyJSON supports easyjson.Marshaler interface 350 | func (v WafHttpRequest) MarshalEasyJSON(w *jwriter.Writer) { 351 | easyjsonCf9917fEncodeWafRpc1(w, v) 352 | } 353 | 354 | // UnmarshalJSON supports json.Unmarshaler interface 355 | func (v *WafHttpRequest) UnmarshalJSON(data []byte) error { 356 | r := jlexer.Lexer{Data: data} 357 | easyjsonCf9917fDecodeWafRpc1(&r, v) 358 | return r.Error() 359 | } 360 | 361 | // UnmarshalEasyJSON supports easyjson.Unmarshaler interface 362 | func (v *WafHttpRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { 363 | easyjsonCf9917fDecodeWafRpc1(l, v) 364 | } 365 | --------------------------------------------------------------------------------