├── image
└── ui.jpg
├── kafka
├── config.go
├── kafka_test.go
└── kafka.go
├── .github
└── dependabot.yml
├── flow
├── model.go
├── static
│ └── react.production.min.js
├── flow.go
└── index.html
├── http
├── http_test.go
└── http.go
├── zlog
├── zlog_test.go
├── config.go
└── zlog.go
├── utils
├── utils_test.go
└── utils.go
├── .gitignore
├── config.toml
├── conf
└── conf.go
├── Makefile
├── notify
├── notify.go
├── wecom_test.go
├── mail_test.go
├── wecom.go
└── mail.go
├── go.mod
├── main.go
├── README.md
├── LICENSE
└── go.sum
/image/ui.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xxddpac/go-flow/HEAD/image/ui.jpg
--------------------------------------------------------------------------------
/kafka/config.go:
--------------------------------------------------------------------------------
1 | package kafka
2 |
3 | type Config struct {
4 | Enable bool
5 | Brokers []string
6 | Topic string
7 | Size int
8 | }
9 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: gomod
4 | directory: /
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 | ignore:
9 | - dependency-name: github.com/Shopify/sarama
10 | versions: ["*"]
11 |
--------------------------------------------------------------------------------
/flow/model.go:
--------------------------------------------------------------------------------
1 | package flow
2 |
3 | func paginate(pageNum int, pageSize int, sliceLength int) (start, end int) {
4 | start = (pageNum - 1) * pageSize
5 | if start > sliceLength {
6 | start = sliceLength
7 | }
8 | end = start + pageSize
9 | if end > sliceLength {
10 | end = sliceLength
11 | }
12 | return
13 | }
14 |
15 | type Base struct {
16 | Total int `json:"total"`
17 | Pages int `json:"pages"`
18 | Page int `json:"page"`
19 | Size int `json:"size"`
20 | }
21 |
--------------------------------------------------------------------------------
/http/http_test.go:
--------------------------------------------------------------------------------
1 | package http
2 |
3 | import "testing"
4 |
5 | func TestClient(t *testing.T) {
6 | var (
7 | err error
8 | respGet, respPost []byte
9 | cli = NewClient("")
10 | )
11 | if respGet, err = cli.Get("https://httpbin.org/get"); err != nil {
12 | t.Fatal(err)
13 | }
14 | t.Log(string(respGet))
15 |
16 | if respPost, err = cli.Post("https://httpbin.org/post", nil, nil); err != nil {
17 | t.Fatal(err)
18 | }
19 | t.Log(string(respPost))
20 | }
21 |
--------------------------------------------------------------------------------
/zlog/zlog_test.go:
--------------------------------------------------------------------------------
1 | package zlog
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | var fakeConfig = Config{
8 | LogLevel: "debug",
9 | Compress: true,
10 | MaxSize: 10,
11 | MaxBackups: 5,
12 | MaxAge: 30,
13 | Format: FormatJSON,
14 | }
15 |
16 | func TestLog(t *testing.T) {
17 | Init(NewZLog(&fakeConfig))
18 | defer Close()
19 | Infof("info", "test %s", "info")
20 | Debugf("debug", "test %s", "debug")
21 | Warnf("warn", "test %s", "warn")
22 | Errorf("error", "test %s", "error")
23 | }
24 |
--------------------------------------------------------------------------------
/utils/utils_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | )
7 |
8 | func TestIsValidIP(t *testing.T) {
9 | fmt.Println(IsValidIP("1.2.2.1"))
10 | }
11 |
12 | func TestGetCpuUsage(t *testing.T) {
13 | fmt.Println(GetCpuUsage())
14 | }
15 |
16 | func TestGetLocalIp(t *testing.T) {
17 | fmt.Println(GetLocalIp())
18 | }
19 |
20 | func TestGetMemUsage(t *testing.T) {
21 | fmt.Println(GetMemUsage())
22 | }
23 |
24 | func TestGetTimeRangeString(t *testing.T) {
25 | fmt.Println(GetTimeRangeString(1))
26 | }
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
2 | *.o
3 | *.a
4 | *.so
5 | *.out
6 | *.log
7 |
8 | # Folders
9 | _obj
10 | _test
11 | src
12 | data
13 | vendor
14 |
15 | # Architecture specific extensions/prefixes
16 | *.[568vq]
17 | [568vq].out
18 |
19 | *.cgo1.go
20 | *.cgo2.c
21 | _cgo_defun.c
22 | _cgo_gotypes.go
23 | _cgo_export.*
24 | _testmain.go
25 |
26 | *.test
27 | *.prof
28 |
29 | # development & test config files
30 | *.development.yml
31 | *.development.json
32 | *.test.yml
33 | *.test.json
34 |
35 |
36 | # temp files
37 | .idea
38 | .DS_Store
39 | pid
40 |
--------------------------------------------------------------------------------
/zlog/config.go:
--------------------------------------------------------------------------------
1 | package zlog
2 |
3 | const (
4 | FormatText = "text"
5 | FormatJSON = "json"
6 | )
7 |
8 | type Config struct {
9 | LogPath string
10 | LogLevel string
11 | Compress bool
12 | MaxSize int
13 | MaxAge int
14 | MaxBackups int
15 | Format string
16 | }
17 |
18 | func (c *Config) fillWithDefault() {
19 | if c.MaxSize <= 0 {
20 | c.MaxSize = 10
21 | }
22 | if c.MaxAge <= 0 {
23 | c.MaxAge = 7
24 | }
25 | if c.MaxBackups <= 0 {
26 | c.MaxBackups = 3
27 | }
28 | if c.LogLevel == "" {
29 | c.LogLevel = "debug"
30 | }
31 | if c.Format == "" {
32 | c.Format = FormatText
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/kafka/kafka_test.go:
--------------------------------------------------------------------------------
1 | package kafka
2 |
3 | import (
4 | "fmt"
5 | "go-flow/utils"
6 | "go-flow/zlog"
7 | "testing"
8 | "time"
9 | )
10 |
11 | var (
12 | fakeKafkaConfig = &Config{
13 | Enable: true,
14 | Brokers: []string{"localhost:9092"},
15 | Topic: "go-flow",
16 | Size: 100,
17 | }
18 | fakeLogConfig = &zlog.Config{
19 | LogLevel: "debug",
20 | Compress: true,
21 | MaxSize: 10,
22 | MaxBackups: 5,
23 | MaxAge: 30,
24 | Format: "json",
25 | }
26 | )
27 |
28 | func TestKafka(t *testing.T) {
29 | zlog.Init(zlog.NewZLog(fakeLogConfig))
30 | if err = Init(fakeKafkaConfig); err != nil {
31 | t.Fatal(err)
32 | }
33 | go func() {
34 | for {
35 | Push([]byte(fmt.Sprintf("test %s", time.Now().Format(utils.TimeLayout))))
36 | time.Sleep(10 * time.Millisecond)
37 | }
38 | }()
39 | <-time.After(5 * time.Second)
40 | utils.Cancel()
41 | Close()
42 | }
43 |
--------------------------------------------------------------------------------
/config.toml:
--------------------------------------------------------------------------------
1 | [server]
2 | Workers = 5000 # 解析数据包协程数
3 | Size = 10 # 滑动窗口大小(分钟)
4 | Rank = 30 # 滑动窗口内的流量排名数
5 | PacketChan = 500000 # 数据包通道大小
6 | Interval = 2 # 缓存更新间隔(秒),用于页面数据支撑
7 | Eth = "em1" # 网卡名称
8 | Port = 31415 # WEB监听端口
9 |
10 |
11 | [notify]
12 | Enable = false # 是否启用通知,true启用
13 | ThresholdValue = 10 # 大流量告警阈值
14 | ThresholdUnit = "GB" # 大流量告警单位,GB或MB
15 | FrequencyThreshold = 10000 # 高频请求告警阈值
16 | WhiteList = ["", ""] # 告警忽略白名单IP
17 | Location = "IDC" # 流量镜像服务器位置
18 |
19 | [wecom]
20 | Enable = false # 是否启用企业微信通知,true启用
21 | WebHook = "" # 企业微信机器人Webhook地址
22 |
23 | [mail]
24 | Enable = false # 是否启用邮件通知,true启用
25 | SmtpHost = "" # SMTP服务器地址
26 | SmtpPort = 25 # SMTP服务器端口
27 | Username = "" # SMTP用户名
28 | Password = "" # SMTP密码
29 | To = "" # 收件人地址
30 | From = "" # 发件人地址
31 | Cc = [""] # 抄送人地址
32 |
33 | [kafka]
34 | Enable = false # 是否启用Kafka,true启用
35 | Brokers = ["", "", ""] # brokers集群地址
36 | Topic = "" # topic名称
37 | Size = 500000 # Kafka消息队列大小
38 |
39 | [log]
40 | LogPath = "/var/log/go_flow.log" # 日志路径,为空将输出控制台
41 | LogLevel = "debug" # 日志级别
42 | MaxSize = 10 # 日志文件大小(MB)
43 | Compress = true # 是否压缩日志文件
44 | MaxAge = 7 # 日志文件保存天数
45 | MaxBackups = 10 # 日志文件保留个数
46 | Format = "json" # 日志格式,json或text
--------------------------------------------------------------------------------
/conf/conf.go:
--------------------------------------------------------------------------------
1 | package conf
2 |
3 | import (
4 | "fmt"
5 | "github.com/BurntSushi/toml"
6 | "go-flow/kafka"
7 | "go-flow/zlog"
8 | "os"
9 | )
10 |
11 | var (
12 | CoreConf *config
13 | )
14 |
15 | func Init(conf string) {
16 | _, err := toml.DecodeFile(conf, &CoreConf)
17 | if err != nil {
18 | fmt.Printf("Err %v", err)
19 | os.Exit(1)
20 | }
21 | }
22 |
23 | type config struct {
24 | Server Server
25 | Mail Mail
26 | WeCom WeCom
27 | Notify Notify
28 | Kafka kafka.Config
29 | Log zlog.Config
30 | }
31 |
32 | type Server struct {
33 | Port int
34 | Workers int
35 | Size int
36 | Rank int
37 | Eth string
38 | Interval int
39 | PacketChan int
40 | }
41 |
42 | type WeCom struct {
43 | Enable bool
44 | WebHook string
45 | }
46 |
47 | type Mail struct {
48 | Enable bool
49 | SmtpPort int
50 | SmtpHost string
51 | Username string
52 | Password string
53 | From string
54 | To string
55 | Cc []string
56 | }
57 |
58 | type Notify struct {
59 | Enable bool
60 | ThresholdValue float64
61 | ThresholdUnit string
62 | Whitelist []string
63 | Location string
64 | FrequencyThreshold int
65 | }
66 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | APP_NAME = go-flow
2 | SRC = .
3 | BIN_DIR = bin
4 | BINARY_LINUX = $(BIN_DIR)/$(APP_NAME)
5 | GO = go
6 |
7 | LIBPCAP_VERSION = 1.10.1
8 | LIBPCAP_TAR = libpcap-$(LIBPCAP_VERSION).tar.gz
9 | LIBPCAP_DIR = libpcap-$(LIBPCAP_VERSION)
10 | LIBPCAP_URL = https://www.tcpdump.org/release/$(LIBPCAP_TAR)
11 | LIBPCAP_PREFIX = /usr/local
12 |
13 | .PHONY: all
14 | all: build
15 |
16 | .PHONY: install-build-deps
17 | install-build-deps:
18 | @echo "Installing build dependencies..."
19 | @sudo yum install -y gcc make autoconf automake libtool wget curl flex bison || true
20 |
21 | .PHONY: install-libpcap-static
22 | install-libpcap-static: install-build-deps
23 | @if [ ! -f "$(LIBPCAP_PREFIX)/lib/libpcap.a" ]; then \
24 | echo "libpcap static library not found, building from source..."; \
25 | if [ ! -d "$(LIBPCAP_DIR)" ]; then \
26 | echo "Downloading libpcap..."; \
27 | curl -LO $(LIBPCAP_URL); \
28 | tar xzf $(LIBPCAP_TAR); \
29 | fi; \
30 | cd $(LIBPCAP_DIR) && ./configure --enable-static --disable-shared --prefix=$(LIBPCAP_PREFIX) && make && sudo make install; \
31 | else \
32 | echo "libpcap static library already installed."; \
33 | fi
34 |
35 | .PHONY: build
36 | build: install-libpcap-static
37 | @echo "Building $(APP_NAME) for Linux (static)..."
38 | @mkdir -p $(BIN_DIR)
39 | @CGO_LDFLAGS="-L$(LIBPCAP_PREFIX)/lib" CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \
40 | $(GO) build -a -ldflags '-extldflags "-static"' -o $(BINARY_LINUX) $(SRC)
41 |
42 | .PHONY: clean
43 | clean:
44 | @echo "Cleaning..."
45 | @rm -rf $(BIN_DIR) $(LIBPCAP_DIR) $(LIBPCAP_TAR)
46 |
--------------------------------------------------------------------------------
/kafka/kafka.go:
--------------------------------------------------------------------------------
1 | package kafka
2 |
3 | import (
4 | "github.com/Shopify/sarama"
5 | "go-flow/utils"
6 | "go-flow/zlog"
7 | "time"
8 | )
9 |
10 | var (
11 | producer sarama.AsyncProducer
12 | err error
13 | Queue chan []byte
14 | )
15 |
16 | func Init(config *Config) error {
17 | kafkaConfig := sarama.NewConfig()
18 | kafkaConfig.Producer.Timeout = 5 * time.Second
19 | kafkaConfig.Producer.Return.Successes = true
20 | kafkaConfig.Producer.MaxMessageBytes = 1024 * 1024 * 10
21 | kafkaConfig.Producer.Compression = sarama.CompressionSnappy
22 | producer, err = sarama.NewAsyncProducer(config.Brokers, kafkaConfig)
23 | if err != nil {
24 | return err
25 | }
26 | Queue = make(chan []byte, config.Size)
27 | go func() {
28 | for {
29 | select {
30 | case msg := <-producer.Successes():
31 | //value, _ := msg.Value.Encode()
32 | //zlog.Infof("Kafka", "topic: %s, partition: %d, offset: %d, value: %s", msg.Topic, msg.Partition, msg.Offset, string(value))
33 | _ = msg
34 | case err = <-producer.Errors():
35 | zlog.Errorf("Kafka", "Failed to send message: %s", err.Error())
36 | case <-utils.Ctx.Done():
37 | return
38 | }
39 | }
40 | }()
41 | go func() {
42 | for {
43 | select {
44 | case msg := <-Queue:
45 | producer.Input() <- &sarama.ProducerMessage{Topic: config.Topic, Value: sarama.StringEncoder(msg)}
46 | case <-utils.Ctx.Done():
47 | return
48 | }
49 | }
50 | }()
51 | return nil
52 | }
53 |
54 | func Push(msg []byte) {
55 | select {
56 | case Queue <- msg:
57 | default:
58 | }
59 | }
60 |
61 | func Close() {
62 | if producer == nil {
63 | return
64 | }
65 | producer.AsyncClose()
66 | }
67 |
--------------------------------------------------------------------------------
/notify/notify.go:
--------------------------------------------------------------------------------
1 | package notify
2 |
3 | import (
4 | "go-flow/conf"
5 | "go-flow/utils"
6 | "go-flow/zlog"
7 | "sync"
8 | )
9 |
10 | type Notify interface {
11 | Send(d DdosAlert) error
12 | }
13 |
14 | var (
15 | _ Notify = (*Mail)(nil)
16 | _ Notify = (*WeCom)(nil)
17 | )
18 |
19 | var (
20 | ns = make([]Notify, 0, 10)
21 | ddosChan = make(chan DdosAlert, 100)
22 | whitelist []string
23 | )
24 |
25 | func Init(wg *sync.WaitGroup) {
26 | defer wg.Done()
27 | whitelist = conf.CoreConf.Notify.Whitelist
28 | if conf.CoreConf.Mail.Enable {
29 | ns = append(ns, &Mail{})
30 | }
31 | if conf.CoreConf.WeCom.Enable {
32 | ns = append(ns, &WeCom{})
33 | }
34 | for {
35 | select {
36 | case <-utils.Ctx.Done():
37 | close(ddosChan)
38 | return
39 | case d := <-ddosChan:
40 | for _, n := range ns {
41 | if err := n.Send(d); err != nil {
42 | zlog.Errorf("Notify", "send notify error: %v", err)
43 | }
44 | }
45 | }
46 | }
47 | }
48 |
49 | type Bandwidth struct {
50 | IP string
51 | Bandwidth string
52 | }
53 |
54 | type Frequency struct {
55 | IP string
56 | Count int
57 | Desc string
58 | }
59 |
60 | type DdosAlert struct {
61 | BandwidthS []Bandwidth
62 | FrequencyS []Frequency
63 | Title string
64 | Timestamp string
65 | Location string
66 | TimeRange string
67 | }
68 |
69 | func Push(d DdosAlert) {
70 | select {
71 | case ddosChan <- d:
72 | default:
73 | zlog.Warnf("Notify", "notify queue is full, dropping message")
74 | }
75 | }
76 |
77 | func IsWhiteIp(ip string) bool {
78 | for index := range whitelist {
79 | if whitelist[index] == ip {
80 | return true
81 | }
82 | }
83 | return false
84 | }
85 |
--------------------------------------------------------------------------------
/notify/wecom_test.go:
--------------------------------------------------------------------------------
1 | package notify
2 |
3 | import (
4 | "go-flow/conf"
5 | "go-flow/utils"
6 | "testing"
7 | "time"
8 | )
9 |
10 | func TestWeCom(t *testing.T) {
11 | conf.Init("../config.toml")
12 | var (
13 | wc = &WeCom{}
14 | bws = make([]Bandwidth, 0, 10)
15 | fqs = make([]Frequency, 0, 10)
16 | )
17 | bws = append(bws,
18 | Bandwidth{IP: "10.188.61.20", Bandwidth: "2GB"},
19 | Bandwidth{IP: "10.188.61.21", Bandwidth: "700MB"},
20 | Bandwidth{IP: "10.188.61.22", Bandwidth: "500MB"},
21 | Bandwidth{IP: "10.188.61.29", Bandwidth: "400MB"},
22 | Bandwidth{IP: "221.133.2.20", Bandwidth: "400MB"})
23 | fqs = append(fqs,
24 | Frequency{Desc: "检测到源IP 10.188.61.20 在最近1分钟内尝试连接 10000 个不同目标 IP,疑似自动化探测等异常行为"},
25 | Frequency{Desc: "检测到源IP 10.188.61.21 在最近1分钟内尝试连接 10000 个不同目标 IP,疑似自动化探测等异常行为"},
26 | Frequency{Desc: "检测到源IP 10.188.61.22 在最近1分钟内尝试连接 10000 个不同目标 IP,疑似自动化探测等异常行为"},
27 | Frequency{Desc: "检测到目标IP 221.133.2.19 在最近1分钟内接收来自 10000 个不同源IP访问请求,疑似分布式拒绝服务等异常行为"},
28 | Frequency{Desc: "检测到目标IP 221.133.2.20 在最近1分钟内接收来自 10000 个不同源IP访问请求,疑似分布式拒绝服务等异常行为"},
29 | Frequency{Desc: "检测到目标IP 221.133.2.21 在最近1分钟内接收来自 10000 个不同源IP访问请求,疑似分布式拒绝服务等异常行为"},
30 | )
31 | err := wc.Send(DdosAlert{
32 | Title: "大流量预警",
33 | Location: "办公网",
34 | Timestamp: time.Now().Format(utils.TimeLayout),
35 | BandwidthS: bws,
36 | TimeRange: utils.GetTimeRangeString(5),
37 | })
38 | if err != nil {
39 | t.Fatal(err)
40 | }
41 | err = wc.Send(DdosAlert{
42 | Title: "高频请求预警",
43 | Location: "办公网",
44 | Timestamp: time.Now().Format(utils.TimeLayout),
45 | TimeRange: utils.GetTimeRangeString(1),
46 | FrequencyS: fqs})
47 | if err != nil {
48 | t.Fatal(err)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/notify/mail_test.go:
--------------------------------------------------------------------------------
1 | package notify
2 |
3 | import (
4 | "go-flow/conf"
5 | "go-flow/utils"
6 | "testing"
7 | "time"
8 | )
9 |
10 | func TestMail(t *testing.T) {
11 | conf.Init("../config.toml")
12 | var (
13 | mail = &Mail{}
14 | bws = make([]Bandwidth, 0, 10)
15 | fqs = make([]Frequency, 0, 10)
16 | )
17 | bws = append(bws,
18 | Bandwidth{IP: "10.188.61.20", Bandwidth: "2GB"},
19 | Bandwidth{IP: "10.188.61.21", Bandwidth: "700MB"},
20 | Bandwidth{IP: "10.188.61.22", Bandwidth: "500MB"},
21 | Bandwidth{IP: "10.188.61.29", Bandwidth: "400MB"},
22 | Bandwidth{IP: "221.133.2.20", Bandwidth: "400MB"})
23 | fqs = append(fqs,
24 | Frequency{Desc: "检测到源IP 10.188.61.20 在最近1分钟内尝试连接 10000 个不同目标 IP,疑似自动化探测等异常行为"},
25 | Frequency{Desc: "检测到源IP 10.188.61.21 在最近1分钟内尝试连接 10000 个不同目标 IP,疑似自动化探测等异常行为"},
26 | Frequency{Desc: "检测到源IP 10.188.61.22 在最近1分钟内尝试连接 10000 个不同目标 IP,疑似自动化探测等异常行为"},
27 | Frequency{Desc: "检测到目标IP 221.133.2.19 在最近1分钟内接收来自 10000 个不同源IP访问请求,疑似分布式拒绝服务等异常行为"},
28 | Frequency{Desc: "检测到目标IP 221.133.2.20 在最近1分钟内接收来自 10000 个不同源IP访问请求,疑似分布式拒绝服务等异常行为"},
29 | Frequency{Desc: "检测到目标IP 221.133.2.21 在最近1分钟内接收来自 10000 个不同源IP访问请求,疑似分布式拒绝服务等异常行为"},
30 | )
31 | err := mail.Send(DdosAlert{
32 | Title: "大流量预警",
33 | Location: "办公网",
34 | Timestamp: time.Now().Format(utils.TimeLayout),
35 | BandwidthS: bws,
36 | TimeRange: utils.GetTimeRangeString(5),
37 | })
38 | if err != nil {
39 | t.Fatal(err)
40 | }
41 | err = mail.Send(DdosAlert{
42 | Title: "高频请求预警",
43 | Location: "办公网",
44 | Timestamp: time.Now().Format(utils.TimeLayout),
45 | TimeRange: utils.GetTimeRangeString(1),
46 | FrequencyS: fqs})
47 | if err != nil {
48 | t.Fatal(err)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/notify/wecom.go:
--------------------------------------------------------------------------------
1 | package notify
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "go-flow/conf"
8 | "go-flow/http"
9 | "text/template"
10 | )
11 |
12 | const WcComTemplate = "【GO-FLOW】 `**{{if .Title}}{{.Title}}{{end}}**`\n" +
13 | "{{if .Timestamp}}>告警时间:{{.Timestamp}}\n{{end}}" +
14 | "{{if .Location}}>所属位置:{{.Location}}\n{{end}}" +
15 | "{{if .TimeRange}}>滑动窗口:{{.TimeRange}}\n{{end}}" +
16 | "**== 告警详情 ==**\n" +
17 | "{{if .BandwidthS}}" +
18 | "{{range $index, $alert := .BandwidthS}}" +
19 | "{{add $index 1}} 异常IP {{$alert.IP}}, 使用流量 {{$alert.Bandwidth}}\n" +
20 | "{{end}}" +
21 | "{{end}}" +
22 | "{{if .FrequencyS}}" +
23 | "{{range $index, $alert := .FrequencyS}}" +
24 | "{{add $index 1}} {{$alert.Desc}}\n" +
25 | "{{end}}" +
26 | "{{end}}"
27 |
28 | var (
29 | request = http.NewClient("")
30 | url string
31 | funcMap = template.FuncMap{
32 | "add": func(a, b int) int {
33 | return a + b
34 | },
35 | }
36 | )
37 |
38 | type Message struct {
39 | MsgType string `json:"msgtype"`
40 | Markdown Markdown `json:"markdown"`
41 | }
42 |
43 | type Markdown struct {
44 | Content string `json:"content"`
45 | }
46 |
47 | type WeCom struct {
48 | }
49 |
50 | type WeComResponse struct {
51 | ErrCode int `json:"errcode"`
52 | ErrMsg string `json:"errmsg"`
53 | }
54 |
55 | func (w *WeCom) Send(d DdosAlert) error {
56 | var (
57 | err error
58 | b []byte
59 | tmpl *template.Template
60 | )
61 | url = conf.CoreConf.WeCom.WebHook
62 | tmpl, err = template.New("alert").Funcs(funcMap).Parse(WcComTemplate)
63 | if err != nil {
64 | return err
65 | }
66 | var msg bytes.Buffer
67 | if err = tmpl.Execute(&msg, d); err != nil {
68 | return err
69 | }
70 | b, err = request.Post(url, &Message{MsgType: "markdown", Markdown: Markdown{msg.String()}})
71 | if err != nil {
72 | return err
73 | }
74 | var weComResponse WeComResponse
75 | if err = json.Unmarshal(b, &weComResponse); err != nil {
76 | return err
77 | }
78 | if weComResponse.ErrCode != 0 {
79 | return fmt.Errorf("wecom send error, code: %d, msg: %s", weComResponse.ErrCode, weComResponse.ErrMsg)
80 | }
81 | return nil
82 | }
83 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module go-flow
2 |
3 | go 1.23.0
4 |
5 | require (
6 | github.com/BurntSushi/toml v1.5.0
7 | github.com/Shopify/sarama v1.24.1
8 | github.com/google/gopacket v1.1.19
9 | github.com/json-iterator/go v1.1.12
10 | github.com/panjf2000/ants/v2 v2.11.3
11 | github.com/shirou/gopsutil/v3 v3.24.5
12 | github.com/spf13/cast v1.9.2
13 | go.uber.org/zap v1.27.0
14 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
15 | gopkg.in/natefinch/lumberjack.v2 v2.2.1
16 | )
17 |
18 | require (
19 | github.com/davecgh/go-spew v1.1.1 // indirect
20 | github.com/eapache/go-resiliency v1.1.0 // indirect
21 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 // indirect
22 | github.com/eapache/queue v1.1.0 // indirect
23 | github.com/go-ole/go-ole v1.2.6 // indirect
24 | github.com/golang/snappy v0.0.1 // indirect
25 | github.com/hashicorp/go-uuid v1.0.1 // indirect
26 | github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03 // indirect
27 | github.com/klauspost/compress v1.8.2 // indirect
28 | github.com/klauspost/cpuid v1.3.1 // indirect
29 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
30 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
31 | github.com/modern-go/reflect2 v1.0.2 // indirect
32 | github.com/pierrec/lz4 v2.2.6+incompatible // indirect
33 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
34 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a // indirect
35 | github.com/shoenig/go-m1cpu v0.1.6 // indirect
36 | github.com/tklauser/go-sysconf v0.3.12 // indirect
37 | github.com/tklauser/numcpus v0.6.1 // indirect
38 | github.com/yusufpapurcu/wmi v1.2.4 // indirect
39 | go.uber.org/multierr v1.10.0 // indirect
40 | golang.org/x/crypto v0.39.0 // indirect
41 | golang.org/x/net v0.41.0 // indirect
42 | golang.org/x/sync v0.11.0 // indirect
43 | golang.org/x/sys v0.33.0 // indirect
44 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
45 | gopkg.in/jcmturner/aescts.v1 v1.0.1 // indirect
46 | gopkg.in/jcmturner/dnsutils.v1 v1.0.1 // indirect
47 | gopkg.in/jcmturner/gokrb5.v7 v7.2.3 // indirect
48 | gopkg.in/jcmturner/rpc.v1 v1.1.0 // indirect
49 | )
50 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "go-flow/conf"
7 | "go-flow/flow"
8 | "go-flow/kafka"
9 | "go-flow/notify"
10 | "go-flow/utils"
11 | "go-flow/zlog"
12 | "log"
13 | "net/http"
14 | _ "net/http/pprof"
15 | "os"
16 | "os/signal"
17 | "sync"
18 | "syscall"
19 | "time"
20 | )
21 |
22 | func main() {
23 | var (
24 | cfg string
25 | wg sync.WaitGroup
26 | )
27 | flag.StringVar(&cfg, "c", "", "config.toml")
28 | flag.Parse()
29 | if len(cfg) == 0 {
30 | fmt.Println("config is empty")
31 | os.Exit(0)
32 | }
33 | conf.Init(cfg)
34 | zlog.Init(zlog.NewZLog(&conf.CoreConf.Log))
35 | window := flow.NewWindow(time.Duration(conf.CoreConf.Server.Size)*time.Minute, conf.CoreConf.Server.Rank)
36 | mux := http.NewServeMux()
37 | mux.Handle("/", window)
38 | server := &http.Server{
39 | Addr: fmt.Sprintf(":%d", conf.CoreConf.Server.Port),
40 | Handler: mux,
41 | }
42 | if conf.CoreConf.Kafka.Enable {
43 | if err := kafka.Init(&conf.CoreConf.Kafka); err != nil {
44 | log.Fatalf("Init kafka failed: %v", err)
45 | }
46 | }
47 | wg.Add(3)
48 | go notify.Init(&wg)
49 | go window.StartCacheUpdate(&wg)
50 | go flow.Capture(conf.CoreConf.Server.Eth, window, &wg)
51 | go func() {
52 | pprofPort := conf.CoreConf.Server.Port - 1
53 | zlog.Infof("Main", "Starting pprof on http://%s:%d/debug/pprof/", utils.LocalIpAddr, pprofPort)
54 | if err := http.ListenAndServe(fmt.Sprintf(":%d", pprofPort), nil); err != nil {
55 | zlog.Errorf("Main", "pprof ListenAndServe Error %s", err.Error())
56 | }
57 | }()
58 | go func() {
59 | log.Printf("Starting HTTP server on http://%s%s\n", utils.LocalIpAddr, server.Addr)
60 | zlog.Infof("Main", "Starting HTTP server on http://%s%s", utils.LocalIpAddr, server.Addr)
61 | if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
62 | zlog.Errorf("Main", "HTTP server ListenAndServe Error %s", err.Error())
63 | }
64 | }()
65 | go func() {
66 | signals := make(chan os.Signal, 1)
67 | signal.Notify(signals, os.Interrupt, syscall.SIGTERM)
68 | select {
69 | case sig := <-signals:
70 | log.Printf("Received signal: %s\n", sig)
71 | <-time.After(time.Second)
72 | _ = server.Shutdown(utils.Ctx)
73 | utils.Cancel()
74 | kafka.Close()
75 | zlog.Close()
76 | }
77 | }()
78 | wg.Wait()
79 | log.Println("+++++ Bye +++++")
80 | }
81 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | 🚀 A Lightweight and High-Performance Network Traffic Analyzer built with Go
3 |
4 |
5 |
6 | ---
7 |
8 | ### 项目简介
9 |
10 | go-flow 是一款轻量级、高性能的网络流量分析工具,基于 Go 语言开发,结合高效的数据处理和低资源占用,适用于边缘计算节点到高吞吐数据中心的多种场景。
11 |
12 | ---
13 |
14 | ### 核心功能
15 |
16 | - 滑动窗口分析:实时统计指定时间窗口内的 IP、端口和协议流量,支持 Top-N 排序,便于快速定位流量热点。
17 | - 异常检测与告警:自动检测大流量、高频请求等异常行为,提供灵活的告警配置。
18 | - Kafka 数据流输出:支持将分析结果异步推送至 Kafka,无缝集成 ELK Stack、Grafana 等可视化平台。
19 |
20 | ---
21 |
22 | ### 安装与使用
23 |
24 | #### 下载 release 包
25 |
26 | 前往 [Releases](https://github.com/xxddpac/go-flow/releases) 下载最新版本压缩文件。
27 |
28 | #### 编辑配置文件
29 |
30 | ```toml
31 | Eth = "eth0" # 监听的网卡名
32 | ```
33 | #### 启动服务
34 | ```
35 | chmod +x go-flow
36 | ./go-flow -c config.toml
37 | ```
38 | #### WEB访问
39 | ```
40 | http://server_ip:31415
41 | ```
42 | ---
43 |
44 | ### ScreenShot
45 |
46 | 
47 |
48 | ---
49 |
50 | ### 源码编译
51 |
52 | ```
53 | git clone https://github.com/xxddpac/go-flow.git
54 | cd go-flow
55 | make build
56 | ```
57 | ---
58 |
59 | ### 部署与性能常见问题
60 |
61 | #### Q: 对主机资源占用情况如何?
62 |
63 | A: `go-flow` 使用了如下优化设计以降低资源占用:
64 |
65 | - 高效数据结构:采用堆排序和滑动窗口机制,高效完成流量聚合。
66 |
67 | - 协程池管理:动态调度 Goroutine,避免资源泄漏。
68 |
69 | - 内存缓存:减少重复计算,提升处理效率。
70 |
71 | #### Q: 能支持多大的网络流量?性能如何?
72 |
73 | A: `go-flow` 使用 Linux 原生的 `AF_PACKET` 零拷贝技术,在内核态完成数据捕获和复制,数据通过共享内存直达用户态:
74 | - 性能上限:理论支持网卡带宽的最大吞吐量。
75 | - 高可靠性:内核态零拷贝确保数据捕获无丢失。
76 |
77 | 在实测网络环境中(4Gbps 带宽、约2万终端),go-flow 运行稳定,未发现丢包。
78 |
79 | > ⚠️ **注意**
80 | > 虽然内核采集是零拷贝高性能的,但在用户态中程序仍需处理流量聚合、滑动窗口维护、堆排序等操作,尤其是在超高并发流量下,如果用户空间处理速度跟不上,仍有概率导致丢包。
81 | >
82 | > 可通过以下方式优化:
83 | > - 增加协程池容量(`config.toml` 中的 `Workers` 参数)
84 | > - 调整生产者通道缓冲区(`config.toml` 中的 `PacketChan` 参数)
85 | > - 延迟缓存更新频率(`config.toml` 中的 `Interval` 参数)
86 | > - 使用 pprof 分析热点函数,默认监听端口`31414`,访问 `http://server_ip:31414/debug/pprof/` 查看性能分析数据
87 |
88 | 另外通过日志可观察丢包情况,默认日志路径`/var/log/go_flow.log`
89 |
90 | #### Q:为什么选择 AF_PACKET 而非 eBPF?
91 |
92 | A: 相较于 `eBPF`,`go-flow` 选择使用 `AF_PACKET` 的原因主要有以下几点:
93 |
94 | - 开发效率:`AF_PACKET` 是 `Linux` 原生抓包接口,配合 `TPACKETv3` 实现零拷贝,无需开发复杂的 `eBPF` 程序,调试更高效。
95 | - 广泛兼容性:支持大多数 `Linux` 系统,无需特定内核版本或额外模块,部署简单可靠。
96 |
97 | #### Q: 为什么最新版只提供了 Linux 版本?
98 |
99 | A: `go-flow` 使用的核心采集机制是 `AF_PACKET`,这是 `Linux` 内核特有的高性能网络捕获机制,不支持 `Windows` 平台。
100 | 如需在 `Windows` 上体验功能,可下载 `v1.2.1` 版本中的 `go_flow_windows.zip`,该版本基于 `libpcap` 构建。
--------------------------------------------------------------------------------
/utils/utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/shirou/gopsutil/v3/cpu"
7 | "github.com/shirou/gopsutil/v3/mem"
8 | "net"
9 | "strings"
10 | "time"
11 | )
12 |
13 | const (
14 | TimeLayout = "2006-01-02 15:04:05"
15 | BroadcastAddress = "255.255.255.255"
16 | )
17 |
18 | var (
19 | Ctx context.Context
20 | Cancel context.CancelFunc
21 | LocalIpAddr string
22 | )
23 |
24 | func init() {
25 | Ctx, Cancel = context.WithCancel(context.Background())
26 | LocalIpAddr = GetLocalIp()
27 | }
28 |
29 | func GetTimeRangeString(minutes int) string {
30 | now := time.Now()
31 | start := now.Add(-time.Duration(minutes * int(time.Minute)))
32 | return fmt.Sprintf("%s - %s",
33 | start.Format("2006-01-02 15:04:05"),
34 | now.Format("2006-01-02 15:04:05"))
35 | }
36 |
37 | func IsValidIP(ip string) bool {
38 | parsedIP := net.ParseIP(ip)
39 | if parsedIP == nil {
40 | return false
41 | }
42 | if parsedIP.IsLoopback() || parsedIP.IsUnspecified() {
43 | return false
44 | }
45 | if parsedIP.IsMulticast() {
46 | return false
47 | }
48 | if ip == BroadcastAddress {
49 | return false
50 | }
51 | if strings.HasPrefix(ip, "169.254.") {
52 | return false
53 | }
54 | return true
55 | }
56 |
57 | func GetLocalIp() string {
58 | var (
59 | err error
60 | localIpAddr string
61 | ifs []net.Interface
62 | as []net.Addr
63 | )
64 | if localIpAddr != "" {
65 | return localIpAddr
66 | }
67 | ifs, err = net.Interfaces()
68 | if err != nil {
69 | return "Unknown"
70 | }
71 | for _, i := range ifs {
72 | if i.Flags&net.FlagUp == 0 {
73 | continue
74 | }
75 | if i.Flags&net.FlagLoopback != 0 {
76 | continue
77 | }
78 | as, err = i.Addrs()
79 | if err != nil {
80 | return "Unknown"
81 | }
82 | for _, addr := range as {
83 | var ip net.IP
84 | switch v := addr.(type) {
85 | case *net.IPNet:
86 | ip = v.IP
87 | case *net.IPAddr:
88 | ip = v.IP
89 | }
90 | if ip == nil || ip.IsLoopback() {
91 | continue
92 | }
93 | ip = ip.To4()
94 |
95 | if ip == nil {
96 | continue
97 | }
98 | localIpAddr = ip.String()
99 | return localIpAddr
100 | }
101 | }
102 | return "Unknown"
103 | }
104 |
105 | func GetCpuUsage() string {
106 | percent, _ := cpu.Percent(time.Second, false)
107 | if len(percent) > 0 {
108 | return fmt.Sprintf("%.2f%%", percent[0])
109 | }
110 | return "Unknown"
111 | }
112 |
113 | func GetMemUsage() string {
114 | v, _ := mem.VirtualMemory()
115 | if v != nil {
116 | return fmt.Sprintf("%.2f%%", v.UsedPercent)
117 | }
118 | return "Unknown"
119 | }
120 |
--------------------------------------------------------------------------------
/notify/mail.go:
--------------------------------------------------------------------------------
1 | package notify
2 |
3 | import (
4 | "bytes"
5 | "crypto/tls"
6 | "fmt"
7 | "go-flow/conf"
8 | "gopkg.in/gomail.v2"
9 | "text/template"
10 | )
11 |
12 | const MailTemplate = `
13 |
14 |
15 |
16 |
17 |
18 | {{.Title}}
19 |
20 |
21 |
22 |
{{.Title}}
23 |
24 | {{if .Timestamp}}
25 |
告警时间:{{.Timestamp}}
26 | {{end}}
27 | {{if .Location}}
28 |
所属位置:{{.Location}}
29 | {{end}}
30 | {{if .TimeRange}}
31 |
滑动窗口:{{.TimeRange}}
32 | {{end}}
33 | {{if .BandwidthS}}
34 |
35 |
36 |
37 | | # |
38 | 异常IP |
39 | 使用流量 |
40 |
41 |
42 |
43 | {{range $index, $alert := .BandwidthS}}
44 |
45 | | {{add $index 1}} |
46 | {{$alert.IP}} |
47 | {{$alert.Bandwidth}} |
48 |
49 | {{end}}
50 |
51 |
52 | {{end}}
53 | {{if .FrequencyS}}
54 |
55 |
56 |
57 | | # |
58 | 描述 |
59 |
60 |
61 |
62 | {{range $index, $alert := .FrequencyS}}
63 |
64 | | {{add $index 1}} |
65 | {{$alert.Desc}} |
66 |
67 | {{end}}
68 |
69 |
70 | {{end}}
71 |
72 |
73 |
74 | `
75 |
76 | type Mail struct {
77 | }
78 |
79 | var (
80 | smtpPort int
81 | smtpHost string
82 | username string
83 | password string
84 | from string
85 | to string
86 | cc []string
87 | )
88 |
89 | func (m *Mail) Send(d DdosAlert) error {
90 | var (
91 | err error
92 | tmpl *template.Template
93 | )
94 | tmpl, err = template.New("html_alert").Funcs(funcMap).Parse(MailTemplate)
95 | if err != nil {
96 | return err
97 | }
98 | var msg bytes.Buffer
99 | if err = tmpl.Execute(&msg, d); err != nil {
100 | return err
101 | }
102 | smtpPort = conf.CoreConf.Mail.SmtpPort
103 | smtpHost = conf.CoreConf.Mail.SmtpHost
104 | username = conf.CoreConf.Mail.Username
105 | password = conf.CoreConf.Mail.Password
106 | from = conf.CoreConf.Mail.From
107 | to = conf.CoreConf.Mail.To
108 | cc = conf.CoreConf.Mail.Cc
109 | mail := gomail.NewMessage()
110 | mail.SetHeader("From", from)
111 | mail.SetHeader("To", to)
112 | mail.SetHeader("Cc", cc...)
113 | mail.SetHeader("Subject", d.Title)
114 | mail.SetBody("text/html", msg.String())
115 | dialer := gomail.NewDialer(smtpHost, smtpPort, username, password)
116 | dialer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
117 | if err = dialer.DialAndSend(mail); err != nil {
118 | return fmt.Errorf("failed to send mail: %w", err)
119 | }
120 | return nil
121 | }
122 |
--------------------------------------------------------------------------------
/http/http.go:
--------------------------------------------------------------------------------
1 | package http
2 |
3 | import (
4 | "bytes"
5 | "compress/gzip"
6 | "context"
7 | "encoding/json"
8 | "fmt"
9 | "io"
10 | "net"
11 | "net/http"
12 | "net/url"
13 | "strings"
14 | "time"
15 | )
16 |
17 | type Header map[string]string
18 |
19 | func defaultHeader() Header {
20 | return Header{
21 | "Content-Type": "application/json;charset=utf-8",
22 | }
23 | }
24 |
25 | func NewClient(baseUrl string, headers ...Header) *Client {
26 | header := defaultHeader()
27 | if 0 != len(headers) {
28 | for key, val := range headers[0] {
29 | header[key] = val
30 | }
31 | }
32 | return &Client{
33 | baseUrl,
34 | header,
35 | newHttpClient(),
36 | }
37 | }
38 |
39 | type Client struct {
40 | baseUrl string
41 | header Header
42 | httpclient *http.Client
43 | }
44 |
45 | func newHttpClient() *http.Client {
46 | transport := &http.Transport{
47 | IdleConnTimeout: 60 * time.Second,
48 | ExpectContinueTimeout: 2 * time.Second,
49 | ResponseHeaderTimeout: 30 * time.Second,
50 | DisableKeepAlives: false,
51 | DisableCompression: false,
52 | TLSHandshakeTimeout: 10 * time.Second,
53 | MaxIdleConnsPerHost: 200,
54 | MaxIdleConns: 2000,
55 | DialContext: (&net.Dialer{
56 | Timeout: 30 * time.Second,
57 | KeepAlive: 30 * time.Second,
58 | }).DialContext,
59 | }
60 |
61 | return &http.Client{
62 | Transport: transport,
63 | Timeout: 100 * time.Second,
64 | }
65 | }
66 |
67 | func (c *Client) Get(uri string, headers ...Header) ([]byte, error) {
68 | return c.Request(http.MethodGet, uri, nil, headers...)
69 | }
70 |
71 | func (c *Client) Post(uri string, body interface{}, headers ...Header) ([]byte, error) {
72 | return c.Request(http.MethodPost, uri, body, headers...)
73 | }
74 |
75 | func (c *Client) RequestWithContext(ctx context.Context, method, uri string, body interface{}, headers ...Header) (string, []byte, error) {
76 | req, err := c.newHttpRequest(method, uri, body, headers...)
77 | if nil != err {
78 | return "", nil, fmt.Errorf("failed build http request, err:%s", err.Error())
79 | }
80 | resp, err := c.httpclient.Do(req.WithContext(ctx))
81 | if nil != err {
82 | return "", nil, fmt.Errorf("failed send http request, err: %s", err.Error())
83 | }
84 | defer func() {
85 | _ = resp.Body.Close()
86 | }()
87 | rBytes, err := c.bytes(resp)
88 | return req.URL.String(), rBytes, err
89 | }
90 |
91 | func (c *Client) Request(method, uri string, body interface{}, headers ...Header) ([]byte, error) {
92 | req, err := c.newHttpRequest(method, uri, body, headers...)
93 | if nil != err {
94 | return nil, fmt.Errorf("failed build http request, err:%s", err.Error())
95 | }
96 | resp, err := c.httpclient.Do(req)
97 | if nil != err {
98 | return nil, fmt.Errorf("failed send http request, err: %s", err.Error())
99 | }
100 | defer func() {
101 | _ = resp.Body.Close()
102 | }()
103 | rBytes, err := c.bytes(resp)
104 | return rBytes, err
105 | }
106 |
107 | func (c *Client) newHttpRequest(method, uri string, body interface{}, headers ...Header) (*http.Request, error) {
108 | var err error
109 | req := &http.Request{
110 | Method: method,
111 | Header: make(http.Header),
112 | Proto: "HTTP/1.1",
113 | ProtoMajor: 1,
114 | ProtoMinor: 1,
115 | }
116 | req.URL, err = c.getUrl(uri)
117 | if nil != err {
118 | return req, fmt.Errorf("failed get request uri, err: %s", err.Error())
119 | }
120 | for k, v := range c.getHeaders(headers...) {
121 | req.Header.Set(k, v)
122 | }
123 | if nil != body {
124 | if err = c.setBody(req, body); nil != err {
125 | return req, fmt.Errorf("failed set request body, err: %s", err.Error())
126 | }
127 | }
128 | return req, nil
129 | }
130 |
131 | func (c *Client) setBody(req *http.Request, body interface{}) error {
132 | var (
133 | err error
134 | buffer []byte
135 | )
136 | if val, ok := body.(io.Reader); ok {
137 | req.Body = io.NopCloser(val)
138 | return nil
139 | }
140 | if val, ok := body.(string); ok {
141 | buffer = []byte(val)
142 | } else if body != nil {
143 | buffer, err = json.Marshal(body)
144 | if err != nil {
145 | return err
146 | }
147 | }
148 | req.Body = io.NopCloser(bytes.NewReader(buffer))
149 | req.ContentLength = int64(len(buffer))
150 | return nil
151 | }
152 |
153 | func (c *Client) getHeaders(headers ...Header) Header {
154 | if 0 != len(headers) {
155 | for key, val := range headers[0] {
156 | c.header[key] = val
157 | }
158 | }
159 | return c.header
160 | }
161 |
162 | func (c *Client) getUrl(uri string) (*url.URL, error) {
163 | if c.baseUrl == "" { // base url is empty, use uri as url
164 | return url.Parse(uri)
165 | }
166 | if "" == uri || strings.HasPrefix(uri, "/") {
167 | return url.Parse(c.baseUrl + uri)
168 | }
169 | return url.Parse(c.baseUrl + "/" + uri)
170 | }
171 |
172 | func (c *Client) bytes(resp *http.Response) ([]byte, error) {
173 | if strings.EqualFold("gzip", resp.Header.Get("Content-Encoding")) {
174 | reader, err := gzip.NewReader(resp.Body)
175 | if nil != err {
176 | return nil, err
177 | }
178 | return io.ReadAll(reader)
179 | }
180 | return io.ReadAll(resp.Body)
181 | }
182 |
--------------------------------------------------------------------------------
/zlog/zlog.go:
--------------------------------------------------------------------------------
1 | package zlog
2 |
3 | import (
4 | "fmt"
5 | "go-flow/utils"
6 | "go.uber.org/zap"
7 | "go.uber.org/zap/zapcore"
8 | "gopkg.in/natefinch/lumberjack.v2"
9 | "io"
10 | "os"
11 | )
12 |
13 | var defaultLogger *ZLog
14 |
15 | func Init(logger *ZLog) {
16 | defaultLogger = logger
17 | zap.ReplaceGlobals(defaultLogger.provider)
18 | }
19 |
20 | func L() *ZLog {
21 | return defaultLogger
22 | }
23 |
24 | func getZapLevel(level string) zapcore.Level {
25 | switch level {
26 | case "debug":
27 | return zap.DebugLevel
28 | case "info":
29 | return zap.InfoLevel
30 | case "warn":
31 | return zap.WarnLevel
32 | case "error":
33 | return zap.ErrorLevel
34 | case "panic":
35 | return zap.PanicLevel
36 | case "fatal":
37 | return zap.FatalLevel
38 | default:
39 | return zap.InfoLevel
40 | }
41 | }
42 |
43 | func newLogWriter(logPath string, maxSize, maxBackups, maxAge int, compress bool) io.Writer {
44 | if logPath == "" || logPath == "-" {
45 | return os.Stdout
46 | }
47 | return &lumberjack.Logger{
48 | Filename: logPath,
49 | MaxSize: maxSize,
50 | MaxBackups: maxBackups,
51 | MaxAge: maxAge,
52 | Compress: compress,
53 | }
54 | }
55 |
56 | func newZapEncoder() zapcore.EncoderConfig {
57 | encoderConfig := zapcore.EncoderConfig{
58 | TimeKey: "timestamp",
59 | LevelKey: "level",
60 | NameKey: "logger",
61 | CallerKey: "line",
62 | MessageKey: "message",
63 | StacktraceKey: "stacktrace",
64 | LineEnding: zapcore.DefaultLineEnding,
65 | EncodeLevel: zapcore.LowercaseLevelEncoder,
66 | EncodeTime: zapcore.TimeEncoderOfLayout(utils.TimeLayout),
67 | EncodeDuration: zapcore.SecondsDurationEncoder,
68 | EncodeCaller: zapcore.ShortCallerEncoder,
69 | EncodeName: zapcore.FullNameEncoder,
70 | }
71 | return encoderConfig
72 | }
73 |
74 | func newZapCore(cfg *Config) zapcore.Core {
75 | hook := newLogWriter(cfg.LogPath, cfg.MaxSize, cfg.MaxBackups, cfg.MaxAge, cfg.Compress)
76 |
77 | encoderConfig := newZapEncoder()
78 |
79 | atomLevel := zap.NewAtomicLevelAt(getZapLevel(cfg.LogLevel))
80 |
81 | var encoder zapcore.Encoder
82 | if cfg.Format == FormatJSON {
83 | encoder = zapcore.NewJSONEncoder(encoderConfig)
84 | } else {
85 | encoder = zapcore.NewConsoleEncoder(encoderConfig)
86 | }
87 |
88 | core := zapcore.NewCore(
89 | encoder,
90 | zapcore.NewMultiWriteSyncer(zapcore.AddSync(hook)),
91 | atomLevel,
92 | )
93 | return core
94 | }
95 |
96 | type ZLog struct {
97 | provider *zap.Logger
98 | level zapcore.Level
99 | prefix string
100 | }
101 |
102 | func Close() {
103 | if defaultLogger == nil {
104 | return
105 | }
106 | _ = defaultLogger.provider.Sync()
107 | }
108 |
109 | func newZapOptions() []zap.Option {
110 | options := []zap.Option{
111 | zap.AddCaller(),
112 | zap.AddCallerSkip(1),
113 | zap.Development(),
114 | zap.Fields(zap.String("app", "go-flow")),
115 | }
116 | return options
117 | }
118 |
119 | func NewZLog(cfg *Config) *ZLog {
120 | cfg.fillWithDefault()
121 | return &ZLog{
122 | provider: zap.New(newZapCore(cfg), newZapOptions()...),
123 | level: getZapLevel(cfg.LogLevel),
124 | }
125 | }
126 |
127 | func (l *ZLog) SetPrefix(prefix string) {
128 | l.prefix = prefix
129 | }
130 |
131 | func (l *ZLog) Print(v ...interface{}) {
132 | l.provider.Info(fmt.Sprintf("[%s] %v", l.prefix, v))
133 | }
134 |
135 | func (l *ZLog) Printf(format string, v ...interface{}) {
136 | l.provider.Info(fmt.Sprintf("[%s] %s", l.prefix, fmt.Sprintf(format, v...)))
137 | }
138 |
139 | func (l *ZLog) Println(v ...interface{}) {
140 | l.provider.Info(fmt.Sprintf("[%s] %v", l.prefix, v))
141 | }
142 |
143 | func Fatalf(prefix string, format string, v ...interface{}) {
144 | if defaultLogger == nil {
145 | fmt.Printf(format+"\n", v...)
146 | return
147 | }
148 | if defaultLogger.level <= zap.FatalLevel {
149 | defaultLogger.provider.Fatal(fmt.Sprintf("[%s] %s", prefix, fmt.Sprintf(format, v...)))
150 | }
151 | }
152 |
153 | func Errorf(prefix string, format string, v ...interface{}) {
154 | if defaultLogger == nil {
155 | fmt.Printf(format+"\n", v...)
156 | return
157 | }
158 | if defaultLogger.level <= zap.ErrorLevel {
159 | defaultLogger.provider.Error(fmt.Sprintf("[%s] %s", prefix, fmt.Sprintf(format, v...)))
160 | }
161 | }
162 |
163 | func Warnf(prefix string, format string, v ...interface{}) {
164 | if defaultLogger == nil {
165 | fmt.Printf(format+"\n", v...)
166 | return
167 | }
168 | if defaultLogger.level <= zap.WarnLevel {
169 | defaultLogger.provider.Warn(fmt.Sprintf("[%s] %s", prefix, fmt.Sprintf(format, v...)))
170 | }
171 | }
172 |
173 | func Infof(prefix string, format string, v ...interface{}) {
174 | if defaultLogger == nil {
175 | fmt.Printf(format+"\n", v...)
176 | return
177 | }
178 | if defaultLogger.level <= zap.InfoLevel {
179 | defaultLogger.provider.Info(fmt.Sprintf("[%s] %s", prefix, fmt.Sprintf(format, v...)))
180 | }
181 | }
182 |
183 | func Debugf(prefix string, format string, v ...interface{}) {
184 | if defaultLogger == nil {
185 | fmt.Printf(format+"\n", v...)
186 | return
187 | }
188 | if defaultLogger.level <= zap.DebugLevel {
189 | defaultLogger.provider.Debug(fmt.Sprintf("[%s] %s", prefix, fmt.Sprintf(format, v...)))
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/flow/static/react.production.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license React
3 | * react.production.min.js
4 | *
5 | * Copyright (c) Facebook, Inc. and its affiliates.
6 | *
7 | * This source code is licensed under the MIT license found in the
8 | * LICENSE file in the root directory of this source tree.
9 | */
10 | (function(){'use strict';(function(c,x){"object"===typeof exports&&"undefined"!==typeof module?x(exports):"function"===typeof define&&define.amd?define(["exports"],x):(c=c||self,x(c.React={}))})(this,function(c){function x(a){if(null===a||"object"!==typeof a)return null;a=V&&a[V]||a["@@iterator"];return"function"===typeof a?a:null}function w(a,b,e){this.props=a;this.context=b;this.refs=W;this.updater=e||X}function Y(){}function K(a,b,e){this.props=a;this.context=b;this.refs=W;this.updater=e||X}function Z(a,b,
11 | e){var m,d={},c=null,h=null;if(null!=b)for(m in void 0!==b.ref&&(h=b.ref),void 0!==b.key&&(c=""+b.key),b)aa.call(b,m)&&!ba.hasOwnProperty(m)&&(d[m]=b[m]);var l=arguments.length-2;if(1===l)d.children=e;else if(1>>1,d=a[c];if(0>>1;cD(l,e))fD(g,l)?(a[c]=g,a[f]=e,c=f):(a[c]=l,a[h]=e,c=h);else if(fD(g,e))a[c]=g,a[f]=e,c=f;else break a}}return b}
16 | function D(a,b){var c=a.sortIndex-b.sortIndex;return 0!==c?c:a.id-b.id}function P(a){for(var b=p(r);null!==b;){if(null===b.callback)E(r);else if(b.startTime<=a)E(r),b.sortIndex=b.expirationTime,O(q,b);else break;b=p(r)}}function Q(a){z=!1;P(a);if(!u)if(null!==p(q))u=!0,R(S);else{var b=p(r);null!==b&&T(Q,b.startTime-a)}}function S(a,b){u=!1;z&&(z=!1,ea(A),A=-1);F=!0;var c=k;try{P(b);for(n=p(q);null!==n&&(!(n.expirationTime>b)||a&&!fa());){var m=n.callback;if("function"===typeof m){n.callback=null;
17 | k=n.priorityLevel;var d=m(n.expirationTime<=b);b=v();"function"===typeof d?n.callback=d:n===p(q)&&E(q);P(b)}else E(q);n=p(q)}if(null!==n)var g=!0;else{var h=p(r);null!==h&&T(Q,h.startTime-b);g=!1}return g}finally{n=null,k=c,F=!1}}function fa(){return v()-hae?(a.sortIndex=c,O(r,a),null===p(q)&&a===p(r)&&(z?(ea(A),A=-1):z=!0,T(Q,c-e))):(a.sortIndex=d,O(q,a),u||F||(u=!0,R(S)));return a},unstable_cancelCallback:function(a){a.callback=null},unstable_wrapCallback:function(a){var b=
24 | k;return function(){var c=k;k=b;try{return a.apply(this,arguments)}finally{k=c}}},unstable_getCurrentPriorityLevel:function(){return k},unstable_shouldYield:fa,unstable_requestPaint:function(){},unstable_continueExecution:function(){u||F||(u=!0,R(S))},unstable_pauseExecution:function(){},unstable_getFirstCallbackNode:function(){return p(q)},get unstable_now(){return v},unstable_forceFrameRate:function(a){0>a||125= 1000 {
379 | s.Bytes = mb / 1000
380 | s.Unit = "GB"
381 | } else {
382 | s.Bytes = mb
383 | s.Unit = "MB"
384 | }
385 | if h.Len() < rank {
386 | heap.Push(h, s)
387 | } else if s.Bytes*getUnitFactor(s.Unit) > (*h)[0].Bytes*getUnitFactor((*h)[0].Unit) {
388 | heap.Pop(h)
389 | heap.Push(h, s)
390 | }
391 | }
392 | result := make([]Traffic, h.Len())
393 | for i := h.Len() - 1; i >= 0; i-- {
394 | result[i] = heap.Pop(h).(Traffic)
395 | }
396 | return result
397 | }
398 |
399 | func topIPHeap(m map[string]IPTraffic, rank int) []IPTraffic {
400 | h := &IPTrafficHeap{}
401 | heap.Init(h)
402 | for _, s := range m {
403 | if s.Bytes <= 0 {
404 | continue
405 | }
406 | mb := s.Bytes / 1e6
407 | if mb >= 1000 {
408 | s.Bytes = mb / 1000
409 | s.Unit = "GB"
410 | } else {
411 | s.Bytes = mb
412 | s.Unit = "MB"
413 | }
414 | if h.Len() < rank {
415 | heap.Push(h, s)
416 | } else if s.Bytes*getUnitFactor(s.Unit) > (*h)[0].Bytes*getUnitFactor((*h)[0].Unit) {
417 | heap.Pop(h)
418 | heap.Push(h, s)
419 | }
420 | }
421 | result := make([]IPTraffic, h.Len())
422 | for i := h.Len() - 1; i >= 0; i-- {
423 | result[i] = heap.Pop(h).(IPTraffic)
424 | }
425 | return result
426 | }
427 |
428 | func topPortHeap(m map[string]PortTraffic, rank int) []PortTraffic {
429 | h := &PortTrafficHeap{}
430 | heap.Init(h)
431 | for _, s := range m {
432 | if s.Bytes <= 0 {
433 | continue
434 | }
435 | mb := s.Bytes / 1e6
436 | unit := "MB"
437 | if mb >= 1000 {
438 | s.Bytes = mb / 1000
439 | unit = "GB"
440 | } else {
441 | s.Bytes = mb
442 | }
443 | s.Unit = unit
444 | if h.Len() < rank {
445 | heap.Push(h, s)
446 | } else if s.Bytes*getUnitFactor(s.Unit) > (*h)[0].Bytes*getUnitFactor((*h)[0].Unit) {
447 | heap.Pop(h)
448 | heap.Push(h, s)
449 | }
450 | }
451 | result := make([]PortTraffic, h.Len())
452 | for i := h.Len() - 1; i >= 0; i-- {
453 | result[i] = heap.Pop(h).(PortTraffic)
454 | }
455 | return result
456 | }
457 |
458 | func completeTrend(trendMap map[int64]TrendItem, total int) []TrendItem {
459 | result := make([]TrendItem, 0, len(trendMap))
460 | for _, item := range trendMap {
461 | result = append(result, item)
462 | }
463 | sort.Slice(result, func(i, j int) bool {
464 | return result[i].Timestamp < result[j].Timestamp
465 | })
466 | if len(result) < total {
467 | now := time.Now().Truncate(time.Minute).Unix()
468 | for ts := now - int64(total)*60 + 60; ts <= now; ts += 60 {
469 | if _, exists := trendMap[ts]; !exists {
470 | result = append(result, TrendItem{
471 | Timestamp: ts,
472 | Bytes: 0,
473 | Requests: 0,
474 | Unit: "MB",
475 | })
476 | }
477 | }
478 | sort.Slice(result, func(i, j int) bool {
479 | return result[i].Timestamp < result[j].Timestamp
480 | })
481 | }
482 | return result
483 | }
484 |
485 | func topProtoStatsFromSnapshot(buckets []*Bucket) ProtocolCache {
486 | protoBytes := make(map[string]float64)
487 | portBytes := make(map[string]float64)
488 | totalBytes := 0.0
489 | for _, bucket := range buckets {
490 | if bucket.Timestamp == 0 {
491 | continue
492 | }
493 | for _, stat := range bucket.Stats {
494 | proto := extractProtocol(stat.DstPort)
495 | if proto == "" {
496 | continue
497 | }
498 | protoBytes[proto] += float64(stat.Bytes)
499 | portBytes[stat.DstPort] += float64(stat.Bytes)
500 | totalBytes += float64(stat.Bytes)
501 | }
502 | }
503 | return topProtoStats(protoBytes, portBytes, totalBytes)
504 | }
505 |
506 | func topProtoStats(protoBytes, portBytes map[string]float64, totalBytes float64) ProtocolCache {
507 | protoHeap := &StatHeap{}
508 | heap.Init(protoHeap)
509 | for proto, bytes := range protoBytes {
510 | if bytes <= 0 {
511 | continue
512 | }
513 | percent := 0.0
514 | if totalBytes > 0 {
515 | percent = (bytes / totalBytes) * 100
516 | percent = math.Round(percent*100) / 100
517 | }
518 | stat := ProtocolStat{Protocol: proto, Percent: percent}
519 | if protoHeap.Len() < 5 {
520 | heap.Push(protoHeap, stat)
521 | } else if percent > (*protoHeap)[0].Percent {
522 | heap.Pop(protoHeap)
523 | heap.Push(protoHeap, stat)
524 | }
525 | }
526 | protocolStats := make([]ProtocolStat, 0, protoHeap.Len())
527 | for protoHeap.Len() > 0 {
528 | protocolStats = append(protocolStats, heap.Pop(protoHeap).(ProtocolStat))
529 | }
530 | sort.Slice(protocolStats, func(i, j int) bool {
531 | return protocolStats[i].Percent > protocolStats[j].Percent
532 | })
533 |
534 | portHeap := &PortTrafficHeap{}
535 | heap.Init(portHeap)
536 | for port, bytes := range portBytes {
537 | if bytes <= 0 {
538 | continue
539 | }
540 | mb := bytes / 1e6
541 | unit := "MB"
542 | if mb >= 1000 {
543 | mb = mb / 1000
544 | unit = "GB"
545 | }
546 | pt := PortTraffic{DstPort: port, Bytes: mb, Unit: unit}
547 | if portHeap.Len() < 5 {
548 | heap.Push(portHeap, pt)
549 | } else if bytes > (*portHeap)[0].Bytes*getUnitFactor((*portHeap)[0].Unit) {
550 | heap.Pop(portHeap)
551 | heap.Push(portHeap, pt)
552 | }
553 | }
554 | portStats := make([]ProtocolStat, 0, 5)
555 | for portHeap.Len() > 0 {
556 | pt := heap.Pop(portHeap).(PortTraffic)
557 | bytes := pt.Bytes * getUnitFactor(pt.Unit)
558 | percent := 0.0
559 | if totalBytes > 0 {
560 | percent = (bytes / totalBytes) * 100
561 | percent = math.Round(percent*100) / 100
562 | }
563 | portStats = append(portStats, ProtocolStat{
564 | Protocol: extractPort(pt.DstPort),
565 | Percent: percent,
566 | })
567 | }
568 | return ProtocolCache{
569 | ProtocolStats: protocolStats,
570 | PortStats: portStats,
571 | }
572 | }
573 |
574 | type PacketData struct {
575 | Data []byte
576 | Timestamp time.Time
577 | }
578 |
579 | func Capture(device string, w *Window, wg *sync.WaitGroup) {
580 | var (
581 | packetChan = make(chan PacketData, conf.CoreConf.Server.PacketChan)
582 | err error
583 | batchSize = 1000
584 | numConsumers = runtime.NumCPU()
585 | producerWg sync.WaitGroup
586 | consumerWg sync.WaitGroup
587 | )
588 |
589 | defer wg.Done()
590 | // init goroutine pool
591 | pool, err := ants.NewPoolWithFunc(conf.CoreConf.Server.Workers, func(payload interface{}) {
592 | batch := payload.([]PacketData)
593 | for _, pkt := range batch {
594 | packet := gopacket.NewPacket(pkt.Data, layers.LayerTypeEthernet, gopacket.DecodeOptions{
595 | Lazy: false,
596 | NoCopy: false,
597 | })
598 | if packet == nil {
599 | return
600 | }
601 | packet.Metadata().CaptureLength = len(pkt.Data)
602 | packet.Metadata().Length = len(pkt.Data)
603 |
604 | var ipStr, dstIP, protocol, dstPort string
605 | if ipLayer := packet.Layer(layers.LayerTypeIPv4); ipLayer != nil {
606 | ip := ipLayer.(*layers.IPv4)
607 | ipStr = ip.SrcIP.String()
608 | dstIP = ip.DstIP.String()
609 | } else {
610 | return
611 | }
612 | if tcpLayer := packet.Layer(layers.LayerTypeTCP); tcpLayer != nil {
613 | tcp := tcpLayer.(*layers.TCP)
614 | protocol = "TCP"
615 | dstPort = tcp.DstPort.String()
616 | } else if udpLayer := packet.Layer(layers.LayerTypeUDP); udpLayer != nil {
617 | udp := udpLayer.(*layers.UDP)
618 | protocol = "UDP"
619 | dstPort = udp.DstPort.String()
620 | } else {
621 | return
622 | }
623 | if !utils.IsValidIP(ipStr) || !utils.IsValidIP(dstIP) {
624 | return
625 | }
626 | packetLen := int64(packet.Metadata().Length)
627 | stats := syncPool.Get().(*Traffic)
628 | *stats = Traffic{
629 | Timestamp: pkt.Timestamp,
630 | SrcIP: ipStr,
631 | DstIP: dstIP,
632 | DstPort: dstPort,
633 | Protocol: protocol,
634 | Bytes: float64(packetLen),
635 | Requests: 1,
636 | }
637 | copied := *stats
638 | syncPool.Put(stats)
639 | if conf.CoreConf.Kafka.Enable {
640 | msg, _ := json.Marshal(copied)
641 | kafka.Push(msg)
642 | }
643 | w.Add(copied)
644 | }
645 | }, ants.WithMaxBlockingTasks(conf.CoreConf.Server.Workers*20))
646 | if err != nil {
647 | log.Fatalf("Failed to create goroutine pool: %v", err)
648 | }
649 | defer pool.Release()
650 |
651 | tPacket, err := afpacket.NewTPacket(
652 | afpacket.OptInterface(device),
653 | afpacket.OptFrameSize(65536),
654 | afpacket.OptBlockSize(1<<22),
655 | afpacket.OptNumBlocks(64),
656 | afpacket.OptPollTimeout(100*time.Millisecond),
657 | afpacket.OptTPacketVersion(afpacket.TPacketVersion3),
658 | )
659 | if err != nil {
660 | log.Fatalf("Failed to create TPACKET_V3: %v", err)
661 | }
662 |
663 | // monitor drop packets and send big bandwidth and high frequency alerts
664 | go func() {
665 | var monitorTicker = time.NewTicker(time.Minute)
666 | defer monitorTicker.Stop()
667 | for {
668 | select {
669 | case <-utils.Ctx.Done():
670 | return
671 | case <-monitorTicker.C:
672 | // monitor dropped packets
673 | count := atomic.SwapInt64(&counter, 0)
674 | if count != 0 {
675 | zlog.Infof("COUNT", "Dropped packets: %d", count)
676 | }
677 | if !conf.CoreConf.Notify.Enable {
678 | continue
679 | }
680 | // alert notification for Big Bandwidth and High Frequency
681 | now := time.Now().Format(utils.TimeLayout)
682 | var (
683 | bws []notify.Bandwidth
684 | fqs []notify.Frequency
685 | )
686 | thresholdBytes := getThresholdBytes()
687 | var snapshot []IPTraffic
688 |
689 | w.cacheMu.RLock()
690 | snapshot = append([]IPTraffic(nil), w.ipCache...)
691 | w.cacheMu.RUnlock()
692 |
693 | for _, s := range snapshot {
694 | bytes := s.Bytes * getUnitFactor(s.Unit)
695 | if bytes > thresholdBytes && !notify.IsWhiteIp(s.IP) {
696 | bws = append(bws, notify.Bandwidth{
697 | IP: s.IP,
698 | Bandwidth: fmt.Sprintf("%.2f%s", s.Bytes, s.Unit),
699 | })
700 | }
701 | }
702 | if len(bws) != 0 {
703 | notify.Push(notify.DdosAlert{
704 | BandwidthS: bws,
705 | Timestamp: now,
706 | Title: highBandwidth,
707 | Location: conf.CoreConf.Notify.Location,
708 | TimeRange: utils.GetTimeRangeString(conf.CoreConf.Server.Size),
709 | })
710 | }
711 |
712 | frequencyThreshold := conf.CoreConf.Notify.FrequencyThreshold
713 |
714 | w.freqStatsMu.RLock()
715 | srcCopy := make(map[string]map[string]bool, len(w.srcToDstStats))
716 | for src, dstMap := range w.srcToDstStats {
717 | dstCopy := make(map[string]bool, len(dstMap))
718 | for dst := range dstMap {
719 | dstCopy[dst] = true
720 | }
721 | srcCopy[src] = dstCopy
722 | }
723 | dstCopy := make(map[string]map[string]bool, len(w.dstToSrcStats))
724 | for dst, srcMap := range w.dstToSrcStats {
725 | srcS := make(map[string]bool, len(srcMap))
726 | for src := range srcMap {
727 | srcS[src] = true
728 | }
729 | dstCopy[dst] = srcS
730 | }
731 | w.freqStatsMu.RUnlock()
732 |
733 | for srcIP, dstIPs := range srcCopy {
734 | if len(dstIPs) > frequencyThreshold && !notify.IsWhiteIp(srcIP) {
735 | fqs = append(fqs, notify.Frequency{
736 | IP: srcIP,
737 | Count: len(dstIPs),
738 | Desc: fmt.Sprintf("检测到源IP %s 在最近1分钟内尝试连接 %d 个不同目标 IP,疑似异常行为", srcIP, len(dstIPs)),
739 | })
740 | }
741 | }
742 | for dstIP, srcIPs := range dstCopy {
743 | if len(srcIPs) > frequencyThreshold && !notify.IsWhiteIp(dstIP) {
744 | fqs = append(fqs, notify.Frequency{
745 | IP: dstIP,
746 | Count: len(srcIPs),
747 | Desc: fmt.Sprintf("检测到目标IP %s 在最近1分钟内接收来自 %d 个不同源IP访问请求,疑似异常行为", dstIP, len(srcIPs)),
748 | })
749 | }
750 | }
751 | if len(fqs) != 0 {
752 | notify.Push(notify.DdosAlert{
753 | FrequencyS: fqs,
754 | Timestamp: now,
755 | Title: highFrequency,
756 | Location: conf.CoreConf.Notify.Location,
757 | TimeRange: utils.GetTimeRangeString(1),
758 | })
759 | }
760 | }
761 | }
762 | }()
763 |
764 | // producer
765 | producerWg.Add(1)
766 | go func() {
767 | defer producerWg.Done()
768 | defer close(packetChan)
769 | packetSource := gopacket.NewPacketSource(tPacket, layers.LayerTypeEthernet)
770 | for {
771 | select {
772 | case <-utils.Ctx.Done():
773 | return
774 | default:
775 | packet, err := packetSource.NextPacket()
776 | if err != nil {
777 | continue
778 | }
779 | data := packet.Data()
780 | buf := make([]byte, len(data))
781 | copy(buf, data)
782 |
783 | select {
784 | case packetChan <- PacketData{
785 | Data: buf,
786 | Timestamp: packet.Metadata().Timestamp,
787 | }:
788 | default:
789 | atomic.AddInt64(&counter, 1)
790 | }
791 | }
792 | }
793 | }()
794 |
795 | // consumer
796 | // use goroutine pool to process packets in batches
797 | for i := 0; i < numConsumers; i++ {
798 | consumerWg.Add(1)
799 | go func() {
800 | defer consumerWg.Done()
801 | batch := make([]PacketData, 0, batchSize)
802 | ticker := time.NewTicker(2 * time.Millisecond)
803 | defer ticker.Stop()
804 | for {
805 | select {
806 | case pkt, ok := <-packetChan:
807 | if !ok {
808 | if len(batch) > 0 {
809 | copyBatch := append([]PacketData{}, batch...)
810 | _ = pool.Invoke(copyBatch)
811 | }
812 | return
813 | }
814 | batch = append(batch, pkt)
815 | if len(batch) >= batchSize {
816 | copyBatch := append([]PacketData{}, batch...)
817 | _ = pool.Invoke(copyBatch)
818 | batch = batch[:0]
819 | }
820 | case <-ticker.C:
821 | if len(batch) > 0 {
822 | copyBatch := append([]PacketData{}, batch...)
823 | _ = pool.Invoke(copyBatch)
824 | batch = batch[:0]
825 | }
826 | case <-utils.Ctx.Done():
827 | return
828 | }
829 | }
830 | }()
831 | }
832 |
833 | producerWg.Wait()
834 | consumerWg.Wait()
835 |
836 | tPacket.Close()
837 | }
838 |
839 | func (w *Window) ServeHTTP(wr http.ResponseWriter, r *http.Request) {
840 | if r.URL.Path == "/top" {
841 | var (
842 | page = 1
843 | size = 10
844 | )
845 | w.cacheMu.RLock()
846 | top := w.cache
847 | w.cacheMu.RUnlock()
848 | pageStr := r.URL.Query().Get("page")
849 | sizeStr := r.URL.Query().Get("size")
850 | keyword := r.URL.Query().Get("keyword")
851 | if len(pageStr) != 0 {
852 | page = cast.ToInt(pageStr)
853 | }
854 | if len(sizeStr) != 0 {
855 | size = cast.ToInt(sizeStr)
856 | }
857 | resp := make([]TopItem, 0, len(top))
858 | for _, s := range top {
859 | matches := true
860 | if len(keyword) != 0 {
861 | if !strings.Contains(s.SrcIP, keyword) && !strings.Contains(s.DstIP, keyword) &&
862 | !strings.Contains(s.DstPort, keyword) {
863 | matches = false
864 | }
865 | }
866 | if matches {
867 | resp = append(resp, TopItem{
868 | SrcIP: s.SrcIP,
869 | DstIP: s.DstIP,
870 | DstPort: s.DstPort,
871 | Protocol: s.Protocol,
872 | Bandwidth: s.Bytes,
873 | Unit: s.Unit,
874 | Requests: s.Requests,
875 | Desc: fmt.Sprintf("%v%v", fmt.Sprintf("%0.2f", s.Bytes)+s.Unit, w.Mbps(s.Bytes, s.Unit)),
876 | })
877 | }
878 | }
879 | start, end := paginate(page, size, len(resp))
880 | response := TopResponse{
881 | Base: Base{
882 | Page: page,
883 | Size: size,
884 | Total: len(resp),
885 | Pages: int(math.Ceil(float64(len(resp)) / float64(size))),
886 | },
887 | Data: resp[start:end],
888 | Timestamp: w.lastCalc.Unix(),
889 | WindowSize: int(w.size.Seconds()),
890 | Rank: w.Rank,
891 | }
892 | wr.Header().Set("Content-Type", "application/json")
893 | if err := ji.NewEncoder(wr).Encode(response); err != nil {
894 | http.Error(wr, "Failed to encode JSON", http.StatusInternalServerError)
895 | }
896 | return
897 | }
898 | if r.URL.Path == "/ip_top" {
899 | var (
900 | page = 1
901 | size = 10
902 | )
903 | w.cacheMu.RLock()
904 | top := w.ipCache
905 | w.cacheMu.RUnlock()
906 | pageStr := r.URL.Query().Get("page")
907 | sizeStr := r.URL.Query().Get("size")
908 | keyword := r.URL.Query().Get("keyword")
909 | if len(pageStr) != 0 {
910 | page = cast.ToInt(pageStr)
911 | }
912 | if len(sizeStr) != 0 {
913 | size = cast.ToInt(sizeStr)
914 | }
915 | resp := make([]IPTraffic, 0, len(top))
916 | for _, s := range top {
917 | matches := true
918 | if len(keyword) != 0 {
919 | if !strings.Contains(s.IP, keyword) {
920 | matches = false
921 | }
922 | }
923 | if matches {
924 | resp = append(resp, IPTraffic{
925 | IP: s.IP,
926 | Unit: s.Unit,
927 | Requests: s.Requests,
928 | Bytes: s.Bytes,
929 | Desc: fmt.Sprintf("%v%v", fmt.Sprintf("%0.2f", s.Bytes)+s.Unit, w.Mbps(s.Bytes, s.Unit)),
930 | })
931 | }
932 | }
933 | start, end := paginate(page, size, len(resp))
934 | response := IPTrafficResponse{
935 | Base: Base{
936 | Page: page,
937 | Size: size,
938 | Total: len(resp),
939 | Pages: int(math.Ceil(float64(len(resp)) / float64(size))),
940 | },
941 | Data: resp[start:end],
942 | }
943 | wr.Header().Set("Content-Type", "application/json")
944 | if err := ji.NewEncoder(wr).Encode(response); err != nil {
945 | http.Error(wr, "Failed to encode JSON", http.StatusInternalServerError)
946 | }
947 | return
948 | }
949 | if r.URL.Path == "/stats/ports" {
950 | var (
951 | page = 1
952 | size = 10
953 | )
954 | w.cacheMu.RLock()
955 | top := w.portCache
956 | w.cacheMu.RUnlock()
957 | pageStr := r.URL.Query().Get("page")
958 | sizeStr := r.URL.Query().Get("size")
959 | keyword := r.URL.Query().Get("keyword")
960 | if len(pageStr) != 0 {
961 | page = cast.ToInt(pageStr)
962 | }
963 | if len(sizeStr) != 0 {
964 | size = cast.ToInt(sizeStr)
965 | }
966 | resp := make([]PortItem, 0, len(top))
967 | for _, s := range top {
968 | matches := true
969 | if len(keyword) != 0 {
970 | if !strings.Contains(s.DstPort, keyword) {
971 | matches = false
972 | }
973 | }
974 | if matches {
975 | resp = append(resp, PortItem{
976 | DstPort: s.DstPort,
977 | Bytes: s.Bytes,
978 | Protocol: s.Protocol,
979 | Unit: s.Unit,
980 | })
981 | }
982 | }
983 | start, end := paginate(page, size, len(resp))
984 | response := PortItemResponse{
985 | Base: Base{
986 | Page: page,
987 | Size: size,
988 | Total: len(resp),
989 | Pages: int(math.Ceil(float64(len(resp)) / float64(size))),
990 | },
991 | Data: resp[start:end],
992 | }
993 | wr.Header().Set("Content-Type", "application/json")
994 | if err := ji.NewEncoder(wr).Encode(response); err != nil {
995 | http.Error(wr, "Failed to encode JSON", http.StatusInternalServerError)
996 | }
997 | return
998 | }
999 | if r.URL.Path == "/trend" {
1000 | w.cacheMu.RLock()
1001 | trend := w.trendCache
1002 | w.cacheMu.RUnlock()
1003 | response := TrendResponse{
1004 | Data: trend,
1005 | WindowSize: int(w.size.Seconds()),
1006 | }
1007 | wr.Header().Set("Content-Type", "application/json")
1008 | if err := ji.NewEncoder(wr).Encode(response); err != nil {
1009 | http.Error(wr, "Failed to encode JSON", http.StatusInternalServerError)
1010 | }
1011 | return
1012 | }
1013 | if r.URL.Path == "/sys" {
1014 | response := systemMonitorManager.Get(utils.LocalIpAddr)
1015 | wr.Header().Set("Content-Type", "application/json")
1016 | if err := ji.NewEncoder(wr).Encode(response); err != nil {
1017 | http.Error(wr, "Failed to encode JSON", http.StatusInternalServerError)
1018 | }
1019 | return
1020 | }
1021 | if r.URL.Path == "/stats/protocols" {
1022 | w.cacheMu.RLock()
1023 | protoCache := w.protoCache
1024 | w.cacheMu.RUnlock()
1025 | response := ProtocolResponse{
1026 | ProtocolStats: protoCache.ProtocolStats,
1027 | PortStats: protoCache.PortStats,
1028 | Timestamp: w.lastCalc.Unix(),
1029 | WindowSize: int(w.size.Seconds()),
1030 | }
1031 | wr.Header().Set("Content-Type", "application/json")
1032 | if err := ji.NewEncoder(wr).Encode(response); err != nil {
1033 | http.Error(wr, "Failed to encode JSON", http.StatusInternalServerError)
1034 | }
1035 | return
1036 | }
1037 | if r.URL.Path == "/" {
1038 | file, err := content.ReadFile("index.html")
1039 | if err != nil {
1040 | http.Error(wr, "Failed to read index.html", http.StatusInternalServerError)
1041 | return
1042 | }
1043 | wr.Header().Set("Content-Type", "text/html")
1044 | _, _ = wr.Write(file)
1045 | return
1046 | }
1047 | http.FileServer(http.FS(content)).ServeHTTP(wr, r)
1048 | }
1049 |
1050 | func getKey(ip, dstIP, protocol string, dstPort string) string {
1051 | return fmt.Sprintf("%s|%s|%s|%s", ip, dstIP, protocol, dstPort)
1052 | }
1053 |
1054 | func getUnitFactor(unit string) float64 {
1055 | switch unit {
1056 | case "GB":
1057 | return 1e9
1058 | case "MB":
1059 | return 1e6
1060 | default:
1061 | return 1
1062 | }
1063 | }
1064 |
1065 | func getThresholdBytes() float64 {
1066 | var (
1067 | unit = conf.CoreConf.Notify.ThresholdUnit
1068 | value = conf.CoreConf.Notify.ThresholdValue
1069 | )
1070 | switch unit {
1071 | case "GB":
1072 | return value * 1e9
1073 | case "MB":
1074 | return value * 1e6
1075 | default:
1076 | return value
1077 | }
1078 | }
1079 |
1080 | func extractPort(dstPort string) string {
1081 | if strings.Contains(dstPort, "(") {
1082 | return dstPort[:strings.Index(dstPort, "(")]
1083 | }
1084 | return dstPort
1085 | }
1086 |
1087 | func extractProtocol(dstPort string) string {
1088 | if strings.Contains(dstPort, "(") && strings.HasSuffix(dstPort, ")") {
1089 | start := strings.Index(dstPort, "(") + 1
1090 | end := len(dstPort) - 1
1091 | return strings.ToLower(dstPort[start:end])
1092 | }
1093 | return ""
1094 | }
1095 |
1096 | //go:embed index.html
1097 | //go:embed static/*
1098 | var content embed.FS
1099 |
1100 | type TopItem struct {
1101 | SrcIP string `json:"src_ip"`
1102 | DstIP string `json:"dest_ip"`
1103 | DstPort string `json:"dest_port"`
1104 | Protocol string `json:"protocol"`
1105 | Bandwidth float64 `json:"bandwidth"`
1106 | Unit string `json:"unit"`
1107 | Requests int64 `json:"requests"`
1108 | Desc string `json:"desc"`
1109 | }
1110 |
1111 | type TopResponse struct {
1112 | Base
1113 | Data []TopItem `json:"data"`
1114 | Timestamp int64 `json:"timestamp"`
1115 | WindowSize int `json:"window_size"`
1116 | Rank int `json:"rank"`
1117 | }
1118 |
1119 | type PortItem struct {
1120 | DstPort string `json:"dest_port"`
1121 | Bytes float64 `json:"bytes"`
1122 | Protocol string `json:"protocol"`
1123 | Unit string `json:"unit"`
1124 | }
1125 |
1126 | type PortItemResponse struct {
1127 | Data []PortItem `json:"data"`
1128 | Base
1129 | }
1130 |
1131 | type TrendResponse struct {
1132 | Data []TrendItem `json:"data"`
1133 | WindowSize int `json:"window_size"`
1134 | }
1135 |
1136 | type ProtocolStat struct {
1137 | Protocol string `json:"protocol"`
1138 | Percent float64 `json:"percent"`
1139 | }
1140 |
1141 | type ProtocolResponse struct {
1142 | ProtocolStats []ProtocolStat `json:"protocol_stats"`
1143 | PortStats []ProtocolStat `json:"port_stats"`
1144 | Timestamp int64 `json:"timestamp"`
1145 | WindowSize int `json:"window_size"`
1146 | }
1147 |
1148 | type ProtocolCache struct {
1149 | ProtocolStats []ProtocolStat
1150 | PortStats []ProtocolStat
1151 | }
1152 |
1153 | var (
1154 | systemMonitorManager *SystemMonitorManager
1155 | )
1156 |
1157 | type SystemMonitor struct {
1158 | NetworkInterface string `json:"network_interface"`
1159 | LocalIp string `json:"local_ip"`
1160 | Cpu string `json:"cpu"`
1161 | Mem string `json:"mem"`
1162 | Workers int `json:"workers"`
1163 | }
1164 |
1165 | type SystemMonitorManager struct {
1166 | Mu sync.RWMutex
1167 | Data map[string]SystemMonitor
1168 | }
1169 |
1170 | func (s *SystemMonitorManager) Get(key string) SystemMonitor {
1171 | s.Mu.RLock()
1172 | defer s.Mu.RUnlock()
1173 | return s.Data[key]
1174 | }
1175 |
1176 | func (s *SystemMonitorManager) Set(key string, value SystemMonitor) {
1177 | s.Mu.Lock()
1178 | defer s.Mu.Unlock()
1179 | s.Data[key] = value
1180 | }
1181 |
1182 | func init() {
1183 | systemMonitorManager = &SystemMonitorManager{
1184 | Data: make(map[string]SystemMonitor),
1185 | }
1186 | go systemMonitorManager.run()
1187 | }
1188 |
1189 | func (s *SystemMonitorManager) run() {
1190 | ticker := time.NewTicker(2 * time.Second)
1191 | defer ticker.Stop()
1192 | for {
1193 | select {
1194 | case <-ticker.C:
1195 | s.Set(utils.LocalIpAddr, SystemMonitor{
1196 | NetworkInterface: conf.CoreConf.Server.Eth,
1197 | LocalIp: utils.LocalIpAddr,
1198 | Cpu: utils.GetCpuUsage(),
1199 | Mem: utils.GetMemUsage(),
1200 | Workers: runtime.NumGoroutine(),
1201 | })
1202 | case <-utils.Ctx.Done():
1203 | return
1204 | }
1205 | }
1206 | }
1207 |
1208 | func (w *Window) Mbps(bytes float64, unit string) string {
1209 | duration := w.endTime.Sub(w.startTime)
1210 | if duration <= 0 {
1211 | return ""
1212 | }
1213 | if duration > w.size {
1214 | duration = w.size
1215 | }
1216 | var bits float64
1217 | switch unit {
1218 | case "MB":
1219 | bits = bytes * 8 * 1_000_000
1220 | case "GB":
1221 | bits = bytes * 8 * 1_000_000_000
1222 | default:
1223 | return ""
1224 | }
1225 | mbps := bits / duration.Seconds() / 1_000_000
1226 | return fmt.Sprintf(" ( %.2fMbps )", mbps)
1227 | }
1228 |
--------------------------------------------------------------------------------
/flow/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | GO-FLOW
7 |
8 |
9 |
10 |
11 |
12 |
60 |
61 |
62 |
63 |
1154 |
1155 |
--------------------------------------------------------------------------------