diff --git a/Makefile b/Makefile index a49d916..32a4777 100644 --- a/Makefile +++ b/Makefile @@ -22,4 +22,5 @@ dev: build-dev -e FIRETAIL_API_TOKEN=${FIRETAIL_API_TOKEN} \ -e FIRETAIL_KUBERNETES_SENSOR_DEV_MODE=true \ -e FIRETAIL_KUBERNETES_SENSOR_DEV_SERVER_ENABLED=true \ + -e DISABLE_SERVICE_IP_FILTERING=true \ firetail/kubernetes-sensor-dev diff --git a/README.md b/README.md index ef40b78..4b89cc4 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,13 @@ POC for a FireTail Kubernetes Sensor. | ----------------------------------------------- | --------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | `FIRETAIL_API_TOKEN` | ✅ | `PS-02-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX` | The API token the sensor will use to report logs to FireTail | | `BPF_EXPRESSION` | ❌ | `tcp and (port 80 or port 443)` | The BPF filter used by the sensor. See docs for syntax info: https://www.tcpdump.org/manpages/pcap-filter.7.html | +| `DISABLE_SERVICE_IP_FILTERING` | ❌ | `true` | Disables polling Kubernetes for the IP addresses of services & subsequently ignoring all requests captured that aren't made to one of those IPs. | | `FIRETAIL_API_URL` | ❌ | `https://api.logging.eu-west-1.prod.firetail.app/logs/bulk` | The API url the sensor will send logs to. Defaults to the EU region production environment. | -| `FIRETAIL_KUBERNETES_SENSOR_DEV_MODE` | ❌ | `true` | Enables debug logging when set to `true`. | +| `FIRETAIL_KUBERNETES_SENSOR_DEV_MODE` | ❌ | `true` | Enables debug logging when set to `true`, and reduces the max age of a log in a batch to be sent to FireTail. | | `FIRETAIL_KUBERNETES_SENSOR_DEV_SERVER_ENABLED` | ❌ | `true` | Enables a demo web server when set to `true`; useful for sending test requests to. | - - ## Dev Quickstart Clone the repo, make a `.env` file with your API token in it, then use the `dev` target in [the provided makefile](./Makefile): @@ -54,5 +53,10 @@ ftauth make publish VERSION=latest ``` -[Kubernetes sensor docker repository link](https://github.com/firetail-io/firetail-kubernetes-sensor/pkgs/container/kubernetes-sensor) + +## Publishing to GHCR + +Publishing to GHCR is done via GitHub actions found in [./.github/workflows](./.github/workflows). + +You can find the images published here: [github.com/firetail-io/firetail-kubernetes-sensor/pkgs/container/kubernetes-sensor](https://github.com/firetail-io/firetail-kubernetes-sensor/pkgs/container/kubernetes-sensor) diff --git a/helm/README.md b/helm/README.md new file mode 100644 index 0000000..e5fa1ad --- /dev/null +++ b/helm/README.md @@ -0,0 +1,3 @@ +```bash +helm install firetail-sensor-helm firetail-sensor/ --set apiKey="example" +``` \ No newline at end of file diff --git a/helm/firetail-sensor/.helmignore b/helm/firetail-sensor/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/firetail-sensor/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/firetail-sensor/Chart.yaml b/helm/firetail-sensor/Chart.yaml new file mode 100644 index 0000000..54651dc --- /dev/null +++ b/helm/firetail-sensor/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: firetail-sensor +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/firetail-sensor/templates/NOTES.txt b/helm/firetail-sensor/templates/NOTES.txt new file mode 100644 index 0000000..e69de29 diff --git a/helm/firetail-sensor/templates/_helpers.tpl b/helm/firetail-sensor/templates/_helpers.tpl new file mode 100644 index 0000000..eb1ac4a --- /dev/null +++ b/helm/firetail-sensor/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "firetail-sensor.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "firetail-sensor.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "firetail-sensor.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "firetail-sensor.labels" -}} +helm.sh/chart: {{ include "firetail-sensor.chart" . }} +{{ include "firetail-sensor.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "firetail-sensor.selectorLabels" -}} +app.kubernetes.io/name: {{ include "firetail-sensor.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "firetail-sensor.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "firetail-sensor.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/firetail-sensor/templates/api-secret.yaml b/helm/firetail-sensor/templates/api-secret.yaml new file mode 100644 index 0000000..dfd1784 --- /dev/null +++ b/helm/firetail-sensor/templates/api-secret.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + name: firetail-api-token-secret + namespace: {{ .Values.namespace }} +type: Opaque +data: + api-key: {{ .Values.apiKey | b64enc | quote }} \ No newline at end of file diff --git a/helm/firetail-sensor/templates/daemonset.yaml b/helm/firetail-sensor/templates/daemonset.yaml new file mode 100644 index 0000000..7ed4cfd --- /dev/null +++ b/helm/firetail-sensor/templates/daemonset.yaml @@ -0,0 +1,53 @@ +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: {{ .Release.Name }}-daemonset + namespace: {{ .Values.namespace }} + labels: + app: {{ .Chart.Name }} + release: {{ .Release.Name }} +spec: + selector: + matchLabels: + app: {{ .Chart.Name }} + release: {{ .Release.Name }} + template: + metadata: + labels: + app: {{ .Chart.Name }} + release: {{ .Release.Name }} + annotations: + {{- toYaml .Values.podAnnotations | nindent 8 }} + spec: + serviceAccountName: {{ .Release.Name }}-sa + hostNetwork: true + containers: + - name: firetail-kubernetes-sensor + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: "FIRETAIL_API_TOKEN" + valueFrom: + secretKeyRef: + name: "firetail-api-token-secret" + key: "api-key" + {{- range $key, $value := .Values.env }} + - name: "{{ $key }}" + value: "{{ $value }}" + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + volumeMounts: + - name: lib-modules + mountPath: /lib/modules + - name: usr-src + mountPath: /usr/src + volumes: + - name: lib-modules + hostPath: + path: /lib/modules + - name: usr-src + hostPath: + path: /usr/src \ No newline at end of file diff --git a/helm/firetail-sensor/templates/namespace.yaml b/helm/firetail-sensor/templates/namespace.yaml new file mode 100644 index 0000000..77db5f9 --- /dev/null +++ b/helm/firetail-sensor/templates/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: {{ .Values.namespace }} diff --git a/helm/firetail-sensor/templates/rbac.yaml b/helm/firetail-sensor/templates/rbac.yaml new file mode 100644 index 0000000..ee197c9 --- /dev/null +++ b/helm/firetail-sensor/templates/rbac.yaml @@ -0,0 +1,36 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Release.Name }}-sa + namespace: {{ .Values.namespace }} + labels: + app: {{ .Chart.Name }} + release: {{ .Release.Name }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .Release.Name }}-service-list-access + labels: + app: {{ .Chart.Name }} + release: {{ .Release.Name }} +subjects: +- kind: ServiceAccount + name: {{ .Release.Name }}-sa + namespace: {{ .Values.namespace }} +roleRef: + kind: ClusterRole + name: {{ .Release.Name }}-list-services + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ .Release.Name }}-list-services + labels: + app: {{ .Chart.Name }} + release: {{ .Release.Name }} +rules: +- apiGroups: [""] + resources: ["services"] + verbs: ["get", "list", "watch"] \ No newline at end of file diff --git a/helm/firetail-sensor/values.yaml b/helm/firetail-sensor/values.yaml new file mode 100644 index 0000000..1176f8e --- /dev/null +++ b/helm/firetail-sensor/values.yaml @@ -0,0 +1,42 @@ +# Default values for firetail-sensor. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +image: + repository: ghcr.io/firetail-io/kubernetes-sensor + tag: 1dbc044 + pullPolicy: IfNotPresent + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" +namespace: "firetail" +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + namespace: default + +securityContext: + privileged: true + + + + +env: + FIRETAIL_API_URL: "https://api.logging.eu-west-1.sandbox.firetail.app/logs/bulk" + FIRETAIL_API_URL_EU: "https://api.logging.eu-west-1.firetail.app/logs/bulk" + FIRETAIL_API_URL_US: "https://api.logging.us-east-2.us.firetail.app/logs/bulk" + FIRETAIL_KUBERNETES_SENSOR_DEV_MODE: "true" + FIRETAIL_KUBERNETES_SENSOR_DEV_SERVER_ENABLED: "false" + BPF_EXPRESSION: "tcp and (port 80 or port 443) and not net 169.254.0.0/16 and not net fd00::/8" + DISABLE_SERVICE_IP_FILTERING: "true" + + +apiKey: "" \ No newline at end of file diff --git a/kube_templates/daemonset.yaml b/kube_templates/daemonset.yaml index d78b6a6..8824504 100644 --- a/kube_templates/daemonset.yaml +++ b/kube_templates/daemonset.yaml @@ -15,7 +15,7 @@ spec: serviceAccountName: firetail-ebpf-sa hostNetwork: true containers: - - name: firetail-ebpf-daemonset + - name: firetail-ebpf-daemonset # nosemgrep: yaml.kubernetes.security.privileged-container.privileged-container image: ghcr.io/firetail-io/kubernetes-sensor:v0.1.5 imagePullPolicy: IfNotPresent securityContext: @@ -24,7 +24,7 @@ spec: - name: FIRETAIL_API_URL value: "https://api.logging.eu-west-1.sandbox.firetail.app/logs/bulk" - name: FIRETAIL_API_TOKEN - value: "" + value: - name: FIRETAIL_KUBERNETES_SENSOR_DEV_MODE value: "true" - name: BPF_EXPRESSION diff --git a/src/bidirectionalStream.go b/src/bidirectionalStream.go new file mode 100644 index 0000000..e75cf14 --- /dev/null +++ b/src/bidirectionalStream.go @@ -0,0 +1,115 @@ +package main + +import ( + "bufio" + "bytes" + "fmt" + "io" + "net/http" + "sync" + + "github.com/google/gopacket" + "github.com/google/gopacket/tcpassembly" + "github.com/google/gopacket/tcpassembly/tcpreader" +) + +type bidirectionalStreamFactory struct { + conns map[string]*bidirectionalStream + requestAndResponseChannel *chan httpRequestAndResponse +} + +func (f *bidirectionalStreamFactory) New(netFlow, tcpFlow gopacket.Flow) tcpassembly.Stream { + key := netFlow.FastHash() ^ tcpFlow.FastHash() + + // The second time we see the same connection, it will be from the server to the client + if conn, ok := f.conns[fmt.Sprint(key)]; ok { + return &conn.serverToClient + } + + s := &bidirectionalStream{ + net: netFlow, + transport: tcpFlow, + clientToServer: tcpreader.NewReaderStream(), + serverToClient: tcpreader.NewReaderStream(), + requestAndResponseChannel: f.requestAndResponseChannel, + } + f.conns[fmt.Sprint(key)] = s + go s.run() + + // The first time we see the connection, it will be from the client to the server + return &s.clientToServer +} + +type bidirectionalStream struct { + net, transport gopacket.Flow + clientToServer tcpreader.ReaderStream + serverToClient tcpreader.ReaderStream + requestAndResponseChannel *chan httpRequestAndResponse +} + +func (s *bidirectionalStream) run() { + wg := sync.WaitGroup{} + wg.Add(2) + + requestChannel := make(chan *http.Request, 1) + responseChannel := make(chan *http.Response, 1) + + go func() { + reader := bufio.NewReader(&s.clientToServer) + for { + request, err := http.ReadRequest(reader) + if err == io.EOF { + wg.Done() + return + } else if err != nil { + continue + } + // RemoteAddr is not filled in by ReadRequest so we have to populate it ourselves + request.RemoteAddr = fmt.Sprintf("%s:%s", s.net.Src().String(), s.transport.Src().String()) + responseBody := make([]byte, request.ContentLength) + if request.ContentLength > 0 { + io.ReadFull(request.Body, responseBody) + } + request.Body.Close() + request.Body = io.NopCloser(bytes.NewReader(responseBody)) + requestChannel <- request + + } + }() + + go func() { + reader := bufio.NewReader(&s.serverToClient) + for { + response, err := http.ReadResponse(reader, nil) + if err == io.ErrUnexpectedEOF { + wg.Done() + return + } else if err != nil { + continue + } + responseBody := make([]byte, response.ContentLength) + if response.ContentLength > 0 { + io.ReadFull(response.Body, responseBody) + } + response.Body.Close() + response.Body = io.NopCloser(bytes.NewReader(responseBody)) + responseChannel <- response + } + }() + + wg.Wait() + + capturedRequest := <-requestChannel + capturedResponse := <-responseChannel + close(requestChannel) + close(responseChannel) + + *s.requestAndResponseChannel <- httpRequestAndResponse{ + request: capturedRequest, + response: capturedResponse, + src: s.net.Src().String(), + dst: s.net.Dst().String(), + srcPort: s.transport.Src().String(), + dstPort: s.transport.Dst().String(), + } +} diff --git a/src/go.mod b/src/go.mod index 82a9b23..0aa4594 100644 --- a/src/go.mod +++ b/src/go.mod @@ -1,22 +1,57 @@ module firetail-kubernetes-sensor -go 1.23.0 +go 1.24.0 -toolchain go1.23.8 +toolchain go1.24.3 -require github.com/google/gopacket v1.1.19 +require ( + github.com/FireTail-io/firetail-go-lib v0.3.0 + github.com/google/gopacket v1.1.19 + k8s.io/client-go v0.33.0 +) require ( - github.com/FireTail-io/firetail-go-lib v0.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/getkin/kin-openapi v0.110.0 // indirect - github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.0 // indirect github.com/invopop/yaml v0.2.0 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/x448/float16 v0.8.4 // indirect golang.org/x/net v0.39.0 // indirect + golang.org/x/oauth2 v0.27.0 // indirect golang.org/x/sys v0.32.0 // indirect + golang.org/x/term v0.31.0 // indirect + golang.org/x/text v0.24.0 // indirect + golang.org/x/time v0.9.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.33.0 // indirect + k8s.io/apimachinery v0.33.0 + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/src/go.sum b/src/go.sum index fec9b28..e0e25f1 100644 --- a/src/go.sum +++ b/src/go.sum @@ -1,16 +1,43 @@ github.com/FireTail-io/firetail-go-lib v0.3.0 h1:P8qc6hV2qLffQ0peX9oqdQyhswFnSw4xgcK8h3NBvIo= github.com/FireTail-io/firetail-go-lib v0.3.0/go.mod h1:PH4aGBwry6z/3vzXEdcMaxK22E3xqPq2+w2y3FzETj4= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/getkin/kin-openapi v0.110.0 h1:1GnJALxsltcSzCMqgtqKlLhYQeULv3/jesmV2sC5qE0= github.com/getkin/kin-openapi v0.110.0/go.mod h1:QtwUNt0PAAgIIBEvFWYfB7dfngxtAaqCX1zYHMZDeK8= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= @@ -18,44 +45,136 @@ github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/sbabiv/xml2map v1.2.1 h1:1lT7t0hhUvXZCkdxqtq4n8/ZCnwLWGq4rDuDv5XOoFE= +github.com/sbabiv/xml2map v1.2.1/go.mod h1:2TPoAfcaM7+Sd4iriPvzyntb2mx7GY+kkQpB/GQa/eo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= +k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= +k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= +k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98= +k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/src/main.go b/src/main.go index 1217164..420338a 100644 --- a/src/main.go +++ b/src/main.go @@ -1,8 +1,6 @@ package main import ( - "bufio" - "bytes" "fmt" "io" "log" @@ -11,178 +9,13 @@ import ( "net/http/httptest" "os" "strconv" - "sync" "time" firetail "github.com/FireTail-io/firetail-go-lib/middlewares/http" - "github.com/google/gopacket" - "github.com/google/gopacket/layers" - "github.com/google/gopacket/pcap" - "github.com/google/gopacket/tcpassembly" - "github.com/google/gopacket/tcpassembly/tcpreader" ) -type httpRequestAndResponse struct { - request *http.Request - response *http.Response - src string - dst string - srcPort string - dstPort string -} - -type httpRequestAndResponseStreamer struct { - bpfExpression string - requestAndResponseChannel *chan httpRequestAndResponse -} - -func (s *httpRequestAndResponseStreamer) start() { - handle, err := pcap.OpenLive("any", 1600, true, pcap.BlockForever) - if err != nil { - log.Fatal(err) - } - defer handle.Close() - - err = handle.SetBPFFilter(s.bpfExpression) - if err != nil { - log.Fatal(err) - } - - assembler := tcpassembly.NewAssembler( - tcpassembly.NewStreamPool( - &bidirectionalStreamFactory{ - conns: make(map[string]*bidirectionalStream), - requestAndResponseChannel: s.requestAndResponseChannel, - }, - ), - ) - ticker := time.Tick(time.Minute) - packetsChannel := gopacket.NewPacketSource(handle, handle.LinkType()).Packets() - for { - select { - case packet := <-packetsChannel: - if packet.NetworkLayer() == nil || packet.TransportLayer() == nil { - continue - } - tcp, ok := packet.TransportLayer().(*layers.TCP) - if !ok { - continue - } - assembler.AssembleWithTimestamp(packet.NetworkLayer().NetworkFlow(), tcp, packet.Metadata().Timestamp) - case <-ticker: - assembler.FlushOlderThan(time.Now().Add(-2 * time.Minute)) - default: - } - } -} - -type bidirectionalStreamFactory struct { - conns map[string]*bidirectionalStream - requestAndResponseChannel *chan httpRequestAndResponse -} - -func (f *bidirectionalStreamFactory) New(netFlow, tcpFlow gopacket.Flow) tcpassembly.Stream { - key := netFlow.FastHash() ^ tcpFlow.FastHash() - - // The second time we see the same connection, it will be from the server to the client - if conn, ok := f.conns[fmt.Sprint(key)]; ok { - return &conn.serverToClient - } - - s := &bidirectionalStream{ - net: netFlow, - transport: tcpFlow, - clientToServer: tcpreader.NewReaderStream(), - serverToClient: tcpreader.NewReaderStream(), - requestAndResponseChannel: f.requestAndResponseChannel, - } - f.conns[fmt.Sprint(key)] = s - go s.run() - - // The first time we see the connection, it will be from the client to the server - return &s.clientToServer -} - -type bidirectionalStream struct { - net, transport gopacket.Flow - clientToServer tcpreader.ReaderStream - serverToClient tcpreader.ReaderStream - requestAndResponseChannel *chan httpRequestAndResponse -} - -func (s *bidirectionalStream) run() { - wg := sync.WaitGroup{} - wg.Add(2) - - requestChannel := make(chan *http.Request, 1) - responseChannel := make(chan *http.Response, 1) - - go func() { - reader := bufio.NewReader(&s.clientToServer) - for { - request, err := http.ReadRequest(reader) - if err == io.EOF { - wg.Done() - return - } else if err != nil { - continue - } - // RemoteAddr is not filled in by ReadRequest so we have to populate it ourselves - request.RemoteAddr = fmt.Sprintf("%s:%s", s.net.Src().String(), s.transport.Src().String()) - responseBody := make([]byte, request.ContentLength) - if request.ContentLength > 0 { - io.ReadFull(request.Body, responseBody) - } - request.Body.Close() - request.Body = io.NopCloser(bytes.NewReader(responseBody)) - requestChannel <- request - - } - }() - - go func() { - reader := bufio.NewReader(&s.serverToClient) - for { - response, err := http.ReadResponse(reader, nil) - if err == io.ErrUnexpectedEOF { - wg.Done() - return - } else if err != nil { - continue - } - responseBody := make([]byte, response.ContentLength) - if response.ContentLength > 0 { - io.ReadFull(response.Body, responseBody) - } - response.Body.Close() - response.Body = io.NopCloser(bytes.NewReader(responseBody)) - responseChannel <- response - } - }() - - wg.Wait() - - capturedRequest := <-requestChannel - capturedResponse := <-responseChannel - close(requestChannel) - close(responseChannel) - - *s.requestAndResponseChannel <- httpRequestAndResponse{ - request: capturedRequest, - response: capturedResponse, - src: s.net.Src().String(), - dst: s.net.Dst().String(), - srcPort: s.transport.Src().String(), - dstPort: s.transport.Dst().String(), - } -} - func main() { - devEnabled, err := strconv.ParseBool(os.Getenv("FIRETAIL_KUBERNETES_SENSOR_DEV_MODE")) - if err != nil { - devEnabled = false - } - + devEnabled, _ := strconv.ParseBool(os.Getenv("FIRETAIL_KUBERNETES_SENSOR_DEV_MODE")) if devEnabled { slog.Warn("🧰 Development mode enabled, setting log level to debug...") slog.SetLogLoggerLevel(slog.LevelDebug) @@ -193,6 +26,14 @@ func main() { log.Fatal("FIRETAIL_API_TOKEN environment variable not set") } + var ipManager *serviceIpManager + if disableServiceIpFilter, err := strconv.ParseBool(os.Getenv("DISABLE_SERVICE_IP_FILTERING")); !(err == nil && disableServiceIpFilter) { + slog.Info( + "Service IP filter enabled, monitoring service IPs...", + ) + ipManager = newServiceIpManager() + } + bpfExpression, bpfExpressionSet := os.LookupEnv("BPF_EXPRESSION") if !bpfExpressionSet { slog.Info( @@ -217,6 +58,7 @@ func main() { httpRequestStreamer := &httpRequestAndResponseStreamer{ bpfExpression: bpfExpression, requestAndResponseChannel: &requestAndResponseChannel, + ipManager: ipManager, } go httpRequestStreamer.start() @@ -239,6 +81,16 @@ func main() { for { select { case requestAndResponse := <-requestAndResponseChannel: + if !(ipManager == nil || ipManager.isServiceIP(requestAndResponse.dst)) { + slog.Debug( + "Ignoring request to non-service IP:", + "Src", requestAndResponse.src, + "Dst", requestAndResponse.dst, + "SrcPort", requestAndResponse.srcPort, + "DstPort", requestAndResponse.dstPort, + ) + continue + } slog.Debug( "Captured request and response:", "Method", requestAndResponse.request.Method, diff --git a/src/requestAndResponse.go b/src/requestAndResponse.go new file mode 100644 index 0000000..186a182 --- /dev/null +++ b/src/requestAndResponse.go @@ -0,0 +1,67 @@ +package main + +import ( + "log" + "net/http" + "time" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/google/gopacket/pcap" + "github.com/google/gopacket/tcpassembly" +) + +type httpRequestAndResponse struct { + request *http.Request + response *http.Response + src string + dst string + srcPort string + dstPort string +} + +type httpRequestAndResponseStreamer struct { + bpfExpression string + requestAndResponseChannel *chan httpRequestAndResponse + ipManager *serviceIpManager +} + +func (s *httpRequestAndResponseStreamer) start() { + handle, err := pcap.OpenLive("any", 1600, true, pcap.BlockForever) + if err != nil { + log.Fatal(err) + } + defer handle.Close() + + err = handle.SetBPFFilter(s.bpfExpression) + if err != nil { + log.Fatal(err) + } + + assembler := tcpassembly.NewAssembler( + tcpassembly.NewStreamPool( + &bidirectionalStreamFactory{ + conns: make(map[string]*bidirectionalStream), + requestAndResponseChannel: s.requestAndResponseChannel, + }, + ), + ) + ticker := time.Tick(time.Minute) + packetsChannel := gopacket.NewPacketSource(handle, handle.LinkType()).Packets() + for { + select { + case packet := <-packetsChannel: + if packet.NetworkLayer() == nil || packet.TransportLayer() == nil { + continue + } + tcp, ok := packet.TransportLayer().(*layers.TCP) + if !ok { + continue + } + assembler.AssembleWithTimestamp(packet.NetworkLayer().NetworkFlow(), tcp, packet.Metadata().Timestamp) + case <-ticker: + assembler.FlushOlderThan(time.Now().Add(-2 * time.Minute)) + default: + } + } +} diff --git a/src/serviceIpManager.go b/src/serviceIpManager.go new file mode 100644 index 0000000..06c6005 --- /dev/null +++ b/src/serviceIpManager.go @@ -0,0 +1,98 @@ +package main + +import ( + "context" + "fmt" + "log/slog" + "sync" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +type serviceIpManager struct { + serviceIPs *sync.Map + getServiceIPs func() ([]string, error) +} + +func newServiceIpManager() *serviceIpManager { + newManager := &serviceIpManager{ + serviceIPs: &sync.Map{}, + getServiceIPs: getServiceIPs, + } + go newManager.run() + return newManager +} + +func (s *serviceIpManager) run() { + t := time.NewTicker(time.Second) + for { + select { + case <-t.C: + currentServiceIPs, err := s.getServiceIPs() + if err != nil { + slog.Error("Failed to get service IPs:", "Err", err.Error()) + continue + } + slog.Debug( + "Discovered service IPs", + "ServiceIpCount", len(currentServiceIPs), + "ServiceIPs", currentServiceIPs, + ) + for _, ip := range currentServiceIPs { + s.serviceIPs.Store(ip, struct{}{}) + } + s.serviceIPs.Range(func(key, value interface{}) bool { + for _, ip := range currentServiceIPs { + if key.(string) == ip { + return true + } + } + s.serviceIPs.Delete(key) + return true + }) + } + } +} + +func (s *serviceIpManager) isServiceIP(ip string) bool { + _, ok := s.serviceIPs.Load(ip) + return ok +} + +func getServiceIPs() ([]string, error) { + // Load config from inside the cluster or from kubeconfig + config, err := rest.InClusterConfig() + if err != nil { + kubeconfig := clientcmd.NewDefaultClientConfigLoadingRules().GetDefaultFilename() + config, err = clientcmd.BuildConfigFromFlags("", kubeconfig) + if err != nil { + return nil, fmt.Errorf("Failed to load kubeconfig: %v", err) + } + } + + // Create clientset + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, fmt.Errorf("Failed to create Kubernetes client: %v", err) + } + + // Get all services in all namespaces + services, err := clientset.CoreV1().Services("").List(context.TODO(), metav1.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("Failed to list services: %v", err) + } + + // Extract service ClusterIPs + var serviceIPs []string + for _, svc := range services.Items { + if svc.Spec.ClusterIP != "" && svc.Spec.ClusterIP != "None" { + serviceIPs = append(serviceIPs, svc.Spec.ClusterIP) + } + } + + return serviceIPs, nil +}