├── config ├── network-policy │ ├── kustomization.yaml │ └── allow-metrics-traffic.yaml ├── samples │ ├── kustomization.yaml │ └── eswitchconfig_v1_eswitch.yaml ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── default │ ├── manager_metrics_patch.yaml │ ├── metrics_service.yaml │ ├── cert_metrics_manager_patch.yaml │ └── kustomization.yaml ├── rbac │ ├── metrics_reader_role.yaml │ ├── service_account.yaml │ ├── metrics_auth_role_binding.yaml │ ├── metrics_auth_role.yaml │ ├── role_binding.yaml │ ├── leader_election_role_binding.yaml │ ├── role.yaml │ ├── leader_election_role.yaml │ ├── eswitch_admin_role.yaml │ ├── eswitch_viewer_role.yaml │ ├── eswitch_editor_role.yaml │ └── kustomization.yaml ├── prometheus │ ├── kustomization.yaml │ ├── monitor_tls_patch.yaml │ └── monitor.yaml └── crd │ ├── kustomizeconfig.yaml │ ├── kustomization.yaml │ └── bases │ └── eswitchconfig.kube-vm.io_eswitches.yaml ├── pkg └── networkswitch │ ├── bpf_bpfeb.o │ ├── bpf_bpfel.o │ ├── types.go │ ├── ebpf.go │ ├── utils.go │ ├── bpf_bpfeb.go │ ├── bpf_bpfel.go │ ├── stats.go │ └── ports.go ├── .dockerignore ├── .github └── workflows │ ├── test.yml │ ├── lint.yml │ └── test-e2e.yml ├── .gitignore ├── hack └── boilerplate.go.txt ├── PROJECT ├── .devcontainer ├── devcontainer.json └── post-install.sh ├── .golangci.yml ├── api └── v1 │ ├── groupversion_info.go │ ├── eswitch_types.go │ └── zz_generated.deepcopy.go ├── Dockerfile ├── internal └── controller │ ├── eswitch_controller_test.go │ ├── suite_test.go │ └── eswitch_controller.go ├── README.md ├── test ├── e2e │ ├── e2e_suite_test.go │ └── e2e_test.go └── utils │ └── utils.go ├── go.mod ├── cmd └── main.go ├── Makefile ├── dist └── install.yaml └── LICENSE /config/network-policy/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - allow-metrics-traffic.yaml 3 | -------------------------------------------------------------------------------- /pkg/networkswitch/bpf_bpfeb.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kube-vm-project/eSwitch/HEAD/pkg/networkswitch/bpf_bpfeb.o -------------------------------------------------------------------------------- /pkg/networkswitch/bpf_bpfel.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kube-vm-project/eSwitch/HEAD/pkg/networkswitch/bpf_bpfel.o -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples of your project ## 2 | resources: 3 | - eswitchconfig_v1_eswitch.yaml 4 | # +kubebuilder:scaffold:manifestskustomizesamples 5 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | images: 6 | - name: controller 7 | newName: controller 8 | newTag: latest 9 | -------------------------------------------------------------------------------- /config/default/manager_metrics_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch adds the args to allow exposing the metrics endpoint using HTTPS 2 | - op: add 3 | path: /spec/template/spec/containers/0/args/0 4 | value: --metrics-bind-address=:8443 5 | -------------------------------------------------------------------------------- /config/rbac/metrics_reader_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: 7 | - "/metrics" 8 | verbs: 9 | - get 10 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: eswitch 6 | app.kubernetes.io/managed-by: kustomize 7 | name: controller-manager 8 | namespace: system 9 | -------------------------------------------------------------------------------- /config/samples/eswitchconfig_v1_eswitch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: eswitchconfig.kube-vm.io/v1 2 | kind: Eswitch 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: eswitch 6 | app.kubernetes.io/managed-by: kustomize 7 | name: eswitch-sample 8 | spec: 9 | # TODO(user): Add fields here 10 | -------------------------------------------------------------------------------- /config/rbac/metrics_auth_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: metrics-auth-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: metrics-auth-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/metrics_auth_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-auth-role 5 | rules: 6 | - apiGroups: 7 | - authentication.k8s.io 8 | resources: 9 | - tokenreviews 10 | verbs: 11 | - create 12 | - apiGroups: 13 | - authorization.k8s.io 14 | resources: 15 | - subjectaccessreviews 16 | verbs: 17 | - create 18 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: eswitch 6 | app.kubernetes.io/managed-by: kustomize 7 | name: manager-rolebinding 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: ClusterRole 11 | name: manager-role 12 | subjects: 13 | - kind: ServiceAccount 14 | name: controller-manager 15 | namespace: system 16 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: eswitch 6 | app.kubernetes.io/managed-by: kustomize 7 | name: leader-election-rolebinding 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: Role 11 | name: leader-election-role 12 | subjects: 13 | - kind: ServiceAccount 14 | name: controller-manager 15 | namespace: system 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | name: Run on Ubuntu 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Clone the code 13 | uses: actions/checkout@v4 14 | 15 | - name: Setup Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version-file: go.mod 19 | 20 | - name: Running Tests 21 | run: | 22 | go mod tidy 23 | make test 24 | -------------------------------------------------------------------------------- /config/default/metrics_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: eswitch 7 | app.kubernetes.io/managed-by: kustomize 8 | name: controller-manager-metrics-service 9 | namespace: system 10 | spec: 11 | ports: 12 | - name: https 13 | port: 8443 14 | protocol: TCP 15 | targetPort: 8443 16 | selector: 17 | control-plane: controller-manager 18 | app.kubernetes.io/name: eswitch 19 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | lint: 9 | name: Run on Ubuntu 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Clone the code 13 | uses: actions/checkout@v4 14 | 15 | - name: Setup Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version-file: go.mod 19 | 20 | - name: Run linter 21 | uses: golangci/golangci-lint-action@v6 22 | with: 23 | version: v1.63.4 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | bin/* 8 | Dockerfile.cross 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Go workspace file 17 | go.work 18 | 19 | # Kubernetes Generated files - skip generated files, except for vendored files 20 | !vendor/**/zz_generated.* 21 | 22 | # editor and IDE paraphernalia 23 | .idea 24 | .vscode 25 | *.swp 26 | *.swo 27 | *~ 28 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | 4 | # [PROMETHEUS-WITH-CERTS] The following patch configures the ServiceMonitor in ../prometheus 5 | # to securely reference certificates created and managed by cert-manager. 6 | # Additionally, ensure that you uncomment the [METRICS WITH CERTMANAGER] patch under config/default/kustomization.yaml 7 | # to mount the "metrics-server-cert" secret in the Manager Deployment. 8 | #patches: 9 | # - path: monitor_tls_patch.yaml 10 | # target: 11 | # kind: ServiceMonitor 12 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: manager-role 6 | rules: 7 | - apiGroups: 8 | - eswitchconfig.kube-vm.io 9 | resources: 10 | - eswitches 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - eswitchconfig.kube-vm.io 21 | resources: 22 | - eswitches/finalizers 23 | verbs: 24 | - update 25 | - apiGroups: 26 | - eswitchconfig.kube-vm.io 27 | resources: 28 | - eswitches/status 29 | verbs: 30 | - get 31 | - patch 32 | - update 33 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | # Code generated by tool. DO NOT EDIT. 2 | # This file is used to track the info used to scaffold your project 3 | # and allow the plugins properly work. 4 | # More info: https://book.kubebuilder.io/reference/project-config.html 5 | domain: kube-vm.io 6 | layout: 7 | - go.kubebuilder.io/v4 8 | projectName: eswitch 9 | repo: github.com/kube-vm-project/eSwitch 10 | resources: 11 | - api: 12 | crdVersion: v1 13 | namespaced: true 14 | controller: true 15 | domain: kube-vm.io 16 | group: eswitchconfig 17 | kind: Eswitch 18 | path: github.com/kube-vm-project/eSwitch/api/v1 19 | version: v1 20 | version: "3" 21 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Kubebuilder DevContainer", 3 | "image": "docker.io/golang:1.23", 4 | "features": { 5 | "ghcr.io/devcontainers/features/docker-in-docker:2": {}, 6 | "ghcr.io/devcontainers/features/git:1": {} 7 | }, 8 | 9 | "runArgs": ["--network=host"], 10 | 11 | "customizations": { 12 | "vscode": { 13 | "settings": { 14 | "terminal.integrated.shell.linux": "/bin/bash" 15 | }, 16 | "extensions": [ 17 | "ms-kubernetes-tools.vscode-kubernetes-tools", 18 | "ms-azuretools.vscode-docker" 19 | ] 20 | } 21 | }, 22 | 23 | "onCreateCommand": "bash .devcontainer/post-install.sh" 24 | } 25 | 26 | -------------------------------------------------------------------------------- /config/prometheus/monitor_tls_patch.yaml: -------------------------------------------------------------------------------- 1 | # Patch for Prometheus ServiceMonitor to enable secure TLS configuration 2 | # using certificates managed by cert-manager 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | name: controller-manager-metrics-monitor 7 | namespace: system 8 | spec: 9 | endpoints: 10 | - tlsConfig: 11 | insecureSkipVerify: false 12 | ca: 13 | secret: 14 | name: metrics-server-cert 15 | key: ca.crt 16 | cert: 17 | secret: 18 | name: metrics-server-cert 19 | key: tls.crt 20 | keySecret: 21 | name: metrics-server-cert 22 | key: tls.key 23 | -------------------------------------------------------------------------------- /.devcontainer/post-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | 4 | curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64 5 | chmod +x ./kind 6 | mv ./kind /usr/local/bin/kind 7 | 8 | curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/linux/amd64 9 | chmod +x kubebuilder 10 | mv kubebuilder /usr/local/bin/ 11 | 12 | KUBECTL_VERSION=$(curl -L -s https://dl.k8s.io/release/stable.txt) 13 | curl -LO "https://dl.k8s.io/release/$KUBECTL_VERSION/bin/linux/amd64/kubectl" 14 | chmod +x kubectl 15 | mv kubectl /usr/local/bin/kubectl 16 | 17 | docker network create -d=bridge --subnet=172.19.0.0/24 kind 18 | 19 | kind version 20 | kubebuilder version 21 | docker --version 22 | go version 23 | kubectl version --client 24 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/eswitchconfig.kube-vm.io_eswitches.yaml 6 | # +kubebuilder:scaffold:crdkustomizeresource 7 | 8 | patches: 9 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 10 | # patches here are for enabling the conversion webhook for each CRD 11 | # +kubebuilder:scaffold:crdkustomizewebhookpatch 12 | 13 | # [WEBHOOK] To enable webhook, uncomment the following section 14 | # the following config is for teaching kustomize how to do kustomization for CRDs. 15 | #configurations: 16 | #- kustomizeconfig.yaml 17 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: eswitch 7 | app.kubernetes.io/managed-by: kustomize 8 | name: leader-election-role 9 | rules: 10 | - apiGroups: 11 | - "" 12 | resources: 13 | - configmaps 14 | verbs: 15 | - get 16 | - list 17 | - watch 18 | - create 19 | - update 20 | - patch 21 | - delete 22 | - apiGroups: 23 | - coordination.k8s.io 24 | resources: 25 | - leases 26 | verbs: 27 | - get 28 | - list 29 | - watch 30 | - create 31 | - update 32 | - patch 33 | - delete 34 | - apiGroups: 35 | - "" 36 | resources: 37 | - events 38 | verbs: 39 | - create 40 | - patch 41 | -------------------------------------------------------------------------------- /config/rbac/eswitch_admin_role.yaml: -------------------------------------------------------------------------------- 1 | # This rule is not used by the project eswitch itself. 2 | # It is provided to allow the cluster admin to help manage permissions for users. 3 | # 4 | # Grants full permissions ('*') over eswitchconfig.kube-vm.io. 5 | # This role is intended for users authorized to modify roles and bindings within the cluster, 6 | # enabling them to delegate specific permissions to other users or groups as needed. 7 | 8 | apiVersion: rbac.authorization.k8s.io/v1 9 | kind: ClusterRole 10 | metadata: 11 | labels: 12 | app.kubernetes.io/name: eswitch 13 | app.kubernetes.io/managed-by: kustomize 14 | name: eswitch-admin-role 15 | rules: 16 | - apiGroups: 17 | - eswitchconfig.kube-vm.io 18 | resources: 19 | - eswitches 20 | verbs: 21 | - '*' 22 | - apiGroups: 23 | - eswitchconfig.kube-vm.io 24 | resources: 25 | - eswitches/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /.github/workflows/test-e2e.yml: -------------------------------------------------------------------------------- 1 | name: E2E Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test-e2e: 9 | name: Run on Ubuntu 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Clone the code 13 | uses: actions/checkout@v4 14 | 15 | - name: Setup Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version-file: go.mod 19 | 20 | - name: Install the latest version of kind 21 | run: | 22 | curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64 23 | chmod +x ./kind 24 | sudo mv ./kind /usr/local/bin/kind 25 | 26 | - name: Verify kind installation 27 | run: kind version 28 | 29 | - name: Create kind cluster 30 | run: kind create cluster 31 | 32 | - name: Running Test e2e 33 | run: | 34 | go mod tidy 35 | make test-e2e 36 | -------------------------------------------------------------------------------- /config/rbac/eswitch_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # This rule is not used by the project eswitch itself. 2 | # It is provided to allow the cluster admin to help manage permissions for users. 3 | # 4 | # Grants read-only access to eswitchconfig.kube-vm.io resources. 5 | # This role is intended for users who need visibility into these resources 6 | # without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing. 7 | 8 | apiVersion: rbac.authorization.k8s.io/v1 9 | kind: ClusterRole 10 | metadata: 11 | labels: 12 | app.kubernetes.io/name: eswitch 13 | app.kubernetes.io/managed-by: kustomize 14 | name: eswitch-viewer-role 15 | rules: 16 | - apiGroups: 17 | - eswitchconfig.kube-vm.io 18 | resources: 19 | - eswitches 20 | verbs: 21 | - get 22 | - list 23 | - watch 24 | - apiGroups: 25 | - eswitchconfig.kube-vm.io 26 | resources: 27 | - eswitches/status 28 | verbs: 29 | - get 30 | -------------------------------------------------------------------------------- /config/rbac/eswitch_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # This rule is not used by the project eswitch itself. 2 | # It is provided to allow the cluster admin to help manage permissions for users. 3 | # 4 | # Grants permissions to create, update, and delete resources within the eswitchconfig.kube-vm.io. 5 | # This role is intended for users who need to manage these resources 6 | # but should not control RBAC or manage permissions for others. 7 | 8 | apiVersion: rbac.authorization.k8s.io/v1 9 | kind: ClusterRole 10 | metadata: 11 | labels: 12 | app.kubernetes.io/name: eswitch 13 | app.kubernetes.io/managed-by: kustomize 14 | name: eswitch-editor-role 15 | rules: 16 | - apiGroups: 17 | - eswitchconfig.kube-vm.io 18 | resources: 19 | - eswitches 20 | verbs: 21 | - create 22 | - delete 23 | - get 24 | - list 25 | - patch 26 | - update 27 | - watch 28 | - apiGroups: 29 | - eswitchconfig.kube-vm.io 30 | resources: 31 | - eswitches/status 32 | verbs: 33 | - get 34 | -------------------------------------------------------------------------------- /config/network-policy/allow-metrics-traffic.yaml: -------------------------------------------------------------------------------- 1 | # This NetworkPolicy allows ingress traffic 2 | # with Pods running on namespaces labeled with 'metrics: enabled'. Only Pods on those 3 | # namespaces are able to gather data from the metrics endpoint. 4 | apiVersion: networking.k8s.io/v1 5 | kind: NetworkPolicy 6 | metadata: 7 | labels: 8 | app.kubernetes.io/name: eswitch 9 | app.kubernetes.io/managed-by: kustomize 10 | name: allow-metrics-traffic 11 | namespace: system 12 | spec: 13 | podSelector: 14 | matchLabels: 15 | control-plane: controller-manager 16 | app.kubernetes.io/name: eswitch 17 | policyTypes: 18 | - Ingress 19 | ingress: 20 | # This allows ingress traffic from any namespace with the label metrics: enabled 21 | - from: 22 | - namespaceSelector: 23 | matchLabels: 24 | metrics: enabled # Only from namespaces with this label 25 | ports: 26 | - port: 8443 27 | protocol: TCP 28 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | allow-parallel-runners: true 4 | 5 | issues: 6 | # don't skip warning about doc comments 7 | # don't exclude the default set of lint 8 | exclude-use-default: false 9 | # restore some of the defaults 10 | # (fill in the rest as needed) 11 | exclude-rules: 12 | - path: "api/*" 13 | linters: 14 | - lll 15 | - path: "internal/*" 16 | linters: 17 | - dupl 18 | - lll 19 | linters: 20 | disable-all: true 21 | enable: 22 | - dupl 23 | - errcheck 24 | - copyloopvar 25 | - ginkgolinter 26 | - goconst 27 | - gocyclo 28 | - gofmt 29 | - goimports 30 | - gosimple 31 | - govet 32 | - ineffassign 33 | - lll 34 | - misspell 35 | - nakedret 36 | - prealloc 37 | - revive 38 | - staticcheck 39 | - typecheck 40 | - unconvert 41 | - unparam 42 | - unused 43 | 44 | linters-settings: 45 | revive: 46 | rules: 47 | - name: comment-spacings 48 | -------------------------------------------------------------------------------- /config/default/cert_metrics_manager_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch adds the args, volumes, and ports to allow the manager to use the metrics-server certs. 2 | 3 | # Add the volumeMount for the metrics-server certs 4 | - op: add 5 | path: /spec/template/spec/containers/0/volumeMounts/- 6 | value: 7 | mountPath: /tmp/k8s-metrics-server/metrics-certs 8 | name: metrics-certs 9 | readOnly: true 10 | 11 | # Add the --metrics-cert-path argument for the metrics server 12 | - op: add 13 | path: /spec/template/spec/containers/0/args/- 14 | value: --metrics-cert-path=/tmp/k8s-metrics-server/metrics-certs 15 | 16 | # Add the metrics-server certs volume configuration 17 | - op: add 18 | path: /spec/template/spec/volumes/- 19 | value: 20 | name: metrics-certs 21 | secret: 22 | secretName: metrics-server-cert 23 | optional: false 24 | items: 25 | - key: ca.crt 26 | path: ca.crt 27 | - key: tls.crt 28 | path: tls.crt 29 | - key: tls.key 30 | path: tls.key 31 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | # Prometheus Monitor Service (Metrics) 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | labels: 6 | control-plane: controller-manager 7 | app.kubernetes.io/name: eswitch 8 | app.kubernetes.io/managed-by: kustomize 9 | name: controller-manager-metrics-monitor 10 | namespace: system 11 | spec: 12 | endpoints: 13 | - path: /metrics 14 | port: https # Ensure this is the name of the port that exposes HTTPS metrics 15 | scheme: https 16 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 17 | tlsConfig: 18 | # TODO(user): The option insecureSkipVerify: true is not recommended for production since it disables 19 | # certificate verification, exposing the system to potential man-in-the-middle attacks. 20 | # For production environments, it is recommended to use cert-manager for automatic TLS certificate management. 21 | # To apply this configuration, enable cert-manager and use the patch located at config/prometheus/servicemonitor_tls_patch.yaml, 22 | # which securely references the certificate from the 'metrics-server-cert' secret. 23 | insecureSkipVerify: true 24 | selector: 25 | matchLabels: 26 | control-plane: controller-manager 27 | app.kubernetes.io/name: eswitch 28 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # The following RBAC configurations are used to protect 13 | # the metrics endpoint with authn/authz. These configurations 14 | # ensure that only authorized users and service accounts 15 | # can access the metrics endpoint. Comment the following 16 | # permissions if you want to disable this protection. 17 | # More info: https://book.kubebuilder.io/reference/metrics.html 18 | - metrics_auth_role.yaml 19 | - metrics_auth_role_binding.yaml 20 | - metrics_reader_role.yaml 21 | # For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by 22 | # default, aiding admins in cluster management. Those roles are 23 | # not used by the {{ .ProjectName }} itself. You can comment the following lines 24 | # if you do not want those helpers be installed with your Project. 25 | - eswitch_admin_role.yaml 26 | - eswitch_editor_role.yaml 27 | - eswitch_viewer_role.yaml 28 | 29 | -------------------------------------------------------------------------------- /api/v1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1 contains API Schema definitions for the eswitchconfig v1 API group. 18 | // +kubebuilder:object:generate=true 19 | // +groupName=eswitchconfig.kube-vm.io 20 | package v1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects. 29 | GroupVersion = schema.GroupVersion{Group: "eswitchconfig.kube-vm.io", Version: "v1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme. 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM docker.io/golang:1.23 AS builder 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | 6 | WORKDIR /workspace 7 | # Copy the Go Modules manifests 8 | COPY go.mod go.mod 9 | COPY go.sum go.sum 10 | # cache deps before building and copying source so that we don't need to re-download as much 11 | # and so that source changes don't invalidate our downloaded layer 12 | RUN go mod download 13 | 14 | # Copy the go source 15 | COPY cmd/main.go cmd/main.go 16 | COPY api/ api/ 17 | COPY internal/ internal/ 18 | COPY pkg/ pkg/ 19 | 20 | # Build 21 | # the GOARCH has not a default value to allow the binary be built according to the host where the command 22 | # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO 23 | # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, 24 | # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. 25 | RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go 26 | 27 | # Use distroless as minimal base image to package the manager binary 28 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 29 | # TODO: move to different baseimage 30 | FROM ubuntu 31 | WORKDIR / 32 | COPY --from=builder /workspace/manager . 33 | # USER 65532:65532 // bit daft as we can't do the things we need as root 34 | 35 | ENTRYPOINT ["/manager"] 36 | -------------------------------------------------------------------------------- /api/v1/eswitch_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 24 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 25 | 26 | type EswitchPortSpec struct { 27 | // Physical interface name being added to the switch 28 | Interface string `json:"interface"` 29 | // The type of XDP mode being used for the interface 30 | XDPMode string `json:"xdpmode"` 31 | // Port VLANID to be used on the port itself 32 | PVID int `json:"pvid,omitempty"` 33 | // Tagged VLAN traffic to be allowed on the port 34 | VLANS []uint16 `json:"vlans,omitempty"` 35 | } 36 | 37 | // EswitchSpec defines the desired state of Eswitch. 38 | type EswitchSpec struct { 39 | // Ports contains the configuration for each port being added to the switch 40 | Ports []EswitchPortSpec `json:"ports"` 41 | // MAC addresses that are immediately ignored by the switch 42 | IgnoreMAC []string `json:"ignoreMAC,omitempty"` 43 | } 44 | 45 | // EswitchStatus defines the observed state of Eswitch. 46 | type EswitchStatus struct { 47 | Configured bool `json:"configured"` 48 | Errors bool `json:"errors"` 49 | } 50 | 51 | // +kubebuilder:object:root=true 52 | // +kubebuilder:subresource:status 53 | 54 | // Eswitch is the Schema for the eswitches API. 55 | type Eswitch struct { 56 | metav1.TypeMeta `json:",inline"` 57 | metav1.ObjectMeta `json:"metadata,omitempty"` 58 | 59 | Spec EswitchSpec `json:"spec,omitempty"` 60 | Status EswitchStatus `json:"status,omitempty"` 61 | } 62 | 63 | // +kubebuilder:object:root=true 64 | 65 | // EswitchList contains a list of Eswitch. 66 | type EswitchList struct { 67 | metav1.TypeMeta `json:",inline"` 68 | metav1.ListMeta `json:"metadata,omitempty"` 69 | Items []Eswitch `json:"items"` 70 | } 71 | 72 | func init() { 73 | SchemeBuilder.Register(&Eswitch{}, &EswitchList{}) 74 | } 75 | -------------------------------------------------------------------------------- /pkg/networkswitch/types.go: -------------------------------------------------------------------------------- 1 | package networkswitch 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/cilium/ebpf" 7 | "github.com/safchain/ethtool" 8 | "github.com/vishvananda/netlink" 9 | ) 10 | 11 | const MAX_IFACES = 255 // (was 80, this might break something) 12 | const TRAFFIC_KEY_SIZE = 16 13 | const STATS_ENABLED = false 14 | 15 | const ( 16 | HANDLE_UNTAGGED_TAIL_CALL = 1 17 | HANDLE_TAGGED_TAIL_CALL = 2 18 | HANDLE_UNTAGGED_TAIL_CALL_B = 3 19 | HANDLE_TAGGED_TAIL_CALL_B = 4 20 | 21 | HOOK_DROP_TC = 5 22 | HOOK_EGRESS_TC = 6 23 | ) 24 | 25 | var FEATURES_ENABLE = map[string]bool{ 26 | // "rx-gro": true, 27 | "rx-vlan-hw-parse": true, 28 | "tx-vlan-hw-insert": true, 29 | // "rx-hashing": true, 30 | // "tx-tcp-segmentation": true, 31 | // "tx-tcp-ecn-segmentation": true, 32 | // "tx-tcp6-segmentation": true, 33 | // "generic-receive-offload": true, 34 | } 35 | var FEATURES_DISABLE = map[string]bool{ 36 | // "rx-gro": false, 37 | "rx-vlan-hw-parse": false, 38 | "tx-vlan-hw-insert": false, 39 | // "rx-hashing": false, 40 | // "tx-tcp-segmentation": false, 41 | // "tx-tcp-ecn-segmentation": false, 42 | // "tx-tcp6-segmentation": false, 43 | // "generic-receive-offload": false, 44 | } 45 | 46 | var PROG_NAME string 47 | var DEFAULT_XDP_MODE string 48 | 49 | type BridgeGroup struct { 50 | IfMap map[string]*SwitchPort 51 | IfMapByIndex map[uint16]*SwitchPort 52 | 53 | IfList []*SwitchPort 54 | } 55 | 56 | type SwitchPort struct { 57 | driverName string 58 | speed uint32 59 | settings PortSettings 60 | iface *net.Interface 61 | netlink netlink.Link 62 | ethtoolHandle *ethtool.Ethtool 63 | ethtoolCmd *ethtool.EthtoolCmd 64 | eBPFSpec *ebpf.CollectionSpec 65 | eBPF bpfObjects 66 | // Tap *water.Interface //todo 67 | Stats portStats 68 | Traffic TrafficObserver 69 | } 70 | 71 | type PortSettings struct { 72 | Tap bool `json:"tap,omitempty" yaml:"tap,omitempty"` //todo 73 | PVID uint16 `json:"pvid,omitempty" yaml:"pvid,omitempty"` 74 | Vlans []uint16 `json:"vlans,omitempty" yaml:"vlans,omitempty"` 75 | Trunk bool `json:"trunk,omitempty" yaml:"trunk,omitempty"` 76 | XDPMode string `json:"xdpMode,omitempty" yaml:"xdpMode,omitempty"` 77 | Transparent bool `json:"transparent,omitempty" yaml:"transparent,omitempty"` 78 | HookDrop string `json:"hookDrop,omitempty" yaml:"hookDrop,omitempty"` 79 | HookEgress string `json:"hookEgress,omitempty" yaml:"hookEgress,omitempty"` 80 | IngressFiltering bool `json:"-" yaml:"-"` 81 | DontDown bool 82 | } 83 | -------------------------------------------------------------------------------- /internal/controller/eswitch_controller_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controller 18 | 19 | import ( 20 | "context" 21 | 22 | . "github.com/onsi/ginkgo/v2" 23 | . "github.com/onsi/gomega" 24 | "k8s.io/apimachinery/pkg/api/errors" 25 | "k8s.io/apimachinery/pkg/types" 26 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 27 | 28 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 | 30 | eswitchconfigv1 "github.com/kube-vm-project/eSwitch/api/v1" 31 | ) 32 | 33 | var _ = Describe("Eswitch Controller", func() { 34 | Context("When reconciling a resource", func() { 35 | const resourceName = "test-resource" 36 | 37 | ctx := context.Background() 38 | 39 | typeNamespacedName := types.NamespacedName{ 40 | Name: resourceName, 41 | Namespace: "default", // TODO(user):Modify as needed 42 | } 43 | eswitch := &eswitchconfigv1.Eswitch{} 44 | 45 | BeforeEach(func() { 46 | By("creating the custom resource for the Kind Eswitch") 47 | err := k8sClient.Get(ctx, typeNamespacedName, eswitch) 48 | if err != nil && errors.IsNotFound(err) { 49 | resource := &eswitchconfigv1.Eswitch{ 50 | ObjectMeta: metav1.ObjectMeta{ 51 | Name: resourceName, 52 | Namespace: "default", 53 | }, 54 | // TODO(user): Specify other spec details if needed. 55 | } 56 | Expect(k8sClient.Create(ctx, resource)).To(Succeed()) 57 | } 58 | }) 59 | 60 | AfterEach(func() { 61 | // TODO(user): Cleanup logic after each test, like removing the resource instance. 62 | resource := &eswitchconfigv1.Eswitch{} 63 | err := k8sClient.Get(ctx, typeNamespacedName, resource) 64 | Expect(err).NotTo(HaveOccurred()) 65 | 66 | By("Cleanup the specific resource instance Eswitch") 67 | Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) 68 | }) 69 | It("should successfully reconcile the resource", func() { 70 | By("Reconciling the created resource") 71 | controllerReconciler := &EswitchReconciler{ 72 | Client: k8sClient, 73 | Scheme: k8sClient.Scheme(), 74 | } 75 | 76 | _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ 77 | NamespacedName: typeNamespacedName, 78 | }) 79 | Expect(err).NotTo(HaveOccurred()) 80 | // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. 81 | // Example: If you expect a certain status condition after reconciliation, verify it here. 82 | }) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /pkg/networkswitch/ebpf.go: -------------------------------------------------------------------------------- 1 | package networkswitch 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/cilium/cilium/pkg/option" 7 | "github.com/vishvananda/netlink" 8 | "github.com/vishvananda/netlink/nl" 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -go-package networkswitch -cc clang -cflags "-O2 -g -Wall -Werror" bpf ../../ebpf/switch.c -- -I ../../ebpf/include 13 | 14 | func xdpModeToFlag(xdpMode string) uint32 { 15 | switch xdpMode { 16 | case option.XDPModeNative: 17 | return nl.XDP_FLAGS_DRV_MODE 18 | case option.XDPModeGeneric: 19 | return nl.XDP_FLAGS_SKB_MODE 20 | case option.XDPModeLinkDriver: 21 | return nl.XDP_FLAGS_DRV_MODE 22 | case option.XDPModeLinkGeneric: 23 | return nl.XDP_FLAGS_SKB_MODE 24 | } 25 | return 0 26 | } 27 | 28 | // attachPrograms, will attach the XDP and TC programs to the specific interface 29 | // If will attempt to determine which type of XDP mode to use before attaching 30 | func (port *SwitchPort) attachPrograms() error { 31 | if port.eBPF.ProgXdp != nil { 32 | mode := port.settings.XDPMode 33 | if mode == "" { 34 | mode = DEFAULT_XDP_MODE 35 | } 36 | 37 | err := netlink.LinkSetXdpFdWithFlags(port.netlink, port.eBPF.ProgXdp.FD(), int(xdpModeToFlag(mode))) 38 | if err != nil { //forced, todo 39 | fmt.Printf("Error attaching XDP program with flag: %s. Using xdpgeneric instead.", mode) 40 | if DEFAULT_XDP_MODE == option.XDPModeLinkGeneric { 41 | port.settings.XDPMode = "" 42 | } else { 43 | port.settings.XDPMode = option.XDPModeLinkGeneric 44 | } 45 | mode = option.XDPModeLinkGeneric 46 | err = netlink.LinkSetXdpFdWithFlags(port.netlink, port.eBPF.ProgXdp.FD(), int(xdpModeToFlag(mode))) 47 | if err != nil { 48 | return fmt.Errorf("attaching XDP program to interface %s: %w", port.iface.Name, err) 49 | } 50 | } 51 | } 52 | 53 | if port.eBPF.ProgTc != nil { 54 | if err := replaceQdisc(port.netlink); err != nil { 55 | return fmt.Errorf("replacing clsact qdisc for interface %s: %w", port.iface.Name, err) 56 | } 57 | 58 | filter := &netlink.BpfFilter{ 59 | FilterAttrs: netlink.FilterAttrs{ 60 | LinkIndex: port.netlink.Attrs().Index, 61 | Handle: netlink.MakeHandle(0, 1), 62 | Parent: netlink.HANDLE_MIN_INGRESS, 63 | Protocol: unix.ETH_P_ALL, 64 | Priority: 1, 65 | // Priority: uint16(option.Config.TCFilterPriority), 66 | }, 67 | Fd: port.eBPF.ProgTc.FD(), 68 | Name: fmt.Sprintf("%s-tc-ingress-%s", PROG_NAME, port.iface.Name), 69 | DirectAction: true, 70 | } 71 | 72 | if err := netlink.FilterReplace(filter); err != nil { 73 | return fmt.Errorf("replacing tc filter: %w", err) 74 | } 75 | } 76 | 77 | return nil 78 | } 79 | 80 | func replaceQdisc(link netlink.Link) error { 81 | attrs := netlink.QdiscAttrs{ 82 | LinkIndex: link.Attrs().Index, 83 | Handle: netlink.MakeHandle(0xffff, 0), 84 | Parent: netlink.HANDLE_CLSACT, 85 | } 86 | 87 | qdisc := &netlink.GenericQdisc{ 88 | QdiscAttrs: attrs, 89 | QdiscType: "clsact", 90 | } 91 | 92 | return netlink.QdiscReplace(qdisc) 93 | } 94 | -------------------------------------------------------------------------------- /config/crd/bases/eswitchconfig.kube-vm.io_eswitches.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.17.1 7 | name: eswitches.eswitchconfig.kube-vm.io 8 | spec: 9 | group: eswitchconfig.kube-vm.io 10 | names: 11 | kind: Eswitch 12 | listKind: EswitchList 13 | plural: eswitches 14 | singular: eswitch 15 | scope: Namespaced 16 | versions: 17 | - name: v1 18 | schema: 19 | openAPIV3Schema: 20 | description: Eswitch is the Schema for the eswitches API. 21 | properties: 22 | apiVersion: 23 | description: |- 24 | APIVersion defines the versioned schema of this representation of an object. 25 | Servers should convert recognized schemas to the latest internal value, and 26 | may reject unrecognized values. 27 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 28 | type: string 29 | kind: 30 | description: |- 31 | Kind is a string value representing the REST resource this object represents. 32 | Servers may infer this from the endpoint the client submits requests to. 33 | Cannot be updated. 34 | In CamelCase. 35 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 36 | type: string 37 | metadata: 38 | type: object 39 | spec: 40 | description: EswitchSpec defines the desired state of Eswitch. 41 | properties: 42 | ignoreMAC: 43 | description: MAC addresses that are immediately ignored by the switch 44 | items: 45 | type: string 46 | type: array 47 | ports: 48 | description: Ports contains the configuration for each port being 49 | added to the switch 50 | items: 51 | properties: 52 | interface: 53 | description: Physical interface name being added to the switch 54 | type: string 55 | pvid: 56 | description: Port VLANID to be used on the port itself 57 | type: integer 58 | vlans: 59 | description: Tagged VLAN traffic to be allowed on the port 60 | items: 61 | type: integer 62 | type: array 63 | xdpmode: 64 | description: The type of XDP mode being used for the interface 65 | type: string 66 | required: 67 | - interface 68 | - xdpmode 69 | type: object 70 | type: array 71 | required: 72 | - ports 73 | type: object 74 | status: 75 | description: EswitchStatus defines the observed state of Eswitch. 76 | properties: 77 | configured: 78 | type: boolean 79 | errors: 80 | type: boolean 81 | required: 82 | - configured 83 | - errors 84 | type: object 85 | type: object 86 | served: true 87 | storage: true 88 | subresources: 89 | status: {} 90 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: eswitch 7 | app.kubernetes.io/managed-by: kustomize 8 | name: system 9 | --- 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | metadata: 13 | name: controller-manager 14 | namespace: system 15 | labels: 16 | control-plane: controller-manager 17 | app.kubernetes.io/name: eswitch 18 | app.kubernetes.io/managed-by: kustomize 19 | spec: 20 | selector: 21 | matchLabels: 22 | control-plane: controller-manager 23 | app.kubernetes.io/name: eswitch 24 | replicas: 1 25 | template: 26 | metadata: 27 | annotations: 28 | kubectl.kubernetes.io/default-container: manager 29 | labels: 30 | control-plane: controller-manager 31 | app.kubernetes.io/name: eswitch 32 | spec: 33 | # TODO(user): Uncomment the following code to configure the nodeAffinity expression 34 | # according to the platforms which are supported by your solution. 35 | # It is considered best practice to support multiple architectures. You can 36 | # build your manager image using the makefile target docker-buildx. 37 | # affinity: 38 | # nodeAffinity: 39 | # requiredDuringSchedulingIgnoredDuringExecution: 40 | # nodeSelectorTerms: 41 | # - matchExpressions: 42 | # - key: kubernetes.io/arch 43 | # operator: In 44 | # values: 45 | # - amd64 46 | # - arm64 47 | # - ppc64le 48 | # - s390x 49 | # - key: kubernetes.io/os 50 | # operator: In 51 | # values: 52 | # - linux 53 | securityContext: 54 | # Projects are configured by default to adhere to the "restricted" Pod Security Standards. 55 | # This ensures that deployments meet the highest security requirements for Kubernetes. 56 | # For more details, see: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 57 | # runAsNonRoot: true 58 | # seccompProfile: 59 | # type: RuntimeDefault 60 | containers: 61 | - command: 62 | - /manager 63 | args: 64 | - --leader-elect 65 | - --health-probe-bind-address=:8081 66 | image: controller:latest 67 | name: manager 68 | ports: [] 69 | securityContext: 70 | privileged: true 71 | # allowPrivilegeEscalation: false 72 | # capabilities: 73 | # drop: 74 | # - "ALL" 75 | livenessProbe: 76 | httpGet: 77 | path: /healthz 78 | port: 8081 79 | initialDelaySeconds: 15 80 | periodSeconds: 20 81 | readinessProbe: 82 | httpGet: 83 | path: /readyz 84 | port: 8081 85 | initialDelaySeconds: 5 86 | periodSeconds: 10 87 | # TODO(user): Configure the resources accordingly based on the project requirements. 88 | # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 89 | resources: 90 | limits: 91 | cpu: 500m 92 | memory: 128Mi 93 | requests: 94 | cpu: 10m 95 | memory: 64Mi 96 | volumeMounts: 97 | - mountPath: /sys/fs/bpf 98 | name: bpf-maps 99 | volumes: 100 | - hostPath: 101 | path: /sys/fs/bpf 102 | type: DirectoryOrCreate 103 | name: bpf-maps 104 | serviceAccountName: controller-manager 105 | terminationGracePeriodSeconds: 10 106 | -------------------------------------------------------------------------------- /internal/controller/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controller 18 | 19 | import ( 20 | "context" 21 | "os" 22 | "path/filepath" 23 | "testing" 24 | 25 | . "github.com/onsi/ginkgo/v2" 26 | . "github.com/onsi/gomega" 27 | 28 | "k8s.io/client-go/kubernetes/scheme" 29 | "k8s.io/client-go/rest" 30 | "sigs.k8s.io/controller-runtime/pkg/client" 31 | "sigs.k8s.io/controller-runtime/pkg/envtest" 32 | logf "sigs.k8s.io/controller-runtime/pkg/log" 33 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 34 | 35 | eswitchconfigv1 "github.com/kube-vm-project/eSwitch/api/v1" 36 | // +kubebuilder:scaffold:imports 37 | ) 38 | 39 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 40 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 41 | 42 | var ( 43 | ctx context.Context 44 | cancel context.CancelFunc 45 | testEnv *envtest.Environment 46 | cfg *rest.Config 47 | k8sClient client.Client 48 | ) 49 | 50 | func TestControllers(t *testing.T) { 51 | RegisterFailHandler(Fail) 52 | 53 | RunSpecs(t, "Controller Suite") 54 | } 55 | 56 | var _ = BeforeSuite(func() { 57 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 58 | 59 | ctx, cancel = context.WithCancel(context.TODO()) 60 | 61 | var err error 62 | err = eswitchconfigv1.AddToScheme(scheme.Scheme) 63 | Expect(err).NotTo(HaveOccurred()) 64 | 65 | // +kubebuilder:scaffold:scheme 66 | 67 | By("bootstrapping test environment") 68 | testEnv = &envtest.Environment{ 69 | CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, 70 | ErrorIfCRDPathMissing: true, 71 | } 72 | 73 | // Retrieve the first found binary directory to allow running tests from IDEs 74 | if getFirstFoundEnvTestBinaryDir() != "" { 75 | testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir() 76 | } 77 | 78 | // cfg is defined in this file globally. 79 | cfg, err = testEnv.Start() 80 | Expect(err).NotTo(HaveOccurred()) 81 | Expect(cfg).NotTo(BeNil()) 82 | 83 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 84 | Expect(err).NotTo(HaveOccurred()) 85 | Expect(k8sClient).NotTo(BeNil()) 86 | }) 87 | 88 | var _ = AfterSuite(func() { 89 | By("tearing down the test environment") 90 | cancel() 91 | err := testEnv.Stop() 92 | Expect(err).NotTo(HaveOccurred()) 93 | }) 94 | 95 | // getFirstFoundEnvTestBinaryDir locates the first binary in the specified path. 96 | // ENVTEST-based tests depend on specific binaries, usually located in paths set by 97 | // controller-runtime. When running tests directly (e.g., via an IDE) without using 98 | // Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured. 99 | // 100 | // This function streamlines the process by finding the required binaries, similar to 101 | // setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are 102 | // properly set up, run 'make setup-envtest' beforehand. 103 | func getFirstFoundEnvTestBinaryDir() string { 104 | basePath := filepath.Join("..", "..", "bin", "k8s") 105 | entries, err := os.ReadDir(basePath) 106 | if err != nil { 107 | logf.Log.Error(err, "Failed to read directory", "path", basePath) 108 | return "" 109 | } 110 | for _, entry := range entries { 111 | if entry.IsDir() { 112 | return filepath.Join(basePath, entry.Name()) 113 | } 114 | } 115 | return "" 116 | } 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eswitch 2 | The eSwitch project is an eBPF 🐝 layer2 switch 3 | 4 | ## Description 5 | 6 | The eSwitch is a software switch designed to move packets between a set of interfaces and is all powered by eBPF and wrapped in Kubernetes! 7 | 8 | ## Getting Started 9 | 10 | Use at your own risk, this is a Proof of Concept and will require a lot of work to be stable. 11 | 12 | ### Prerequisites 13 | - go version v1.23.0+ 14 | - docker version 17.03+. 15 | - kubectl version v1.11.3+. 16 | - Access to a Kubernetes v1.11.3+ cluster. 17 | 18 | ### To Deploy on the cluster 19 | **Build and push your image to the location specified by `IMG`:** 20 | 21 | ```sh 22 | make docker-build docker-push IMG=/eswitch:tag 23 | ``` 24 | 25 | **NOTE:** This image ought to be published in the personal registry you specified. 26 | And it is required to have access to pull the image from the working environment. 27 | Make sure you have the proper permission to the registry if the above commands don’t work. 28 | 29 | **Install the CRDs into the cluster:** 30 | 31 | ```sh 32 | make install 33 | ``` 34 | 35 | **Deploy the Manager to the cluster with the image specified by `IMG`:** 36 | 37 | ```sh 38 | make deploy IMG=/eswitch:tag 39 | ``` 40 | 41 | > **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin 42 | privileges or be logged in as admin. 43 | 44 | **Create instances of your solution** 45 | You can apply the samples (examples) from the config/sample: 46 | 47 | ```sh 48 | kubectl apply -k config/samples/ 49 | ``` 50 | 51 | >**NOTE**: Ensure that the samples has default values to test it out. 52 | 53 | ### To Uninstall 54 | **Delete the instances (CRs) from the cluster:** 55 | 56 | ```sh 57 | kubectl delete -k config/samples/ 58 | ``` 59 | 60 | **Delete the APIs(CRDs) from the cluster:** 61 | 62 | ```sh 63 | make uninstall 64 | ``` 65 | 66 | **UnDeploy the controller from the cluster:** 67 | 68 | ```sh 69 | make undeploy 70 | ``` 71 | 72 | ## Project Distribution 73 | 74 | Following the options to release and provide this solution to the users. 75 | 76 | ### By providing a bundle with all YAML files 77 | 78 | 1. Build the installer for the image built and published in the registry: 79 | 80 | ```sh 81 | make build-installer IMG=/eswitch:tag 82 | ``` 83 | 84 | **NOTE:** The makefile target mentioned above generates an 'install.yaml' 85 | file in the dist directory. This file contains all the resources built 86 | with Kustomize, which are necessary to install this project without its 87 | dependencies. 88 | 89 | 2. Using the installer 90 | 91 | Users can just run 'kubectl apply -f ' to install 92 | the project, i.e.: 93 | 94 | ```sh 95 | kubectl apply -f https://raw.githubusercontent.com//eswitch//dist/install.yaml 96 | ``` 97 | 98 | ### By providing a Helm Chart 99 | 100 | 1. Build the chart using the optional helm plugin 101 | 102 | ```sh 103 | kubebuilder edit --plugins=helm/v1-alpha 104 | ``` 105 | 106 | 2. See that a chart was generated under 'dist/chart', and users 107 | can obtain this solution from there. 108 | 109 | **NOTE:** If you change the project, you need to update the Helm Chart 110 | using the same command above to sync the latest changes. Furthermore, 111 | if you create webhooks, you need to use the above command with 112 | the '--force' flag and manually ensure that any custom configuration 113 | previously added to 'dist/chart/values.yaml' or 'dist/chart/manager/manager.yaml' 114 | is manually re-applied afterwards. 115 | 116 | ## Contributing 117 | // TODO(user): Add detailed information on how you would like others to contribute to this project 118 | 119 | **NOTE:** Run `make help` for more information on all potential `make` targets 120 | 121 | More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) 122 | 123 | ## License 124 | 125 | Copyright 2025. 126 | 127 | Licensed under the Apache License, Version 2.0 (the "License"); 128 | you may not use this file except in compliance with the License. 129 | You may obtain a copy of the License at 130 | 131 | http://www.apache.org/licenses/LICENSE-2.0 132 | 133 | Unless required by applicable law or agreed to in writing, software 134 | distributed under the License is distributed on an "AS IS" BASIS, 135 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 136 | See the License for the specific language governing permissions and 137 | limitations under the License. 138 | 139 | -------------------------------------------------------------------------------- /api/v1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | /* 4 | Copyright 2025. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Code generated by controller-gen. DO NOT EDIT. 20 | 21 | package v1 22 | 23 | import ( 24 | runtime "k8s.io/apimachinery/pkg/runtime" 25 | ) 26 | 27 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 28 | func (in *Eswitch) DeepCopyInto(out *Eswitch) { 29 | *out = *in 30 | out.TypeMeta = in.TypeMeta 31 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 32 | in.Spec.DeepCopyInto(&out.Spec) 33 | out.Status = in.Status 34 | } 35 | 36 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Eswitch. 37 | func (in *Eswitch) DeepCopy() *Eswitch { 38 | if in == nil { 39 | return nil 40 | } 41 | out := new(Eswitch) 42 | in.DeepCopyInto(out) 43 | return out 44 | } 45 | 46 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 47 | func (in *Eswitch) DeepCopyObject() runtime.Object { 48 | if c := in.DeepCopy(); c != nil { 49 | return c 50 | } 51 | return nil 52 | } 53 | 54 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 55 | func (in *EswitchList) DeepCopyInto(out *EswitchList) { 56 | *out = *in 57 | out.TypeMeta = in.TypeMeta 58 | in.ListMeta.DeepCopyInto(&out.ListMeta) 59 | if in.Items != nil { 60 | in, out := &in.Items, &out.Items 61 | *out = make([]Eswitch, len(*in)) 62 | for i := range *in { 63 | (*in)[i].DeepCopyInto(&(*out)[i]) 64 | } 65 | } 66 | } 67 | 68 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EswitchList. 69 | func (in *EswitchList) DeepCopy() *EswitchList { 70 | if in == nil { 71 | return nil 72 | } 73 | out := new(EswitchList) 74 | in.DeepCopyInto(out) 75 | return out 76 | } 77 | 78 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 79 | func (in *EswitchList) DeepCopyObject() runtime.Object { 80 | if c := in.DeepCopy(); c != nil { 81 | return c 82 | } 83 | return nil 84 | } 85 | 86 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 87 | func (in *EswitchPortSpec) DeepCopyInto(out *EswitchPortSpec) { 88 | *out = *in 89 | if in.VLANS != nil { 90 | in, out := &in.VLANS, &out.VLANS 91 | *out = make([]uint16, len(*in)) 92 | copy(*out, *in) 93 | } 94 | } 95 | 96 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EswitchPortSpec. 97 | func (in *EswitchPortSpec) DeepCopy() *EswitchPortSpec { 98 | if in == nil { 99 | return nil 100 | } 101 | out := new(EswitchPortSpec) 102 | in.DeepCopyInto(out) 103 | return out 104 | } 105 | 106 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 107 | func (in *EswitchSpec) DeepCopyInto(out *EswitchSpec) { 108 | *out = *in 109 | if in.Ports != nil { 110 | in, out := &in.Ports, &out.Ports 111 | *out = make([]EswitchPortSpec, len(*in)) 112 | for i := range *in { 113 | (*in)[i].DeepCopyInto(&(*out)[i]) 114 | } 115 | } 116 | if in.IgnoreMAC != nil { 117 | in, out := &in.IgnoreMAC, &out.IgnoreMAC 118 | *out = make([]string, len(*in)) 119 | copy(*out, *in) 120 | } 121 | } 122 | 123 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EswitchSpec. 124 | func (in *EswitchSpec) DeepCopy() *EswitchSpec { 125 | if in == nil { 126 | return nil 127 | } 128 | out := new(EswitchSpec) 129 | in.DeepCopyInto(out) 130 | return out 131 | } 132 | 133 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 134 | func (in *EswitchStatus) DeepCopyInto(out *EswitchStatus) { 135 | *out = *in 136 | } 137 | 138 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EswitchStatus. 139 | func (in *EswitchStatus) DeepCopy() *EswitchStatus { 140 | if in == nil { 141 | return nil 142 | } 143 | out := new(EswitchStatus) 144 | in.DeepCopyInto(out) 145 | return out 146 | } 147 | -------------------------------------------------------------------------------- /test/e2e/e2e_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package e2e 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "os/exec" 23 | "testing" 24 | 25 | . "github.com/onsi/ginkgo/v2" 26 | . "github.com/onsi/gomega" 27 | 28 | "github.com/kube-vm-project/eSwitch/test/utils" 29 | ) 30 | 31 | var ( 32 | // Optional Environment Variables: 33 | // - PROMETHEUS_INSTALL_SKIP=true: Skips Prometheus Operator installation during test setup. 34 | // - CERT_MANAGER_INSTALL_SKIP=true: Skips CertManager installation during test setup. 35 | // These variables are useful if Prometheus or CertManager is already installed, avoiding 36 | // re-installation and conflicts. 37 | skipPrometheusInstall = os.Getenv("PROMETHEUS_INSTALL_SKIP") == "true" 38 | skipCertManagerInstall = os.Getenv("CERT_MANAGER_INSTALL_SKIP") == "true" 39 | // isPrometheusOperatorAlreadyInstalled will be set true when prometheus CRDs be found on the cluster 40 | isPrometheusOperatorAlreadyInstalled = false 41 | // isCertManagerAlreadyInstalled will be set true when CertManager CRDs be found on the cluster 42 | isCertManagerAlreadyInstalled = false 43 | 44 | // projectImage is the name of the image which will be build and loaded 45 | // with the code source changes to be tested. 46 | projectImage = "example.com/eswitch:v0.0.1" 47 | ) 48 | 49 | // TestE2E runs the end-to-end (e2e) test suite for the project. These tests execute in an isolated, 50 | // temporary environment to validate project changes with the the purposed to be used in CI jobs. 51 | // The default setup requires Kind, builds/loads the Manager Docker image locally, and installs 52 | // CertManager and Prometheus. 53 | func TestE2E(t *testing.T) { 54 | RegisterFailHandler(Fail) 55 | _, _ = fmt.Fprintf(GinkgoWriter, "Starting eswitch integration test suite\n") 56 | RunSpecs(t, "e2e suite") 57 | } 58 | 59 | var _ = BeforeSuite(func() { 60 | By("Ensure that Prometheus is enabled") 61 | _ = utils.UncommentCode("config/default/kustomization.yaml", "#- ../prometheus", "#") 62 | 63 | By("building the manager(Operator) image") 64 | cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", projectImage)) 65 | _, err := utils.Run(cmd) 66 | ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to build the manager(Operator) image") 67 | 68 | // TODO(user): If you want to change the e2e test vendor from Kind, ensure the image is 69 | // built and available before running the tests. Also, remove the following block. 70 | By("loading the manager(Operator) image on Kind") 71 | err = utils.LoadImageToKindClusterWithName(projectImage) 72 | ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to load the manager(Operator) image into Kind") 73 | 74 | // The tests-e2e are intended to run on a temporary cluster that is created and destroyed for testing. 75 | // To prevent errors when tests run in environments with Prometheus or CertManager already installed, 76 | // we check for their presence before execution. 77 | // Setup Prometheus and CertManager before the suite if not skipped and if not already installed 78 | if !skipPrometheusInstall { 79 | By("checking if prometheus is installed already") 80 | isPrometheusOperatorAlreadyInstalled = utils.IsPrometheusCRDsInstalled() 81 | if !isPrometheusOperatorAlreadyInstalled { 82 | _, _ = fmt.Fprintf(GinkgoWriter, "Installing Prometheus Operator...\n") 83 | Expect(utils.InstallPrometheusOperator()).To(Succeed(), "Failed to install Prometheus Operator") 84 | } else { 85 | _, _ = fmt.Fprintf(GinkgoWriter, "WARNING: Prometheus Operator is already installed. Skipping installation...\n") 86 | } 87 | } 88 | if !skipCertManagerInstall { 89 | By("checking if cert manager is installed already") 90 | isCertManagerAlreadyInstalled = utils.IsCertManagerCRDsInstalled() 91 | if !isCertManagerAlreadyInstalled { 92 | _, _ = fmt.Fprintf(GinkgoWriter, "Installing CertManager...\n") 93 | Expect(utils.InstallCertManager()).To(Succeed(), "Failed to install CertManager") 94 | } else { 95 | _, _ = fmt.Fprintf(GinkgoWriter, "WARNING: CertManager is already installed. Skipping installation...\n") 96 | } 97 | } 98 | }) 99 | 100 | var _ = AfterSuite(func() { 101 | // Teardown Prometheus and CertManager after the suite if not skipped and if they were not already installed 102 | if !skipPrometheusInstall && !isPrometheusOperatorAlreadyInstalled { 103 | _, _ = fmt.Fprintf(GinkgoWriter, "Uninstalling Prometheus Operator...\n") 104 | utils.UninstallPrometheusOperator() 105 | } 106 | if !skipCertManagerInstall && !isCertManagerAlreadyInstalled { 107 | _, _ = fmt.Fprintf(GinkgoWriter, "Uninstalling CertManager...\n") 108 | utils.UninstallCertManager() 109 | } 110 | }) 111 | -------------------------------------------------------------------------------- /internal/controller/eswitch_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controller 18 | 19 | import ( 20 | "context" 21 | "time" 22 | 23 | "k8s.io/apimachinery/pkg/runtime" 24 | ctrl "sigs.k8s.io/controller-runtime" 25 | "sigs.k8s.io/controller-runtime/pkg/client" 26 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 27 | "sigs.k8s.io/controller-runtime/pkg/log" 28 | 29 | v1 "github.com/kube-vm-project/eSwitch/api/v1" 30 | "github.com/kube-vm-project/eSwitch/pkg/networkswitch" 31 | ) 32 | 33 | var eSwitchFinalizer = "eswitch.kube-vm.io/finalizer" 34 | 35 | // EswitchReconciler reconciles a Eswitch object 36 | type EswitchReconciler struct { 37 | client.Client 38 | Scheme *runtime.Scheme 39 | } 40 | 41 | // +kubebuilder:rbac:groups=eswitchconfig.kube-vm.io,resources=eswitches,verbs=get;list;watch;create;update;patch;delete 42 | // +kubebuilder:rbac:groups=eswitchconfig.kube-vm.io,resources=eswitches/status,verbs=get;update;patch 43 | // +kubebuilder:rbac:groups=eswitchconfig.kube-vm.io,resources=eswitches/finalizers,verbs=update 44 | 45 | // Reconcile is part of the main kubernetes reconciliation loop which aims to 46 | // move the current state of the cluster closer to the desired state. 47 | // TODO(user): Modify the Reconcile function to compare the state specified by 48 | // the Eswitch object against the actual cluster state, and then 49 | // perform operations to make the cluster state reflect the state specified by 50 | // the user. 51 | // 52 | // For more details, check Reconcile and its Result here: 53 | // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.0/pkg/reconcile 54 | func (r *EswitchReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 55 | log := log.FromContext(ctx) 56 | var eswitch v1.Eswitch 57 | 58 | if err := r.Get(ctx, req.NamespacedName, &eswitch); err != nil { 59 | //log.Error(err, "unable to fetch Switch configuration") 60 | // we'll ignore not-found errors, since they can't be fixed by an immediate 61 | // requeue (we'll need to wait for a new notification), and we can get them 62 | // on deleted requests. 63 | return ctrl.Result{}, client.IgnoreNotFound(err) 64 | } 65 | 66 | if eswitch.DeletionTimestamp.IsZero() { 67 | if !controllerutil.ContainsFinalizer(&eswitch, eSwitchFinalizer) { 68 | controllerutil.AddFinalizer(&eswitch, eSwitchFinalizer) 69 | if err := r.Update(ctx, &eswitch); err != nil { 70 | return ctrl.Result{}, err 71 | } 72 | // return here as it should be reconciled again 73 | return ctrl.Result{}, nil 74 | } 75 | } else { 76 | log.Info("removing", "switch name", eswitch.Name) 77 | 78 | if !controllerutil.ContainsFinalizer(&eswitch, eSwitchFinalizer) { 79 | // Clean up resources 80 | } 81 | err := networkswitch.DownAll() 82 | if err != nil { 83 | log.Error(err, "bringing switch down") 84 | } 85 | 86 | controllerutil.RemoveFinalizer(&eswitch, eSwitchFinalizer) 87 | if err := r.Update(ctx, &eswitch); err != nil { 88 | return ctrl.Result{}, err 89 | } 90 | 91 | // Stop reconciliation as the item is being deleted 92 | return ctrl.Result{}, nil 93 | } 94 | 95 | bg, err := parseSwitchConfig(&eswitch) 96 | if err != nil { 97 | log.Error(err, "parsing switch configuration") 98 | return ctrl.Result{RequeueAfter: time.Second * 5}, err 99 | 100 | } 101 | 102 | err = bg.Up() 103 | if err != nil { 104 | log.Error(err, "bringing switch up") 105 | return ctrl.Result{RequeueAfter: time.Second * 5}, err 106 | 107 | } 108 | log.Info("switch programmed", "switch name", eswitch.Name) 109 | eswitch.Status.Configured = true 110 | err = r.Client.Status().Update(context.TODO(), &eswitch, &client.SubResourceUpdateOptions{}) 111 | if err != nil { 112 | log.Error(err, "unable to update switch") 113 | } 114 | bg.PrettyPrint() // todo - don't need this 115 | return ctrl.Result{}, nil 116 | } 117 | 118 | // SetupWithManager sets up the controller with the Manager. 119 | func (r *EswitchReconciler) SetupWithManager(mgr ctrl.Manager) error { 120 | return ctrl.NewControllerManagedBy(mgr). 121 | For(&v1.Eswitch{}). 122 | Named("eswitch"). 123 | Complete(r) 124 | } 125 | 126 | func parseSwitchConfig(config *v1.Eswitch) (*networkswitch.BridgeGroup, error) { 127 | ifaces := make(map[string]networkswitch.PortSettings) 128 | for x := range config.Spec.Ports { 129 | iface := networkswitch.PortSettings{ 130 | Trunk: false, //todo 131 | PVID: uint16(config.Spec.Ports[x].PVID), 132 | Vlans: config.Spec.Ports[x].VLANS, 133 | XDPMode: config.Spec.Ports[x].XDPMode, 134 | // Tap: tap, 135 | Transparent: false, 136 | IngressFiltering: true, 137 | HookDrop: "", 138 | HookEgress: "", 139 | } 140 | iface.Validate() 141 | ifaces[config.Spec.Ports[x].Interface] = iface 142 | } 143 | 144 | ports := &networkswitch.BridgeGroup{ 145 | IfMap: make(map[string]*networkswitch.SwitchPort), 146 | IfMapByIndex: make(map[uint16]*networkswitch.SwitchPort), 147 | IfList: []*networkswitch.SwitchPort{}, 148 | } 149 | for ifName, ifSettings := range ifaces { 150 | ifSettings.Validate() 151 | 152 | err := ports.AddPort(ifName, ifSettings) 153 | if err != nil { 154 | return nil, err 155 | } 156 | } 157 | ports.IfList = ports.BuildPortList() 158 | 159 | return ports, nil 160 | } 161 | -------------------------------------------------------------------------------- /pkg/networkswitch/utils.go: -------------------------------------------------------------------------------- 1 | package networkswitch 2 | 3 | import ( 4 | "encoding/binary" 5 | "net" 6 | "net/netip" 7 | ) 8 | 9 | func ip2int(ip net.IP) uint32 { 10 | // if len(ip) == 16 { 11 | // panic("no sane way to convert ipv6 into uint32") 12 | // } 13 | return binary.BigEndian.Uint32(ip) 14 | } 15 | 16 | func int2ip(nn uint32) net.IP { 17 | ip := make(net.IP, 4) 18 | binary.BigEndian.PutUint32(ip, nn) 19 | return ip 20 | } 21 | 22 | func int2netip(nn uint32) (addr netip.Addr, ok bool) { 23 | ipBytes := make([]byte, 4) 24 | binary.BigEndian.PutUint32(ipBytes, nn) 25 | addr, ok = netip.AddrFromSlice(ipBytes) 26 | return 27 | } 28 | 29 | func extractDstMac(frame *[]byte) net.HardwareAddr { 30 | return net.HardwareAddr((*frame)[0:6]) 31 | } 32 | 33 | func extractSrcMac(frame *[]byte) net.HardwareAddr { 34 | return net.HardwareAddr((*frame)[6:12]) 35 | } 36 | 37 | func sliceRepeat[T any](size int, v T) []T { 38 | retval := make([]T, 0, size) 39 | for i := 0; i < size; i++ { 40 | retval = append(retval, v) 41 | } 42 | return retval 43 | } 44 | 45 | func divmod(numerator, denominator int64) (quotient, remainder int64) { 46 | quotient = numerator / denominator // integer division, decimals are truncated 47 | remainder = numerator % denominator 48 | return 49 | } 50 | 51 | func bitmaskVlanList64(vlans []uint16) [64]uint64 { 52 | var out [64]uint64 53 | for _, vlan := range vlans { 54 | vlan64 := uint64(vlan) 55 | 56 | section := vlan64 / 64 // integer division, decimals are truncated 57 | offset := vlan64 % 64 // decimal offset in range 0-63 58 | offsetBitmask := uint64(1) << offset 59 | 60 | out[section] = out[section] | offsetBitmask 61 | } 62 | 63 | return out 64 | } 65 | 66 | func bitmaskVlanList32(vlans []uint16) [128]uint32 { 67 | var out [128]uint32 68 | for _, vlan := range vlans { 69 | vlan32 := uint32(vlan) 70 | 71 | section := vlan32 / 32 // integer division, decimals are truncated 72 | offset := vlan32 % 32 // decimal offset in range 0-127 73 | offsetBitmask := uint32(1) << offset 74 | 75 | out[section] = out[section] | offsetBitmask 76 | } 77 | 78 | return out 79 | } 80 | 81 | func bitmaskVlanList16(vlans []uint16) [256]uint16 { 82 | out := [256]uint16{} 83 | for _, vlan := range vlans { 84 | section := vlan / 16 // integer division, decimals are truncated 85 | offset := vlan % 16 // decimal offset in range 0-255 86 | offsetBitmask := uint16(1) << offset 87 | 88 | out[section] = out[section] | offsetBitmask 89 | } 90 | 91 | return out 92 | } 93 | 94 | func bitmaskAllVlans16(exclude []uint16) [256]uint16 { 95 | out := ALL_VLANS_BITMASK_16 96 | for _, vlan := range exclude { 97 | section := vlan / 16 // integer division, decimals are truncated 98 | offset := vlan % 16 // decimal offset in range 0-255 99 | offsetBitmask := uint16(1) << offset 100 | 101 | out[section] = out[section] ^ offsetBitmask 102 | } 103 | 104 | return out 105 | } 106 | 107 | func bitmaskAllVlans64(exclude []uint16) [64]uint64 { 108 | 109 | out := ALL_VLANS_BITMASK_64 110 | for _, vlan := range exclude { 111 | section := vlan / 64 // integer division, decimals are truncated 112 | offset := vlan % 64 // decimal offset in range 0-255 113 | offsetBitmask := uint64(1) << offset 114 | out[section] = out[section] ^ offsetBitmask 115 | } 116 | 117 | return out 118 | } 119 | 120 | var ALL_VLANS_BITMASK_64 [64]uint64 = [64]uint64{18446744073709551614, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 18446744073709551615, 9223372036854775807} 121 | 122 | var ALL_VLANS_BITMASK_16 [256]uint16 = [256]uint16{65534, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535, 32767} 123 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kube-vm-project/eSwitch 2 | 3 | go 1.23.0 4 | 5 | godebug default=go1.23 6 | 7 | require ( 8 | github.com/cilium/cilium v1.17.1 9 | github.com/cilium/ebpf v0.17.3 10 | github.com/hashicorp/golang-lru/v2 v2.0.7 11 | github.com/onsi/ginkgo/v2 v2.21.0 12 | github.com/onsi/gomega v1.35.1 13 | github.com/safchain/ethtool v0.5.10 14 | github.com/vishvananda/netlink v1.3.1-0.20241022031324-976bd8de7d81 15 | github.com/xlab/treeprint v1.2.0 16 | golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa 17 | golang.org/x/sys v0.30.0 18 | k8s.io/apimachinery v0.32.0 19 | k8s.io/client-go v0.32.0 20 | sigs.k8s.io/controller-runtime v0.20.1 21 | ) 22 | 23 | require ( 24 | cel.dev/expr v0.18.0 // indirect 25 | github.com/antlr4-go/antlr/v4 v4.13.0 // indirect 26 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect 27 | github.com/beorn7/perks v1.0.1 // indirect 28 | github.com/blang/semver/v4 v4.0.0 // indirect 29 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 30 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 31 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 32 | github.com/emicklei/go-restful/v3 v3.12.0 // indirect 33 | github.com/evanphx/json-patch/v5 v5.9.0 // indirect 34 | github.com/felixge/httpsnoop v1.0.4 // indirect 35 | github.com/fsnotify/fsnotify v1.8.0 // indirect 36 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 37 | github.com/go-logr/logr v1.4.2 // indirect 38 | github.com/go-logr/stdr v1.2.2 // indirect 39 | github.com/go-logr/zapr v1.3.0 // indirect 40 | github.com/go-openapi/analysis v0.23.0 // indirect 41 | github.com/go-openapi/errors v0.22.0 // indirect 42 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 43 | github.com/go-openapi/jsonreference v0.21.0 // indirect 44 | github.com/go-openapi/loads v0.22.0 // indirect 45 | github.com/go-openapi/spec v0.21.0 // indirect 46 | github.com/go-openapi/strfmt v0.23.0 // indirect 47 | github.com/go-openapi/swag v0.23.0 // indirect 48 | github.com/go-openapi/validate v0.24.0 // indirect 49 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 50 | github.com/gogo/protobuf v1.3.2 // indirect 51 | github.com/golang/protobuf v1.5.4 // indirect 52 | github.com/google/btree v1.1.3 // indirect 53 | github.com/google/cel-go v0.22.1 // indirect 54 | github.com/google/gnostic-models v0.6.8 // indirect 55 | github.com/google/go-cmp v0.6.0 // indirect 56 | github.com/google/gofuzz v1.2.0 // indirect 57 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect 58 | github.com/google/uuid v1.6.0 // indirect 59 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect 60 | github.com/hashicorp/hcl v1.0.1-vault-5 // indirect 61 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 62 | github.com/josharian/intern v1.0.0 // indirect 63 | github.com/json-iterator/go v1.1.12 // indirect 64 | github.com/klauspost/compress v1.17.9 // indirect 65 | github.com/mackerelio/go-osstat v0.2.5 // indirect 66 | github.com/magiconair/properties v1.8.7 // indirect 67 | github.com/mailru/easyjson v0.7.7 // indirect 68 | github.com/mitchellh/mapstructure v1.5.0 // indirect 69 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 70 | github.com/modern-go/reflect2 v1.0.2 // indirect 71 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 72 | github.com/oklog/ulid v1.3.1 // indirect 73 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 74 | github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect 75 | github.com/pkg/errors v0.9.1 // indirect 76 | github.com/prometheus/client_golang v1.20.5 // indirect 77 | github.com/prometheus/client_model v0.6.1 // indirect 78 | github.com/prometheus/common v0.61.0 // indirect 79 | github.com/prometheus/procfs v0.15.1 // indirect 80 | github.com/sagikazarmark/locafero v0.4.0 // indirect 81 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 82 | github.com/sasha-s/go-deadlock v0.3.5 // indirect 83 | github.com/sirupsen/logrus v1.9.3 // indirect 84 | github.com/sourcegraph/conc v0.3.0 // indirect 85 | github.com/spf13/afero v1.11.0 // indirect 86 | github.com/spf13/cast v1.7.0 // indirect 87 | github.com/spf13/cobra v1.8.1 // indirect 88 | github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace // indirect 89 | github.com/spf13/viper v1.19.0 // indirect 90 | github.com/stoewer/go-strcase v1.3.0 // indirect 91 | github.com/subosito/gotenv v1.6.0 // indirect 92 | github.com/vishvananda/netns v0.0.5 // indirect 93 | github.com/x448/float16 v0.8.4 // indirect 94 | go.mongodb.org/mongo-driver v1.14.0 // indirect 95 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect 96 | go.opentelemetry.io/otel v1.32.0 // indirect 97 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect 98 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect 99 | go.opentelemetry.io/otel/metric v1.32.0 // indirect 100 | go.opentelemetry.io/otel/sdk v1.31.0 // indirect 101 | go.opentelemetry.io/otel/trace v1.32.0 // indirect 102 | go.opentelemetry.io/proto/otlp v1.3.1 // indirect 103 | go.uber.org/multierr v1.11.0 // indirect 104 | go.uber.org/zap v1.27.0 // indirect 105 | go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect 106 | golang.org/x/net v0.33.0 // indirect 107 | golang.org/x/oauth2 v0.24.0 // indirect 108 | golang.org/x/sync v0.10.0 // indirect 109 | golang.org/x/term v0.27.0 // indirect 110 | golang.org/x/text v0.21.0 // indirect 111 | golang.org/x/time v0.8.0 // indirect 112 | golang.org/x/tools v0.28.0 // indirect 113 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 114 | google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect 115 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect 116 | google.golang.org/grpc v1.69.0 // indirect 117 | google.golang.org/protobuf v1.35.2 // indirect 118 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 119 | gopkg.in/inf.v0 v0.9.1 // indirect 120 | gopkg.in/ini.v1 v1.67.0 // indirect 121 | gopkg.in/yaml.v3 v3.0.1 // indirect 122 | k8s.io/api v0.32.0 // indirect 123 | k8s.io/apiextensions-apiserver v0.32.0 // indirect 124 | k8s.io/apiserver v0.32.0 // indirect 125 | k8s.io/component-base v0.32.0 // indirect 126 | k8s.io/klog/v2 v2.130.1 // indirect 127 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 128 | k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect 129 | sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect 130 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 131 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 132 | sigs.k8s.io/yaml v1.4.0 // indirect 133 | ) 134 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: eswitch-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: eswitch- 10 | 11 | # Labels to add to all resources and selectors. 12 | #labels: 13 | #- includeSelectors: true 14 | # pairs: 15 | # someName: someValue 16 | 17 | resources: 18 | - ../crd 19 | - ../rbac 20 | - ../manager 21 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 22 | # crd/kustomization.yaml 23 | #- ../webhook 24 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 25 | #- ../certmanager 26 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 27 | #- ../prometheus 28 | # [METRICS] Expose the controller manager metrics service. 29 | - metrics_service.yaml 30 | # [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy. 31 | # Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics. 32 | # Only CR(s) which requires webhooks and are applied on namespaces labeled with 'webhooks: enabled' will 33 | # be able to communicate with the Webhook Server. 34 | #- ../network-policy 35 | 36 | # Uncomment the patches line if you enable Metrics 37 | patches: 38 | # [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443. 39 | # More info: https://book.kubebuilder.io/reference/metrics 40 | - path: manager_metrics_patch.yaml 41 | target: 42 | kind: Deployment 43 | 44 | # Uncomment the patches line if you enable Metrics and CertManager 45 | # [METRICS-WITH-CERTS] To enable metrics protected with certManager, uncomment the following line. 46 | # This patch will protect the metrics with certManager self-signed certs. 47 | #- path: cert_metrics_manager_patch.yaml 48 | # target: 49 | # kind: Deployment 50 | 51 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 52 | # crd/kustomization.yaml 53 | #- path: manager_webhook_patch.yaml 54 | # target: 55 | # kind: Deployment 56 | 57 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 58 | # Uncomment the following replacements to add the cert-manager CA injection annotations 59 | #replacements: 60 | # - source: # Uncomment the following block to enable certificates for metrics 61 | # kind: Service 62 | # version: v1 63 | # name: controller-manager-metrics-service 64 | # fieldPath: metadata.name 65 | # targets: 66 | # - select: 67 | # kind: Certificate 68 | # group: cert-manager.io 69 | # version: v1 70 | # name: metrics-certs 71 | # fieldPaths: 72 | # - spec.dnsNames.0 73 | # - spec.dnsNames.1 74 | # options: 75 | # delimiter: '.' 76 | # index: 0 77 | # create: true 78 | # 79 | # - source: 80 | # kind: Service 81 | # version: v1 82 | # name: controller-manager-metrics-service 83 | # fieldPath: metadata.namespace 84 | # targets: 85 | # - select: 86 | # kind: Certificate 87 | # group: cert-manager.io 88 | # version: v1 89 | # name: metrics-certs 90 | # fieldPaths: 91 | # - spec.dnsNames.0 92 | # - spec.dnsNames.1 93 | # options: 94 | # delimiter: '.' 95 | # index: 1 96 | # create: true 97 | # 98 | # - source: # Uncomment the following block if you have any webhook 99 | # kind: Service 100 | # version: v1 101 | # name: webhook-service 102 | # fieldPath: .metadata.name # Name of the service 103 | # targets: 104 | # - select: 105 | # kind: Certificate 106 | # group: cert-manager.io 107 | # version: v1 108 | # name: serving-cert 109 | # fieldPaths: 110 | # - .spec.dnsNames.0 111 | # - .spec.dnsNames.1 112 | # options: 113 | # delimiter: '.' 114 | # index: 0 115 | # create: true 116 | # - source: 117 | # kind: Service 118 | # version: v1 119 | # name: webhook-service 120 | # fieldPath: .metadata.namespace # Namespace of the service 121 | # targets: 122 | # - select: 123 | # kind: Certificate 124 | # group: cert-manager.io 125 | # version: v1 126 | # name: serving-cert 127 | # fieldPaths: 128 | # - .spec.dnsNames.0 129 | # - .spec.dnsNames.1 130 | # options: 131 | # delimiter: '.' 132 | # index: 1 133 | # create: true 134 | # 135 | # - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation) 136 | # kind: Certificate 137 | # group: cert-manager.io 138 | # version: v1 139 | # name: serving-cert # This name should match the one in certificate.yaml 140 | # fieldPath: .metadata.namespace # Namespace of the certificate CR 141 | # targets: 142 | # - select: 143 | # kind: ValidatingWebhookConfiguration 144 | # fieldPaths: 145 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 146 | # options: 147 | # delimiter: '/' 148 | # index: 0 149 | # create: true 150 | # - source: 151 | # kind: Certificate 152 | # group: cert-manager.io 153 | # version: v1 154 | # name: serving-cert 155 | # fieldPath: .metadata.name 156 | # targets: 157 | # - select: 158 | # kind: ValidatingWebhookConfiguration 159 | # fieldPaths: 160 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 161 | # options: 162 | # delimiter: '/' 163 | # index: 1 164 | # create: true 165 | # 166 | # - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting ) 167 | # kind: Certificate 168 | # group: cert-manager.io 169 | # version: v1 170 | # name: serving-cert 171 | # fieldPath: .metadata.namespace # Namespace of the certificate CR 172 | # targets: 173 | # - select: 174 | # kind: MutatingWebhookConfiguration 175 | # fieldPaths: 176 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 177 | # options: 178 | # delimiter: '/' 179 | # index: 0 180 | # create: true 181 | # - source: 182 | # kind: Certificate 183 | # group: cert-manager.io 184 | # version: v1 185 | # name: serving-cert 186 | # fieldPath: .metadata.name 187 | # targets: 188 | # - select: 189 | # kind: MutatingWebhookConfiguration 190 | # fieldPaths: 191 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 192 | # options: 193 | # delimiter: '/' 194 | # index: 1 195 | # create: true 196 | # 197 | # - source: # Uncomment the following block if you have a ConversionWebhook (--conversion) 198 | # kind: Certificate 199 | # group: cert-manager.io 200 | # version: v1 201 | # name: serving-cert 202 | # fieldPath: .metadata.namespace # Namespace of the certificate CR 203 | # targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. 204 | # +kubebuilder:scaffold:crdkustomizecainjectionns 205 | # - source: 206 | # kind: Certificate 207 | # group: cert-manager.io 208 | # version: v1 209 | # name: serving-cert 210 | # fieldPath: .metadata.name 211 | # targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. 212 | # +kubebuilder:scaffold:crdkustomizecainjectionname 213 | -------------------------------------------------------------------------------- /pkg/networkswitch/bpf_bpfeb.go: -------------------------------------------------------------------------------- 1 | // Code generated by bpf2go; DO NOT EDIT. 2 | //go:build mips || mips64 || ppc64 || s390x 3 | 4 | package networkswitch 5 | 6 | import ( 7 | "bytes" 8 | _ "embed" 9 | "fmt" 10 | "io" 11 | 12 | "github.com/cilium/ebpf" 13 | ) 14 | 15 | type bpfFdbKey struct { 16 | Mac uint64 17 | Vlan uint16 18 | } 19 | 20 | type bpfFdbVal struct { 21 | IfaceIndex uint16 22 | Ktimestamp uint64 23 | Tagged bool 24 | } 25 | 26 | type bpfPortCfg struct { 27 | IfIndex uint16 28 | Pvid uint16 29 | VlanBitmask [64]uint64 30 | Mac [6]uint8 31 | Transparent uint16 32 | IngressFiltering uint16 33 | HookDrop uint16 34 | HookEgress uint16 35 | Tap uint16 36 | } 37 | 38 | type bpfTrafficKey struct { 39 | SrcIpv4 uint32 40 | DstIpv4 uint32 41 | Vlan uint16 42 | ProtoL2 uint16 43 | ProtoL3 uint16 44 | TargetIfIndex uint16 45 | } 46 | 47 | type bpfTrafficStats struct { 48 | Timestamp uint64 49 | Tagged uint8 50 | Size uint16 51 | RxDroppedBytes uint64 52 | RxDroppedPackets uint64 53 | RxPassedBytes uint64 54 | RxPassedPackets uint64 55 | RxRedirectedBytes uint64 56 | RxRedirectedPackets uint64 57 | } 58 | 59 | type bpfXdpStats struct { 60 | RxDroppedBytes uint64 61 | RxDroppedPackets uint64 62 | RxPassedBytes uint64 63 | RxPassedPackets uint64 64 | RxRedirectedBytes uint64 65 | RxRedirectedPackets uint64 66 | RxLastTimestamp uint64 67 | TxRedirectedBytes uint64 68 | TxRedirectedPackets uint64 69 | TxLastTimestamp uint64 70 | } 71 | 72 | // loadBpf returns the embedded CollectionSpec for bpf. 73 | func loadBpf() (*ebpf.CollectionSpec, error) { 74 | reader := bytes.NewReader(_BpfBytes) 75 | spec, err := ebpf.LoadCollectionSpecFromReader(reader) 76 | if err != nil { 77 | return nil, fmt.Errorf("can't load bpf: %w", err) 78 | } 79 | 80 | return spec, err 81 | } 82 | 83 | // loadBpfObjects loads bpf and converts it into a struct. 84 | // 85 | // The following types are suitable as obj argument: 86 | // 87 | // *bpfObjects 88 | // *bpfPrograms 89 | // *bpfMaps 90 | // 91 | // See ebpf.CollectionSpec.LoadAndAssign documentation for details. 92 | func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error { 93 | spec, err := loadBpf() 94 | if err != nil { 95 | return err 96 | } 97 | 98 | return spec.LoadAndAssign(obj, opts) 99 | } 100 | 101 | // bpfSpecs contains maps and programs before they are loaded into the kernel. 102 | // 103 | // It can be passed ebpf.CollectionSpec.Assign. 104 | type bpfSpecs struct { 105 | bpfProgramSpecs 106 | bpfMapSpecs 107 | bpfVariableSpecs 108 | } 109 | 110 | // bpfProgramSpecs contains programs before they are loaded into the kernel. 111 | // 112 | // It can be passed ebpf.CollectionSpec.Assign. 113 | type bpfProgramSpecs struct { 114 | ProgEgressTc *ebpf.ProgramSpec `ebpf:"Prog_egress_tc"` 115 | ProgTc *ebpf.ProgramSpec `ebpf:"Prog_tc"` 116 | ProgXdp *ebpf.ProgramSpec `ebpf:"Prog_xdp"` 117 | HookDropTc *ebpf.ProgramSpec `ebpf:"hook_drop_tc"` 118 | HookDropXdp *ebpf.ProgramSpec `ebpf:"hook_drop_xdp"` 119 | HookEgressTc *ebpf.ProgramSpec `ebpf:"hook_egress_tc"` 120 | HookEgressXdp *ebpf.ProgramSpec `ebpf:"hook_egress_xdp"` 121 | HookRouteXdp *ebpf.ProgramSpec `ebpf:"hook_route_xdp"` 122 | TailCall1 *ebpf.ProgramSpec `ebpf:"tail_call1"` 123 | TailCall1B *ebpf.ProgramSpec `ebpf:"tail_call1B"` 124 | TailCall2 *ebpf.ProgramSpec `ebpf:"tail_call2"` 125 | TailCall2B *ebpf.ProgramSpec `ebpf:"tail_call2B"` 126 | } 127 | 128 | // bpfMapSpecs contains maps before they are loaded into the kernel. 129 | // 130 | // It can be passed ebpf.CollectionSpec.Assign. 131 | type bpfMapSpecs struct { 132 | MapFdbXdp *ebpf.MapSpec `ebpf:"Map_fdb_xdp"` 133 | MapJumpTableTc *ebpf.MapSpec `ebpf:"Map_jump_table_tc"` 134 | MapJumpTableXdp *ebpf.MapSpec `ebpf:"Map_jump_table_xdp"` 135 | MapStatsTraffic *ebpf.MapSpec `ebpf:"Map_stats_traffic"` 136 | MapStatsXdp *ebpf.MapSpec `ebpf:"Map_stats_xdp"` 137 | } 138 | 139 | // bpfVariableSpecs contains global variables before they are loaded into the kernel. 140 | // 141 | // It can be passed ebpf.CollectionSpec.Assign. 142 | type bpfVariableSpecs struct { 143 | PORTS_CFG_BY_IDX *ebpf.VariableSpec `ebpf:"PORTS_CFG_BY_IDX"` 144 | PORTS_IDX *ebpf.VariableSpec `ebpf:"PORTS_IDX"` 145 | PORT_CFG *ebpf.VariableSpec `ebpf:"PORT_CFG"` 146 | PORT_COUNT *ebpf.VariableSpec `ebpf:"PORT_COUNT"` 147 | STATS_ENABLED *ebpf.VariableSpec `ebpf:"STATS_ENABLED"` 148 | } 149 | 150 | // bpfObjects contains all objects after they have been loaded into the kernel. 151 | // 152 | // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. 153 | type bpfObjects struct { 154 | bpfPrograms 155 | bpfMaps 156 | bpfVariables 157 | } 158 | 159 | func (o *bpfObjects) Close() error { 160 | return _BpfClose( 161 | &o.bpfPrograms, 162 | &o.bpfMaps, 163 | ) 164 | } 165 | 166 | // bpfMaps contains all maps after they have been loaded into the kernel. 167 | // 168 | // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. 169 | type bpfMaps struct { 170 | MapFdbXdp *ebpf.Map `ebpf:"Map_fdb_xdp"` 171 | MapJumpTableTc *ebpf.Map `ebpf:"Map_jump_table_tc"` 172 | MapJumpTableXdp *ebpf.Map `ebpf:"Map_jump_table_xdp"` 173 | MapStatsTraffic *ebpf.Map `ebpf:"Map_stats_traffic"` 174 | MapStatsXdp *ebpf.Map `ebpf:"Map_stats_xdp"` 175 | } 176 | 177 | func (m *bpfMaps) Close() error { 178 | return _BpfClose( 179 | m.MapFdbXdp, 180 | m.MapJumpTableTc, 181 | m.MapJumpTableXdp, 182 | m.MapStatsTraffic, 183 | m.MapStatsXdp, 184 | ) 185 | } 186 | 187 | // bpfVariables contains all global variables after they have been loaded into the kernel. 188 | // 189 | // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. 190 | type bpfVariables struct { 191 | PORTS_CFG_BY_IDX *ebpf.Variable `ebpf:"PORTS_CFG_BY_IDX"` 192 | PORTS_IDX *ebpf.Variable `ebpf:"PORTS_IDX"` 193 | PORT_CFG *ebpf.Variable `ebpf:"PORT_CFG"` 194 | PORT_COUNT *ebpf.Variable `ebpf:"PORT_COUNT"` 195 | STATS_ENABLED *ebpf.Variable `ebpf:"STATS_ENABLED"` 196 | } 197 | 198 | // bpfPrograms contains all programs after they have been loaded into the kernel. 199 | // 200 | // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. 201 | type bpfPrograms struct { 202 | ProgEgressTc *ebpf.Program `ebpf:"Prog_egress_tc"` 203 | ProgTc *ebpf.Program `ebpf:"Prog_tc"` 204 | ProgXdp *ebpf.Program `ebpf:"Prog_xdp"` 205 | HookDropTc *ebpf.Program `ebpf:"hook_drop_tc"` 206 | HookDropXdp *ebpf.Program `ebpf:"hook_drop_xdp"` 207 | HookEgressTc *ebpf.Program `ebpf:"hook_egress_tc"` 208 | HookEgressXdp *ebpf.Program `ebpf:"hook_egress_xdp"` 209 | HookRouteXdp *ebpf.Program `ebpf:"hook_route_xdp"` 210 | TailCall1 *ebpf.Program `ebpf:"tail_call1"` 211 | TailCall1B *ebpf.Program `ebpf:"tail_call1B"` 212 | TailCall2 *ebpf.Program `ebpf:"tail_call2"` 213 | TailCall2B *ebpf.Program `ebpf:"tail_call2B"` 214 | } 215 | 216 | func (p *bpfPrograms) Close() error { 217 | return _BpfClose( 218 | p.ProgEgressTc, 219 | p.ProgTc, 220 | p.ProgXdp, 221 | p.HookDropTc, 222 | p.HookDropXdp, 223 | p.HookEgressTc, 224 | p.HookEgressXdp, 225 | p.HookRouteXdp, 226 | p.TailCall1, 227 | p.TailCall1B, 228 | p.TailCall2, 229 | p.TailCall2B, 230 | ) 231 | } 232 | 233 | func _BpfClose(closers ...io.Closer) error { 234 | for _, closer := range closers { 235 | if err := closer.Close(); err != nil { 236 | return err 237 | } 238 | } 239 | return nil 240 | } 241 | 242 | // Do not access this directly. 243 | // 244 | //go:embed bpf_bpfeb.o 245 | var _BpfBytes []byte 246 | -------------------------------------------------------------------------------- /pkg/networkswitch/bpf_bpfel.go: -------------------------------------------------------------------------------- 1 | // Code generated by bpf2go; DO NOT EDIT. 2 | //go:build 386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 3 | 4 | package networkswitch 5 | 6 | import ( 7 | "bytes" 8 | _ "embed" 9 | "fmt" 10 | "io" 11 | 12 | "github.com/cilium/ebpf" 13 | ) 14 | 15 | type bpfFdbKey struct { 16 | Mac uint64 17 | Vlan uint16 18 | } 19 | 20 | type bpfFdbVal struct { 21 | IfaceIndex uint16 22 | Ktimestamp uint64 23 | Tagged bool 24 | } 25 | 26 | type bpfPortCfg struct { 27 | IfIndex uint16 28 | Pvid uint16 29 | VlanBitmask [64]uint64 30 | Mac [6]uint8 31 | Transparent uint16 32 | IngressFiltering uint16 33 | HookDrop uint16 34 | HookEgress uint16 35 | Tap uint16 36 | } 37 | 38 | type bpfTrafficKey struct { 39 | SrcIpv4 uint32 40 | DstIpv4 uint32 41 | Vlan uint16 42 | ProtoL2 uint16 43 | ProtoL3 uint16 44 | TargetIfIndex uint16 45 | } 46 | 47 | type bpfTrafficStats struct { 48 | Timestamp uint64 49 | Tagged uint8 50 | Size uint16 51 | RxDroppedBytes uint64 52 | RxDroppedPackets uint64 53 | RxPassedBytes uint64 54 | RxPassedPackets uint64 55 | RxRedirectedBytes uint64 56 | RxRedirectedPackets uint64 57 | } 58 | 59 | type bpfXdpStats struct { 60 | RxDroppedBytes uint64 61 | RxDroppedPackets uint64 62 | RxPassedBytes uint64 63 | RxPassedPackets uint64 64 | RxRedirectedBytes uint64 65 | RxRedirectedPackets uint64 66 | RxLastTimestamp uint64 67 | TxRedirectedBytes uint64 68 | TxRedirectedPackets uint64 69 | TxLastTimestamp uint64 70 | } 71 | 72 | // loadBpf returns the embedded CollectionSpec for bpf. 73 | func loadBpf() (*ebpf.CollectionSpec, error) { 74 | reader := bytes.NewReader(_BpfBytes) 75 | spec, err := ebpf.LoadCollectionSpecFromReader(reader) 76 | if err != nil { 77 | return nil, fmt.Errorf("can't load bpf: %w", err) 78 | } 79 | 80 | return spec, err 81 | } 82 | 83 | // loadBpfObjects loads bpf and converts it into a struct. 84 | // 85 | // The following types are suitable as obj argument: 86 | // 87 | // *bpfObjects 88 | // *bpfPrograms 89 | // *bpfMaps 90 | // 91 | // See ebpf.CollectionSpec.LoadAndAssign documentation for details. 92 | func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error { 93 | spec, err := loadBpf() 94 | if err != nil { 95 | return err 96 | } 97 | 98 | return spec.LoadAndAssign(obj, opts) 99 | } 100 | 101 | // bpfSpecs contains maps and programs before they are loaded into the kernel. 102 | // 103 | // It can be passed ebpf.CollectionSpec.Assign. 104 | type bpfSpecs struct { 105 | bpfProgramSpecs 106 | bpfMapSpecs 107 | bpfVariableSpecs 108 | } 109 | 110 | // bpfProgramSpecs contains programs before they are loaded into the kernel. 111 | // 112 | // It can be passed ebpf.CollectionSpec.Assign. 113 | type bpfProgramSpecs struct { 114 | ProgEgressTc *ebpf.ProgramSpec `ebpf:"Prog_egress_tc"` 115 | ProgTc *ebpf.ProgramSpec `ebpf:"Prog_tc"` 116 | ProgXdp *ebpf.ProgramSpec `ebpf:"Prog_xdp"` 117 | HookDropTc *ebpf.ProgramSpec `ebpf:"hook_drop_tc"` 118 | HookDropXdp *ebpf.ProgramSpec `ebpf:"hook_drop_xdp"` 119 | HookEgressTc *ebpf.ProgramSpec `ebpf:"hook_egress_tc"` 120 | HookEgressXdp *ebpf.ProgramSpec `ebpf:"hook_egress_xdp"` 121 | HookRouteXdp *ebpf.ProgramSpec `ebpf:"hook_route_xdp"` 122 | TailCall1 *ebpf.ProgramSpec `ebpf:"tail_call1"` 123 | TailCall1B *ebpf.ProgramSpec `ebpf:"tail_call1B"` 124 | TailCall2 *ebpf.ProgramSpec `ebpf:"tail_call2"` 125 | TailCall2B *ebpf.ProgramSpec `ebpf:"tail_call2B"` 126 | } 127 | 128 | // bpfMapSpecs contains maps before they are loaded into the kernel. 129 | // 130 | // It can be passed ebpf.CollectionSpec.Assign. 131 | type bpfMapSpecs struct { 132 | MapFdbXdp *ebpf.MapSpec `ebpf:"Map_fdb_xdp"` 133 | MapJumpTableTc *ebpf.MapSpec `ebpf:"Map_jump_table_tc"` 134 | MapJumpTableXdp *ebpf.MapSpec `ebpf:"Map_jump_table_xdp"` 135 | MapStatsTraffic *ebpf.MapSpec `ebpf:"Map_stats_traffic"` 136 | MapStatsXdp *ebpf.MapSpec `ebpf:"Map_stats_xdp"` 137 | } 138 | 139 | // bpfVariableSpecs contains global variables before they are loaded into the kernel. 140 | // 141 | // It can be passed ebpf.CollectionSpec.Assign. 142 | type bpfVariableSpecs struct { 143 | PORTS_CFG_BY_IDX *ebpf.VariableSpec `ebpf:"PORTS_CFG_BY_IDX"` 144 | PORTS_IDX *ebpf.VariableSpec `ebpf:"PORTS_IDX"` 145 | PORT_CFG *ebpf.VariableSpec `ebpf:"PORT_CFG"` 146 | PORT_COUNT *ebpf.VariableSpec `ebpf:"PORT_COUNT"` 147 | STATS_ENABLED *ebpf.VariableSpec `ebpf:"STATS_ENABLED"` 148 | } 149 | 150 | // bpfObjects contains all objects after they have been loaded into the kernel. 151 | // 152 | // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. 153 | type bpfObjects struct { 154 | bpfPrograms 155 | bpfMaps 156 | bpfVariables 157 | } 158 | 159 | func (o *bpfObjects) Close() error { 160 | return _BpfClose( 161 | &o.bpfPrograms, 162 | &o.bpfMaps, 163 | ) 164 | } 165 | 166 | // bpfMaps contains all maps after they have been loaded into the kernel. 167 | // 168 | // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. 169 | type bpfMaps struct { 170 | MapFdbXdp *ebpf.Map `ebpf:"Map_fdb_xdp"` 171 | MapJumpTableTc *ebpf.Map `ebpf:"Map_jump_table_tc"` 172 | MapJumpTableXdp *ebpf.Map `ebpf:"Map_jump_table_xdp"` 173 | MapStatsTraffic *ebpf.Map `ebpf:"Map_stats_traffic"` 174 | MapStatsXdp *ebpf.Map `ebpf:"Map_stats_xdp"` 175 | } 176 | 177 | func (m *bpfMaps) Close() error { 178 | return _BpfClose( 179 | m.MapFdbXdp, 180 | m.MapJumpTableTc, 181 | m.MapJumpTableXdp, 182 | m.MapStatsTraffic, 183 | m.MapStatsXdp, 184 | ) 185 | } 186 | 187 | // bpfVariables contains all global variables after they have been loaded into the kernel. 188 | // 189 | // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. 190 | type bpfVariables struct { 191 | PORTS_CFG_BY_IDX *ebpf.Variable `ebpf:"PORTS_CFG_BY_IDX"` 192 | PORTS_IDX *ebpf.Variable `ebpf:"PORTS_IDX"` 193 | PORT_CFG *ebpf.Variable `ebpf:"PORT_CFG"` 194 | PORT_COUNT *ebpf.Variable `ebpf:"PORT_COUNT"` 195 | STATS_ENABLED *ebpf.Variable `ebpf:"STATS_ENABLED"` 196 | } 197 | 198 | // bpfPrograms contains all programs after they have been loaded into the kernel. 199 | // 200 | // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. 201 | type bpfPrograms struct { 202 | ProgEgressTc *ebpf.Program `ebpf:"Prog_egress_tc"` 203 | ProgTc *ebpf.Program `ebpf:"Prog_tc"` 204 | ProgXdp *ebpf.Program `ebpf:"Prog_xdp"` 205 | HookDropTc *ebpf.Program `ebpf:"hook_drop_tc"` 206 | HookDropXdp *ebpf.Program `ebpf:"hook_drop_xdp"` 207 | HookEgressTc *ebpf.Program `ebpf:"hook_egress_tc"` 208 | HookEgressXdp *ebpf.Program `ebpf:"hook_egress_xdp"` 209 | HookRouteXdp *ebpf.Program `ebpf:"hook_route_xdp"` 210 | TailCall1 *ebpf.Program `ebpf:"tail_call1"` 211 | TailCall1B *ebpf.Program `ebpf:"tail_call1B"` 212 | TailCall2 *ebpf.Program `ebpf:"tail_call2"` 213 | TailCall2B *ebpf.Program `ebpf:"tail_call2B"` 214 | } 215 | 216 | func (p *bpfPrograms) Close() error { 217 | return _BpfClose( 218 | p.ProgEgressTc, 219 | p.ProgTc, 220 | p.ProgXdp, 221 | p.HookDropTc, 222 | p.HookDropXdp, 223 | p.HookEgressTc, 224 | p.HookEgressXdp, 225 | p.HookRouteXdp, 226 | p.TailCall1, 227 | p.TailCall1B, 228 | p.TailCall2, 229 | p.TailCall2B, 230 | ) 231 | } 232 | 233 | func _BpfClose(closers ...io.Closer) error { 234 | for _, closer := range closers { 235 | if err := closer.Close(); err != nil { 236 | return err 237 | } 238 | } 239 | return nil 240 | } 241 | 242 | // Do not access this directly. 243 | // 244 | //go:embed bpf_bpfel.o 245 | var _BpfBytes []byte 246 | -------------------------------------------------------------------------------- /test/utils/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package utils 18 | 19 | import ( 20 | "bufio" 21 | "bytes" 22 | "fmt" 23 | "os" 24 | "os/exec" 25 | "strings" 26 | 27 | . "github.com/onsi/ginkgo/v2" //nolint:golint,revive 28 | ) 29 | 30 | const ( 31 | prometheusOperatorVersion = "v0.77.1" 32 | prometheusOperatorURL = "https://github.com/prometheus-operator/prometheus-operator/" + 33 | "releases/download/%s/bundle.yaml" 34 | 35 | certmanagerVersion = "v1.16.3" 36 | certmanagerURLTmpl = "https://github.com/cert-manager/cert-manager/releases/download/%s/cert-manager.yaml" 37 | ) 38 | 39 | func warnError(err error) { 40 | _, _ = fmt.Fprintf(GinkgoWriter, "warning: %v\n", err) 41 | } 42 | 43 | // Run executes the provided command within this context 44 | func Run(cmd *exec.Cmd) (string, error) { 45 | dir, _ := GetProjectDir() 46 | cmd.Dir = dir 47 | 48 | if err := os.Chdir(cmd.Dir); err != nil { 49 | _, _ = fmt.Fprintf(GinkgoWriter, "chdir dir: %s\n", err) 50 | } 51 | 52 | cmd.Env = append(os.Environ(), "GO111MODULE=on") 53 | command := strings.Join(cmd.Args, " ") 54 | _, _ = fmt.Fprintf(GinkgoWriter, "running: %s\n", command) 55 | output, err := cmd.CombinedOutput() 56 | if err != nil { 57 | return string(output), fmt.Errorf("%s failed with error: (%v) %s", command, err, string(output)) 58 | } 59 | 60 | return string(output), nil 61 | } 62 | 63 | // InstallPrometheusOperator installs the prometheus Operator to be used to export the enabled metrics. 64 | func InstallPrometheusOperator() error { 65 | url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) 66 | cmd := exec.Command("kubectl", "create", "-f", url) 67 | _, err := Run(cmd) 68 | return err 69 | } 70 | 71 | // UninstallPrometheusOperator uninstalls the prometheus 72 | func UninstallPrometheusOperator() { 73 | url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) 74 | cmd := exec.Command("kubectl", "delete", "-f", url) 75 | if _, err := Run(cmd); err != nil { 76 | warnError(err) 77 | } 78 | } 79 | 80 | // IsPrometheusCRDsInstalled checks if any Prometheus CRDs are installed 81 | // by verifying the existence of key CRDs related to Prometheus. 82 | func IsPrometheusCRDsInstalled() bool { 83 | // List of common Prometheus CRDs 84 | prometheusCRDs := []string{ 85 | "prometheuses.monitoring.coreos.com", 86 | "prometheusrules.monitoring.coreos.com", 87 | "prometheusagents.monitoring.coreos.com", 88 | } 89 | 90 | cmd := exec.Command("kubectl", "get", "crds", "-o", "custom-columns=NAME:.metadata.name") 91 | output, err := Run(cmd) 92 | if err != nil { 93 | return false 94 | } 95 | crdList := GetNonEmptyLines(output) 96 | for _, crd := range prometheusCRDs { 97 | for _, line := range crdList { 98 | if strings.Contains(line, crd) { 99 | return true 100 | } 101 | } 102 | } 103 | 104 | return false 105 | } 106 | 107 | // UninstallCertManager uninstalls the cert manager 108 | func UninstallCertManager() { 109 | url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) 110 | cmd := exec.Command("kubectl", "delete", "-f", url) 111 | if _, err := Run(cmd); err != nil { 112 | warnError(err) 113 | } 114 | } 115 | 116 | // InstallCertManager installs the cert manager bundle. 117 | func InstallCertManager() error { 118 | url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) 119 | cmd := exec.Command("kubectl", "apply", "-f", url) 120 | if _, err := Run(cmd); err != nil { 121 | return err 122 | } 123 | // Wait for cert-manager-webhook to be ready, which can take time if cert-manager 124 | // was re-installed after uninstalling on a cluster. 125 | cmd = exec.Command("kubectl", "wait", "deployment.apps/cert-manager-webhook", 126 | "--for", "condition=Available", 127 | "--namespace", "cert-manager", 128 | "--timeout", "5m", 129 | ) 130 | 131 | _, err := Run(cmd) 132 | return err 133 | } 134 | 135 | // IsCertManagerCRDsInstalled checks if any Cert Manager CRDs are installed 136 | // by verifying the existence of key CRDs related to Cert Manager. 137 | func IsCertManagerCRDsInstalled() bool { 138 | // List of common Cert Manager CRDs 139 | certManagerCRDs := []string{ 140 | "certificates.cert-manager.io", 141 | "issuers.cert-manager.io", 142 | "clusterissuers.cert-manager.io", 143 | "certificaterequests.cert-manager.io", 144 | "orders.acme.cert-manager.io", 145 | "challenges.acme.cert-manager.io", 146 | } 147 | 148 | // Execute the kubectl command to get all CRDs 149 | cmd := exec.Command("kubectl", "get", "crds") 150 | output, err := Run(cmd) 151 | if err != nil { 152 | return false 153 | } 154 | 155 | // Check if any of the Cert Manager CRDs are present 156 | crdList := GetNonEmptyLines(output) 157 | for _, crd := range certManagerCRDs { 158 | for _, line := range crdList { 159 | if strings.Contains(line, crd) { 160 | return true 161 | } 162 | } 163 | } 164 | 165 | return false 166 | } 167 | 168 | // LoadImageToKindClusterWithName loads a local docker image to the kind cluster 169 | func LoadImageToKindClusterWithName(name string) error { 170 | cluster := "kind" 171 | if v, ok := os.LookupEnv("KIND_CLUSTER"); ok { 172 | cluster = v 173 | } 174 | kindOptions := []string{"load", "docker-image", name, "--name", cluster} 175 | cmd := exec.Command("kind", kindOptions...) 176 | _, err := Run(cmd) 177 | return err 178 | } 179 | 180 | // GetNonEmptyLines converts given command output string into individual objects 181 | // according to line breakers, and ignores the empty elements in it. 182 | func GetNonEmptyLines(output string) []string { 183 | var res []string 184 | elements := strings.Split(output, "\n") 185 | for _, element := range elements { 186 | if element != "" { 187 | res = append(res, element) 188 | } 189 | } 190 | 191 | return res 192 | } 193 | 194 | // GetProjectDir will return the directory where the project is 195 | func GetProjectDir() (string, error) { 196 | wd, err := os.Getwd() 197 | if err != nil { 198 | return wd, err 199 | } 200 | wd = strings.Replace(wd, "/test/e2e", "", -1) 201 | return wd, nil 202 | } 203 | 204 | // UncommentCode searches for target in the file and remove the comment prefix 205 | // of the target content. The target content may span multiple lines. 206 | func UncommentCode(filename, target, prefix string) error { 207 | // false positive 208 | // nolint:gosec 209 | content, err := os.ReadFile(filename) 210 | if err != nil { 211 | return err 212 | } 213 | strContent := string(content) 214 | 215 | idx := strings.Index(strContent, target) 216 | if idx < 0 { 217 | return fmt.Errorf("unable to find the code %s to be uncomment", target) 218 | } 219 | 220 | out := new(bytes.Buffer) 221 | _, err = out.Write(content[:idx]) 222 | if err != nil { 223 | return err 224 | } 225 | 226 | scanner := bufio.NewScanner(bytes.NewBufferString(target)) 227 | if !scanner.Scan() { 228 | return nil 229 | } 230 | for { 231 | _, err := out.WriteString(strings.TrimPrefix(scanner.Text(), prefix)) 232 | if err != nil { 233 | return err 234 | } 235 | // Avoid writing a newline in case the previous line was the last in target. 236 | if !scanner.Scan() { 237 | break 238 | } 239 | if _, err := out.WriteString("\n"); err != nil { 240 | return err 241 | } 242 | } 243 | 244 | _, err = out.Write(content[idx+len(target):]) 245 | if err != nil { 246 | return err 247 | } 248 | // false positive 249 | // nolint:gosec 250 | return os.WriteFile(filename, out.Bytes(), 0644) 251 | } 252 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "crypto/tls" 21 | "flag" 22 | "os" 23 | "path/filepath" 24 | 25 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 26 | // to ensure that exec-entrypoint and run can make use of them. 27 | _ "k8s.io/client-go/plugin/pkg/client/auth" 28 | 29 | "k8s.io/apimachinery/pkg/runtime" 30 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 31 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 32 | ctrl "sigs.k8s.io/controller-runtime" 33 | "sigs.k8s.io/controller-runtime/pkg/certwatcher" 34 | "sigs.k8s.io/controller-runtime/pkg/healthz" 35 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 36 | "sigs.k8s.io/controller-runtime/pkg/metrics/filters" 37 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 38 | "sigs.k8s.io/controller-runtime/pkg/webhook" 39 | 40 | eswitchconfigv1 "github.com/kube-vm-project/eSwitch/api/v1" 41 | "github.com/kube-vm-project/eSwitch/internal/controller" 42 | // +kubebuilder:scaffold:imports 43 | ) 44 | 45 | var ( 46 | scheme = runtime.NewScheme() 47 | setupLog = ctrl.Log.WithName("setup") 48 | ) 49 | 50 | func init() { 51 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 52 | 53 | utilruntime.Must(eswitchconfigv1.AddToScheme(scheme)) 54 | // +kubebuilder:scaffold:scheme 55 | } 56 | 57 | // nolint:gocyclo 58 | func main() { 59 | var metricsAddr string 60 | var metricsCertPath, metricsCertName, metricsCertKey string 61 | var webhookCertPath, webhookCertName, webhookCertKey string 62 | var enableLeaderElection bool 63 | var probeAddr string 64 | var secureMetrics bool 65 | var enableHTTP2 bool 66 | var tlsOpts []func(*tls.Config) 67 | flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ 68 | "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") 69 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 70 | flag.BoolVar(&enableLeaderElection, "leader-elect", false, 71 | "Enable leader election for controller manager. "+ 72 | "Enabling this will ensure there is only one active controller manager.") 73 | flag.BoolVar(&secureMetrics, "metrics-secure", true, 74 | "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") 75 | flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.") 76 | flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.") 77 | flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.") 78 | flag.StringVar(&metricsCertPath, "metrics-cert-path", "", 79 | "The directory that contains the metrics server certificate.") 80 | flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.") 81 | flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.") 82 | flag.BoolVar(&enableHTTP2, "enable-http2", false, 83 | "If set, HTTP/2 will be enabled for the metrics and webhook servers") 84 | opts := zap.Options{ 85 | Development: true, 86 | } 87 | opts.BindFlags(flag.CommandLine) 88 | flag.Parse() 89 | 90 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 91 | 92 | // if the enable-http2 flag is false (the default), http/2 should be disabled 93 | // due to its vulnerabilities. More specifically, disabling http/2 will 94 | // prevent from being vulnerable to the HTTP/2 Stream Cancellation and 95 | // Rapid Reset CVEs. For more information see: 96 | // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 97 | // - https://github.com/advisories/GHSA-4374-p667-p6c8 98 | disableHTTP2 := func(c *tls.Config) { 99 | setupLog.Info("disabling http/2") 100 | c.NextProtos = []string{"http/1.1"} 101 | } 102 | 103 | if !enableHTTP2 { 104 | tlsOpts = append(tlsOpts, disableHTTP2) 105 | } 106 | 107 | // Create watchers for metrics and webhooks certificates 108 | var metricsCertWatcher, webhookCertWatcher *certwatcher.CertWatcher 109 | 110 | // Initial webhook TLS options 111 | webhookTLSOpts := tlsOpts 112 | 113 | if len(webhookCertPath) > 0 { 114 | setupLog.Info("Initializing webhook certificate watcher using provided certificates", 115 | "webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey) 116 | 117 | var err error 118 | webhookCertWatcher, err = certwatcher.New( 119 | filepath.Join(webhookCertPath, webhookCertName), 120 | filepath.Join(webhookCertPath, webhookCertKey), 121 | ) 122 | if err != nil { 123 | setupLog.Error(err, "Failed to initialize webhook certificate watcher") 124 | os.Exit(1) 125 | } 126 | 127 | webhookTLSOpts = append(webhookTLSOpts, func(config *tls.Config) { 128 | config.GetCertificate = webhookCertWatcher.GetCertificate 129 | }) 130 | } 131 | 132 | webhookServer := webhook.NewServer(webhook.Options{ 133 | TLSOpts: webhookTLSOpts, 134 | }) 135 | 136 | // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. 137 | // More info: 138 | // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.0/pkg/metrics/server 139 | // - https://book.kubebuilder.io/reference/metrics.html 140 | metricsServerOptions := metricsserver.Options{ 141 | BindAddress: metricsAddr, 142 | SecureServing: secureMetrics, 143 | TLSOpts: tlsOpts, 144 | } 145 | 146 | if secureMetrics { 147 | // FilterProvider is used to protect the metrics endpoint with authn/authz. 148 | // These configurations ensure that only authorized users and service accounts 149 | // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: 150 | // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.0/pkg/metrics/filters#WithAuthenticationAndAuthorization 151 | metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization 152 | } 153 | 154 | // If the certificate is not specified, controller-runtime will automatically 155 | // generate self-signed certificates for the metrics server. While convenient for development and testing, 156 | // this setup is not recommended for production. 157 | // 158 | // TODO(user): If you enable certManager, uncomment the following lines: 159 | // - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates 160 | // managed by cert-manager for the metrics server. 161 | // - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification. 162 | if len(metricsCertPath) > 0 { 163 | setupLog.Info("Initializing metrics certificate watcher using provided certificates", 164 | "metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey) 165 | 166 | var err error 167 | metricsCertWatcher, err = certwatcher.New( 168 | filepath.Join(metricsCertPath, metricsCertName), 169 | filepath.Join(metricsCertPath, metricsCertKey), 170 | ) 171 | if err != nil { 172 | setupLog.Error(err, "to initialize metrics certificate watcher", "error", err) 173 | os.Exit(1) 174 | } 175 | 176 | metricsServerOptions.TLSOpts = append(metricsServerOptions.TLSOpts, func(config *tls.Config) { 177 | config.GetCertificate = metricsCertWatcher.GetCertificate 178 | }) 179 | } 180 | 181 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 182 | Scheme: scheme, 183 | Metrics: metricsServerOptions, 184 | WebhookServer: webhookServer, 185 | HealthProbeBindAddress: probeAddr, 186 | LeaderElection: enableLeaderElection, 187 | LeaderElectionID: "6b640de7.kube-vm.io", 188 | // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily 189 | // when the Manager ends. This requires the binary to immediately end when the 190 | // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly 191 | // speeds up voluntary leader transitions as the new leader don't have to wait 192 | // LeaseDuration time first. 193 | // 194 | // In the default scaffold provided, the program ends immediately after 195 | // the manager stops, so would be fine to enable this option. However, 196 | // if you are doing or is intended to do any operation such as perform cleanups 197 | // after the manager stops then its usage might be unsafe. 198 | // LeaderElectionReleaseOnCancel: true, 199 | }) 200 | if err != nil { 201 | setupLog.Error(err, "unable to start manager") 202 | os.Exit(1) 203 | } 204 | 205 | if err = (&controller.EswitchReconciler{ 206 | Client: mgr.GetClient(), 207 | Scheme: mgr.GetScheme(), 208 | }).SetupWithManager(mgr); err != nil { 209 | setupLog.Error(err, "unable to create controller", "controller", "Eswitch") 210 | os.Exit(1) 211 | } 212 | // +kubebuilder:scaffold:builder 213 | 214 | if metricsCertWatcher != nil { 215 | setupLog.Info("Adding metrics certificate watcher to manager") 216 | if err := mgr.Add(metricsCertWatcher); err != nil { 217 | setupLog.Error(err, "unable to add metrics certificate watcher to manager") 218 | os.Exit(1) 219 | } 220 | } 221 | 222 | if webhookCertWatcher != nil { 223 | setupLog.Info("Adding webhook certificate watcher to manager") 224 | if err := mgr.Add(webhookCertWatcher); err != nil { 225 | setupLog.Error(err, "unable to add webhook certificate watcher to manager") 226 | os.Exit(1) 227 | } 228 | } 229 | 230 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 231 | setupLog.Error(err, "unable to set up health check") 232 | os.Exit(1) 233 | } 234 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 235 | setupLog.Error(err, "unable to set up ready check") 236 | os.Exit(1) 237 | } 238 | 239 | setupLog.Info("starting manager") 240 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 241 | setupLog.Error(err, "problem running manager") 242 | os.Exit(1) 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Image URL to use all building/pushing image targets 2 | IMG ?= controller:latest 3 | 4 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 5 | ifeq (,$(shell go env GOBIN)) 6 | GOBIN=$(shell go env GOPATH)/bin 7 | else 8 | GOBIN=$(shell go env GOBIN) 9 | endif 10 | 11 | # CONTAINER_TOOL defines the container tool to be used for building images. 12 | # Be aware that the target commands are only tested with Docker which is 13 | # scaffolded by default. However, you might want to replace it to use other 14 | # tools. (i.e. podman) 15 | CONTAINER_TOOL ?= docker 16 | 17 | # Setting SHELL to bash allows bash commands to be executed by recipes. 18 | # Options are set to exit when a recipe line exits non-zero or a piped command fails. 19 | SHELL = /usr/bin/env bash -o pipefail 20 | .SHELLFLAGS = -ec 21 | 22 | .PHONY: all 23 | all: build 24 | 25 | ##@ General 26 | 27 | # The help target prints out all targets with their descriptions organized 28 | # beneath their categories. The categories are represented by '##@' and the 29 | # target descriptions by '##'. The awk command is responsible for reading the 30 | # entire set of makefiles included in this invocation, looking for lines of the 31 | # file as xyz: ## something, and then pretty-format the target and help. Then, 32 | # if there's a line with ##@ something, that gets pretty-printed as a category. 33 | # More info on the usage of ANSI control characters for terminal formatting: 34 | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 35 | # More info on the awk command: 36 | # http://linuxcommand.org/lc3_adv_awk.php 37 | 38 | .PHONY: help 39 | help: ## Display this help. 40 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 41 | 42 | ##@ Development 43 | 44 | .PHONY: manifests 45 | manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. 46 | $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases 47 | 48 | .PHONY: generate 49 | generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. 50 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." 51 | 52 | .PHONY: fmt 53 | fmt: ## Run go fmt against code. 54 | go fmt ./... 55 | 56 | .PHONY: vet 57 | vet: ## Run go vet against code. 58 | go vet ./... 59 | 60 | .PHONY: test 61 | test: manifests generate fmt vet setup-envtest ## Run tests. 62 | KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out 63 | 64 | # TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'. 65 | # The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally. 66 | # Prometheus and CertManager are installed by default; skip with: 67 | # - PROMETHEUS_INSTALL_SKIP=true 68 | # - CERT_MANAGER_INSTALL_SKIP=true 69 | .PHONY: test-e2e 70 | test-e2e: manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind. 71 | @command -v kind >/dev/null 2>&1 || { \ 72 | echo "Kind is not installed. Please install Kind manually."; \ 73 | exit 1; \ 74 | } 75 | @kind get clusters | grep -q 'kind' || { \ 76 | echo "No Kind cluster is running. Please start a Kind cluster before running the e2e tests."; \ 77 | exit 1; \ 78 | } 79 | go test ./test/e2e/ -v -ginkgo.v 80 | 81 | .PHONY: lint 82 | lint: golangci-lint ## Run golangci-lint linter 83 | $(GOLANGCI_LINT) run 84 | 85 | .PHONY: lint-fix 86 | lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes 87 | $(GOLANGCI_LINT) run --fix 88 | 89 | .PHONY: lint-config 90 | lint-config: golangci-lint ## Verify golangci-lint linter configuration 91 | $(GOLANGCI_LINT) config verify 92 | 93 | ##@ Build 94 | 95 | .PHONY: build 96 | build: manifests generate fmt vet ## Build manager binary. 97 | go build -o bin/manager cmd/main.go 98 | 99 | .PHONY: run 100 | run: manifests generate fmt vet ## Run a controller from your host. 101 | go run ./cmd/main.go 102 | 103 | # If you wish to build the manager image targeting other platforms you can use the --platform flag. 104 | # (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. 105 | # More info: https://docs.docker.com/develop/develop-images/build_enhancements/ 106 | .PHONY: docker-build 107 | docker-build: ## Build docker image with the manager. 108 | $(CONTAINER_TOOL) build -t ${IMG} . 109 | 110 | .PHONY: docker-push 111 | docker-push: ## Push docker image with the manager. 112 | $(CONTAINER_TOOL) push ${IMG} 113 | 114 | # PLATFORMS defines the target platforms for the manager image be built to provide support to multiple 115 | # architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: 116 | # - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/ 117 | # - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/ 118 | # - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=> then the export will fail) 119 | # To adequately provide solutions that are compatible with multiple platforms, you should consider using this option. 120 | PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le 121 | .PHONY: docker-buildx 122 | docker-buildx: ## Build and push docker image for the manager for cross-platform support 123 | # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile 124 | sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross 125 | - $(CONTAINER_TOOL) buildx create --name eswitch-builder 126 | $(CONTAINER_TOOL) buildx use eswitch-builder 127 | - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . 128 | - $(CONTAINER_TOOL) buildx rm eswitch-builder 129 | rm Dockerfile.cross 130 | 131 | .PHONY: build-installer 132 | build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment. 133 | mkdir -p dist 134 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 135 | $(KUSTOMIZE) build config/default > dist/install.yaml 136 | 137 | ##@ Deployment 138 | 139 | ifndef ignore-not-found 140 | ignore-not-found = false 141 | endif 142 | 143 | .PHONY: install 144 | install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. 145 | $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - 146 | 147 | .PHONY: uninstall 148 | uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. 149 | $(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - 150 | 151 | .PHONY: deploy 152 | deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. 153 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 154 | $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - 155 | 156 | .PHONY: undeploy 157 | undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. 158 | $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - 159 | 160 | ##@ Dependencies 161 | 162 | ## Location to install dependencies to 163 | LOCALBIN ?= $(shell pwd)/bin 164 | $(LOCALBIN): 165 | mkdir -p $(LOCALBIN) 166 | 167 | ## Tool Binaries 168 | KUBECTL ?= kubectl 169 | KUSTOMIZE ?= $(LOCALBIN)/kustomize 170 | CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen 171 | ENVTEST ?= $(LOCALBIN)/setup-envtest 172 | GOLANGCI_LINT = $(LOCALBIN)/golangci-lint 173 | 174 | ## Tool Versions 175 | KUSTOMIZE_VERSION ?= v5.5.0 176 | CONTROLLER_TOOLS_VERSION ?= v0.17.1 177 | #ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20) 178 | ENVTEST_VERSION ?= $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime | awk -F'[v.]' '{printf "release-%d.%d", $$2, $$3}') 179 | #ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31) 180 | ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $$3}') 181 | GOLANGCI_LINT_VERSION ?= v1.63.4 182 | 183 | .PHONY: kustomize 184 | kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. 185 | $(KUSTOMIZE): $(LOCALBIN) 186 | $(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION)) 187 | 188 | .PHONY: controller-gen 189 | controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. 190 | $(CONTROLLER_GEN): $(LOCALBIN) 191 | $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION)) 192 | 193 | .PHONY: setup-envtest 194 | setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory. 195 | @echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..." 196 | @$(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path || { \ 197 | echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \ 198 | exit 1; \ 199 | } 200 | 201 | .PHONY: envtest 202 | envtest: $(ENVTEST) ## Download setup-envtest locally if necessary. 203 | $(ENVTEST): $(LOCALBIN) 204 | $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION)) 205 | 206 | .PHONY: golangci-lint 207 | golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. 208 | $(GOLANGCI_LINT): $(LOCALBIN) 209 | $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) 210 | 211 | # go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist 212 | # $1 - target path with name of binary 213 | # $2 - package url which can be installed 214 | # $3 - specific version of package 215 | define go-install-tool 216 | @[ -f "$(1)-$(3)" ] || { \ 217 | set -e; \ 218 | package=$(2)@$(3) ;\ 219 | echo "Downloading $${package}" ;\ 220 | rm -f $(1) || true ;\ 221 | GOBIN=$(LOCALBIN) go install $${package} ;\ 222 | mv $(1) $(1)-$(3) ;\ 223 | } ;\ 224 | ln -sf $(1)-$(3) $(1) 225 | endef 226 | -------------------------------------------------------------------------------- /dist/install.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | app.kubernetes.io/managed-by: kustomize 6 | app.kubernetes.io/name: eswitch 7 | control-plane: controller-manager 8 | name: eswitch-system 9 | --- 10 | apiVersion: apiextensions.k8s.io/v1 11 | kind: CustomResourceDefinition 12 | metadata: 13 | annotations: 14 | controller-gen.kubebuilder.io/version: v0.17.1 15 | name: eswitches.eswitchconfig.kube-vm.io 16 | spec: 17 | group: eswitchconfig.kube-vm.io 18 | names: 19 | kind: Eswitch 20 | listKind: EswitchList 21 | plural: eswitches 22 | singular: eswitch 23 | scope: Namespaced 24 | versions: 25 | - name: v1 26 | schema: 27 | openAPIV3Schema: 28 | description: Eswitch is the Schema for the eswitches API. 29 | properties: 30 | apiVersion: 31 | description: |- 32 | APIVersion defines the versioned schema of this representation of an object. 33 | Servers should convert recognized schemas to the latest internal value, and 34 | may reject unrecognized values. 35 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 36 | type: string 37 | kind: 38 | description: |- 39 | Kind is a string value representing the REST resource this object represents. 40 | Servers may infer this from the endpoint the client submits requests to. 41 | Cannot be updated. 42 | In CamelCase. 43 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 44 | type: string 45 | metadata: 46 | type: object 47 | spec: 48 | description: EswitchSpec defines the desired state of Eswitch. 49 | properties: 50 | ignoreMAC: 51 | description: MAC addresses that are immediately ignored by the switch 52 | items: 53 | type: string 54 | type: array 55 | ports: 56 | description: Ports contains the configuration for each port being 57 | added to the switch 58 | items: 59 | properties: 60 | interface: 61 | description: Physical interface name being added to the switch 62 | type: string 63 | pvid: 64 | description: Port VLANID to be used on the port itself 65 | type: integer 66 | vlans: 67 | description: Tagged VLAN traffic to be allowed on the port 68 | items: 69 | type: integer 70 | type: array 71 | xdpmode: 72 | description: The type of XDP mode being used for the interface 73 | type: string 74 | required: 75 | - interface 76 | - xdpmode 77 | type: object 78 | type: array 79 | required: 80 | - ports 81 | type: object 82 | status: 83 | description: EswitchStatus defines the observed state of Eswitch. 84 | properties: 85 | configured: 86 | type: boolean 87 | errors: 88 | type: boolean 89 | required: 90 | - configured 91 | - errors 92 | type: object 93 | type: object 94 | served: true 95 | storage: true 96 | subresources: 97 | status: {} 98 | --- 99 | apiVersion: v1 100 | kind: ServiceAccount 101 | metadata: 102 | labels: 103 | app.kubernetes.io/managed-by: kustomize 104 | app.kubernetes.io/name: eswitch 105 | name: eswitch-controller-manager 106 | namespace: eswitch-system 107 | --- 108 | apiVersion: rbac.authorization.k8s.io/v1 109 | kind: Role 110 | metadata: 111 | labels: 112 | app.kubernetes.io/managed-by: kustomize 113 | app.kubernetes.io/name: eswitch 114 | name: eswitch-leader-election-role 115 | namespace: eswitch-system 116 | rules: 117 | - apiGroups: 118 | - "" 119 | resources: 120 | - configmaps 121 | verbs: 122 | - get 123 | - list 124 | - watch 125 | - create 126 | - update 127 | - patch 128 | - delete 129 | - apiGroups: 130 | - coordination.k8s.io 131 | resources: 132 | - leases 133 | verbs: 134 | - get 135 | - list 136 | - watch 137 | - create 138 | - update 139 | - patch 140 | - delete 141 | - apiGroups: 142 | - "" 143 | resources: 144 | - events 145 | verbs: 146 | - create 147 | - patch 148 | --- 149 | apiVersion: rbac.authorization.k8s.io/v1 150 | kind: ClusterRole 151 | metadata: 152 | labels: 153 | app.kubernetes.io/managed-by: kustomize 154 | app.kubernetes.io/name: eswitch 155 | name: eswitch-eswitch-admin-role 156 | rules: 157 | - apiGroups: 158 | - eswitchconfig.kube-vm.io 159 | resources: 160 | - eswitches 161 | verbs: 162 | - '*' 163 | - apiGroups: 164 | - eswitchconfig.kube-vm.io 165 | resources: 166 | - eswitches/status 167 | verbs: 168 | - get 169 | --- 170 | apiVersion: rbac.authorization.k8s.io/v1 171 | kind: ClusterRole 172 | metadata: 173 | labels: 174 | app.kubernetes.io/managed-by: kustomize 175 | app.kubernetes.io/name: eswitch 176 | name: eswitch-eswitch-editor-role 177 | rules: 178 | - apiGroups: 179 | - eswitchconfig.kube-vm.io 180 | resources: 181 | - eswitches 182 | verbs: 183 | - create 184 | - delete 185 | - get 186 | - list 187 | - patch 188 | - update 189 | - watch 190 | - apiGroups: 191 | - eswitchconfig.kube-vm.io 192 | resources: 193 | - eswitches/status 194 | verbs: 195 | - get 196 | --- 197 | apiVersion: rbac.authorization.k8s.io/v1 198 | kind: ClusterRole 199 | metadata: 200 | labels: 201 | app.kubernetes.io/managed-by: kustomize 202 | app.kubernetes.io/name: eswitch 203 | name: eswitch-eswitch-viewer-role 204 | rules: 205 | - apiGroups: 206 | - eswitchconfig.kube-vm.io 207 | resources: 208 | - eswitches 209 | verbs: 210 | - get 211 | - list 212 | - watch 213 | - apiGroups: 214 | - eswitchconfig.kube-vm.io 215 | resources: 216 | - eswitches/status 217 | verbs: 218 | - get 219 | --- 220 | apiVersion: rbac.authorization.k8s.io/v1 221 | kind: ClusterRole 222 | metadata: 223 | name: eswitch-manager-role 224 | rules: 225 | - apiGroups: 226 | - eswitchconfig.kube-vm.io 227 | resources: 228 | - eswitches 229 | verbs: 230 | - create 231 | - delete 232 | - get 233 | - list 234 | - patch 235 | - update 236 | - watch 237 | - apiGroups: 238 | - eswitchconfig.kube-vm.io 239 | resources: 240 | - eswitches/finalizers 241 | verbs: 242 | - update 243 | - apiGroups: 244 | - eswitchconfig.kube-vm.io 245 | resources: 246 | - eswitches/status 247 | verbs: 248 | - get 249 | - patch 250 | - update 251 | --- 252 | apiVersion: rbac.authorization.k8s.io/v1 253 | kind: ClusterRole 254 | metadata: 255 | name: eswitch-metrics-auth-role 256 | rules: 257 | - apiGroups: 258 | - authentication.k8s.io 259 | resources: 260 | - tokenreviews 261 | verbs: 262 | - create 263 | - apiGroups: 264 | - authorization.k8s.io 265 | resources: 266 | - subjectaccessreviews 267 | verbs: 268 | - create 269 | --- 270 | apiVersion: rbac.authorization.k8s.io/v1 271 | kind: ClusterRole 272 | metadata: 273 | name: eswitch-metrics-reader 274 | rules: 275 | - nonResourceURLs: 276 | - /metrics 277 | verbs: 278 | - get 279 | --- 280 | apiVersion: rbac.authorization.k8s.io/v1 281 | kind: RoleBinding 282 | metadata: 283 | labels: 284 | app.kubernetes.io/managed-by: kustomize 285 | app.kubernetes.io/name: eswitch 286 | name: eswitch-leader-election-rolebinding 287 | namespace: eswitch-system 288 | roleRef: 289 | apiGroup: rbac.authorization.k8s.io 290 | kind: Role 291 | name: eswitch-leader-election-role 292 | subjects: 293 | - kind: ServiceAccount 294 | name: eswitch-controller-manager 295 | namespace: eswitch-system 296 | --- 297 | apiVersion: rbac.authorization.k8s.io/v1 298 | kind: ClusterRoleBinding 299 | metadata: 300 | labels: 301 | app.kubernetes.io/managed-by: kustomize 302 | app.kubernetes.io/name: eswitch 303 | name: eswitch-manager-rolebinding 304 | roleRef: 305 | apiGroup: rbac.authorization.k8s.io 306 | kind: ClusterRole 307 | name: eswitch-manager-role 308 | subjects: 309 | - kind: ServiceAccount 310 | name: eswitch-controller-manager 311 | namespace: eswitch-system 312 | --- 313 | apiVersion: rbac.authorization.k8s.io/v1 314 | kind: ClusterRoleBinding 315 | metadata: 316 | name: eswitch-metrics-auth-rolebinding 317 | roleRef: 318 | apiGroup: rbac.authorization.k8s.io 319 | kind: ClusterRole 320 | name: eswitch-metrics-auth-role 321 | subjects: 322 | - kind: ServiceAccount 323 | name: eswitch-controller-manager 324 | namespace: eswitch-system 325 | --- 326 | apiVersion: v1 327 | kind: Service 328 | metadata: 329 | labels: 330 | app.kubernetes.io/managed-by: kustomize 331 | app.kubernetes.io/name: eswitch 332 | control-plane: controller-manager 333 | name: eswitch-controller-manager-metrics-service 334 | namespace: eswitch-system 335 | spec: 336 | ports: 337 | - name: https 338 | port: 8443 339 | protocol: TCP 340 | targetPort: 8443 341 | selector: 342 | app.kubernetes.io/name: eswitch 343 | control-plane: controller-manager 344 | --- 345 | apiVersion: apps/v1 346 | kind: Deployment 347 | metadata: 348 | labels: 349 | app.kubernetes.io/managed-by: kustomize 350 | app.kubernetes.io/name: eswitch 351 | control-plane: controller-manager 352 | name: eswitch-controller-manager 353 | namespace: eswitch-system 354 | spec: 355 | replicas: 1 356 | selector: 357 | matchLabels: 358 | app.kubernetes.io/name: eswitch 359 | control-plane: controller-manager 360 | template: 361 | metadata: 362 | annotations: 363 | kubectl.kubernetes.io/default-container: manager 364 | labels: 365 | app.kubernetes.io/name: eswitch 366 | control-plane: controller-manager 367 | spec: 368 | containers: 369 | - args: 370 | - --metrics-bind-address=:8443 371 | - --leader-elect 372 | - --health-probe-bind-address=:8081 373 | command: 374 | - /manager 375 | image: controller:latest 376 | livenessProbe: 377 | httpGet: 378 | path: /healthz 379 | port: 8081 380 | initialDelaySeconds: 15 381 | periodSeconds: 20 382 | name: manager 383 | ports: [] 384 | readinessProbe: 385 | httpGet: 386 | path: /readyz 387 | port: 8081 388 | initialDelaySeconds: 5 389 | periodSeconds: 10 390 | resources: 391 | limits: 392 | cpu: 500m 393 | memory: 128Mi 394 | requests: 395 | cpu: 10m 396 | memory: 64Mi 397 | securityContext: 398 | privileged: true 399 | volumeMounts: 400 | - mountPath: /sys/fs/bpf 401 | name: bpf-maps 402 | securityContext: null 403 | serviceAccountName: eswitch-controller-manager 404 | terminationGracePeriodSeconds: 10 405 | volumes: 406 | - hostPath: 407 | path: /sys/fs/bpf 408 | type: DirectoryOrCreate 409 | name: bpf-maps 410 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /test/e2e/e2e_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package e2e 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "os" 23 | "os/exec" 24 | "path/filepath" 25 | "time" 26 | 27 | . "github.com/onsi/ginkgo/v2" 28 | . "github.com/onsi/gomega" 29 | 30 | "github.com/kube-vm-project/eSwitch/test/utils" 31 | ) 32 | 33 | // namespace where the project is deployed in 34 | const namespace = "eswitch-system" 35 | 36 | // serviceAccountName created for the project 37 | const serviceAccountName = "eswitch-controller-manager" 38 | 39 | // metricsServiceName is the name of the metrics service of the project 40 | const metricsServiceName = "eswitch-controller-manager-metrics-service" 41 | 42 | // metricsRoleBindingName is the name of the RBAC that will be created to allow get the metrics data 43 | const metricsRoleBindingName = "eswitch-metrics-binding" 44 | 45 | var _ = Describe("Manager", Ordered, func() { 46 | var controllerPodName string 47 | 48 | // Before running the tests, set up the environment by creating the namespace, 49 | // enforce the restricted security policy to the namespace, installing CRDs, 50 | // and deploying the controller. 51 | BeforeAll(func() { 52 | By("creating manager namespace") 53 | cmd := exec.Command("kubectl", "create", "ns", namespace) 54 | _, err := utils.Run(cmd) 55 | Expect(err).NotTo(HaveOccurred(), "Failed to create namespace") 56 | 57 | By("labeling the namespace to enforce the restricted security policy") 58 | cmd = exec.Command("kubectl", "label", "--overwrite", "ns", namespace, 59 | "pod-security.kubernetes.io/enforce=restricted") 60 | _, err = utils.Run(cmd) 61 | Expect(err).NotTo(HaveOccurred(), "Failed to label namespace with restricted policy") 62 | 63 | By("installing CRDs") 64 | cmd = exec.Command("make", "install") 65 | _, err = utils.Run(cmd) 66 | Expect(err).NotTo(HaveOccurred(), "Failed to install CRDs") 67 | 68 | By("deploying the controller-manager") 69 | cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", projectImage)) 70 | _, err = utils.Run(cmd) 71 | Expect(err).NotTo(HaveOccurred(), "Failed to deploy the controller-manager") 72 | }) 73 | 74 | // After all tests have been executed, clean up by undeploying the controller, uninstalling CRDs, 75 | // and deleting the namespace. 76 | AfterAll(func() { 77 | By("cleaning up the curl pod for metrics") 78 | cmd := exec.Command("kubectl", "delete", "pod", "curl-metrics", "-n", namespace) 79 | _, _ = utils.Run(cmd) 80 | 81 | By("undeploying the controller-manager") 82 | cmd = exec.Command("make", "undeploy") 83 | _, _ = utils.Run(cmd) 84 | 85 | By("uninstalling CRDs") 86 | cmd = exec.Command("make", "uninstall") 87 | _, _ = utils.Run(cmd) 88 | 89 | By("removing manager namespace") 90 | cmd = exec.Command("kubectl", "delete", "ns", namespace) 91 | _, _ = utils.Run(cmd) 92 | }) 93 | 94 | // After each test, check for failures and collect logs, events, 95 | // and pod descriptions for debugging. 96 | AfterEach(func() { 97 | specReport := CurrentSpecReport() 98 | if specReport.Failed() { 99 | By("Fetching controller manager pod logs") 100 | cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace) 101 | controllerLogs, err := utils.Run(cmd) 102 | if err == nil { 103 | _, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs) 104 | } else { 105 | _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Controller logs: %s", err) 106 | } 107 | 108 | By("Fetching Kubernetes events") 109 | cmd = exec.Command("kubectl", "get", "events", "-n", namespace, "--sort-by=.lastTimestamp") 110 | eventsOutput, err := utils.Run(cmd) 111 | if err == nil { 112 | _, _ = fmt.Fprintf(GinkgoWriter, "Kubernetes events:\n%s", eventsOutput) 113 | } else { 114 | _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Kubernetes events: %s", err) 115 | } 116 | 117 | By("Fetching curl-metrics logs") 118 | cmd = exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace) 119 | metricsOutput, err := utils.Run(cmd) 120 | if err == nil { 121 | _, _ = fmt.Fprintf(GinkgoWriter, "Metrics logs:\n %s", metricsOutput) 122 | } else { 123 | _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get curl-metrics logs: %s", err) 124 | } 125 | 126 | By("Fetching controller manager pod description") 127 | cmd = exec.Command("kubectl", "describe", "pod", controllerPodName, "-n", namespace) 128 | podDescription, err := utils.Run(cmd) 129 | if err == nil { 130 | fmt.Println("Pod description:\n", podDescription) 131 | } else { 132 | fmt.Println("Failed to describe controller pod") 133 | } 134 | } 135 | }) 136 | 137 | SetDefaultEventuallyTimeout(2 * time.Minute) 138 | SetDefaultEventuallyPollingInterval(time.Second) 139 | 140 | Context("Manager", func() { 141 | It("should run successfully", func() { 142 | By("validating that the controller-manager pod is running as expected") 143 | verifyControllerUp := func(g Gomega) { 144 | // Get the name of the controller-manager pod 145 | cmd := exec.Command("kubectl", "get", 146 | "pods", "-l", "control-plane=controller-manager", 147 | "-o", "go-template={{ range .items }}"+ 148 | "{{ if not .metadata.deletionTimestamp }}"+ 149 | "{{ .metadata.name }}"+ 150 | "{{ \"\\n\" }}{{ end }}{{ end }}", 151 | "-n", namespace, 152 | ) 153 | 154 | podOutput, err := utils.Run(cmd) 155 | g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve controller-manager pod information") 156 | podNames := utils.GetNonEmptyLines(podOutput) 157 | g.Expect(podNames).To(HaveLen(1), "expected 1 controller pod running") 158 | controllerPodName = podNames[0] 159 | g.Expect(controllerPodName).To(ContainSubstring("controller-manager")) 160 | 161 | // Validate the pod's status 162 | cmd = exec.Command("kubectl", "get", 163 | "pods", controllerPodName, "-o", "jsonpath={.status.phase}", 164 | "-n", namespace, 165 | ) 166 | output, err := utils.Run(cmd) 167 | g.Expect(err).NotTo(HaveOccurred()) 168 | g.Expect(output).To(Equal("Running"), "Incorrect controller-manager pod status") 169 | } 170 | Eventually(verifyControllerUp).Should(Succeed()) 171 | }) 172 | 173 | It("should ensure the metrics endpoint is serving metrics", func() { 174 | By("creating a ClusterRoleBinding for the service account to allow access to metrics") 175 | cmd := exec.Command("kubectl", "create", "clusterrolebinding", metricsRoleBindingName, 176 | "--clusterrole=eswitch-metrics-reader", 177 | fmt.Sprintf("--serviceaccount=%s:%s", namespace, serviceAccountName), 178 | ) 179 | _, err := utils.Run(cmd) 180 | Expect(err).NotTo(HaveOccurred(), "Failed to create ClusterRoleBinding") 181 | 182 | By("validating that the metrics service is available") 183 | cmd = exec.Command("kubectl", "get", "service", metricsServiceName, "-n", namespace) 184 | _, err = utils.Run(cmd) 185 | Expect(err).NotTo(HaveOccurred(), "Metrics service should exist") 186 | 187 | By("validating that the ServiceMonitor for Prometheus is applied in the namespace") 188 | cmd = exec.Command("kubectl", "get", "ServiceMonitor", "-n", namespace) 189 | _, err = utils.Run(cmd) 190 | Expect(err).NotTo(HaveOccurred(), "ServiceMonitor should exist") 191 | 192 | By("getting the service account token") 193 | token, err := serviceAccountToken() 194 | Expect(err).NotTo(HaveOccurred()) 195 | Expect(token).NotTo(BeEmpty()) 196 | 197 | By("waiting for the metrics endpoint to be ready") 198 | verifyMetricsEndpointReady := func(g Gomega) { 199 | cmd := exec.Command("kubectl", "get", "endpoints", metricsServiceName, "-n", namespace) 200 | output, err := utils.Run(cmd) 201 | g.Expect(err).NotTo(HaveOccurred()) 202 | g.Expect(output).To(ContainSubstring("8443"), "Metrics endpoint is not ready") 203 | } 204 | Eventually(verifyMetricsEndpointReady).Should(Succeed()) 205 | 206 | By("verifying that the controller manager is serving the metrics server") 207 | verifyMetricsServerStarted := func(g Gomega) { 208 | cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace) 209 | output, err := utils.Run(cmd) 210 | g.Expect(err).NotTo(HaveOccurred()) 211 | g.Expect(output).To(ContainSubstring("controller-runtime.metrics\tServing metrics server"), 212 | "Metrics server not yet started") 213 | } 214 | Eventually(verifyMetricsServerStarted).Should(Succeed()) 215 | 216 | By("creating the curl-metrics pod to access the metrics endpoint") 217 | cmd = exec.Command("kubectl", "run", "curl-metrics", "--restart=Never", 218 | "--namespace", namespace, 219 | "--image=curlimages/curl:latest", 220 | "--overrides", 221 | fmt.Sprintf(`{ 222 | "spec": { 223 | "containers": [{ 224 | "name": "curl", 225 | "image": "curlimages/curl:latest", 226 | "command": ["/bin/sh", "-c"], 227 | "args": ["curl -v -k -H 'Authorization: Bearer %s' https://%s.%s.svc.cluster.local:8443/metrics"], 228 | "securityContext": { 229 | "allowPrivilegeEscalation": false, 230 | "capabilities": { 231 | "drop": ["ALL"] 232 | }, 233 | "runAsNonRoot": true, 234 | "runAsUser": 1000, 235 | "seccompProfile": { 236 | "type": "RuntimeDefault" 237 | } 238 | } 239 | }], 240 | "serviceAccount": "%s" 241 | } 242 | }`, token, metricsServiceName, namespace, serviceAccountName)) 243 | _, err = utils.Run(cmd) 244 | Expect(err).NotTo(HaveOccurred(), "Failed to create curl-metrics pod") 245 | 246 | By("waiting for the curl-metrics pod to complete.") 247 | verifyCurlUp := func(g Gomega) { 248 | cmd := exec.Command("kubectl", "get", "pods", "curl-metrics", 249 | "-o", "jsonpath={.status.phase}", 250 | "-n", namespace) 251 | output, err := utils.Run(cmd) 252 | g.Expect(err).NotTo(HaveOccurred()) 253 | g.Expect(output).To(Equal("Succeeded"), "curl pod in wrong status") 254 | } 255 | Eventually(verifyCurlUp, 5*time.Minute).Should(Succeed()) 256 | 257 | By("getting the metrics by checking curl-metrics logs") 258 | metricsOutput := getMetricsOutput() 259 | Expect(metricsOutput).To(ContainSubstring( 260 | "controller_runtime_reconcile_total", 261 | )) 262 | }) 263 | 264 | // +kubebuilder:scaffold:e2e-webhooks-checks 265 | 266 | // TODO: Customize the e2e test suite with scenarios specific to your project. 267 | // Consider applying sample/CR(s) and check their status and/or verifying 268 | // the reconciliation by using the metrics, i.e.: 269 | // metricsOutput := getMetricsOutput() 270 | // Expect(metricsOutput).To(ContainSubstring( 271 | // fmt.Sprintf(`controller_runtime_reconcile_total{controller="%s",result="success"} 1`, 272 | // strings.ToLower(), 273 | // )) 274 | }) 275 | }) 276 | 277 | // serviceAccountToken returns a token for the specified service account in the given namespace. 278 | // It uses the Kubernetes TokenRequest API to generate a token by directly sending a request 279 | // and parsing the resulting token from the API response. 280 | func serviceAccountToken() (string, error) { 281 | const tokenRequestRawString = `{ 282 | "apiVersion": "authentication.k8s.io/v1", 283 | "kind": "TokenRequest" 284 | }` 285 | 286 | // Temporary file to store the token request 287 | secretName := fmt.Sprintf("%s-token-request", serviceAccountName) 288 | tokenRequestFile := filepath.Join("/tmp", secretName) 289 | err := os.WriteFile(tokenRequestFile, []byte(tokenRequestRawString), os.FileMode(0o644)) 290 | if err != nil { 291 | return "", err 292 | } 293 | 294 | var out string 295 | verifyTokenCreation := func(g Gomega) { 296 | // Execute kubectl command to create the token 297 | cmd := exec.Command("kubectl", "create", "--raw", fmt.Sprintf( 298 | "/api/v1/namespaces/%s/serviceaccounts/%s/token", 299 | namespace, 300 | serviceAccountName, 301 | ), "-f", tokenRequestFile) 302 | 303 | output, err := cmd.CombinedOutput() 304 | g.Expect(err).NotTo(HaveOccurred()) 305 | 306 | // Parse the JSON output to extract the token 307 | var token tokenRequest 308 | err = json.Unmarshal(output, &token) 309 | g.Expect(err).NotTo(HaveOccurred()) 310 | 311 | out = token.Status.Token 312 | } 313 | Eventually(verifyTokenCreation).Should(Succeed()) 314 | 315 | return out, err 316 | } 317 | 318 | // getMetricsOutput retrieves and returns the logs from the curl pod used to access the metrics endpoint. 319 | func getMetricsOutput() string { 320 | By("getting the curl-metrics logs") 321 | cmd := exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace) 322 | metricsOutput, err := utils.Run(cmd) 323 | Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod") 324 | Expect(metricsOutput).To(ContainSubstring("< HTTP/1.1 200 OK")) 325 | return metricsOutput 326 | } 327 | 328 | // tokenRequest is a simplified representation of the Kubernetes TokenRequest API response, 329 | // containing only the token field that we need to extract. 330 | type tokenRequest struct { 331 | Status struct { 332 | Token string `json:"token"` 333 | } `json:"status"` 334 | } 335 | -------------------------------------------------------------------------------- /pkg/networkswitch/stats.go: -------------------------------------------------------------------------------- 1 | package networkswitch 2 | 3 | import ( 4 | "container/list" 5 | "encoding/binary" 6 | "fmt" 7 | "runtime" 8 | "sync" 9 | "time" 10 | 11 | lru "github.com/hashicorp/golang-lru/v2" 12 | ) 13 | 14 | type StatsXDP struct { 15 | // mutex sync.RWMutex 16 | RxDroppedBytes uint64 `json:"RxDroppedBytes" yaml:"RxDroppedBytes"` 17 | RxDroppedPackets uint64 `json:"RxDroppedPackets" yaml:"RxDroppedPackets"` 18 | RxPassedBytes uint64 `json:"RxPassedBytes" yaml:"RxPassedBytes"` 19 | RxPassedPackets uint64 `json:"RxPassedPackets" yaml:"RxPassedPackets"` 20 | RxRedirectedBytes uint64 `json:"RxRedirectedBytes" yaml:"RxRedirectedBytes"` 21 | RxRedirectedPackets uint64 `json:"RxRedirectedPackets" yaml:"RxRedirectedPackets"` 22 | RxLastTimestamp uint64 `json:"RxLastTimestamp" yaml:"RxLastTimestamp"` 23 | 24 | TxRedirectedBytes uint64 `json:"TxRedirectedBytes" yaml:"TxRedirectedBytes"` 25 | TxRedirectedPackets uint64 `json:"TxRedirectedPackets" yaml:"TxRedirectedPackets"` 26 | TxLastTimestamp uint64 `json:"TxLastTimestamp" yaml:"TxLastTimestamp"` 27 | } 28 | 29 | type StatsTrafficKey struct { 30 | SrcIPv4 uint32 `json:"SrcIPv4" yaml:"SrcIPv4"` 31 | DstIPv4 uint32 `json:"DstIPv4" yaml:"DstIPv4"` 32 | Vlan uint16 `json:"Vlan" yaml:"Vlan"` 33 | ProtoL2 uint16 `json:"ProtoL2" yaml:"ProtoL2"` 34 | ProtoL3 uint16 `json:"ProtoL3" yaml:"ProtoL3"` 35 | TargetIfIndex uint16 `json:"TargetIfIndex" yaml:"TargetIfIndex"` 36 | } 37 | 38 | type LatestPacketInfo struct { 39 | Timestamp uint64 `json:"Timestamp" yaml:"Timestamp"` 40 | Tagged uint8 `json:"Tagged" yaml:"Tagged"` 41 | Size uint16 `json:"Size" yaml:"Size"` 42 | } 43 | 44 | func NewStatsTraffic() *StatsTraffic { 45 | return &StatsTraffic{ 46 | mutex: sync.RWMutex{}, 47 | LatestPacket: LatestPacketInfo{ 48 | Timestamp: 0, 49 | Tagged: 0, 50 | Size: 0, 51 | }, 52 | 53 | RxDroppedBytes: 0, 54 | RxDroppedPackets: 0, 55 | RxPassedBytes: 0, 56 | RxPassedPackets: 0, 57 | RxRedirectedBytes: 0, 58 | RxRedirectedPackets: 0, 59 | } 60 | } 61 | 62 | func UnmarshallStatsTraffic(trafficBytes []byte) StatsTraffic { 63 | return StatsTraffic{ 64 | mutex: sync.RWMutex{}, 65 | LatestPacket: LatestPacketInfo{ 66 | Timestamp: binary.LittleEndian.Uint64(trafficBytes[:8]), 67 | Tagged: trafficBytes[8], 68 | Size: binary.BigEndian.Uint16(trafficBytes[9:11]), 69 | }, 70 | 71 | RxDroppedBytes: binary.LittleEndian.Uint64(trafficBytes[11:19]), 72 | RxDroppedPackets: binary.LittleEndian.Uint64(trafficBytes[19:27]), 73 | RxPassedBytes: binary.LittleEndian.Uint64(trafficBytes[27:35]), 74 | RxPassedPackets: binary.LittleEndian.Uint64(trafficBytes[35:43]), 75 | RxRedirectedBytes: binary.LittleEndian.Uint64(trafficBytes[43:51]), 76 | RxRedirectedPackets: binary.LittleEndian.Uint64(trafficBytes[51:59]), 77 | } 78 | } 79 | 80 | func (stats *StatsTraffic) Add(statsToAdd *StatsTraffic) { 81 | stats.RxDroppedBytes += statsToAdd.RxDroppedBytes 82 | stats.RxDroppedPackets += statsToAdd.RxDroppedPackets 83 | stats.RxPassedBytes += statsToAdd.RxPassedBytes 84 | stats.RxPassedPackets += statsToAdd.RxPassedPackets 85 | stats.RxRedirectedBytes += statsToAdd.RxRedirectedBytes 86 | stats.RxRedirectedPackets += statsToAdd.RxRedirectedPackets 87 | if statsToAdd.LatestPacket.Timestamp > stats.LatestPacket.Timestamp { 88 | stats.LatestPacket = statsToAdd.LatestPacket 89 | } 90 | } 91 | 92 | func (stats *StatsTraffic) Sub(statsToSubtract *StatsTraffic) *StatsTraffic { 93 | ret := StatsTraffic{ 94 | LatestPacket: stats.LatestPacket, 95 | RxDroppedBytes: stats.RxDroppedBytes - statsToSubtract.RxDroppedBytes, 96 | RxDroppedPackets: stats.RxDroppedPackets - statsToSubtract.RxDroppedPackets, 97 | RxPassedBytes: stats.RxPassedBytes - statsToSubtract.RxPassedBytes, 98 | RxPassedPackets: stats.RxPassedPackets - statsToSubtract.RxPassedPackets, 99 | RxRedirectedBytes: stats.RxRedirectedBytes - statsToSubtract.RxRedirectedBytes, 100 | RxRedirectedPackets: stats.RxRedirectedPackets - statsToSubtract.RxRedirectedPackets, 101 | } 102 | return &ret 103 | } 104 | 105 | type StatsTraffic struct { 106 | mutex sync.RWMutex 107 | LatestPacket LatestPacketInfo `json:"LatestPacket" yaml:"LatestPacket"` 108 | 109 | RxDroppedBytes uint64 `json:"RxDroppedBytes" yaml:"RxDroppedBytes"` 110 | RxDroppedPackets uint64 `json:"RxDroppedPackets" yaml:"RxDroppedPackets"` 111 | RxPassedBytes uint64 `json:"RxPassedBytes" yaml:"RxPassedBytes"` 112 | RxPassedPackets uint64 `json:"RxPassedPackets" yaml:"RxPassedPackets"` 113 | RxRedirectedBytes uint64 `json:"RxRedirectedBytes" yaml:"RxRedirectedBytes"` 114 | RxRedirectedPackets uint64 `json:"RxRedirectedPackets" yaml:"RxRedirectedPackets"` 115 | } 116 | 117 | type TrafficObserver struct { 118 | mutex sync.RWMutex 119 | trafficTotal *StatsTraffic 120 | trafficByCore *lru.Cache[int, *StatsTraffic] 121 | trafficByKey *lru.Cache[StatsTrafficKey, *StatsTraffic] 122 | trafficBySrcIPv4 *lru.Cache[uint32, *StatsTraffic] 123 | trafficByDstIPv4 *lru.Cache[uint32, *StatsTraffic] 124 | trafficByVLAN *lru.Cache[uint16, *StatsTraffic] 125 | trafficByProtoL2 *lru.Cache[uint16, *StatsTraffic] 126 | trafficByProtoL3 *lru.Cache[uint16, *StatsTraffic] 127 | trafficByDstIface *lru.Cache[uint16, *StatsTraffic] 128 | } 129 | 130 | type portStats struct { 131 | mutex sync.RWMutex 132 | PortName string `json:"PortName" yaml:"PortName"` 133 | XdpStats StatsXDP `json:"XdpStats" yaml:"XdpStats"` 134 | XdpStatsPerCore []StatsXDP `json:"XdpStatsPerCore" yaml:"XdpStatsPerCore"` 135 | xdpStatsHistory list.List 136 | UpTimestamp time.Time `json:"UpTimestamp" yaml:"UpTimestamp"` 137 | UpdatedAt time.Time `json:"UpdatedAt" yaml:"UpdatedAt"` 138 | RxRate uint64 `json:"RxRate" yaml:"RxRate"` //rate in bytes per second 139 | RxPackets uint64 `json:"RxPackets" yaml:"RxPackets"` 140 | RxBytes uint64 `json:"RxBytes" yaml:"RxBytes"` 141 | TxRate uint64 `json:"TxRate" yaml:"TxRate"` //rate in bytes per second 142 | TxPackets uint64 `json:"TxPackets" yaml:"TxPackets"` 143 | TxBytes uint64 `json:"TxBytes" yaml:"TxBytes"` 144 | 145 | PortTraffic StatsTraffic 146 | PortTrafficByCore map[uint32]StatsTraffic 147 | PortTrafficByKey map[StatsTrafficKey]StatsTraffic 148 | PortTrafficBySrcIPv4 map[uint32]StatsTraffic 149 | PortTrafficByDstIPv4 map[uint32]StatsTraffic 150 | PortTrafficByVLAN map[uint16]StatsTraffic 151 | PortTrafficByProtoL2 map[uint16]StatsTraffic 152 | PortTrafficByProtoL3 map[uint16]StatsTraffic 153 | PortTrafficByDstIface map[uint16]StatsTraffic 154 | PortTrafficMutex sync.RWMutex 155 | } 156 | 157 | func (bridge *BridgeGroup) getStats() []*portStats { 158 | var list []*portStats 159 | for _, port := range bridge.IfList { 160 | list = append(list, &port.Stats) 161 | } 162 | return list 163 | } 164 | 165 | type IPRecord struct { 166 | IP string `json:"ip" yaml:"ip" maxminddb:"ip"` 167 | RDNS string `json:"rdns" yaml:"rdns" maxminddb:"rdns"` 168 | Continent string `json:"continent" yaml:"continent" maxminddb:"continent"` 169 | Country string `json:"country" yaml:"country" maxminddb:"country"` 170 | City string `json:"city" yaml:"city" maxminddb:"city"` 171 | Lat float32 `json:"lat" yaml:"lat" maxminddb:"lat"` 172 | Lng float32 `json:"lng" yaml:"lng" maxminddb:"lng"` 173 | Traffic StatsTraffic `json:"traffic,omitempty" yaml:"traffic,omitempty"` 174 | PrevHop *IPRecord `json:"prevHop,omitempty" yaml:"prevHop,omitempty"` 175 | } 176 | 177 | func (port *SwitchPort) refreshStats(countLocal, traceroute bool) { 178 | 179 | iter := port.eBPF.MapStatsTraffic.Iterate() 180 | var keyBytes [TRAFFIC_KEY_SIZE]byte 181 | seen := make(map[[TRAFFIC_KEY_SIZE]byte]struct{}) 182 | cpuVals := make([][]byte, runtime.NumCPU()) 183 | trafficTotalPort := NewStatsTraffic() 184 | for iter.Next(&keyBytes, &cpuVals) { // grab array of per-cpu stats for this key on this port 185 | _, exists := seen[keyBytes] 186 | if exists { 187 | continue // skip if already seen 188 | } 189 | seen[keyBytes] = struct{}{} 190 | 191 | key := StatsTrafficKey{ 192 | SrcIPv4: binary.BigEndian.Uint32(keyBytes[:4]), 193 | DstIPv4: binary.BigEndian.Uint32(keyBytes[4:8]), 194 | Vlan: binary.LittleEndian.Uint16(keyBytes[8:10]), 195 | ProtoL2: binary.LittleEndian.Uint16(keyBytes[10:12]), 196 | ProtoL3: binary.LittleEndian.Uint16(keyBytes[12:14]), 197 | TargetIfIndex: binary.LittleEndian.Uint16(keyBytes[14:16]), 198 | } 199 | 200 | srcIp := int2ip(key.SrcIPv4) 201 | dstIp := int2ip(key.DstIPv4) 202 | 203 | if srcIp.IsUnspecified() || dstIp.IsUnspecified() { 204 | continue 205 | } 206 | 207 | if !countLocal && srcIp.IsPrivate() { 208 | continue 209 | } 210 | trafficTotalKey := NewStatsTraffic() 211 | 212 | newTrafficOnKey := false 213 | for cpuIdx, cpuValBytes := range cpuVals { // iterate array of per-cpu stats for this key on this port 214 | timestamp := binary.LittleEndian.Uint64(cpuValBytes[:8]) 215 | if timestamp == 0 { 216 | continue 217 | } 218 | 219 | cpuTraffic := UnmarshallStatsTraffic(cpuValBytes) 220 | trafficTotalKey.Add(&cpuTraffic) 221 | 222 | prevByCore, prevByCoreExists := port.Traffic.trafficByCore.Peek(cpuIdx) 223 | if !prevByCoreExists || timestamp > prevByCore.LatestPacket.Timestamp { 224 | port.Traffic.trafficByCore.Add(cpuIdx, &cpuTraffic) 225 | newTrafficOnKey = true 226 | } 227 | 228 | } 229 | trafficTotalPort.Add(trafficTotalKey) 230 | 231 | if newTrafficOnKey { 232 | var trafficDiff *StatsTraffic 233 | prev, exists, _ := port.Traffic.trafficByKey.PeekOrAdd(key, trafficTotalKey) 234 | if !exists { 235 | trafficStats := NewStatsTraffic() 236 | prev = trafficStats 237 | trafficDiff = trafficTotalKey 238 | } else { 239 | port.Traffic.trafficByKey.Add(key, trafficTotalKey) 240 | trafficDiff = trafficTotalKey.Sub(prev) 241 | } 242 | 243 | if key.TargetIfIndex != uint16(port.iface.Index) { 244 | 245 | prev, exists, _ = port.Traffic.trafficByDstIface.PeekOrAdd(key.TargetIfIndex, trafficDiff) 246 | if exists { 247 | prev.Add(trafficDiff) 248 | port.Traffic.trafficByDstIface.Add(key.TargetIfIndex, prev) 249 | } 250 | } 251 | 252 | prev, exists, _ = port.Traffic.trafficBySrcIPv4.PeekOrAdd(key.SrcIPv4, trafficDiff) 253 | if exists { 254 | prev.Add(trafficDiff) 255 | port.Traffic.trafficBySrcIPv4.Add(key.SrcIPv4, prev) //todo - remove this and add mutex to StatsTraffic? 256 | } 257 | 258 | prev, exists, _ = port.Traffic.trafficByDstIPv4.PeekOrAdd(key.DstIPv4, trafficDiff) 259 | if exists { 260 | prev.Add(trafficDiff) 261 | port.Traffic.trafficByDstIPv4.Add(key.DstIPv4, prev) 262 | } 263 | 264 | prev, exists, _ = port.Traffic.trafficByVLAN.PeekOrAdd(key.Vlan, trafficDiff) 265 | if exists { 266 | prev.Add(trafficDiff) 267 | port.Traffic.trafficByVLAN.Add(key.Vlan, prev) 268 | } 269 | 270 | prev, exists, _ = port.Traffic.trafficByProtoL2.PeekOrAdd(key.ProtoL2, trafficDiff) 271 | if exists { 272 | prev.Add(trafficDiff) 273 | port.Traffic.trafficByProtoL2.Add(key.ProtoL2, prev) 274 | } 275 | 276 | prev, exists, _ = port.Traffic.trafficByProtoL3.PeekOrAdd(key.ProtoL3, trafficDiff) 277 | if exists { 278 | prev.Add(trafficDiff) 279 | port.Traffic.trafficByProtoL3.Add(key.ProtoL3, prev) 280 | } 281 | } 282 | } 283 | 284 | port.Traffic.mutex.Lock() 285 | port.Traffic.trafficTotal = trafficTotalPort 286 | port.Traffic.mutex.Unlock() 287 | return 288 | } 289 | 290 | func (bridge *BridgeGroup) refreshStats() { 291 | var wg sync.WaitGroup 292 | for _, port := range bridge.IfList { 293 | wg.Add(1) 294 | go func(port *SwitchPort) { 295 | defer wg.Done() 296 | 297 | portTraffic := new(StatsTraffic) 298 | portTraffic.LatestPacket.Timestamp = 1 299 | 300 | iter := port.eBPF.MapStatsTraffic.Iterate() 301 | 302 | var keyBytes [TRAFFIC_KEY_SIZE]byte 303 | 304 | seen := make(map[[TRAFFIC_KEY_SIZE]byte]struct{}) 305 | 306 | cpuVals := make([][]byte, runtime.NumCPU()) 307 | 308 | for iter.Next(&keyBytes, &cpuVals) { //cpus array 309 | 310 | _, exists := seen[keyBytes] 311 | if exists { 312 | continue 313 | } 314 | seen[keyBytes] = struct{}{} 315 | 316 | key := StatsTrafficKey{ 317 | SrcIPv4: binary.BigEndian.Uint32(keyBytes[:4]), 318 | DstIPv4: binary.BigEndian.Uint32(keyBytes[4:8]), 319 | Vlan: binary.LittleEndian.Uint16(keyBytes[8:10]), 320 | ProtoL2: binary.LittleEndian.Uint16(keyBytes[10:12]), 321 | ProtoL3: binary.LittleEndian.Uint16(keyBytes[12:14]), 322 | TargetIfIndex: binary.LittleEndian.Uint16(keyBytes[14:16]), 323 | } 324 | srcIP := int2ip(key.SrcIPv4) 325 | fmt.Println(srcIP.String(), key.Vlan) 326 | 327 | for cpuIdx, cpuValBytes := range cpuVals { //per cpu val 328 | _ = cpuIdx 329 | 330 | Timestamp := binary.LittleEndian.Uint64(cpuValBytes[:8]) 331 | if Timestamp > portTraffic.LatestPacket.Timestamp { 332 | portTraffic = &StatsTraffic{ 333 | mutex: sync.RWMutex{}, 334 | LatestPacket: LatestPacketInfo{ 335 | Timestamp: Timestamp, 336 | Tagged: cpuValBytes[8], 337 | Size: binary.BigEndian.Uint16(cpuValBytes[9:11]), 338 | }, 339 | 340 | RxDroppedBytes: binary.LittleEndian.Uint64(cpuValBytes[11:19]), 341 | RxDroppedPackets: binary.LittleEndian.Uint64(cpuValBytes[19:27]), 342 | RxPassedBytes: binary.LittleEndian.Uint64(cpuValBytes[27:35]), 343 | RxPassedPackets: binary.LittleEndian.Uint64(cpuValBytes[35:43]), 344 | RxRedirectedBytes: binary.LittleEndian.Uint64(cpuValBytes[43:51]), 345 | RxRedirectedPackets: binary.LittleEndian.Uint64(cpuValBytes[51:59]), 346 | } 347 | } 348 | } 349 | 350 | } 351 | 352 | }(port) 353 | } 354 | 355 | wg.Wait() 356 | return 357 | } 358 | 359 | func (bridge *BridgeGroup) updateStats() error { 360 | for _, port := range bridge.IfList { 361 | stats := make([]StatsXDP, MAX_IFACES) 362 | tsStart := time.Now() 363 | err := port.eBPF.MapStatsXdp.Lookup(uint32(port.iface.Index), &stats) 364 | if err != nil { 365 | return err 366 | } 367 | ttlStats := StatsXDP{} 368 | for _, coreStats := range stats { 369 | ttlStats.RxDroppedBytes += coreStats.RxDroppedBytes 370 | ttlStats.RxDroppedPackets += coreStats.RxDroppedPackets 371 | ttlStats.RxPassedBytes += coreStats.RxPassedBytes 372 | ttlStats.RxPassedPackets += coreStats.RxPassedPackets 373 | ttlStats.RxRedirectedBytes += coreStats.RxRedirectedBytes 374 | ttlStats.RxRedirectedPackets += coreStats.RxRedirectedPackets 375 | if coreStats.RxLastTimestamp > ttlStats.RxLastTimestamp { 376 | ttlStats.RxLastTimestamp = coreStats.RxLastTimestamp 377 | } 378 | 379 | ttlStats.TxRedirectedBytes += coreStats.TxRedirectedBytes 380 | ttlStats.TxRedirectedPackets += coreStats.TxRedirectedPackets 381 | if coreStats.TxLastTimestamp > ttlStats.TxLastTimestamp { 382 | ttlStats.TxLastTimestamp = coreStats.TxLastTimestamp 383 | } 384 | } 385 | port.Stats.mutex.Lock() 386 | port.Stats.RxPackets = ttlStats.RxPassedPackets + ttlStats.RxDroppedPackets + ttlStats.RxRedirectedPackets 387 | port.Stats.RxBytes = ttlStats.RxPassedBytes + ttlStats.RxDroppedBytes + ttlStats.RxRedirectedBytes 388 | port.Stats.TxPackets = ttlStats.TxRedirectedPackets 389 | port.Stats.TxBytes = ttlStats.TxRedirectedBytes 390 | var diffRxBytes, diffTxBytes uint64 391 | if port.Stats.RxBytes == 0 { 392 | diffRxBytes = port.Stats.XdpStats.RxPassedBytes + port.Stats.XdpStats.RxDroppedBytes + port.Stats.XdpStats.RxRedirectedBytes 393 | } else { 394 | diffRxBytes = port.Stats.RxBytes - (port.Stats.XdpStats.RxPassedBytes + port.Stats.XdpStats.RxDroppedBytes + port.Stats.XdpStats.RxRedirectedBytes) 395 | } 396 | if port.Stats.TxBytes == 0 { 397 | diffTxBytes = port.Stats.XdpStats.TxRedirectedBytes 398 | } else { 399 | diffTxBytes = port.Stats.TxBytes - port.Stats.XdpStats.TxRedirectedBytes 400 | } 401 | diffNanoSecs := tsStart.UnixNano() - port.Stats.UpdatedAt.UnixNano() 402 | port.Stats.UpdatedAt = tsStart 403 | mult := 1000000000 / float64(diffNanoSecs) 404 | port.Stats.RxRate = uint64(float64(diffRxBytes) * mult) 405 | port.Stats.TxRate = uint64(float64(diffTxBytes) * mult) 406 | port.Stats.XdpStatsPerCore = stats 407 | port.Stats.XdpStats = ttlStats 408 | port.Stats.xdpStatsHistory.PushBack(port.Stats.XdpStats) 409 | if port.Stats.xdpStatsHistory.Len() >= 100000 { 410 | _ = port.Stats.xdpStatsHistory.Remove(port.Stats.xdpStatsHistory.Front()) 411 | } 412 | port.Stats.mutex.Unlock() 413 | } 414 | return nil 415 | } 416 | -------------------------------------------------------------------------------- /pkg/networkswitch/ports.go: -------------------------------------------------------------------------------- 1 | package networkswitch 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "runtime" 7 | "sort" 8 | "sync" 9 | "time" 10 | 11 | "github.com/cilium/cilium/pkg/option" 12 | "github.com/cilium/ebpf" 13 | lru "github.com/hashicorp/golang-lru/v2" 14 | "github.com/safchain/ethtool" 15 | "github.com/vishvananda/netlink" 16 | "github.com/xlab/treeprint" 17 | "golang.org/x/exp/maps" 18 | "golang.org/x/sys/unix" 19 | ) 20 | 21 | func init() { 22 | DEFAULT_XDP_MODE = option.XDPModeLinkDriver 23 | } 24 | 25 | func (settings *PortSettings) Validate() { 26 | if settings.Trunk { 27 | settings.Vlans = []uint16{} 28 | } else { 29 | allKeys := make(map[uint16]bool) 30 | list := []uint16{} 31 | for _, vlan := range settings.Vlans { 32 | if vlan != settings.PVID { 33 | _, exists := allKeys[vlan] 34 | if !exists { 35 | allKeys[vlan] = true 36 | list = append(list, vlan) 37 | } 38 | } 39 | } 40 | sort.Slice(list, func(i, j int) bool { 41 | return list[i] < list[j] //todo- vlan priority sort order, for now numerical order 42 | }) 43 | settings.Vlans = list 44 | } 45 | } 46 | 47 | func DownAll() error { 48 | ifaces, err := net.Interfaces() 49 | if err != nil { 50 | return err 51 | } 52 | 53 | for _, iface := range ifaces { 54 | err = DownInterface(iface.Name) 55 | if err != nil { 56 | return err 57 | } 58 | } 59 | 60 | /////// Close FDB Map 61 | mapFdb, err := ebpf.LoadPinnedMap("/sys/fs/bpf/Map_fdb_xdp", nil) 62 | if err == nil { 63 | if mapFdb.IsPinned() { 64 | err = mapFdb.Unpin() 65 | if err != nil { 66 | return err 67 | } 68 | } 69 | err = mapFdb.Close() 70 | if err != nil { 71 | return err 72 | } 73 | } 74 | 75 | /////// Close xdp stats 76 | mapStats, err := ebpf.LoadPinnedMap("/sys/fs/bpf/Map_stats_xdp", nil) 77 | if err == nil { 78 | if mapStats.IsPinned() { 79 | err = mapStats.Unpin() 80 | if err != nil { 81 | return err 82 | } 83 | } 84 | err = mapStats.Close() 85 | if err != nil { 86 | return err 87 | } 88 | } 89 | 90 | /////// Close Jump Map xdp 91 | mapJmpXdp, err := ebpf.LoadPinnedMap("/sys/fs/bpf/Map_jump_table_xdp", nil) 92 | if err == nil { 93 | if mapJmpXdp.IsPinned() { 94 | err = mapJmpXdp.Unpin() 95 | if err != nil { 96 | return err 97 | } 98 | } 99 | err = mapJmpXdp.Close() 100 | if err != nil { 101 | return err 102 | } 103 | } 104 | 105 | /////// Close Jump Map tc 106 | mapJmpTc, err := ebpf.LoadPinnedMap("/sys/fs/bpf/Map_jump_table_tc", nil) 107 | if err == nil { 108 | if mapJmpTc.IsPinned() { 109 | err = mapJmpTc.Unpin() 110 | if err != nil { 111 | return err 112 | } 113 | } 114 | err = mapJmpTc.Close() 115 | if err != nil { 116 | return err 117 | } 118 | } 119 | 120 | fmt.Printf("%s down\n", PROG_NAME) 121 | return nil 122 | } 123 | 124 | func DownInterface(ifName string) error { 125 | iface, err := net.InterfaceByName(ifName) 126 | if err != nil { 127 | return err 128 | } 129 | 130 | ethHandle, err := ethtool.NewEthtool() 131 | if err != nil { 132 | return err 133 | } 134 | defer ethHandle.Close() 135 | 136 | link, err := netlink.LinkByName(iface.Name) 137 | if err != nil { 138 | return err 139 | } 140 | 141 | filtersIngress, err := netlink.FilterList(link, netlink.HANDLE_MIN_INGRESS) 142 | if err != nil { 143 | return err 144 | } 145 | 146 | /////// TC detach 147 | matchedIngress := false 148 | for _, filt := range filtersIngress { 149 | attrs := filt.Attrs() 150 | if filt.Type() == "bpf" && attrs.Protocol == unix.ETH_P_ALL && attrs.Handle == netlink.MakeHandle(0, 1) { 151 | matchedIngress = true 152 | 153 | fmt.Printf("[%s] Setting port down... ", iface.Name) 154 | /////// set port down 155 | err = netlink.LinkSetDown(link) 156 | if err != nil { 157 | return err 158 | } 159 | 160 | err = netlink.SetPromiscOff(link) 161 | if err != nil { 162 | return err 163 | } 164 | 165 | ethHandle.Change(iface.Name, FEATURES_ENABLE) 166 | 167 | fmt.Printf("detaching TC... ") 168 | /////// TC detach 169 | err = netlink.FilterDel(filt) 170 | if err != nil { 171 | return err 172 | } 173 | } 174 | } 175 | 176 | /////// TC EGRESS detach 177 | filtersEgress, err := netlink.FilterList(link, netlink.HANDLE_MIN_EGRESS) 178 | if err != nil { 179 | return err 180 | } 181 | for _, filt := range filtersEgress { 182 | attrs := filt.Attrs() 183 | if matchedIngress && filt.Type() == "bpf" && attrs.Protocol == unix.ETH_P_ALL && attrs.Handle == netlink.MakeHandle(0, 1) { 184 | err = netlink.FilterDel(filt) 185 | if err != nil { 186 | return err 187 | } 188 | } 189 | } 190 | 191 | /////// XDP detach 192 | if matchedIngress { 193 | fmt.Printf("detaching XDP... ") 194 | // err = netlink.LinkSetXdpFd(link, -1) 195 | err = netlink.LinkSetXdpFdWithFlags(link, -1, int(xdpModeToFlag(option.XDPModeLinkDriver))) 196 | if err != nil { 197 | return err 198 | } 199 | 200 | err = netlink.LinkSetXdpFdWithFlags(link, -1, int(xdpModeToFlag(option.XDPModeGeneric))) 201 | if err != nil { 202 | return err 203 | } 204 | fmt.Printf("👌\n") 205 | } 206 | 207 | return nil 208 | } 209 | 210 | // func (bridge *BridgeGroup) Down() error { 211 | // for _, port := range bridge.IfList { 212 | // err := DownInterface(port.iface.Name) 213 | // if err != nil { 214 | // return err 215 | // } 216 | // } 217 | 218 | // /////// Close FDB Map 219 | // mapFdb, err := ebpf.LoadPinnedMap("/sys/fs/bpf/Map_fdb_xdp", nil) 220 | // if err == nil { 221 | // if mapFdb.IsPinned() { 222 | // err = mapFdb.Unpin() 223 | // if err != nil { 224 | // return err 225 | // } 226 | // } 227 | // err = mapFdb.Close() 228 | // if err != nil { 229 | // return err 230 | // } 231 | // } 232 | 233 | // /////// Close xdp stats 234 | // mapStats, err := ebpf.LoadPinnedMap("/sys/fs/bpf/Map_stats_xdp", nil) 235 | // if err == nil { 236 | // if mapStats.IsPinned() { 237 | // err = mapStats.Unpin() 238 | // if err != nil { 239 | // return err 240 | // } 241 | // } 242 | // err = mapStats.Close() 243 | // if err != nil { 244 | // return err 245 | // } 246 | // } 247 | 248 | // /////// Close xdp Jump Map 249 | // mapJmpXdp, err := ebpf.LoadPinnedMap("/sys/fs/bpf/Map_jump_table_xdp", nil) 250 | // if err == nil { 251 | // if mapJmpXdp.IsPinned() { 252 | // err = mapJmpXdp.Unpin() 253 | // if err != nil { 254 | // return err 255 | // } 256 | // } 257 | // err = mapJmpXdp.Close() 258 | // if err != nil { 259 | // return err 260 | // } 261 | // } 262 | 263 | // /////// Close tc Jump Map 264 | // mapJmpTc, err := ebpf.LoadPinnedMap("/sys/fs/bpf/Map_jump_table_tc", nil) 265 | // if err == nil { 266 | // if mapJmpTc.IsPinned() { 267 | // err = mapJmpTc.Unpin() 268 | // if err != nil { 269 | // return err 270 | // } 271 | // } 272 | // err = mapJmpTc.Close() 273 | // if err != nil { 274 | // return err 275 | // } 276 | // } 277 | 278 | // fmt.Printf("%s down\n", PROG_NAME) 279 | // return nil 280 | // } 281 | 282 | func (group *BridgeGroup) GetPortByName(name string) (*SwitchPort, error) { 283 | port, exists := group.IfMap[name] 284 | if !exists { 285 | return nil, fmt.Errorf("Port doesn't exist in bridge (name: %s)", name) 286 | } 287 | return port, nil 288 | } 289 | 290 | func (group *BridgeGroup) GetPortByIndex(index int) (*SwitchPort, error) { 291 | port, exists := group.IfMapByIndex[uint16(index)] 292 | if !exists { 293 | return nil, fmt.Errorf("Port doesn't exist in bridge (index: %d)", index) 294 | } 295 | return port, nil 296 | } 297 | 298 | func (group *BridgeGroup) GetPortList() []*SwitchPort { 299 | return group.IfList 300 | } 301 | 302 | func (group *BridgeGroup) BuildPortList() []*SwitchPort { 303 | list := maps.Values(group.IfMap) 304 | sort.Slice(list, func(i, j int) bool { 305 | return list[i].iface.Index < list[j].iface.Index 306 | }) 307 | return list 308 | } 309 | 310 | func (group *BridgeGroup) PrettyPrint() string { 311 | tree := treeprint.NewWithRoot(PROG_NAME) 312 | 313 | for _, port := range group.GetPortList() { 314 | mode := port.settings.XDPMode 315 | if mode == "" { 316 | mode = DEFAULT_XDP_MODE 317 | } 318 | 319 | treePortName := tree.AddBranch(fmt.Sprintf("%s", port.iface.Name)) 320 | treePortName.AddNode(fmt.Sprintf("driver: %s (%s)", port.driverName, mode)) 321 | if port.settings.Transparent { 322 | treePortName.AddNode("transparent: true") 323 | } 324 | 325 | treePortVlans := treePortName.AddBranch("VLANs") 326 | treePortVlans.AddNode(fmt.Sprintf("untagged: %d", port.settings.PVID)) 327 | 328 | if port.settings.Trunk { 329 | treePortVlans.AddNode("tagged: trunk") 330 | } else if len(port.settings.Vlans) > 0 { 331 | switch len(port.settings.Vlans) { 332 | case 0: 333 | _ = 0 334 | case 1: 335 | treePortVlans.AddNode(fmt.Sprintf("tagged: %d", port.settings.Vlans[0])) 336 | default: 337 | treePortTagged := treePortVlans.AddBranch("tagged:") 338 | for _, vlan := range port.settings.Vlans { 339 | treePortTagged.AddNode(fmt.Sprintf("%d", vlan)) 340 | } 341 | } 342 | } 343 | 344 | } 345 | 346 | return tree.String() 347 | } 348 | 349 | func (group *BridgeGroup) AddPort(ifName string, settings PortSettings) error { 350 | var err error 351 | name := ifName 352 | 353 | ifReference, err := net.InterfaceByName(name) 354 | if err != nil { 355 | return err 356 | } 357 | 358 | ethHandle, err := ethtool.NewEthtool() 359 | if err != nil { 360 | return err 361 | } 362 | 363 | nl, err := netlink.LinkByName(ifReference.Name) 364 | if err != nil { 365 | return err 366 | } 367 | 368 | driverName, err := ethHandle.DriverName(ifReference.Name) 369 | if err != nil { 370 | return err 371 | } 372 | 373 | trafficByCore, err := lru.New[int, *StatsTraffic](runtime.NumCPU()) 374 | if err != nil { 375 | return err 376 | } 377 | 378 | trafficByKey, err := lru.New[StatsTrafficKey, *StatsTraffic](65536) 379 | if err != nil { 380 | return err 381 | } 382 | 383 | trafficBySrcIPv4, err := lru.New[uint32, *StatsTraffic](65536) 384 | if err != nil { 385 | return err 386 | } 387 | 388 | trafficByDstIPv4, err := lru.New[uint32, *StatsTraffic](65536) 389 | if err != nil { 390 | return err 391 | } 392 | 393 | trafficByVLAN, err := lru.New[uint16, *StatsTraffic](4094) 394 | if err != nil { 395 | return err 396 | } 397 | 398 | trafficByProtoL2, err := lru.New[uint16, *StatsTraffic](256) 399 | if err != nil { 400 | return err 401 | } 402 | 403 | trafficByProtoL3, err := lru.New[uint16, *StatsTraffic](256) 404 | if err != nil { 405 | return err 406 | } 407 | 408 | trafficByDstIface, err := lru.New[uint16, *StatsTraffic](MAX_IFACES) 409 | if err != nil { 410 | return err 411 | } 412 | 413 | port := SwitchPort{ 414 | iface: ifReference, 415 | settings: settings, 416 | driverName: driverName, 417 | speed: 0, 418 | ethtoolHandle: ethHandle, 419 | ethtoolCmd: ðtool.EthtoolCmd{}, 420 | netlink: nl, 421 | // Tap: waterInterface, //todo 422 | Stats: portStats{}, 423 | Traffic: TrafficObserver{ //todo -move to port 424 | mutex: sync.RWMutex{}, 425 | trafficTotal: NewStatsTraffic(), 426 | trafficByCore: trafficByCore, 427 | trafficByKey: trafficByKey, 428 | trafficBySrcIPv4: trafficBySrcIPv4, 429 | trafficByDstIPv4: trafficByDstIPv4, 430 | trafficByVLAN: trafficByVLAN, 431 | trafficByProtoL2: trafficByProtoL2, 432 | trafficByProtoL3: trafficByProtoL3, 433 | trafficByDstIface: trafficByDstIface, 434 | }, 435 | } 436 | port.Stats.PortName = ifReference.Name 437 | 438 | group.IfMap[ifName] = &port 439 | group.IfMapByIndex[uint16(ifReference.Index)] = &port 440 | 441 | return nil 442 | } 443 | 444 | func (bridge *BridgeGroup) Up() error { 445 | return bridge.allUpEbpf() 446 | } 447 | 448 | func (bridge *BridgeGroup) allUpEbpf() error { 449 | for _, port := range bridge.IfList { 450 | err := port.upEbpf(bridge.IfList) 451 | if err != nil { 452 | return err 453 | } 454 | } 455 | return nil 456 | } 457 | 458 | // func (group *BridgeGroup) allDownEbpf() { 459 | // for _, port := range group.IfMap { 460 | // err := port.downEbpf() 461 | // if err != nil { 462 | // fmt.Println("ERROR: " + err.Error()) 463 | // } 464 | // } 465 | // } 466 | 467 | func (port *SwitchPort) upEbpf(ifList []*SwitchPort) error { 468 | fmt.Println("Setting promisc") 469 | err := netlink.SetPromiscOn(port.netlink) 470 | if err != nil { 471 | return err 472 | } 473 | fmt.Println("Setting ethtool features") 474 | 475 | err = port.ethtoolHandle.Change(port.iface.Name, FEATURES_DISABLE) 476 | if err != nil { 477 | return err 478 | } 479 | 480 | fmt.Println("Setting link") 481 | err = netlink.LinkSetUp(port.netlink) 482 | if err != nil { 483 | return err 484 | } 485 | 486 | fmt.Println("Getting eth speed") 487 | port.speed, err = port.ethtoolHandle.CmdGet(port.ethtoolCmd, port.iface.Name) 488 | if err != nil { 489 | return err 490 | } 491 | if port.speed == 4294967295 { //unknown speed 492 | port.speed = 0 493 | } 494 | 495 | fmt.Println("loading bpf") 496 | port.eBPFSpec, err = loadBpf() 497 | if err != nil { 498 | return err 499 | } 500 | 501 | port.eBPFSpec.Maps["Map_fdb_xdp"].Pinning = ebpf.PinByName 502 | port.eBPFSpec.Maps["Map_stats_xdp"].Pinning = ebpf.PinByName 503 | port.eBPFSpec.Maps["Map_jump_table_xdp"].Pinning = ebpf.PinByName 504 | port.eBPFSpec.Maps["Map_jump_table_tc"].Pinning = ebpf.PinByName 505 | 506 | var portCfgVlanBitmask [64]uint64 507 | if port.settings.Trunk { 508 | portCfgVlanBitmask = bitmaskAllVlans64([]uint16{port.settings.PVID}) 509 | } else { 510 | portCfgVlanBitmask = bitmaskVlanList64(port.settings.Vlans) 511 | } 512 | 513 | ingressFiltering := uint16(0) 514 | if port.settings.IngressFiltering { 515 | ingressFiltering = 1 516 | } 517 | 518 | macBytes := [6]byte{} 519 | copy(macBytes[:], port.iface.HardwareAddr[:6]) 520 | transparent := uint16(0) 521 | if port.settings.Transparent { 522 | transparent = 1 523 | } 524 | hookDrop := uint16(0) 525 | if port.settings.HookDrop != "" { 526 | hookDrop = 1 527 | } 528 | hookEgress := uint16(0) 529 | if port.settings.HookEgress != "" { 530 | hookEgress = 1 531 | } 532 | portCfg := bpfPortCfg{ 533 | IfIndex: uint16(port.iface.Index), 534 | Pvid: port.settings.PVID, 535 | VlanBitmask: portCfgVlanBitmask, 536 | Mac: macBytes, 537 | Transparent: transparent, 538 | IngressFiltering: ingressFiltering, 539 | HookDrop: hookDrop, 540 | HookEgress: hookEgress, 541 | Tap: uint16(1), 542 | } 543 | 544 | portCfgListByIdx := [MAX_IFACES]bpfPortCfg{} 545 | portIdxList := [MAX_IFACES]uint8{} 546 | for idx, p := range ifList { 547 | var pCfgVlanBitmask [64]uint64 548 | if p.settings.Trunk { 549 | pCfgVlanBitmask = bitmaskAllVlans64([]uint16{p.settings.PVID}) 550 | } else { 551 | pCfgVlanBitmask = bitmaskVlanList64(p.settings.Vlans) 552 | } 553 | 554 | ingressFiltering = uint16(0) 555 | if p.settings.IngressFiltering { 556 | ingressFiltering = 1 557 | } 558 | 559 | macBytes = [6]byte{} 560 | copy(macBytes[:], p.iface.HardwareAddr[:6]) 561 | 562 | transparent = 0 563 | if p.settings.Transparent { 564 | transparent = 1 565 | } 566 | 567 | hookDrop = 0 568 | if p.settings.HookDrop != "" { 569 | hookDrop = 1 570 | } 571 | hookEgress = 0 572 | if p.settings.HookEgress != "" { 573 | hookEgress = 1 574 | } 575 | 576 | fmt.Printf("index: %d\n", p.iface.Index) 577 | portCfgListByIdx[p.iface.Index] = bpfPortCfg{ 578 | IfIndex: uint16(p.iface.Index), 579 | Pvid: p.settings.PVID, 580 | VlanBitmask: pCfgVlanBitmask, 581 | Mac: macBytes, 582 | Transparent: transparent, 583 | IngressFiltering: ingressFiltering, 584 | HookDrop: hookDrop, 585 | HookEgress: hookEgress, 586 | Tap: uint16(0), 587 | } 588 | 589 | portIdxList[idx] = uint8(p.iface.Index) 590 | } 591 | 592 | enableStats := uint8(0) 593 | if STATS_ENABLED { 594 | enableStats = 1 595 | } 596 | err = port.eBPFSpec.RewriteConstants(map[string]interface{}{ 597 | "PORT_CFG": portCfg, 598 | "PORT_COUNT": uint8(len(ifList)), 599 | "PORTS_CFG_BY_IDX": portCfgListByIdx, 600 | 601 | "PORTS_IDX": portIdxList, 602 | "STATS_ENABLED": enableStats, 603 | }) 604 | if err != nil { 605 | panic(err) 606 | } 607 | 608 | collectionOpts := ebpf.CollectionOptions{ 609 | Maps: ebpf.MapOptions{ 610 | // The base path to pin maps in if requested via PinByName. 611 | // Existing maps will be re-used if they are compatible, otherwise an 612 | // error is returned. 613 | // PinPath: "/sys/fs/bpf/xdp_switch", 614 | PinPath: "/sys/fs/bpf/", 615 | // LoadPinOptions: ebpf.LoadPinOptions{}, 616 | }, 617 | // Programs: ProgramOptions, 618 | // MapReplacements: map[string]*ebpf.Map, 619 | } 620 | 621 | if err := port.eBPFSpec.LoadAndAssign(&port.eBPF, &collectionOpts); err != nil { 622 | panic(err) 623 | } 624 | 625 | // This section aligns all of the tail calls with the maps so that they're called 626 | // correctly within the eBPF program 627 | port.eBPF.MapJumpTableTc.Put(int32(1), int32(port.eBPF.TailCall1.FD())) // HANDLE_UNTAGGED 628 | port.eBPF.MapJumpTableTc.Put(int32(2), int32(port.eBPF.TailCall2.FD())) // HANDLE_TAGGED 629 | port.eBPF.MapJumpTableTc.Put(int32(3), int32(port.eBPF.TailCall1B.FD())) // HANDLE_UNTAGGED_B 630 | port.eBPF.MapJumpTableTc.Put(int32(4), int32(port.eBPF.TailCall2B.FD())) // HANDLE_TAGGED_B 631 | port.eBPF.MapJumpTableTc.Put(int32(5), int32(port.eBPF.HookDropTc.FD())) 632 | port.eBPF.MapJumpTableTc.Put(int32(6), int32(port.eBPF.HookEgressTc.FD())) 633 | 634 | port.eBPF.MapJumpTableXdp.Put(int32(1), int32(port.eBPF.HookDropXdp.FD())) 635 | port.eBPF.MapJumpTableXdp.Put(int32(2), int32(port.eBPF.HookEgressXdp.FD())) 636 | 637 | port.Stats.UpTimestamp = time.Now() 638 | 639 | err = port.attachPrograms() 640 | if err != nil { 641 | panic(err) 642 | } 643 | 644 | return nil 645 | } 646 | --------------------------------------------------------------------------------