From fafbd0097f9950fb1c04b16ffb33febe7152096e Mon Sep 17 00:00:00 2001 From: Hongliang Liu Date: Fri, 10 Nov 2023 15:40:49 +0800 Subject: [PATCH] Add support for NodeNetworkPolicy dataplane Signed-off-by: Hongliang Liu --- build/charts/antrea/README.md | 74 ++ build/charts/antrea/conf/antrea-agent.conf | 11 + .../charts/antrea/conf/antrea-controller.conf | 3 + build/charts/antrea/values.yaml | 91 ++ build/yamls/antrea-aks.yml | 99 +- build/yamls/antrea-eks.yml | 99 +- build/yamls/antrea-gke.yml | 99 +- build/yamls/antrea-ipsec.yml | 99 +- build/yamls/antrea.yml | 99 +- cmd/antrea-agent/agent.go | 12 +- cmd/antrea-agent/options.go | 55 +- cmd/antrea-agent/options_linux_test.go | 118 +++ cmd/antrea-agent/util.go | 6 +- cmd/antrea-agent/util_test.go | 2 +- docs/feature-gates.md | 9 + hack/update-checksum-windows.sh | 8 +- pkg/agent/config/node_config.go | 5 + pkg/agent/controller/networkpolicy/cache.go | 21 +- pkg/agent/controller/networkpolicy/fqdn.go | 6 +- .../networkpolicy/networkpolicy_controller.go | 126 ++- .../networkpolicy_controller_test.go | 6 +- .../networkpolicy/node_reconciler_other.go | 711 ++++++++++++++ .../node_reconciler_other_test.go | 871 ++++++++++++++++++ .../networkpolicy/node_reconciler_windows.go | 49 + .../{reconciler.go => pod_reconciler.go} | 86 +- ...onciler_test.go => pod_reconciler_test.go} | 26 +- pkg/agent/route/interfaces.go | 14 + pkg/agent/route/route_linux.go | 570 +++++++++++- pkg/agent/route/route_linux_test.go | 479 +++++++++- pkg/agent/route/route_windows.go | 26 +- pkg/agent/route/route_windows_test.go | 2 +- pkg/agent/route/testing/mock_route.go | 57 ++ pkg/agent/util/iptables/builder.go | 168 ++++ pkg/agent/util/iptables/interfaces.go | 42 + pkg/agent/util/iptables/iptables.go | 16 +- pkg/apis/controlplane/v1beta2/types.go | 1 + .../handlers/featuregates/handler_test.go | 3 + pkg/config/agent/config.go | 16 + .../networkpolicy/clusternetworkpolicy.go | 1 + pkg/features/antrea_features.go | 8 + test/integration/agent/route_test.go | 18 +- 41 files changed, 4028 insertions(+), 184 deletions(-) create mode 100644 pkg/agent/controller/networkpolicy/node_reconciler_other.go create mode 100644 pkg/agent/controller/networkpolicy/node_reconciler_other_test.go create mode 100644 pkg/agent/controller/networkpolicy/node_reconciler_windows.go rename pkg/agent/controller/networkpolicy/{reconciler.go => pod_reconciler.go} (93%) rename pkg/agent/controller/networkpolicy/{reconciler_test.go => pod_reconciler_test.go} (98%) create mode 100644 pkg/agent/util/iptables/builder.go create mode 100644 pkg/agent/util/iptables/interfaces.go diff --git a/build/charts/antrea/README.md b/build/charts/antrea/README.md index 6bb8d4233ef..a512a590ece 100644 --- a/build/charts/antrea/README.md +++ b/build/charts/antrea/README.md @@ -108,6 +108,80 @@ Kubernetes: `>= 1.16.0-0` | nodeIPAM.nodeCIDRMaskSizeIPv6 | int | `64` | Mask size for IPv6 Node CIDR in IPv6 or dual-stack cluster. | | nodeIPAM.serviceCIDR | string | `""` | IPv4 CIDR ranges reserved for Services. | | nodeIPAM.serviceCIDRv6 | string | `""` | IPv6 CIDR ranges reserved for Services. | +| nodeNetworkPolicy.privilegedEgressRules[0].comment | string | `"allow egress traffic to DNS port"` | | +| nodeNetworkPolicy.privilegedEgressRules[0].ports[0] | int | `53` | | +| nodeNetworkPolicy.privilegedEgressRules[0].protocol | string | `"udp"` | | +| nodeNetworkPolicy.privilegedEgressRules[10].comment | string | `"allow ICMPv6 echo-reply traffic"` | | +| nodeNetworkPolicy.privilegedEgressRules[10].icmpType | int | `129` | | +| nodeNetworkPolicy.privilegedEgressRules[10].protocol | string | `"icmp6"` | | +| nodeNetworkPolicy.privilegedEgressRules[11].comment | string | `"allow IPv6 Neighbor Solicitation traffic"` | | +| nodeNetworkPolicy.privilegedEgressRules[11].icmpType | int | `135` | | +| nodeNetworkPolicy.privilegedEgressRules[11].protocol | string | `"icmp6"` | | +| nodeNetworkPolicy.privilegedEgressRules[12].comment | string | `"allow IPv6 Neighbor Advertisement traffic"` | | +| nodeNetworkPolicy.privilegedEgressRules[12].icmpType | int | `136` | | +| nodeNetworkPolicy.privilegedEgressRules[12].protocol | string | `"icmp6"` | | +| nodeNetworkPolicy.privilegedEgressRules[1].comment | string | `"allow egress traffic to DNS port"` | | +| nodeNetworkPolicy.privilegedEgressRules[1].ports[0] | int | `53` | | +| nodeNetworkPolicy.privilegedEgressRules[1].protocol | string | `"tcp"` | | +| nodeNetworkPolicy.privilegedEgressRules[2].comment | string | `"allow egress traffic to HTTP/HTTPS port"` | | +| nodeNetworkPolicy.privilegedEgressRules[2].ports[0] | int | `80` | | +| nodeNetworkPolicy.privilegedEgressRules[2].ports[1] | int | `443` | | +| nodeNetworkPolicy.privilegedEgressRules[2].protocol | string | `"tcp"` | | +| nodeNetworkPolicy.privilegedEgressRules[3].comment | string | `"allow egress traffic to Kubernetes DNS metric"` | | +| nodeNetworkPolicy.privilegedEgressRules[3].ports[0] | int | `9153` | | +| nodeNetworkPolicy.privilegedEgressRules[3].protocol | string | `"tcp"` | | +| nodeNetworkPolicy.privilegedEgressRules[4].comment | string | `"allow egress traffic to Kubernetes ports"` | | +| nodeNetworkPolicy.privilegedEgressRules[4].ports[0] | int | `6443` | | +| nodeNetworkPolicy.privilegedEgressRules[4].ports[1] | string | `"10248:10250"` | | +| nodeNetworkPolicy.privilegedEgressRules[4].ports[2] | string | `"10256:10258"` | | +| nodeNetworkPolicy.privilegedEgressRules[4].protocol | string | `"tcp"` | | +| nodeNetworkPolicy.privilegedEgressRules[5].comment | string | `"allow egress traffic to Antrea ports"` | | +| nodeNetworkPolicy.privilegedEgressRules[5].ports[0] | string | `"10348:10351"` | | +| nodeNetworkPolicy.privilegedEgressRules[5].protocol | string | `"tcp"` | | +| nodeNetworkPolicy.privilegedEgressRules[6].comment | string | `"allow egress traffic to Wireguard ports"` | | +| nodeNetworkPolicy.privilegedEgressRules[6].ports[0] | string | `"51820:51821"` | | +| nodeNetworkPolicy.privilegedEgressRules[6].protocol | string | `"udp"` | | +| nodeNetworkPolicy.privilegedEgressRules[7].comment | string | `"allow ICMP echo-reply traffic"` | | +| nodeNetworkPolicy.privilegedEgressRules[7].icmpType | int | `0` | | +| nodeNetworkPolicy.privilegedEgressRules[7].protocol | string | `"icmp"` | | +| nodeNetworkPolicy.privilegedEgressRules[8].comment | string | `"allow ICMP echo-request traffic"` | | +| nodeNetworkPolicy.privilegedEgressRules[8].icmpType | int | `8` | | +| nodeNetworkPolicy.privilegedEgressRules[8].protocol | string | `"icmp"` | | +| nodeNetworkPolicy.privilegedEgressRules[9].comment | string | `"allow ICMPv6 echo-request traffic"` | | +| nodeNetworkPolicy.privilegedEgressRules[9].icmpType | int | `128` | | +| nodeNetworkPolicy.privilegedEgressRules[9].protocol | string | `"icmp6"` | | +| nodeNetworkPolicy.privilegedIngressRules[0].comment | string | `"allow ingress traffic to SSH"` | | +| nodeNetworkPolicy.privilegedIngressRules[0].ports[0] | int | `22` | | +| nodeNetworkPolicy.privilegedIngressRules[0].protocol | string | `"tcp"` | | +| nodeNetworkPolicy.privilegedIngressRules[1].comment | string | `"allow ingress traffic to Kubernetes ports"` | | +| nodeNetworkPolicy.privilegedIngressRules[1].ports[0] | int | `6443` | | +| nodeNetworkPolicy.privilegedIngressRules[1].ports[1] | string | `"10248:10250"` | | +| nodeNetworkPolicy.privilegedIngressRules[1].ports[2] | string | `"10256:10258"` | | +| nodeNetworkPolicy.privilegedIngressRules[1].protocol | string | `"tcp"` | | +| nodeNetworkPolicy.privilegedIngressRules[2].comment | string | `"allow ingress traffic to Antrea ports"` | | +| nodeNetworkPolicy.privilegedIngressRules[2].ports[0] | string | `"10348:10351"` | | +| nodeNetworkPolicy.privilegedIngressRules[2].protocol | string | `"tcp"` | | +| nodeNetworkPolicy.privilegedIngressRules[3].comment | string | `"allow ingress traffic to Wireguard ports"` | | +| nodeNetworkPolicy.privilegedIngressRules[3].ports[0] | string | `"51820:51821"` | | +| nodeNetworkPolicy.privilegedIngressRules[3].protocol | string | `"udp"` | | +| nodeNetworkPolicy.privilegedIngressRules[4].comment | string | `"allow ICMP echo-reply traffic"` | | +| nodeNetworkPolicy.privilegedIngressRules[4].icmpType | int | `0` | | +| nodeNetworkPolicy.privilegedIngressRules[4].protocol | string | `"icmp"` | | +| nodeNetworkPolicy.privilegedIngressRules[5].comment | string | `"allow ICMP echo-request traffic"` | | +| nodeNetworkPolicy.privilegedIngressRules[5].icmpType | int | `8` | | +| nodeNetworkPolicy.privilegedIngressRules[5].protocol | string | `"icmp"` | | +| nodeNetworkPolicy.privilegedIngressRules[6].comment | string | `"allow ICMPv6 echo-request traffic"` | | +| nodeNetworkPolicy.privilegedIngressRules[6].icmpType | int | `128` | | +| nodeNetworkPolicy.privilegedIngressRules[6].protocol | string | `"icmp6"` | | +| nodeNetworkPolicy.privilegedIngressRules[7].comment | string | `"allow ICMPv6 echo-reply traffic"` | | +| nodeNetworkPolicy.privilegedIngressRules[7].icmpType | int | `129` | | +| nodeNetworkPolicy.privilegedIngressRules[7].protocol | string | `"icmp6"` | | +| nodeNetworkPolicy.privilegedIngressRules[8].comment | string | `"allow IPv6 Neighbor Solicitation traffic"` | | +| nodeNetworkPolicy.privilegedIngressRules[8].icmpType | int | `135` | | +| nodeNetworkPolicy.privilegedIngressRules[8].protocol | string | `"icmp6"` | | +| nodeNetworkPolicy.privilegedIngressRules[9].comment | string | `"allow IPv6 Neighbor Advertisement traffic"` | | +| nodeNetworkPolicy.privilegedIngressRules[9].icmpType | int | `136` | | +| nodeNetworkPolicy.privilegedIngressRules[9].protocol | string | `"icmp6"` | | | nodePortLocal.enable | bool | `false` | Enable the NodePortLocal feature. | | nodePortLocal.portRange | string | `"61000-62000"` | Port range used by NodePortLocal when creating Pod port mappings. | | ovs.bridgeName | string | `"br-int"` | Name of the OVS bridge antrea-agent will create and use. | diff --git a/build/charts/antrea/conf/antrea-agent.conf b/build/charts/antrea/conf/antrea-agent.conf index e93fe46c4fb..78590bcff73 100644 --- a/build/charts/antrea/conf/antrea-agent.conf +++ b/build/charts/antrea/conf/antrea-agent.conf @@ -79,6 +79,9 @@ featureGates: # Enable Egress traffic shaping. {{- include "featureGate" (dict "featureGates" .Values.featureGates "name" "EgressTrafficShaping" "default" false) }} +# Enable users to protect their Kubernetes Node. +{{- include "featureGate" (dict "featureGates" .Values.featureGates "name" "NodeNetworkPolicy" "default" false) }} + # Name of the OpenVSwitch bridge antrea-agent will create and use. # Make sure it doesn't conflict with your existing OpenVSwitch bridges. ovsBridge: {{ .Values.ovs.bridgeName | quote }} @@ -431,3 +434,11 @@ secondaryNetwork: {{- end }} {{- end }} + +nodeNetworkPolicy: +{{- with .Values.nodeNetworkPolicy }} + privilegedIngressRules: + {{- toYaml .privilegedIngressRules | trim | nindent 4 }} + privilegedEgressRules: + {{- toYaml .privilegedEgressRules | trim | nindent 4 }} +{{- end }} diff --git a/build/charts/antrea/conf/antrea-controller.conf b/build/charts/antrea/conf/antrea-controller.conf index 7a66e0afcb9..e6fa44daf6e 100644 --- a/build/charts/antrea/conf/antrea-controller.conf +++ b/build/charts/antrea/conf/antrea-controller.conf @@ -54,6 +54,9 @@ featureGates: # set security postures for their clusters. {{- include "featureGate" (dict "featureGates" .Values.featureGates "name" "AdminNetworkPolicy" "default" false) }} +# Enable users to protect their Kubernetes Node. +{{- include "featureGate" (dict "featureGates" .Values.featureGates "name" "NodeNetworkPolicy" "default" false) }} + # The port for the antrea-controller APIServer to serve on. # Note that if it's set to another value, the `containerPort` of the `api` port of the # `antrea-controller` container must be set to the same value. diff --git a/build/charts/antrea/values.yaml b/build/charts/antrea/values.yaml index 78efe1833a4..b6db0ed7207 100644 --- a/build/charts/antrea/values.yaml +++ b/build/charts/antrea/values.yaml @@ -388,6 +388,97 @@ multicluster: # -- WireGuard tunnel port for cross-cluster traffic. port: 51821 +nodeNetworkPolicy: + # Configure nodeNetworkPolicy privileged ingress rules for Antrea Agent. They could be applied to both IPv4 and IPv6. + privilegedIngressRules: + - protocol: tcp + ports: + - 22 + comment: "allow ingress traffic to SSH" + - protocol: tcp + ports: + - 6443 + - 10248:10250 + - 10256:10258 + comment: "allow ingress traffic to Kubernetes ports" + - protocol: tcp + ports: + - 10348:10351 + comment: "allow ingress traffic to Antrea ports" + - protocol: udp + ports: + - 51820:51821 + comment: "allow ingress traffic to Wireguard ports" + - protocol: icmp + icmpType: 0 + comment: "allow ICMP echo-reply traffic" + - protocol: icmp + icmpType: 8 + comment: "allow ICMP echo-request traffic" + - protocol: icmp6 + icmpType: 128 + comment: "allow ICMPv6 echo-request traffic" + - protocol: icmp6 + icmpType: 129 + comment: "allow ICMPv6 echo-reply traffic" + - protocol: icmp6 + icmpType: 135 + comment: "allow IPv6 Neighbor Solicitation traffic" + - protocol: icmp6 + icmpType: 136 + comment: "allow IPv6 Neighbor Advertisement traffic" + # Configure nodeNetworkPolicy privileged egress rules for Antrea Agent. They could be applied to both IPv4 and IPv6. + privilegedEgressRules: + - protocol: udp + ports: + - 53 + comment: "allow egress traffic to DNS port" + - protocol: tcp + ports: + - 53 + comment: "allow egress traffic to DNS port" + - protocol: tcp + ports: + - 80 + - 443 + comment: "allow egress traffic to HTTP/HTTPS port" + - protocol: tcp + ports: + - 9153 + comment: "allow egress traffic to Kubernetes DNS metric" + - protocol: tcp + ports: + - 6443 + - 10248:10250 + - 10256:10258 + comment: "allow egress traffic to Kubernetes ports" + - protocol: tcp + ports: + - 10348:10351 + comment: "allow egress traffic to Antrea ports" + - protocol: udp + ports: + - 51820:51821 + comment: "allow egress traffic to Wireguard ports" + - protocol: icmp + icmpType: 0 + comment: "allow ICMP echo-reply traffic" + - protocol: icmp + icmpType: 8 + comment: "allow ICMP echo-request traffic" + - protocol: icmp6 + icmpType: 128 + comment: "allow ICMPv6 echo-request traffic" + - protocol: icmp6 + icmpType: 129 + comment: "allow ICMPv6 echo-reply traffic" + - protocol: icmp6 + icmpType: 135 + comment: "allow IPv6 Neighbor Solicitation traffic" + - protocol: icmp6 + icmpType: 136 + comment: "allow IPv6 Neighbor Advertisement traffic" + testing: # -- Enable code coverage measurement (used when testing Antrea only). coverage: false diff --git a/build/yamls/antrea-aks.yml b/build/yamls/antrea-aks.yml index 0b08d80b60a..2b5378c15c5 100644 --- a/build/yamls/antrea-aks.yml +++ b/build/yamls/antrea-aks.yml @@ -5574,6 +5574,9 @@ data: # Enable Egress traffic shaping. # EgressTrafficShaping: false + # Enable users to protect their Kubernetes Node. + # NodeNetworkPolicy: false + # Name of the OpenVSwitch bridge antrea-agent will create and use. # Make sure it doesn't conflict with your existing OpenVSwitch bridges. ovsBridge: "br-int" @@ -5882,6 +5885,95 @@ data: maxAge: 28 # Compress enables gzip compression on rotated files. compress: true + + nodeNetworkPolicy: + privilegedIngressRules: + - comment: allow ingress traffic to SSH + ports: + - 22 + protocol: tcp + - comment: allow ingress traffic to Kubernetes ports + ports: + - 6443 + - 10248:10250 + - 10256:10258 + protocol: tcp + - comment: allow ingress traffic to Antrea ports + ports: + - 10348:10351 + protocol: tcp + - comment: allow ingress traffic to Wireguard ports + ports: + - 51820:51821 + protocol: udp + - comment: allow ICMP echo-reply traffic + icmpType: 0 + protocol: icmp + - comment: allow ICMP echo-request traffic + icmpType: 8 + protocol: icmp + - comment: allow ICMPv6 echo-request traffic + icmpType: 128 + protocol: icmp6 + - comment: allow ICMPv6 echo-reply traffic + icmpType: 129 + protocol: icmp6 + - comment: allow IPv6 Neighbor Solicitation traffic + icmpType: 135 + protocol: icmp6 + - comment: allow IPv6 Neighbor Advertisement traffic + icmpType: 136 + protocol: icmp6 + privilegedEgressRules: + - comment: allow egress traffic to DNS port + ports: + - 53 + protocol: udp + - comment: allow egress traffic to DNS port + ports: + - 53 + protocol: tcp + - comment: allow egress traffic to HTTP/HTTPS port + ports: + - 80 + - 443 + protocol: tcp + - comment: allow egress traffic to Kubernetes DNS metric + ports: + - 9153 + protocol: tcp + - comment: allow egress traffic to Kubernetes ports + ports: + - 6443 + - 10248:10250 + - 10256:10258 + protocol: tcp + - comment: allow egress traffic to Antrea ports + ports: + - 10348:10351 + protocol: tcp + - comment: allow egress traffic to Wireguard ports + ports: + - 51820:51821 + protocol: udp + - comment: allow ICMP echo-reply traffic + icmpType: 0 + protocol: icmp + - comment: allow ICMP echo-request traffic + icmpType: 8 + protocol: icmp + - comment: allow ICMPv6 echo-request traffic + icmpType: 128 + protocol: icmp6 + - comment: allow ICMPv6 echo-reply traffic + icmpType: 129 + protocol: icmp6 + - comment: allow IPv6 Neighbor Solicitation traffic + icmpType: 135 + protocol: icmp6 + - comment: allow IPv6 Neighbor Advertisement traffic + icmpType: 136 + protocol: icmp6 antrea-cni.conflist: | { "cniVersion":"0.3.0", @@ -5962,6 +6054,9 @@ data: # set security postures for their clusters. # AdminNetworkPolicy: false + # Enable users to protect their Kubernetes Node. + # NodeNetworkPolicy: false + # The port for the antrea-controller APIServer to serve on. # Note that if it's set to another value, the `containerPort` of the `api` port of the # `antrea-controller` container must be set to the same value. @@ -6866,7 +6961,7 @@ spec: kubectl.kubernetes.io/default-container: antrea-agent # Automatically restart Pods with a RollingUpdate if the ConfigMap changes # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments - checksum/config: e59e0431902646d46cba490279184fea2bdd3c8b486b5a7b1d3ece9a91614634 + checksum/config: 62084808ac873d0c0d719e5254ec35c6612d7b9780972b39a508d7cc3f4245c0 labels: app: antrea component: antrea-agent @@ -7107,7 +7202,7 @@ spec: annotations: # Automatically restart Pod if the ConfigMap changes # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments - checksum/config: e59e0431902646d46cba490279184fea2bdd3c8b486b5a7b1d3ece9a91614634 + checksum/config: 62084808ac873d0c0d719e5254ec35c6612d7b9780972b39a508d7cc3f4245c0 labels: app: antrea component: antrea-controller diff --git a/build/yamls/antrea-eks.yml b/build/yamls/antrea-eks.yml index 1ebcf9995d5..ce6c37193df 100644 --- a/build/yamls/antrea-eks.yml +++ b/build/yamls/antrea-eks.yml @@ -5574,6 +5574,9 @@ data: # Enable Egress traffic shaping. # EgressTrafficShaping: false + # Enable users to protect their Kubernetes Node. + # NodeNetworkPolicy: false + # Name of the OpenVSwitch bridge antrea-agent will create and use. # Make sure it doesn't conflict with your existing OpenVSwitch bridges. ovsBridge: "br-int" @@ -5882,6 +5885,95 @@ data: maxAge: 28 # Compress enables gzip compression on rotated files. compress: true + + nodeNetworkPolicy: + privilegedIngressRules: + - comment: allow ingress traffic to SSH + ports: + - 22 + protocol: tcp + - comment: allow ingress traffic to Kubernetes ports + ports: + - 6443 + - 10248:10250 + - 10256:10258 + protocol: tcp + - comment: allow ingress traffic to Antrea ports + ports: + - 10348:10351 + protocol: tcp + - comment: allow ingress traffic to Wireguard ports + ports: + - 51820:51821 + protocol: udp + - comment: allow ICMP echo-reply traffic + icmpType: 0 + protocol: icmp + - comment: allow ICMP echo-request traffic + icmpType: 8 + protocol: icmp + - comment: allow ICMPv6 echo-request traffic + icmpType: 128 + protocol: icmp6 + - comment: allow ICMPv6 echo-reply traffic + icmpType: 129 + protocol: icmp6 + - comment: allow IPv6 Neighbor Solicitation traffic + icmpType: 135 + protocol: icmp6 + - comment: allow IPv6 Neighbor Advertisement traffic + icmpType: 136 + protocol: icmp6 + privilegedEgressRules: + - comment: allow egress traffic to DNS port + ports: + - 53 + protocol: udp + - comment: allow egress traffic to DNS port + ports: + - 53 + protocol: tcp + - comment: allow egress traffic to HTTP/HTTPS port + ports: + - 80 + - 443 + protocol: tcp + - comment: allow egress traffic to Kubernetes DNS metric + ports: + - 9153 + protocol: tcp + - comment: allow egress traffic to Kubernetes ports + ports: + - 6443 + - 10248:10250 + - 10256:10258 + protocol: tcp + - comment: allow egress traffic to Antrea ports + ports: + - 10348:10351 + protocol: tcp + - comment: allow egress traffic to Wireguard ports + ports: + - 51820:51821 + protocol: udp + - comment: allow ICMP echo-reply traffic + icmpType: 0 + protocol: icmp + - comment: allow ICMP echo-request traffic + icmpType: 8 + protocol: icmp + - comment: allow ICMPv6 echo-request traffic + icmpType: 128 + protocol: icmp6 + - comment: allow ICMPv6 echo-reply traffic + icmpType: 129 + protocol: icmp6 + - comment: allow IPv6 Neighbor Solicitation traffic + icmpType: 135 + protocol: icmp6 + - comment: allow IPv6 Neighbor Advertisement traffic + icmpType: 136 + protocol: icmp6 antrea-cni.conflist: | { "cniVersion":"0.3.0", @@ -5962,6 +6054,9 @@ data: # set security postures for their clusters. # AdminNetworkPolicy: false + # Enable users to protect their Kubernetes Node. + # NodeNetworkPolicy: false + # The port for the antrea-controller APIServer to serve on. # Note that if it's set to another value, the `containerPort` of the `api` port of the # `antrea-controller` container must be set to the same value. @@ -6866,7 +6961,7 @@ spec: kubectl.kubernetes.io/default-container: antrea-agent # Automatically restart Pods with a RollingUpdate if the ConfigMap changes # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments - checksum/config: e59e0431902646d46cba490279184fea2bdd3c8b486b5a7b1d3ece9a91614634 + checksum/config: 62084808ac873d0c0d719e5254ec35c6612d7b9780972b39a508d7cc3f4245c0 labels: app: antrea component: antrea-agent @@ -7108,7 +7203,7 @@ spec: annotations: # Automatically restart Pod if the ConfigMap changes # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments - checksum/config: e59e0431902646d46cba490279184fea2bdd3c8b486b5a7b1d3ece9a91614634 + checksum/config: 62084808ac873d0c0d719e5254ec35c6612d7b9780972b39a508d7cc3f4245c0 labels: app: antrea component: antrea-controller diff --git a/build/yamls/antrea-gke.yml b/build/yamls/antrea-gke.yml index 572595eb632..baa7fdb74c2 100644 --- a/build/yamls/antrea-gke.yml +++ b/build/yamls/antrea-gke.yml @@ -5574,6 +5574,9 @@ data: # Enable Egress traffic shaping. # EgressTrafficShaping: false + # Enable users to protect their Kubernetes Node. + # NodeNetworkPolicy: false + # Name of the OpenVSwitch bridge antrea-agent will create and use. # Make sure it doesn't conflict with your existing OpenVSwitch bridges. ovsBridge: "br-int" @@ -5882,6 +5885,95 @@ data: maxAge: 28 # Compress enables gzip compression on rotated files. compress: true + + nodeNetworkPolicy: + privilegedIngressRules: + - comment: allow ingress traffic to SSH + ports: + - 22 + protocol: tcp + - comment: allow ingress traffic to Kubernetes ports + ports: + - 6443 + - 10248:10250 + - 10256:10258 + protocol: tcp + - comment: allow ingress traffic to Antrea ports + ports: + - 10348:10351 + protocol: tcp + - comment: allow ingress traffic to Wireguard ports + ports: + - 51820:51821 + protocol: udp + - comment: allow ICMP echo-reply traffic + icmpType: 0 + protocol: icmp + - comment: allow ICMP echo-request traffic + icmpType: 8 + protocol: icmp + - comment: allow ICMPv6 echo-request traffic + icmpType: 128 + protocol: icmp6 + - comment: allow ICMPv6 echo-reply traffic + icmpType: 129 + protocol: icmp6 + - comment: allow IPv6 Neighbor Solicitation traffic + icmpType: 135 + protocol: icmp6 + - comment: allow IPv6 Neighbor Advertisement traffic + icmpType: 136 + protocol: icmp6 + privilegedEgressRules: + - comment: allow egress traffic to DNS port + ports: + - 53 + protocol: udp + - comment: allow egress traffic to DNS port + ports: + - 53 + protocol: tcp + - comment: allow egress traffic to HTTP/HTTPS port + ports: + - 80 + - 443 + protocol: tcp + - comment: allow egress traffic to Kubernetes DNS metric + ports: + - 9153 + protocol: tcp + - comment: allow egress traffic to Kubernetes ports + ports: + - 6443 + - 10248:10250 + - 10256:10258 + protocol: tcp + - comment: allow egress traffic to Antrea ports + ports: + - 10348:10351 + protocol: tcp + - comment: allow egress traffic to Wireguard ports + ports: + - 51820:51821 + protocol: udp + - comment: allow ICMP echo-reply traffic + icmpType: 0 + protocol: icmp + - comment: allow ICMP echo-request traffic + icmpType: 8 + protocol: icmp + - comment: allow ICMPv6 echo-request traffic + icmpType: 128 + protocol: icmp6 + - comment: allow ICMPv6 echo-reply traffic + icmpType: 129 + protocol: icmp6 + - comment: allow IPv6 Neighbor Solicitation traffic + icmpType: 135 + protocol: icmp6 + - comment: allow IPv6 Neighbor Advertisement traffic + icmpType: 136 + protocol: icmp6 antrea-cni.conflist: | { "cniVersion":"0.3.0", @@ -5962,6 +6054,9 @@ data: # set security postures for their clusters. # AdminNetworkPolicy: false + # Enable users to protect their Kubernetes Node. + # NodeNetworkPolicy: false + # The port for the antrea-controller APIServer to serve on. # Note that if it's set to another value, the `containerPort` of the `api` port of the # `antrea-controller` container must be set to the same value. @@ -6866,7 +6961,7 @@ spec: kubectl.kubernetes.io/default-container: antrea-agent # Automatically restart Pods with a RollingUpdate if the ConfigMap changes # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments - checksum/config: 3b1758664de8044af1aa7454c64bd1a4911750e562e1ae9375c9c16a335a469d + checksum/config: 1a8031e4e01406dfd8b17e0625b0eb2d402f53e07977e4e8aa13e96a2c4c8dd7 labels: app: antrea component: antrea-agent @@ -7105,7 +7200,7 @@ spec: annotations: # Automatically restart Pod if the ConfigMap changes # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments - checksum/config: 3b1758664de8044af1aa7454c64bd1a4911750e562e1ae9375c9c16a335a469d + checksum/config: 1a8031e4e01406dfd8b17e0625b0eb2d402f53e07977e4e8aa13e96a2c4c8dd7 labels: app: antrea component: antrea-controller diff --git a/build/yamls/antrea-ipsec.yml b/build/yamls/antrea-ipsec.yml index a9cc6bd36d3..9c5098812ce 100644 --- a/build/yamls/antrea-ipsec.yml +++ b/build/yamls/antrea-ipsec.yml @@ -5587,6 +5587,9 @@ data: # Enable Egress traffic shaping. # EgressTrafficShaping: false + # Enable users to protect their Kubernetes Node. + # NodeNetworkPolicy: false + # Name of the OpenVSwitch bridge antrea-agent will create and use. # Make sure it doesn't conflict with your existing OpenVSwitch bridges. ovsBridge: "br-int" @@ -5895,6 +5898,95 @@ data: maxAge: 28 # Compress enables gzip compression on rotated files. compress: true + + nodeNetworkPolicy: + privilegedIngressRules: + - comment: allow ingress traffic to SSH + ports: + - 22 + protocol: tcp + - comment: allow ingress traffic to Kubernetes ports + ports: + - 6443 + - 10248:10250 + - 10256:10258 + protocol: tcp + - comment: allow ingress traffic to Antrea ports + ports: + - 10348:10351 + protocol: tcp + - comment: allow ingress traffic to Wireguard ports + ports: + - 51820:51821 + protocol: udp + - comment: allow ICMP echo-reply traffic + icmpType: 0 + protocol: icmp + - comment: allow ICMP echo-request traffic + icmpType: 8 + protocol: icmp + - comment: allow ICMPv6 echo-request traffic + icmpType: 128 + protocol: icmp6 + - comment: allow ICMPv6 echo-reply traffic + icmpType: 129 + protocol: icmp6 + - comment: allow IPv6 Neighbor Solicitation traffic + icmpType: 135 + protocol: icmp6 + - comment: allow IPv6 Neighbor Advertisement traffic + icmpType: 136 + protocol: icmp6 + privilegedEgressRules: + - comment: allow egress traffic to DNS port + ports: + - 53 + protocol: udp + - comment: allow egress traffic to DNS port + ports: + - 53 + protocol: tcp + - comment: allow egress traffic to HTTP/HTTPS port + ports: + - 80 + - 443 + protocol: tcp + - comment: allow egress traffic to Kubernetes DNS metric + ports: + - 9153 + protocol: tcp + - comment: allow egress traffic to Kubernetes ports + ports: + - 6443 + - 10248:10250 + - 10256:10258 + protocol: tcp + - comment: allow egress traffic to Antrea ports + ports: + - 10348:10351 + protocol: tcp + - comment: allow egress traffic to Wireguard ports + ports: + - 51820:51821 + protocol: udp + - comment: allow ICMP echo-reply traffic + icmpType: 0 + protocol: icmp + - comment: allow ICMP echo-request traffic + icmpType: 8 + protocol: icmp + - comment: allow ICMPv6 echo-request traffic + icmpType: 128 + protocol: icmp6 + - comment: allow ICMPv6 echo-reply traffic + icmpType: 129 + protocol: icmp6 + - comment: allow IPv6 Neighbor Solicitation traffic + icmpType: 135 + protocol: icmp6 + - comment: allow IPv6 Neighbor Advertisement traffic + icmpType: 136 + protocol: icmp6 antrea-cni.conflist: | { "cniVersion":"0.3.0", @@ -5975,6 +6067,9 @@ data: # set security postures for their clusters. # AdminNetworkPolicy: false + # Enable users to protect their Kubernetes Node. + # NodeNetworkPolicy: false + # The port for the antrea-controller APIServer to serve on. # Note that if it's set to another value, the `containerPort` of the `api` port of the # `antrea-controller` container must be set to the same value. @@ -6879,7 +6974,7 @@ spec: kubectl.kubernetes.io/default-container: antrea-agent # Automatically restart Pods with a RollingUpdate if the ConfigMap changes # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments - checksum/config: a34de3efa658ac40c9bde28e08832dd897259fdcf639beab9d4e47531d7da948 + checksum/config: 430e2b757c0c513eb4398f1f5ebb99d4409a8de7b12125d03d4763a51b4dab56 checksum/ipsec-secret: d0eb9c52d0cd4311b6d252a951126bf9bea27ec05590bed8a394f0f792dcb2a4 labels: app: antrea @@ -7164,7 +7259,7 @@ spec: annotations: # Automatically restart Pod if the ConfigMap changes # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments - checksum/config: a34de3efa658ac40c9bde28e08832dd897259fdcf639beab9d4e47531d7da948 + checksum/config: 430e2b757c0c513eb4398f1f5ebb99d4409a8de7b12125d03d4763a51b4dab56 labels: app: antrea component: antrea-controller diff --git a/build/yamls/antrea.yml b/build/yamls/antrea.yml index 3227814289f..4f789549fc6 100644 --- a/build/yamls/antrea.yml +++ b/build/yamls/antrea.yml @@ -5574,6 +5574,9 @@ data: # Enable Egress traffic shaping. # EgressTrafficShaping: false + # Enable users to protect their Kubernetes Node. + # NodeNetworkPolicy: false + # Name of the OpenVSwitch bridge antrea-agent will create and use. # Make sure it doesn't conflict with your existing OpenVSwitch bridges. ovsBridge: "br-int" @@ -5882,6 +5885,95 @@ data: maxAge: 28 # Compress enables gzip compression on rotated files. compress: true + + nodeNetworkPolicy: + privilegedIngressRules: + - comment: allow ingress traffic to SSH + ports: + - 22 + protocol: tcp + - comment: allow ingress traffic to Kubernetes ports + ports: + - 6443 + - 10248:10250 + - 10256:10258 + protocol: tcp + - comment: allow ingress traffic to Antrea ports + ports: + - 10348:10351 + protocol: tcp + - comment: allow ingress traffic to Wireguard ports + ports: + - 51820:51821 + protocol: udp + - comment: allow ICMP echo-reply traffic + icmpType: 0 + protocol: icmp + - comment: allow ICMP echo-request traffic + icmpType: 8 + protocol: icmp + - comment: allow ICMPv6 echo-request traffic + icmpType: 128 + protocol: icmp6 + - comment: allow ICMPv6 echo-reply traffic + icmpType: 129 + protocol: icmp6 + - comment: allow IPv6 Neighbor Solicitation traffic + icmpType: 135 + protocol: icmp6 + - comment: allow IPv6 Neighbor Advertisement traffic + icmpType: 136 + protocol: icmp6 + privilegedEgressRules: + - comment: allow egress traffic to DNS port + ports: + - 53 + protocol: udp + - comment: allow egress traffic to DNS port + ports: + - 53 + protocol: tcp + - comment: allow egress traffic to HTTP/HTTPS port + ports: + - 80 + - 443 + protocol: tcp + - comment: allow egress traffic to Kubernetes DNS metric + ports: + - 9153 + protocol: tcp + - comment: allow egress traffic to Kubernetes ports + ports: + - 6443 + - 10248:10250 + - 10256:10258 + protocol: tcp + - comment: allow egress traffic to Antrea ports + ports: + - 10348:10351 + protocol: tcp + - comment: allow egress traffic to Wireguard ports + ports: + - 51820:51821 + protocol: udp + - comment: allow ICMP echo-reply traffic + icmpType: 0 + protocol: icmp + - comment: allow ICMP echo-request traffic + icmpType: 8 + protocol: icmp + - comment: allow ICMPv6 echo-request traffic + icmpType: 128 + protocol: icmp6 + - comment: allow ICMPv6 echo-reply traffic + icmpType: 129 + protocol: icmp6 + - comment: allow IPv6 Neighbor Solicitation traffic + icmpType: 135 + protocol: icmp6 + - comment: allow IPv6 Neighbor Advertisement traffic + icmpType: 136 + protocol: icmp6 antrea-cni.conflist: | { "cniVersion":"0.3.0", @@ -5962,6 +6054,9 @@ data: # set security postures for their clusters. # AdminNetworkPolicy: false + # Enable users to protect their Kubernetes Node. + # NodeNetworkPolicy: false + # The port for the antrea-controller APIServer to serve on. # Note that if it's set to another value, the `containerPort` of the `api` port of the # `antrea-controller` container must be set to the same value. @@ -6866,7 +6961,7 @@ spec: kubectl.kubernetes.io/default-container: antrea-agent # Automatically restart Pods with a RollingUpdate if the ConfigMap changes # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments - checksum/config: aa947bf5c403412b9c8cfcbcc335659992f19bd428886e80f43bafa052bac1e6 + checksum/config: b7b901b351c4b293d88063e53ed5e5aacfd68dcaab5040c1f859202b6067d567 labels: app: antrea component: antrea-agent @@ -7105,7 +7200,7 @@ spec: annotations: # Automatically restart Pod if the ConfigMap changes # See https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments - checksum/config: aa947bf5c403412b9c8cfcbcc335659992f19bd428886e80f43bafa052bac1e6 + checksum/config: b7b901b351c4b293d88063e53ed5e5aacfd68dcaab5040c1f859202b6067d567 labels: app: antrea component: antrea-controller diff --git a/cmd/antrea-agent/agent.go b/cmd/antrea-agent/agent.go index d4464951921..962ab5c1d7f 100644 --- a/cmd/antrea-agent/agent.go +++ b/cmd/antrea-agent/agent.go @@ -138,6 +138,7 @@ func run(o *Options) error { enableAntreaIPAM := features.DefaultFeatureGate.Enabled(features.AntreaIPAM) enableBridgingMode := enableAntreaIPAM && o.config.EnableBridgingMode l7NetworkPolicyEnabled := features.DefaultFeatureGate.Enabled(features.L7NetworkPolicy) + nodeNetworkPolicyEnabled := features.DefaultFeatureGate.Enabled(features.NodeNetworkPolicy) enableMulticlusterGW := features.DefaultFeatureGate.Enabled(features.Multicluster) && o.config.Multicluster.EnableGateway enableMulticlusterNP := features.DefaultFeatureGate.Enabled(features.Multicluster) && o.config.Multicluster.EnableStretchedNetworkPolicy enableFlowExporter := features.DefaultFeatureGate.Enabled(features.FlowExporter) && o.config.FlowExporter.Enable @@ -217,7 +218,14 @@ func run(o *Options) error { egressConfig := &config.EgressConfig{ ExceptCIDRs: exceptCIDRs, } - routeClient, err := route.NewClient(networkConfig, o.config.NoSNAT, o.config.AntreaProxy.ProxyAll, connectUplinkToBridge, multicastEnabled, serviceCIDRProvider) + routeClient, err := route.NewClient(networkConfig, + o.config.NoSNAT, + o.config.AntreaProxy.ProxyAll, + connectUplinkToBridge, + nodeNetworkPolicyEnabled, + &o.config.NodeNetworkPolicy, + multicastEnabled, + serviceCIDRProvider) if err != nil { return fmt.Errorf("error creating route client: %v", err) } @@ -461,6 +469,7 @@ func run(o *Options) error { networkPolicyController, err := networkpolicy.NewNetworkPolicyController( antreaClientProvider, ofClient, + routeClient, ifaceStore, nodeKey, podUpdateChannel, @@ -469,6 +478,7 @@ func run(o *Options) error { groupIDUpdates, antreaPolicyEnabled, l7NetworkPolicyEnabled, + nodeNetworkPolicyEnabled, o.enableAntreaProxy, statusManagerEnabled, multicastEnabled, diff --git a/cmd/antrea-agent/options.go b/cmd/antrea-agent/options.go index 33d243611c5..d73d383e0f4 100644 --- a/cmd/antrea-agent/options.go +++ b/cmd/antrea-agent/options.go @@ -18,6 +18,7 @@ import ( "fmt" "net" "os" + "regexp" "strings" "time" @@ -612,6 +613,9 @@ func (o *Options) validateK8sNodeOptions() error { if err := o.validateSecondaryNetworkConfig(); err != nil { return fmt.Errorf("failed to validate secondary network config: %v", err) } + if err := o.validateNodeNetworkPolicyConfig(); err != nil { + return fmt.Errorf("failed to validate NodeNetworkPolicy config: %v", err) + } return nil } @@ -749,7 +753,7 @@ func (o *Options) validateNodePortLocalConfig() error { klog.InfoS("Feature gate `NodePortLocal` is deprecated, please use option `nodePortLocal.enable` to disable NodePortLocal") } if o.enableNodePortLocal { - startPort, endPort, err := parsePortRange(o.config.NodePortLocal.PortRange) + startPort, endPort, err := parsePortRange(o.config.NodePortLocal.PortRange, "-") if err != nil { return fmt.Errorf("NodePortLocal portRange is not valid: %v", err) } @@ -758,3 +762,52 @@ func (o *Options) validateNodePortLocalConfig() error { } return nil } + +func validateNodeNetworkPolicyPrivilegedRules(rules []agentconfig.PrivilegedRule) error { + if len(rules) == 0 { + return fmt.Errorf("no privileged rule is found. The absence of privileged rules may result in network unavailability in the Kubernetes cluster") + } + regex := regexp.MustCompile(`^\d+(?::\d+)?$`) + + for _, rule := range rules { + protocol := strings.ToLower(rule.Protocol) + switch protocol { + case "tcp": + fallthrough + case "udp": + fallthrough + case "sctp": + for _, portStr := range rule.Ports { + if regex.MatchString(portStr) { + if len(strings.Split(portStr, ":")) == 2 { + if _, _, err := parsePortRange(portStr, ":"); err != nil { + return fmt.Errorf("port range is not valid: %v", err) + } + } + } else { + return fmt.Errorf("invalid port pattern: %s", portStr) + } + } + case "icmp": + fallthrough + case "icmp6": + return nil + default: + return fmt.Errorf("unknown protocol: %s", rule.Protocol) + } + } + return nil +} + +func (o *Options) validateNodeNetworkPolicyConfig() error { + if !features.DefaultFeatureGate.Enabled(features.NodeNetworkPolicy) { + return nil + } + if err := validateNodeNetworkPolicyPrivilegedRules(o.config.NodeNetworkPolicy.PrivilegedIngressRules); err != nil { + return fmt.Errorf("failed to valid privileged ingress rules: %v", err) + } + if err := validateNodeNetworkPolicyPrivilegedRules(o.config.NodeNetworkPolicy.PrivilegedEgressRules); err != nil { + return fmt.Errorf("failed to valid privileged egress rules: %v", err) + } + return nil +} diff --git a/cmd/antrea-agent/options_linux_test.go b/cmd/antrea-agent/options_linux_test.go index 18bdff77634..2b1f36c63da 100644 --- a/cmd/antrea-agent/options_linux_test.go +++ b/cmd/antrea-agent/options_linux_test.go @@ -128,3 +128,121 @@ func TestMulticlusterOptions(t *testing.T) { }) } } + +func TestNodeNetworkPolicyOptions(t *testing.T) { + validTCP80 := agentconfig.PrivilegedRule{ + Protocol: "tcp", + Ports: []string{"80"}, + } + invalidPortRange := agentconfig.PrivilegedRule{ + Protocol: "tcp", + Ports: []string{"443", "80:79"}, + } + invalidPorts := agentconfig.PrivilegedRule{ + Protocol: "tcp", + Ports: []string{"80.80"}, + } + invalidPorts2 := agentconfig.PrivilegedRule{ + Protocol: "tcp", + Ports: []string{"80", "81-82"}, + } + unknownProtocol := agentconfig.PrivilegedRule{ + Protocol: "unknown", + } + validPorts := agentconfig.PrivilegedRule{ + Protocol: "tcp", + Ports: []string{"22", "80", "81", "12580:12590"}, + } + + tests := []struct { + name string + nodeNetworkPolicyConfig agentconfig.NodeNetworkPolicyConfig + featureGate bool + expectedErr string + }{ + { + name: "feature is not enabled", + nodeNetworkPolicyConfig: agentconfig.NodeNetworkPolicyConfig{}, + expectedErr: "", + }, + { + name: "no privileged egress rule", + nodeNetworkPolicyConfig: agentconfig.NodeNetworkPolicyConfig{ + PrivilegedIngressRules: []agentconfig.PrivilegedRule{validTCP80}, + }, + featureGate: true, + expectedErr: "no privileged rule is found. The absence of privileged rules may result in network unavailability in the Kubernetes cluster", + }, + { + name: "no privileged ingress rule", + nodeNetworkPolicyConfig: agentconfig.NodeNetworkPolicyConfig{ + PrivilegedEgressRules: []agentconfig.PrivilegedRule{validTCP80}, + }, + featureGate: true, + expectedErr: "no privileged rule is found. The absence of privileged rules may result in network unavailability in the Kubernetes cluster", + }, + { + name: "invalid port range", + nodeNetworkPolicyConfig: agentconfig.NodeNetworkPolicyConfig{ + PrivilegedIngressRules: []agentconfig.PrivilegedRule{invalidPortRange}, + PrivilegedEgressRules: []agentconfig.PrivilegedRule{validTCP80}, + }, + featureGate: true, + expectedErr: "start port must be smaller than end port", + }, + { + name: "invalid port pattern", + nodeNetworkPolicyConfig: agentconfig.NodeNetworkPolicyConfig{ + PrivilegedIngressRules: []agentconfig.PrivilegedRule{invalidPorts}, + PrivilegedEgressRules: []agentconfig.PrivilegedRule{validTCP80}, + }, + featureGate: true, + expectedErr: "invalid port pattern", + }, + { + name: "invalid port pattern", + nodeNetworkPolicyConfig: agentconfig.NodeNetworkPolicyConfig{ + PrivilegedIngressRules: []agentconfig.PrivilegedRule{invalidPorts2}, + PrivilegedEgressRules: []agentconfig.PrivilegedRule{validTCP80}, + }, + featureGate: true, + expectedErr: "invalid port pattern", + }, + { + name: "valid", + nodeNetworkPolicyConfig: agentconfig.NodeNetworkPolicyConfig{ + PrivilegedIngressRules: []agentconfig.PrivilegedRule{validPorts}, + PrivilegedEgressRules: []agentconfig.PrivilegedRule{validTCP80}, + }, + featureGate: true, + expectedErr: "", + }, + { + name: "unknown protocol", + nodeNetworkPolicyConfig: agentconfig.NodeNetworkPolicyConfig{ + PrivilegedIngressRules: []agentconfig.PrivilegedRule{unknownProtocol}, + PrivilegedEgressRules: []agentconfig.PrivilegedRule{validTCP80}, + }, + featureGate: true, + expectedErr: "unknown protocol", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config := &agentconfig.AgentConfig{ + FeatureGates: map[string]bool{"NodeNetworkPolicy": tt.featureGate}, + NodeNetworkPolicy: tt.nodeNetworkPolicyConfig, + } + o := &Options{config: config} + features.DefaultMutableFeatureGate.SetFromMap(o.config.FeatureGates) + o.setDefaults() + + err := o.validate(nil) + if tt.expectedErr == "" { + assert.NoError(t, err) + } else { + assert.ErrorContains(t, err, tt.expectedErr) + } + }) + } +} diff --git a/cmd/antrea-agent/util.go b/cmd/antrea-agent/util.go index 3bd34c87721..51518079aa6 100644 --- a/cmd/antrea-agent/util.go +++ b/cmd/antrea-agent/util.go @@ -58,9 +58,9 @@ func getAvailableNodePortAddresses(nodePortAddressesFromConfig []string, exclude return nodePortAddressesIPv4, nodePortAddressesIPv6, nil } -// parsePortRange parses a port range ("-") and checks that it is valid. -func parsePortRange(portRangeStr string) (start, end int, err error) { - portsRange := strings.Split(portRangeStr, "-") +// parsePortRange parses a port range (" ") and checks that it is valid. +func parsePortRange(portRangeStr string, sep string) (start, end int, err error) { + portsRange := strings.Split(portRangeStr, sep) if len(portsRange) != 2 { return 0, 0, fmt.Errorf("wrong port range format: %s", portRangeStr) } diff --git a/cmd/antrea-agent/util_test.go b/cmd/antrea-agent/util_test.go index c1c6ac0a1d5..65c195eb2a2 100644 --- a/cmd/antrea-agent/util_test.go +++ b/cmd/antrea-agent/util_test.go @@ -110,7 +110,7 @@ func TestParsePortRange(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - start, end, err := parsePortRange(tc.portRangeStr) + start, end, err := parsePortRange(tc.portRangeStr, "-") if tc.expectedErr != "" { assert.EqualError(t, err, tc.expectedErr) } else { diff --git a/docs/feature-gates.md b/docs/feature-gates.md index bec2c838fc1..c45988b7dcd 100644 --- a/docs/feature-gates.md +++ b/docs/feature-gates.md @@ -56,6 +56,7 @@ edit the Agent configuration in the | `L7NetworkPolicy` | Agent + Controller | `false` | Alpha | v1.10 | N/A | N/A | Yes | | | `AdminNetworkPolicy` | Controller | `false` | Alpha | v1.13 | N/A | N/A | Yes | | | `EgressTrafficShaping` | Agent | `false` | Alpha | v1.14 | N/A | N/A | Yes | OVS meters should be supported | +| `NodeNetworkPolicy` | Agent + Controller | `false` | Alpha | v1.15 | N/A | N/A | Yes | | ## Description and Requirements of Features @@ -404,6 +405,14 @@ this [document](antrea-l7-network-policy.md#prerequisites) for more information The `AdminNetworkPolicy` API (which currently includes the AdminNetworkPolicy and BaselineAdminNetworkPolicy objects) complements the Antrea-native policies and help cluster administrators to set security postures in a portable manner. +### NodeNetworkPolicy + +`NodeNetworkPolicy` enables users to protect their Kubernetes Nodes. + +#### Requirements for this Feature + +This feature is currently only supported for Nodes running Linux. + ### EgressTrafficShaping The `EgressTrafficShaping` feature gate of Antrea Agent enables traffic shaping of Egress, which could limit the diff --git a/hack/update-checksum-windows.sh b/hack/update-checksum-windows.sh index 43c8650671b..c6f8943bd1e 100755 --- a/hack/update-checksum-windows.sh +++ b/hack/update-checksum-windows.sh @@ -27,13 +27,13 @@ CONTAINERD_CONF_FILES="${WINDOWS_DIR}/containerd/conf/Install-WindowsCNI-Contain CONTAINERD_WITH_OVS_CONF_FILES="${WINDOWS_DIR}/containerd-with-ovs/conf/Run-AntreaOVS-Containerd.ps1 \ ${WINDOWS_DIR}/containerd-with-ovs/conf/VMSwitchExtension-AntreaAgent-Containerd.ps1" -checksum_windows_config=$(cat ${BASE_CONF_FILES} | sha256sum | cut -d " " -f 1) +checksum_windows_config=$(cat ${BASE_CONF_FILES} | shasum -a 256 | cut -d " " -f 1) -checksum_default=$(cat ${DEFAULT_CONF_FILES} | sha256sum | cut -d " " -f 1) +checksum_default=$(cat ${DEFAULT_CONF_FILES} | shasum -a 256 | cut -d " " -f 1) -checksum_containerd=$( cat ${CONTAINERD_CONF_FILES} | sha256sum | cut -d " " -f 1) +checksum_containerd=$( cat ${CONTAINERD_CONF_FILES} | shasum -a 256 | cut -d " " -f 1) -checksum_containerd_with_ovs=$(cat ${CONTAINERD_CONF_FILES} ${CONTAINERD_WITH_OVS_CONF_FILES} | sha256sum | cut -d " " -f 1) +checksum_containerd_with_ovs=$(cat ${CONTAINERD_CONF_FILES} ${CONTAINERD_WITH_OVS_CONF_FILES} | shasum -a 256 | cut -d " " -f 1) for file in ${MANIFESTS[@]}; do sed -i.bak "s/windows-config-checksum-placeholder/${checksum_windows_config}/g" ${file} diff --git a/pkg/agent/config/node_config.go b/pkg/agent/config/node_config.go index ebba7f3da9e..5a4ab531579 100644 --- a/pkg/agent/config/node_config.go +++ b/pkg/agent/config/node_config.go @@ -50,6 +50,11 @@ const ( L7NetworkPolicyReturnPortName = "antrea-l7-tap1" ) +const ( + NodeNetworkPolicyIngressRulesChain = "ANTREA-POL-INGRESS-RULES" + NodeNetworkPolicyEgressRulesChain = "ANTREA-POL-EGRESS-RULES" +) + var ( // VirtualServiceIPv4 or VirtualServiceIPv6 is used in the following scenarios: // - The IP is used to perform SNAT for packets of Service sourced from Antrea gateway and destined for external diff --git a/pkg/agent/controller/networkpolicy/cache.go b/pkg/agent/controller/networkpolicy/cache.go index efe4d254cb7..193adecec0d 100644 --- a/pkg/agent/controller/networkpolicy/cache.go +++ b/pkg/agent/controller/networkpolicy/cache.go @@ -48,7 +48,7 @@ const ( ) // rule is the struct stored in ruleCache, it contains necessary information -// to construct a complete rule that can be used by reconciler to enforce. +// to construct a complete rule that can be used by podReconciler to enforce. // The K8s NetworkPolicy object doesn't provide ID for its rule, here we // calculate an ID based on the rule's fields. That means: // 1. If a rule's selector/services/direction changes, it becomes "another" rule. @@ -141,7 +141,7 @@ func hashRule(r *rule) string { } // CompletedRule contains IPAddresses and Pods flattened from AddressGroups and AppliedToGroups. -// It's the struct used by reconciler. +// It's the struct used by podReconciler. type CompletedRule struct { *rule // Source GroupMembers of this rule, can't coexist with ToAddresses. @@ -182,8 +182,23 @@ func (r *CompletedRule) isIGMPEgressPolicyRule() bool { return false } +func (r *CompletedRule) isNodeNetworkPolicyRule() bool { + // TODO: remove this since this is only for testing. + + if strings.Contains(r.SourceRef.ToString(), "test-nnp-") { + fmt.Println("isNodeNetworkPolicy") + return true + } + for _, m := range r.TargetMembers { + if m.Node != nil { + return true + } + } + return false +} + // ruleCache caches Antrea AddressGroups, AppliedToGroups and NetworkPolicies, -// can construct complete rules that can be used by reconciler to enforce. +// can construct complete rules that can be used by podReconciler to enforce. type ruleCache struct { appliedToSetLock sync.RWMutex // appliedToSetByGroup stores the AppliedToGroup members. diff --git a/pkg/agent/controller/networkpolicy/fqdn.go b/pkg/agent/controller/networkpolicy/fqdn.go index 882eb2fa1c1..3fe75d586ea 100644 --- a/pkg/agent/controller/networkpolicy/fqdn.go +++ b/pkg/agent/controller/networkpolicy/fqdn.go @@ -93,7 +93,7 @@ type subscriber struct { } // ruleRealizationUpdate is a rule realization result reported by policy -// rule reconciler. +// rule podReconciler. type ruleRealizationUpdate struct { ruleId string err error @@ -103,7 +103,7 @@ type ruleRealizationUpdate struct { // applied to workloads on this Node. type ruleSyncTracker struct { mutex sync.RWMutex - // updateCh is the channel used by the rule reconciler to report rule realization status. + // updateCh is the channel used by the rule podReconciler to report rule realization status. updateCh chan ruleRealizationUpdate // ruleToSubscribers keeps track of the subscribers that are currently subscribed // to each dirty rule. Once an update of the rule realization status is received, @@ -575,7 +575,7 @@ func (rst *ruleSyncTracker) Run(stopCh <-chan struct{}) { } } -// notifyRuleUpdate is an interface for the reconciler to notify the ruleSyncTracker of a +// notifyRuleUpdate is an interface for the podReconciler to notify the ruleSyncTracker of a // rule realization status. func (f *fqdnController) notifyRuleUpdate(ruleID string, err error) { f.ruleSyncTracker.updateCh <- ruleRealizationUpdate{ruleID, err} diff --git a/pkg/agent/controller/networkpolicy/networkpolicy_controller.go b/pkg/agent/controller/networkpolicy/networkpolicy_controller.go index f25446e30d2..13f769017c5 100644 --- a/pkg/agent/controller/networkpolicy/networkpolicy_controller.go +++ b/pkg/agent/controller/networkpolicy/networkpolicy_controller.go @@ -38,6 +38,7 @@ import ( "antrea.io/antrea/pkg/agent/interfacestore" "antrea.io/antrea/pkg/agent/openflow" proxytypes "antrea.io/antrea/pkg/agent/proxy/types" + "antrea.io/antrea/pkg/agent/route" "antrea.io/antrea/pkg/agent/types" "antrea.io/antrea/pkg/apis/controlplane/v1beta2" "antrea.io/antrea/pkg/querier" @@ -69,19 +70,20 @@ type packetInAction func(*ofctrl.PacketIn) error // Controller is responsible for watching Antrea AddressGroups, AppliedToGroups, // and NetworkPolicies, feeding them to ruleCache, getting dirty rules from -// ruleCache, invoking reconciler to reconcile them. +// ruleCache, invoking podReconciler or nodeReconciler to reconcile them. // // a.Feed AddressGroups,AppliedToGroups // and NetworkPolicies -// |-----------| <-------- |----------- | c. Reconcile dirty rules |----------- | -// | ruleCache | | Controller | ------------> | reconciler | -// | ----------| --------> |----------- | |----------- | +// |-----------| <-------- |----------- | c. Reconcile dirty rules |-------------- | +// | ruleCache | | Controller | ------------> | podReconciler | +// | ----------| --------> |----------- | |-------------- | // b. Notify dirty rules type Controller struct { // antreaPolicyEnabled indicates whether Antrea NetworkPolicy and // ClusterNetworkPolicy are enabled. - antreaPolicyEnabled bool - l7NetworkPolicyEnabled bool + antreaPolicyEnabled bool + l7NetworkPolicyEnabled bool + nodeNetworkPolicyEnabled bool // antreaProxyEnabled indicates whether Antrea proxy is enabled. antreaProxyEnabled bool // statusManagerEnabled indicates whether a statusManager is configured. @@ -102,9 +104,12 @@ type Controller struct { queue workqueue.RateLimitingInterface // ruleCache maintains the desired state of NetworkPolicy rules. ruleCache *ruleCache - // reconciler provides interfaces to reconcile the desired state of + // podReconciler provides interfaces to reconcile the desired state of // NetworkPolicy rules with the actual state of Openflow entries. - reconciler Reconciler + podReconciler Reconciler + // nodeReconciler provides interfaces to reconcile the desired state of + // NetworkPolicy rules with the actual state of iptables entries. + nodeReconciler Reconciler // l7RuleReconciler provides interfaces to reconcile the desired state of // NetworkPolicy rules which have L7 rules with the actual state of Suricata rules. l7RuleReconciler L7RuleReconciler @@ -136,6 +141,7 @@ type Controller struct { // NewNetworkPolicyController returns a new *Controller. func NewNetworkPolicyController(antreaClientGetter agent.AntreaClientProvider, ofClient openflow.Client, + routeClient route.Interface, ifaceStore interfacestore.InterfaceStore, nodeName string, podUpdateSubscriber channel.Subscriber, @@ -144,6 +150,7 @@ func NewNetworkPolicyController(antreaClientGetter agent.AntreaClientProvider, groupIDUpdates <-chan string, antreaPolicyEnabled bool, l7NetworkPolicyEnabled bool, + nodeNetworkPolicyEnabled bool, antreaProxyEnabled bool, statusManagerEnabled bool, multicastEnabled bool, @@ -157,18 +164,19 @@ func NewNetworkPolicyController(antreaClientGetter agent.AntreaClientProvider, nodeConfig *config.NodeConfig) (*Controller, error) { idAllocator := newIDAllocator(asyncRuleDeleteInterval, dnsInterceptRuleID) c := &Controller{ - antreaClientProvider: antreaClientGetter, - queue: workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(minRetryDelay, maxRetryDelay), "networkpolicyrule"), - ofClient: ofClient, - nodeType: nodeType, - antreaPolicyEnabled: antreaPolicyEnabled, - l7NetworkPolicyEnabled: l7NetworkPolicyEnabled, - antreaProxyEnabled: antreaProxyEnabled, - statusManagerEnabled: statusManagerEnabled, - multicastEnabled: multicastEnabled, - gwPort: gwPort, - tunPort: tunPort, - nodeConfig: nodeConfig, + antreaClientProvider: antreaClientGetter, + queue: workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(minRetryDelay, maxRetryDelay), "networkpolicyrule"), + ofClient: ofClient, + nodeType: nodeType, + antreaPolicyEnabled: antreaPolicyEnabled, + l7NetworkPolicyEnabled: l7NetworkPolicyEnabled, + nodeNetworkPolicyEnabled: nodeNetworkPolicyEnabled, + antreaProxyEnabled: antreaProxyEnabled, + statusManagerEnabled: statusManagerEnabled, + multicastEnabled: multicastEnabled, + gwPort: gwPort, + tunPort: tunPort, + nodeConfig: nodeConfig, } if l7NetworkPolicyEnabled { @@ -186,8 +194,16 @@ func NewNetworkPolicyController(antreaClientGetter agent.AntreaClientProvider, c.ofClient.RegisterPacketInHandler(uint8(openflow.PacketInCategoryDNS), c.fqdnController) } } - c.reconciler = newReconciler(ofClient, ifaceStore, idAllocator, c.fqdnController, groupCounters, + c.podReconciler = newPodReconciler(ofClient, ifaceStore, idAllocator, c.fqdnController, groupCounters, v4Enabled, v6Enabled, antreaPolicyEnabled, multicastEnabled) + + if c.nodeNetworkPolicyEnabled { + var err error + c.nodeReconciler, err = newNodeReconciler(routeClient, v4Enabled, v6Enabled) + if err != nil { + return nil, err + } + } c.ruleCache = newRuleCache(c.enqueueRule, podUpdateSubscriber, externalEntityUpdateSubscriber, groupIDUpdates, nodeType) if statusManagerEnabled { c.statusManager = newStatusController(antreaClientGetter, nodeName, c.ruleCache) @@ -445,7 +461,7 @@ func (c *Controller) GetNetworkPolicyByRuleFlowID(ruleFlowID uint32) *v1beta2.Ne } func (c *Controller) GetRuleByFlowID(ruleFlowID uint32) *types.PolicyRule { - rule, exists, err := c.reconciler.GetRuleByFlowID(ruleFlowID) + rule, exists, err := c.podReconciler.GetRuleByFlowID(ruleFlowID) if err != nil { klog.Errorf("Error when getting network policy by rule flow ID: %v", err) return nil @@ -511,7 +527,7 @@ func (c *Controller) Run(stopCh <-chan struct{}) { } klog.Infof("Starting IDAllocator worker to maintain the async rule cache") - go c.reconciler.RunIDAllocatorWorker(stopCh) + go c.podReconciler.RunIDAllocatorWorker(stopCh) if c.statusManagerEnabled { go c.statusManager.Run(stopCh) @@ -621,9 +637,14 @@ func (c *Controller) syncRule(key string) error { rule, effective, realizable := c.ruleCache.GetCompletedRule(key) if !effective { klog.V(2).InfoS("Rule was not effective, removing it", "ruleID", key) - if err := c.reconciler.Forget(key); err != nil { + if err := c.podReconciler.Forget(key); err != nil { return err } + if c.nodeNetworkPolicyEnabled { + if err := c.nodeReconciler.Forget(key); err != nil { + return err + } + } if c.statusManagerEnabled { // We don't know whether this is a rule owned by Antrea Policy, but // harmless to delete it. @@ -646,7 +667,12 @@ func (c *Controller) syncRule(key string) error { return nil } - if c.l7NetworkPolicyEnabled && len(rule.L7Protocols) != 0 { + var isNodeNetworkPolicy bool + if c.nodeNetworkPolicyEnabled { + isNodeNetworkPolicy = rule.isNodeNetworkPolicyRule() + } + + if c.l7NetworkPolicyEnabled && len(rule.L7Protocols) != 0 && !isNodeNetworkPolicy { // Allocate VLAN ID for the L7 rule. vlanID := c.l7VlanIDAllocator.allocate(key) rule.L7RuleVlanID = &vlanID @@ -656,23 +682,30 @@ func (c *Controller) syncRule(key string) error { } } - err := c.reconciler.Reconcile(rule) - if c.fqdnController != nil { - // No matter whether the rule reconciliation succeeds or not, fqdnController - // needs to be notified of the status. - klog.V(2).InfoS("Rule realization was done", "ruleID", key) - c.fqdnController.notifyRuleUpdate(key, err) - } - if err != nil { - return err + if isNodeNetworkPolicy { + if err := c.nodeReconciler.Reconcile(rule); err != nil { + return err + } + } else { + err := c.podReconciler.Reconcile(rule) + if c.fqdnController != nil { + // No matter whether the rule reconciliation succeeds or not, fqdnController + // needs to be notified of the status. + klog.V(2).InfoS("Rule realization was done", "ruleID", key) + c.fqdnController.notifyRuleUpdate(key, err) + } + if err != nil { + return err + } } + if c.statusManagerEnabled && v1beta2.IsSourceAntreaNativePolicy(rule.SourceRef) { c.statusManager.SetRuleRealization(key, rule.PolicyUID) } return nil } -// syncRules calls the reconciler to sync all the rules after watchers complete full sync. +// syncRules calls the podReconciler to sync all the rules after watchers complete full sync. // After flows for those init events are installed, subsequent rules will be handled asynchronously // by the syncRule() function. func (c *Controller) syncRules(keys []string) error { @@ -681,7 +714,7 @@ func (c *Controller) syncRules(keys []string) error { klog.V(4).Infof("Finished syncing all rules before bookmark event (%v)", time.Since(startTime)) }() - var allRules []*CompletedRule + var allPodRules, allNodeRules []*CompletedRule for _, key := range keys { rule, effective, realizable := c.ruleCache.GetCompletedRule(key) // It's normal that a rule is not effective on this Node but abnormal that it is not realizable after watchers @@ -691,7 +724,11 @@ func (c *Controller) syncRules(keys []string) error { } else if !realizable { klog.Errorf("Rule %s is effective but not realizable", key) } else { - if c.l7NetworkPolicyEnabled && len(rule.L7Protocols) != 0 { + var isNodeNetworkPolicy bool + if c.nodeNetworkPolicyEnabled { + isNodeNetworkPolicy = rule.isNodeNetworkPolicyRule() + } + if c.l7NetworkPolicyEnabled && len(rule.L7Protocols) != 0 && !isNodeNetworkPolicy { // Allocate VLAN ID for the L7 rule. vlanID := c.l7VlanIDAllocator.allocate(key) rule.L7RuleVlanID = &vlanID @@ -700,14 +737,23 @@ func (c *Controller) syncRules(keys []string) error { return err } } - allRules = append(allRules, rule) + if isNodeNetworkPolicy { + allNodeRules = append(allNodeRules, rule) + } else { + allPodRules = append(allPodRules, rule) + } + } + } + if c.nodeNetworkPolicyEnabled { + if err := c.nodeReconciler.BatchReconcile(allNodeRules); err != nil { + return err } } - if err := c.reconciler.BatchReconcile(allRules); err != nil { + if err := c.podReconciler.BatchReconcile(allPodRules); err != nil { return err } if c.statusManagerEnabled { - for _, rule := range allRules { + for _, rule := range allPodRules { if v1beta2.IsSourceAntreaNativePolicy(rule.SourceRef) { c.statusManager.SetRuleRealization(rule.ID, rule.PolicyUID) } diff --git a/pkg/agent/controller/networkpolicy/networkpolicy_controller_test.go b/pkg/agent/controller/networkpolicy/networkpolicy_controller_test.go index d2179ce6a88..317352bf3c1 100644 --- a/pkg/agent/controller/networkpolicy/networkpolicy_controller_test.go +++ b/pkg/agent/controller/networkpolicy/networkpolicy_controller_test.go @@ -71,9 +71,9 @@ func newTestController() (*Controller, *fake.Clientset, *mockReconciler) { ch2 := make(chan string, 100) groupIDAllocator := openflow.NewGroupAllocator() groupCounters := []proxytypes.GroupCounter{proxytypes.NewGroupCounter(groupIDAllocator, ch2)} - controller, _ := NewNetworkPolicyController(&antreaClientGetter{clientset}, nil, nil, "node1", podUpdateChannel, nil, groupCounters, ch2, true, true, true, true, false, nil, testAsyncDeleteInterval, "8.8.8.8:53", config.K8sNode, true, false, config.HostGatewayOFPort, config.DefaultTunOFPort, &config.NodeConfig{}) + controller, _ := NewNetworkPolicyController(&antreaClientGetter{clientset}, nil, nil, nil, "node1", podUpdateChannel, nil, groupCounters, ch2, true, true, false, true, true, false, nil, testAsyncDeleteInterval, "8.8.8.8:53", config.K8sNode, true, false, config.HostGatewayOFPort, config.DefaultTunOFPort, &config.NodeConfig{}) reconciler := newMockReconciler() - controller.reconciler = reconciler + controller.podReconciler = reconciler controller.auditLogger = nil return controller, clientset, reconciler } @@ -513,7 +513,7 @@ func TestNetworkPolicyMetrics(t *testing.T) { metrics.InitializeNetworkPolicyMetrics() controller, clientset, reconciler := newTestController() - // Define functions to wait for a message from reconciler + // Define functions to wait for a message from podReconciler waitForReconcilerUpdated := func() { select { case ruleID := <-reconciler.updated: diff --git a/pkg/agent/controller/networkpolicy/node_reconciler_other.go b/pkg/agent/controller/networkpolicy/node_reconciler_other.go new file mode 100644 index 00000000000..dd325e9de27 --- /dev/null +++ b/pkg/agent/controller/networkpolicy/node_reconciler_other.go @@ -0,0 +1,711 @@ +//go:build !windows +// +build !windows + +// Copyright 2023 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package networkpolicy + +import ( + "container/list" + "fmt" + "net" + "sort" + "strings" + "sync" + + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/klog/v2" + + "antrea.io/antrea/pkg/agent/config" + "antrea.io/antrea/pkg/agent/route" + "antrea.io/antrea/pkg/agent/types" + "antrea.io/antrea/pkg/agent/util/iptables" + "antrea.io/antrea/pkg/apis/controlplane/v1beta2" + secv1beta1 "antrea.io/antrea/pkg/apis/crd/v1beta1" + "antrea.io/antrea/pkg/util/ip" +) + +const ( + ipsetPrefix = "ANTREA-POL" + iptablesChainPrefix = "ANTREA-POL" +) + +// ruleKey is a struct to identify a core iptables rule. +type ruleKey struct { + priority types.Priority + ruleID string +} + +type iptRulesCache struct { + keyList *list.List // keyList is a linked list to store + rules map[string]string + sync.Mutex +} + +type iptLastRealised struct { + // ipsetEntries tracks the last realized ipset members for the policy rule. + ipsetEntries map[bool]sets.Set[string] + // ipsetNames tracks the last realized names of ipsets for the policy rule. + ipsetNames map[bool]string + // serviceIPTChainName tracks the last realized iptables chains holding iptables + // rules for the policy rule's services. + serviceIPTChainName string + coreIPTChainName string + hasServices bool + priority *types.Priority +} + +func newIPTLastRealized() *iptLastRealised { + return &iptLastRealised{ + ipsetEntries: make(map[bool]sets.Set[string]), + ipsetNames: make(map[bool]string), + } +} + +type nodeReconciler struct { + ipProtocols []iptables.Protocol + routeClient route.Interface + cachedCoreIPTRulesMap map[string]*iptRulesCache + + // lastRealizeds caches the last realized rules. It's a mapping from ruleID to *CompletedRule. + lastRealizeds sync.Map +} + +func newIPTRulesCache() *iptRulesCache { + return &iptRulesCache{ + keyList: list.New(), + rules: make(map[string]string), + } +} + +func newNodeReconciler(routeClient route.Interface, ipv4Enabled, ipv6Enabled bool) (*nodeReconciler, error) { + var ipProtocols []iptables.Protocol + cachedCoreIPTRulesMap := make(map[string]*iptRulesCache) + if ipv4Enabled { + ipProtocols = append(ipProtocols, iptables.ProtocolIPv4) + cachedCoreIPTRulesMap[genCacheCategory(config.NodeNetworkPolicyIngressRulesChain, false)] = newIPTRulesCache() + cachedCoreIPTRulesMap[genCacheCategory(config.NodeNetworkPolicyEgressRulesChain, false)] = newIPTRulesCache() + } + if ipv6Enabled { + ipProtocols = append(ipProtocols, iptables.ProtocolIPv6) + cachedCoreIPTRulesMap[genCacheCategory(config.NodeNetworkPolicyIngressRulesChain, true)] = newIPTRulesCache() + cachedCoreIPTRulesMap[genCacheCategory(config.NodeNetworkPolicyEgressRulesChain, true)] = newIPTRulesCache() + } + return &nodeReconciler{ + ipProtocols: ipProtocols, + routeClient: routeClient, + cachedCoreIPTRulesMap: cachedCoreIPTRulesMap, + }, nil +} + +// Reconcile checks whether the provided rule have been enforced or not, and invoke the add or update method accordingly. +func (r *nodeReconciler) Reconcile(rule *CompletedRule) error { + klog.InfoS("Reconciling Node NetworkPolicy rule", "rule", rule.ID, "policy", rule.SourceRef.ToString()) + + value, exists := r.lastRealizeds.Load(rule.ID) + var err error + if !exists { + err = r.add(rule) + } else { + err = r.update(value.(*iptLastRealised), rule) + } + return err +} + +func (r *nodeReconciler) RunIDAllocatorWorker(stopCh <-chan struct{}) { + +} + +func (r *nodeReconciler) BatchReconcile(rules []*CompletedRule) error { + if len(rules) == 0 { + return nil + } + + var ingressRulesToInstall, egressRulesToInstall, rulesToInstall []*CompletedRule + for _, rule := range rules { + if _, exists := r.lastRealizeds.Load(rule.ID); exists { + klog.ErrorS(nil, "Rule should not have been realized yet: initialization phase", "rule", rule.ID) + } else { + rulesToInstall = append(rulesToInstall, rule) + if rule.Direction == v1beta2.DirectionIn { + ingressRulesToInstall = append(ingressRulesToInstall, rule) + } else { + egressRulesToInstall = append(egressRulesToInstall, rule) + } + } + } + + lastRealizeds := make(map[string]*iptLastRealised) + ruleIDToPriority := make(map[string]*types.Priority) + ruleIDToServiceIPTRuleChain := make(map[string]string) + ruleIDToCoreIPTRuleTarget := make(map[string]string) + for _, rule := range rulesToInstall { + ruleID := rule.ID + // Create the *iptLastRealised for every rule. + lastRealizeds[ruleID] = newIPTLastRealized() + // Generate priority for every rule. + ruleIDToPriority[ruleID] = genPriority(rule) + // If there are services in the rule, generate the iptables chains to install iptables rules related to the services. + // In that case, the target of the core iptables rule is the chain; if there is no service in the rule, the target + // of the core iptables rule is generated from the rule action. + if len(rule.Services) > 0 { + serviceIPTRuleChain := genServiceIPTRuleChain(ruleID) + ruleIDToServiceIPTRuleChain[ruleID] = serviceIPTRuleChain + ruleIDToCoreIPTRuleTarget[ruleID] = serviceIPTRuleChain + } else { + ruleIDToCoreIPTRuleTarget[ruleID] = genIPTRuleTargetFromRuleAction(rule.Action) + } + } + + for _, ipProtocol := range r.ipProtocols { + isIPv6 := iptables.IsIPv6Protocol(ipProtocol) + + ruleIDToIPSetName := make(map[string]string) + for _, rule := range rulesToInstall { + ruleID := rule.ID + + ipsetName := genIPSetName(ruleID, isIPv6) + ipsetEntries := getIPSetEntries(rule, isIPv6) + if err := r.routeClient.AddOrUpdateNodeNetworkPolicyIPSet(ipsetName, nil, ipsetEntries, isIPv6); err != nil { + return err + } + ruleIDToIPSetName[ruleID] = ipsetName + + // Store the ipset name and entries to lastRealized. + lastRealizeds[ruleID].ipsetNames[isIPv6] = ipsetName + lastRealizeds[ruleID].ipsetEntries[isIPv6] = ipsetEntries + } + + var allChainsToAdd []string + var allIPTRulesToAdd [][]string + // Get all iptables chains and iptables rules for services. + allServiceIPTRuleChainsToAdd, allServiceIPTRulesToAdd := buildAllServiceIPTRules(rulesToInstall, + ruleIDToServiceIPTRuleChain, + ipProtocol) + // Add the iptables chains and rules to `allChainsToAdd` and `allIPTRulesToAdd` respectively. + allChainsToAdd = allServiceIPTRuleChainsToAdd + allIPTRulesToAdd = allServiceIPTRulesToAdd + + if len(ingressRulesToInstall) > 0 { + // Get all rule keys and iptables rules for egress rules. Note that, `allSortedIngressRuleKeys` is a sorted list, but + // `allIngressCoreIPTRulesToAdd` is a map. `allIngressCoreIPTRulesToAdd` will be used as cache. + allSortedIngressRuleKeys, allIngressCoreIPTRulesToAdd := buildCoreIPTRules(ingressRulesToInstall, + ruleIDToPriority, + ruleIDToIPSetName, + ruleIDToCoreIPTRuleTarget, + config.NodeNetworkPolicyIngressRulesChain, + true) + // The core iptables rules for egress rules will be installed in chain ANTREA-POL-INGRESS-RULES, append the chain + // name to `allChainsToAdd`. + allChainsToAdd = append(allChainsToAdd, config.NodeNetworkPolicyIngressRulesChain) + // Get the sorted core iptables rules for egress rules and append it to `allIPTRulesToAdd`. + allIPTRulesToAdd = append(allIPTRulesToAdd, getSortedCoreIPTRules(allSortedIngressRuleKeys, allIngressCoreIPTRulesToAdd)) + + // TODO: whether need a lock. + // Store the sorted ingress rule keys into a linked list in order. + cachedCoreIPTRules := r.cachedCoreIPTRulesMap[genCacheCategory(config.NodeNetworkPolicyIngressRulesChain, isIPv6)] + cachedCoreIPTRules.keyList.Init() + for _, ruleKey := range allSortedIngressRuleKeys { + cachedCoreIPTRules.keyList.PushBack(ruleKey) + } + // Store all ingress core rules. + cachedCoreIPTRules.rules = allIngressCoreIPTRulesToAdd + } + + if len(egressRulesToInstall) > 0 { + // Get all rule keys and iptables rules for ingress rules. Note that, `allSortedEgressRuleKeys` is a sorted list, but + // `allEgressCoreIPTRulesToAdd` is a map. `allEgressCoreIPTRulesToAdd` will be used as cache. + allSortedEgressRuleKeys, allEgressCoreIPTRulesToAdd := buildCoreIPTRules(egressRulesToInstall, + ruleIDToPriority, + ruleIDToIPSetName, + ruleIDToCoreIPTRuleTarget, + config.NodeNetworkPolicyEgressRulesChain, + false) + // The core iptables rules for egress rules will be installed in chain ANTREA-POL-EGRESS-RULES, append the chain + // name to `allChainsToAdd`. + allChainsToAdd = append(allChainsToAdd, config.NodeNetworkPolicyEgressRulesChain) + // Get the sorted core iptables rules for egress rules and append it to `allIPTRulesToAdd`. + allIPTRulesToAdd = append(allIPTRulesToAdd, getSortedCoreIPTRules(allSortedEgressRuleKeys, allEgressCoreIPTRulesToAdd)) + + // TODO: whether need a lock. + // Store the sorted egress rule keys into a linked list in order. + cachedCoreIPTRules := r.cachedCoreIPTRulesMap[genCacheCategory(config.NodeNetworkPolicyEgressRulesChain, isIPv6)] + cachedCoreIPTRules.keyList.Init() + for _, ruleKey := range allSortedEgressRuleKeys { + cachedCoreIPTRules.keyList.PushBack(ruleKey) + } + // Store all egress core rules. + cachedCoreIPTRules.rules = allEgressCoreIPTRulesToAdd + } + + // Add all iptables chains and rules. + if err := r.routeClient.AddOrUpdateNodeNetworkPolicyIPTables(allChainsToAdd, allIPTRulesToAdd, isIPv6); err != nil { + return err + } + } + + // Update all lastRealizeds. + for _, rule := range rulesToInstall { + ruleID := rule.ID + lastRealized := lastRealizeds[ruleID] + lastRealized.priority = ruleIDToPriority[ruleID] + lastRealized.coreIPTChainName = getCoreIPTRuleChain(rule) + if serviceIPTChain, exists := ruleIDToServiceIPTRuleChain[ruleID]; exists { + lastRealized.hasServices = true + lastRealized.serviceIPTChainName = serviceIPTChain + } + r.lastRealizeds.Store(ruleID, lastRealized) + } + + return nil +} + +func (r *nodeReconciler) Forget(ruleID string) error { + klog.InfoS("Forgetting rule", "rule", ruleID) + + value, exists := r.lastRealizeds.Load(ruleID) + if !exists { + return nil + } + + lastRealized := value.(*iptLastRealised) + priority := lastRealized.priority + coreIPTChain := lastRealized.coreIPTChainName + + for _, ipProtocol := range r.ipProtocols { + isIPv6 := iptables.IsIPv6Protocol(ipProtocol) + + if err := r.deleteCoreIPRule(ruleID, priority, coreIPTChain, isIPv6); err != nil { + return err + } + if err := r.routeClient.DeleteNodeNetworkPolicyIPSet(lastRealized.ipsetNames[isIPv6], isIPv6); err != nil { + return err + } + if lastRealized.hasServices { + if err := r.routeClient.DeleteNodeNetworkPolicyIPTables([]string{lastRealized.serviceIPTChainName}, isIPv6); err != nil { + return err + } + } + } + + r.lastRealizeds.Delete(ruleID) + return nil +} + +func (r *nodeReconciler) GetRuleByFlowID(ruleFlowID uint32) (*types.PolicyRule, bool, error) { + return nil, false, nil +} + +func (r *nodeReconciler) update(lastRealized *iptLastRealised, rule *CompletedRule) error { + for _, ipProtocol := range r.ipProtocols { + isIPv6 := iptables.IsIPv6Protocol(ipProtocol) + + ipsetName := lastRealized.ipsetNames[isIPv6] + ipsetEntries := getIPSetEntries(rule, isIPv6) + pIPSetEntries := lastRealized.ipsetEntries[isIPv6] + if err := r.routeClient.AddOrUpdateNodeNetworkPolicyIPSet(ipsetName, pIPSetEntries, ipsetEntries, isIPv6); err != nil { + return err + } + lastRealized.ipsetEntries[isIPv6] = ipsetEntries + } + + r.lastRealizeds.Store(rule.ID, lastRealized) + return nil +} + +func (r *nodeReconciler) add(rule *CompletedRule) error { + ruleID := rule.ID + lastRealized := newIPTLastRealized() + + var serviceIPTRuleChain, serviceIPTRuleTarget, coreIPTRuleTarget string + if len(rule.Services) != 0 { + serviceIPTRuleChain = genServiceIPTRuleChain(ruleID) + serviceIPTRuleTarget = genIPTRuleTargetFromRuleAction(rule.Action) + coreIPTRuleTarget = serviceIPTRuleChain + } else { + coreIPTRuleTarget = genIPTRuleTargetFromRuleAction(rule.Action) + } + coreIPTRuleChain := getCoreIPTRuleChain(rule) + + coreIPTRuleComment := genCoreIPTRuleComment(ruleID, rule.SourceRef.ToString()) + isIngress := isIngressRule(rule) + priority := genPriority(rule) + + for _, ipProtocol := range r.ipProtocols { + isIPv6 := iptables.IsIPv6Protocol(ipProtocol) + + ipsetName := genIPSetName(ruleID, isIPv6) + ipsetEntries := getIPSetEntries(rule, isIPv6) + if err := r.routeClient.AddOrUpdateNodeNetworkPolicyIPSet(ipsetName, nil, ipsetEntries, isIPv6); err != nil { + return err + } + lastRealized.ipsetNames[isIPv6] = ipsetName + lastRealized.ipsetEntries[isIPv6] = ipsetEntries + + if len(rule.Services) > 0 { + serviceIPTRules := buildServiceIPTRules(ipProtocol, rule.Services, serviceIPTRuleChain, serviceIPTRuleTarget) + if err := r.routeClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{serviceIPTRuleChain}, [][]string{serviceIPTRules}, isIPv6); err != nil { + return err + } + } + + coreIPTRule := buildCoreIPTRule(coreIPTRuleChain, ipsetName, coreIPTRuleTarget, coreIPTRuleComment, isIngress) + if err := r.addCoreIPTRule(rule.ID, priority, coreIPTRuleChain, coreIPTRule, isIPv6); err != nil { + return err + } + } + + lastRealized.priority = priority + lastRealized.coreIPTChainName = coreIPTRuleChain + if len(rule.Services) > 0 { + lastRealized.hasServices = true + lastRealized.serviceIPTChainName = serviceIPTRuleChain + } + r.lastRealizeds.Store(rule.ID, lastRealized) + + return nil +} + +func (r *nodeReconciler) addCoreIPTRule(ruleID string, priority *types.Priority, chain string, rule string, isIPv6 bool) error { + // There are 4 categories of cached core iptables rules: + // - For IPv4, iptables rules installed in chain ANTREA-INGRESS-RULE for ingress policy rules. + // - For IPv6, ip6tables rules installed in chain ANTREA-INGRESS-RULE for ingress policy rules. + // - For IPv4, iptables rules installed in chain ANTREA-EGRESS-RULE for egress policy rules. + // - For IPv6, ip6tables rules installed in chain ANTREA-EGRESS-RULE for egress policy rules. + cachedCoreIPTRules := r.cachedCoreIPTRulesMap[genCacheCategory(chain, isIPv6)] + cachedCoreIPTRules.Lock() + defer cachedCoreIPTRules.Unlock() + + cachedRuleKeyList := cachedCoreIPTRules.keyList + cachedRules := cachedCoreIPTRules.rules + + // Generate a rule key with the current priority of the policy rule. + curRuleKey := ruleKey{ + priority: *priority, + ruleID: ruleID, + } + + // rules stores the latest iptables rules to sync. + var rules []string + + // KeyEleToRemove is the element storing stale rule key that should be removed. + var keyEleToMark *list.Element + + if cachedRuleKeyList.Len() == 0 { + // If there is no cached iptables rule, just append the iptables rule to add. + rules = append(rules, rule) + } else { + var ruleAdded bool + // Iterate all cached rule keys. + for keyEle := cachedRuleKeyList.Front(); keyEle != nil; keyEle = keyEle.Next() { + ruleKey := keyEle.Value.(ruleKey) + rulePriority := ruleKey.priority + // If the current iptables rule has not been added to the iptables rule list to be synced, and its corresponding + // priority is not less than the priority stored in the element, which means the location before the element + // is the appropriate place to insert the rule key for the current policy rule. + if !rulePriority.Less(*priority) && !ruleAdded { + rules = append(rules, rule) + // keyEleToMark is used to mark the element stored in the list where all rule keys are stored. The rule key + // generated from current priority will be inserted before this element. + keyEleToMark = keyEle + ruleAdded = true + } + // Get the cached iptables rule with its corresponding rule key and add it the iptables rule list to be synced. + rules = append(rules, cachedRules[ruleKey.ruleID]) + } + } + + // Sync the iptables rule list. + if err := r.routeClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{chain}, [][]string{rules}, isIPv6); err != nil { + return err + } + + // Insert the rule key for the current iptables rule before the marked element and store the current iptables rule. + if keyEleToMark == nil { + cachedRuleKeyList.PushBack(curRuleKey) + } else { + cachedRuleKeyList.InsertBefore(curRuleKey, keyEleToMark) + } + cachedRules[curRuleKey.ruleID] = rule + + return nil +} + +func (r *nodeReconciler) deleteCoreIPRule(ruleID string, priority *types.Priority, chain string, isIPv6 bool) error { + cachedCoreIPTRules := r.cachedCoreIPTRulesMap[genCacheCategory(chain, isIPv6)] + cachedCoreIPTRules.Lock() + defer cachedCoreIPTRules.Unlock() + + cachedRuleKeyList := cachedCoreIPTRules.keyList + cachedRules := cachedCoreIPTRules.rules + + ruleKeyToRemove := ruleKey{ + priority: *priority, + ruleID: ruleID, + } + + // rules stores the latest iptables rules to sync. + var rules []string + var keyEleToRemove *list.Element + for keyEle := cachedRuleKeyList.Front(); keyEle != nil; keyEle = keyEle.Next() { + // If the rule key stored in the element equals the previous rule key, it means this is a rule key for + // stale iptables rule, which should not be added to the iptables rule list to be synced. + ruleKey := keyEle.Value.(ruleKey) + if ruleKeyToRemove == ruleKey { + keyEleToRemove = keyEle + continue + } + // Get the cached iptables rule with its corresponding rule key and add it the iptables rule list to be synced. + rules = append(rules, cachedRules[ruleKey.ruleID]) + } + if err := r.routeClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{chain}, [][]string{rules}, isIPv6); err != nil { + return err + } + + if keyEleToRemove != nil { + cachedRuleKeyList.Remove(keyEleToRemove) + delete(cachedRules, ruleKeyToRemove.ruleID) + } + + return nil +} + +func genPriority(rule *CompletedRule) *types.Priority { + if rule == nil { + return nil + } + return &types.Priority{ + TierPriority: *rule.TierPriority, + PolicyPriority: *rule.PolicyPriority, + RulePriority: rule.Priority, + } +} + +func isIngressRule(rule *CompletedRule) bool { + if rule.Direction == v1beta2.DirectionIn { + return true + } + return false +} + +func groupMembersToIPNets(groups v1beta2.GroupMemberSet, isIPv6 bool) sets.Set[string] { + ipNets := sets.New[string]() + suffix := "/32" + if isIPv6 { + suffix = "/128" + } + for _, member := range groups { + for _, ip := range member.IPs { + ipAddr := net.IP(ip) + if isIPv6 && ipAddr.To4() == nil || !isIPv6 && ipAddr.To4() != nil { + ipNets.Insert(ipAddr.String() + suffix) + } + } + } + return ipNets +} + +func ipBlocksToIPNets(ipBlocks []v1beta2.IPBlock, isIPv6 bool) sets.Set[string] { + ipNets := sets.New[string]() + for _, b := range ipBlocks { + blockCIDR := ip.IPNetToNetIPNet(&b.CIDR) + // TODO: if remove this? + if !isIPNetSupportedByAF(blockCIDR, !isIPv6, isIPv6) { + // This is part of normal operations: "allow all" in a policy is represented + // by the combination of 2 CIDRs. One is "0.0.0.0/0" (any v4) and one is + // "::/0" (any v6). In single-stack clusters, one of these CIDRs is + // irrelevant and should be ignored. + klog.V(2).InfoS("IPBlock is using unsupported address family, skipping it", "cidr", blockCIDR.String()) + continue + } + if (blockCIDR.IP.To4() != nil && !isIPv6) || (blockCIDR.IP.To4() == nil && isIPv6) { + continue + } + exceptIPNets := make([]*net.IPNet, 0, len(b.Except)) + for i := range b.Except { + c := b.Except[i] + except := ip.IPNetToNetIPNet(&c) + exceptIPNets = append(exceptIPNets, except) + } + diffCIDRs, err := ip.DiffFromCIDRs(blockCIDR, exceptIPNets) + if err != nil { + klog.ErrorS(err, "Error when computing effective CIDRs by removing except IPNets from IPBlock") + continue + } + for _, d := range diffCIDRs { + ipNets.Insert(d.String()) + } + } + return ipNets +} + +func getIPSetEntries(rule *CompletedRule, isIPv6 bool) sets.Set[string] { + if rule == nil { + return nil + } + if rule.Direction == v1beta2.DirectionIn { + from1 := groupMembersToIPNets(rule.FromAddresses, isIPv6) + from2 := ipBlocksToIPNets(rule.From.IPBlocks, isIPv6) + return from1.Union(from2) + } else { + to1 := groupMembersToIPNets(rule.ToAddresses, isIPv6) + to2 := ipBlocksToIPNets(rule.To.IPBlocks, isIPv6) + return to1.Union(to2) + } +} + +func genIPSetName(ruleID string, isIPv6 bool) string { + suffix := "4" + if isIPv6 { + suffix = "6" + } + return fmt.Sprintf("%s-%s-%s", ipsetPrefix, strings.ToUpper(ruleID), suffix) +} + +func genCacheCategory(chain string, isIPv6 bool) string { + if isIPv6 { + return fmt.Sprintf("%s_6", chain) + } + return fmt.Sprintf("%s_4", chain) +} + +func getCoreIPTRuleChain(rule *CompletedRule) string { + if rule.Direction == v1beta2.DirectionIn { + return config.NodeNetworkPolicyIngressRulesChain + } + return config.NodeNetworkPolicyEgressRulesChain +} + +func genCoreIPTRuleComment(ruleID, policyName string) string { + return fmt.Sprintf("Antrea: for rule %s, policy %s", ruleID, policyName) +} + +func buildCoreIPTRule(chain, ipset, target, comment string, isIngress bool) string { + builder := iptables.NewRuleBuilder(chain) + if isIngress { + builder = builder.MatchIPSetSrc(ipset) + } else { + builder = builder.MatchIPSetDst(ipset) + } + return builder.SetTarget(target). + SetComment(comment). + Done(). + GetRule() +} + +func getSortedCoreIPTRules(ruleKeys []ruleKey, ruleMap map[string]string) []string { + var sortedRules []string + for _, ruleKey := range ruleKeys { + sortedRules = append(sortedRules, ruleMap[ruleKey.ruleID]) + } + return sortedRules +} + +func buildServiceIPTRules(ipProtocol iptables.Protocol, services []v1beta2.Service, chain string, target string) []string { + var rules []string + baseBuilder := iptables.NewRuleBuilder(chain) + for _, svc := range services { + var rule iptables.IPTablesRule + copiedBuilder := baseBuilder.CopyBuilder().SetTarget(target) + transProtocol := *svc.Protocol + switch transProtocol { + case v1beta2.ProtocolTCP: + rule = copiedBuilder.MatchTransProtocol(iptables.ProtocolTCP). + MatchSrcPort(svc.SrcPort, svc.SrcEndPort). + MatchDstPort(svc.Port, svc.EndPort). + Done() + case v1beta2.ProtocolUDP: + rule = copiedBuilder.MatchTransProtocol(iptables.ProtocolUDP). + MatchSrcPort(svc.SrcPort, svc.SrcEndPort). + MatchDstPort(svc.Port, svc.EndPort). + Done() + case v1beta2.ProtocolSCTP: + rule = copiedBuilder.MatchTransProtocol(iptables.ProtocolSCTP). + MatchSrcPort(svc.SrcPort, svc.SrcEndPort). + MatchDstPort(svc.Port, svc.EndPort). + Done() + case v1beta2.ProtocolICMP: + rule = copiedBuilder.MatchICMP(svc.ICMPType, svc.ICMPCode, ipProtocol). + Done() + } + rules = append(rules, rule.GetRule()) + } + return rules +} + +func buildAllServiceIPTRules(rules []*CompletedRule, serviceIPTRuleChains map[string]string, ipProtocol iptables.Protocol) ([]string, [][]string) { + var iptRules [][]string + var chains []string + for _, rule := range rules { + if serviceIPTRuleChain, exist := serviceIPTRuleChains[rule.ID]; exist { + serviceIPTRuleTarget := genIPTRuleTargetFromRuleAction(rule.Action) + iptRule := buildServiceIPTRules(ipProtocol, rule.Services, serviceIPTRuleChain, serviceIPTRuleTarget) + iptRules = append(iptRules, iptRule) + chains = append(chains, serviceIPTRuleChain) + } + } + return chains, iptRules +} + +func buildCoreIPTRules(rules []*CompletedRule, + priorities map[string]*types.Priority, + ipsetNameMap map[string]string, + ruleTargetMap map[string]string, + chain string, + isIngress bool) ([]ruleKey, map[string]string) { + var ruleKeys []ruleKey + iptRuleMap := make(map[string]string) + + for _, rule := range rules { + ruleID := rule.ID + + ipsetName := ipsetNameMap[ruleID] + iptRuleTarget := ruleTargetMap[ruleID] + iptRuleComment := genCoreIPTRuleComment(ruleID, rule.SourceRef.ToString()) + iptRule := buildCoreIPTRule(chain, ipsetName, iptRuleTarget, iptRuleComment, isIngress) + + ruleKey := ruleKey{ + priority: *priorities[ruleID], + ruleID: ruleID, + } + ruleKeys = append(ruleKeys, ruleKey) + iptRuleMap[ruleID] = iptRule + } + + sort.Slice(ruleKeys, func(i, j int) bool { + return ruleKeys[i].priority.Less(ruleKeys[j].priority) + }) + + return ruleKeys, iptRuleMap +} + +func genServiceIPTRuleChain(ruleID string) string { + return fmt.Sprintf("%s-%s", iptablesChainPrefix, strings.ToUpper(ruleID)) +} + +func genIPTRuleTargetFromRuleAction(ruleAction *secv1beta1.RuleAction) string { + var target string + switch *ruleAction { + case secv1beta1.RuleActionDrop: + target = iptables.DropTarget + case secv1beta1.RuleActionReject: + target = iptables.RejectTarget + case secv1beta1.RuleActionAllow: + target = iptables.AcceptTarget + default: + klog.InfoS("Unknown rule action", "action", ruleAction) + } + return target +} diff --git a/pkg/agent/controller/networkpolicy/node_reconciler_other_test.go b/pkg/agent/controller/networkpolicy/node_reconciler_other_test.go new file mode 100644 index 00000000000..53d38dde48d --- /dev/null +++ b/pkg/agent/controller/networkpolicy/node_reconciler_other_test.go @@ -0,0 +1,871 @@ +//go:build !windows +// +build !windows + +// Copyright 2023 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package networkpolicy + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "k8s.io/apimachinery/pkg/util/sets" + + routetest "antrea.io/antrea/pkg/agent/route/testing" + "antrea.io/antrea/pkg/apis/controlplane/v1beta2" + secv1beta1 "antrea.io/antrea/pkg/apis/crd/v1beta1" +) + +var ( + ruleActionAllow = secv1beta1.RuleActionAllow + + policyPriority1 = float64(1) + tierPriority1 = int32(1) + tierPriority2 = int32(2) + + ingressRuleID1 = "ingressRule1" + ingressRuleID2 = "ingressRule2" + ingressRuleID3 = "ingressRule3" + egressRuleID1 = "egressRule1" + egressRuleID2 = "egressRule2" + ingressRule1 = &CompletedRule{ + rule: &rule{ + ID: ingressRuleID1, + Name: "rule-01", + PolicyName: "ingress-policy", + Direction: v1beta2.DirectionIn, + Services: []v1beta2.Service{serviceTCP80, serviceTCP443}, + Action: &ruleActionAllow, + Priority: 1, + PolicyPriority: &policyPriority1, + TierPriority: &tierPriority1, + SourceRef: &cnp1, + }, + FromAddresses: dualAddressGroup1, + ToAddresses: nil, + } + updatedIngressRule1 = &CompletedRule{ + rule: &rule{ + ID: ingressRuleID1, + Name: "rule-01", + PolicyName: "ingress-policy", + Direction: v1beta2.DirectionIn, + Services: []v1beta2.Service{serviceTCP80, serviceTCP443}, + Action: &ruleActionAllow, + Priority: 1, + PolicyPriority: &policyPriority1, + TierPriority: &tierPriority1, + SourceRef: &cnp1, + }, + FromAddresses: addressGroup2, + ToAddresses: nil, + } + ingressRule2 = &CompletedRule{ + rule: &rule{ + ID: ingressRuleID2, + Name: "rule-02", + PolicyName: "ingress-policy", + Direction: v1beta2.DirectionIn, + Services: []v1beta2.Service{serviceTCP443}, + Action: &ruleActionAllow, + Priority: 2, + PolicyPriority: &policyPriority1, + TierPriority: &tierPriority2, + SourceRef: &cnp1, + }, + FromAddresses: dualAddressGroup1, + ToAddresses: nil, + } + ingressRule3 = &CompletedRule{ + rule: &rule{ + ID: ingressRuleID3, + Name: "rule-03", + PolicyName: "ingress-policy", + Direction: v1beta2.DirectionIn, + Services: []v1beta2.Service{serviceTCP8080}, + Action: &ruleActionAllow, + Priority: 1, + PolicyPriority: &policyPriority1, + TierPriority: &tierPriority2, + SourceRef: &cnp1, + }, + FromAddresses: dualAddressGroup1, + ToAddresses: nil, + } + egressRule1 = &CompletedRule{ + rule: &rule{ + ID: egressRuleID1, + Name: "rule-01", + PolicyName: "egress-policy", + Direction: v1beta2.DirectionOut, + Services: []v1beta2.Service{serviceTCP80, serviceTCP443}, + Action: &ruleActionAllow, + Priority: 1, + PolicyPriority: &policyPriority1, + TierPriority: &tierPriority1, + SourceRef: &cnp1, + }, + ToAddresses: dualAddressGroup1, + FromAddresses: nil, + } + egressRule2 = &CompletedRule{ + rule: &rule{ + ID: egressRuleID2, + Name: "rule-02", + PolicyName: "egress-policy", + Direction: v1beta2.DirectionOut, + Services: []v1beta2.Service{serviceTCP443}, + Action: &ruleActionAllow, + Priority: 2, + PolicyPriority: &policyPriority1, + TierPriority: &tierPriority2, + SourceRef: &cnp1, + }, + ToAddresses: dualAddressGroup1, + FromAddresses: nil, + } +) + +func newTestNodeReconciler(mockRouteClient *routetest.MockInterface, ipv4Enabled, ipv6Enabled bool) *nodeReconciler { + r, _ := newNodeReconciler(mockRouteClient, ipv4Enabled, ipv6Enabled) + return r +} + +func TestNodeReconcilerReconcileAndForget(t *testing.T) { + tests := []struct { + name string + rulesToAdd []*CompletedRule + rulesToForget []string + ipv4Enabled bool + ipv6Enabled bool + expectedCalls func(mockRouteClient *routetest.MockInterfaceMockRecorder) + }{ + { + name: "IPv4, add an ingress rule then forget it", + ipv4Enabled: true, + ipv6Enabled: false, + expectedCalls: func(mockRouteClient *routetest.MockInterfaceMockRecorder) { + serviceRules := [][]string{ + { + "-A ANTREA-POL-INGRESSRULE1 -j ACCEPT -p tcp --dport 80", + "-A ANTREA-POL-INGRESSRULE1 -j ACCEPT -p tcp --dport 443", + }, + } + coreRules := [][]string{ + { + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE1-4", nil, sets.New[string]("1.1.1.1/32"), false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESSRULE1"}, serviceRules, false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESS-RULES"}, coreRules, false).Times(1) + + mockRouteClient.DeleteNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE1-4", false) + mockRouteClient.DeleteNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESSRULE1"}, false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESS-RULES"}, [][]string{nil}, false).Times(1) + }, + rulesToAdd: []*CompletedRule{ + ingressRule1, + }, + rulesToForget: []string{ + ingressRuleID1, + }, + }, + { + name: "IPv6, add an egress rule and forget it", + ipv4Enabled: false, + ipv6Enabled: true, + expectedCalls: func(mockRouteClient *routetest.MockInterfaceMockRecorder) { + serviceRules := [][]string{ + { + "-A ANTREA-POL-EGRESSRULE1 -j ACCEPT -p tcp --dport 80", + "-A ANTREA-POL-EGRESSRULE1 -j ACCEPT -p tcp --dport 443", + }, + } + coreRules := [][]string{ + { + `-A ANTREA-POL-EGRESS-RULES -m set --match-set ANTREA-POL-EGRESSRULE1-6 dst -j ANTREA-POL-EGRESSRULE1 -m comment --comment "Antrea: for rule egressRule1, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-EGRESSRULE1-6", nil, sets.New[string]("2002:1a23:fb44::1/128"), true).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-EGRESSRULE1"}, serviceRules, true).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-EGRESS-RULES"}, coreRules, true).Times(1) + + mockRouteClient.DeleteNodeNetworkPolicyIPSet("ANTREA-POL-EGRESSRULE1-6", true) + mockRouteClient.DeleteNodeNetworkPolicyIPTables([]string{"ANTREA-POL-EGRESSRULE1"}, true).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-EGRESS-RULES"}, [][]string{nil}, true).Times(1) + }, + rulesToAdd: []*CompletedRule{ + egressRule1, + }, + rulesToForget: []string{ + egressRuleID1, + }, + }, + { + name: "Dualstack, add an ingress rule and forget it", + ipv4Enabled: true, + ipv6Enabled: true, + expectedCalls: func(mockRouteClient *routetest.MockInterfaceMockRecorder) { + serviceRules := [][]string{ + { + "-A ANTREA-POL-INGRESSRULE1 -j ACCEPT -p tcp --dport 80", + "-A ANTREA-POL-INGRESSRULE1 -j ACCEPT -p tcp --dport 443", + }, + } + coreRulesIPv4 := [][]string{ + { + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + coreRulesIPv6 := [][]string{ + { + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-6 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE1-4", nil, sets.New[string]("1.1.1.1/32"), false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESSRULE1"}, serviceRules, false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESS-RULES"}, coreRulesIPv4, false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE1-6", nil, sets.New[string]("2002:1a23:fb44::1/128"), true).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESSRULE1"}, serviceRules, true).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESS-RULES"}, coreRulesIPv6, true).Times(1) + + mockRouteClient.DeleteNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE1-4", false) + mockRouteClient.DeleteNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESSRULE1"}, false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESS-RULES"}, [][]string{nil}, false).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE1-6", true) + mockRouteClient.DeleteNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESSRULE1"}, true).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESS-RULES"}, [][]string{nil}, true).Times(1) + }, + rulesToAdd: []*CompletedRule{ + ingressRule1, + }, + rulesToForget: []string{ + ingressRuleID1, + }, + }, + { + name: "IPv4, add multiple ingress rules, then forget some", + ipv4Enabled: true, + ipv6Enabled: false, + expectedCalls: func(mockRouteClient *routetest.MockInterfaceMockRecorder) { + serviceRules1 := [][]string{ + { + "-A ANTREA-POL-INGRESSRULE1 -j ACCEPT -p tcp --dport 80", + "-A ANTREA-POL-INGRESSRULE1 -j ACCEPT -p tcp --dport 443", + }, + } + serviceRules2 := [][]string{ + { + "-A ANTREA-POL-INGRESSRULE2 -j ACCEPT -p tcp --dport 443", + }, + } + serviceRules3 := [][]string{ + { + "-A ANTREA-POL-INGRESSRULE3 -j ACCEPT -p tcp --dport 8080", + }, + } + coreRules1 := [][]string{ + { + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + coreRules2 := [][]string{ + { + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE2-4 src -j ANTREA-POL-INGRESSRULE2 -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`, + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + coreRules3 := [][]string{ + { + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE2-4 src -j ANTREA-POL-INGRESSRULE2 -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`, + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE3-4 src -j ANTREA-POL-INGRESSRULE3 -m comment --comment "Antrea: for rule ingressRule3, policy AntreaClusterNetworkPolicy:name1"`, + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + coreRules4 := [][]string{ + { + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE2-4 src -j ANTREA-POL-INGRESSRULE2 -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`, + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + coreRules5 := [][]string{ + { + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE1-4", nil, sets.New[string]("1.1.1.1/32"), false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESSRULE1"}, serviceRules1, false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESS-RULES"}, coreRules1, false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE2-4", nil, sets.New[string]("1.1.1.1/32"), false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESSRULE2"}, serviceRules2, false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESS-RULES"}, coreRules2, false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE3-4", nil, sets.New[string]("1.1.1.1/32"), false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESSRULE3"}, serviceRules3, false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESS-RULES"}, coreRules3, false).Times(1) + + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESS-RULES"}, coreRules4, false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESS-RULES"}, coreRules5, false).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE3-4", false).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE2-4", false).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESSRULE3"}, false).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESSRULE2"}, false).Times(1) + }, + rulesToAdd: []*CompletedRule{ + ingressRule1, + ingressRule2, + ingressRule3, + }, + rulesToForget: []string{ + ingressRuleID3, + ingressRuleID2, + }, + }, + { + name: "IPv4, add an ingress rule, update it and forget it", + ipv4Enabled: true, + ipv6Enabled: false, + expectedCalls: func(mockRouteClient *routetest.MockInterfaceMockRecorder) { + serviceRules := [][]string{ + { + "-A ANTREA-POL-INGRESSRULE1 -j ACCEPT -p tcp --dport 80", + "-A ANTREA-POL-INGRESSRULE1 -j ACCEPT -p tcp --dport 443", + }, + } + coreRules := [][]string{ + { + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE1-4", nil, sets.New[string]("1.1.1.1/32"), false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESSRULE1"}, serviceRules, false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESS-RULES"}, coreRules, false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE1-4", sets.New[string]("1.1.1.1/32"), sets.New[string]("1.1.1.2/32"), false).Times(1) + + mockRouteClient.DeleteNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE1-4", false) + mockRouteClient.DeleteNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESSRULE1"}, false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESS-RULES"}, [][]string{nil}, false).Times(1) + }, + rulesToAdd: []*CompletedRule{ + ingressRule1, + updatedIngressRule1, + }, + rulesToForget: []string{ + ingressRuleID1, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + controller := gomock.NewController(t) + mockRouteClient := routetest.NewMockInterface(controller) + r := newTestNodeReconciler(mockRouteClient, tt.ipv4Enabled, tt.ipv6Enabled) + + tt.expectedCalls(mockRouteClient.EXPECT()) + for _, rule := range tt.rulesToAdd { + assert.NoError(t, r.Reconcile(rule)) + } + for _, rule := range tt.rulesToForget { + assert.NoError(t, r.Forget(rule)) + } + }) + } +} + +func TestNodeReconcilerBatchReconcileAndForget(t *testing.T) { + tests := []struct { + name string + ipv4Enabled bool + ipv6Enabled bool + rulesToAdd []*CompletedRule + rulesToForget []string + expectedCalls func(mockRouteClient *routetest.MockInterfaceMockRecorder) + }{ + { + name: "IPv4, only add ingress rules and forget one", + ipv4Enabled: true, + rulesToAdd: []*CompletedRule{ + ingressRule1, + ingressRule2, + }, + rulesToForget: []string{ + ingressRuleID1, + }, + expectedCalls: func(mockRouteClient *routetest.MockInterfaceMockRecorder) { + chains := []string{ + "ANTREA-POL-INGRESSRULE1", + "ANTREA-POL-INGRESSRULE2", + "ANTREA-POL-INGRESS-RULES", + } + rules1 := [][]string{ + { + "-A ANTREA-POL-INGRESSRULE1 -j ACCEPT -p tcp --dport 80", + "-A ANTREA-POL-INGRESSRULE1 -j ACCEPT -p tcp --dport 443", + }, + { + "-A ANTREA-POL-INGRESSRULE2 -j ACCEPT -p tcp --dport 443", + }, + { + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE2-4 src -j ANTREA-POL-INGRESSRULE2 -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`, + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + + rules2 := [][]string{ + { + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE2-4 src -j ANTREA-POL-INGRESSRULE2 -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE1-4", nil, sets.New[string]("1.1.1.1/32"), false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE2-4", nil, sets.New[string]("1.1.1.1/32"), false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables(chains, rules1, false).Times(1) + + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESS-RULES"}, rules2, false).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE1-4", false).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESSRULE1"}, false).Times(1) + }, + }, + { + name: "IPv6, only add ingress rules and forget one", + ipv6Enabled: true, + rulesToAdd: []*CompletedRule{ + ingressRule1, + ingressRule2, + }, + rulesToForget: []string{ + ingressRuleID2, + }, + expectedCalls: func(mockRouteClient *routetest.MockInterfaceMockRecorder) { + chains := []string{ + "ANTREA-POL-INGRESSRULE1", + "ANTREA-POL-INGRESSRULE2", + "ANTREA-POL-INGRESS-RULES", + } + rules1 := [][]string{ + { + "-A ANTREA-POL-INGRESSRULE1 -j ACCEPT -p tcp --dport 80", + "-A ANTREA-POL-INGRESSRULE1 -j ACCEPT -p tcp --dport 443", + }, + { + "-A ANTREA-POL-INGRESSRULE2 -j ACCEPT -p tcp --dport 443", + }, + { + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE2-6 src -j ANTREA-POL-INGRESSRULE2 -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`, + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-6 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + + rules2 := [][]string{ + { + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-6 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE1-6", nil, sets.New[string]("2002:1a23:fb44::1/128"), true).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE2-6", nil, sets.New[string]("2002:1a23:fb44::1/128"), true).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables(chains, rules1, true).Times(1) + + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESS-RULES"}, rules2, true).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE2-6", true).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESSRULE2"}, true).Times(1) + }, + }, + { + name: "dualstack, only add ingress rules and forget one", + ipv4Enabled: true, + ipv6Enabled: true, + rulesToAdd: []*CompletedRule{ + ingressRule1, + ingressRule2, + }, + rulesToForget: []string{ + ingressRuleID1, + }, + expectedCalls: func(mockRouteClient *routetest.MockInterfaceMockRecorder) { + chains := []string{ + "ANTREA-POL-INGRESSRULE1", + "ANTREA-POL-INGRESSRULE2", + "ANTREA-POL-INGRESS-RULES", + } + ipv4Rules1 := [][]string{ + { + "-A ANTREA-POL-INGRESSRULE1 -j ACCEPT -p tcp --dport 80", + "-A ANTREA-POL-INGRESSRULE1 -j ACCEPT -p tcp --dport 443", + }, + { + "-A ANTREA-POL-INGRESSRULE2 -j ACCEPT -p tcp --dport 443", + }, + { + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE2-4 src -j ANTREA-POL-INGRESSRULE2 -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`, + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + ipv6Rules1 := [][]string{ + { + "-A ANTREA-POL-INGRESSRULE1 -j ACCEPT -p tcp --dport 80", + "-A ANTREA-POL-INGRESSRULE1 -j ACCEPT -p tcp --dport 443", + }, + { + "-A ANTREA-POL-INGRESSRULE2 -j ACCEPT -p tcp --dport 443", + }, + { + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE2-6 src -j ANTREA-POL-INGRESSRULE2 -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`, + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-6 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + + ipv4Rules2 := [][]string{ + { + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE2-4 src -j ANTREA-POL-INGRESSRULE2 -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + ipv6Rules2 := [][]string{ + { + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE2-6 src -j ANTREA-POL-INGRESSRULE2 -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE1-4", nil, sets.New[string]("1.1.1.1/32"), false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE2-4", nil, sets.New[string]("1.1.1.1/32"), false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE1-6", nil, sets.New[string]("2002:1a23:fb44::1/128"), true).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE2-6", nil, sets.New[string]("2002:1a23:fb44::1/128"), true).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables(chains, ipv4Rules1, false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables(chains, ipv6Rules1, true).Times(1) + + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESS-RULES"}, ipv4Rules2, false).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE1-4", false).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESSRULE1"}, false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESS-RULES"}, ipv6Rules2, true).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE1-6", true).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESSRULE1"}, true).Times(1) + }, + }, + { + name: "IPv4, only add egress rules and forget one", + ipv4Enabled: true, + rulesToAdd: []*CompletedRule{ + egressRule1, + egressRule2, + }, + rulesToForget: []string{ + egressRuleID1, + }, + expectedCalls: func(mockRouteClient *routetest.MockInterfaceMockRecorder) { + chains := []string{ + "ANTREA-POL-EGRESSRULE1", + "ANTREA-POL-EGRESSRULE2", + "ANTREA-POL-EGRESS-RULES", + } + rules1 := [][]string{ + { + "-A ANTREA-POL-EGRESSRULE1 -j ACCEPT -p tcp --dport 80", + "-A ANTREA-POL-EGRESSRULE1 -j ACCEPT -p tcp --dport 443", + }, + { + "-A ANTREA-POL-EGRESSRULE2 -j ACCEPT -p tcp --dport 443", + }, + { + `-A ANTREA-POL-EGRESS-RULES -m set --match-set ANTREA-POL-EGRESSRULE2-4 dst -j ANTREA-POL-EGRESSRULE2 -m comment --comment "Antrea: for rule egressRule2, policy AntreaClusterNetworkPolicy:name1"`, + `-A ANTREA-POL-EGRESS-RULES -m set --match-set ANTREA-POL-EGRESSRULE1-4 dst -j ANTREA-POL-EGRESSRULE1 -m comment --comment "Antrea: for rule egressRule1, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + + rules2 := [][]string{ + { + `-A ANTREA-POL-EGRESS-RULES -m set --match-set ANTREA-POL-EGRESSRULE2-4 dst -j ANTREA-POL-EGRESSRULE2 -m comment --comment "Antrea: for rule egressRule2, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-EGRESSRULE1-4", nil, sets.New[string]("1.1.1.1/32"), false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-EGRESSRULE2-4", nil, sets.New[string]("1.1.1.1/32"), false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables(chains, rules1, false).Times(1) + + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-EGRESS-RULES"}, rules2, false).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPSet("ANTREA-POL-EGRESSRULE1-4", false).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPTables([]string{"ANTREA-POL-EGRESSRULE1"}, false).Times(1) + }, + }, + { + name: "IPv6, only add egress rules and forget one", + ipv6Enabled: true, + rulesToAdd: []*CompletedRule{ + egressRule1, + egressRule2, + }, + rulesToForget: []string{ + egressRuleID1, + }, + expectedCalls: func(mockRouteClient *routetest.MockInterfaceMockRecorder) { + chains := []string{ + "ANTREA-POL-EGRESSRULE1", + "ANTREA-POL-EGRESSRULE2", + "ANTREA-POL-EGRESS-RULES", + } + rules1 := [][]string{ + { + "-A ANTREA-POL-EGRESSRULE1 -j ACCEPT -p tcp --dport 80", + "-A ANTREA-POL-EGRESSRULE1 -j ACCEPT -p tcp --dport 443", + }, + { + "-A ANTREA-POL-EGRESSRULE2 -j ACCEPT -p tcp --dport 443", + }, + { + `-A ANTREA-POL-EGRESS-RULES -m set --match-set ANTREA-POL-EGRESSRULE2-6 dst -j ANTREA-POL-EGRESSRULE2 -m comment --comment "Antrea: for rule egressRule2, policy AntreaClusterNetworkPolicy:name1"`, + `-A ANTREA-POL-EGRESS-RULES -m set --match-set ANTREA-POL-EGRESSRULE1-6 dst -j ANTREA-POL-EGRESSRULE1 -m comment --comment "Antrea: for rule egressRule1, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + + rules2 := [][]string{ + { + `-A ANTREA-POL-EGRESS-RULES -m set --match-set ANTREA-POL-EGRESSRULE2-6 dst -j ANTREA-POL-EGRESSRULE2 -m comment --comment "Antrea: for rule egressRule2, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-EGRESSRULE1-6", nil, sets.New[string]("2002:1a23:fb44::1/128"), true).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-EGRESSRULE2-6", nil, sets.New[string]("2002:1a23:fb44::1/128"), true).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables(chains, rules1, true).Times(1) + + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-EGRESS-RULES"}, rules2, true).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPSet("ANTREA-POL-EGRESSRULE1-6", true).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPTables([]string{"ANTREA-POL-EGRESSRULE1"}, true).Times(1) + }, + }, + { + name: "dualstack, only add egress rules and forget one", + ipv4Enabled: true, + ipv6Enabled: true, + rulesToAdd: []*CompletedRule{ + egressRule1, + egressRule2, + }, + rulesToForget: []string{ + egressRuleID1, + }, + expectedCalls: func(mockRouteClient *routetest.MockInterfaceMockRecorder) { + chains := []string{ + "ANTREA-POL-EGRESSRULE1", + "ANTREA-POL-EGRESSRULE2", + "ANTREA-POL-EGRESS-RULES", + } + ipv4Rules1 := [][]string{ + { + "-A ANTREA-POL-EGRESSRULE1 -j ACCEPT -p tcp --dport 80", + "-A ANTREA-POL-EGRESSRULE1 -j ACCEPT -p tcp --dport 443", + }, + { + "-A ANTREA-POL-EGRESSRULE2 -j ACCEPT -p tcp --dport 443", + }, + { + `-A ANTREA-POL-EGRESS-RULES -m set --match-set ANTREA-POL-EGRESSRULE2-4 dst -j ANTREA-POL-EGRESSRULE2 -m comment --comment "Antrea: for rule egressRule2, policy AntreaClusterNetworkPolicy:name1"`, + `-A ANTREA-POL-EGRESS-RULES -m set --match-set ANTREA-POL-EGRESSRULE1-4 dst -j ANTREA-POL-EGRESSRULE1 -m comment --comment "Antrea: for rule egressRule1, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + ipv6Rules1 := [][]string{ + { + "-A ANTREA-POL-EGRESSRULE1 -j ACCEPT -p tcp --dport 80", + "-A ANTREA-POL-EGRESSRULE1 -j ACCEPT -p tcp --dport 443", + }, + { + "-A ANTREA-POL-EGRESSRULE2 -j ACCEPT -p tcp --dport 443", + }, + { + `-A ANTREA-POL-EGRESS-RULES -m set --match-set ANTREA-POL-EGRESSRULE2-6 dst -j ANTREA-POL-EGRESSRULE2 -m comment --comment "Antrea: for rule egressRule2, policy AntreaClusterNetworkPolicy:name1"`, + `-A ANTREA-POL-EGRESS-RULES -m set --match-set ANTREA-POL-EGRESSRULE1-6 dst -j ANTREA-POL-EGRESSRULE1 -m comment --comment "Antrea: for rule egressRule1, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + ipv4Rules2 := [][]string{ + { + `-A ANTREA-POL-EGRESS-RULES -m set --match-set ANTREA-POL-EGRESSRULE2-4 dst -j ANTREA-POL-EGRESSRULE2 -m comment --comment "Antrea: for rule egressRule2, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + ipv6Rules2 := [][]string{ + { + `-A ANTREA-POL-EGRESS-RULES -m set --match-set ANTREA-POL-EGRESSRULE2-6 dst -j ANTREA-POL-EGRESSRULE2 -m comment --comment "Antrea: for rule egressRule2, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-EGRESSRULE1-4", nil, sets.New[string]("1.1.1.1/32"), false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-EGRESSRULE2-4", nil, sets.New[string]("1.1.1.1/32"), false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-EGRESSRULE1-6", nil, sets.New[string]("2002:1a23:fb44::1/128"), true).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-EGRESSRULE2-6", nil, sets.New[string]("2002:1a23:fb44::1/128"), true).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables(chains, ipv4Rules1, false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables(chains, ipv6Rules1, true).Times(1) + + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-EGRESS-RULES"}, ipv4Rules2, false).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPSet("ANTREA-POL-EGRESSRULE1-4", false).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPTables([]string{"ANTREA-POL-EGRESSRULE1"}, false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-EGRESS-RULES"}, ipv6Rules2, true).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPSet("ANTREA-POL-EGRESSRULE1-6", true).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPTables([]string{"ANTREA-POL-EGRESSRULE1"}, true).Times(1) + }, + }, + { + name: "IPv4, add ingress and egress rules and forget some rules", + ipv4Enabled: true, + rulesToAdd: []*CompletedRule{ + ingressRule1, + ingressRule2, + egressRule1, + egressRule2, + }, + rulesToForget: []string{ + ingressRuleID1, + egressRuleID1, + }, + expectedCalls: func(mockRouteClient *routetest.MockInterfaceMockRecorder) { + chains := []string{ + "ANTREA-POL-INGRESSRULE1", + "ANTREA-POL-INGRESSRULE2", + "ANTREA-POL-EGRESSRULE1", + "ANTREA-POL-EGRESSRULE2", + "ANTREA-POL-INGRESS-RULES", + "ANTREA-POL-EGRESS-RULES", + } + rules1 := [][]string{ + { + "-A ANTREA-POL-INGRESSRULE1 -j ACCEPT -p tcp --dport 80", + "-A ANTREA-POL-INGRESSRULE1 -j ACCEPT -p tcp --dport 443", + }, + { + "-A ANTREA-POL-INGRESSRULE2 -j ACCEPT -p tcp --dport 443", + }, + { + "-A ANTREA-POL-EGRESSRULE1 -j ACCEPT -p tcp --dport 80", + "-A ANTREA-POL-EGRESSRULE1 -j ACCEPT -p tcp --dport 443", + }, + { + "-A ANTREA-POL-EGRESSRULE2 -j ACCEPT -p tcp --dport 443", + }, + { + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE2-4 src -j ANTREA-POL-INGRESSRULE2 -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`, + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-4 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`, + }, + { + `-A ANTREA-POL-EGRESS-RULES -m set --match-set ANTREA-POL-EGRESSRULE2-4 dst -j ANTREA-POL-EGRESSRULE2 -m comment --comment "Antrea: for rule egressRule2, policy AntreaClusterNetworkPolicy:name1"`, + `-A ANTREA-POL-EGRESS-RULES -m set --match-set ANTREA-POL-EGRESSRULE1-4 dst -j ANTREA-POL-EGRESSRULE1 -m comment --comment "Antrea: for rule egressRule1, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + + rules2 := [][]string{ + { + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE2-4 src -j ANTREA-POL-INGRESSRULE2 -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + rules3 := [][]string{ + { + `-A ANTREA-POL-EGRESS-RULES -m set --match-set ANTREA-POL-EGRESSRULE2-4 dst -j ANTREA-POL-EGRESSRULE2 -m comment --comment "Antrea: for rule egressRule2, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE1-4", nil, sets.New[string]("1.1.1.1/32"), false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE2-4", nil, sets.New[string]("1.1.1.1/32"), false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-EGRESSRULE1-4", nil, sets.New[string]("1.1.1.1/32"), false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-EGRESSRULE2-4", nil, sets.New[string]("1.1.1.1/32"), false).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables(chains, rules1, false).Times(1) + + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESS-RULES"}, rules2, false).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE1-4", false).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESSRULE1"}, false).Times(1) + + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-EGRESS-RULES"}, rules3, false).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPSet("ANTREA-POL-EGRESSRULE1-4", false).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPTables([]string{"ANTREA-POL-EGRESSRULE1"}, false).Times(1) + }, + }, + { + name: "IPv6, add ingress and egress rules and forget some rules", + ipv6Enabled: true, + rulesToAdd: []*CompletedRule{ + ingressRule1, + ingressRule2, + egressRule1, + egressRule2, + }, + rulesToForget: []string{ + ingressRuleID1, + egressRuleID1, + }, + expectedCalls: func(mockRouteClient *routetest.MockInterfaceMockRecorder) { + chains := []string{ + "ANTREA-POL-INGRESSRULE1", + "ANTREA-POL-INGRESSRULE2", + "ANTREA-POL-EGRESSRULE1", + "ANTREA-POL-EGRESSRULE2", + "ANTREA-POL-INGRESS-RULES", + "ANTREA-POL-EGRESS-RULES", + } + rules1 := [][]string{ + { + "-A ANTREA-POL-INGRESSRULE1 -j ACCEPT -p tcp --dport 80", + "-A ANTREA-POL-INGRESSRULE1 -j ACCEPT -p tcp --dport 443", + }, + { + "-A ANTREA-POL-INGRESSRULE2 -j ACCEPT -p tcp --dport 443", + }, + { + "-A ANTREA-POL-EGRESSRULE1 -j ACCEPT -p tcp --dport 80", + "-A ANTREA-POL-EGRESSRULE1 -j ACCEPT -p tcp --dport 443", + }, + { + "-A ANTREA-POL-EGRESSRULE2 -j ACCEPT -p tcp --dport 443", + }, + { + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE2-6 src -j ANTREA-POL-INGRESSRULE2 -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`, + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE1-6 src -j ANTREA-POL-INGRESSRULE1 -m comment --comment "Antrea: for rule ingressRule1, policy AntreaClusterNetworkPolicy:name1"`, + }, + { + `-A ANTREA-POL-EGRESS-RULES -m set --match-set ANTREA-POL-EGRESSRULE2-6 dst -j ANTREA-POL-EGRESSRULE2 -m comment --comment "Antrea: for rule egressRule2, policy AntreaClusterNetworkPolicy:name1"`, + `-A ANTREA-POL-EGRESS-RULES -m set --match-set ANTREA-POL-EGRESSRULE1-6 dst -j ANTREA-POL-EGRESSRULE1 -m comment --comment "Antrea: for rule egressRule1, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + + rules2 := [][]string{ + { + `-A ANTREA-POL-INGRESS-RULES -m set --match-set ANTREA-POL-INGRESSRULE2-6 src -j ANTREA-POL-INGRESSRULE2 -m comment --comment "Antrea: for rule ingressRule2, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + rules3 := [][]string{ + { + `-A ANTREA-POL-EGRESS-RULES -m set --match-set ANTREA-POL-EGRESSRULE2-6 dst -j ANTREA-POL-EGRESSRULE2 -m comment --comment "Antrea: for rule egressRule2, policy AntreaClusterNetworkPolicy:name1"`, + }, + } + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE1-6", nil, sets.New[string]("2002:1a23:fb44::1/128"), true).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE2-6", nil, sets.New[string]("2002:1a23:fb44::1/128"), true).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-EGRESSRULE1-6", nil, sets.New[string]("2002:1a23:fb44::1/128"), true).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPSet("ANTREA-POL-EGRESSRULE2-6", nil, sets.New[string]("2002:1a23:fb44::1/128"), true).Times(1) + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables(chains, rules1, true).Times(1) + + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESS-RULES"}, rules2, true).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPSet("ANTREA-POL-INGRESSRULE1-6", true).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPTables([]string{"ANTREA-POL-INGRESSRULE1"}, true).Times(1) + + mockRouteClient.AddOrUpdateNodeNetworkPolicyIPTables([]string{"ANTREA-POL-EGRESS-RULES"}, rules3, true).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPSet("ANTREA-POL-EGRESSRULE1-6", true).Times(1) + mockRouteClient.DeleteNodeNetworkPolicyIPTables([]string{"ANTREA-POL-EGRESSRULE1"}, true).Times(1) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + controller := gomock.NewController(t) + mockRouteClient := routetest.NewMockInterface(controller) + r := newTestNodeReconciler(mockRouteClient, tt.ipv4Enabled, tt.ipv6Enabled) + + tt.expectedCalls(mockRouteClient.EXPECT()) + assert.NoError(t, r.BatchReconcile(tt.rulesToAdd)) + + for _, ruleID := range tt.rulesToForget { + assert.NoError(t, r.Forget(ruleID)) + } + }) + } +} diff --git a/pkg/agent/controller/networkpolicy/node_reconciler_windows.go b/pkg/agent/controller/networkpolicy/node_reconciler_windows.go new file mode 100644 index 00000000000..074c9eeef59 --- /dev/null +++ b/pkg/agent/controller/networkpolicy/node_reconciler_windows.go @@ -0,0 +1,49 @@ +//go:build windows +// +build windows + +// Copyright 2023 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package networkpolicy + +import ( + "antrea.io/antrea/pkg/agent/route" + "antrea.io/antrea/pkg/agent/types" +) + +type nodeReconciler struct{} + +func newNodeReconciler(routeClient route.Interface, ipv4Enabled, ipv6Enabled bool) (*nodeReconciler, error) { + return &nodeReconciler{}, nil +} + +func (r *nodeReconciler) Reconcile(rule *CompletedRule) error { + return nil +} + +func (r *nodeReconciler) BatchReconcile(rules []*CompletedRule) error { + return nil +} + +func (r *nodeReconciler) Forget(ruleID string) error { + return nil +} + +func (r *nodeReconciler) GetRuleByFlowID(ruleID uint32) (*types.PolicyRule, bool, error) { + return nil, false, nil +} + +func (r *nodeReconciler) RunIDAllocatorWorker(stopCh <-chan struct{}) { + +} diff --git a/pkg/agent/controller/networkpolicy/reconciler.go b/pkg/agent/controller/networkpolicy/pod_reconciler.go similarity index 93% rename from pkg/agent/controller/networkpolicy/reconciler.go rename to pkg/agent/controller/networkpolicy/pod_reconciler.go index a20771bc4e4..1fbda0c023b 100644 --- a/pkg/agent/controller/networkpolicy/reconciler.go +++ b/pkg/agent/controller/networkpolicy/pod_reconciler.go @@ -98,10 +98,10 @@ func normalizeServices(services []v1beta2.Service) servicesKey { return servicesKey(b.String()) } -// lastRealized is the struct cached by reconciler. It's used to track the +// ofLastRealized is the struct cached by podReconciler. It's used to track the // actual state of rules we have enforced, so that we can know how to reconcile // a rule when it's updated/removed. -// It includes the last version of CompletedRule the reconciler has realized +// It includes the last version of CompletedRule the podReconciler has realized // and the related runtime information including the ofIDs, the Openflow ports // or the IP addresses of the target Pods got from the InterfaceStore. // @@ -142,7 +142,7 @@ func normalizeServices(services []v1beta2.Service) servicesKey { // while Pod C will have another Openflow rule as it resolves "http" to 8080. // In the implementation, we group Pods by their resolved services value so Pod A and B // can be mapped to same group. -type lastRealized struct { +type ofLastRealized struct { // ofIDs identifies Openflow rules in Openflow implementation. // It's a map of servicesKey to Openflow rule ID. ofIDs map[servicesKey]uint32 @@ -175,8 +175,8 @@ type lastRealized struct { groupAddresses sets.Set[string] } -func newLastRealized(rule *CompletedRule) *lastRealized { - return &lastRealized{ +func newOFLastRealized(rule *CompletedRule) *ofLastRealized { + return &ofLastRealized{ ofIDs: map[servicesKey]uint32{}, CompletedRule: rule, podOFPorts: map[servicesKey]sets.Set[int32]{}, @@ -194,11 +194,11 @@ type tablePriorityAssigner struct { mutex sync.RWMutex } -// reconciler implements Reconciler. +// podReconciler implements Reconciler. // Note that although its Reconcile and Forget methods are thread-safe, it's // assumed each rule can only be processed by a single client at any given // time. Different rules can be processed in parallel. -type reconciler struct { +type podReconciler struct { // ofClient is the Openflow interface. ofClient openflow.Client @@ -206,7 +206,7 @@ type reconciler struct { ifaceStore interfacestore.InterfaceStore // lastRealizeds caches the last realized rules. - // It's a mapping from ruleID to *lastRealized. + // It's a mapping from ruleID to *ofLastRealized. lastRealizeds sync.Map // idAllocator provides interfaces to allocateForRule and release uint32 id. @@ -220,11 +220,11 @@ type reconciler struct { ipv6Enabled bool // fqdnController manages dns cache of FQDN rules. It provides interfaces for the - // reconciler to register FQDN policy rules and query the IP addresses corresponded + // podReconciler to register FQDN policy rules and query the IP addresses corresponded // to a FQDN. fqdnController *fqdnController - // groupCounters is a list of GroupCounter for v4 and v6 env. reconciler uses these + // groupCounters is a list of GroupCounter for v4 and v6 env. podReconciler uses these // GroupCounters to get the groupIDs of a specific Service. groupCounters []proxytypes.GroupCounter @@ -232,8 +232,8 @@ type reconciler struct { multicastEnabled bool } -// newReconciler returns a new *reconciler. -func newReconciler(ofClient openflow.Client, +// newPodReconciler returns a new *podReconciler. +func newPodReconciler(ofClient openflow.Client, ifaceStore interfacestore.InterfaceStore, idAllocator *idAllocator, fqdnController *fqdnController, @@ -242,7 +242,7 @@ func newReconciler(ofClient openflow.Client, v6Enabled bool, antreaPolicyEnabled bool, multicastEnabled bool, -) *reconciler { +) *podReconciler { priorityAssigners := map[uint8]*tablePriorityAssigner{} if antreaPolicyEnabled { for _, table := range openflow.GetAntreaPolicyBaselineTierTables() { @@ -268,7 +268,7 @@ func newReconciler(ofClient openflow.Client, } } } - reconciler := &reconciler{ + reconciler := &podReconciler{ ofClient: ofClient, ifaceStore: ifaceStore, lastRealizeds: sync.Map{}, @@ -288,14 +288,14 @@ func newReconciler(ofClient openflow.Client, // RunIDAllocatorWorker runs the worker that deletes the rules from the cache in // idAllocator. -func (r *reconciler) RunIDAllocatorWorker(stopCh <-chan struct{}) { +func (r *podReconciler) RunIDAllocatorWorker(stopCh <-chan struct{}) { r.idAllocator.runWorker(stopCh) } // Reconcile checks whether the provided rule have been enforced or not, and // invoke the add or update method accordingly. -func (r *reconciler) Reconcile(rule *CompletedRule) error { - klog.InfoS("Reconciling NetworkPolicy rule", "rule", rule.ID, "policy", rule.SourceRef.ToString()) +func (r *podReconciler) Reconcile(rule *CompletedRule) error { + klog.InfoS("Reconciling Pod NetworkPolicy rule", "rule", rule.ID, "policy", rule.SourceRef.ToString()) var err error var ofPriority *uint16 @@ -319,7 +319,7 @@ func (r *reconciler) Reconcile(rule *CompletedRule) error { if !exists { ofRuleInstallErr = r.add(rule, ofPriority, ruleTable) } else { - ofRuleInstallErr = r.update(value.(*lastRealized), rule, ofPriority, ruleTable) + ofRuleInstallErr = r.update(value.(*ofLastRealized), rule, ofPriority, ruleTable) } if ofRuleInstallErr != nil && ofPriority != nil && !registeredBefore { priorityAssigner.assigner.release(*ofPriority) @@ -327,7 +327,7 @@ func (r *reconciler) Reconcile(rule *CompletedRule) error { return ofRuleInstallErr } -func (r *reconciler) getRuleType(rule *CompletedRule) ruleType { +func (r *podReconciler) getRuleType(rule *CompletedRule) ruleType { if !r.multicastEnabled { return unicast } @@ -349,7 +349,7 @@ func (r *reconciler) getRuleType(rule *CompletedRule) ruleType { // getOFRuleTable retrieves the OpenFlow table to install the CompletedRule. // The decision is made based on whether the rule is created for an ACNP/ANNP, and // the Tier of that NetworkPolicy. -func (r *reconciler) getOFRuleTable(rule *CompletedRule) uint8 { +func (r *podReconciler) getOFRuleTable(rule *CompletedRule) uint8 { rType := r.getRuleType(rule) var ruleTables []*openflow.Table var tableID uint8 @@ -388,7 +388,7 @@ func (r *reconciler) getOFRuleTable(rule *CompletedRule) uint8 { // getOFPriority retrieves the OFPriority for the input CompletedRule to be installed, // and re-arranges installed priorities on OVS if necessary. -func (r *reconciler) getOFPriority(rule *CompletedRule, tableID uint8, pa *tablePriorityAssigner) (*uint16, bool, error) { +func (r *podReconciler) getOFPriority(rule *CompletedRule, tableID uint8, pa *tablePriorityAssigner) (*uint16, bool, error) { // IGMP Egress policy is enforced in userspace via packet-in message, there won't be OpenFlow // rules created for such rules. Therefore, assigning priority is not required. if !rule.isAntreaNetworkPolicyRule() || rule.isIGMPEgressPolicyRule() { @@ -431,7 +431,7 @@ func (r *reconciler) getOFPriority(rule *CompletedRule, tableID uint8, pa *table // BatchReconcile reconciles the desired state of the provided CompletedRules // with the actual state of Openflow entries in batch. It should only be invoked // if all rules are newly added without last realized status. -func (r *reconciler) BatchReconcile(rules []*CompletedRule) error { +func (r *podReconciler) BatchReconcile(rules []*CompletedRule) error { var rulesToInstall []*CompletedRule var priorities []*uint16 prioritiesByTable := map[uint8][]*uint16{} @@ -471,7 +471,7 @@ func (r *reconciler) BatchReconcile(rules []*CompletedRule) error { // registerOFPriorities constructs a Priority type for each CompletedRule in the input list, // and registers those Priorities with appropriate tablePriorityAssigner based on Tier. -func (r *reconciler) registerOFPriorities(rules []*CompletedRule) error { +func (r *podReconciler) registerOFPriorities(rules []*CompletedRule) error { prioritiesToRegister := map[uint8][]types.Priority{} for _, rule := range rules { // IGMP Egress policy is enforced in userspace via packet-in message, there won't be OpenFlow @@ -495,7 +495,7 @@ func (r *reconciler) registerOFPriorities(rules []*CompletedRule) error { } // add converts CompletedRule to PolicyRule(s) and invokes installOFRule to install them. -func (r *reconciler) add(rule *CompletedRule, ofPriority *uint16, table uint8) error { +func (r *podReconciler) add(rule *CompletedRule, ofPriority *uint16, table uint8) error { klog.V(2).InfoS("Adding new rule", "rule", rule) ofRuleByServicesMap, lastRealized := r.computeOFRulesForAdd(rule, ofPriority, table) for svcKey, ofRule := range ofRuleByServicesMap { @@ -517,9 +517,9 @@ func (r *reconciler) add(rule *CompletedRule, ofPriority *uint16, table uint8) e return nil } -func (r *reconciler) computeOFRulesForAdd(rule *CompletedRule, ofPriority *uint16, table uint8) ( - map[servicesKey]*types.PolicyRule, *lastRealized) { - lastRealized := newLastRealized(rule) +func (r *podReconciler) computeOFRulesForAdd(rule *CompletedRule, ofPriority *uint16, table uint8) ( + map[servicesKey]*types.PolicyRule, *ofLastRealized) { + lastRealized := newOFLastRealized(rule) // TODO: Handle the case that the following processing fails or partially succeeds. r.lastRealizeds.Store(rule.ID, lastRealized) @@ -561,7 +561,7 @@ func (r *reconciler) computeOFRulesForAdd(rule *CompletedRule, ofPriority *uint1 svcGroupIDs := r.getSvcGroupIDs(members) toAddresses = svcGroupIDsToOFAddresses(svcGroupIDs) // If rule is applied to Services, there will be only one svcKey, which is "", in - // membersByServicesMap. So lastRealized.serviceGroupIDs won't be overwritten in + // membersByServicesMap. So ofLastRealized.serviceGroupIDs won't be overwritten in // this for-loop. lastRealized.serviceGroupIDs = svcGroupIDs } else { @@ -672,8 +672,8 @@ func (r *reconciler) computeOFRulesForAdd(rule *CompletedRule, ofPriority *uint1 } // batchAdd converts CompletedRules to PolicyRules and invokes BatchInstallPolicyRuleFlows to install them. -func (r *reconciler) batchAdd(rules []*CompletedRule, ofPriorities []*uint16) error { - lastRealizeds := make([]*lastRealized, len(rules)) +func (r *podReconciler) batchAdd(rules []*CompletedRule, ofPriorities []*uint16) error { + lastRealizeds := make([]*ofLastRealized, len(rules)) ofIDUpdateMaps := make([]map[servicesKey]uint32, len(rules)) var allOFRules []*types.PolicyRule @@ -711,7 +711,7 @@ func (r *reconciler) batchAdd(rules []*CompletedRule, ofPriorities []*uint16) er // update calculates the difference of Addresses between oldRule and newRule, // and invokes Openflow client's methods to reconcile them. -func (r *reconciler) update(lastRealized *lastRealized, newRule *CompletedRule, ofPriority *uint16, table uint8) error { +func (r *podReconciler) update(lastRealized *ofLastRealized, newRule *CompletedRule, ofPriority *uint16, table uint8) error { klog.V(2).InfoS("Updating existing rule", "rule", newRule) // staleOFIDs tracks servicesKey that are no long needed. // Firstly fill it with the last realized ofIDs. @@ -871,7 +871,7 @@ func (r *reconciler) update(lastRealized *lastRealized, newRule *CompletedRule, LogLabel: newRule.LogLabel, } // If the PolicyRule for the original services doesn't exist and IPBlocks is present, it means the - // reconciler hasn't installed flows for IPBlocks, then it must be added to the new PolicyRule. + // podReconciler hasn't installed flows for IPBlocks, then it must be added to the new PolicyRule. if svcKey == originalSvcKey && len(newRule.To.IPBlocks) > 0 { to := ipBlocksToOFAddresses(newRule.To.IPBlocks, r.ipv4Enabled, r.ipv6Enabled, false) ofRule.To = append(ofRule.To, to...) @@ -943,7 +943,7 @@ func (r *reconciler) update(lastRealized *lastRealized, newRule *CompletedRule, return nil } -func (r *reconciler) installOFRule(ofRule *types.PolicyRule) error { +func (r *podReconciler) installOFRule(ofRule *types.PolicyRule) error { klog.V(2).InfoS("Installing ofRule", "id", ofRule.FlowID, "direction", ofRule.Direction, "from", len(ofRule.From), "to", len(ofRule.To), "service", len(ofRule.Service)) if err := r.ofClient.InstallPolicyRuleFlows(ofRule); err != nil { r.idAllocator.forgetRule(ofRule.FlowID) @@ -952,7 +952,7 @@ func (r *reconciler) installOFRule(ofRule *types.PolicyRule) error { return nil } -func (r *reconciler) updateOFRule(ofID uint32, addedFrom []types.Address, addedTo []types.Address, deletedFrom []types.Address, deletedTo []types.Address, priority *uint16, enableLogging, isMCNPRule bool) error { +func (r *podReconciler) updateOFRule(ofID uint32, addedFrom []types.Address, addedTo []types.Address, deletedFrom []types.Address, deletedTo []types.Address, priority *uint16, enableLogging, isMCNPRule bool) error { klog.V(2).InfoS("Updating ofRule", "id", ofID, "addedFrom", len(addedFrom), "addedTo", len(addedTo), "deletedFrom", len(deletedFrom), "deletedTo", len(deletedTo)) // TODO: This might be unnecessarily complex and hard for error handling, consider revising the Openflow interfaces. if len(addedFrom) > 0 { @@ -978,7 +978,7 @@ func (r *reconciler) updateOFRule(ofID uint32, addedFrom []types.Address, addedT return nil } -func (r *reconciler) uninstallOFRule(ofID uint32, table uint8) error { +func (r *podReconciler) uninstallOFRule(ofID uint32, table uint8) error { klog.V(2).InfoS("Uninstalling ofRule", "id", ofID) stalePriorities, err := r.ofClient.UninstallPolicyRuleFlows(ofID) if err != nil { @@ -1003,7 +1003,7 @@ func (r *reconciler) uninstallOFRule(ofID uint32, table uint8) error { // Forget invokes UninstallPolicyRuleFlows to uninstall Openflow entries // associated with the provided ruleID if it was enforced before. -func (r *reconciler) Forget(ruleID string) error { +func (r *podReconciler) Forget(ruleID string) error { klog.InfoS("Forgetting rule", "rule", ruleID) value, exists := r.lastRealizeds.Load(ruleID) @@ -1012,7 +1012,7 @@ func (r *reconciler) Forget(ruleID string) error { return nil } - lastRealized := value.(*lastRealized) + lastRealized := value.(*ofLastRealized) table := r.getOFRuleTable(lastRealized.CompletedRule) priorityAssigner, exists := r.priorityAssigners[table] if exists { @@ -1033,7 +1033,7 @@ func (r *reconciler) Forget(ruleID string) error { return nil } -func (r *reconciler) isIGMPRule(rule *CompletedRule) bool { +func (r *podReconciler) isIGMPRule(rule *CompletedRule) bool { isIGMP := false if len(rule.Services) > 0 && (rule.Services[0].Protocol != nil) && (*rule.Services[0].Protocol == v1beta2.ProtocolIGMP) { @@ -1042,11 +1042,11 @@ func (r *reconciler) isIGMPRule(rule *CompletedRule) bool { return isIGMP } -func (r *reconciler) GetRuleByFlowID(ruleFlowID uint32) (*types.PolicyRule, bool, error) { +func (r *podReconciler) GetRuleByFlowID(ruleFlowID uint32) (*types.PolicyRule, bool, error) { return r.idAllocator.getRuleFromAsyncCache(ruleFlowID) } -func (r *reconciler) getOFPorts(members v1beta2.GroupMemberSet) sets.Set[int32] { +func (r *podReconciler) getOFPorts(members v1beta2.GroupMemberSet) sets.Set[int32] { ofPorts := sets.New[int32]() for _, m := range members { var entityName, ns string @@ -1071,7 +1071,7 @@ func (r *reconciler) getOFPorts(members v1beta2.GroupMemberSet) sets.Set[int32] return ofPorts } -func (r *reconciler) getIPs(members v1beta2.GroupMemberSet) sets.Set[string] { +func (r *podReconciler) getIPs(members v1beta2.GroupMemberSet) sets.Set[string] { ips := sets.New[string]() for _, m := range members { var entityName, ns string @@ -1100,7 +1100,7 @@ func (r *reconciler) getIPs(members v1beta2.GroupMemberSet) sets.Set[string] { return ips } -func (r *reconciler) getSvcGroupIDs(members v1beta2.GroupMemberSet) sets.Set[int64] { +func (r *podReconciler) getSvcGroupIDs(members v1beta2.GroupMemberSet) sets.Set[int64] { var svcRefs []v1beta2.ServiceReference for _, m := range members { if m.Service != nil { @@ -1162,7 +1162,7 @@ func ofPortsToOFAddresses(ofPorts sets.Set[int32]) []types.Address { return addresses } -func (r *reconciler) svcRefsToGroupIDs(svcRefs []v1beta2.ServiceReference) sets.Set[int64] { +func (r *podReconciler) svcRefsToGroupIDs(svcRefs []v1beta2.ServiceReference) sets.Set[int64] { groupIDs := sets.New[int64]() for _, svcRef := range svcRefs { for _, groupCounter := range r.groupCounters { diff --git a/pkg/agent/controller/networkpolicy/reconciler_test.go b/pkg/agent/controller/networkpolicy/pod_reconciler_test.go similarity index 98% rename from pkg/agent/controller/networkpolicy/reconciler_test.go rename to pkg/agent/controller/networkpolicy/pod_reconciler_test.go index 0b6cbc58f30..6f51e26b33a 100644 --- a/pkg/agent/controller/networkpolicy/reconciler_test.go +++ b/pkg/agent/controller/networkpolicy/pod_reconciler_test.go @@ -107,12 +107,12 @@ func newCIDR(cidrStr string) *net.IPNet { return tmpIPNet } -func newTestReconciler(t *testing.T, controller *gomock.Controller, ifaceStore interfacestore.InterfaceStore, ofClient *openflowtest.MockClient, v4Enabled, v6Enabled bool) *reconciler { +func newTestReconciler(t *testing.T, controller *gomock.Controller, ifaceStore interfacestore.InterfaceStore, ofClient *openflowtest.MockClient, v4Enabled, v6Enabled bool) *podReconciler { f, _ := newMockFQDNController(t, controller, nil) ch := make(chan string, 100) groupIDAllocator := openflow.NewGroupAllocator() groupCounters := []proxytypes.GroupCounter{proxytypes.NewGroupCounter(groupIDAllocator, ch)} - r := newReconciler(ofClient, ifaceStore, newIDAllocator(testAsyncDeleteInterval), f, groupCounters, v4Enabled, v6Enabled, true, false) + r := newPodReconciler(ofClient, ifaceStore, newIDAllocator(testAsyncDeleteInterval), f, groupCounters, v4Enabled, v6Enabled, true, false) return r } @@ -120,14 +120,14 @@ func TestReconcilerForget(t *testing.T) { prepareMockTables() tests := []struct { name string - lastRealizeds map[string]*lastRealized + lastRealizeds map[string]*ofLastRealized args string expectedOFRuleIDs []uint32 wantErr bool }{ { "unknown-rule", - map[string]*lastRealized{ + map[string]*ofLastRealized{ "foo": { ofIDs: map[servicesKey]uint32{servicesKey1: 8}, CompletedRule: &CompletedRule{ @@ -141,7 +141,7 @@ func TestReconcilerForget(t *testing.T) { }, { "known-single-ofrule", - map[string]*lastRealized{ + map[string]*ofLastRealized{ "foo": { ofIDs: map[servicesKey]uint32{servicesKey1: 8}, CompletedRule: &CompletedRule{ @@ -155,7 +155,7 @@ func TestReconcilerForget(t *testing.T) { }, { "known-multiple-ofrule", - map[string]*lastRealized{ + map[string]*ofLastRealized{ "foo": { ofIDs: map[servicesKey]uint32{servicesKey1: 8, servicesKey2: 9}, CompletedRule: &CompletedRule{ @@ -169,7 +169,7 @@ func TestReconcilerForget(t *testing.T) { }, { "known-multiple-ofrule-cnp", - map[string]*lastRealized{ + map[string]*ofLastRealized{ "foo": { ofIDs: map[servicesKey]uint32{servicesKey1: 8, servicesKey2: 9}, CompletedRule: &CompletedRule{ @@ -864,7 +864,7 @@ func TestReconcilerReconcileServiceRelatedRule(t *testing.T) { } } -// TestReconcileWithTransientError ensures the reconciler can reconcile a rule properly after the first attempt meets +// TestReconcileWithTransientError ensures the podReconciler can reconcile a rule properly after the first attempt meets // transient error. // The input rule is an egress rule with named port, applying to 3 Pods and 1 IPBlock. The first 2 Pods have different // port numbers for the named port and the 3rd Pod cannot resolve it. @@ -922,10 +922,10 @@ func TestReconcileWithTransientError(t *testing.T) { mockOFClient.EXPECT().InstallPolicyRuleFlows(gomock.Any()).Return(transientError).Times(1) err := r.Reconcile(egressRule) assert.Error(t, err) - // Ensure the openflow ID is not persistent in lastRealized and is released to idAllocator upon error. + // Ensure the openflow ID is not persistent in ofLastRealized and is released to idAllocator upon error. value, exists := r.lastRealizeds.Load(egressRule.ID) assert.True(t, exists) - assert.Empty(t, value.(*lastRealized).ofIDs) + assert.Empty(t, value.(*ofLastRealized).ofIDs) assert.Equal(t, 1, r.idAllocator.deleteQueue.Len()) // Make the second call success. @@ -961,10 +961,10 @@ func TestReconcileWithTransientError(t *testing.T) { } err = r.Reconcile(egressRule) assert.NoError(t, err) - // Ensure the openflow IDs are persistent in lastRealized and are not released to idAllocator upon success. + // Ensure the openflow IDs are persistent in ofLastRealized and are not released to idAllocator upon success. value, exists = r.lastRealizeds.Load(egressRule.ID) assert.True(t, exists) - assert.Len(t, value.(*lastRealized).ofIDs, 3) + assert.Len(t, value.(*ofLastRealized).ofIDs, 3) // Ensure the number of released IDs doesn't change. assert.Equal(t, 1, r.idAllocator.deleteQueue.Len()) @@ -1075,7 +1075,7 @@ func TestReconcilerBatchReconcile(t *testing.T) { r := newTestReconciler(t, controller, ifaceStore, mockOFClient, true, true) if tt.numInstalledRules > 0 { // BatchInstall should skip rules already installed - r.lastRealizeds.Store(tt.args[0].ID, newLastRealized(tt.args[0])) + r.lastRealizeds.Store(tt.args[0].ID, newOFLastRealized(tt.args[0])) } // TODO: mock idAllocator and priorityAssigner mockOFClient.EXPECT().BatchInstallPolicyRuleFlows(gomock.Any()). diff --git a/pkg/agent/route/interfaces.go b/pkg/agent/route/interfaces.go index 30b86097bc3..1459e0c7109 100644 --- a/pkg/agent/route/interfaces.go +++ b/pkg/agent/route/interfaces.go @@ -18,6 +18,8 @@ import ( "net" "time" + "k8s.io/apimachinery/pkg/util/sets" + "antrea.io/antrea/pkg/agent/config" binding "antrea.io/antrea/pkg/ovs/openflow" ) @@ -90,4 +92,16 @@ type Interface interface { // ClearConntrackEntryForService deletes a conntrack entry for a Service connection. ClearConntrackEntryForService(svcIP net.IP, svcPort uint16, endpointIP net.IP, protocol binding.Protocol) error + + // AddOrUpdateNodeNetworkPolicyIPSet adds or updates ipset created for NodeNetworkPolicy. + AddOrUpdateNodeNetworkPolicyIPSet(ipsetName string, prevIPSetEntries, curIPSetEntries sets.Set[string], isIPv6 bool) error + + // DeleteNodeNetworkPolicyIPSet deletes ipset created for NodeNetworkPolicy. + DeleteNodeNetworkPolicyIPSet(ipsetName string, isIPv6 bool) error + + // AddOrUpdateNodeNetworkPolicyIPTables adds or updates iptables chains and rules within the chains for NodeNetworkPolicy. + AddOrUpdateNodeNetworkPolicyIPTables(iptablesChains []string, iptablesRules [][]string, isIPv6 bool) error + + // DeleteNodeNetworkPolicyIPTables deletes iptables chains and rules within the chains for NodeNetworkPolicy. + DeleteNodeNetworkPolicyIPTables(iptablesChains []string, isIPv6 bool) error } diff --git a/pkg/agent/route/route_linux.go b/pkg/agent/route/route_linux.go index e4c6c4dfcec..2919b67cae3 100644 --- a/pkg/agent/route/route_linux.go +++ b/pkg/agent/route/route_linux.go @@ -20,12 +20,14 @@ import ( "net" "reflect" "strconv" + "strings" "sync" "time" "github.com/containernetworking/plugins/pkg/ip" "github.com/vishvananda/netlink" "golang.org/x/sys/unix" + "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/klog/v2" @@ -38,6 +40,7 @@ import ( "antrea.io/antrea/pkg/agent/util/iptables" utilnetlink "antrea.io/antrea/pkg/agent/util/netlink" "antrea.io/antrea/pkg/agent/util/sysctl" + agentconfig "antrea.io/antrea/pkg/config/agent" binding "antrea.io/antrea/pkg/ovs/openflow" "antrea.io/antrea/pkg/ovs/ovsconfig" "antrea.io/antrea/pkg/util/env" @@ -72,11 +75,17 @@ const ( antreaForwardChain = "ANTREA-FORWARD" antreaPreRoutingChain = "ANTREA-PREROUTING" antreaPostRoutingChain = "ANTREA-POSTROUTING" + antreaInputChain = "ANTREA-INPUT" antreaOutputChain = "ANTREA-OUTPUT" antreaMangleChain = "ANTREA-MANGLE" serviceIPv4CIDRKey = "serviceIPv4CIDRKey" serviceIPv6CIDRKey = "serviceIPv6CIDRKey" + + privilegedNodeNetworkPolicyIngressRulesChain = "ANTREA-POL-PRI-INGRESS-RULES" + privilegedNodeNetworkPolicyEgressRulesChain = "ANTREA-POL-PRI-EGRESS-RULES" + defaultNodeNetworkPolicyIngressRulesChain = "ANTREA-POL-DEF-INGRESS-RULES" + defaultNodeNetworkPolicyEgressRulesChain = "ANTREA-POL-DEF-EGRESS-RULES" ) // Client implements Interface. @@ -90,6 +99,11 @@ var ( // The system auto-generated IPv6 link-local route always uses "fe80::/64" as the destination regardless of the // interface's global address's mask. _, llrCIDR, _ = net.ParseCIDR("fe80::/64") + + nodeNetworkPolicyJumpRules = []jumpRule{ + {iptables.FilterTable, iptables.InputChain, antreaInputChain, "Antrea: jump to Antrea input rules"}, + {iptables.FilterTable, iptables.OutputChain, antreaOutputChain, "Antrea: jump to Antrea output rules"}, + } ) // Client takes care of routing container packets in host network, coordinating ip route, ip rule, iptables and ipset. @@ -107,11 +121,13 @@ type Client struct { // markToSNATIP caches marks to SNAT IPs. It's used in Egress feature. markToSNATIP sync.Map // iptablesInitialized is used to notify when iptables initialization is done. - iptablesInitialized chan struct{} - proxyAll bool - connectUplinkToBridge bool - multicastEnabled bool - isCloudEKS bool + iptablesInitialized chan struct{} + proxyAll bool + connectUplinkToBridge bool + multicastEnabled bool + isCloudEKS bool + nodeNetworkPolicyEnabled bool + nodeNetworkPolicyConfig *agentconfig.NodeNetworkPolicyConfig // serviceRoutes caches ip routes about Services. serviceRoutes sync.Map // serviceNeighbors caches neighbors. @@ -126,20 +142,118 @@ type Client struct { clusterNodeIP6s sync.Map // The latest calculated Service CIDRs can be got from serviceCIDRProvider. serviceCIDRProvider servicecidr.Interface + // nodeNetworkPolicyIPSetsIPv4 caches all existing IPv4 ipsets for NodeNetworkPolicy. + nodeNetworkPolicyIPSetsIPv4 sync.Map + // nodeNetworkPolicyIPSetsIPv6 caches all existing IPv6 ipsets for NodeNetworkPolicy. + nodeNetworkPolicyIPSetsIPv6 sync.Map + // nodeNetworkPolicyIPSetsIPv4 caches all existing IPv4 iptables chains and rules for NodeNetworkPolicy. + nodeNetworkPolicyIPTablesIPv4 *nodeNetworkPolicyIPTablesCache + // nodeNetworkPolicyIPSetsIPv6 caches all existing IPv4 iptables chains and rules for NodeNetworkPolicy. + nodeNetworkPolicyIPTablesIPv6 *nodeNetworkPolicyIPTablesCache + // fixedNodeNetworkPolicyIPTablesIPv4 stores the IPv4 iptables rules that should be created before adding + // NodeNetworkPolicy rules. They should be also deleted after all NodeNetworkPolicy rules are removed. + fixedNodeNetworkPolicyIPTablesIPv4 []string + // fixedNodeNetworkPolicyIPTablesIPv6 stores the IPv6 iptables rules that should be created before adding + // NodeNetworkPolicy rules. They should be also deleted after all NodeNetworkPolicy rules are removed. + fixedNodeNetworkPolicyIPTablesIPv6 []string + // fixedNodeNetworkPolicyChains stores the iptables chains that should be created before adding NodeNetworkPolicy + // rules. They should be also deleted after all NodeNetworkPolicy rules are removed. + fixedNodeNetworkPolicyChains []string +} + +// nodeNetworkPolicyIPTablesCache is used for caching iptables chains and rules for NodeNetworkPolicy. +type nodeNetworkPolicyIPTablesCache struct { + sync.RWMutex + ipProtocol iptables.Protocol + data map[string][]string + preFunc func(iptables.Protocol) error // Before adding elements to data, if there is no cache in data, run this function. + postFunc func(iptables.Protocol) error // After delete elements from data, if there is no cache in data, run this function. +} + +func (n *nodeNetworkPolicyIPTablesCache) store(chains []string, rules [][]string) error { + n.Lock() + defer n.Unlock() + + if len(n.data) == 0 { + if err := n.preFunc(n.ipProtocol); err != nil { + return err + } + } + for index, chain := range chains { + if len(rules[index]) == 0 { + delete(n.data, chain) // If there is no rule in this chain, no need to store it. + } else { + n.data[chain] = rules[index] + } + } + return nil +} + +func (n *nodeNetworkPolicyIPTablesCache) delete(chains []string) error { + n.Lock() + defer n.Unlock() + + for _, chain := range chains { + delete(n.data, chain) + } + + if len(n.data) == 0 { + if err := n.postFunc(n.ipProtocol); err != nil { + return err + } + } + return nil +} + +func (n *nodeNetworkPolicyIPTablesCache) load(chain string) []string { + n.RLock() + defer n.RUnlock() + + data := n.data[chain] + return append(make([]string, 0, len(data)), data...) +} + +func (n *nodeNetworkPolicyIPTablesCache) loadAll() map[string][]string { + n.RLock() + defer n.RUnlock() + + data := make(map[string][]string) + for chain, rules := range n.data { + data[chain] = append(make([]string, 0, len(rules)), rules...) + } + return data +} + +func newNodeNetworkPolicyIPTablesCache(ipProtocol iptables.Protocol, preFunc, postFunc func(iptables.Protocol) error) *nodeNetworkPolicyIPTablesCache { + return &nodeNetworkPolicyIPTablesCache{ + data: make(map[string][]string), + ipProtocol: ipProtocol, + preFunc: preFunc, + postFunc: postFunc, + } } // NewClient returns a route client. -func NewClient(networkConfig *config.NetworkConfig, noSNAT, proxyAll, connectUplinkToBridge, multicastEnabled bool, serviceCIDRProvider servicecidr.Interface) (*Client, error) { +func NewClient(networkConfig *config.NetworkConfig, + noSNAT bool, + proxyAll bool, + connectUplinkToBridge bool, + nodeNetworkPolicyEnabled bool, + nodeNetworkPolicyConfig *agentconfig.NodeNetworkPolicyConfig, + multicastEnabled bool, + serviceCIDRProvider servicecidr.Interface) (*Client, error) { return &Client{ - networkConfig: networkConfig, - noSNAT: noSNAT, - proxyAll: proxyAll, - multicastEnabled: multicastEnabled, - connectUplinkToBridge: connectUplinkToBridge, - ipset: ipset.NewClient(), - netlink: &netlink.Handle{}, - isCloudEKS: env.IsCloudEKS(), - serviceCIDRProvider: serviceCIDRProvider, + networkConfig: networkConfig, + noSNAT: noSNAT, + proxyAll: proxyAll, + multicastEnabled: multicastEnabled, + connectUplinkToBridge: connectUplinkToBridge, + nodeNetworkPolicyEnabled: nodeNetworkPolicyEnabled, + nodeNetworkPolicyConfig: nodeNetworkPolicyConfig, + ipset: ipset.NewClient(), + netlink: &netlink.Handle{}, + isCloudEKS: env.IsCloudEKS(), + serviceCIDRProvider: serviceCIDRProvider, }, nil } @@ -200,6 +314,11 @@ func (c *Client) Initialize(nodeConfig *config.NodeConfig, done func()) error { return fmt.Errorf("failed to initialize Service IP routes: %v", err) } } + // Build privileged iptables rules for NodeNetworkPolicy. Note that, the privileged iptables rules are currently built + // from NodeNetworkPolicy config, not synchronized to Node. + if c.nodeNetworkPolicyEnabled { + c.initNodeNetworkPolicy() + } return nil } @@ -396,6 +515,33 @@ func (c *Client) syncIPSet() error { }) } + c.nodeNetworkPolicyIPSetsIPv4.Range(func(key, value any) bool { + ipsetName := key.(string) + ipsetEntries := value.(sets.Set[string]) + if err := c.ipset.CreateIPSet(ipsetName, ipset.HashNet, false); err != nil { + return false + } + for ipsetEntry := range ipsetEntries { + if err := c.ipset.AddEntry(ipsetName, ipsetEntry); err != nil { + return false + } + } + return true + }) + c.nodeNetworkPolicyIPSetsIPv6.Range(func(key, value any) bool { + ipsetName := key.(string) + ipsetEntries := value.(sets.Set[string]) + if err := c.ipset.CreateIPSet(ipsetName, ipset.HashNet, true); err != nil { + return false + } + for ipsetEntry := range ipsetEntries { + if err := c.ipset.AddEntry(ipsetName, ipsetEntry); err != nil { + return false + } + } + return true + }) + return nil } @@ -482,18 +628,19 @@ func (c *Client) writeEKSNATRules(iptablesData *bytes.Buffer) { }...) } +// Create the antrea managed chains and link them to built-in chains. +// We cannot use iptables-restore for these jump rules because there +// are non antrea managed rules in built-in chains. +type jumpRule struct { + table string + srcChain string + dstChain string + comment string +} + // syncIPTables ensure that the iptables infrastructure we use is set up. // It's idempotent and can safely be called on every startup. func (c *Client) syncIPTables() error { - // Create the antrea managed chains and link them to built-in chains. - // We cannot use iptables-restore for these jump rules because there - // are non antrea managed rules in built-in chains. - type jumpRule struct { - table string - srcChain string - dstChain string - comment string - } jumpRules := []jumpRule{ {iptables.RawTable, iptables.PreRoutingChain, antreaPreRoutingChain, "Antrea: jump to Antrea prerouting rules"}, {iptables.RawTable, iptables.OutputChain, antreaOutputChain, "Antrea: jump to Antrea output rules"}, @@ -508,6 +655,20 @@ func (c *Client) syncIPTables() error { if c.proxyAll { jumpRules = append(jumpRules, jumpRule{iptables.NATTable, iptables.OutputChain, antreaOutputChain, "Antrea: jump to Antrea output rules"}) } + nodeNetworkPolicyIPTablesIPv4 := map[string][]string{} + nodeNetworkPolicyIPTablesIPv6 := map[string][]string{} + if c.nodeNetworkPolicyEnabled { + if c.nodeNetworkPolicyIPTablesIPv4 != nil { + nodeNetworkPolicyIPTablesIPv4 = c.nodeNetworkPolicyIPTablesIPv4.loadAll() + } + if c.nodeNetworkPolicyIPTablesIPv6 != nil { + nodeNetworkPolicyIPTablesIPv6 = c.nodeNetworkPolicyIPTablesIPv6.loadAll() + } + // Add these two jump rules only when NodeNetworkPolicy rules exist. + if len(nodeNetworkPolicyIPTablesIPv4) != 0 || len(nodeNetworkPolicyIPTablesIPv6) != 0 { + jumpRules = append(jumpRules, nodeNetworkPolicyJumpRules...) + } + } for _, rule := range jumpRules { if err := c.iptables.EnsureChain(iptables.ProtocolDual, rule.table, rule.dstChain); err != nil { return err @@ -541,6 +702,8 @@ func (c *Client) syncIPTables() error { config.VirtualNodePortDNATIPv4, config.VirtualServiceIPv4, snatMarkToIPv4, + nodeNetworkPolicyIPTablesIPv4, + c.fixedNodeNetworkPolicyIPTablesIPv4, false) // Setting --noflush to keep the previous contents (i.e. non antrea managed chains) of the tables. @@ -559,6 +722,8 @@ func (c *Client) syncIPTables() error { config.VirtualNodePortDNATIPv6, config.VirtualServiceIPv6, snatMarkToIPv6, + nodeNetworkPolicyIPTablesIPv6, + c.fixedNodeNetworkPolicyIPTablesIPv6, true) // Setting --noflush to keep the previous contents (i.e. non antrea managed chains) of the tables. if err := c.iptables.Restore(iptablesData.String(), false, true); err != nil { @@ -577,6 +742,8 @@ func (c *Client) restoreIptablesData(podCIDR *net.IPNet, nodePortDNATVirtualIP, serviceVirtualIP net.IP, snatMarkToIP map[uint32]net.IP, + nodeNetWorkPolicyIPTables map[string][]string, + fixedNodeNetWorkPolicyIPTables []string, isIPv6 bool) *bytes.Buffer { // Create required rules in the antrea chains. // Use iptables-restore as it flushes the involved chains and creates the desired rules @@ -623,7 +790,7 @@ func (c *Client) restoreIptablesData(podCIDR *net.IPNet, "-m", "comment", "--comment", `"Antrea: drop Pod multicast traffic forwarded via underlay network"`, "-m", "set", "--match-set", clusterNodeIPSet, "src", "-d", types.McastCIDR.String(), - "-j", iptables.DROPTarget, + "-j", iptables.DropTarget, }...) } } @@ -665,6 +832,20 @@ func (c *Client) restoreIptablesData(podCIDR *net.IPNet, writeLine(iptablesData, "*filter") writeLine(iptablesData, iptables.MakeChainLine(antreaForwardChain)) + + hasNodeNetworkPolicyRules := len(nodeNetWorkPolicyIPTables) > 0 + if hasNodeNetworkPolicyRules { + for _, chain := range c.fixedNodeNetworkPolicyChains { + writeLine(iptablesData, iptables.MakeChainLine(chain)) + } + for chain := range nodeNetWorkPolicyIPTables { + // Skip these two chains since they are also included in fixedNodeNetworkPolicyChains. + if chain == config.NodeNetworkPolicyIngressRulesChain || chain == config.NodeNetworkPolicyEgressRulesChain { + continue + } + writeLine(iptablesData, iptables.MakeChainLine(chain)) + } + } writeLine(iptablesData, []string{ "-A", antreaForwardChain, "-m", "comment", "--comment", `"Antrea: accept packets from local Pods"`, @@ -694,6 +875,16 @@ func (c *Client) restoreIptablesData(podCIDR *net.IPNet, "-j", iptables.AcceptTarget, }...) } + if hasNodeNetworkPolicyRules { + for _, rule := range fixedNodeNetWorkPolicyIPTables { + writeLine(iptablesData, rule) + } + for _, rules := range nodeNetWorkPolicyIPTables { + for _, rule := range rules { + writeLine(iptablesData, rule) + } + } + } writeLine(iptablesData, "COMMIT") writeLine(iptablesData, "*nat") @@ -863,6 +1054,208 @@ func (c *Client) initServiceIPRoutes() error { return nil } +func (c *Client) initNodeNetworkPolicy() { + if c.networkConfig.IPv4Enabled { + c.nodeNetworkPolicyIPTablesIPv4 = newNodeNetworkPolicyIPTablesCache(iptables.ProtocolIPv4, + c.nodeNetworkPolicyPreFunc, + c.nodeNetworkPolicyPostFunc) + } + if c.networkConfig.IPv6Enabled { + c.nodeNetworkPolicyIPTablesIPv6 = newNodeNetworkPolicyIPTablesCache(iptables.ProtocolIPv6, + c.nodeNetworkPolicyPreFunc, + c.nodeNetworkPolicyPostFunc) + } + c.fixedNodeNetworkPolicyChains = []string{ + antreaInputChain, + antreaOutputChain, + privilegedNodeNetworkPolicyIngressRulesChain, + privilegedNodeNetworkPolicyEgressRulesChain, + config.NodeNetworkPolicyIngressRulesChain, + config.NodeNetworkPolicyEgressRulesChain, + defaultNodeNetworkPolicyIngressRulesChain, + defaultNodeNetworkPolicyEgressRulesChain, + } + c.buildFixedNodeNetworkPolicyIPTablesRules(c.networkConfig.IPv4Enabled, c.networkConfig.IPv6Enabled) +} + +func (c *Client) buildFixedNodeNetworkPolicyIPTablesRules(ipv4Enabled, ipv6Enabled bool) { + var ipProtocols []iptables.Protocol + if ipv4Enabled { + ipProtocols = append(ipProtocols, iptables.ProtocolIPv4) + } + if ipv6Enabled { + ipProtocols = append(ipProtocols, iptables.ProtocolIPv6) + } + + antreaInputChainRules := []string{ + iptables.NewRuleBuilder(antreaInputChain). + SetComment("Antrea: jump to privileged ingress NodeNetworkPolicy rules"). + SetTarget(privilegedNodeNetworkPolicyIngressRulesChain). + Done(). + GetRule(), + iptables.NewRuleBuilder(antreaInputChain). + SetComment("Antrea: jump to ingress NodeNetworkPolicy rules"). + SetTarget(config.NodeNetworkPolicyIngressRulesChain). + Done(). + GetRule(), + iptables.NewRuleBuilder(antreaInputChain). + SetComment("Antrea: jump to default ingress NodeNetworkPolicy rules"). + SetTarget(defaultNodeNetworkPolicyIngressRulesChain). + Done(). + GetRule(), + } + antreaOutputChainRules := []string{ + iptables.NewRuleBuilder(antreaOutputChain). + SetComment("Antrea: jump to privileged egress NodeNetworkPolicy rules"). + SetTarget(privilegedNodeNetworkPolicyEgressRulesChain). + Done(). + GetRule(), + iptables.NewRuleBuilder(antreaOutputChain). + SetComment("Antrea: jump to egress NodeNetworkPolicy rules"). + SetTarget(config.NodeNetworkPolicyEgressRulesChain). + Done(). + GetRule(), + iptables.NewRuleBuilder(antreaOutputChain). + SetComment("Antrea: jump to default egress NodeNetworkPolicy rules"). + SetTarget(defaultNodeNetworkPolicyEgressRulesChain). + Done(). + GetRule(), + } + privilegedIngressChainRules := []string{ + iptables.NewRuleBuilder(privilegedNodeNetworkPolicyIngressRulesChain). + MatchEstablishedOrRelated(). + SetComment("Antrea: allow ingress established or related packets"). + SetTarget(iptables.AcceptTarget). + Done(). + GetRule(), + iptables.NewRuleBuilder(privilegedNodeNetworkPolicyIngressRulesChain). + MatchInputInterface("lo"). + SetComment("Antrea: allow ingress packets from loopback"). + SetTarget(iptables.AcceptTarget). + Done(). + GetRule(), + } + privilegedEgressChainRules := []string{ + iptables.NewRuleBuilder(privilegedNodeNetworkPolicyEgressRulesChain). + MatchEstablishedOrRelated(). + SetComment("Antrea: allow egress established or related packets"). + SetTarget(iptables.AcceptTarget). + Done(). + GetRule(), + iptables.NewRuleBuilder(privilegedNodeNetworkPolicyEgressRulesChain). + MatchOutputInterface("lo"). + SetComment("Antrea: allow egress packets to loopback"). + SetTarget(iptables.AcceptTarget). + Done(). + GetRule(), + } + defaultIngressChainRules := []string{ + iptables.NewRuleBuilder(defaultNodeNetworkPolicyIngressRulesChain). + SetComment("Antrea: default ingress rule to drop any packet"). + SetTarget(iptables.DropTarget). + Done(). + GetRule(), + } + defaultEgressChainRules := []string{ + iptables.NewRuleBuilder(defaultNodeNetworkPolicyEgressRulesChain). + SetComment("Antrea: default egress rule to drop any packet"). + SetTarget(iptables.DropTarget). + Done(). + GetRule(), + } + for _, ipProtocol := range ipProtocols { + if ipProtocol == iptables.ProtocolIPv6 { + c.fixedNodeNetworkPolicyIPTablesIPv6 = append(c.fixedNodeNetworkPolicyIPTablesIPv6, antreaInputChainRules...) + c.fixedNodeNetworkPolicyIPTablesIPv6 = append(c.fixedNodeNetworkPolicyIPTablesIPv6, privilegedIngressChainRules...) + c.fixedNodeNetworkPolicyIPTablesIPv6 = append(c.fixedNodeNetworkPolicyIPTablesIPv6, buildPrivilegedChainRulesFromConfig( + privilegedNodeNetworkPolicyIngressRulesChain, + c.nodeNetworkPolicyConfig.PrivilegedIngressRules, + ipProtocol)...) + c.fixedNodeNetworkPolicyIPTablesIPv6 = append(c.fixedNodeNetworkPolicyIPTablesIPv6, defaultIngressChainRules...) + + c.fixedNodeNetworkPolicyIPTablesIPv6 = append(c.fixedNodeNetworkPolicyIPTablesIPv6, antreaOutputChainRules...) + c.fixedNodeNetworkPolicyIPTablesIPv6 = append(c.fixedNodeNetworkPolicyIPTablesIPv6, privilegedEgressChainRules...) + c.fixedNodeNetworkPolicyIPTablesIPv6 = append(c.fixedNodeNetworkPolicyIPTablesIPv6, buildPrivilegedChainRulesFromConfig( + privilegedNodeNetworkPolicyEgressRulesChain, + c.nodeNetworkPolicyConfig.PrivilegedEgressRules, + ipProtocol)...) + c.fixedNodeNetworkPolicyIPTablesIPv6 = append(c.fixedNodeNetworkPolicyIPTablesIPv6, defaultEgressChainRules...) + } + if ipProtocol == iptables.ProtocolIPv4 { + c.fixedNodeNetworkPolicyIPTablesIPv4 = append(c.fixedNodeNetworkPolicyIPTablesIPv4, antreaInputChainRules...) + c.fixedNodeNetworkPolicyIPTablesIPv4 = append(c.fixedNodeNetworkPolicyIPTablesIPv4, privilegedIngressChainRules...) + c.fixedNodeNetworkPolicyIPTablesIPv4 = append(c.fixedNodeNetworkPolicyIPTablesIPv4, buildPrivilegedChainRulesFromConfig( + privilegedNodeNetworkPolicyIngressRulesChain, + c.nodeNetworkPolicyConfig.PrivilegedIngressRules, + ipProtocol)...) + c.fixedNodeNetworkPolicyIPTablesIPv4 = append(c.fixedNodeNetworkPolicyIPTablesIPv4, defaultIngressChainRules...) + + c.fixedNodeNetworkPolicyIPTablesIPv4 = append(c.fixedNodeNetworkPolicyIPTablesIPv4, antreaOutputChainRules...) + c.fixedNodeNetworkPolicyIPTablesIPv4 = append(c.fixedNodeNetworkPolicyIPTablesIPv4, privilegedEgressChainRules...) + c.fixedNodeNetworkPolicyIPTablesIPv4 = append(c.fixedNodeNetworkPolicyIPTablesIPv4, buildPrivilegedChainRulesFromConfig( + privilegedNodeNetworkPolicyEgressRulesChain, + c.nodeNetworkPolicyConfig.PrivilegedEgressRules, + ipProtocol)...) + c.fixedNodeNetworkPolicyIPTablesIPv4 = append(c.fixedNodeNetworkPolicyIPTablesIPv4, defaultEgressChainRules...) + } + } +} + +func buildPrivilegedChainRulesFromConfig(chain string, rules []agentconfig.PrivilegedRule, ipProtocol iptables.Protocol) []string { + baseBuilder := iptables.NewRuleBuilder(chain) + var iptRules []string + for _, rule := range rules { + switch rule.Protocol { + case iptables.ProtocolTCP: + fallthrough + case iptables.ProtocolUDP: + fallthrough + case iptables.ProtocolSCTP: + for _, portStr := range rule.Ports { + startPort, endPort := getPortRange(portStr) + iptRule := baseBuilder.CopyBuilder(). + MatchTransProtocol(rule.Protocol). + MatchDstPort(startPort, endPort). + SetComment(fmt.Sprintf("Antrea: privileged NodeNetworkPolicy: %s", rule.Comment)). + SetTarget(iptables.AcceptTarget). + Done() + iptRules = append(iptRules, iptRule.GetRule()) + } + case iptables.ProtocolICMP: + if ipProtocol == iptables.ProtocolIPv4 { + iptRule := baseBuilder.CopyBuilder(). + MatchICMP(rule.ICMPType, rule.ICMPCode, ipProtocol). + SetComment(fmt.Sprintf("Antrea: privileged NodeNetworkPolicy: %s", rule.Comment)). + SetTarget(iptables.AcceptTarget). + Done() + iptRules = append(iptRules, iptRule.GetRule()) + } + case iptables.ProtocolICMPv6: + if ipProtocol == iptables.ProtocolIPv6 { + iptRule := baseBuilder.CopyBuilder(). + MatchICMP(rule.ICMPType, rule.ICMPCode, ipProtocol). + SetComment(fmt.Sprintf("Antrea: privileged NodeNetworkPolicy: %s", rule.Comment)). + SetTarget(iptables.AcceptTarget). + Done() + iptRules = append(iptRules, iptRule.GetRule()) + } + } + } + return iptRules +} + +func getPortRange(portStr string) (*intstr.IntOrString, *int32) { + parts := strings.Split(portStr, ":") + startPort := intstr.Parse(parts[0]) + var endPort *int32 + if len(parts) == 2 { + portInt, _ := strconv.Atoi(parts[1]) + portInt32 := int32(portInt) // nolint: gosec + endPort = &portInt32 + } + return &startPort, endPort +} + // Reconcile removes orphaned podCIDRs from ipset and removes routes to orphaned podCIDRs // based on the desired podCIDRs. func (c *Client) Reconcile(podCIDRs []string) error { @@ -1700,3 +2093,130 @@ func generateNeigh(ip net.IP, linkIndex int) *netlink.Neigh { HardwareAddr: globalVMAC, } } + +func (c *Client) AddOrUpdateNodeNetworkPolicyIPSet(ipsetName string, prevIPSetEntries, curIPSetEntries sets.Set[string], isIPv6 bool) error { + ipsetEntriesToAdd := curIPSetEntries.Difference(prevIPSetEntries) + ipsetEntriesToDelete := prevIPSetEntries.Difference(curIPSetEntries) + + if err := c.ipset.CreateIPSet(ipsetName, ipset.HashNet, isIPv6); err != nil { + return err + } + for ipsetEntry := range ipsetEntriesToAdd { + if err := c.ipset.AddEntry(ipsetName, ipsetEntry); err != nil { + return err + } + } + for ipsetEntry := range ipsetEntriesToDelete { + if err := c.ipset.DelEntry(ipsetName, ipsetEntry); err != nil { + return err + } + } + if isIPv6 { + c.nodeNetworkPolicyIPSetsIPv6.Store(ipsetName, curIPSetEntries) + } else { + c.nodeNetworkPolicyIPSetsIPv4.Store(ipsetName, curIPSetEntries) + } + return nil +} + +func (c *Client) DeleteNodeNetworkPolicyIPSet(ipsetName string, isIPv6 bool) error { + if err := c.ipset.DestroyIPSet(ipsetName); err != nil { + return err + } + if isIPv6 { + c.nodeNetworkPolicyIPSetsIPv6.Delete(ipsetName) + } else { + c.nodeNetworkPolicyIPSetsIPv4.Delete(ipsetName) + } + return nil +} + +func (c *Client) AddOrUpdateNodeNetworkPolicyIPTables(iptablesChains []string, iptablesRules [][]string, isIPv6 bool) error { + iptablesData := bytes.NewBuffer(nil) + + writeLine(iptablesData, "*filter") + for _, iptablesChain := range iptablesChains { + writeLine(iptablesData, iptables.MakeChainLine(iptablesChain)) + } + for _, rules := range iptablesRules { + for _, rule := range rules { + writeLine(iptablesData, rule) + } + } + writeLine(iptablesData, "COMMIT") + + if err := c.iptables.Restore(iptablesData.String(), false, isIPv6); err != nil { + return err + } + if isIPv6 { + return c.nodeNetworkPolicyIPTablesIPv6.store(iptablesChains, iptablesRules) + } else { + return c.nodeNetworkPolicyIPTablesIPv4.store(iptablesChains, iptablesRules) + } +} + +func (c *Client) DeleteNodeNetworkPolicyIPTables(iptablesChains []string, isIPv6 bool) error { + ipProtocol := iptables.ProtocolIPv4 + if isIPv6 { + ipProtocol = iptables.ProtocolIPv6 + } + for _, iptablesChain := range iptablesChains { + if err := c.iptables.DeleteChain(ipProtocol, iptables.FilterTable, iptablesChain); err != nil { + return err + } + } + if isIPv6 { + return c.nodeNetworkPolicyIPTablesIPv6.delete(iptablesChains) + } else { + return c.nodeNetworkPolicyIPTablesIPv4.delete(iptablesChains) + } +} + +func (c *Client) nodeNetworkPolicyPreFunc(ipProtocol iptables.Protocol) error { + iptablesData := bytes.NewBuffer(nil) + + // Restore the fixed chains and rules for NodeNetworkPolicy. + writeLine(iptablesData, "*filter") + for _, chain := range c.fixedNodeNetworkPolicyChains { + writeLine(iptablesData, iptables.MakeChainLine(chain)) + } + var rules []string + if ipProtocol == iptables.ProtocolIPv6 { + rules = c.fixedNodeNetworkPolicyIPTablesIPv6 + } else { + rules = c.fixedNodeNetworkPolicyIPTablesIPv4 + } + for _, rule := range rules { + writeLine(iptablesData, rule) + } + writeLine(iptablesData, "COMMIT") + if err := c.iptables.Restore(iptablesData.String(), false, ipProtocol == iptables.ProtocolIPv6); err != nil { + return err + } + // Add jump rules. + for _, rule := range nodeNetworkPolicyJumpRules { + ruleSpec := []string{"-j", rule.dstChain, "-m", "comment", "--comment", rule.comment} + if err := c.iptables.AppendRule(ipProtocol, rule.table, rule.srcChain, ruleSpec); err != nil { + return err + } + } + + return nil +} + +func (c *Client) nodeNetworkPolicyPostFunc(ipProtocol iptables.Protocol) error { + // Delete the jump rules. + for _, rule := range nodeNetworkPolicyJumpRules { + ruleSpec := []string{"-j", rule.dstChain, "-m", "comment", "--comment", rule.comment} + if err := c.iptables.DeleteRule(ipProtocol, rule.table, rule.srcChain, ruleSpec); err != nil { + return err + } + } + // Delete the fixed chains for NodeNetworkPolicy. + for _, chain := range c.fixedNodeNetworkPolicyChains { + if err := c.iptables.DeleteChain(ipProtocol, iptables.FilterTable, chain); err != nil { + return err + } + } + return nil +} diff --git a/pkg/agent/route/route_linux_test.go b/pkg/agent/route/route_linux_test.go index e9f14ade813..37005335607 100644 --- a/pkg/agent/route/route_linux_test.go +++ b/pkg/agent/route/route_linux_test.go @@ -23,6 +23,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/vishvananda/netlink" "go.uber.org/mock/gomock" + "k8s.io/apimachinery/pkg/util/sets" "antrea.io/antrea/pkg/agent/config" servicecidrtest "antrea.io/antrea/pkg/agent/servicecidr/testing" @@ -32,13 +33,107 @@ import ( "antrea.io/antrea/pkg/agent/util/iptables" iptablestest "antrea.io/antrea/pkg/agent/util/iptables/testing" netlinktest "antrea.io/antrea/pkg/agent/util/netlink/testing" + agentconfig "antrea.io/antrea/pkg/config/agent" "antrea.io/antrea/pkg/ovs/openflow" "antrea.io/antrea/pkg/ovs/ovsconfig" "antrea.io/antrea/pkg/util/ip" ) var ( - nodeConfig = &config.NodeConfig{GatewayConfig: &config.GatewayConfig{LinkIndex: 10}} + icmpType0 = int32(0) + icmpType8 = int32(8) + icmp6Type128 = int32(128) + icmp6Type129 = int32(129) + icmp6Type135 = int32(135) + icmp6Type136 = int32(136) + + nodeConfig = &config.NodeConfig{GatewayConfig: &config.GatewayConfig{LinkIndex: 10}} + nodeNetworkPolicyConfig = &agentconfig.NodeNetworkPolicyConfig{ + PrivilegedIngressRules: []agentconfig.PrivilegedRule{ + { + Protocol: "tcp", + Ports: []string{"22"}, + Comment: "allow ingress SSH traffic", + }, + { + Protocol: "tcp", + Ports: []string{"80", "443"}, + Comment: "allow ingress HTTP/HTTPS traffic", + }, + { + Protocol: "icmp", + ICMPType: &icmpType0, + Comment: "allow ICMP echo-reply traffic", + }, + { + Protocol: "icmp", + ICMPType: &icmpType8, + Comment: "allow ICMP echo-request traffic", + }, + { + Protocol: "icmp6", + ICMPType: &icmp6Type128, + Comment: "allow ICMPv6 echo-request traffic", + }, + { + Protocol: "icmp6", + ICMPType: &icmp6Type129, + Comment: "allow ICMPv6 echo-reply traffic", + }, + { + Protocol: "icmp6", + ICMPType: &icmp6Type135, + Comment: "allow IPv6 Neighbor Solicitation traffic", + }, + { + Protocol: "icmp6", + ICMPType: &icmp6Type136, + Comment: "allow IPv6 Neighbor Advertisement traffic", + }, + }, + PrivilegedEgressRules: []agentconfig.PrivilegedRule{ + { + Protocol: "tcp", + Ports: []string{"53"}, + Comment: "allow egress DNS traffic", + }, + { + Protocol: "udp", + Ports: []string{"53"}, + Comment: "allow egress DNS traffic", + }, + { + Protocol: "icmp", + ICMPType: &icmpType0, + Comment: "allow ICMP echo-reply traffic", + }, + { + Protocol: "icmp", + ICMPType: &icmpType8, + Comment: "allow ICMP echo-request traffic", + }, + { + Protocol: "icmp6", + ICMPType: &icmp6Type128, + Comment: "allow ICMPv6 echo-request traffic", + }, + { + Protocol: "icmp6", + ICMPType: &icmp6Type129, + Comment: "allow ICMPv6 echo-reply traffic", + }, + { + Protocol: "icmp6", + ICMPType: &icmp6Type135, + Comment: "allow IPv6 Neighbor Solicitation traffic", + }, + { + Protocol: "icmp6", + ICMPType: &icmp6Type136, + Comment: "allow IPv6 Neighbor Advertisement traffic", + }, + }, + } externalIPv4Addr1 = "1.1.1.1" externalIPv4Addr2 = "1.1.1.2" @@ -235,22 +330,26 @@ func TestSyncIPSet(t *testing.T) { func TestSyncIPTables(t *testing.T) { tests := []struct { - name string - isCloudEKS bool - proxyAll bool - multicastEnabled bool - connectUplinkToBridge bool - networkConfig *config.NetworkConfig - nodeConfig *config.NodeConfig - nodePortsIPv4 []string - nodePortsIPv6 []string - markToSNATIP map[uint32]string - expectedCalls func(iptables *iptablestest.MockInterfaceMockRecorder) + name string + isCloudEKS bool + proxyAll bool + multicastEnabled bool + connectUplinkToBridge bool + nodeNetworkPolicyEnabled bool + nodeNetworkPolicyConfig *agentconfig.NodeNetworkPolicyConfig + networkConfig *config.NetworkConfig + nodeConfig *config.NodeConfig + nodePortsIPv4 []string + nodePortsIPv6 []string + markToSNATIP map[uint32]string + expectedCalls func(iptables *iptablestest.MockInterfaceMockRecorder) }{ { - name: "encap,egress=true,multicastEnabled=true,proxyAll=true", - proxyAll: true, - multicastEnabled: true, + name: "encap,egress=true,multicastEnabled=true,proxyAll=true,nodeNetworkPolicy=true", + proxyAll: true, + multicastEnabled: true, + nodeNetworkPolicyEnabled: true, + nodeNetworkPolicyConfig: nodeNetworkPolicyConfig, networkConfig: &config.NetworkConfig{ TrafficEncapMode: config.TrafficEncapModeEncap, TunnelType: ovsconfig.GeneveTunnel, @@ -285,6 +384,10 @@ func TestSyncIPTables(t *testing.T) { mockIPTables.AppendRule(iptables.ProtocolDual, iptables.NATTable, iptables.PreRoutingChain, []string{"-j", antreaPreRoutingChain, "-m", "comment", "--comment", "Antrea: jump to Antrea prerouting rules"}) mockIPTables.EnsureChain(iptables.ProtocolDual, iptables.NATTable, antreaOutputChain) mockIPTables.AppendRule(iptables.ProtocolDual, iptables.NATTable, iptables.OutputChain, []string{"-j", antreaOutputChain, "-m", "comment", "--comment", "Antrea: jump to Antrea output rules"}) + mockIPTables.EnsureChain(iptables.ProtocolDual, iptables.FilterTable, antreaInputChain) + mockIPTables.AppendRule(iptables.ProtocolDual, iptables.FilterTable, iptables.InputChain, []string{"-j", antreaInputChain, "-m", "comment", "--comment", "Antrea: jump to Antrea input rules"}) + mockIPTables.EnsureChain(iptables.ProtocolDual, iptables.FilterTable, antreaOutputChain) + mockIPTables.AppendRule(iptables.ProtocolDual, iptables.FilterTable, iptables.OutputChain, []string{"-j", antreaOutputChain, "-m", "comment", "--comment", "Antrea: jump to Antrea output rules"}) mockIPTables.Restore(`*raw :ANTREA-PREROUTING - [0:0] :ANTREA-OUTPUT - [0:0] @@ -299,8 +402,38 @@ COMMIT COMMIT *filter :ANTREA-FORWARD - [0:0] +:ANTREA-INPUT - [0:0] +:ANTREA-OUTPUT - [0:0] +:ANTREA-POL-PRI-INGRESS-RULES - [0:0] +:ANTREA-POL-PRI-EGRESS-RULES - [0:0] +:ANTREA-POL-INGRESS-RULES - [0:0] +:ANTREA-POL-EGRESS-RULES - [0:0] +:ANTREA-POL-DEF-INGRESS-RULES - [0:0] +:ANTREA-POL-DEF-EGRESS-RULES - [0:0] -A ANTREA-FORWARD -m comment --comment "Antrea: accept packets from local Pods" -i antrea-gw0 -j ACCEPT -A ANTREA-FORWARD -m comment --comment "Antrea: accept packets to local Pods" -o antrea-gw0 -j ACCEPT +-A ANTREA-INPUT -m comment --comment "Antrea: jump to privileged ingress NodeNetworkPolicy rules" -j ANTREA-POL-PRI-INGRESS-RULES +-A ANTREA-INPUT -m comment --comment "Antrea: jump to ingress NodeNetworkPolicy rules" -j ANTREA-POL-INGRESS-RULES +-A ANTREA-INPUT -m comment --comment "Antrea: jump to default ingress NodeNetworkPolicy rules" -j ANTREA-POL-DEF-INGRESS-RULES +-A ANTREA-POL-PRI-INGRESS-RULES -m conntrack --ctstate ESTABLISHED,RELATED -m comment --comment "Antrea: allow ingress established or related packets" -j ACCEPT +-A ANTREA-POL-PRI-INGRESS-RULES -i lo -m comment --comment "Antrea: allow ingress packets from loopback" -j ACCEPT +-A ANTREA-POL-PRI-INGRESS-RULES -p tcp --dport 22 -m comment --comment "Antrea: privileged NodeNetworkPolicy: allow ingress SSH traffic" -j ACCEPT +-A ANTREA-POL-PRI-INGRESS-RULES -p tcp --dport 80 -m comment --comment "Antrea: privileged NodeNetworkPolicy: allow ingress HTTP/HTTPS traffic" -j ACCEPT +-A ANTREA-POL-PRI-INGRESS-RULES -p tcp --dport 443 -m comment --comment "Antrea: privileged NodeNetworkPolicy: allow ingress HTTP/HTTPS traffic" -j ACCEPT +-A ANTREA-POL-PRI-INGRESS-RULES -p icmp --icmp-type 0 -m comment --comment "Antrea: privileged NodeNetworkPolicy: allow ICMP echo-reply traffic" -j ACCEPT +-A ANTREA-POL-PRI-INGRESS-RULES -p icmp --icmp-type 8 -m comment --comment "Antrea: privileged NodeNetworkPolicy: allow ICMP echo-request traffic" -j ACCEPT +-A ANTREA-POL-DEF-INGRESS-RULES -m comment --comment "Antrea: default ingress rule to drop any packet" -j DROP +-A ANTREA-OUTPUT -m comment --comment "Antrea: jump to privileged egress NodeNetworkPolicy rules" -j ANTREA-POL-PRI-EGRESS-RULES +-A ANTREA-OUTPUT -m comment --comment "Antrea: jump to egress NodeNetworkPolicy rules" -j ANTREA-POL-EGRESS-RULES +-A ANTREA-OUTPUT -m comment --comment "Antrea: jump to default egress NodeNetworkPolicy rules" -j ANTREA-POL-DEF-EGRESS-RULES +-A ANTREA-POL-PRI-EGRESS-RULES -m conntrack --ctstate ESTABLISHED,RELATED -m comment --comment "Antrea: allow egress established or related packets" -j ACCEPT +-A ANTREA-POL-PRI-EGRESS-RULES -o lo -m comment --comment "Antrea: allow egress packets to loopback" -j ACCEPT +-A ANTREA-POL-PRI-EGRESS-RULES -p tcp --dport 53 -m comment --comment "Antrea: privileged NodeNetworkPolicy: allow egress DNS traffic" -j ACCEPT +-A ANTREA-POL-PRI-EGRESS-RULES -p udp --dport 53 -m comment --comment "Antrea: privileged NodeNetworkPolicy: allow egress DNS traffic" -j ACCEPT +-A ANTREA-POL-PRI-EGRESS-RULES -p icmp --icmp-type 0 -m comment --comment "Antrea: privileged NodeNetworkPolicy: allow ICMP echo-reply traffic" -j ACCEPT +-A ANTREA-POL-PRI-EGRESS-RULES -p icmp --icmp-type 8 -m comment --comment "Antrea: privileged NodeNetworkPolicy: allow ICMP echo-request traffic" -j ACCEPT +-A ANTREA-POL-DEF-EGRESS-RULES -m comment --comment "Antrea: default egress rule to drop any packet" -j DROP +-A ANTREA-POL-INGRESS-RULES -j ACCEPT -m comment --comment "mock rule" COMMIT *nat :ANTREA-PREROUTING - [0:0] @@ -328,8 +461,42 @@ COMMIT COMMIT *filter :ANTREA-FORWARD - [0:0] +:ANTREA-INPUT - [0:0] +:ANTREA-OUTPUT - [0:0] +:ANTREA-POL-PRI-INGRESS-RULES - [0:0] +:ANTREA-POL-PRI-EGRESS-RULES - [0:0] +:ANTREA-POL-INGRESS-RULES - [0:0] +:ANTREA-POL-EGRESS-RULES - [0:0] +:ANTREA-POL-DEF-INGRESS-RULES - [0:0] +:ANTREA-POL-DEF-EGRESS-RULES - [0:0] -A ANTREA-FORWARD -m comment --comment "Antrea: accept packets from local Pods" -i antrea-gw0 -j ACCEPT -A ANTREA-FORWARD -m comment --comment "Antrea: accept packets to local Pods" -o antrea-gw0 -j ACCEPT +-A ANTREA-INPUT -m comment --comment "Antrea: jump to privileged ingress NodeNetworkPolicy rules" -j ANTREA-POL-PRI-INGRESS-RULES +-A ANTREA-INPUT -m comment --comment "Antrea: jump to ingress NodeNetworkPolicy rules" -j ANTREA-POL-INGRESS-RULES +-A ANTREA-INPUT -m comment --comment "Antrea: jump to default ingress NodeNetworkPolicy rules" -j ANTREA-POL-DEF-INGRESS-RULES +-A ANTREA-POL-PRI-INGRESS-RULES -m conntrack --ctstate ESTABLISHED,RELATED -m comment --comment "Antrea: allow ingress established or related packets" -j ACCEPT +-A ANTREA-POL-PRI-INGRESS-RULES -i lo -m comment --comment "Antrea: allow ingress packets from loopback" -j ACCEPT +-A ANTREA-POL-PRI-INGRESS-RULES -p tcp --dport 22 -m comment --comment "Antrea: privileged NodeNetworkPolicy: allow ingress SSH traffic" -j ACCEPT +-A ANTREA-POL-PRI-INGRESS-RULES -p tcp --dport 80 -m comment --comment "Antrea: privileged NodeNetworkPolicy: allow ingress HTTP/HTTPS traffic" -j ACCEPT +-A ANTREA-POL-PRI-INGRESS-RULES -p tcp --dport 443 -m comment --comment "Antrea: privileged NodeNetworkPolicy: allow ingress HTTP/HTTPS traffic" -j ACCEPT +-A ANTREA-POL-PRI-INGRESS-RULES -p icmpv6 --icmpv6-type 128 -m comment --comment "Antrea: privileged NodeNetworkPolicy: allow ICMPv6 echo-request traffic" -j ACCEPT +-A ANTREA-POL-PRI-INGRESS-RULES -p icmpv6 --icmpv6-type 129 -m comment --comment "Antrea: privileged NodeNetworkPolicy: allow ICMPv6 echo-reply traffic" -j ACCEPT +-A ANTREA-POL-PRI-INGRESS-RULES -p icmpv6 --icmpv6-type 135 -m comment --comment "Antrea: privileged NodeNetworkPolicy: allow IPv6 Neighbor Solicitation traffic" -j ACCEPT +-A ANTREA-POL-PRI-INGRESS-RULES -p icmpv6 --icmpv6-type 136 -m comment --comment "Antrea: privileged NodeNetworkPolicy: allow IPv6 Neighbor Advertisement traffic" -j ACCEPT +-A ANTREA-POL-DEF-INGRESS-RULES -m comment --comment "Antrea: default ingress rule to drop any packet" -j DROP +-A ANTREA-OUTPUT -m comment --comment "Antrea: jump to privileged egress NodeNetworkPolicy rules" -j ANTREA-POL-PRI-EGRESS-RULES +-A ANTREA-OUTPUT -m comment --comment "Antrea: jump to egress NodeNetworkPolicy rules" -j ANTREA-POL-EGRESS-RULES +-A ANTREA-OUTPUT -m comment --comment "Antrea: jump to default egress NodeNetworkPolicy rules" -j ANTREA-POL-DEF-EGRESS-RULES +-A ANTREA-POL-PRI-EGRESS-RULES -m conntrack --ctstate ESTABLISHED,RELATED -m comment --comment "Antrea: allow egress established or related packets" -j ACCEPT +-A ANTREA-POL-PRI-EGRESS-RULES -o lo -m comment --comment "Antrea: allow egress packets to loopback" -j ACCEPT +-A ANTREA-POL-PRI-EGRESS-RULES -p tcp --dport 53 -m comment --comment "Antrea: privileged NodeNetworkPolicy: allow egress DNS traffic" -j ACCEPT +-A ANTREA-POL-PRI-EGRESS-RULES -p udp --dport 53 -m comment --comment "Antrea: privileged NodeNetworkPolicy: allow egress DNS traffic" -j ACCEPT +-A ANTREA-POL-PRI-EGRESS-RULES -p icmpv6 --icmpv6-type 128 -m comment --comment "Antrea: privileged NodeNetworkPolicy: allow ICMPv6 echo-request traffic" -j ACCEPT +-A ANTREA-POL-PRI-EGRESS-RULES -p icmpv6 --icmpv6-type 129 -m comment --comment "Antrea: privileged NodeNetworkPolicy: allow ICMPv6 echo-reply traffic" -j ACCEPT +-A ANTREA-POL-PRI-EGRESS-RULES -p icmpv6 --icmpv6-type 135 -m comment --comment "Antrea: privileged NodeNetworkPolicy: allow IPv6 Neighbor Solicitation traffic" -j ACCEPT +-A ANTREA-POL-PRI-EGRESS-RULES -p icmpv6 --icmpv6-type 136 -m comment --comment "Antrea: privileged NodeNetworkPolicy: allow IPv6 Neighbor Advertisement traffic" -j ACCEPT +-A ANTREA-POL-DEF-EGRESS-RULES -m comment --comment "Antrea: default egress rule to drop any packet" -j DROP +-A ANTREA-POL-INGRESS-RULES -j ACCEPT -m comment --comment "mock rule" COMMIT *nat :ANTREA-PREROUTING - [0:0] @@ -485,17 +652,28 @@ COMMIT ctrl := gomock.NewController(t) mockIPTables := iptablestest.NewMockInterface(ctrl) c := &Client{iptables: mockIPTables, - networkConfig: tt.networkConfig, - nodeConfig: tt.nodeConfig, - proxyAll: tt.proxyAll, - isCloudEKS: tt.isCloudEKS, - multicastEnabled: tt.multicastEnabled, - connectUplinkToBridge: tt.connectUplinkToBridge, - markToSNATIP: sync.Map{}, + networkConfig: tt.networkConfig, + nodeConfig: tt.nodeConfig, + proxyAll: tt.proxyAll, + isCloudEKS: tt.isCloudEKS, + multicastEnabled: tt.multicastEnabled, + connectUplinkToBridge: tt.connectUplinkToBridge, + nodeNetworkPolicyEnabled: tt.nodeNetworkPolicyEnabled, + nodeNetworkPolicyConfig: tt.nodeNetworkPolicyConfig, } for mark, snatIP := range tt.markToSNATIP { c.markToSNATIP.Store(mark, net.ParseIP(snatIP)) } + if tt.nodeNetworkPolicyEnabled { + c.initNodeNetworkPolicy() + // Insert mock rules to cache, otherwise NodeNetworkPolicy related chains and rules won't be synced.' + c.nodeNetworkPolicyIPTablesIPv4.data[config.NodeNetworkPolicyIngressRulesChain] = []string{ + `-A ANTREA-POL-INGRESS-RULES -j ACCEPT -m comment --comment "mock rule" `, + } + c.nodeNetworkPolicyIPTablesIPv6.data[config.NodeNetworkPolicyIngressRulesChain] = []string{ + `-A ANTREA-POL-INGRESS-RULES -j ACCEPT -m comment --comment "mock rule" `, + } + } tt.expectedCalls(mockIPTables.EXPECT()) assert.NoError(t, c.syncIPTables()) }) @@ -1721,3 +1899,260 @@ func TestAddAndDeleteNodeIP(t *testing.T) { }) } } + +func TestAddAndDeleteNodeNetworkPolicyIPSet(t *testing.T) { + ipv4SetName := "TEST-IPSET-4" + ipv4Net1 := "1.1.1.1/32" + ipv4Net2 := "2.2.2.2/32" + ipv4Net3 := "3.3.3.3/32" + ipv6SetName := "TEST-IPSET-6" + ipv6Net1 := "fec0::1111/128" + ipv6Net2 := "fec0::2222/128" + ipv6Net3 := "fec0::3333/128" + + tests := []struct { + name string + ipsetName string + prevIPSetEntries sets.Set[string] + curIPSetEntries sets.Set[string] + isIPv6 bool + expectedCalls func(mockIPSet *ipsettest.MockInterfaceMockRecorder) + }{ + { + name: "IPv4, add an ipset and delete it", + ipsetName: ipv4SetName, + curIPSetEntries: sets.New[string](ipv4Net1, ipv4Net3), + isIPv6: false, + expectedCalls: func(mockIPSet *ipsettest.MockInterfaceMockRecorder) { + mockIPSet.CreateIPSet(ipv4SetName, ipset.HashNet, false).Times(1) + mockIPSet.AddEntry(ipv4SetName, ipv4Net1).Times(1) + mockIPSet.AddEntry(ipv4SetName, ipv4Net3).Times(1) + mockIPSet.DestroyIPSet(ipv4SetName).Times(1) + }, + }, + { + name: "IPv4, update an ipset and delete it", + ipsetName: ipv4SetName, + prevIPSetEntries: sets.New[string](ipv4Net1, ipv4Net2), + curIPSetEntries: sets.New[string](ipv4Net1, ipv4Net3), + isIPv6: false, + expectedCalls: func(mockIPSet *ipsettest.MockInterfaceMockRecorder) { + mockIPSet.CreateIPSet(ipv4SetName, ipset.HashNet, false).Times(1) + mockIPSet.AddEntry(ipv4SetName, ipv4Net3).Times(1) + mockIPSet.DelEntry(ipv4SetName, ipv4Net2).Times(1) + mockIPSet.DestroyIPSet(ipv4SetName).Times(1) + }, + }, + { + name: "IPv6, add an ipset and delete it", + ipsetName: ipv6SetName, + curIPSetEntries: sets.New[string](ipv6Net1, ipv6Net3), + isIPv6: true, + expectedCalls: func(mockIPSet *ipsettest.MockInterfaceMockRecorder) { + mockIPSet.CreateIPSet(ipv6SetName, ipset.HashNet, true).Times(1) + mockIPSet.AddEntry(ipv6SetName, ipv6Net1).Times(1) + mockIPSet.AddEntry(ipv6SetName, ipv6Net3).Times(1) + mockIPSet.DestroyIPSet(ipv6SetName).Times(1) + }, + }, + { + name: "IPv6, update an ipset and delete it", + ipsetName: ipv6SetName, + prevIPSetEntries: sets.New[string](ipv6Net1, ipv6Net2), + curIPSetEntries: sets.New[string](ipv6Net1, ipv6Net3), + isIPv6: true, + expectedCalls: func(mockIPSet *ipsettest.MockInterfaceMockRecorder) { + mockIPSet.CreateIPSet(ipv6SetName, ipset.HashNet, true).Times(1) + mockIPSet.AddEntry(ipv6SetName, ipv6Net3).Times(1) + mockIPSet.DelEntry(ipv6SetName, ipv6Net2).Times(1) + mockIPSet.DestroyIPSet(ipv6SetName).Times(1) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + mockIPSet := ipsettest.NewMockInterface(ctrl) + c := &Client{ipset: mockIPSet} + tt.expectedCalls(mockIPSet.EXPECT()) + + assert.NoError(t, c.AddOrUpdateNodeNetworkPolicyIPSet(tt.ipsetName, tt.prevIPSetEntries, tt.curIPSetEntries, tt.isIPv6)) + var exists bool + if tt.isIPv6 { + _, exists = c.nodeNetworkPolicyIPSetsIPv6.Load(tt.ipsetName) + } else { + _, exists = c.nodeNetworkPolicyIPSetsIPv4.Load(tt.ipsetName) + } + assert.True(t, exists) + + assert.NoError(t, c.DeleteNodeNetworkPolicyIPSet(tt.ipsetName, tt.isIPv6)) + if tt.isIPv6 { + _, exists = c.nodeNetworkPolicyIPSetsIPv6.Load(tt.ipsetName) + } else { + _, exists = c.nodeNetworkPolicyIPSetsIPv4.Load(tt.ipsetName) + } + assert.False(t, exists) + }) + } +} + +func TestAddAndDeleteNodeNetworkPolicyIPTables(t *testing.T) { + ruleChain1 := "TEST-CHAIN1" + ruleChain2 := "TEST-CHAIN2" + privilegedRulesChain := "PRIVILEGED-RULES" + defaultRulesChain := "DEFAULT-RULES" + + chains := []string{ruleChain1, ruleChain2} + rules := [][]string{ + { + "-A TEST-CHAIN1 -j ACCEPT -p tcp --dport 80", + "-A TEST-CHAIN1 -j ACCEPT -p tcp --dport 443", + }, + { + "-A TEST-CHAIN2 -j ACCEPT -p tcp --dport 80", + "-A TEST-CHAIN2 -j ACCEPT -p tcp --dport 443", + }, + } + fixedRules := []string{ + "-A PRIVILEGED-RULES -j ACCEPT -p tcp --dport 6443", + "-A DEFAULT-RULES -j DROP", + } + tests := []struct { + name string + iptablesChains []string + iptablesRules [][]string + isIPv6 bool + expectedCalls func(mockIPTables *iptablestest.MockInterfaceMockRecorder) + expectedRules map[string][]string + }{ + { + name: "IPv4", + iptablesChains: chains, + iptablesRules: rules, + isIPv6: false, + expectedCalls: func(mockIPTables *iptablestest.MockInterfaceMockRecorder) { + mockIPTables.Restore(`*filter +:TEST-CHAIN1 - [0:0] +:TEST-CHAIN2 - [0:0] +-A TEST-CHAIN1 -j ACCEPT -p tcp --dport 80 +-A TEST-CHAIN1 -j ACCEPT -p tcp --dport 443 +-A TEST-CHAIN2 -j ACCEPT -p tcp --dport 80 +-A TEST-CHAIN2 -j ACCEPT -p tcp --dport 443 +COMMIT +`, false, false) + mockIPTables.Restore(`*filter +:PRIVILEGED-RULES - [0:0] +:DEFAULT-RULES - [0:0] +-A PRIVILEGED-RULES -j ACCEPT -p tcp --dport 6443 +-A DEFAULT-RULES -j DROP +COMMIT +`, false, false) + mockIPTables.AppendRule(iptables.ProtocolIPv4, iptables.FilterTable, iptables.InputChain, gomock.Any()).Times(1) + mockIPTables.AppendRule(iptables.ProtocolIPv4, iptables.FilterTable, iptables.OutputChain, gomock.Any()).Times(1) + + mockIPTables.DeleteRule(iptables.ProtocolIPv4, iptables.FilterTable, iptables.InputChain, gomock.Any()).Times(1) + mockIPTables.DeleteRule(iptables.ProtocolIPv4, iptables.FilterTable, iptables.OutputChain, gomock.Any()).Times(1) + mockIPTables.DeleteChain(iptables.ProtocolIPv4, iptables.FilterTable, ruleChain1).Times(1) + mockIPTables.DeleteChain(iptables.ProtocolIPv4, iptables.FilterTable, ruleChain2).Times(1) + mockIPTables.DeleteChain(iptables.ProtocolIPv4, iptables.FilterTable, privilegedRulesChain).Times(1) + mockIPTables.DeleteChain(iptables.ProtocolIPv4, iptables.FilterTable, defaultRulesChain).Times(1) + }, + expectedRules: map[string][]string{ + "TEST-CHAIN1": { + "-A TEST-CHAIN1 -j ACCEPT -p tcp --dport 80", + "-A TEST-CHAIN1 -j ACCEPT -p tcp --dport 443", + }, + "TEST-CHAIN2": { + "-A TEST-CHAIN2 -j ACCEPT -p tcp --dport 80", + "-A TEST-CHAIN2 -j ACCEPT -p tcp --dport 443", + }, + }, + }, + { + name: "IPv6", + iptablesChains: chains, + iptablesRules: rules, + isIPv6: true, + expectedCalls: func(mockIPTables *iptablestest.MockInterfaceMockRecorder) { + mockIPTables.Restore(`*filter +:TEST-CHAIN1 - [0:0] +:TEST-CHAIN2 - [0:0] +-A TEST-CHAIN1 -j ACCEPT -p tcp --dport 80 +-A TEST-CHAIN1 -j ACCEPT -p tcp --dport 443 +-A TEST-CHAIN2 -j ACCEPT -p tcp --dport 80 +-A TEST-CHAIN2 -j ACCEPT -p tcp --dport 443 +COMMIT +`, false, true) + mockIPTables.Restore(`*filter +:PRIVILEGED-RULES - [0:0] +:DEFAULT-RULES - [0:0] +-A PRIVILEGED-RULES -j ACCEPT -p tcp --dport 6443 +-A DEFAULT-RULES -j DROP +COMMIT +`, false, true) + mockIPTables.AppendRule(iptables.ProtocolIPv6, iptables.FilterTable, iptables.InputChain, gomock.Any()).Times(1) + mockIPTables.AppendRule(iptables.ProtocolIPv6, iptables.FilterTable, iptables.OutputChain, gomock.Any()).Times(1) + + mockIPTables.DeleteRule(iptables.ProtocolIPv6, iptables.FilterTable, iptables.InputChain, gomock.Any()).Times(1) + mockIPTables.DeleteRule(iptables.ProtocolIPv6, iptables.FilterTable, iptables.OutputChain, gomock.Any()).Times(1) + mockIPTables.DeleteChain(iptables.ProtocolIPv6, iptables.FilterTable, ruleChain1).Times(1) + mockIPTables.DeleteChain(iptables.ProtocolIPv6, iptables.FilterTable, ruleChain2).Times(1) + mockIPTables.DeleteChain(iptables.ProtocolIPv6, iptables.FilterTable, privilegedRulesChain).Times(1) + mockIPTables.DeleteChain(iptables.ProtocolIPv6, iptables.FilterTable, defaultRulesChain).Times(1) + }, + expectedRules: map[string][]string{ + "TEST-CHAIN1": { + "-A TEST-CHAIN1 -j ACCEPT -p tcp --dport 80", + "-A TEST-CHAIN1 -j ACCEPT -p tcp --dport 443", + }, + "TEST-CHAIN2": { + "-A TEST-CHAIN2 -j ACCEPT -p tcp --dport 80", + "-A TEST-CHAIN2 -j ACCEPT -p tcp --dport 443", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + mockIPTables := iptablestest.NewMockInterface(ctrl) + c := &Client{iptables: mockIPTables} + c.nodeNetworkPolicyIPTablesIPv4 = newNodeNetworkPolicyIPTablesCache(iptables.ProtocolIPv4, + c.nodeNetworkPolicyPreFunc, + c.nodeNetworkPolicyPostFunc, + ) + c.nodeNetworkPolicyIPTablesIPv6 = newNodeNetworkPolicyIPTablesCache(iptables.ProtocolIPv6, + c.nodeNetworkPolicyPreFunc, + c.nodeNetworkPolicyPostFunc, + ) + c.fixedNodeNetworkPolicyChains = []string{privilegedRulesChain, defaultRulesChain} + c.fixedNodeNetworkPolicyIPTablesIPv4 = fixedRules + c.fixedNodeNetworkPolicyIPTablesIPv6 = fixedRules + + tt.expectedCalls(mockIPTables.EXPECT()) + + assert.NoError(t, c.AddOrUpdateNodeNetworkPolicyIPTables(tt.iptablesChains, tt.iptablesRules, tt.isIPv6)) + for _, chain := range chains { + var gotRules []string + if tt.isIPv6 { + gotRules = c.nodeNetworkPolicyIPTablesIPv6.load(chain) + } else { + gotRules = c.nodeNetworkPolicyIPTablesIPv4.load(chain) + } + assert.EqualValues(t, tt.expectedRules[chain], gotRules) + } + + assert.NoError(t, c.DeleteNodeNetworkPolicyIPTables(tt.iptablesChains, tt.isIPv6)) + for _, chain := range chains { + var gotRules []string + if tt.isIPv6 { + gotRules = c.nodeNetworkPolicyIPTablesIPv6.load(chain) + } else { + gotRules = c.nodeNetworkPolicyIPTablesIPv4.load(chain) + } + assert.Equal(t, []string{}, gotRules) + } + }) + } +} diff --git a/pkg/agent/route/route_windows.go b/pkg/agent/route/route_windows.go index 7a67d6475b7..ccb780a8455 100644 --- a/pkg/agent/route/route_windows.go +++ b/pkg/agent/route/route_windows.go @@ -33,6 +33,7 @@ import ( "antrea.io/antrea/pkg/agent/util" antreasyscall "antrea.io/antrea/pkg/agent/util/syscall" "antrea.io/antrea/pkg/agent/util/winfirewall" + agentconfig "antrea.io/antrea/pkg/config/agent" binding "antrea.io/antrea/pkg/ovs/openflow" iputil "antrea.io/antrea/pkg/util/ip" ) @@ -71,7 +72,14 @@ type Client struct { } // NewClient returns a route client. -func NewClient(networkConfig *config.NetworkConfig, noSNAT, proxyAll, connectUplinkToBridge, multicastEnabled bool, serviceCIDRProvider servicecidr.Interface) (*Client, error) { +func NewClient(networkConfig *config.NetworkConfig, + noSNAT bool, + proxyAll bool, + connectUplinkToBridge bool, + nodeNetworkPolicyEnabled bool, + nodeNetworkPolicyConfig *agentconfig.NodeNetworkPolicyConfig, + multicastEnabled bool, + serviceCIDRProvider servicecidr.Interface) (*Client, error) { return &Client{ networkConfig: networkConfig, nodeRoutes: &sync.Map{}, @@ -573,3 +581,19 @@ func (c *Client) DeleteRouteForLink(dstCIDR *net.IPNet, linkIndex int) error { func (c *Client) ClearConntrackEntryForService(svcIP net.IP, svcPort uint16, endpointIP net.IP, protocol binding.Protocol) error { return errors.New("ClearConntrackEntryForService is not implemented on Windows") } + +func (c *Client) AddOrUpdateNodeNetworkPolicyIPSet(ipsetName string, prevIPSetEntries, curIPSetEntries sets.Set[string], isIPv6 bool) error { + return errors.New("AddOrUpdateNodeNetworkPolicyIPSet is not implemented on Windows") +} + +func (c *Client) DeleteNodeNetworkPolicyIPSet(ipsetName string, isIPv6 bool) error { + return errors.New("DeleteNodeNetworkPolicyIPSet is not implemented on Windows") +} + +func (c *Client) AddOrUpdateNodeNetworkPolicyIPTables(iptablesChains []string, iptablesRules [][]string, isIPv6 bool) error { + return errors.New("AddOrUpdateNodeNetworkPolicyIPTables is not implemented on Windows") +} + +func (c *Client) DeleteNodeNetworkPolicyIPTables(iptablesChains []string, isIPv6 bool) error { + return errors.New("DeleteNodeNetworkPolicyIPTables is not implemented on Windows") +} diff --git a/pkg/agent/route/route_windows_test.go b/pkg/agent/route/route_windows_test.go index c29afd469dc..e919c0f240e 100644 --- a/pkg/agent/route/route_windows_test.go +++ b/pkg/agent/route/route_windows_test.go @@ -61,7 +61,7 @@ func TestRouteOperation(t *testing.T) { gwIP2 := net.ParseIP("192.168.3.1") _, destCIDR2, _ := net.ParseCIDR(dest2) - client, err := NewClient(&config.NetworkConfig{}, true, false, false, false, nil) + client, err := NewClient(&config.NetworkConfig{}, true, false, false, false, nil, false, nil) require.Nil(t, err) called := false diff --git a/pkg/agent/route/testing/mock_route.go b/pkg/agent/route/testing/mock_route.go index c6a3e39921f..f945652f004 100644 --- a/pkg/agent/route/testing/mock_route.go +++ b/pkg/agent/route/testing/mock_route.go @@ -30,6 +30,7 @@ import ( config "antrea.io/antrea/pkg/agent/config" openflow "antrea.io/antrea/pkg/ovs/openflow" gomock "go.uber.org/mock/gomock" + sets "k8s.io/apimachinery/pkg/util/sets" ) // MockInterface is a mock of Interface interface. @@ -97,6 +98,34 @@ func (mr *MockInterfaceMockRecorder) AddNodePort(arg0, arg1, arg2 any) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddNodePort", reflect.TypeOf((*MockInterface)(nil).AddNodePort), arg0, arg1, arg2) } +// AddOrUpdateNodeNetworkPolicyIPSet mocks base method. +func (m *MockInterface) AddOrUpdateNodeNetworkPolicyIPSet(arg0 string, arg1, arg2 sets.Set[string], arg3 bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddOrUpdateNodeNetworkPolicyIPSet", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddOrUpdateNodeNetworkPolicyIPSet indicates an expected call of AddOrUpdateNodeNetworkPolicyIPSet. +func (mr *MockInterfaceMockRecorder) AddOrUpdateNodeNetworkPolicyIPSet(arg0, arg1, arg2, arg3 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddOrUpdateNodeNetworkPolicyIPSet", reflect.TypeOf((*MockInterface)(nil).AddOrUpdateNodeNetworkPolicyIPSet), arg0, arg1, arg2, arg3) +} + +// AddOrUpdateNodeNetworkPolicyIPTables mocks base method. +func (m *MockInterface) AddOrUpdateNodeNetworkPolicyIPTables(arg0 []string, arg1 [][]string, arg2 bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddOrUpdateNodeNetworkPolicyIPTables", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddOrUpdateNodeNetworkPolicyIPTables indicates an expected call of AddOrUpdateNodeNetworkPolicyIPTables. +func (mr *MockInterfaceMockRecorder) AddOrUpdateNodeNetworkPolicyIPTables(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddOrUpdateNodeNetworkPolicyIPTables", reflect.TypeOf((*MockInterface)(nil).AddOrUpdateNodeNetworkPolicyIPTables), arg0, arg1, arg2) +} + // AddRouteForLink mocks base method. func (m *MockInterface) AddRouteForLink(arg0 *net.IPNet, arg1 int) error { m.ctrl.T.Helper() @@ -181,6 +210,34 @@ func (mr *MockInterfaceMockRecorder) DeleteLocalAntreaFlexibleIPAMPodRule(arg0 a return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLocalAntreaFlexibleIPAMPodRule", reflect.TypeOf((*MockInterface)(nil).DeleteLocalAntreaFlexibleIPAMPodRule), arg0) } +// DeleteNodeNetworkPolicyIPSet mocks base method. +func (m *MockInterface) DeleteNodeNetworkPolicyIPSet(arg0 string, arg1 bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteNodeNetworkPolicyIPSet", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteNodeNetworkPolicyIPSet indicates an expected call of DeleteNodeNetworkPolicyIPSet. +func (mr *MockInterfaceMockRecorder) DeleteNodeNetworkPolicyIPSet(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteNodeNetworkPolicyIPSet", reflect.TypeOf((*MockInterface)(nil).DeleteNodeNetworkPolicyIPSet), arg0, arg1) +} + +// DeleteNodeNetworkPolicyIPTables mocks base method. +func (m *MockInterface) DeleteNodeNetworkPolicyIPTables(arg0 []string, arg1 bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteNodeNetworkPolicyIPTables", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteNodeNetworkPolicyIPTables indicates an expected call of DeleteNodeNetworkPolicyIPTables. +func (mr *MockInterfaceMockRecorder) DeleteNodeNetworkPolicyIPTables(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteNodeNetworkPolicyIPTables", reflect.TypeOf((*MockInterface)(nil).DeleteNodeNetworkPolicyIPTables), arg0, arg1) +} + // DeleteNodePort mocks base method. func (m *MockInterface) DeleteNodePort(arg0 []net.IP, arg1 uint16, arg2 openflow.Protocol) error { m.ctrl.T.Helper() diff --git a/pkg/agent/util/iptables/builder.go b/pkg/agent/util/iptables/builder.go new file mode 100644 index 00000000000..e50e8596637 --- /dev/null +++ b/pkg/agent/util/iptables/builder.go @@ -0,0 +1,168 @@ +//go:build !windows +// +build !windows + +// Copyright 2023 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package iptables + +import ( + "fmt" + "strconv" + "strings" + + "k8s.io/apimachinery/pkg/util/intstr" +) + +type iptablesRule struct { + chain string + specs *strings.Builder +} + +type iptablesRuleBuilder struct { + iptablesRule +} + +func NewRuleBuilder(chain string) IPTablesRuleBuilder { + builder := &iptablesRuleBuilder{ + iptablesRule{ + chain: chain, + specs: &strings.Builder{}, + }, + } + return builder +} + +func (b *iptablesRuleBuilder) MatchIPSetSrc(ipset string) IPTablesRuleBuilder { + matchStr := fmt.Sprintf("-m set --match-set %s src ", ipset) + b.specs.WriteString(matchStr) + return b +} + +func (b *iptablesRuleBuilder) MatchIPSetDst(ipset string) IPTablesRuleBuilder { + matchStr := fmt.Sprintf("-m set --match-set %s dst ", ipset) + b.specs.WriteString(matchStr) + return b +} + +func (b *iptablesRuleBuilder) MatchTransProtocol(protocol string) IPTablesRuleBuilder { + matchStr := fmt.Sprintf("-p %s ", protocol) + b.specs.WriteString(matchStr) + return b +} + +func (b *iptablesRuleBuilder) MatchDstPort(port *intstr.IntOrString, endPort *int32) IPTablesRuleBuilder { + if port == nil { + return b + } + var matchStr string + if endPort != nil { + matchStr = fmt.Sprintf("--dport %s:%d ", port.String(), *endPort) + } else { + matchStr = fmt.Sprintf("--dport %s ", port.String()) + } + b.specs.WriteString(matchStr) + return b +} + +func (b *iptablesRuleBuilder) MatchSrcPort(port, endPort *int32) IPTablesRuleBuilder { + if port == nil { + return b + } + var matchStr string + if endPort != nil { + matchStr = fmt.Sprintf("--sport %d:%d ", *port, *endPort) + } else { + matchStr = fmt.Sprintf("--sport %d ", *port) + } + b.specs.WriteString(matchStr) + return b +} + +func (b *iptablesRuleBuilder) MatchICMP(icmpType, icmpCode *int32, ipProtocol Protocol) IPTablesRuleBuilder { + parts := []string{"-p"} + icmpTypeStr := "icmp" + if ipProtocol != ProtocolIPv4 { + icmpTypeStr = "icmpv6" + } + parts = append(parts, icmpTypeStr) + + if icmpType != nil { + icmpTypeFlag := "--icmp-type" + if ipProtocol != ProtocolIPv4 { + icmpTypeFlag = "--icmpv6-type" + } + + if icmpCode != nil { + parts = append(parts, icmpTypeFlag, fmt.Sprintf("%d/%d", *icmpType, *icmpCode)) + } else { + parts = append(parts, icmpTypeFlag, strconv.Itoa(int(*icmpType))) + } + } + b.specs.WriteString(strings.Join(parts, " ")) + b.specs.WriteByte(' ') + + return b +} + +func (b *iptablesRuleBuilder) MatchEstablishedOrRelated() IPTablesRuleBuilder { + b.specs.WriteString("-m conntrack --ctstate ESTABLISHED,RELATED ") + return b +} + +func (b *iptablesRuleBuilder) MatchInputInterface(interfaceName string) IPTablesRuleBuilder { + specStr := fmt.Sprintf("-i %s ", interfaceName) + b.specs.WriteString(specStr) + return b +} + +func (b *iptablesRuleBuilder) MatchOutputInterface(interfaceName string) IPTablesRuleBuilder { + specStr := fmt.Sprintf("-o %s ", interfaceName) + b.specs.WriteString(specStr) + return b +} + +func (b *iptablesRuleBuilder) SetTarget(target string) IPTablesRuleBuilder { + targetStr := fmt.Sprintf("-j %s ", target) + b.specs.WriteString(targetStr) + return b +} + +func (b *iptablesRuleBuilder) SetComment(comment string) IPTablesRuleBuilder { + commentStr := fmt.Sprintf("-m comment --comment \"%s\" ", comment) + b.specs.WriteString(commentStr) + return b +} + +func (b *iptablesRuleBuilder) CopyBuilder() IPTablesRuleBuilder { + var copiedSpec strings.Builder + copiedSpec.Grow(b.specs.Len()) + copiedSpec.WriteString(b.specs.String()) + builder := &iptablesRuleBuilder{ + iptablesRule{ + chain: b.chain, + specs: &copiedSpec, + }, + } + return builder +} + +func (b *iptablesRuleBuilder) Done() IPTablesRule { + return &b.iptablesRule +} + +func (e *iptablesRule) GetRule() string { + ruleStr := fmt.Sprintf("-A %s %s", e.chain, e.specs.String()) + return ruleStr[:len(ruleStr)-1] +} diff --git a/pkg/agent/util/iptables/interfaces.go b/pkg/agent/util/iptables/interfaces.go new file mode 100644 index 00000000000..4a50295e6f8 --- /dev/null +++ b/pkg/agent/util/iptables/interfaces.go @@ -0,0 +1,42 @@ +//go:build !windows +// +build !windows + +// Copyright 2023 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package iptables + +import ( + "k8s.io/apimachinery/pkg/util/intstr" +) + +type IPTablesRuleBuilder interface { + MatchIPSetSrc(ipset string) IPTablesRuleBuilder + MatchIPSetDst(ipset string) IPTablesRuleBuilder + MatchTransProtocol(protocol string) IPTablesRuleBuilder + MatchDstPort(port *intstr.IntOrString, endPort *int32) IPTablesRuleBuilder + MatchSrcPort(port, endPort *int32) IPTablesRuleBuilder + MatchICMP(icmpType, icmpCode *int32, ipProtocol Protocol) IPTablesRuleBuilder + MatchEstablishedOrRelated() IPTablesRuleBuilder + MatchInputInterface(interfaceName string) IPTablesRuleBuilder + MatchOutputInterface(interfaceName string) IPTablesRuleBuilder + SetTarget(target string) IPTablesRuleBuilder + SetComment(comment string) IPTablesRuleBuilder + CopyBuilder() IPTablesRuleBuilder + Done() IPTablesRule +} + +type IPTablesRule interface { + GetRule() string +} diff --git a/pkg/agent/util/iptables/iptables.go b/pkg/agent/util/iptables/iptables.go index 9514c16008d..7089bb17fa2 100644 --- a/pkg/agent/util/iptables/iptables.go +++ b/pkg/agent/util/iptables/iptables.go @@ -36,7 +36,7 @@ const ( RawTable = "raw" AcceptTarget = "ACCEPT" - DROPTarget = "DROP" + DropTarget = "DROP" MasqueradeTarget = "MASQUERADE" MarkTarget = "MARK" ReturnTarget = "RETURN" @@ -44,8 +44,10 @@ const ( NoTrackTarget = "NOTRACK" SNATTarget = "SNAT" DNATTarget = "DNAT" + RejectTarget = "REJECT" PreRoutingChain = "PREROUTING" + InputChain = "INPUT" ForwardChain = "FORWARD" PostRoutingChain = "POSTROUTING" OutputChain = "OUTPUT" @@ -71,6 +73,14 @@ const ( ProtocolIPv6 ) +const ( + ProtocolTCP = "tcp" + ProtocolUDP = "udp" + ProtocolSCTP = "sctp" + ProtocolICMP = "icmp" + ProtocolICMPv6 = "icmp6" +) + // https://netfilter.org/projects/iptables/files/changes-iptables-1.6.2.txt: // iptables-restore: support acquiring the lock. var restoreWaitSupportedMinVersion = semver.Version{Major: 1, Minor: 6, Patch: 2} @@ -352,3 +362,7 @@ func (c *Client) Save() ([]byte, error) { func MakeChainLine(chain string) string { return fmt.Sprintf(":%s - [0:0]", chain) } + +func IsIPv6Protocol(protocol Protocol) bool { + return protocol == ProtocolIPv6 +} diff --git a/pkg/apis/controlplane/v1beta2/types.go b/pkg/apis/controlplane/v1beta2/types.go index 8eae3c3cc4e..32b1ff0ad92 100644 --- a/pkg/apis/controlplane/v1beta2/types.go +++ b/pkg/apis/controlplane/v1beta2/types.go @@ -277,6 +277,7 @@ type NetworkPolicyRule struct { L7Protocols []L7Protocol `json:"l7Protocols,omitempty" protobuf:"bytes,10,rep,name=l7Protocols"` // LogLabel is a user-defined arbitrary string which will be printed in the NetworkPolicy logs. LogLabel string `json:"logLabel,omitempty" protobuf:"bytes,11,opt,name=logLabel"` + // TODO: add a flag IsHNP } // Protocol defines network protocols supported for things like container ports. diff --git a/pkg/apiserver/handlers/featuregates/handler_test.go b/pkg/apiserver/handlers/featuregates/handler_test.go index e7a3d9c1f76..702e1eb9a66 100644 --- a/pkg/apiserver/handlers/featuregates/handler_test.go +++ b/pkg/apiserver/handlers/featuregates/handler_test.go @@ -66,6 +66,7 @@ func Test_getGatesResponse(t *testing.T) { {Component: "agent", Name: "Multicast", Status: multicastStatus, Version: "BETA"}, {Component: "agent", Name: "Multicluster", Status: "Disabled", Version: "ALPHA"}, {Component: "agent", Name: "NetworkPolicyStats", Status: "Enabled", Version: "BETA"}, + {Component: "agent", Name: "NodeNetworkPolicy", Status: "Disabled", Version: "ALPHA"}, {Component: "agent", Name: "NodePortLocal", Status: "Enabled", Version: "GA"}, {Component: "agent", Name: "SecondaryNetwork", Status: "Disabled", Version: "ALPHA"}, {Component: "agent", Name: "ServiceExternalIP", Status: "Disabled", Version: "ALPHA"}, @@ -192,12 +193,14 @@ func Test_getControllerGatesResponse(t *testing.T) { {Component: "controller", Name: "AntreaIPAM", Status: "Disabled", Version: "ALPHA"}, {Component: "controller", Name: "AntreaPolicy", Status: "Enabled", Version: "BETA"}, {Component: "controller", Name: "Egress", Status: egressStatus, Version: "BETA"}, + {Component: "controller", Name: "IPsecCertAuth", Status: "Disabled", Version: "ALPHA"}, {Component: "controller", Name: "L7NetworkPolicy", Status: "Disabled", Version: "ALPHA"}, {Component: "controller", Name: "Multicast", Status: multicastStatus, Version: "BETA"}, {Component: "controller", Name: "Multicluster", Status: "Disabled", Version: "ALPHA"}, {Component: "controller", Name: "NetworkPolicyStats", Status: "Enabled", Version: "BETA"}, {Component: "controller", Name: "NodeIPAM", Status: "Enabled", Version: "BETA"}, + {Component: "controller", Name: "NodeNetworkPolicy", Status: "Disabled", Version: "ALPHA"}, {Component: "controller", Name: "ServiceExternalIP", Status: "Disabled", Version: "ALPHA"}, {Component: "controller", Name: "SupportBundleCollection", Status: "Disabled", Version: "ALPHA"}, {Component: "controller", Name: "Traceflow", Status: "Enabled", Version: "BETA"}, diff --git a/pkg/config/agent/config.go b/pkg/config/agent/config.go index ed57bd00db1..6a15f33891f 100644 --- a/pkg/config/agent/config.go +++ b/pkg/config/agent/config.go @@ -205,6 +205,8 @@ type AgentConfig struct { // second(pps) and the burst size will be automatically set to twice the rate. // When the rate and burst size are exceeded, new packets will be dropped. PacketInRate int `yaml:"packetInRate,omitempty"` + // NodeNetworkPolicy includes the privileged ingress and egress rules for NodeNetworkPolicy. + NodeNetworkPolicy NodeNetworkPolicyConfig `yaml:"nodeNetworkPolicy,omitempty"` } type AntreaProxyConfig struct { @@ -404,3 +406,17 @@ type OVSBridgeConfig struct { // only a single physical interface is supported. PhysicalInterfaces []string `yaml:"physicalInterfaces,omitempty"` } + +type PrivilegedRule struct { + Protocol string `yaml:"protocol"` + Ports []string `yaml:"ports,omitempty"` + ICMPType *int32 `yaml:"icmpType,omitempty"` + ICMPCode *int32 `yaml:"icmpCode,omitempty"` + Comment string `yaml:"comment,omitempty"` +} + +// NodeNetworkPolicyConfig includes the privileged ingress and egress protocols and ports. +type NodeNetworkPolicyConfig struct { + PrivilegedIngressRules []PrivilegedRule `yaml:"privilegedIngressRules,omitempty"` + PrivilegedEgressRules []PrivilegedRule `yaml:"privilegedEgressRules,omitempty"` +} diff --git a/pkg/controller/networkpolicy/clusternetworkpolicy.go b/pkg/controller/networkpolicy/clusternetworkpolicy.go index ae595fd9135..59cd7dc90ef 100644 --- a/pkg/controller/networkpolicy/clusternetworkpolicy.go +++ b/pkg/controller/networkpolicy/clusternetworkpolicy.go @@ -346,6 +346,7 @@ func (n *NetworkPolicyController) processClusterNetworkPolicy(cnp *crdv1beta1.Cl var clusterSetScopeSelectorKeys sets.Set[string] if hasPerNamespaceRule && len(cnp.Spec.AppliedTo) > 0 { for _, at := range cnp.Spec.AppliedTo { + //TODO: add something to allow Node NetworkPolicy if at.ServiceAccount != nil { atg := n.createAppliedToGroup(at.ServiceAccount.Namespace, serviceAccountNameToPodSelector(at.ServiceAccount.Name), nil, nil) appliedToGroups = mergeAppliedToGroups(appliedToGroups, atg) diff --git a/pkg/features/antrea_features.go b/pkg/features/antrea_features.go index e2b5cb801c8..ad74a3a0513 100644 --- a/pkg/features/antrea_features.go +++ b/pkg/features/antrea_features.go @@ -146,6 +146,10 @@ const ( // alpha: v1.14 // Enable Egress traffic shaping. EgressTrafficShaping featuregate.Feature = "EgressTrafficShaping" + + // alpha: v1.15 + // Enable users to protect their Kubernetes Node. + NodeNetworkPolicy featuregate.Feature = "NodeNetworkPolicy" ) var ( @@ -184,6 +188,7 @@ var ( LoadBalancerModeDSR: {Default: false, PreRelease: featuregate.Alpha}, AdminNetworkPolicy: {Default: false, PreRelease: featuregate.Alpha}, EgressTrafficShaping: {Default: false, PreRelease: featuregate.Alpha}, + NodeNetworkPolicy: {Default: false, PreRelease: featuregate.Alpha}, } // AgentGates consists of all known feature gates for the Antrea Agent. @@ -211,6 +216,7 @@ var ( Traceflow, TrafficControl, EgressTrafficShaping, + NodeNetworkPolicy, ) // ControllerGates consists of all known feature gates for the Antrea Controller. @@ -229,6 +235,7 @@ var ( ServiceExternalIP, SupportBundleCollection, Traceflow, + NodeNetworkPolicy, ) // UnsupportedFeaturesOnWindows records the features not supported on @@ -255,6 +262,7 @@ var ( LoadBalancerModeDSR: {}, CleanupStaleUDPSvcConntrack: {}, EgressTrafficShaping: {}, + NodeNetworkPolicy: {}, } // supportedFeaturesOnExternalNode records the features supported on an external // Node. Antrea Agent checks the enabled features if it is running on an diff --git a/test/integration/agent/route_test.go b/test/integration/agent/route_test.go index 65ae265a71c..0cddd7edf55 100644 --- a/test/integration/agent/route_test.go +++ b/test/integration/agent/route_test.go @@ -145,7 +145,7 @@ func TestInitialize(t *testing.T) { for _, tc := range tcs { t.Logf("Running Initialize test with mode %s node config %s", tc.networkConfig.TrafficEncapMode, nodeConfig) - routeClient, err := route.NewClient(tc.networkConfig, tc.noSNAT, false, false, false, nil) + routeClient, err := route.NewClient(tc.networkConfig, tc.noSNAT, false, false, false, nil, false, nil) assert.NoError(t, err) var xtablesReleasedTime, initializedTime time.Time @@ -252,7 +252,7 @@ func TestIpTablesSync(t *testing.T) { gwLink := createDummyGW(t) defer netlink.LinkDel(gwLink) - routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeEncap, IPv4Enabled: true}, false, false, false, false, nil) + routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeEncap, IPv4Enabled: true}, false, false, false, false, nil, false, nil) assert.Nil(t, err) inited := make(chan struct{}) @@ -303,7 +303,7 @@ func TestAddAndDeleteSNATRule(t *testing.T) { gwLink := createDummyGW(t) defer netlink.LinkDel(gwLink) - routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeEncap, IPv4Enabled: true}, false, false, false, false, nil) + routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeEncap, IPv4Enabled: true}, false, false, false, false, nil, false, nil) assert.Nil(t, err) inited := make(chan struct{}) @@ -357,7 +357,7 @@ func TestAddAndDeleteRoutes(t *testing.T) { for _, tc := range tcs { t.Logf("Running test with mode %s peer cidr %s peer ip %s node config %s", tc.mode, tc.peerCIDR, tc.peerIP, nodeConfig) - routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: tc.mode, IPv4Enabled: true}, false, false, false, false, nil) + routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: tc.mode, IPv4Enabled: true}, false, false, false, false, nil, false, nil) assert.NoError(t, err) err = routeClient.Initialize(nodeConfig, func() {}) assert.NoError(t, err) @@ -422,7 +422,7 @@ func TestSyncRoutes(t *testing.T) { for _, tc := range tcs { t.Logf("Running test with mode %s peer cidr %s peer ip %s node config %s", tc.mode, tc.peerCIDR, tc.peerIP, nodeConfig) - routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: tc.mode, IPv4Enabled: true}, false, false, false, false, nil) + routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: tc.mode, IPv4Enabled: true}, false, false, false, false, nil, false, nil) assert.NoError(t, err) err = routeClient.Initialize(nodeConfig, func() {}) assert.NoError(t, err) @@ -465,7 +465,7 @@ func TestSyncGatewayKernelRoute(t *testing.T) { } require.NoError(t, netlink.AddrAdd(gwLink, &netlink.Addr{IPNet: gwNet}), "configuring gw IP failed") - routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeEncap}, false, false, false, false, nil) + routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeEncap}, false, false, false, false, nil, false, nil) assert.NoError(t, err) err = routeClient.Initialize(nodeConfig, func() {}) assert.NoError(t, err) @@ -559,7 +559,7 @@ func TestReconcile(t *testing.T) { for _, tc := range tcs { t.Logf("Running test with mode %s added routes %v desired routes %v", tc.mode, tc.addedRoutes, tc.desiredPeerCIDRs) - routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: tc.mode, IPv4Enabled: true}, false, false, false, false, nil) + routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: tc.mode, IPv4Enabled: true}, false, false, false, false, nil, false, nil) assert.NoError(t, err) err = routeClient.Initialize(nodeConfig, func() {}) assert.NoError(t, err) @@ -598,7 +598,7 @@ func TestRouteTablePolicyOnly(t *testing.T) { gwLink := createDummyGW(t) defer netlink.LinkDel(gwLink) - routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeNetworkPolicyOnly, IPv4Enabled: true}, false, false, false, false, nil) + routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeNetworkPolicyOnly, IPv4Enabled: true}, false, false, false, false, nil, false, nil) assert.NoError(t, err) err = routeClient.Initialize(nodeConfig, func() {}) assert.NoError(t, err) @@ -654,7 +654,7 @@ func TestIPv6RoutesAndNeighbors(t *testing.T) { gwLink := createDummyGW(t) defer netlink.LinkDel(gwLink) - routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeEncap, IPv4Enabled: true, IPv6Enabled: true}, false, false, false, false, nil) + routeClient, err := route.NewClient(&config.NetworkConfig{TrafficEncapMode: config.TrafficEncapModeEncap, IPv4Enabled: true, IPv6Enabled: true}, false, false, false, false, nil, false, nil) assert.Nil(t, err) _, ipv6Subnet, _ := net.ParseCIDR("fd74:ca9b:172:19::/64") gwIPv6 := net.ParseIP("fd74:ca9b:172:19::1")