diff --git a/AGENTS.md b/AGENTS.md index 8a59440..f9417a0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,7 @@ - `go build` – compile the binary. - `go test -v ./... -count 1` – run all tests once, verbose output. - `go test -race ./...` – run tests with race detector. +- `GOEXPERIMENT=goroutineleakprofile go test ./...` – verify no goroutine leaks (Go 1.26+). - `golangci-lint run` – linting (requires golangci‑lint installed). - `gosec ./...` – security scan. - `trivy fs .` – container image scanning. @@ -39,6 +40,11 @@ If you need to supply environment variables, create a `.env` file in the reposit Dependencies that read environment variables include `github.com/subosito/gotenv`. Use `gotenv.Load()` in your code as needed. +## Go Version Requirement +- **Required:** Go 1.26 or later (latest stable). +- Install from https://go.dev/dl/ or use `go env -w GOTOOLCHAIN=auto`. +- Experimental features available: `goroutineleakprofile`, `simd`, `runtimesecret`. + ## Dependency Management - Keep dependencies minimal and use standard library packages where possible. - Run `go mod tidy` before building/testing to remove unused modules. diff --git a/barrage/barrage_test.go b/barrage/barrage_test.go index 5fadc85..9ae43d3 100644 --- a/barrage/barrage_test.go +++ b/barrage/barrage_test.go @@ -7,8 +7,8 @@ import ( "bytes" "context" "encoding/binary" - "github.com/dmabry/flowgre/netflow" "github.com/dmabry/flowgre/models" + "github.com/dmabry/flowgre/netflow" "net" "sync" "testing" @@ -96,7 +96,7 @@ func receiver(ctx context.Context, wg *sync.WaitGroup, ip string, port int, t *t } // read all flows from the payload count := int(header.FlowCount) - for i := 0; i < count; i++ { + for range count { flow := netflow.GenericFlow{} err := binary.Read(reader, binary.BigEndian, &flow) if err != nil { diff --git a/config/config.go b/config/config.go index 4210d57..c36d32a 100644 --- a/config/config.go +++ b/config/config.go @@ -30,7 +30,7 @@ func LoadBarrageConfig() (*models.Config, error) { } targets := viper.Get("targets") - targetMap, ok := targets.(map[string]interface{}) + targetMap, ok := targets.(map[string]any) if !ok || len(targetMap) == 0 { return nil, fmt.Errorf("no targets found in config") } @@ -41,10 +41,10 @@ func LoadBarrageConfig() (*models.Config, error) { // Get the single target var targetName string - var targetValues map[string]interface{} + var targetValues map[string]any for name, vals := range targetMap { targetName = name - tv, ok := vals.(map[string]interface{}) + tv, ok := vals.(map[string]any) if !ok { return nil, fmt.Errorf("unexpected type for target %s: %T", name, vals) } @@ -79,7 +79,7 @@ func LoadBarrageConfig() (*models.Config, error) { } // getString safely gets a string value from a map with a default. -func getString(m map[string]interface{}, key, def string) string { +func getString(m map[string]any, key, def string) string { if v, ok := m[key]; ok { if s, ok := v.(string); ok { return s @@ -90,7 +90,7 @@ func getString(m map[string]interface{}, key, def string) string { // getInt safely gets an int value from a map with a default. // Viper returns float64 for numbers, so we handle that. -func getInt(m map[string]interface{}, key string, def int) int { +func getInt(m map[string]any, key string, def int) int { if v, ok := m[key]; ok { switch val := v.(type) { case int: @@ -112,7 +112,7 @@ func getInt(m map[string]interface{}, key string, def int) int { } // getBool safely gets a bool value from a map with a default. -func getBool(m map[string]interface{}, key string, def bool) bool { +func getBool(m map[string]any, key string, def bool) bool { if v, ok := m[key]; ok { if b, ok := v.(bool); ok { return b diff --git a/netflow/dataflowset.go b/netflow/dataflowset.go index 67396d1..3f29164 100644 --- a/netflow/dataflowset.go +++ b/netflow/dataflowset.go @@ -14,7 +14,7 @@ type DataItem struct { Fields []uint32 } -type DataAny interface{} +type DataAny any // DataFlowSet for Netflow type DataFlowSet struct { @@ -33,7 +33,7 @@ func (d *DataFlowSet) Generate(flowCount int, srcRange string, dstRange string, dataFlowSet.FlowSetID = 256 protoPorts := [13]int{21, 22, 53, 80, 443, 123, 161, 993, 3306, 8080, 8443, 6681, 6682} items := make([]DataAny, flowCount) - for i := 0; i < flowCount; i++ { + for i := range flowCount { srcIP, err := utils.RandomIP(srcRange) if err != nil { log.Printf("Issue generating IP... proceeding anyway: %v", err) @@ -68,7 +68,7 @@ func (d *DataFlowSet) size() int { if remainder > 0 { padding = 4 - remainder } - size += padding // number of uint8 to pad in order to reach 32 bit boundary + size += padding // number of uint8 to pad in order to reach 32 bit boundary d.Padding = padding // save the padding as an int for later. return size } diff --git a/netflow/netflow_test.go b/netflow/netflow_test.go index ac69040..107847f 100644 --- a/netflow/netflow_test.go +++ b/netflow/netflow_test.go @@ -126,7 +126,7 @@ func TestToBytes(t *testing.T) { } // Parse TemplateFlow tFlowCount := int(tparsed.Header.FlowCount) - for i := 0; i < tFlowCount; i++ { + for range tFlowCount { tFlowSet := new(TemplateFlowSet) template := new(Template) err := binary.Read(treader, binary.BigEndian, &tFlowSet.FlowSetID) @@ -146,7 +146,7 @@ func TestToBytes(t *testing.T) { t.Errorf("Failed to parse Netflow FlowSet FieldCount! Got: %v", err) } fc := int(template.FieldCount) - for f := 0; f < fc; f++ { + for range fc { tField := new(Field) err := binary.Read(treader, binary.BigEndian, &tField.Type) if err != nil { @@ -183,7 +183,7 @@ func TestToBytes(t *testing.T) { } // I know the field count from the template generated above. Going to use that dataItems := make([]DataAny, flowcount) - for i := 0; i < flowcount; i++ { + for i := range flowcount { dataItem := GenericFlow{} err := binary.Read(dreader, binary.BigEndian, &dataItem) if err != nil { diff --git a/proxy/proxy.go b/proxy/proxy.go index f6ec483..d475cbd 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -16,8 +16,8 @@ import ( "time" "github.com/dmabry/flowgre/lifecycle" - "github.com/dmabry/flowgre/netflow" "github.com/dmabry/flowgre/models" + "github.com/dmabry/flowgre/netflow" "github.com/dmabry/flowgre/utils" ) @@ -144,8 +144,8 @@ func proxyListener(ctx context.Context, wg *sync.WaitGroup, ip string, port int, return default: if verbose { - log.Printf("proxyListener: dropped packet (proxyChan full)") - } + log.Printf("proxyListener: dropped packet (proxyChan full)") + } } } } @@ -180,24 +180,24 @@ func parseNetflow(ctx context.Context, wg *sync.WaitGroup, proxyChan <-chan []by return case payload := <-proxyChan: ok, err := netflow.IsValidNetFlow(payload, 9) - if err != nil { - log.Printf("Skipping packet due to issue parsing: %v", err) - rStats.IncrInvalid() - } else if ok { - rStats.IncrValid() - select { - case dataChan <- payload: - case <-ctx.Done(): - log.Println("Netflow parser context cancelled during send") - return - default: - if verbose { - log.Printf("Netflow parser: dropped packet (dataChan full)") + if err != nil { + log.Printf("Skipping packet due to issue parsing: %v", err) + rStats.IncrInvalid() + } else if ok { + rStats.IncrValid() + select { + case dataChan <- payload: + case <-ctx.Done(): + log.Println("Netflow parser context cancelled during send") + return + default: + if verbose { + log.Printf("Netflow parser: dropped packet (dataChan full)") + } } + } else { + rStats.IncrInvalid() } - } else { - rStats.IncrInvalid() - } case <-ticker.C: log.Printf("Netflow v9 Packets: %d Ignored Packets: %d", rStats.LoadValid(), rStats.LoadInvalid()) @@ -230,7 +230,7 @@ func Run(ip string, port int, verbose bool, targets []string) { workerChans := make([]chan []byte, workers) // start workers wg.Add(workers) - for w := 0; w < workers; w++ { + for w := range workers { id := w + 1 workerChan := make(chan []byte, bufferSize) workerChans[w] = workerChan diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go index 07f190b..ac1672c 100644 --- a/proxy/proxy_test.go +++ b/proxy/proxy_test.go @@ -116,9 +116,7 @@ func TestWorker(t *testing.T) { // Start a receiver on the target port receiverDone := make(chan struct{}) receiverReady := make(chan struct{}) - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { defer close(receiverDone) conn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 19996}) @@ -141,7 +139,7 @@ func TestWorker(t *testing.T) { if !bytes.Equal(payload[:n], []byte("worker test")) { t.Errorf("Received wrong payload: got %v, want %v", payload[:n], "worker test") } - }() + }) // Wait until the receiver is actually bound and ready to receive <-receiverReady @@ -249,9 +247,7 @@ func TestRunIntegration(t *testing.T) { targetPort := 19997 received := make(chan struct{}, 1) var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { conn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: targetPort}) if err != nil { t.Errorf("Failed to listen: %v", err) @@ -267,7 +263,7 @@ func TestRunIntegration(t *testing.T) { return } received <- struct{}{} - }() + }) // Start proxy in a goroutine proxyDone := make(chan struct{}) diff --git a/replay/replay_test.go b/replay/replay_test.go index 37d67e4..f151073 100644 --- a/replay/replay_test.go +++ b/replay/replay_test.go @@ -30,9 +30,7 @@ func TestWorker(t *testing.T) { // Start a receiver on the target port received := make(chan struct{}, 1) - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { conn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 39995}) if err != nil { t.Errorf("Failed to listen: %v", err) @@ -54,7 +52,7 @@ func TestWorker(t *testing.T) { t.Errorf("Received wrong payload: got %v, want %v", payload[:n], "worker test") } received <- struct{}{} - }() + }) // Wait until the receiver is actually bound and ready to receive <-receiverReady @@ -206,9 +204,7 @@ func TestRunIntegration(t *testing.T) { // Start a receiver on target port received := make(chan struct{}, 1) var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { conn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 39997}) if err != nil { t.Errorf("Failed to listen: %v", err) @@ -224,7 +220,7 @@ func TestRunIntegration(t *testing.T) { return } received <- struct{}{} - }() + }) // Start replay replayDone := make(chan struct{}) @@ -253,9 +249,7 @@ func TestSendPacket(t *testing.T) { received := make(chan []byte, 1) receiverReady := make(chan struct{}) var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { conn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 39998}) if err != nil { t.Errorf("Failed to listen: %v", err) @@ -273,7 +267,7 @@ func TestSendPacket(t *testing.T) { return } received <- payload[:n] - }() + }) // Wait until the receiver is actually bound and ready to receive <-receiverReady diff --git a/single/single_test.go b/single/single_test.go index 011d5a1..7b2ccf6 100644 --- a/single/single_test.go +++ b/single/single_test.go @@ -83,7 +83,7 @@ func receiver(ctx context.Context, wg *sync.WaitGroup, ip string, port int, t *t } // read all flows from the payload count := int(header.FlowCount) - for i := 0; i < count; i++ { + for range count { flow := netflow.GenericFlow{} err := binary.Read(reader, binary.BigEndian, &flow) if err != nil { diff --git a/utils/utils.go b/utils/utils.go index 82d0a58..8186cfc 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -14,7 +14,7 @@ import ( ) // BinaryDecoder decodes the given payload from a binary stream into multiple destinations. -func BinaryDecoder(payload io.Reader, dests ...interface{}) error { +func BinaryDecoder(payload io.Reader, dests ...any) error { for _, dest := range dests { err := binary.Read(payload, binary.BigEndian, dest) if err != nil { @@ -25,7 +25,7 @@ func BinaryDecoder(payload io.Reader, dests ...interface{}) error { } // ToBytes converts an interface to a gob-encoded byte stream. -func ToBytes(key interface{}) ([]byte, error) { +func ToBytes(key any) ([]byte, error) { var buf bytes.Buffer enc := gob.NewEncoder(&buf) err := enc.Encode(key) diff --git a/utils/utils_test.go b/utils/utils_test.go index 063ba48..327826a 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -76,7 +76,7 @@ func TestIPto32(t *testing.T) { func TestRandomNum(t *testing.T) { t.Parallel() count := 10000 - for i := 0; i < count; i++ { + for range count { min := 10 max := 250 result := RandomNum(min, max) @@ -104,7 +104,7 @@ func TestRandomIP(t *testing.T) { cidr = "10.0.0.0/8" itr = 10000 ) - for i := 0; i < itr; i++ { + for range itr { _, ipNet, _ := net.ParseCIDR(cidr) result, _ := RandomIP(cidr)