@@ -3,14 +3,18 @@ package modules
33import (
44 "bytes"
55 "context"
6+ "encoding/binary"
67 "errors"
78 "fmt"
89 "net"
910 "strconv"
11+ "strings"
1012 "sync"
1113 "time"
1214
13- "github.com/tehmaze/netflow"
15+ "github.com/netsampler/goflow2/decoders/netflow"
16+ "github.com/netsampler/goflow2/decoders/netflowlegacy"
17+ tehmaze "github.com/tehmaze/netflow"
1418 "github.com/tehmaze/netflow/session"
1519 "github.com/utmstack/UTMStack/agent/config"
1620 "github.com/utmstack/UTMStack/agent/logservice"
@@ -23,44 +27,128 @@ var (
2327 netflowOnce sync.Once
2428)
2529
30+ // templateSystem implements netflow.NetFlowTemplateSystem for goflow2
31+ type templateSystem struct {
32+ templates map [uint16 ]map [uint32 ]map [uint16 ]interface {}
33+ mu sync.RWMutex
34+ }
35+
36+ func newTemplateSystem () * templateSystem {
37+ return & templateSystem {
38+ templates : make (map [uint16 ]map [uint32 ]map [uint16 ]interface {}),
39+ }
40+ }
41+
42+ func (t * templateSystem ) GetTemplate (version uint16 , obsDomainId uint32 , templateId uint16 ) (interface {}, error ) {
43+ t .mu .RLock ()
44+ defer t .mu .RUnlock ()
45+
46+ if versionMap , ok := t .templates [version ]; ok {
47+ if domainMap , ok := versionMap [obsDomainId ]; ok {
48+ if template , ok := domainMap [templateId ]; ok {
49+ return template , nil
50+ }
51+ }
52+ }
53+ return nil , fmt .Errorf ("template not found: version=%d, obsDomainId=%d, templateId=%d" , version , obsDomainId , templateId )
54+ }
55+
56+ func (t * templateSystem ) AddTemplate (version uint16 , obsDomainId uint32 , template interface {}) {
57+ t .mu .Lock ()
58+ defer t .mu .Unlock ()
59+
60+ if _ , ok := t .templates [version ]; ! ok {
61+ t .templates [version ] = make (map [uint32 ]map [uint16 ]interface {})
62+ }
63+ if _ , ok := t.templates [version ][obsDomainId ]; ! ok {
64+ t.templates [version ][obsDomainId ] = make (map [uint16 ]interface {})
65+ }
66+
67+ // Extract template ID based on type
68+ var templateId uint16
69+ switch tmpl := template .(type ) {
70+ case netflow.TemplateRecord :
71+ templateId = tmpl .TemplateId
72+ case netflow.IPFIXOptionsTemplateRecord :
73+ templateId = tmpl .TemplateId
74+ case netflow.NFv9OptionsTemplateRecord :
75+ templateId = tmpl .TemplateId
76+ default :
77+ return
78+ }
79+
80+ t.templates [version ][obsDomainId ][templateId ] = template
81+ }
82+
2683type NetflowModule struct {
27- DataType string
28- Parser parser.Parser
29- Decoders map [string ]* netflow.Decoder
30- Listener * net.UDPConn
31- CTX context.Context
32- Cancel context.CancelFunc
33- IsEnabled bool
84+ DataType string
85+ Parser parser.Parser
86+ LegacyDecoders map [string ]* tehmaze.Decoder // For v1, v6, v7 (tehmaze/netflow)
87+ TemplateSystem map [string ]* templateSystem // For v5, v9, IPFIX (goflow2)
88+ Listener * net.UDPConn
89+ CTX context.Context
90+ Cancel context.CancelFunc
91+ IsEnabled bool
92+ mu sync.RWMutex
3493}
3594
3695func GetNetflowModule () * NetflowModule {
3796 netflowOnce .Do (func () {
3897 netflowModule = & NetflowModule {
39- Parser : parser .GetParser ("netflow" ),
40- DataType : "netflow" ,
41- IsEnabled : false ,
42- Decoders : make (map [string ]* netflow.Decoder ),
98+ Parser : parser .GetParser ("netflow" ),
99+ DataType : "netflow" ,
100+ IsEnabled : false ,
101+ LegacyDecoders : make (map [string ]* tehmaze.Decoder ),
102+ TemplateSystem : make (map [string ]* templateSystem ),
43103 }
44104 })
45105 return netflowModule
46106}
47107
108+ func (m * NetflowModule ) getOrCreateTemplateSystem (addr string ) * templateSystem {
109+ m .mu .Lock ()
110+ defer m .mu .Unlock ()
111+
112+ if ts , ok := m .TemplateSystem [addr ]; ok {
113+ return ts
114+ }
115+ ts := newTemplateSystem ()
116+ m .TemplateSystem [addr ] = ts
117+ return ts
118+ }
119+
120+ func (m * NetflowModule ) getOrCreateLegacyDecoder (addr string ) * tehmaze.Decoder {
121+ m .mu .Lock ()
122+ defer m .mu .Unlock ()
123+
124+ if d , ok := m .LegacyDecoders [addr ]; ok {
125+ return d
126+ }
127+ s := session .New ()
128+ d := tehmaze .NewDecoder (s )
129+ m .LegacyDecoders [addr ] = d
130+ return d
131+ }
132+
133+ func (m * NetflowModule ) removeLegacyDecoder (addr string ) {
134+ m .mu .Lock ()
135+ defer m .mu .Unlock ()
136+ delete (m .LegacyDecoders , addr )
137+ }
138+
48139func (m * NetflowModule ) EnablePort (proto string , enableTLS bool ) error {
49140 if enableTLS {
50141 return fmt .Errorf ("TLS not supported for NetFlow protocol" )
51142 }
52143
53144 if proto == "udp" && ! m .IsEnabled {
54- utils .Logger .Info ("Server %s listening in port: %s protocol: UDP" , m .DataType , config .ProtoPorts [config .DataTypeNetflow ].UDP )
55- m .IsEnabled = true
56-
57145 port , err := strconv .Atoi (config .ProtoPorts [config .DataTypeNetflow ].UDP )
58146 if err != nil {
59147 utils .Logger .ErrorF ("error converting port to int: %v" , err )
60148 return err
61149 }
62150
63- m . Listener , err = net .ListenUDP ("udp" , & net.UDPAddr {
151+ listener , err : = net .ListenUDP ("udp" , & net.UDPAddr {
64152 Port : port ,
65153 IP : net .ParseIP ("0.0.0.0" ),
66154 })
@@ -69,9 +157,13 @@ func (m *NetflowModule) EnablePort(proto string, enableTLS bool) error {
69157 return err
70158 }
71159
160+ m .IsEnabled = true
161+ m .Listener = listener
72162 m .CTX , m .Cancel = context .WithCancel (context .Background ())
73163
74- buffer := make ([]byte , 2048 )
164+ utils .Logger .Info ("Server %s listening in port: %s protocol: UDP" , m .DataType , config .ProtoPorts [config .DataTypeNetflow ].UDP )
165+
166+ buffer := make ([]byte , 65535 )
75167
76168 go func () {
77169 for {
@@ -97,16 +189,54 @@ func (m *NetflowModule) EnablePort(proto string, enableTLS bool) error {
97189 continue
98190 }
99191
100- d , found := m .Decoders [addr .String ()]
101- if ! found {
102- s := session .New ()
103- d = netflow .NewDecoder (s )
104- m .Decoders [addr .String ()] = d
192+ // Validate packet structure before attempting to decode
193+ packetData := buffer [:length ]
194+ packetInfo , validationErr := validateNetflowPacket (packetData )
195+ if validationErr != nil {
196+ utils .Logger .ErrorF ("invalid NetFlow packet from %s (length: %d bytes): %v" , addr .String (), length , validationErr )
197+ continue
105198 }
106199
107- message , err := d .Read (bytes .NewBuffer (buffer [:length ]))
108- if err != nil {
109- utils .Logger .ErrorF ("error decoding NetFlow message: %v" , err )
200+ var message interface {}
201+
202+ // Use hybrid approach: goflow2 for v5/v9/IPFIX, tehmaze for v1/v6/v7
203+ switch packetInfo .version {
204+ case 5 :
205+ // Use goflow2 for NetFlow v5
206+ msg , err := netflowlegacy .DecodeMessage (bytes .NewBuffer (packetData ))
207+ if err != nil {
208+ utils .Logger .ErrorF ("error decoding %s message from %s: %v" , packetInfo .versionName , addr .String (), err )
209+ continue
210+ }
211+ message = msg
212+
213+ case 9 , 10 :
214+ // Use goflow2 for NetFlow v9 and IPFIX
215+ ts := m .getOrCreateTemplateSystem (addr .String ())
216+ msg , err := netflow .DecodeMessage (bytes .NewBuffer (packetData ), ts )
217+ if err != nil {
218+ // Template not found is expected when data arrives before template
219+ // This is normal NetFlow v9/IPFIX behavior, don't log as error
220+ if ! strings .Contains (err .Error (), "template not found" ) {
221+ utils .Logger .ErrorF ("error decoding %s message from %s: %v" , packetInfo .versionName , addr .String (), err )
222+ }
223+ continue
224+ }
225+ message = msg
226+
227+ case 1 , 6 , 7 :
228+ // Use tehmaze/netflow for legacy versions (v1, v6, v7)
229+ d := m .getOrCreateLegacyDecoder (addr .String ())
230+ msg , err := d .Read (bytes .NewBuffer (packetData ))
231+ if err != nil {
232+ utils .Logger .ErrorF ("error decoding %s message from %s: %v" , packetInfo .versionName , addr .String (), err )
233+ m .removeLegacyDecoder (addr .String ())
234+ continue
235+ }
236+ message = msg
237+
238+ default :
239+ utils .Logger .ErrorF ("unsupported NetFlow version %d from %s" , packetInfo .version , addr .String ())
110240 continue
111241 }
112242
@@ -159,3 +289,64 @@ func (m *NetflowModule) GetPort(proto string) string {
159289 return ""
160290 }
161291}
292+
293+ // netflowPacketInfo contains basic information about a NetFlow packet for validation
294+ type netflowPacketInfo struct {
295+ version uint16
296+ count uint16
297+ minSize int
298+ versionName string
299+ }
300+
301+ // validateNetflowPacket checks if a NetFlow packet has valid structure before decoding
302+ // Returns packet info if valid, error otherwise
303+ func validateNetflowPacket (data []byte ) (* netflowPacketInfo , error ) {
304+ if len (data ) < 4 {
305+ return nil , fmt .Errorf ("packet too small: %d bytes (minimum 4 bytes for version and count)" , len (data ))
306+ }
307+
308+ version := binary .BigEndian .Uint16 (data [0 :2 ])
309+ count := binary .BigEndian .Uint16 (data [2 :4 ])
310+
311+ info := & netflowPacketInfo {
312+ version : version ,
313+ count : count ,
314+ }
315+
316+ switch version {
317+ case 1 :
318+ info .versionName = "NetFlow v1"
319+ info .minSize = 24 + int (count )* 48 // header (24) + records (48 each)
320+ case 5 :
321+ info .versionName = "NetFlow v5"
322+ info .minSize = 24 + int (count )* 48 // header (24) + records (48 each)
323+ case 6 :
324+ info .versionName = "NetFlow v6"
325+ info .minSize = 24 + int (count )* 52 // header (24) + records (52 each)
326+ case 7 :
327+ info .versionName = "NetFlow v7"
328+ info .minSize = 24 + int (count )* 52 // header (24) + records (52 each)
329+ case 9 :
330+ info .versionName = "NetFlow v9"
331+ // NetFlow v9 header is 20 bytes, minimum packet size is just the header
332+ info .minSize = 20
333+ case 10 :
334+ info .versionName = "IPFIX"
335+ // IPFIX header is 16 bytes, field at offset 2-4 is the total message length
336+ info .minSize = 16
337+ ipfixLength := binary .BigEndian .Uint16 (data [2 :4 ])
338+ if int (ipfixLength ) != len (data ) {
339+ return nil , fmt .Errorf ("IPFIX length mismatch: header says %d bytes, received %d bytes" , ipfixLength , len (data ))
340+ }
341+ return info , nil
342+ default :
343+ return nil , fmt .Errorf ("unsupported NetFlow version: %d" , version )
344+ }
345+
346+ if len (data ) < info .minSize {
347+ return nil , fmt .Errorf ("%s packet too small: received %d bytes, minimum expected %d bytes (count=%d)" ,
348+ info .versionName , len (data ), info .minSize , count )
349+ }
350+
351+ return info , nil
352+ }
0 commit comments