Skip to content

Commit

Permalink
add admin network policy
Browse files Browse the repository at this point in the history
  • Loading branch information
aojea committed Apr 23, 2024
1 parent 35dd163 commit a37eada
Show file tree
Hide file tree
Showing 6 changed files with 344 additions and 5 deletions.
28 changes: 26 additions & 2 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp"
"sigs.k8s.io/knftables"
"sigs.k8s.io/kube-network-policies/pkg/networkpolicy"
npaclient "sigs.k8s.io/network-policy-api/pkg/client/clientset/versioned"
npainformers "sigs.k8s.io/network-policy-api/pkg/client/informers/externalversions"

utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/informers"
v1 "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/klog/v2"
Expand All @@ -25,12 +28,14 @@ import (

var (
failOpen bool
adminNetworkPolicy bool // AdminNetworkPolicy is alpha so keep it feature gated behind a flag
queueID int
metricsBindAddress string
)

func init() {
flag.BoolVar(&failOpen, "fail-open", false, "If set, don't drop packets if the controller is not running")
flag.BoolVar(&adminNetworkPolicy, "admin-network-policy", false, "If set, enable Admin Network Policy API")
flag.IntVar(&queueID, "nfqueue-id", 100, "Number of the nfqueue used")
flag.StringVar(&metricsBindAddress, "metrics-bind-address", ":9080", "The IP address and port for the metrics server to serve on")

Expand All @@ -55,8 +60,9 @@ func main() {
}

cfg := networkpolicy.Config{
FailOpen: failOpen,
QueueID: queueID,
AdminNetworkPolicy: adminNetworkPolicy,
FailOpen: failOpen,
QueueID: queueID,
}
// creates the in-cluster config
config, err := rest.InClusterConfig()
Expand All @@ -83,6 +89,18 @@ func main() {

informersFactory := informers.NewSharedInformerFactory(clientset, 0)

var npaClient *npaclient.Clientset
var npaInformerFactory npainformers.SharedInformerFactory
var nodeInformer v1.NodeInformer
if adminNetworkPolicy {
nodeInformer = informersFactory.Core().V1().Nodes()
npaClient, err = npaclient.NewForConfig(config)
if err != nil {
klog.Fatalf("Failed to create Network client: %v", err)
}
npaInformerFactory = npainformers.NewSharedInformerFactory(npaClient, 0)
}

http.Handle("/metrics", promhttp.Handler())
go func() {
err := http.ListenAndServe(metricsBindAddress, nil)
Expand All @@ -95,6 +113,9 @@ func main() {
informersFactory.Networking().V1().NetworkPolicies(),
informersFactory.Core().V1().Namespaces(),
informersFactory.Core().V1().Pods(),
nodeInformer,
npaClient,
npaInformerFactory.Policy().V1alpha1().AdminNetworkPolicies(),
cfg,
)
go func() {
Expand All @@ -103,6 +124,9 @@ func main() {
}()

informersFactory.Start(ctx.Done())
if adminNetworkPolicy {
npaInformerFactory.Start(ctx.Done())
}

select {
case <-signalCh:
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ require (
k8s.io/klog/v2 v2.120.1
k8s.io/utils v0.0.0-20240310230437-4693a0247e57
sigs.k8s.io/knftables v0.0.16
sigs.k8s.io/network-policy-api v0.1.5
)

require (
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMm
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/knftables v0.0.16 h1:ZpTfNsjnidgoXdxxzcZLdSctqkpSO3QB3jo3zQ4PXqM=
sigs.k8s.io/knftables v0.0.16/go.mod h1:f/5ZLKYEUPUhVjUCg6l80ACdL7CIIyeL0DxfgojGRTk=
sigs.k8s.io/network-policy-api v0.1.5 h1:xyS7VAaM9EfyB428oFk7WjWaCK6B129i+ILUF4C8l6E=
sigs.k8s.io/network-policy-api v0.1.5/go.mod h1:D7Nkr43VLNd7iYryemnj8qf0N/WjBzTZDxYA+g4u1/Y=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
Expand Down
263 changes: 263 additions & 0 deletions pkg/networkpolicy/adminnetworkpolicy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
package networkpolicy

import (
"cmp"
"fmt"
"net"
"slices"

v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/klog/v2"
npav1alpha1 "sigs.k8s.io/network-policy-api/apis/v1alpha1"
)

// adminNetworkPolicyAction evaluate the Admin Network Policies on the packet
// that is evaluated both from the origin and the destination perspective.
// There can be 3 possible outcomes:
// 1. Policies Allow in both direction then the packet is Allowed and NOT evaluated by Network Policies
// 2. Policies Deny in any direction then the packet is Dropped and NOT evaluated by Network Policies
// 3. Policies Pass and or Allow then the packet is passed to be evaluated by Network Policies
func (c *Controller) adminNetworkPolicyAction(p packet) npav1alpha1.AdminNetworkPolicyRuleAction {
srcIP := p.srcIP
srcPod := c.getPodAssignedToIP(srcIP.String())
srcPort := p.srcPort
dstIP := p.dstIP
dstPod := c.getPodAssignedToIP(dstIP.String())
dstPort := p.dstPort
protocol := p.proto
srcPodAdminNetworkPolices := c.getAdminNetworkPoliciesForPod(srcPod)
dstPodAdminNetworkPolices := c.getAdminNetworkPoliciesForPod(dstPod)
msg := fmt.Sprintf("checking packet %s:", p.String())
if srcPod != nil {
msg += fmt.Sprintf(" SrcPod (%s/%s): %d AdminNetworkPolicy", srcPod.Name, srcPod.Namespace, len(srcPodAdminNetworkPolices))
}
if dstPod != nil {
msg += fmt.Sprintf(" DstPod (%s/%s): %d AdminNetworkPolicy", dstPod.Name, dstPod.Namespace, len(dstPodAdminNetworkPolices))
}
klog.V(2).Infof("%s", msg)

// For a connection from a source pod to a destination pod to be allowed,
// both the egress policy on the source pod and the ingress policy on the
// destination pod need to allow the connection.
// This is the first packet originated from srcPod so we need to check:
// 1. srcPod egress is accepted
for _, policy := range srcPodAdminNetworkPolices {
for _, rule := range policy.Spec.Egress {
// Ports allows for matching traffic based on port and protocols.
// This field is a list of destination ports for the outgoing egress traffic.
// If Ports is not set then the rule does not filter traffic via port.
if rule.Ports != nil {
if !c.evaluateAdminNetworkPolicyPort(*rule.Ports, dstPod, dstPort, protocol) {
continue
}
}
// To is the List of destinations whose traffic this rule applies to.
// If any AdminNetworkPolicyEgressPeer matches the destination of outgoing
// traffic then the specified action is applied.
// This field must be defined and contain at least one item.
for _, to := range rule.To {
// Exactly one of the selector pointers must be set for a given peer. If a
// consumer observes none of its fields are set, they must assume an unknown
// option has been specified and fail closed.
if to.Namespaces != nil {
if c.namespaceSelector(to.Namespaces, dstPod) {
return rule.Action
}
}

if to.Pods != nil {
if c.namespaceSelector(&to.Pods.NamespaceSelector, dstPod) &&
podSelector(&to.Pods.PodSelector, dstPod) {
return rule.Action
}
}

if to.Nodes != nil {
if c.nodeSelector(to.Nodes, dstPod) {
return rule.Action
}
}

for _, network := range to.Networks {
_, cidr, err := net.ParseCIDR(string(network))
if err != nil { // this has been validated by the API
continue
}
if cidr.Contains(dstIP) {
return rule.Action
}
}
}
}
}

// 2. dstPod ingress is accepted
for _, policy := range dstPodAdminNetworkPolices {
// Ingress is the list of Ingress rules to be applied to the selected pods. A total of 100 rules will be allowed in each ANP instance. The relative precedence of ingress rules within a single ANP object (all of which share the priority) will be determined by the order in which the rule is written. Thus, a rule that appears at the top of the ingress rules would take the highest precedence.
// ANPs with no ingress rules do not affect ingress traffic.
for _, rule := range policy.Spec.Ingress {
// Ports allows for matching traffic based on port and protocols.
// This field is a list of destination ports for the outgoing egress traffic.
// If Ports is not set then the rule does not filter traffic via port.
if rule.Ports != nil {
if !c.evaluateAdminNetworkPolicyPort(*rule.Ports, srcPod, srcPort, protocol) {
continue
}
}
// To is the List of destinations whose traffic this rule applies to.
// If any AdminNetworkPolicyEgressPeer matches the destination of outgoing
// traffic then the specified action is applied.
// This field must be defined and contain at least one item.
for _, from := range rule.From {
// Exactly one of the selector pointers must be set for a given peer. If a
// consumer observes none of its fields are set, they must assume an unknown
// option has been specified and fail closed.
if from.Namespaces != nil {
if c.namespaceSelector(from.Namespaces, srcPod) {
return rule.Action
}
}

if from.Pods != nil {
if c.namespaceSelector(&from.Pods.NamespaceSelector, srcPod) &&
podSelector(&from.Pods.PodSelector, srcPod) {
return rule.Action
}
}
}

}
}

return npav1alpha1.AdminNetworkPolicyRuleActionPass
}

// namespaceSelector return true if the namespace selector matches the pod
func (c *Controller) namespaceSelector(selector *metav1.LabelSelector, pod *v1.Pod) bool {
nsSelector, err := metav1.LabelSelectorAsSelector(selector)
if err != nil {
return false
}

namespaces, err := c.namespaceLister.List(nsSelector)
if err != nil {
return false
}

for _, ns := range namespaces {
if pod.Namespace == ns.Name {
return true
}
}
return false
}

// podSelector return true if the pod selector matches the pod
func podSelector(selector *metav1.LabelSelector, pod *v1.Pod) bool {
podSelector, err := metav1.LabelSelectorAsSelector(selector)
if err != nil {
return false
}
return podSelector.Matches(labels.Set(pod.Labels))
}

// nodeSelector return true if the node selector matches the pod
func (c *Controller) nodeSelector(selector *metav1.LabelSelector, pod *v1.Pod) bool {
nodeSelector, err := metav1.LabelSelectorAsSelector(selector)
if err != nil {
return false
}
nodes, err := c.namespaceLister.List(nodeSelector)
if err != nil {
return false
}

for _, node := range nodes {
if pod.Spec.NodeName == node.Name {
return true
}
}
return false
}

// getAdminNetworkPoliciesForPod returns the list of Admin Network Policies matching the Pod
// The list is ordered by priority, from higher to lower.
func (c *Controller) getAdminNetworkPoliciesForPod(pod *v1.Pod) []*npav1alpha1.AdminNetworkPolicy {
if pod == nil {
return nil
}
// Get all the network policies that affect this pod
networkPolices, err := c.adminNetworkPolicyLister.List(labels.Everything())
if err != nil {
klog.Infof("getAdminNetworkPoliciesForPod error: %v", err)
return nil
}

result := []*npav1alpha1.AdminNetworkPolicy{}
for _, policy := range networkPolices {
if policy.Spec.Subject.Namespaces != nil &&
c.namespaceSelector(policy.Spec.Subject.Namespaces, pod) {
klog.V(2).Infof("Pod %s/%s match AdminNetworkPolicy %s", pod.Name, pod.Namespace, policy.Name)
result = append(result, policy)
}

if policy.Spec.Subject.Pods != nil &&
c.namespaceSelector(&policy.Spec.Subject.Pods.NamespaceSelector, pod) &&
podSelector(&policy.Spec.Subject.Pods.PodSelector, pod) {
klog.V(2).Infof("Pod %s/%s match AdminNetworkPolicy %s", pod.Name, pod.Namespace, policy.Name)
result = append(result, policy)
}
}
// Rules with lower priority values have higher precedence
slices.SortFunc(result, func(a, b *npav1alpha1.AdminNetworkPolicy) int {
if n := cmp.Compare(a.Spec.Priority, b.Spec.Priority); n != 0 {
return n
}
// If priorities are equal, order by name
return cmp.Compare(a.Name, b.Name)
})
return result
}

func (c *Controller) evaluateAdminNetworkPolicyPort(networkPolicyPorts []npav1alpha1.AdminNetworkPolicyPort, pod *v1.Pod, port int, protocol v1.Protocol) bool {
// AdminNetworkPolicyPort describes how to select network ports on pod(s).
// Exactly one field must be set.
if len(networkPolicyPorts) == 0 {
return true
}

for _, policyPort := range networkPolicyPorts {
// Port number
if policyPort.PortNumber != nil &&
policyPort.PortNumber.Port == int32(port) &&
policyPort.PortNumber.Protocol == protocol {
return true
}

// Named Port
if policyPort.NamedPort != nil {
if pod == nil {
continue
}
for _, container := range pod.Spec.Containers {
for _, p := range container.Ports {
if p.Name == *policyPort.NamedPort {
return true
}
}
}
}

// Port range
if policyPort.PortRange != nil &&
policyPort.PortRange.Protocol == protocol &&
policyPort.PortRange.Start <= int32(port) &&
policyPort.PortRange.End >= int32(port) {
return true
}

}
return false
}
Loading

0 comments on commit a37eada

Please sign in to comment.