From cb81da65f095202db6d65f5dc227f7f21746fb6f Mon Sep 17 00:00:00 2001 From: Qiyue Yao Date: Tue, 22 Nov 2022 21:32:00 -0800 Subject: [PATCH] L7NetworkPolicy Controller Add support for passing L7Protocols to agent when processing ACNP and ANP. Add validation for Antrea native policy for L7Protocols (HTTP only) to be used with Ports/Protocols, only supports Allow, and not used with toServices. Add UT. Signed-off-by: Qiyue Yao --- .../networkpolicy/antreanetworkpolicy.go | 2 + .../networkpolicy/antreanetworkpolicy_test.go | 50 +++++ .../networkpolicy/clusternetworkpolicy.go | 1 + .../clusternetworkpolicy_test.go | 48 +++++ pkg/controller/networkpolicy/crd_utils.go | 12 ++ .../networkpolicy/crd_utils_test.go | 20 ++ pkg/controller/networkpolicy/validate.go | 42 ++++ pkg/controller/networkpolicy/validate_test.go | 180 +++++++++++++++++- 8 files changed, 353 insertions(+), 2 deletions(-) diff --git a/pkg/controller/networkpolicy/antreanetworkpolicy.go b/pkg/controller/networkpolicy/antreanetworkpolicy.go index c9c7536aa5c..8dfeab4a545 100644 --- a/pkg/controller/networkpolicy/antreanetworkpolicy.go +++ b/pkg/controller/networkpolicy/antreanetworkpolicy.go @@ -107,6 +107,7 @@ func (n *NetworkPolicyController) processAntreaNetworkPolicy(np *crdv1alpha1.Net Priority: int32(idx), EnableLogging: ingressRule.EnableLogging, AppliedToGroups: getAppliedToGroupNames(atgs), + L7Protocols: toAntreaL7ProtocolsForCRD(ingressRule.L7Protocols), }) } // Compute NetworkPolicyRule for Egress Rule. @@ -133,6 +134,7 @@ func (n *NetworkPolicyController) processAntreaNetworkPolicy(np *crdv1alpha1.Net Priority: int32(idx), EnableLogging: egressRule.EnableLogging, AppliedToGroups: getAppliedToGroupNames(atgs), + L7Protocols: toAntreaL7ProtocolsForCRD(egressRule.L7Protocols), }) } tierPriority := n.getTierPriority(np.Spec.Tier) diff --git a/pkg/controller/networkpolicy/antreanetworkpolicy_test.go b/pkg/controller/networkpolicy/antreanetworkpolicy_test.go index d7674b7b86b..e4f155e814e 100644 --- a/pkg/controller/networkpolicy/antreanetworkpolicy_test.go +++ b/pkg/controller/networkpolicy/antreanetworkpolicy_test.go @@ -569,6 +569,56 @@ func TestProcessAntreaNetworkPolicy(t *testing.T) { expectedAppliedToGroups: 1, expectedAddressGroups: 1, }, + { + name: "with-l7Protocol", + inputPolicy: &crdv1alpha1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{Namespace: "ns8", Name: "npH", UID: "uidH"}, + Spec: crdv1alpha1.NetworkPolicySpec{ + AppliedTo: []crdv1alpha1.AppliedTo{ + {PodSelector: &selectorA}, + }, + Priority: p10, + Ingress: []crdv1alpha1.Rule{ + { + L7Protocols: []crdv1alpha1.L7Protocol{{HTTP: &crdv1alpha1.HTTPProtocol{Host: "test.com", Method: "GET", Path: "/admin"}}}, + From: []crdv1alpha1.NetworkPolicyPeer{ + { + PodSelector: &selectorB, + NamespaceSelector: &selectorC, + }, + }, + Action: &allowAction, + }, + }, + }, + }, + expectedPolicy: &antreatypes.NetworkPolicy{ + UID: "uidH", + Name: "uidH", + SourceRef: &controlplane.NetworkPolicyReference{ + Type: controlplane.AntreaNetworkPolicy, + Namespace: "ns8", + Name: "npH", + UID: "uidH", + }, + Priority: &p10, + TierPriority: &DefaultTierPriority, + Rules: []controlplane.NetworkPolicyRule{ + { + Direction: controlplane.DirectionIn, + From: controlplane.NetworkPolicyPeer{ + AddressGroups: []string{getNormalizedUID(antreatypes.NewGroupSelector("", &selectorB, &selectorC, nil, nil).NormalizedName)}, + }, + L7Protocols: []controlplane.L7Protocol{{HTTP: &controlplane.HTTPProtocol{Host: "test.com", Method: "GET", Path: "/admin"}}}, + Priority: 0, + Action: &allowAction, + }, + }, + AppliedToGroups: []string{getNormalizedUID(antreatypes.NewGroupSelector("ns8", &selectorA, nil, nil, nil).NormalizedName)}, + }, + expectedAppliedToGroups: 1, + expectedAddressGroups: 1, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/controller/networkpolicy/clusternetworkpolicy.go b/pkg/controller/networkpolicy/clusternetworkpolicy.go index aabe3d8f66b..98984e91db5 100644 --- a/pkg/controller/networkpolicy/clusternetworkpolicy.go +++ b/pkg/controller/networkpolicy/clusternetworkpolicy.go @@ -371,6 +371,7 @@ func (n *NetworkPolicyController) processClusterNetworkPolicy(cnp *crdv1alpha1.C Priority: int32(idx), EnableLogging: cnpRule.EnableLogging, AppliedToGroups: getAppliedToGroupNames(ruleAppliedTos), + L7Protocols: toAntreaL7ProtocolsForCRD(cnpRule.L7Protocols), } if dir == controlplane.DirectionIn { rule.From = *peer diff --git a/pkg/controller/networkpolicy/clusternetworkpolicy_test.go b/pkg/controller/networkpolicy/clusternetworkpolicy_test.go index 21ef09a97b0..cf0f69a343b 100644 --- a/pkg/controller/networkpolicy/clusternetworkpolicy_test.go +++ b/pkg/controller/networkpolicy/clusternetworkpolicy_test.go @@ -383,6 +383,54 @@ func TestProcessClusterNetworkPolicy(t *testing.T) { expectedAppliedToGroups: 1, expectedAddressGroups: 1, }, + { + name: "with-l7Protocol", + inputPolicy: &crdv1alpha1.ClusterNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{Namespace: "", Name: "cnpE", UID: "uidE"}, + Spec: crdv1alpha1.ClusterNetworkPolicySpec{ + AppliedTo: []crdv1alpha1.AppliedTo{ + {PodSelector: &selectorA}, + }, + Priority: p10, + Ingress: []crdv1alpha1.Rule{ + { + L7Protocols: []crdv1alpha1.L7Protocol{{HTTP: &crdv1alpha1.HTTPProtocol{Host: "test.com", Method: "GET", Path: "/admin"}}}, + From: []crdv1alpha1.NetworkPolicyPeer{ + { + PodSelector: &selectorB, + }, + }, + Action: &allowAction, + }, + }, + }, + }, + expectedPolicy: &antreatypes.NetworkPolicy{ + UID: "uidE", + Name: "uidE", + SourceRef: &controlplane.NetworkPolicyReference{ + Type: controlplane.AntreaClusterNetworkPolicy, + Name: "cnpE", + UID: "uidE", + }, + Priority: &p10, + TierPriority: &DefaultTierPriority, + Rules: []controlplane.NetworkPolicyRule{ + { + Direction: controlplane.DirectionIn, + From: controlplane.NetworkPolicyPeer{ + AddressGroups: []string{getNormalizedUID(antreatypes.NewGroupSelector("", &selectorB, nil, nil, nil).NormalizedName)}, + }, + L7Protocols: []controlplane.L7Protocol{{HTTP: &controlplane.HTTPProtocol{Host: "test.com", Method: "GET", Path: "/admin"}}}, + Priority: 0, + Action: &allowAction, + }, + }, + AppliedToGroups: []string{getNormalizedUID(antreatypes.NewGroupSelector("", &selectorA, nil, nil, nil).NormalizedName)}, + }, + expectedAppliedToGroups: 1, + expectedAddressGroups: 1, + }, { name: "appliedTo-per-rule", inputPolicy: &crdv1alpha1.ClusterNetworkPolicy{ diff --git a/pkg/controller/networkpolicy/crd_utils.go b/pkg/controller/networkpolicy/crd_utils.go index 66a711d847e..f7d1da08250 100644 --- a/pkg/controller/networkpolicy/crd_utils.go +++ b/pkg/controller/networkpolicy/crd_utils.go @@ -108,6 +108,18 @@ func toAntreaServicesForCRD(npPorts []v1alpha1.NetworkPolicyPort, npProtocols [] return antreaServices, namedPortExists } +// toAntreaL7ProtocolsForCRD converts a slice of v1alpha1.L7Protocol objects to +// a slice of Antrea L7Protocol objects. +func toAntreaL7ProtocolsForCRD(l7Protocols []v1alpha1.L7Protocol) []controlplane.L7Protocol { + var antreaL7Protocols []controlplane.L7Protocol + for _, l7p := range l7Protocols { + antreaL7Protocols = append(antreaL7Protocols, controlplane.L7Protocol{ + HTTP: (*controlplane.HTTPProtocol)(l7p.HTTP), + }) + } + return antreaL7Protocols +} + // toAntreaIPBlockForCRD converts a v1alpha1.IPBlock to an Antrea IPBlock. func toAntreaIPBlockForCRD(ipBlock *v1alpha1.IPBlock) (*controlplane.IPBlock, error) { // Convert the allowed IPBlock to networkpolicy.IPNet. diff --git a/pkg/controller/networkpolicy/crd_utils_test.go b/pkg/controller/networkpolicy/crd_utils_test.go index 51f7d88c629..bb592728cc3 100644 --- a/pkg/controller/networkpolicy/crd_utils_test.go +++ b/pkg/controller/networkpolicy/crd_utils_test.go @@ -206,6 +206,26 @@ func TestToAntreaServicesForCRD(t *testing.T) { } } +func TestToAntreaL7ProtocolsForCRD(t *testing.T) { + tables := []struct { + l7Protocol []crdv1alpha1.L7Protocol + expValue []controlplane.L7Protocol + }{ + { + []crdv1alpha1.L7Protocol{ + {HTTP: &crdv1alpha1.HTTPProtocol{Host: "test.com", Method: "GET", Path: "/admin"}}, + }, + []controlplane.L7Protocol{ + {HTTP: &controlplane.HTTPProtocol{Host: "test.com", Method: "GET", Path: "/admin"}}, + }, + }, + } + for _, table := range tables { + gotValue := toAntreaL7ProtocolsForCRD(table.l7Protocol) + assert.Equal(t, table.expValue, gotValue) + } +} + func TestToAntreaIPBlockForCRD(t *testing.T) { expIPNet := controlplane.IPNet{ IP: ipStrToIPAddress("10.0.0.0"), diff --git a/pkg/controller/networkpolicy/validate.go b/pkg/controller/networkpolicy/validate.go index da105f6637b..f33e5720d0b 100644 --- a/pkg/controller/networkpolicy/validate.go +++ b/pkg/controller/networkpolicy/validate.go @@ -25,6 +25,7 @@ import ( admv1 "k8s.io/api/admission/v1" authenticationv1 "k8s.io/api/authentication/v1" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/sets" @@ -448,6 +449,10 @@ func (v *antreaPolicyValidator) createValidate(curObj interface{}, userInfo auth if !allowed { return reason, allowed } + reason, allowed = v.validateL7Protocols(ingress, egress) + if !allowed { + return reason, allowed + } if err := v.validatePort(ingress, egress); err != nil { return err.Error(), false } @@ -760,6 +765,39 @@ func (v *antreaPolicyValidator) validateMulticastIGMP(ingressRules, egressRules return "", true } +// validateL7Protocols validates the L7Protocols field set in Antrea-native policy +// rules are valid, and compatible with the ports or protocols fields. +func (v *antreaPolicyValidator) validateL7Protocols(ingressRules, egressRules []crdv1alpha1.Rule) (string, bool) { + for _, r := range append(ingressRules, egressRules...) { + if len(r.L7Protocols) == 0 { + continue + } + if *r.Action != crdv1alpha1.RuleActionAllow { + return "layer 7 protocols only support Allow", false + } + if len(r.ToServices) != 0 { + return "layer 7 protocols can not be used with toServices", false + } + haveHTTP := false + for _, p := range r.L7Protocols { + if p.HTTP != nil { + haveHTTP = true + } + } + for _, port := range r.Ports { + if haveHTTP && (port.Protocol != nil && *port.Protocol != v1.ProtocolTCP) { + return "HTTP protocol can only be used when layer 4 protocol is TCP or unset", false + } + } + for _, protocol := range r.Protocols { + if haveHTTP && (protocol.IGMP != nil || protocol.ICMP != nil) { + return "HTTP protocol can not be used with protocol IGMP or ICMP", false + } + } + } + return "", true +} + // validateFQDNSelectors validates the toFQDN field set in Antrea-native policy egress rules are valid. func (v *antreaPolicyValidator) validateFQDNSelectors(egressRules []crdv1alpha1.Rule) (string, bool) { for _, r := range egressRules { @@ -814,6 +852,10 @@ func (v *antreaPolicyValidator) updateValidate(curObj, oldObj interface{}, userI if !allowed { return reason, allowed } + reason, allowed = v.validateL7Protocols(ingress, egress) + if !allowed { + return reason, allowed + } if err := v.validatePort(ingress, egress); err != nil { return err.Error(), false } diff --git a/pkg/controller/networkpolicy/validate_test.go b/pkg/controller/networkpolicy/validate_test.go index 3869f965b52..88557debfc2 100644 --- a/pkg/controller/networkpolicy/validate_test.go +++ b/pkg/controller/networkpolicy/validate_test.go @@ -1166,7 +1166,7 @@ func TestValidateAntreaPolicy(t *testing.T) { name: "acnp-appliedto-service-from-psel", policy: &crdv1alpha1.ClusterNetworkPolicy{ ObjectMeta: metav1.ObjectMeta{ - Name: "egress-rule-appliedto-service", + Name: "ingress-rule-appliedto-service", }, Spec: crdv1alpha1.ClusterNetworkPolicySpec{ AppliedTo: []crdv1alpha1.AppliedTo{ @@ -1197,7 +1197,7 @@ func TestValidateAntreaPolicy(t *testing.T) { name: "acnp-appliedto-service-valid", policy: &crdv1alpha1.ClusterNetworkPolicy{ ObjectMeta: metav1.ObjectMeta{ - Name: "egress-rule-appliedto-service", + Name: "ingress-rule-appliedto-service", }, Spec: crdv1alpha1.ClusterNetworkPolicySpec{ AppliedTo: []crdv1alpha1.AppliedTo{ @@ -1224,6 +1224,182 @@ func TestValidateAntreaPolicy(t *testing.T) { }, expectedReason: "", }, + { + name: "acnp-l7protocols-used-with-allow", + policy: &crdv1alpha1.ClusterNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-rule-l7protocols", + }, + Spec: crdv1alpha1.ClusterNetworkPolicySpec{ + AppliedTo: []crdv1alpha1.AppliedTo{ + { + Service: &crdv1alpha1.NamespacedName{ + Namespace: "foo1", + Name: "bar1", + }, + }, + }, + Ingress: []crdv1alpha1.Rule{ + { + Action: &allowAction, + L7Protocols: []crdv1alpha1.L7Protocol{ + { + HTTP: &crdv1alpha1.HTTPProtocol{ + Host: "test.com", + Method: "GET", + Path: "/admin", + }, + }, + }, + }, + }, + }, + }, + expectedReason: "", + }, + { + name: "acnp-l7protocols-used-with-pass", + policy: &crdv1alpha1.ClusterNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-rule-l7protocols", + }, + Spec: crdv1alpha1.ClusterNetworkPolicySpec{ + AppliedTo: []crdv1alpha1.AppliedTo{ + { + Service: &crdv1alpha1.NamespacedName{ + Namespace: "foo1", + Name: "bar1", + }, + }, + }, + Ingress: []crdv1alpha1.Rule{ + { + Action: &passAction, + L7Protocols: []crdv1alpha1.L7Protocol{ + { + HTTP: &crdv1alpha1.HTTPProtocol{ + Host: "test.com", + Method: "GET", + }, + }, + }, + }, + }, + }, + }, + expectedReason: "layer 7 protocols only support Allow", + }, + { + name: "acnp-l7protocols-HTTP-used-with-UDP", + policy: &crdv1alpha1.ClusterNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-rule-l7protocols", + }, + Spec: crdv1alpha1.ClusterNetworkPolicySpec{ + AppliedTo: []crdv1alpha1.AppliedTo{ + { + Service: &crdv1alpha1.NamespacedName{ + Namespace: "foo1", + Name: "bar1", + }, + }, + }, + Ingress: []crdv1alpha1.Rule{ + { + Action: &allowAction, + Ports: []crdv1alpha1.NetworkPolicyPort{ + { + Protocol: &k8sProtocolUDP, + }, + }, + L7Protocols: []crdv1alpha1.L7Protocol{ + { + HTTP: &crdv1alpha1.HTTPProtocol{ + Host: "test.com", + Method: "GET", + }, + }, + }, + }, + }, + }, + }, + expectedReason: "HTTP protocol can only be used when layer 4 protocol is TCP or unset", + }, + { + name: "acnp-l7protocols-HTTP-used-with-ICMP", + policy: &crdv1alpha1.ClusterNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ingress-rule-l7protocols", + }, + Spec: crdv1alpha1.ClusterNetworkPolicySpec{ + AppliedTo: []crdv1alpha1.AppliedTo{ + { + Service: &crdv1alpha1.NamespacedName{ + Namespace: "foo1", + Name: "bar1", + }, + }, + }, + Ingress: []crdv1alpha1.Rule{ + { + Action: &allowAction, + Protocols: []crdv1alpha1.NetworkPolicyProtocol{ + { + ICMP: &crdv1alpha1.ICMPProtocol{}, + }, + }, + L7Protocols: []crdv1alpha1.L7Protocol{ + { + HTTP: &crdv1alpha1.HTTPProtocol{ + Host: "test.com", + Method: "GET", + }, + }, + }, + }, + }, + }, + }, + expectedReason: "HTTP protocol can not be used with protocol IGMP or ICMP", + }, + { + name: "acnp-l7protocols-used-with-toService", + policy: &crdv1alpha1.ClusterNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "egress-rule-l7protocols", + }, + Spec: crdv1alpha1.ClusterNetworkPolicySpec{ + AppliedTo: []crdv1alpha1.AppliedTo{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"foo1": "bar1"}, + }, + }, + }, + Egress: []crdv1alpha1.Rule{ + { + Action: &allowAction, + L7Protocols: []crdv1alpha1.L7Protocol{ + { + HTTP: &crdv1alpha1.HTTPProtocol{ + Host: "test.com", + Method: "GET", + }, + }, + }, + ToServices: []crdv1alpha1.NamespacedName{ + { + Name: "foo", + Namespace: "bar", + }, + }, + }, + }, + }, + }, + expectedReason: "layer 7 protocols can not be used with toServices", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {