├── 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 | 
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 | 
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 |
--------------------------------------------------------------------------------