From f5a7e2ab1330d0a23bbe7e881554571541730d87 Mon Sep 17 00:00:00 2001 From: Philipp Mieden Date: Sun, 6 Sep 2026 14:54:10 +0200 Subject: [PATCH] Stop audit record serialization from crashing on absent nested messages Optional nested protobuf messages are nil whenever the protocol did not carry them, which is the common case rather than an edge case: a BFD packet without authentication segfaulted the entire capture during CSV export, because getString had a value receiver and dereferenced the nil AuthHeader. Dot11, CiscoDiscoveryInfo and both LinkLayerDiscovery records crashed the same way. Give every toString/getString helper a pointer receiver and a nil guard, following the existing GRERouting convention. While covering all record types, CSVHeader and CSVRecord turned out to disagree for six of them, so values were written under the wrong column: IPv4 emitted Padding with no header entry, GRE declared SrcPort/DstPort that the message does not have, Modbus and QUICClientHello and TLSClientHello emitted payload, tag and handshake fields the header omitted, and Service declared Notes without ever emitting it. The GRE mismatch also made Inc() panic on inconsistent Prometheus label cardinality. Keep the newly exposed per-packet values out of the metric labels via dedicated metricValues helpers, otherwise every packet would create its own time series. Sort the QUIC tag values so that column is reproducible. Add a test that exercises every registered audit record type zero-valued. --- io/audit_record_nil_test.go | 90 ++++++++++++++++++++++++ types/bfd.go | 7 +- types/cisco_discovery.go | 6 +- types/cisco_discovery_info.go | 32 +++++++++ types/dhcp4.go | 6 +- types/dhcp6.go | 6 +- types/dns.go | 20 ++++++ types/dot11.go | 32 ++++++++- types/geneve.go | 6 +- types/gre.go | 2 - types/http.go | 4 ++ types/icmp6rs.go | 6 +- types/igmp.go | 6 +- types/ip4.go | 32 +++++---- types/ip6.go | 6 +- types/ip6hop.go | 8 +++ types/lld.go | 12 +++- types/lldi.go | 20 ++++++ types/modbus.go | 49 ++++++++++--- types/ospfv2.go | 84 +++++++++++++++++++++-- types/ospfv3.go | 6 +- types/quic_client_hello.go | 126 ++++++++++++++++++++++++---------- types/service.go | 2 + types/smtp.go | 12 +++- types/tls_client_hello.go | 67 +++++++++++++++++- 25 files changed, 562 insertions(+), 85 deletions(-) create mode 100644 io/audit_record_nil_test.go diff --git a/io/audit_record_nil_test.go b/io/audit_record_nil_test.go new file mode 100644 index 00000000..6928e151 --- /dev/null +++ b/io/audit_record_nil_test.go @@ -0,0 +1,90 @@ +package io + +import ( + "runtime/debug" + "testing" + + "github.com/dreadl0ck/netcap/encoder" + "github.com/dreadl0ck/netcap/types" +) + +// TestAuditRecordSerializationOnZeroValue guards against nil dereferences in the +// serialization helpers. Optional nested messages are nil whenever the protocol +// did not carry them, which is the common case rather than an edge case: BFD +// packets without authentication used to segfault the whole capture here. +func TestAuditRecordSerializationOnZeroValue(t *testing.T) { + // Encode() dereferences the encoder config, which the --encode path installs. + encoder.SetConfig(&encoder.Config{ZScore: true}) + + covered := 0 + + for num, name := range types.Type_name { + typ := types.Type(num) + if typ == types.Type_NC_Header { + continue + } + + record, ok := InitRecord(typ).(types.AuditRecord) + if !ok { + // Not every enum entry has a registered audit record implementation. + continue + } + covered++ + + t.Run(name, func(t *testing.T) { + // Recover so one broken type does not hide the rest. + defer func() { + if r := recover(); r != nil { + t.Fatalf("panic: %v\n%s", r, debug.Stack()) + } + }() + + header := record.CSVHeader() + values := record.CSVRecord() + if len(header) != len(values) { + t.Errorf("CSVHeader has %d fields, CSVRecord has %d", len(header), len(values)) + } + if encoded := record.Encode(); encoded == nil && len(header) > 0 { + t.Error("Encode returned nil") + } + if _, err := record.JSON(); err != nil { + t.Errorf("JSON: %v", err) + } + record.Time() + record.Src() + record.Dst() + record.NetcapType() + record.SetPacketContext(&types.PacketContext{}) + + // Prometheus panics when the label cardinality of the vector does + // not match the values Inc() derives from CSVRecord. Calling it + // twice is safe: WithLabelValues returns the same child. + record.Inc() + record.Inc() + }) + } + + if covered < 50 { + t.Fatalf("only %d audit record types covered, expected the full set", covered) + } +} + +// TestBFDNilAuthHeader pins the specific regression: BFD without authentication. +func TestBFDNilAuthHeader(t *testing.T) { + bfd := &types.BFD{Timestamp: 1, AuthPresent: false} + if bfd.AuthHeader != nil { + t.Fatal("expected a nil AuthHeader") + } + if got := bfd.CSVRecord(); got[len(got)-1] != "" { + t.Errorf("AuthHeader field = %q, want empty", got[len(got)-1]) + } + + withAuth := &types.BFD{ + Timestamp: 1, + AuthPresent: true, + AuthHeader: &types.BFDAuthHeader{AuthType: 2, KeyID: 3, SequenceNumber: 4, Data: []byte{0xab}}, + } + if got := withAuth.CSVRecord(); got[len(got)-1] == "" { + t.Error("populated AuthHeader must still be serialized") + } +} diff --git a/types/bfd.go b/types/bfd.go index cdb7a870..127a2d62 100644 --- a/types/bfd.go +++ b/types/bfd.go @@ -102,7 +102,12 @@ func (b *BFD) Time() int64 { return b.Timestamp } -func (bah BFDAuthHeader) getString() string { +func (bah *BFDAuthHeader) getString() string { + // BFD packets without authentication carry no auth header. + if bah == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) diff --git a/types/cisco_discovery.go b/types/cisco_discovery.go index 8d78ff7e..916f04f3 100644 --- a/types/cisco_discovery.go +++ b/types/cisco_discovery.go @@ -70,7 +70,11 @@ func (cd *CiscoDiscovery) Time() int64 { return cd.Timestamp } -func (v CiscoDiscoveryValue) toString() string { +func (v *CiscoDiscoveryValue) toString() string { + if v == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) diff --git a/types/cisco_discovery_info.go b/types/cisco_discovery_info.go index 32b48ac3..f779d89e 100644 --- a/types/cisco_discovery_info.go +++ b/types/cisco_discovery_info.go @@ -145,6 +145,10 @@ func (a *CiscoDiscoveryInfo) Time() int64 { } func (c *CDPHello) toString() string { + if c == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) @@ -177,6 +181,10 @@ func (c *CDPHello) toString() string { } func (c *CDPCapabilities) toString() string { + if c == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) @@ -204,6 +212,10 @@ func (c *CDPCapabilities) toString() string { } func (i *IPNet) toString() string { + if i == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) @@ -216,6 +228,10 @@ func (i *IPNet) toString() string { } func (c *CDPVLANDialogue) toString() string { + if c == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) @@ -228,6 +244,10 @@ func (c *CDPVLANDialogue) toString() string { } func (c *CDPPowerDialogue) toString() string { + if c == nil { + return "" + } + vals := make([]string, 0, len(c.Values)) for _, v := range c.Values { @@ -248,6 +268,10 @@ func (c *CDPPowerDialogue) toString() string { } func (c *CDPSparePairPoE) toString() string { + if c == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) @@ -264,6 +288,10 @@ func (c *CDPSparePairPoE) toString() string { } func (c *CDPEnergyWise) toString() string { + if c == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) @@ -304,6 +332,10 @@ func (c *CDPEnergyWise) toString() string { } func (c *CDPLocation) toString() string { + if c == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) diff --git a/types/dhcp4.go b/types/dhcp4.go index 5ecca0f3..3a9e6077 100644 --- a/types/dhcp4.go +++ b/types/dhcp4.go @@ -110,7 +110,11 @@ func (d *DHCPv4) Time() int64 { return d.Timestamp } -func (d DHCPOption) toString() string { +func (d *DHCPOption) toString() string { + if d == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) b.WriteString(formatInt32(d.Type)) diff --git a/types/dhcp6.go b/types/dhcp6.go index 4f7c48e5..4171f9e1 100644 --- a/types/dhcp6.go +++ b/types/dhcp6.go @@ -83,7 +83,11 @@ func (d *DHCPv6) Time() int64 { return d.Timestamp } -func (d DHCPv6Option) toString() string { +func (d *DHCPv6Option) toString() string { + if d == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) b.WriteString(formatInt32(d.Code)) diff --git a/types/dns.go b/types/dns.go index ccdffdbd..5330e2cb 100644 --- a/types/dns.go +++ b/types/dns.go @@ -133,6 +133,10 @@ func (d *DNS) Time() int64 { } func (q *DNSQuestion) toString() string { + if q == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) b.WriteString(q.Name) @@ -145,6 +149,10 @@ func (q *DNSQuestion) toString() string { } func (q *DNSResourceRecord) toString() string { + if q == nil { + return "" + } + txts := make([]string, 0, len(q.TXTs)) for _, t := range q.TXTs { txts = append(txts, string(t)) @@ -188,6 +196,10 @@ func (q *DNSResourceRecord) toString() string { } func (q *DNSSOA) toString() string { + if q == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) b.WriteString(string(q.MName)) @@ -208,6 +220,10 @@ func (q *DNSSOA) toString() string { } func (q *DNSSRV) toString() string { + if q == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) b.WriteString(formatInt32(q.Priority)) @@ -222,6 +238,10 @@ func (q *DNSSRV) toString() string { } func (q *DNSMX) toString() string { + if q == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) b.WriteString(formatInt32(q.Preference)) diff --git a/types/dot11.go b/types/dot11.go index e46fd1f0..64ab1ff6 100644 --- a/types/dot11.go +++ b/types/dot11.go @@ -89,7 +89,11 @@ func (d *Dot11) Time() int64 { return d.Timestamp } -func (d Dot11QOS) toString() string { +func (d *Dot11QOS) toString() string { + if d == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) b.WriteString(formatInt32(d.TID)) @@ -103,7 +107,11 @@ func (d Dot11QOS) toString() string { return b.String() } -func (d Dot11HTControl) toString() string { +func (d *Dot11HTControl) toString() string { + if d == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) b.WriteString(strconv.FormatBool(d.ACConstraint)) @@ -118,6 +126,10 @@ func (d Dot11HTControl) toString() string { } func (d *Dot11HTControlVHT) toString() string { + if d == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) b.WriteString(strconv.FormatBool(d.MRQ)) @@ -144,6 +156,10 @@ func (d *Dot11HTControlVHT) toString() string { } func (d *Dot11HTControlMFB) toString() string { + if d == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) b.WriteString(formatInt32(d.NumSTS)) @@ -158,6 +174,10 @@ func (d *Dot11HTControlMFB) toString() string { } func (d *Dot11HTControlHT) toString() string { + if d == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) b.WriteString(d.LinkAdapationControl.toString()) @@ -176,6 +196,10 @@ func (d *Dot11HTControlHT) toString() string { } func (d *Dot11LinkAdapationControl) toString() string { + if d == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) b.WriteString(strconv.FormatBool(d.TRQ)) @@ -194,6 +218,10 @@ func (d *Dot11LinkAdapationControl) toString() string { } func (d *Dot11ASEL) toString() string { + if d == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) b.WriteString(formatInt32(d.Command)) diff --git a/types/geneve.go b/types/geneve.go index c9249714..bb53741e 100644 --- a/types/geneve.go +++ b/types/geneve.go @@ -77,7 +77,11 @@ func (i *Geneve) Time() int64 { return i.Timestamp } -func (i GeneveOption) toString() string { +func (i *GeneveOption) toString() string { + if i == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) b.WriteString(formatInt32(i.Class)) diff --git a/types/gre.go b/types/gre.go index 404dd3e9..f4b60387 100644 --- a/types/gre.go +++ b/types/gre.go @@ -65,8 +65,6 @@ var fieldsGRE = []string{ fieldRouting, // *GRERouting fieldSrcIP, fieldDstIP, - fieldSrcPort, - fieldDstPort, } // CSVHeader returns the CSV header for the audit record. diff --git a/types/http.go b/types/http.go index 0c6ade22..0657325f 100644 --- a/types/http.go +++ b/types/http.go @@ -113,6 +113,10 @@ func (h *HTTP) CSVRecord() []string { } func (c *HTTPCookie) toString() string { + if c == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) diff --git a/types/icmp6rs.go b/types/icmp6rs.go index ae382be6..0041cfdc 100644 --- a/types/icmp6rs.go +++ b/types/icmp6rs.go @@ -61,7 +61,11 @@ func (i *ICMPv6RouterSolicitation) Time() int64 { return i.Timestamp } -func (o ICMPv6Option) toString() string { +func (o *ICMPv6Option) toString() string { + if o == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) b.WriteString(formatInt32(o.Type)) diff --git a/types/igmp.go b/types/igmp.go index d66c5dff..b572b00b 100644 --- a/types/igmp.go +++ b/types/igmp.go @@ -95,7 +95,11 @@ func (i *IGMP) Time() int64 { return i.Timestamp } -func (i IGMPv3GroupRecord) toString() string { +func (i *IGMPv3GroupRecord) toString() string { + if i == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) b.WriteString(formatInt32(i.Type)) diff --git a/types/ip4.go b/types/ip4.go index c6384b00..323a5b5b 100644 --- a/types/ip4.go +++ b/types/ip4.go @@ -51,6 +51,7 @@ var fieldsIPv4 = []string{ fieldChecksum, // int32 fieldSrcIP, // string fieldDstIP, // string + fieldPadding, // []byte //fieldOptions, // []*IPv4Option fieldPayloadEntropy, // float64 fieldPayloadSize, // int32 @@ -93,7 +94,11 @@ func (i *IPv4) Time() int64 { return i.Timestamp } -func (i IPv4Option) toString() string { +func (i *IPv4Option) toString() string { + if i == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) b.WriteString(formatInt32(i.OptionType)) @@ -205,18 +210,19 @@ func (i *IPv4) Encode() []string { // } return filter([]string{ ipv4Encoder.Int64(fieldTimestamp, i.Timestamp), - ipv4Encoder.Int32(fieldVersion, i.Version), // int32 - ipv4Encoder.Int32(fieldIHL, i.IHL), // int32 - ipv4Encoder.Int32(fieldTOS, i.TOS), // int32 - ipv4Encoder.Int32(fieldLength, i.Length), // int32 - ipv4Encoder.Int32(fieldId, i.Id), // int32 - ipv4Encoder.Int32(fieldFlags, i.Flags), // int32 - ipv4Encoder.Int32(fieldFragOffset, i.FragOffset), // int32 - ipv4Encoder.Int32(fieldTTL, i.TTL), // int32 - ipv4Encoder.Int32(fieldProtocol, i.Protocol), // int32 - ipv4Encoder.Int32(fieldChecksum, i.Checksum), // int32 - ipv4Encoder.Int64(fieldSrcIP, ipToInt64(i.SrcIP)), // string - ipv4Encoder.Int64(fieldDstIP, ipToInt64(i.DstIP)), // string + ipv4Encoder.Int32(fieldVersion, i.Version), // int32 + ipv4Encoder.Int32(fieldIHL, i.IHL), // int32 + ipv4Encoder.Int32(fieldTOS, i.TOS), // int32 + ipv4Encoder.Int32(fieldLength, i.Length), // int32 + ipv4Encoder.Int32(fieldId, i.Id), // int32 + ipv4Encoder.Int32(fieldFlags, i.Flags), // int32 + ipv4Encoder.Int32(fieldFragOffset, i.FragOffset), // int32 + ipv4Encoder.Int32(fieldTTL, i.TTL), // int32 + ipv4Encoder.Int32(fieldProtocol, i.Protocol), // int32 + ipv4Encoder.Int32(fieldChecksum, i.Checksum), // int32 + ipv4Encoder.Int64(fieldSrcIP, ipToInt64(i.SrcIP)), // string + ipv4Encoder.Int64(fieldDstIP, ipToInt64(i.DstIP)), // string + ipv4Encoder.String(fieldPadding, hex.EncodeToString(i.Padding)), // []byte //ipv4Encoder.String(fieldOptions, strings.Join(opts, "")), // []*IPv4Option ipv4Encoder.Float64(fieldPayloadEntropy, i.PayloadEntropy), // float64 ipv4Encoder.Int32(fieldPayloadSize, i.PayloadSize), // int32 diff --git a/types/ip6.go b/types/ip6.go index 09247c4e..dfa85679 100644 --- a/types/ip6.go +++ b/types/ip6.go @@ -79,7 +79,11 @@ func (i *IPv6) Time() int64 { return i.Timestamp } -func (h IPv6HopByHop) toString() string { +func (h *IPv6HopByHop) toString() string { + if h == nil { + return "" + } + opts := make([]string, 0, len(h.Options)) for _, o := range h.Options { opts = append(opts, o.toString()) diff --git a/types/ip6hop.go b/types/ip6hop.go index 4630a072..dad39e0f 100644 --- a/types/ip6hop.go +++ b/types/ip6hop.go @@ -62,6 +62,10 @@ func (l *IPv6HopByHop) Time() int64 { } func (o *IPv6HopByHopOption) toString() string { + if o == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) b.WriteString(formatInt32(o.OptionType)) // int32 @@ -74,6 +78,10 @@ func (o *IPv6HopByHopOption) toString() string { } func (a *IPv6HopByHopOptionAlignment) toString() string { + if a == nil { + return "" + } + return join(formatInt32(a.One), formatInt32(a.Two)) } diff --git a/types/lld.go b/types/lld.go index b123a0a1..629421cd 100644 --- a/types/lld.go +++ b/types/lld.go @@ -64,11 +64,19 @@ func (l *LinkLayerDiscovery) Time() int64 { return l.Timestamp } -func (l LLDPChassisID) toString() string { +func (l *LLDPChassisID) toString() string { + if l == nil { + return "" + } + return join(formatInt32(l.Subtype), hex.EncodeToString(l.ID)) } -func (l LLDPPortID) toString() string { +func (l *LLDPPortID) toString() string { + if l == nil { + return "" + } + return join(formatInt32(l.Subtype), hex.EncodeToString(l.ID)) } diff --git a/types/lldi.go b/types/lldi.go index 6b4b07a2..d6759577 100644 --- a/types/lldi.go +++ b/types/lldi.go @@ -84,10 +84,18 @@ func (l *LinkLayerDiscoveryInfo) Time() int64 { } func (lldsc *LLDPSysCapabilities) toString() string { + if lldsc == nil { + return "" + } + return lldsc.SystemCap.toString() + lldsc.EnabledCap.toString() } func (lldma *LLDPMgmtAddress) toString() string { + if lldma == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) b.WriteString(formatInt32(lldma.Subtype)) // int32 // byte @@ -104,6 +112,10 @@ func (lldma *LLDPMgmtAddress) toString() string { } func (lldst *LLDPOrgSpecificTLV) toString() string { + if lldst == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) b.WriteString(strconv.FormatUint(uint64(lldst.OUI), 10)) @@ -116,6 +128,10 @@ func (lldst *LLDPOrgSpecificTLV) toString() string { } func (lldv *LinkLayerDiscoveryValue) toString() string { + if lldv == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) b.WriteString(formatInt32(lldv.Type)) @@ -128,6 +144,10 @@ func (lldv *LinkLayerDiscoveryValue) toString() string { } func (c *LLDPCapabilities) toString() string { + if c == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) b.WriteString(strconv.FormatBool(c.Other)) diff --git a/types/modbus.go b/types/modbus.go index 13acbf7e..8a0864dd 100644 --- a/types/modbus.go +++ b/types/modbus.go @@ -44,9 +44,9 @@ var fieldsModbus = []string{ fieldProtocolID, // int32 fieldLength, // int32 fieldUnitID, // int32 - //fieldPayload, // []byte - fieldException, // bool - fieldFunctionCode, // int32 + fieldPayload, // []byte + fieldException, // bool + fieldFunctionCode, // int32 fieldSrcIP, fieldDstIP, fieldSrcPort, @@ -89,17 +89,46 @@ func (a *Modbus) JSON() (string, error) { return jsonMarshaler.MarshalToString(a) } +// Payload is unique per packet and must stay out of the metric labels. +var fieldsModbusMetrics = []string{ + fieldTransactionID, + fieldProtocolID, + fieldLength, + fieldUnitID, + fieldException, + fieldFunctionCode, + fieldSrcIP, + fieldDstIP, + fieldSrcPort, + fieldDstPort, +} + +func (a *Modbus) metricValues() []string { + return []string{ + formatInt32(a.TransactionID), + formatInt32(a.ProtocolID), + formatInt32(a.Length), + formatInt32(a.UnitID), + strconv.FormatBool(a.Exception), + formatInt32(a.FunctionCode), + a.SrcIP, + a.DstIP, + formatInt32(a.SrcPort), + formatInt32(a.DstPort), + } +} + var modbusTCPMetric = prometheus.NewCounterVec( prometheus.CounterOpts{ Name: strings.ToLower(Type_NC_Modbus.String()), Help: Type_NC_Modbus.String() + " audit records", }, - fieldsModbus[1:], + fieldsModbusMetrics, ) // Inc increments the metrics for the audit record. func (a *Modbus) Inc() { - modbusTCPMetric.WithLabelValues(a.CSVRecord()[1:]...).Inc() + modbusTCPMetric.WithLabelValues(a.metricValues()...).Inc() } // SetPacketContext sets the associated packet context for the audit record. @@ -126,11 +155,11 @@ var modbusEncoder = encoder.NewValueEncoder() func (a *Modbus) Encode() []string { return filter([]string{ modbusEncoder.Int64(fieldTimestamp, a.Timestamp), - modbusEncoder.Int32(fieldTransactionID, a.TransactionID), // int32 - modbusEncoder.Int32(fieldProtocolID, a.ProtocolID), // int32 - modbusEncoder.Int32(fieldLength, a.Length), // int32 - modbusEncoder.Int32(fieldUnitID, a.UnitID), // int32 - //hex.EncodeToString(a.Payload), + modbusEncoder.Int32(fieldTransactionID, a.TransactionID), // int32 + modbusEncoder.Int32(fieldProtocolID, a.ProtocolID), // int32 + modbusEncoder.Int32(fieldLength, a.Length), // int32 + modbusEncoder.Int32(fieldUnitID, a.UnitID), // int32 + modbusEncoder.String(fieldPayload, hex.EncodeToString(a.Payload)), // []byte modbusEncoder.Bool(a.Exception), modbusEncoder.Int32(fieldFunctionCode, a.FunctionCode), modbusEncoder.String(fieldSrcIP, a.SrcIP), diff --git a/types/ospfv2.go b/types/ospfv2.go index 53ec2b3f..7fdb895d 100644 --- a/types/ospfv2.go +++ b/types/ospfv2.go @@ -104,7 +104,11 @@ func (a *OSPFv2) Time() int64 { return a.Timestamp } -func (l LSReq) toString() string { +func (l *LSReq) toString() string { + if l == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) @@ -119,6 +123,10 @@ func (l LSReq) toString() string { } func (r *RouterLSAV2) toString() string { + if r == nil { + return "" + } + routers := make([]string, 0, len(r.Routers)) for _, e := range r.Routers { routers = append(routers, toString(e)) @@ -138,6 +146,10 @@ func (r *RouterLSAV2) toString() string { } func (r *ASExternalLSAV2) toString() string { + if r == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) @@ -156,6 +168,10 @@ func (r *ASExternalLSAV2) toString() string { } func (r *RouterLSA) toString() string { + if r == nil { + return "" + } + routers := make([]string, 0, len(r.Routers)) for _, e := range r.Routers { routers = append(routers, toString(e)) @@ -175,6 +191,10 @@ func (r *RouterLSA) toString() string { } func (r *NetworkLSA) toString() string { + if r == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) @@ -187,6 +207,10 @@ func (r *NetworkLSA) toString() string { } func (r *InterAreaPrefixLSA) toString() string { + if r == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) @@ -203,6 +227,10 @@ func (r *InterAreaPrefixLSA) toString() string { } func (r *InterAreaRouterLSA) toString() string { + if r == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) @@ -217,6 +245,10 @@ func (r *InterAreaRouterLSA) toString() string { } func (r *ASExternalLSA) toString() string { + if r == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) @@ -243,6 +275,10 @@ func (r *ASExternalLSA) toString() string { } func (r *LinkLSA) toString() string { + if r == nil { + return "" + } + prefixes := make([]string, 0, len(r.Prefixes)) for _, p := range r.Prefixes { prefixes = append(prefixes, toString(p)) @@ -266,6 +302,10 @@ func (r *LinkLSA) toString() string { } func (r *IntraAreaPrefixLSA) toString() string { + if r == nil { + return "" + } + prefixes := make([]string, 0, len(r.Prefixes)) for _, p := range r.Prefixes { prefixes = append(prefixes, toString(p)) @@ -289,6 +329,10 @@ func (r *IntraAreaPrefixLSA) toString() string { } func (r *Router) toString() string { + if r == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) @@ -309,6 +353,10 @@ func (r *Router) toString() string { } func (r *RouterV2) toString() string { + if r == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) @@ -325,6 +373,10 @@ func (r *RouterV2) toString() string { } func (l *LSA) toString() string { + if l == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) @@ -354,7 +406,11 @@ func (l *LSA) toString() string { return b.String() } -func (l LSUpdate) toString() string { +func (l *LSUpdate) toString() string { + if l == nil { + return "" + } + lsas := make([]string, 0, len(l.LSAs)) for _, lsa := range l.LSAs { lsas = append(lsas, toString(lsa)) @@ -371,7 +427,11 @@ func (l LSUpdate) toString() string { return b.String() } -func (l DbDescPkg) toString() string { +func (l *DbDescPkg) toString() string { + if l == nil { + return "" + } + headers := make([]string, 0, len(l.LSAinfo)) for _, lsa := range l.LSAinfo { headers = append(headers, toString(lsa)) @@ -394,7 +454,11 @@ func (l DbDescPkg) toString() string { return b.String() } -func (l HelloPkgV2) toString() string { +func (l *HelloPkgV2) toString() string { + if l == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) @@ -420,7 +484,11 @@ func (l HelloPkgV2) toString() string { return b.String() } -func (l LSAPrefix) toString() string { +func (l *LSAPrefix) toString() string { + if l == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) @@ -436,7 +504,11 @@ func (l LSAPrefix) toString() string { return b.String() } -func (l LSAheader) toString() string { +func (l *LSAheader) toString() string { + if l == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) diff --git a/types/ospfv3.go b/types/ospfv3.go index 0098bd7a..a673ca29 100644 --- a/types/ospfv3.go +++ b/types/ospfv3.go @@ -95,7 +95,11 @@ func (a *OSPFv3) Time() int64 { return a.Timestamp } -func (l HelloPkg) toString() string { +func (l *HelloPkg) toString() string { + if l == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) diff --git a/types/quic_client_hello.go b/types/quic_client_hello.go index 48d7e2f2..f25175b9 100644 --- a/types/quic_client_hello.go +++ b/types/quic_client_hello.go @@ -21,6 +21,7 @@ package types import ( "encoding/hex" + "sort" "strconv" "strings" "time" @@ -30,30 +31,30 @@ import ( ) const ( - fieldQUICClientHelloQUICVersion = "QUICVersion" - fieldQUICClientHelloIsIETFQUIC = "IsIETFQUIC" - fieldQUICClientHelloDCID = "DCID" - fieldQUICClientHelloSCID = "SCID" - fieldQUICClientHelloSNI = "SNI" - fieldQUICClientHelloALPNs = "ALPNs" - fieldQUICClientHelloCipherSuites = "CipherSuites" - fieldQUICClientHelloExtensions = "Extensions" - fieldQUICClientHelloSupportedGroups = "SupportedGroups" - fieldQUICClientHelloSignatureAlgs = "SignatureAlgs" - fieldQUICClientHelloSupportedVersion = "SupportedVersion" - fieldQUICClientHelloUAID = "UAID" - fieldQUICClientHelloCHLOTags = "CHLOTags" - fieldQUICClientHelloTagValues = "TagValues" - fieldQUICClientHelloJa4 = "Ja4" - fieldQUICClientHelloJa4Description = "Ja4Description" - fieldQUICClientHelloMaxIdleTimeout = "MaxIdleTimeout" - fieldQUICClientHelloInitialMaxData = "InitialMaxData" + fieldQUICClientHelloQUICVersion = "QUICVersion" + fieldQUICClientHelloIsIETFQUIC = "IsIETFQUIC" + fieldQUICClientHelloDCID = "DCID" + fieldQUICClientHelloSCID = "SCID" + fieldQUICClientHelloSNI = "SNI" + fieldQUICClientHelloALPNs = "ALPNs" + fieldQUICClientHelloCipherSuites = "CipherSuites" + fieldQUICClientHelloExtensions = "Extensions" + fieldQUICClientHelloSupportedGroups = "SupportedGroups" + fieldQUICClientHelloSignatureAlgs = "SignatureAlgs" + fieldQUICClientHelloSupportedVersion = "SupportedVersion" + fieldQUICClientHelloUAID = "UAID" + fieldQUICClientHelloCHLOTags = "CHLOTags" + fieldQUICClientHelloTagValues = "TagValues" + fieldQUICClientHelloJa4 = "Ja4" + fieldQUICClientHelloJa4Description = "Ja4Description" + fieldQUICClientHelloMaxIdleTimeout = "MaxIdleTimeout" + fieldQUICClientHelloInitialMaxData = "InitialMaxData" fieldQUICClientHelloInitialMaxStreamDataBidiLocal = "InitialMaxStreamDataBidiLocal" - fieldQUICClientHelloMaxUdpPayloadSize = "MaxUdpPayloadSize" - fieldQUICClientHelloRandom = "Random" - fieldQUICClientHelloSessionID = "SessionID" - fieldQUICClientHelloSupportedPoints = "SupportedPoints" - fieldQUICClientHelloCompressMethods = "CompressMethods" + fieldQUICClientHelloMaxUdpPayloadSize = "MaxUdpPayloadSize" + fieldQUICClientHelloRandom = "Random" + fieldQUICClientHelloSessionID = "SessionID" + fieldQUICClientHelloSupportedPoints = "SupportedPoints" + fieldQUICClientHelloCompressMethods = "CompressMethods" ) var fieldsQUICClientHello = []string{ @@ -75,6 +76,7 @@ var fieldsQUICClientHello = []string{ fieldQUICClientHelloSupportedVersion, fieldQUICClientHelloUAID, fieldQUICClientHelloCHLOTags, + fieldQUICClientHelloTagValues, fieldQUICClientHelloJa4, fieldQUICClientHelloJa4Description, fieldQUICClientHelloMaxIdleTimeout, @@ -88,18 +90,24 @@ func (q *QUICClientHello) CSVHeader() []string { return filter(fieldsQUICClientHello) } -// CSVRecord returns the CSV record for the audit record. -func (q *QUICClientHello) CSVRecord() []string { - // Convert TagValues map to string representation - tagValuesStr := "" - if q.TagValues != nil { - pairs := make([]string, 0, len(q.TagValues)) - for k, v := range q.TagValues { - pairs = append(pairs, k+"="+v) - } - tagValuesStr = strings.Join(pairs, ";") +// tagValuesString renders the gQUIC CHLO tag map as key=value pairs. +func (q *QUICClientHello) tagValuesString() string { + if q.TagValues == nil { + return "" } + pairs := make([]string, 0, len(q.TagValues)) + for k, v := range q.TagValues { + pairs = append(pairs, k+"="+v) + } + // Map iteration order is randomized: sort so the column is reproducible. + sort.Strings(pairs) + + return strings.Join(pairs, ";") +} + +// CSVRecord returns the CSV record for the audit record. +func (q *QUICClientHello) CSVRecord() []string { return filter([]string{ formatTimestamp(q.Timestamp), q.SrcIP, @@ -119,7 +127,7 @@ func (q *QUICClientHello) CSVRecord() []string { formatInt32(q.SupportedVersion), q.UAID, strings.Join(q.CHLOTags, "|"), - tagValuesStr, + q.tagValuesString(), q.Ja4, q.Ja4Description, strconv.FormatInt(q.MaxIdleTimeout, 10), @@ -142,17 +150,61 @@ func (q *QUICClientHello) JSON() (string, error) { return jsonMarshaler.MarshalToString(q) } +// Connection IDs and CHLO tag values are unique per connection and must stay +// out of the metric labels. +var fieldsQUICClientHelloMetrics = []string{ + fieldSrcIP, + fieldDstIP, + fieldSrcPort, + fieldDstPort, + fieldQUICClientHelloQUICVersion, + fieldQUICClientHelloIsIETFQUIC, + fieldQUICClientHelloSNI, + fieldQUICClientHelloALPNs, + fieldQUICClientHelloCipherSuites, + fieldQUICClientHelloExtensions, + fieldQUICClientHelloSupportedGroups, + fieldQUICClientHelloSignatureAlgs, + fieldQUICClientHelloSupportedVersion, + fieldQUICClientHelloUAID, + fieldQUICClientHelloCHLOTags, + fieldQUICClientHelloJa4, + fieldQUICClientHelloJa4Description, +} + +func (q *QUICClientHello) metricValues() []string { + return []string{ + q.SrcIP, + q.DstIP, + formatInt32(q.SrcPort), + formatInt32(q.DstPort), + q.QUICVersion, + strconv.FormatBool(q.IsIETFQUIC), + q.SNI, + strings.Join(q.ALPNs, "|"), + joinInts(q.CipherSuites), + joinInts(q.Extensions), + joinInts(q.SupportedGroups), + joinInts(q.SignatureAlgs), + formatInt32(q.SupportedVersion), + q.UAID, + strings.Join(q.CHLOTags, "|"), + q.Ja4, + q.Ja4Description, + } +} + var quicClientHelloMetric = prometheus.NewCounterVec( prometheus.CounterOpts{ Name: strings.ToLower(Type_NC_QUICClientHello.String()), Help: Type_NC_QUICClientHello.String() + " audit records", }, - fieldsQUICClientHello[1:], + fieldsQUICClientHelloMetrics, ) // Inc increments the metrics for the audit record. func (q *QUICClientHello) Inc() { - quicClientHelloMetric.WithLabelValues(q.CSVRecord()[1:]...).Inc() + quicClientHelloMetric.WithLabelValues(q.metricValues()...).Inc() } // SetPacketContext sets the associated packet context for the audit record. @@ -196,6 +248,7 @@ func (q *QUICClientHello) Encode() []string { quicClientHelloEncoder.Int32(fieldQUICClientHelloSupportedVersion, q.SupportedVersion), quicClientHelloEncoder.String(fieldQUICClientHelloUAID, q.UAID), quicClientHelloEncoder.String(fieldQUICClientHelloCHLOTags, strings.Join(q.CHLOTags, "|")), + quicClientHelloEncoder.String(fieldQUICClientHelloTagValues, q.tagValuesString()), quicClientHelloEncoder.String(fieldQUICClientHelloJa4, q.Ja4), quicClientHelloEncoder.String(fieldQUICClientHelloJa4Description, q.Ja4Description), quicClientHelloEncoder.Int64(fieldQUICClientHelloMaxIdleTimeout, q.MaxIdleTimeout), @@ -244,4 +297,3 @@ func (q *QUICClientHello) SessionIDHex() string { } return hex.EncodeToString(q.SessionID) } - diff --git a/types/service.go b/types/service.go index 2906369c..f17815a7 100644 --- a/types/service.go +++ b/types/service.go @@ -83,6 +83,7 @@ func (a *Service) CSVRecord() []string { a.Product, // string a.Vendor, // string a.Version, // string + a.Notes, // string formatInt32(a.BytesServer), // int32 formatInt32(a.BytesClient), // int32 a.Hostname, // string @@ -180,6 +181,7 @@ func (a *Service) Encode() []string { serviceEncoder.String(fieldProduct, a.Product), // string serviceEncoder.String(fieldVendor, a.Vendor), // string serviceEncoder.String(fieldVersion, a.Version), // string + serviceEncoder.String(fieldNotes, a.Notes), // string serviceEncoder.Int32(fieldBytesServer, a.BytesServer), // int32 serviceEncoder.Int32(fieldBytesClient, a.BytesClient), // int32 serviceEncoder.String(fieldHostname, a.Hostname), // string diff --git a/types/smtp.go b/types/smtp.go index 04de6430..7979fc8f 100644 --- a/types/smtp.go +++ b/types/smtp.go @@ -72,7 +72,11 @@ func (a *SMTP) Time() int64 { return a.Timestamp } -func (a SMTPCommand) getString() string { +func (a *SMTPCommand) getString() string { + if a == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) b.WriteString(formatInt32(a.Command)) @@ -82,7 +86,11 @@ func (a SMTPCommand) getString() string { return b.String() } -func (a SMTPResponse) getString() string { +func (a *SMTPResponse) getString() string { + if a == nil { + return "" + } + var b strings.Builder b.WriteString(StructureBegin) b.WriteString(formatInt32(a.ResponseCode)) diff --git a/types/tls_client_hello.go b/types/tls_client_hello.go index 4292458f..f92a3e6d 100644 --- a/types/tls_client_hello.go +++ b/types/tls_client_hello.go @@ -34,6 +34,7 @@ const ( fieldHandshakeType = "HandshakeType" fieldHandshakeLen = "HandshakeLen" fieldHandshakeVersion = "HandshakeVersion" + fieldRandom = "Random" fieldSessionIDLen = "SessionIDLen" fieldSessionID = "SessionID" fieldCipherSuiteLen = "CipherSuiteLen" @@ -57,7 +58,9 @@ var fieldsTLSClientHello = []string{ fieldHandshakeType, fieldHandshakeLen, fieldHandshakeVersion, + fieldRandom, fieldSessionIDLen, + fieldSessionID, fieldCipherSuiteLen, fieldExtensionLen, fieldSNI, @@ -128,17 +131,75 @@ func (t *TLSClientHello) JSON() (string, error) { return jsonMarshaler.MarshalToString(t) } +// Random and SessionID are unique per handshake and must stay out of the +// metric labels, otherwise every ClientHello creates its own time series. +var fieldsTLSClientHelloMetrics = []string{ + fieldType, + fieldVersion, + fieldMessageLen, + fieldHandshakeType, + fieldHandshakeLen, + fieldHandshakeVersion, + fieldSessionIDLen, + fieldCipherSuiteLen, + fieldExtensionLen, + fieldSNI, + fieldOSCP, + fieldCipherSuites, + fieldCompressMethods, + fieldSignatureAlgs, + fieldSupportedGroups, + fieldSupportedPoints, + fieldALPNs, + fieldJa4, + fieldSrcIP, + fieldDstIP, + fieldSrcMAC, + fieldDstMAC, + fieldSrcPort, + fieldDstPort, +} + +func (t *TLSClientHello) metricValues() []string { + return []string{ + formatInt32(t.Type), + formatInt32(t.Version), + formatInt32(t.MessageLen), + formatInt32(t.HandshakeType), + strconv.FormatUint(uint64(t.HandshakeLen), 10), + formatInt32(t.HandshakeVersion), + strconv.FormatUint(uint64(t.SessionIDLen), 10), + formatInt32(t.CipherSuiteLen), + formatInt32(t.ExtensionLen), + t.SNI, + strconv.FormatBool(t.OSCP), + joinInts(t.CipherSuites), + joinInts(t.CompressMethods), + joinInts(t.SignatureAlgs), + joinInts(t.SupportedGroups), + joinInts(t.SupportedPoints), + join(t.ALPNs...), + t.Ja4, + t.SrcIP, + t.DstIP, + t.SrcMAC, + t.DstMAC, + formatInt32(t.SrcPort), + formatInt32(t.DstPort), + } +} + var tlsClientMetric = prometheus.NewCounterVec( prometheus.CounterOpts{ Name: strings.ToLower(Type_NC_TLSClientHello.String()), Help: Type_NC_TLSClientHello.String() + " audit records", }, - fieldsTLSClientHello[1:], + fieldsTLSClientHelloMetrics, ) // Inc increments the metrics for the audit record. func (t *TLSClientHello) Inc() { - tlsClientMetric.WithLabelValues(t.CSVRecord()[1:]...).Inc() + tlsClientMetric.WithLabelValues(t.metricValues()...).Inc() } // SetPacketContext sets the associated packet context for the audit record. @@ -168,7 +229,9 @@ func (t *TLSClientHello) Encode() []string { tlsClientHelloEncoder.Int32(fieldHandshakeType, t.HandshakeType), tlsClientHelloEncoder.Uint32(fieldHandshakeLen, t.HandshakeLen), tlsClientHelloEncoder.Int32(fieldHandshakeVersion, t.HandshakeVersion), + tlsClientHelloEncoder.String(fieldRandom, hex.EncodeToString(t.Random)), tlsClientHelloEncoder.Uint32(fieldSessionIDLen, t.SessionIDLen), + tlsClientHelloEncoder.String(fieldSessionID, hex.EncodeToString(t.SessionID)), tlsClientHelloEncoder.Int32(fieldCipherSuiteLen, t.CipherSuiteLen), tlsClientHelloEncoder.Int32(fieldExtensionLen, t.ExtensionLen), tlsClientHelloEncoder.String(fieldSNI, t.SNI),