├── 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 | ![Ui](https://raw.githubusercontent.com/xxddpac/go-flow/main/image/ui.jpg) 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 | 39 | 40 | 41 | 42 | 43 | {{range $index, $alert := .BandwidthS}} 44 | 45 | 46 | 47 | 48 | 49 | {{end}} 50 | 51 |
#异常IP使用流量
{{add $index 1}}{{$alert.IP}}{{$alert.Bandwidth}}
52 | {{end}} 53 | {{if .FrequencyS}} 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | {{range $index, $alert := .FrequencyS}} 63 | 64 | 65 | 66 | 67 | {{end}} 68 | 69 |
#描述
{{add $index 1}}{{$alert.Desc}}
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 | --------------------------------------------------------------------------------