From c9b9e3084bdd5a06b0aa4f2b601b496bef6a2744 Mon Sep 17 00:00:00 2001 From: Simone Rodigari Date: Fri, 22 Nov 2024 09:30:51 +0000 Subject: [PATCH] feat(ct-metrics): add initial conntrack metrics for Prometheus --- pkg/metrics/metrics.go | 20 +++++++ pkg/metrics/types.go | 8 +++ pkg/plugin/conntrack/_cprog/conntrack.c | 50 +++++++++++++++++- pkg/plugin/conntrack/conntrack_bpfel_x86.go | 15 +++++- pkg/plugin/conntrack/conntrack_bpfel_x86.o | Bin 0 -> 2128 bytes pkg/plugin/conntrack/conntrack_linux.go | 40 ++++++++++++++ pkg/plugin/conntrack/types_linux.go | 37 +++++++++++-- .../packetparser/packetparser_bpfel_x86.go | 11 ++++ .../packetparser/packetparser_bpfel_x86.o | Bin 0 -> 59272 bytes pkg/utils/attr_utils.go | 3 ++ pkg/utils/metric_names.go | 8 ++- 11 files changed, 184 insertions(+), 8 deletions(-) diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index fde18e4ab6..ef7cab451a 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -140,6 +140,26 @@ func InitializeMetrics() { dnsResponseCounterDescription, ) + // Conntrack Metrics + ConntrackPacketsCounter = exporter.CreatePrometheusGaugeVecForMetric( + exporter.DefaultRegistry, + utils.ConntrackPacketsCounterName, + conntrackPacketsCountDescription, + utils.ConntrackLabels..., + ) + ConntrackPacketsBytesCounter = exporter.CreatePrometheusGaugeVecForMetric( + exporter.DefaultRegistry, + utils.ConntrackBytesCounterName, + conntrackPacketsBytesCountDescription, + utils.ConntrackLabels..., + ) + ConntrackConnectionsCounter = exporter.CreatePrometheusCounterVecForMetric( + exporter.DefaultRegistry, + utils.ConntrackConnectionsCounterName, + conntrackConnectionsCountDescription, + utils.ConntrackLabels..., + ) + // InfiniBand Metrics InfinibandStatsGauge = exporter.CreatePrometheusGaugeVecForMetric( exporter.DefaultRegistry, diff --git a/pkg/metrics/types.go b/pkg/metrics/types.go index cff5c28884..a442d4a754 100644 --- a/pkg/metrics/types.go +++ b/pkg/metrics/types.go @@ -39,6 +39,9 @@ const ( dnsResponseCounterDescription = "DNS responses by statistics" infinibandStatsGaugeDescription = "InfiniBand statistics gauge" infinibandStatusParamsGaugeDescription = "InfiniBand Status Parameters gauge" + conntrackPacketsCountDescription = "Number of packets per connection" + conntrackPacketsBytesCountDescription = "Number of bytes per connection" + conntrackConnectionsCountDescription = "Number of connections" // Control plane metrics pluginManagerFailedToReconcileCounterDescription = "Number of times the plugin manager failed to reconcile the plugins" @@ -89,6 +92,11 @@ var ( InfinibandStatsGauge GaugeVec InfinibandStatusParamsGauge GaugeVec + + // Conntrack metrics + ConntrackPacketsCounter GaugeVec + ConntrackPacketsBytesCounter GaugeVec + ConntrackConnectionsCounter CounterVec ) func ToPrometheusType(metric interface{}) prometheus.Collector { diff --git a/pkg/plugin/conntrack/_cprog/conntrack.c b/pkg/plugin/conntrack/_cprog/conntrack.c index c078ff78ab..cba579cdf4 100644 --- a/pkg/plugin/conntrack/_cprog/conntrack.c +++ b/pkg/plugin/conntrack/_cprog/conntrack.c @@ -78,6 +78,25 @@ struct { __uint(pinning, LIBBPF_PIN_BY_NAME); // needs pinning so this can be access from other processes .i.e debug cli } retina_conntrack SEC(".maps"); +/* +* Metric to store the number of packets and bytes for a connection. +*/ +struct conntrack_metric_entry { + __u64 packet_count; + __u64 byte_count; + __u8 observation_point; + __u8 traffic_direction; +}; + +struct { + __uint(type, BPF_MAP_TYPE_LRU_HASH); + __uint(max_entries, 512); + __type(key, struct ct_v4_key); + __type(value, struct conntrack_metric_entry); + __uint(pinning, LIBBPF_PIN_BY_NAME); // needs pinning so this can be access from other processes .i.e debug cli +} retina_conntrack_metrics SEC(".maps"); + + /** * Helper function to reverse a key. * @arg reverse_key The key to store the reversed key. @@ -94,6 +113,30 @@ static inline void _ct_reverse_key(struct ct_v4_key *reverse_key, const struct c reverse_key->proto = key->proto; } +static __always_inline void _ct_update_counter_metrics(struct ct_v4_key *key, struct packet *p) { + // lookup if the key exists in packet metrics map + struct conntrack_metric_entry *ct_metric_entry = bpf_map_lookup_elem(&retina_conntrack_metrics, key); + + if (!ct_metric_entry) { + // create a new entry + struct conntrack_metric_entry new_ct_metric_entry; + __builtin_memset(&new_ct_metric_entry, 0, sizeof(struct conntrack_metric_entry)); + // initialize the new entry + new_ct_metric_entry.packet_count = 1; + new_ct_metric_entry.byte_count = p->bytes; + new_ct_metric_entry.observation_point = p->observation_point; + new_ct_metric_entry.traffic_direction = p->traffic_direction; + // update bpf map for packet metrics + bpf_map_update_elem(&retina_conntrack_metrics, key, &new_ct_metric_entry, BPF_ANY); + } else { + // Update packet and byte counters + ct_metric_entry->packet_count += 1; + ct_metric_entry->byte_count += p->bytes; + ct_metric_entry->observation_point = p->observation_point; + ct_metric_entry->traffic_direction = p->traffic_direction; + } +} + /** * Returns the traffic direction based on the observation point. * @arg observation_point The point in the network stack where the packet is observed. @@ -168,6 +211,8 @@ static __always_inline bool _ct_handle_tcp_connection(struct packet *p, struct c // Update packet accordingly. p->is_reply = false; p->traffic_direction = _ct_get_traffic_direction(observation_point); + // Update metrics this initializes the key if it does not exist + _ct_update_counter_metrics(&key, p); // Create a new connection with a timeout of CT_SYN_TIMEOUT. return _ct_create_new_tcp_connection(key, p->flags, observation_point); } @@ -298,7 +343,6 @@ static __always_inline bool _ct_should_report_packet(struct ct_entry *entry, __u * Returns true if the packet should be report to userspace. False otherwise. */ static __always_inline __attribute__((unused)) bool ct_process_packet(struct packet *p, __u8 observation_point) { - if (!p) { return false; } @@ -310,6 +354,10 @@ static __always_inline __attribute__((unused)) bool ct_process_packet(struct pac key.src_port = p->src_port; key.dst_port = p->dst_port; key.proto = p->proto; + + // Update metrics this initializes the key if it does not exist + _ct_update_counter_metrics(&key, p); + // Lookup the connection in the map. struct ct_entry *entry = bpf_map_lookup_elem(&retina_conntrack, &key); diff --git a/pkg/plugin/conntrack/conntrack_bpfel_x86.go b/pkg/plugin/conntrack/conntrack_bpfel_x86.go index 9dede98e2c..914388a73f 100644 --- a/pkg/plugin/conntrack/conntrack_bpfel_x86.go +++ b/pkg/plugin/conntrack/conntrack_bpfel_x86.go @@ -12,6 +12,14 @@ import ( "github.com/cilium/ebpf" ) +type conntrackConntrackMetricEntry struct { + PacketCount uint64 + ByteCount uint64 + ObservationPoint uint8 + TrafficDirection uint8 + _ [6]byte +} + type conntrackCtEntry struct { EvictionTime uint32 LastReportTxDir uint32 @@ -78,7 +86,8 @@ type conntrackProgramSpecs struct { // // It can be passed ebpf.CollectionSpec.Assign. type conntrackMapSpecs struct { - RetinaConntrack *ebpf.MapSpec `ebpf:"retina_conntrack"` + RetinaConntrack *ebpf.MapSpec `ebpf:"retina_conntrack"` + RetinaConntrackMetrics *ebpf.MapSpec `ebpf:"retina_conntrack_metrics"` } // conntrackObjects contains all objects after they have been loaded into the kernel. @@ -100,12 +109,14 @@ func (o *conntrackObjects) Close() error { // // It can be passed to loadConntrackObjects or ebpf.CollectionSpec.LoadAndAssign. type conntrackMaps struct { - RetinaConntrack *ebpf.Map `ebpf:"retina_conntrack"` + RetinaConntrack *ebpf.Map `ebpf:"retina_conntrack"` + RetinaConntrackMetrics *ebpf.Map `ebpf:"retina_conntrack_metrics"` } func (m *conntrackMaps) Close() error { return _ConntrackClose( m.RetinaConntrack, + m.RetinaConntrackMetrics, ) } diff --git a/pkg/plugin/conntrack/conntrack_bpfel_x86.o b/pkg/plugin/conntrack/conntrack_bpfel_x86.o index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..eee7369508177d1b7d99383281b198b5be26893d 100644 GIT binary patch literal 2128 zcmbtVOKTKC5U$-so%o2x_an-JAmU*mF+}k+Mk0!eA;v>QY-e{nn<2Y1<4o_G^v>oxORrTq=>168)gu za$cgj?K$@~^~w8VWFGe~?9G=F9kq!^hlm&-@`lPVUDeVsc%^R=Pf;0Q{|K-RfVeng zA%wSb48Z*GKIqfH4uItxz`O!X0KAS5T(W!)`UnMQ^m<_H|lHx9zj$OkImd!W~V zGvH&;8^9#^0ccLT1VBa)8B-e=%I>~^Z|#o{+erC1^bO==Q^Ooz13zIWW6OQmPr&*7 z)__fb`8x0%euy`Y;g#VfepWuf!gr~-Kil!%kmG9pMFu1?BX5G<~@H2gn z0bAflMTdESaE()%8BSrONQlex^OqOJo$L2z#lqs;tPoUJqB|v4WRJ46F2aNwnPSCO z5~oVrnj~qgVyw|QKS`Y^3!9N_RLruSiRs*xEpr;;F6O@aa*%Q<8L6~KvK!V_7)L^d zZAq;F6;sJIgjyAiFx}!xjZ3A$(o$IGX{nQGsTDLckx7YlOw54CW`$XvAUe@X6hDrr z7RN0TGd!wZB8fNO4O$&Z?O;{67|M*2Fp9#cNvTv}6o`5pq4%J^LVYIMQl)6gY%0N= zREX=~q-#ATEsblLOuK>JZxY+{nx4wfrWHrc3J<@PvIR1#wu2gRdQ9?(#1pQcUGLBYOB?4i=fd+aYLw@sZ<3u z=w4gvRJN)!3s+I$|6~^Fw&g7ged7-Wbr07F9$mJJ^%m! literal 0 HcmV?d00001 diff --git a/pkg/plugin/conntrack/conntrack_linux.go b/pkg/plugin/conntrack/conntrack_linux.go index 0f67b427ef..2835a40f1d 100644 --- a/pkg/plugin/conntrack/conntrack_linux.go +++ b/pkg/plugin/conntrack/conntrack_linux.go @@ -17,6 +17,8 @@ import ( "github.com/microsoft/retina/pkg/utils" "github.com/pkg/errors" "go.uber.org/zap" + + "github.com/microsoft/retina/pkg/metrics" ) //go:generate go run github.com/cilium/ebpf/cmd/bpf2go@master -cflags "-g -O2 -Wall -D__TARGET_ARCH_${GOARCH} -Wall" -target ${GOARCH} -type ct_v4_key conntrack ./_cprog/conntrack.c -- -I../lib/_${GOARCH} -I../lib/common/libbpf/_src -I../lib/common/libbpf/_include/linux -I../lib/common/libbpf/_include/uapi/linux -I../lib/common/libbpf/_include/asm @@ -63,9 +65,25 @@ func New() (*Conntrack, error) { ct.objs = objs // Get the conntrack map from the objects ct.ctMap = objs.RetinaConntrack + ct.ctMetricsMap = objs.RetinaConntrackMetrics return ct, nil } +func (ct *Conntrack) updateConntrackMetrics() { + var key conntrackCtV4Key + var value conntrackConntrackMetricEntry + iter := ct.ctMetricsMap.Iterate() + info, err := ct.ctMetricsMap.Info() + if err != nil { + ct.l.Error("Failed to get conntrack metrics map info", zap.Error(err)) + return + } + ct.l.Debug("Iterating over conntrack metrics" + info.Name) + for iter.Next(&key, &value) { + ct.conntrackMetricAdd(key, float64(value.PacketCount), float64(value.ByteCount), uint8(value.ObservationPoint), uint8(value.TrafficDirection)) + } +} + // Run starts the Conntrack garbage collection loop. func (ct *Conntrack) Run(ctx context.Context) error { ticker := time.NewTicker(ct.gcFrequency) @@ -93,8 +111,13 @@ func (ct *Conntrack) Run(ctx context.Context) error { // List of keys to be deleted var keysToDelete []conntrackCtV4Key + ct.updateConntrackMetrics() + iter := ct.ctMap.Iterate() for iter.Next(&key, &value) { + + // TODO: remove this once the metrics are updated + // ct.conntrackMetricAdd(key, 2, 4) noOfCtEntries++ // Check if the connection is closing or has expired if ktime.MonotonicOffset.Seconds()+float64(value.EvictionTime) < float64((time.Now().Unix())) { @@ -139,3 +162,20 @@ func (ct *Conntrack) Run(ctx context.Context) error { } } } + +func (ct *Conntrack) conntrackMetricAdd(key conntrackCtV4Key, count float64, bytes float64, observationPoint uint8, direction uint8) { + + srcIP := utils.Int2ip(key.SrcIp).To4() + dstIP := utils.Int2ip(key.DstIp).To4() + + labels := []string{ + srcIP.String(), + dstIP.String(), + decodeProto(key.Proto), + decodeObservationPoint(observationPoint), + decodeDirection(direction), + } + metrics.ConntrackPacketsCounter.WithLabelValues(labels...).Set(float64(count)) + metrics.ConntrackPacketsBytesCounter.WithLabelValues(labels...).Set(float64(bytes)) + // TODO metrics.ConntrackConnectionsCounter.WithLabelValues(labels...).Inc() +} diff --git a/pkg/plugin/conntrack/types_linux.go b/pkg/plugin/conntrack/types_linux.go index 637fcade4a..1e84954ef9 100644 --- a/pkg/plugin/conntrack/types_linux.go +++ b/pkg/plugin/conntrack/types_linux.go @@ -13,10 +13,11 @@ const ( ) type Conntrack struct { - l *log.ZapLogger - objs *conntrackObjects - ctMap *ebpf.Map - gcFrequency time.Duration + l *log.ZapLogger + objs *conntrackObjects + ctMap *ebpf.Map + ctMetricsMap *ebpf.Map + gcFrequency time.Duration } // Define TCP flag constants @@ -74,3 +75,31 @@ func decodeProto(proto uint8) string { return "Not supported" } } + +func decodeDirection(trafficDirection uint8) string { + switch trafficDirection { + case 0: // nolint:gomnd // TRAFFIC_DIRECTION_UNKNOWN + return "TRAFFIC_DIRECTION_UNKNOWN" + case 1: // nolint:gomnd // TRAFFIC_DIRECTION_INGRESS + return "TRAFFIC_DIRECTION_INGRESS" + case 2: // nolint:gomnd // TRAFFIC_DIRECTION_EGRESS + return "TRAFFIC_DIRECTION_EGRESS" + default: + return "Not supported" + } +} + +func decodeObservationPoint(trafficDirection uint8) string { + switch trafficDirection { + case 0: // nolint:gomnd // OBSERVATION_POINT_FROM_ENDPOINT + return "OBSERVATION_POINT_FROM_ENDPOINT" + case 1: // nolint:gomnd // OBSERVATION_POINT_TO_ENDPOINT + return "OBSERVATION_POINT_TO_ENDPOINT" + case 2: // nolint:gomnd // OBSERVATION_POINT_FROM_NETWORK + return "OBSERVATION_POINT_FROM_NETWORK" + case 3: // nolint:gomnd // OBSERVATION_POINT_TO_NETWORK + return "OBSERVATION_POINT_TO_NETWORK" + default: + return "Not supported" + } +} diff --git a/pkg/plugin/packetparser/packetparser_bpfel_x86.go b/pkg/plugin/packetparser/packetparser_bpfel_x86.go index 6d3cb0bfe0..cf074ffb8f 100644 --- a/pkg/plugin/packetparser/packetparser_bpfel_x86.go +++ b/pkg/plugin/packetparser/packetparser_bpfel_x86.go @@ -12,6 +12,14 @@ import ( "github.com/cilium/ebpf" ) +type packetparserConntrackMetricEntry struct { + PacketCount uint64 + ByteCount uint64 + ObservationPoint uint8 + TrafficDirection uint8 + _ [6]byte +} + type packetparserCtEntry struct { EvictionTime uint32 LastReportTxDir uint32 @@ -109,6 +117,7 @@ type packetparserProgramSpecs struct { // It can be passed ebpf.CollectionSpec.Assign. type packetparserMapSpecs struct { RetinaConntrack *ebpf.MapSpec `ebpf:"retina_conntrack"` + RetinaConntrackMetrics *ebpf.MapSpec `ebpf:"retina_conntrack_metrics"` RetinaFilter *ebpf.MapSpec `ebpf:"retina_filter"` RetinaPacketparserEvents *ebpf.MapSpec `ebpf:"retina_packetparser_events"` } @@ -133,6 +142,7 @@ func (o *packetparserObjects) Close() error { // It can be passed to loadPacketparserObjects or ebpf.CollectionSpec.LoadAndAssign. type packetparserMaps struct { RetinaConntrack *ebpf.Map `ebpf:"retina_conntrack"` + RetinaConntrackMetrics *ebpf.Map `ebpf:"retina_conntrack_metrics"` RetinaFilter *ebpf.Map `ebpf:"retina_filter"` RetinaPacketparserEvents *ebpf.Map `ebpf:"retina_packetparser_events"` } @@ -140,6 +150,7 @@ type packetparserMaps struct { func (m *packetparserMaps) Close() error { return _PacketparserClose( m.RetinaConntrack, + m.RetinaConntrackMetrics, m.RetinaFilter, m.RetinaPacketparserEvents, ) diff --git a/pkg/plugin/packetparser/packetparser_bpfel_x86.o b/pkg/plugin/packetparser/packetparser_bpfel_x86.o index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..74ba30a04de314274619830e490dd0bb0fbe02e6 100644 GIT binary patch literal 59272 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