Skip to content

Commit

Permalink
Add networkpolicyanalysis antctl query
Browse files Browse the repository at this point in the history
Adds the antctl networkpolicyanalysis query that
returns the predicted effective NetworkPolicy rule,
which affects traffic from ns1/pod1 to ns2/pod2.

Signed-off-by: Qiyue Yao <yaoq@vmware.com>
  • Loading branch information
qiyueyao committed Dec 13, 2023
1 parent 12afd3f commit 9bf6c0f
Show file tree
Hide file tree
Showing 10 changed files with 722 additions and 39 deletions.
16 changes: 16 additions & 0 deletions docs/antctl.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ running in three different modes:
- [controllerinfo and agentinfo commands](#controllerinfo-and-agentinfo-commands)
- [NetworkPolicy commands](#networkpolicy-commands)
- [Mapping endpoints to NetworkPolicies](#mapping-endpoints-to-networkpolicies)
- [Analyzing expected NetworkPolicies behavior](#analyzing-expected-networkpolicies-behavior)
- [Dumping Pod network interface information](#dumping-pod-network-interface-information)
- [Dumping OVS flows](#dumping-ovs-flows)
- [OVS packet tracing](#ovs-packet-tracing)
Expand Down Expand Up @@ -263,6 +264,21 @@ Namespace.
This command only works in "controller mode" and **as of now it can only be run
from inside the Antrea Controller Pod, and not from out-of-cluster**.

#### Analyzing expected NetworkPolicies behavior

`antctl` supports analyzing all the existing Antrea Native NetworkPolicies and
Kubernetes NetworkPolicies to predict the effective policy rule, given two
specific Pods as source and destination.

```bash
antctl query networkpolicyanalysis -S NAMESPACE/POD -D NAMESPACE/POD
```

If only Pod name is provided, the command will default to the "default" Namespace.

This command only works in "controller mode" and **as of now it can only be run
from inside the Antrea Controller Pod, and not from out-of-cluster**.

### Dumping Pod network interface information

`antctl` agent command `get podinterface` (or `get pi`) can dump network
Expand Down
28 changes: 28 additions & 0 deletions pkg/antctl/antctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,34 @@ $ antctl get podmulticaststats pod -n namespace`,
},
transformedResponse: reflect.TypeOf(controllernetworkpolicy.EndpointQueryResponse{}),
},
{use: "networkpolicyanalysis",
aliases: []string{"npanalysis"},
short: "Filter network policy rules .",
long: "Filter network policies expected to be effective on the source and destination endpoints provided.",
example: ` Query network policies between two pods
$ antctl query networkpolicyanalysis -S ns1/pod1 -D ns2/pod2
`,
commandGroup: query,
controllerEndpoint: &endpoint{
nonResourceEndpoint: &nonResourceEndpoint{
path: "/networkpolicyanalysis",
params: []flagInfo{
{
name: "source",
usage: "Source endpoint of network policies. Can be a (local or remote) Pod (specified by <Namespace>/<name>).",
shorthand: "S",
},
{
name: "destination",
usage: "Source endpoint of network policies. Can be a (local or remote) Pod (specified by <Namespace>/<name>).",
shorthand: "D",
},
},
outputType: single,
},
},
transformedResponse: reflect.TypeOf(controllernetworkpolicy.Rule{}),
},
{
use: "flowrecords",
short: "Print the matching flow records in the flow aggregator",
Expand Down
24 changes: 24 additions & 0 deletions pkg/antctl/command_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,27 @@ func (cd *commandDefinition) tableOutputForQueryEndpoint(obj interface{}, writer
return nil
}

// tableOutputForQueryNetworkPolicyAnalysis implements printing rule as query result
func (cd *commandDefinition) tableOutputForQueryNetworkPolicyAnalysis(obj interface{}, writer io.Writer) error {
constructTable := func(header []string, body []string) error {
rows := [][]string{header, body}
numRows, numCol := len(rows), len(rows[0])
widths := output.GetColumnWidths(numRows, numCol, rows)
if err := output.ConstructTable(numRows, numCol, widths, rows, writer); err != nil {
return err
}
return nil
}
queryResponse := obj.(*networkpolicy.Rule)
if queryResponse.Name != "" {
ruleStr := []string{queryResponse.Name, queryResponse.Namespace, strconv.Itoa(queryResponse.RuleIndex), string(queryResponse.UID), string(queryResponse.Direction)}
if err := constructTable([]string{"Name", "Namespace", "RuleIndex", "PolicyUID", "Direction"}, ruleStr); err != nil {
return err
}
}
return nil
}

// output reads bytes from the resp and outputs the data to the writer in desired
// format. If the AddonTransform is set, it will use the function to transform
// the data first. It will try to output the resp in the format ft specified after
Expand Down Expand Up @@ -542,6 +563,9 @@ func (cd *commandDefinition) output(resp io.Reader, writer io.Writer, ft formatt
if cd.controllerEndpoint.nonResourceEndpoint.path == "/endpoint" {
return cd.tableOutputForQueryEndpoint(obj, writer)
}
if cd.controllerEndpoint.nonResourceEndpoint.path == "/networkpolicyanalysis" {
return cd.tableOutputForQueryNetworkPolicyAnalysis(obj, writer)
}
} else {
return output.TableOutput(obj, writer)
}
Expand Down
35 changes: 35 additions & 0 deletions pkg/antctl/command_definition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1008,6 +1008,41 @@ test-ingress-egress testNamespace 0 uid-1
}
}

func TestTableOutputForQueryNetworkPolicyAnalysis(t *testing.T) {
policyRef0 := controllernetworkpolicy.PolicyRef{Namespace: "testNamespace", Name: "test-default-deny", UID: "uid-1"}
tc := []struct {
name string
rawResponseData interface{}
expected string
}{
{
name: "No matching rule",
rawResponseData: &controllernetworkpolicy.Rule{},
expected: ``,
},
{
name: "Matched KNP default drop rule",
rawResponseData: &controllernetworkpolicy.Rule{
PolicyRef: policyRef0,
Direction: cpv1beta.DirectionIn,
RuleIndex: -1,
},
expected: `Name Namespace RuleIndex PolicyUID Direction
test-default-deny testNamespace -1 uid-1 In
`,
},
}
for _, tt := range tc {
t.Run(tt.name, func(t *testing.T) {
cd := &commandDefinition{}
var outputBuf bytes.Buffer
err := cd.tableOutputForQueryNetworkPolicyAnalysis(tt.rawResponseData, &outputBuf)
assert.Nil(t, err)
assert.Equal(t, tt.expected, outputBuf.String())
})
}
}

func TestCollectFlags(t *testing.T) {
tc := []struct {
name string
Expand Down
2 changes: 2 additions & 0 deletions pkg/apiserver/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import (
"antrea.io/antrea/pkg/apiserver/handlers/endpoint"
"antrea.io/antrea/pkg/apiserver/handlers/featuregates"
"antrea.io/antrea/pkg/apiserver/handlers/loglevel"
"antrea.io/antrea/pkg/apiserver/handlers/networkpolicyanalysis"
"antrea.io/antrea/pkg/apiserver/handlers/webhook"
"antrea.io/antrea/pkg/apiserver/registry/controlplane/egressgroup"
"antrea.io/antrea/pkg/apiserver/registry/controlplane/nodestatssummary"
Expand Down Expand Up @@ -295,6 +296,7 @@ func installHandlers(c *ExtraConfig, s *genericapiserver.GenericAPIServer) {
s.Handler.NonGoRestfulMux.HandleFunc("/loglevel", loglevel.HandleFunc())
s.Handler.NonGoRestfulMux.HandleFunc("/featuregates", featuregates.HandleFunc(c.k8sClient))
s.Handler.NonGoRestfulMux.HandleFunc("/endpoint", endpoint.HandleFunc(c.endpointQuerier))
s.Handler.NonGoRestfulMux.HandleFunc("/networkpolicyanalysis", networkpolicyanalysis.HandleFunc(c.endpointQuerier))
// Webhook to mutate Namespace labels and add its metadata.name as a label
s.Handler.NonGoRestfulMux.HandleFunc("/mutate/namespace", webhook.HandleMutationLabels())
if features.DefaultFeatureGate.Enabled(features.AntreaPolicy) {
Expand Down
179 changes: 179 additions & 0 deletions pkg/apiserver/handlers/networkpolicyanalysis/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// 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 networkpolicyanalysis

import (
"encoding/json"
"fmt"
"net/http"
"strings"

"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"

"antrea.io/antrea/pkg/apis/controlplane"
"antrea.io/antrea/pkg/apis/controlplane/v1beta2"
"antrea.io/antrea/pkg/controller/networkpolicy"
)

// parsePeer parses Namespace/Pod name, empty string is returned if the argument is not of a
// valid Namespace/Pod reference (missing pod name or invalid format). Namespace will be set
// as default if missing, string without separator will be considered as pod name.
func parsePeer(str string) (string, string) {
parts := strings.Split(str, "/")
ns, pod := "", ""
if len(parts) == 1 {
ns, pod = "default", parts[0]
} else if len(parts) == 2 {
ns, pod = parts[0], parts[1]
}
return ns, pod
}

// ruleKey returns policyUID+direction+ruleIndex as the key
func ruleKey(uid types.UID, direction v1beta2.Direction, idx int) string {
return fmt.Sprintf("%s/%s%d", uid, direction, idx)
}

// higherPrecedence compares the two rules and returns if newRule has a higher precedence than oldRule.
func higherPrecedence(oldRule, newRule *networkpolicy.RuleInfo) bool {
if oldRule == nil {
return true
}
// compare tier priorities
effectiveTierPriorityK8sNP := (networkpolicy.DefaultTierPriority + networkpolicy.BaselineTierPriority) / 2
oldTierPriority, newTierPriority := effectiveTierPriorityK8sNP, effectiveTierPriorityK8sNP
if oldRule.Policy.TierPriority != nil {
oldTierPriority = *oldRule.Policy.TierPriority
}
if newRule.Policy.TierPriority != nil {
newTierPriority = *newRule.Policy.TierPriority
}
if oldTierPriority != newTierPriority {
return oldTierPriority > newTierPriority
}
// compare policy priorities if tier priorities equal
if oldRule.Policy.Priority != nil && newRule.Policy.Priority != nil && *newRule.Policy.Priority != *oldRule.Policy.Priority {
return *oldRule.Policy.Priority > *newRule.Policy.Priority
}
// compare rule priorities further as previous steps have confirmed policy priorities are equal
// Kubernetes NetworkPolicies rules have the same default priorities, so rule index is hacked for comparison
// "-1" indicates default isolation, which has a lower precedence than KNP policy rules with ">=0" rule indexes
if oldRule.Index != newRule.Index {
if newRule.Policy.SourceRef.Type == controlplane.K8sNetworkPolicy && oldRule.Policy.SourceRef.Type == controlplane.K8sNetworkPolicy {
return oldRule.Index < newRule.Index
} else {
return oldRule.Index > newRule.Index
}
}
// use policy name as the tie-breaker if all priorities are equal
return oldRule.Policy.Name < newRule.Policy.Name
}

// predictEndpointsRules returns the predicted rules effective from srcEndpoints to dstEndpoints.
// Rules returned satisfy a. in source applied policies and destination egress rules,
// or b. in source ingress rules and destination applied policies or c. applied to KNP default isolation.
func predictEndpointsRules(srcEndpoints, dstEndpoints *networkpolicy.EndpointRuleAnalysis) *networkpolicy.Rule {
var commonRule *networkpolicy.RuleInfo
dstEgressRules, srcIngressRules := sets.New[string](), sets.New[string]()
compareRules := func(commonRule, rule *networkpolicy.RuleInfo) *networkpolicy.RuleInfo {
if higherPrecedence(commonRule, rule) {
return rule
}
return commonRule
}
if srcEndpoints != nil && dstEndpoints != nil {
for _, rule := range dstEndpoints.EgressRules {
if srcEndpoints.PolicyUIDs.Has(rule.Policy.SourceRef.UID) {
dstEgressRules.Insert(ruleKey(rule.Policy.SourceRef.UID, rule.Direction, rule.Index))
commonRule = compareRules(commonRule, rule)
}
}
for _, rule := range srcEndpoints.IngressRules {
if dstEndpoints.PolicyUIDs.Has(rule.Policy.SourceRef.UID) {
srcIngressRules.Insert(ruleKey(rule.Policy.SourceRef.UID, rule.Direction, rule.Index))
commonRule = compareRules(commonRule, rule)
}
}
// Manually insert Kubernetes NetworkPolicy default isolation rules if exists.
// Default isolation rules has the index of -1 to indicate lower precedence.
for _, rule := range srcEndpoints.EgressIsolated {
if !dstEgressRules.Has(ruleKey(rule.Policy.SourceRef.UID, rule.Direction, rule.Index)) {
defaultDropRule := &networkpolicy.RuleInfo{
Policy: rule.Policy,
Index: -1,
Direction: v1beta2.DirectionOut,
}
commonRule = compareRules(commonRule, defaultDropRule)
}
}
for _, rule := range dstEndpoints.IngressIsolated {
if !srcIngressRules.Has(ruleKey(rule.Policy.SourceRef.UID, rule.Direction, rule.Index)) {
defaultDropRule := &networkpolicy.RuleInfo{
Policy: rule.Policy,
Index: -1,
Direction: v1beta2.DirectionIn,
}
commonRule = compareRules(commonRule, defaultDropRule)
}
}
}
// if no common rule or default isolation is found, return empty result
if commonRule == nil {
return &networkpolicy.Rule{}
}
return &networkpolicy.Rule{
PolicyRef: networkpolicy.PolicyRef{
Namespace: commonRule.Policy.SourceRef.Namespace,
Name: commonRule.Policy.SourceRef.Name,
UID: commonRule.Policy.SourceRef.UID,
},
Direction: commonRule.Direction,
RuleIndex: commonRule.Index,
}
}

// HandleFunc creates a http.HandlerFunc which uses an AgentNetworkPolicyInfoQuerier
// to query network policy rules in current agent.
func HandleFunc(eq networkpolicy.EndpointQuerier) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("source")
dst := r.URL.Query().Get("destination")

var srcNS, srcPod, dstNS, dstPod string
srcNS, srcPod = parsePeer(src)
dstNS, dstPod = parsePeer(dst)
if srcPod == "" || dstPod == "" {
http.Error(w, "invalid command argument format", http.StatusBadRequest)
return
}

// query endpoints and handle response errors
endpointAnalysisSource, err := eq.QueryNetworkPolicyRules(srcNS, srcPod)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
endpointAnalysisDestination, err := eq.QueryNetworkPolicyRules(dstNS, dstPod)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
endpointAnalysisRule := predictEndpointsRules(endpointAnalysisSource, endpointAnalysisDestination)
if err := json.NewEncoder(w).Encode(endpointAnalysisRule); err != nil {
http.Error(w, "failed to encode response: "+err.Error(), http.StatusInternalServerError)
}
}
}
Loading

0 comments on commit 9bf6c0f

Please sign in to comment.