diff --git a/pkg/plugin/ebpfwindows/dropreasons_windows.go b/pkg/plugin/ebpfwindows/dropreasons_windows.go new file mode 100644 index 0000000000..0dbf9a86c3 --- /dev/null +++ b/pkg/plugin/ebpfwindows/dropreasons_windows.go @@ -0,0 +1,84 @@ +package ebpfwindows + +import ( + "fmt" +) + +// DropMin numbers less than this are non-drop reason codes +var DropMin uint8 = 130 + +// DropInvalid is the Invalid packet reason. +var DropInvalid uint8 = 2 + +// These values are shared with bpf/lib/common.h and api/v1/flow/flow.proto. +var dropErrors = map[uint8]string{ + 0: "Success", + 2: "Invalid packet", + 3: "Plain Text", + 4: "Interface Decrypted", + 5: "LB: No backend slot entry found", + 6: "LB: No backend entry found", + 7: "LB: Reverse entry update failed", + 8: "LB: Reverse entry stale", + 9: "Fragmented packet", + 10: "Fragmented packet entry update failed", + 11: "Missed tail call to custom program", +} + +// Keep in sync with __id_for_file in bpf/lib/source_info.h. +var files = map[uint8]string{ + + // source files from bpf/ + 1: "bpf_host.c", + 2: "bpf_lxc.c", + 3: "bpf_overlay.c", + 4: "bpf_xdp.c", + 5: "bpf_sock.c", + 6: "bpf_network.c", + + // header files from bpf/lib/ + 101: "arp.h", + 102: "drop.h", + 103: "srv6.h", + 104: "icmp6.h", + 105: "nodeport.h", + 106: "lb.h", + 107: "mcast.h", + 108: "ipv4.h", + 109: "conntrack.h", + 110: "l3.h", + 111: "trace.h", + 112: "encap.h", + 113: "encrypt.h", +} + +// BPFFileName returns the file name for the given BPF file id. +func BPFFileName(id uint8) string { + if name, ok := files[id]; ok { + return name + } + return fmt.Sprintf("unknown(%d)", id) +} + +func extendedReason(extError int8) string { + if extError == int8(0) { + return "" + } + return fmt.Sprintf("%d", extError) +} + +func DropReasonExt(reason uint8, extError int8) string { + if err, ok := dropErrors[reason]; ok { + if ext := extendedReason(extError); ext == "" { + return err + } else { + return err + ", " + ext + } + } + return fmt.Sprintf("%d, %d", reason, extError) +} + +// DropReason prints the drop reason in a human readable string +func DropReason(reason uint8) string { + return DropReasonExt(reason, int8(0)) +} diff --git a/pkg/plugin/ebpfwindows/ebpf_windows.go b/pkg/plugin/ebpfwindows/ebpf_windows.go new file mode 100644 index 0000000000..f7e35a5618 --- /dev/null +++ b/pkg/plugin/ebpfwindows/ebpf_windows.go @@ -0,0 +1,298 @@ +package ebpfwindows + +import ( + "context" + "errors" + "net" + "time" + "unsafe" + + "github.com/cilium/cilium/api/v1/flow" + v1 "github.com/cilium/cilium/pkg/hubble/api/v1" + hp "github.com/cilium/cilium/pkg/hubble/parser" + kcfg "github.com/microsoft/retina/pkg/config" + "github.com/microsoft/retina/pkg/enricher" + "github.com/microsoft/retina/pkg/log" + "github.com/microsoft/retina/pkg/metrics" + "github.com/microsoft/retina/pkg/plugin/registry" + "github.com/microsoft/retina/pkg/utils" + "github.com/sirupsen/logrus" + "go.uber.org/zap" +) + +const ( + // name of the ebpfwindows plugin + name string = "windowseBPF" + // name of the metrics + packetsReceived string = "win_packets_recv_count" + packetsSent string = "win_packets_sent_count" + bytesSent string = "win_bytes_sent_count" + bytesReceived string = "win_bytes_recv_count" + droppedPacketsIncoming string = "win_packets_recv_drop_count" + droppedPacketsOutgoing string = "win_packets_sent_drop_count" + // metrics direction + ingressLabel = "ingress" + egressLabel = "egress" +) + +var ( + ErrInvalidEventData = errors.New("The Cilium Event Data is invalid") + ErrNilEnricher = errors.New("enricher is nil") +) + +// Plugin is the ebpfwindows plugin +type Plugin struct { + l *log.ZapLogger + cfg *kcfg.Config + enricher *enricher.Enricher + externalChannel chan *v1.Event + parser *hp.Parser +} + +func init() { + registry.Add(name, New) +} + +func New(cfg *kcfg.Config) registry.Plugin { + return &Plugin{ + l: log.Logger().Named(name), + cfg: cfg, + } +} + +// Init is a no-op for the ebpfwindows plugin +func (p *Plugin) Init() error { + + parser, err := hp.New(logrus.WithField("cilium", "parser"), + nil, + nil, + nil, + nil, + nil, + nil, + nil, + ) + + if err != nil { + p.l.Fatal("Failed to create parser", zap.Error(err)) + return err + } + + p.parser = parser + return nil +} + +// Name returns the name of the ebpfwindows plugin +func (p *Plugin) Name() string { + return name +} + +// Start the plugin by starting a periodic timer. +func (p *Plugin) Start(ctx context.Context) error { + + p.l.Info("Start ebpfWindows plugin...") + p.enricher = enricher.Instance() + + if p.enricher == nil { + return ErrNilEnricher + } + + p.pullCiliumMetricsAndEvents(ctx) + return nil +} + +// metricsMapIterateCallback is the callback function that is called for each key-value pair in the metrics map. +func (p *Plugin) metricsMapIterateCallback(key *MetricsKey, value *MetricsValues) { + p.l.Info("MetricsMapIterateCallback") + p.l.Info("Key", zap.String("Key", key.String())) + p.l.Info("Value", zap.String("Value", value.String())) + + if key.IsDrop() { + if key.IsEgress() { + metrics.DropPacketsGauge.WithLabelValues(egressLabel).Set(float64(value.Count())) + } else if key.IsIngress() { + metrics.DropPacketsGauge.WithLabelValues(ingressLabel).Set(float64(value.Count())) + } + + } else { + + if key.IsEgress() { + metrics.ForwardBytesGauge.WithLabelValues(egressLabel).Set(float64(value.Bytes())) + p.l.Debug("emitting bytes sent count metric", zap.Uint64(bytesSent, value.Bytes())) + metrics.WindowsGauge.WithLabelValues(packetsSent).Set(float64(value.Count())) + p.l.Debug("emitting packets sent count metric", zap.Uint64(packetsSent, value.Count())) + } else if key.IsIngress() { + metrics.ForwardPacketsGauge.WithLabelValues(ingressLabel).Set(float64(value.Count())) + p.l.Debug("emitting packets received count metric", zap.Uint64(packetsReceived, value.Count())) + metrics.ForwardBytesGauge.WithLabelValues(ingressLabel).Set(float64(value.Bytes())) + p.l.Debug("emitting bytes received count metric", zap.Uint64(bytesReceived, value.Bytes())) + } + } +} + +// eventsMapCallback is the callback function that is called for each value in the events map. +func (p *Plugin) eventsMapCallback(data unsafe.Pointer, size uint64) int { + p.l.Info("EventsMapCallback") + p.l.Info("Size", zap.Uint64("Size", size)) + err := p.handleTraceEvent(data, size) + + if err != nil { + p.l.Error("Error handling trace event", zap.Error(err)) + return -1 + } + + return 0 +} + +// pullCiliumeBPFMetrics is the function that is called periodically by the timer. +func (p *Plugin) pullCiliumMetricsAndEvents(ctx context.Context) { + + eventsMap := NewEventsMap() + metricsMap := NewMetricsMap() + + err := eventsMap.RegisterForCallback(p.eventsMapCallback) + + if err != nil { + p.l.Error("Error registering for events map callback", zap.Error(err)) + return + } + + ticker := time.NewTicker(p.cfg.MetricsInterval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + err := metricsMap.IterateWithCallback(p.metricsMapIterateCallback) + if err != nil { + p.l.Error("Error iterating metrics map", zap.Error(err)) + } + case <-ctx.Done(): + p.l.Error("ebpfwindows plugin canceling", zap.Error(ctx.Err())) + eventsMap.UnregisterForCallback() + return + } + } +} + +// SetupChannel saves the external channel to which the plugin will send events. +func (p *Plugin) SetupChannel(ch chan *v1.Event) error { + p.externalChannel = ch + return nil +} + +// Stop the plugin by cancelling the periodic timer. +func (p *Plugin) Stop() error { + + p.l.Info("Stop ebpfWindows plugin...") + return nil +} + +// Compile is a no-op for the ebpfwindows plugin +func (p *Plugin) Compile(context.Context) error { + return nil +} + +// Generate is a no-op for the ebpfwindows plugin +func (p *Plugin) Generate(context.Context) error { + return nil +} + +func (p *Plugin) handleDropNotify(dropNotify *DropNotify) { + p.l.Info("DropNotify", zap.String("DropNotify", dropNotify.String())) +} + +func (p *Plugin) handleTraceNotify(traceNotify *TraceNotify) { + p.l.Info("TraceNotify", zap.String("TraceNotify", traceNotify.String())) +} + +func (p *Plugin) handleTraceSockNotify(traceSockNotify *TraceSockNotify) { + p.l.Info("TraceSockNotify", zap.String("TraceSockNotify", traceSockNotify.String())) +} + +func (p *Plugin) handleTraceEvent(data unsafe.Pointer, size uint64) error { + + if uintptr(size) < unsafe.Sizeof(uint8(0)) { + return ErrInvalidEventData + } + + eventType := *(*uint8)(data) + + switch eventType { + case CiliumNotifyDrop: + + if uintptr(size) < unsafe.Sizeof(DropNotify{}) { + p.l.Error("Invalid DropNotify data size", zap.Uint64("size", size)) + return ErrInvalidEventData + } + + dropNotify := (*DropNotify)(data) + p.handleDropNotify(dropNotify) + + case CiliumNotifyTrace: + + if uintptr(size) < unsafe.Sizeof(TraceNotify{}) { + p.l.Error("Invalid TraceNotify data size", zap.Uint64("size", size)) + return ErrInvalidEventData + } + + traceNotify := (*TraceNotify)(data) + p.handleTraceNotify(traceNotify) + + case CiliumNotifyTraceSock: + if uintptr(size) < unsafe.Sizeof(TraceSockNotify{}) { + p.l.Error("Invalid TraceSockNotify data size", zap.Uint64("size", size)) + return ErrInvalidEventData + } + + traceSockNotify := (*TraceSockNotify)(data) + p.handleTraceSockNotify(traceSockNotify) + + default: + p.l.Error("Unsupported event type", zap.Uint8("eventType", eventType)) + } + + t1 := time.Now().UnixNano() + + // Hardcoded values for flow object. These values will be replaced by the actual values from the event. + fl := utils.ToFlow( + p.l, + t1, + net.ParseIP("192.168.0.1").To4(), // Src IP + net.ParseIP("192.168.0.2").To4(), // Dst IP + 80, // Src Port + 1024, // Dst Port + 6, // Protocol + 2, + flow.Verdict_DROPPED, + ) + + if fl == nil { + p.l.Warn("Could not convert event to flow", zap.Any("handleTraceEvent", data)) + return ErrInvalidEventData + } + + ev := &v1.Event{ + Event: fl, + Timestamp: fl.GetTime(), + } + + if p.enricher != nil { + p.enricher.Write(ev) + } else { + p.l.Error("enricher is nil when writing event") + } + + // Write the event to the external channel. + if p.externalChannel != nil { + select { + case p.externalChannel <- ev: + default: + // Channel is full, drop the event. + // We shouldn't slow down the reader. + metrics.LostEventsCounter.WithLabelValues(utils.ExternalChannel, name).Inc() + } + } + + return nil +} diff --git a/pkg/plugin/ebpfwindows/ebpf_windows_test.go b/pkg/plugin/ebpfwindows/ebpf_windows_test.go new file mode 100644 index 0000000000..3563d0159d --- /dev/null +++ b/pkg/plugin/ebpfwindows/ebpf_windows_test.go @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// nolint + +package ebpfwindows + +import ( + "context" + "testing" + "time" + + kcfg "github.com/microsoft/retina/pkg/config" + "github.com/microsoft/retina/pkg/controllers/cache" + "github.com/microsoft/retina/pkg/enricher" + "github.com/microsoft/retina/pkg/log" + "github.com/microsoft/retina/pkg/metrics" + "github.com/microsoft/retina/pkg/pubsub" + "go.uber.org/zap" +) + +func TestPlugin(t *testing.T) { + log.SetupZapLogger(log.GetDefaultLogOpts()) + l := log.Logger().Named("test-ebpf") + + ctx := context.Background() + + cfg := &kcfg.Config{ + MetricsInterval: 1 * time.Second, + EnablePodLevel: true, + } + + c := cache.New(pubsub.New()) + e := enricher.New(ctx, c) + e.Run() + defer e.Reader.Close() + metrics.InitializeMetrics() + + tt := New(cfg) + + err := tt.Stop() + if err != nil { + l.Error("Failed to stop windows ebpf plugin", zap.Error(err)) + return + } + + ctxTimeout, cancel := context.WithTimeout(ctx, time.Second*10) + defer cancel() + err = tt.Generate(ctxTimeout) + if err != nil { + l.Error("Failed to generate the plugin specific header files", zap.Error(err)) + return + } + + err = tt.Compile(ctxTimeout) + if err != nil { + l.Error("Failed to compile the ebpf to generate bpf object", zap.Error(err)) + return + } + + err = tt.Init() + if err != nil { + l.Error("Failed to initialize plugin specific objects", zap.Error(err)) + return + } + + err = tt.Start(ctx) + if err != nil { + l.Error("Failed to start windows ebpf plugin", zap.Error(err)) + return + } + l.Info("Started windows ebpf plugin") + + defer func() { + if err := tt.Stop(); err != nil { + l.Error("Failed to stop windows ebpf plugin", zap.Error(err)) + } + }() + + for range ctx.Done() { + } +} diff --git a/pkg/plugin/ebpfwindows/eventsmap_types_windows.go b/pkg/plugin/ebpfwindows/eventsmap_types_windows.go new file mode 100644 index 0000000000..6a9fcdeff0 --- /dev/null +++ b/pkg/plugin/ebpfwindows/eventsmap_types_windows.go @@ -0,0 +1,120 @@ +package ebpfwindows + +import ( + "bytes" + "encoding/binary" + "fmt" + "net" +) + +// IP represents an IPv4 or IPv4 or IPv6 address +type IP struct { + Address uint32 + Pad1 uint32 + Pad2 uint32 + Pad3 uint32 +} + +// TraceSockNotify is the notification for a socket trace +type TraceSockNotify struct { + Type uint8 + XlatePoint uint8 + DstIP IP + DstPort uint16 + SockCookie uint64 + CgroupID uint64 + L4Proto uint8 + IPv6 bool +} + +// NotifyCommonHdr is the common header for all notifications +type NotifyCommonHdr struct { + Type uint8 + Subtype uint8 + Source uint16 + Hash uint32 +} + +// NotifyCaptureHdr is the common header for all capture notifications +type NotifyCaptureHdr struct { + NotifyCommonHdr + LenOrig uint32 // Length of original packet + LenCap uint16 // Length of captured bytes + Version uint16 // Capture header version +} + +// DropNotify is the notification for a packet drop +type DropNotify struct { + NotifyCaptureHdr + SrcLabel uint32 + DstLabel uint32 + DstID uint32 // 0 for egress + Line uint16 + File uint8 + ExtError int8 + Ifindex uint32 +} + +// TraceNotify is the notification for a packet trace +type TraceNotify struct { + NotifyCaptureHdr + SrcLabel uint32 + DstLabel uint32 + DstID uint16 + Reason uint8 + IPv6 bool + Ifindex uint32 + OrigIP IP +} + +// Notification types +const ( + CiliumNotifyUnspec = 0 + CiliumNotifyDrop = 1 + CiliumNotifyDebugMessage = 2 + CiliumNotifyDebugCapture = 3 + CiliumNotifyTrace = 4 + CiliumNotifyPolicyVerdict = 5 + CiliumNotifyCapture = 6 + CiliumNotifyTraceSock = 7 +) + +func (ip *IP) ConvertToString(IPv6 bool) string { + var ipAddress string + var buf bytes.Buffer + + err := binary.Write(&buf, binary.BigEndian, *ip) + + if err != nil { + return "" + } + + byteArray := buf.Bytes() + + if IPv6 { + ipAddress = net.IP(byteArray[:16]).String() + } else { + ipAddress = net.IP(byteArray[:4]).String() + } + + return ipAddress + +} + +// String returns a string representation of the DropNotify +func (k *DropNotify) String() string { + + return fmt.Sprintf("Ifindex: %d, SrcLabel:%d, DstLabel:%d, File: %s, Line: %d", k.Ifindex, k.SrcLabel, k.DstLabel, BPFFileName(k.File), k.Line) +} + +// String returns a string representation of the TraceNotify +func (k *TraceNotify) String() string { + ipAddress := k.OrigIP.ConvertToString(k.IPv6) + return fmt.Sprintf("Ifindex: %d, SrcLabel:%d, DstLabel:%d, IpV6:%t, OrigIP:%s", k.Ifindex, k.SrcLabel, k.DstLabel, k.IPv6, ipAddress) +} + +// String returns a string representation of the TraceSockNotify +func (k *TraceSockNotify) String() string { + ipAddress := k.DstIP.ConvertToString(k.IPv6) + return fmt.Sprintf("DstIP:%s, DstPort:%d, SockCookie:%d, CgroupID:%d, L4Proto:%d, IPv6:%t", ipAddress, k.DstPort, k.SockCookie, k.CgroupID, k.L4Proto, k.IPv6) +} diff --git a/pkg/plugin/ebpfwindows/eventsmap_windows.go b/pkg/plugin/ebpfwindows/eventsmap_windows.go new file mode 100644 index 0000000000..0e5d1ad4ea --- /dev/null +++ b/pkg/plugin/ebpfwindows/eventsmap_windows.go @@ -0,0 +1,75 @@ +package ebpfwindows + +import ( + "syscall" + "unsafe" +) + +var ( + registerEventsMapCallback = retinaEbpfApi.NewProc("register_cilium_eventsmap_callback") + unregisterEventsMapCallback = retinaEbpfApi.NewProc("unregister_cilium_eventsmap_callback") +) + +type eventsMapCallback func(data unsafe.Pointer, size uint64) int + +// Callbacks in Go can only be passed as functions with specific signatures and often need to be wrapped in a syscall-compatible function. +var eventsCallback eventsMapCallback = nil + +// This function will be passed to the Windows API +func eventsMapSysCallCallback(data unsafe.Pointer, size uint64) uintptr { + + if eventsCallback != nil { + return uintptr(eventsCallback(data, size)) + } + + return 0 +} + +// EventsMap interface represents a events map +type EventsMap interface { + RegisterForCallback(eventsMapCallback) error + UnregisterForCallback() error +} + +type eventsMap struct { + ringBuffer uintptr +} + +// NewEventsMap creates a new metrics map +func NewEventsMap() EventsMap { + return &eventsMap{ringBuffer: 0} +} + +// RegisterForCallback registers a callback function to be called when a new event is added to the events map +func (e *eventsMap) RegisterForCallback(cb eventsMapCallback) error { + + eventsCallback = cb + + // Convert the Go function into a syscall-compatible function + callback := syscall.NewCallback(eventsMapSysCallCallback) + + // Call the API + ret, _, err := registerEventsMapCallback.Call( + uintptr(callback), + uintptr(unsafe.Pointer(&e.ringBuffer)), + ) + + if ret != 0 { + return err + } + + return nil +} + +// UnregisterForCallback unregisters the callback function +func (e *eventsMap) UnregisterForCallback() error { + + // Call the API + ret, _, err := unregisterEventsMapCallback.Call(e.ringBuffer) + + if ret != 0 { + return err + } + + return nil +} diff --git a/pkg/plugin/ebpfwindows/metricsmap_windows.go b/pkg/plugin/ebpfwindows/metricsmap_windows.go new file mode 100644 index 0000000000..157bcc94d8 --- /dev/null +++ b/pkg/plugin/ebpfwindows/metricsmap_windows.go @@ -0,0 +1,190 @@ +package ebpfwindows + +import ( + "fmt" + "reflect" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +const ( + dirUnknown = 0 + dirIngress = 1 + dirEgress = 2 + dirService = 3 +) + +// direction is the metrics direction i.e ingress (to an endpoint), +// egress (from an endpoint) or service (NodePort service being accessed from +// outside or a ClusterIP service being accessed from inside the cluster). +// If it's none of the above, we return UNKNOWN direction. +var direction = map[uint8]string{ + dirUnknown: "UNKNOWN", + dirIngress: "INGRESS", + dirEgress: "EGRESS", + dirService: "SERVICE", +} + +// Value must be in sync with struct metrics_key in +type MetricsKey struct { + Reason uint8 `align:"reason"` + Dir uint8 `align:"dir"` + // Line contains the line number of the metrics statement. + Line uint16 `align:"line"` + // File is the number of the source file containing the metrics statement. + File uint8 `align:"file"` + Reserved [3]uint8 `align:"reserved"` +} + +// Value must be in sync with struct metrics_value in +type MetricsValue struct { + Count uint64 `align:"count"` + Bytes uint64 `align:"bytes"` +} + +// MetricsMapValues is a slice of MetricsMapValue +type MetricsValues []MetricsValue + +// IterateCallback represents the signature of the callback function expected by +// the IterateWithCallback method, which in turn is used to iterate all the +// keys/values of a metrics map. +type IterateCallback func(*MetricsKey, *MetricsValues) + +// MetricsMap interface represents a metrics map, and can be reused to implement +// mock maps for unit tests. +type MetricsMap interface { + IterateWithCallback(IterateCallback) error +} + +type metricsMap struct { +} + +var ( + // Load the retinaebpfapi.dll + retinaEbpfApi = windows.NewLazyDLL("retinaebpfapi.dll") + // Load the enumerate_cilium_metricsmap function + enumMetricsMap = retinaEbpfApi.NewProc("enumerate_cilium_metricsmap") +) + +// ringBufferEventCallback type definition in Go +type enumMetricsCallback = func(key, value unsafe.Pointer, valueSize int) int + +// Callbacks in Go can only be passed as functions with specific signatures and often need to be wrapped in a syscall-compatible function. +var enumCallBack enumMetricsCallback = nil + +// This function will be passed to the Windows API +func enumMetricsSysCallCallback(key, value unsafe.Pointer, valueSize int) uintptr { + + if enumCallBack != nil { + return uintptr(enumCallBack(key, value, valueSize)) + } + + return 0 +} + +// NewMetricsMap creates a new metrics map +func NewMetricsMap() MetricsMap { + return &metricsMap{} +} + +// IterateWithCallback iterates through all the keys/values of a metrics map, +// passing each key/value pair to the cb callback +func (m metricsMap) IterateWithCallback(cb IterateCallback) error { + + // Define the callback function in Go + enumCallBack = func(key unsafe.Pointer, value unsafe.Pointer, valueSize int) int { + + var metricsValues MetricsValues + sh := (*reflect.SliceHeader)(unsafe.Pointer(&metricsValues)) + sh.Data = uintptr(value) + sh.Len = valueSize + sh.Cap = valueSize + + metricsKey := (*MetricsKey)(key) + cb(metricsKey, &metricsValues) + return 0 + } + + // Convert the Go function into a syscall-compatible function + callback := syscall.NewCallback(enumMetricsSysCallCallback) + + // Call the API + ret, _, err := enumMetricsMap.Call( + uintptr(callback), + ) + + if ret != 0 { + return err + } + + return nil +} + +// MetricDirection gets the direction in human readable string format +func MetricDirection(dir uint8) string { + if desc, ok := direction[dir]; ok { + return desc + } + return direction[dirUnknown] +} + +// Direction gets the direction in human readable string format +func (k *MetricsKey) Direction() string { + return MetricDirection(k.Dir) +} + +// String returns the key in human readable string format +func (k *MetricsKey) String() string { + return fmt.Sprintf("Direction: %s, Reason: %s, File: %s, Line: %d", k.Direction(), DropReason(k.Reason), BPFFileName(k.File), k.Line) +} + +// DropForwardReason gets the forwarded/dropped reason in human readable string format +func (k *MetricsKey) DropForwardReason() string { + return DropReason(k.Reason) +} + +// FileName returns the filename where the event occurred, in string format. +func (k *MetricsKey) FileName() string { + return BPFFileName(k.File) +} + +// IsDrop checks if the reason is drop or not. +func (k *MetricsKey) IsDrop() bool { + return k.Reason == DropInvalid || k.Reason >= DropMin +} + +// IsIngress checks if the direction is ingress or not. +func (k *MetricsKey) IsIngress() bool { + return k.Dir == dirIngress +} + +// IsEgress checks if the direction is egress or not. +func (k *MetricsKey) IsEgress() bool { + return k.Dir == dirEgress +} + +// Count returns the sum of all the per-CPU count values +func (vs MetricsValues) Count() uint64 { + c := uint64(0) + for _, v := range vs { + c += v.Count + } + + return c +} + +// Bytes returns the sum of all the per-CPU bytes values +func (vs MetricsValues) Bytes() uint64 { + b := uint64(0) + for _, v := range vs { + b += v.Bytes + } + + return b +} + +func (vs MetricsValues) String() string { + return fmt.Sprintf("Count: %d, Bytes: %d", vs.Count(), vs.Bytes()) +}