Skip to content

Commit

Permalink
Add API and antctl for effective policy rule query
Browse files Browse the repository at this point in the history
Adds a versioned API and antctl query for NetworkPolicy
access review 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 Jan 25, 2024
1 parent ca5dc45 commit 8137cd6
Show file tree
Hide file tree
Showing 13 changed files with 938 additions and 282 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,
Kubernetes NetworkPolicies and AdminNetworkPolicies to predict the effective
policy rule for traffic between source and destination Pods.

```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
32 changes: 30 additions & 2 deletions pkg/antctl/antctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ import (
"antrea.io/antrea/pkg/antctl/transform/version"
cpv1beta "antrea.io/antrea/pkg/apis/controlplane/v1beta2"
systemv1beta1 "antrea.io/antrea/pkg/apis/system/v1beta1"
endpointServer "antrea.io/antrea/pkg/apiserver/handlers/endpoint"
controllerinforest "antrea.io/antrea/pkg/apiserver/registry/system/controllerinfo"
"antrea.io/antrea/pkg/client/clientset/versioned/scheme"
controllernetworkpolicy "antrea.io/antrea/pkg/controller/networkpolicy"
"antrea.io/antrea/pkg/flowaggregator/apiserver/handlers/flowrecords"
"antrea.io/antrea/pkg/flowaggregator/apiserver/handlers/recordmetrics"
)
Expand Down Expand Up @@ -507,7 +507,35 @@ $ antctl get podmulticaststats pod -n namespace`,
outputType: single,
},
},
transformedResponse: reflect.TypeOf(controllernetworkpolicy.EndpointQueryResponse{}),
transformedResponse: reflect.TypeOf(endpointServer.EndpointQueryResponse{}),
},
{use: "networkpolicyanalysis",
aliases: []string{"npanalysis"},
short: "Analyze network policy rules.",
long: "Analyze network policies in the cluster and return the rule expected to be effective on the source and destination endpoints provided.",
example: ` Query effective network policy rule 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(endpointServer.Rule{}),
},
{
use: "flowrecords",
Expand Down
28 changes: 26 additions & 2 deletions pkg/antctl/command_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import (
"antrea.io/antrea/pkg/antctl/output"
"antrea.io/antrea/pkg/antctl/runtime"
"antrea.io/antrea/pkg/apis/controlplane/v1beta2"
"antrea.io/antrea/pkg/controller/networkpolicy"
endpointServer "antrea.io/antrea/pkg/apiserver/handlers/endpoint"
)

type formatterType string
Expand Down Expand Up @@ -444,7 +444,7 @@ func (cd *commandDefinition) tableOutputForQueryEndpoint(obj interface{}, writer
return nil
}
// iterate through each endpoint and construct response
endpointQueryResponse := obj.(*networkpolicy.EndpointQueryResponse)
endpointQueryResponse := obj.(*endpointServer.EndpointQueryResponse)
for _, endpoint := range endpointQueryResponse.Endpoints {
// transform applied policies to string representation
policies := make([][]string, 0)
Expand Down 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.(*endpointServer.Rule)
if queryResponse.Name != "" {
ruleStr := []string{queryResponse.Name, queryResponse.Namespace, string(queryResponse.Type), strconv.Itoa(queryResponse.RuleIndex), string(queryResponse.Direction)}
if err := constructTable([]string{"Name", "Namespace", "PolicyType", "RuleIndex", "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
64 changes: 50 additions & 14 deletions pkg/antctl/command_definition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@ import (
"antrea.io/antrea/pkg/antctl/transform/controllerinfo"
"antrea.io/antrea/pkg/antctl/transform/networkpolicy"
"antrea.io/antrea/pkg/antctl/transform/version"
"antrea.io/antrea/pkg/apis/controlplane"
cpv1beta "antrea.io/antrea/pkg/apis/controlplane/v1beta2"
"antrea.io/antrea/pkg/apis/crd/v1beta1"
controllernetworkpolicy "antrea.io/antrea/pkg/controller/networkpolicy"
endpointServer "antrea.io/antrea/pkg/apiserver/handlers/endpoint"
)

type Foobar struct {
Expand Down Expand Up @@ -909,18 +910,18 @@ func TestGetRequestErrorFallback(t *testing.T) {
}

func TestTableOutputForQueryEndpoint(t *testing.T) {
policyRef0 := controllernetworkpolicy.PolicyRef{Namespace: "testNamespace", Name: "test-ingress-egress", UID: "uid-1"}
policyRef1 := controllernetworkpolicy.PolicyRef{Namespace: "testNamespace", Name: "default-deny-egress", UID: "uid-2"}
policyRef0 := endpointServer.PolicyRef{Namespace: "testNamespace", Name: "test-ingress-egress", UID: "uid-1"}
policyRef1 := endpointServer.PolicyRef{Namespace: "testNamespace", Name: "default-deny-egress", UID: "uid-2"}
tc := []struct {
name string
rawResponseData interface{}
expected string
}{
{
name: "Pod selected by no policy",
rawResponseData: &controllernetworkpolicy.EndpointQueryResponse{
Endpoints: []controllernetworkpolicy.Endpoint{
{Namespace: "testNamespace", Name: "podA", Policies: []controllernetworkpolicy.Policy{}, Rules: []controllernetworkpolicy.Rule{}},
rawResponseData: &endpointServer.EndpointQueryResponse{
Endpoints: []endpointServer.Endpoint{
{Namespace: "testNamespace", Name: "podA", Policies: []endpointServer.Policy{}, Rules: []endpointServer.Rule{}},
},
},
expected: `Endpoint testNamespace/podA
Expand All @@ -934,13 +935,13 @@ Ingress Rules: None
},
{
name: "Pod selected by 1 policy",
rawResponseData: &controllernetworkpolicy.EndpointQueryResponse{
Endpoints: []controllernetworkpolicy.Endpoint{
rawResponseData: &endpointServer.EndpointQueryResponse{
Endpoints: []endpointServer.Endpoint{
{
Namespace: "testNamespace",
Name: "podA",
Policies: []controllernetworkpolicy.Policy{{PolicyRef: policyRef0}},
Rules: []controllernetworkpolicy.Rule{
Policies: []endpointServer.Policy{{PolicyRef: policyRef0}},
Rules: []endpointServer.Rule{
{PolicyRef: policyRef0, Direction: cpv1beta.DirectionOut, RuleIndex: 0},
{PolicyRef: policyRef0, Direction: cpv1beta.DirectionIn, RuleIndex: 0},
},
Expand All @@ -964,16 +965,16 @@ test-ingress-egress testNamespace 0 uid-1
},
{
name: "Pod selected by 2 different policies",
rawResponseData: &controllernetworkpolicy.EndpointQueryResponse{
Endpoints: []controllernetworkpolicy.Endpoint{
rawResponseData: &endpointServer.EndpointQueryResponse{
Endpoints: []endpointServer.Endpoint{
{
Namespace: "testNamespace",
Name: "podA",
Policies: []controllernetworkpolicy.Policy{
Policies: []endpointServer.Policy{
{PolicyRef: policyRef0},
{PolicyRef: policyRef1},
},
Rules: []controllernetworkpolicy.Rule{
Rules: []endpointServer.Rule{
{PolicyRef: policyRef0, Direction: cpv1beta.DirectionOut, RuleIndex: 0},
{PolicyRef: policyRef0, Direction: cpv1beta.DirectionIn, RuleIndex: 0},
},
Expand Down Expand Up @@ -1008,6 +1009,41 @@ test-ingress-egress testNamespace 0 uid-1
}
}

func TestTableOutputForQueryNetworkPolicyAnalysis(t *testing.T) {
policyRef0 := endpointServer.PolicyRef{Type: controlplane.K8sNetworkPolicy, Namespace: "testNamespace", Name: "test-default-deny", UID: "uid-1"}
tc := []struct {
name string
rawResponseData interface{}
expected string
}{
{
name: "No matching rule",
rawResponseData: &endpointServer.Rule{},
expected: ``,
},
{
name: "Matched KNP default drop rule",
rawResponseData: &endpointServer.Rule{
PolicyRef: policyRef0,
Direction: cpv1beta.DirectionIn,
RuleIndex: -1,
},
expected: `Name Namespace PolicyType RuleIndex Direction
test-default-deny testNamespace K8sNetworkPolicy -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
75 changes: 73 additions & 2 deletions pkg/apiserver/handlers/endpoint/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,42 @@ import (
"encoding/json"
"net/http"

"k8s.io/apimachinery/pkg/types"

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

// EndpointQueryResponse is the reply struct for anctl endpoint queries
type EndpointQueryResponse struct {
Endpoints []Endpoint `json:"endpoints,omitempty"`
}

type Endpoint struct {
Namespace string `json:"namespace,omitempty"`
Name string `json:"name,omitempty"`
Policies []Policy `json:"policies,omitempty"`
Rules []Rule `json:"rules,omitempty"`
}

type PolicyRef struct {
Type controlplane.NetworkPolicyType `json:"type,omitempty"`
Namespace string `json:"namespace,omitempty"`
Name string `json:"name,omitempty"`
UID types.UID `json:"uid,omitempty"`
}

type Policy struct {
PolicyRef
}

type Rule struct {
PolicyRef
Direction cpv1beta.Direction `json:"direction,omitempty"`
RuleIndex int `json:"ruleindex,omitempty"`
}

// HandleFunc creates a http.HandlerFunc which uses an AgentNetworkPolicyInfoQuerier
// to query network policy rules in current agent.
func HandleFunc(eq networkpolicy.EndpointQuerier) http.HandlerFunc {
Expand All @@ -36,15 +69,53 @@ func HandleFunc(eq networkpolicy.EndpointQuerier) http.HandlerFunc {
return
}
// query endpoint and handle response errors
endpointQueryResponse, err := eq.QueryNetworkPolicies(namespace, podName)
endpointNetworkPolicyRules, err := eq.QueryNetworkPolicyRules(namespace, podName)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if endpointQueryResponse == nil {
if endpointNetworkPolicyRules == nil {
http.Error(w, "could not find any endpoints matching your selection", http.StatusNotFound)
return
}

// make response policies
responsePolicies := make([]Policy, 0)
for _, internalPolicy := range endpointNetworkPolicyRules.AppliedPolicies {
responsePolicy := Policy{
PolicyRef: PolicyRef{
Type: internalPolicy.SourceRef.Type,
Namespace: internalPolicy.SourceRef.Namespace,
Name: internalPolicy.SourceRef.Name,
UID: internalPolicy.SourceRef.UID,
},
}
responsePolicies = append(responsePolicies, responsePolicy)
}
responseRules := make([]Rule, 0)
// create rules based on egress and ingress policies
for _, internalPolicy := range append(endpointNetworkPolicyRules.EgressRules, endpointNetworkPolicyRules.IngressRules...) {
newRule := Rule{
PolicyRef: PolicyRef{
Type: internalPolicy.Policy.SourceRef.Type,
Namespace: internalPolicy.Policy.SourceRef.Namespace,
Name: internalPolicy.Policy.SourceRef.Name,
UID: internalPolicy.Policy.SourceRef.UID,
},
Direction: internalPolicy.Direction,
RuleIndex: internalPolicy.Index,
}
responseRules = append(responseRules, newRule)
}
// for now, selector only selects a single endpoint (pod, namespace)
endpoint := Endpoint{
Namespace: namespace,
Name: podName,
Policies: responsePolicies,
Rules: responseRules,
}
endpointQueryResponse := &EndpointQueryResponse{[]Endpoint{endpoint}}

if err := json.NewEncoder(w).Encode(*endpointQueryResponse); err != nil {
http.Error(w, "failed to encode response: "+err.Error(), http.StatusInternalServerError)
}
Expand Down
Loading

0 comments on commit 8137cd6

Please sign in to comment.