diff --git a/cmd/npv/sub/helper.go b/cmd/npv/sub/helper.go index 824a94b..40c09bc 100644 --- a/cmd/npv/sub/helper.go +++ b/cmd/npv/sub/helper.go @@ -4,8 +4,12 @@ import ( "context" "errors" "fmt" + "math/rand" + "strconv" + "strings" "github.com/cilium/cilium/pkg/client" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" @@ -111,3 +115,62 @@ func getPodEndpointID(ctx context.Context, d *dynamic.DynamicClient, namespace, return endpointID, nil } + +func getIdentityMap(ctx context.Context, d *dynamic.DynamicClient) (map[int]*unstructured.Unstructured, error) { + gvr := schema.GroupVersionResource{ + Group: "cilium.io", + Version: "v2", + Resource: "ciliumidentities", + } + li, err := d.Resource(gvr).List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, err + } + + ret := make(map[int]*unstructured.Unstructured) + for _, item := range li.Items { + id, err := strconv.Atoi(item.GetName()) + if err != nil { + return nil, err + } + ret[id] = &item + } + return ret, nil +} + +func getIdentityExampleMap(ctx context.Context, d *dynamic.DynamicClient) (map[int]string, error) { + gvr := schema.GroupVersionResource{ + Group: "cilium.io", + Version: "v2", + Resource: "ciliumendpoints", + } + + li, err := d.Resource(gvr).Namespace(corev1.NamespaceAll).List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, err + } + + ret := make(map[int]string) + for _, ep := range li.Items { + identity, ok, err := unstructured.NestedInt64(ep.Object, "status", "identity", "id") + if err != nil { + return nil, err + } + if !ok { + continue + } + if _, ok := ret[int(identity)]; ok { + ret[int(identity)] += "," + ep.GetName() + } else { + ret[int(identity)] = ep.GetName() + } + } + for k, v := range ret { + if strings.Contains(v, ",") { + samples := strings.Split(v, ",") + i := rand.Intn(len(samples)) + ret[k] = samples[i] + } + } + return ret, nil +} diff --git a/cmd/npv/sub/inspect.go b/cmd/npv/sub/inspect.go new file mode 100644 index 0000000..886f8cb --- /dev/null +++ b/cmd/npv/sub/inspect.go @@ -0,0 +1,237 @@ +package sub + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strconv" + "strings" + "text/tabwriter" + + "github.com/cilium/cilium/pkg/identity" + "github.com/cilium/cilium/pkg/u8proto" + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" +) + +var inspectOptions struct { + prefix bool +} + +func init() { + inspectCmd.Flags().BoolVarP(&inspectOptions.prefix, "prefix", "p", false, "search pod with specified prefix") + rootCmd.AddCommand(inspectCmd) +} + +var inspectCmd = &cobra.Command{ + Use: "inspect", + Short: "Inspect network policies applied to a pod", + Long: `Inspect network policies applied to a pod`, + + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runInspect(context.Background(), cmd.OutOrStdout(), args[0]) + }, +} + +type policyEntryKey struct { + Identity int `json:"Identity"` + Direction int `json:"TrafficDirection"` + Protocol int `json:"Nexthdr"` + BigPort int `json:"DestPortNetwork"` // big endian +} + +// For the meanings of the flags, see: +// https://github.com/cilium/cilium/blob/v1.16.3/bpf/lib/common.h#L394 +type policyEntry struct { + Flags int `json:"Flags"` + Packets int `json:"Packets"` + Bytes int `json:"Bytes"` + Key policyEntryKey `json:"Key"` +} + +func (p policyEntry) IsDenyRule() bool { + return (p.Flags & 1) > 0 +} + +func (p policyEntry) IsEgressRule() bool { + return p.Key.Direction > 0 +} + +func (p policyEntry) IsWildcardProtocol() bool { + return (p.Flags & 2) > 0 +} + +func (p policyEntry) IsWildcardPort() bool { + return (p.Flags & 4) > 0 +} + +// This command aims to show the result of "cilium bpf policy get" from a remote pod. +// https://github.com/cilium/cilium/blob/v1.16.3/cilium-dbg/cmd/bpf_policy_get.go +type inspectEntry struct { + Policy string `json:"policy"` + Direction string `json:"direction"` + Namespace string `json:"namespace"` + Identity int `json:"identity"` + WildcardProtocol bool `json:"wildcard_protocol"` + WildcardPort bool `json:"wildcard_port"` + Protocol int `json:"protocol"` + Port int `json:"port"` + Bytes int `json:"bytes"` + Packets int `json:"packets"` +} + +func queryPolicyMap(ctx context.Context, clientset *kubernetes.Clientset, dynamicClient *dynamic.DynamicClient, namespace, name string) ([]policyEntry, error) { + endpointID, err := getPodEndpointID(ctx, dynamicClient, namespace, name) + if err != nil { + return nil, fmt.Errorf("failed to get pod endpoint ID: %w", err) + } + + url, err := getProxyEndpoint(ctx, clientset, namespace, name) + if err != nil { + return nil, fmt.Errorf("failed to get proxy endpoint: %w", err) + } + + url = fmt.Sprintf("%s/policy/%d", url, endpointID) + resp, err := http.Get(url) + if err != nil { + return nil, fmt.Errorf("failed to request policy: %w", err) + } + defer resp.Body.Close() + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + policies := make([]policyEntry, 0) + err = json.Unmarshal(data, &policies) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + return policies, nil +} + +func runInspect(ctx context.Context, w io.Writer, name string) error { + clientset, dynamicClient, err := createK8sClients() + if err != nil { + return err + } + + if inspectOptions.prefix { + pods, err := clientset.CoreV1().Pods(rootOptions.namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + return nil + } + found := false + prefix := name + for _, p := range pods.Items { + if strings.HasPrefix(p.GetName(), prefix) { + if found { + return errors.New("multiple pods found for the prefix: " + prefix) + } + found = true + name = p.GetName() + } + } + } + + policies, err := queryPolicyMap(ctx, clientset, dynamicClient, rootOptions.namespace, name) + if err != nil { + return err + } + + ids, err := getIdentityMap(ctx, dynamicClient) + if err != nil { + return err + } + + examples, err := getIdentityExampleMap(ctx, dynamicClient) + if err != nil { + return err + } + + arr := make([]inspectEntry, len(policies)) + for i, policy := range policies { + var entry inspectEntry + if policy.IsDenyRule() { + entry.Policy = policyDeny + } else { + entry.Policy = policyAllow + } + if policy.IsEgressRule() { + entry.Direction = directionEgress + } else { + entry.Direction = directionIngress + } + entry.Namespace = "-" + if id, ok := ids[policy.Key.Identity]; ok { + ns, ok, err := unstructured.NestedString(id.Object, "security-labels", "k8s:io.kubernetes.pod.namespace") + if err != nil { + return err + } + if ok { + entry.Namespace = ns + } + } + entry.Identity = policy.Key.Identity + entry.WildcardProtocol = policy.IsWildcardProtocol() + entry.WildcardPort = policy.IsWildcardPort() + entry.Protocol = policy.Key.Protocol + entry.Port = ((policy.Key.BigPort & 0xFF) << 8) + ((policy.Key.BigPort & 0xFF00) >> 8) + entry.Bytes = policy.Bytes + entry.Packets = policy.Packets + arr[i] = entry + } + + switch rootOptions.output { + case OutputJson: + text, err := json.MarshalIndent(arr, "", " ") + if err != nil { + return err + } + _, err = w.Write(text) + return err + case OutputSimple: + tw := tabwriter.NewWriter(w, 0, 1, 1, ' ', 0) + if !rootOptions.noHeaders { + if _, err := tw.Write([]byte("POLICY\tDIRECTION\tIDENTITY\tNAMESPACE\tEXAMPLE\tPROTOCOL\tPORT\tBYTES\tPACKETS\n")); err != nil { + return err + } + } + for _, p := range arr { + var example, protocol, port string + if v, ok := examples[p.Identity]; ok { + example = v + } else { + idObj := identity.NumericIdentity(p.Identity) + if idObj.IsReservedIdentity() { + example = "reserved:" + idObj.String() + } + } + if p.WildcardProtocol { + protocol = "ANY" + } else { + protocol = u8proto.U8proto(p.Protocol).String() + } + if p.WildcardPort { + port = "ANY" + } else { + port = strconv.Itoa(p.Port) + } + if _, err := tw.Write([]byte(fmt.Sprintf("%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\n", p.Policy, p.Direction, p.Identity, p.Namespace, example, protocol, port, p.Bytes, p.Packets))); err != nil { + return err + } + } + return tw.Flush() + default: + return fmt.Errorf("unknown format: %s", rootOptions.output) + } +} diff --git a/cmd/npv/sub/summary.go b/cmd/npv/sub/summary.go new file mode 100644 index 0000000..3d0879b --- /dev/null +++ b/cmd/npv/sub/summary.go @@ -0,0 +1,117 @@ +package sub + +import ( + "context" + "encoding/json" + "fmt" + "io" + "sort" + "strings" + "text/tabwriter" + + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func init() { + rootCmd.AddCommand(summaryCmd) +} + +var summaryCmd = &cobra.Command{ + Use: "summary", + Short: "Show summary of network policy count", + Long: `Show summary of network policy count`, + + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + return runSummary(context.Background(), cmd.OutOrStdout()) + }, +} + +type summaryEntry struct { + Namespace string `json:"namespace"` + Name string `json:"name"` + IngressAllow int `json:"ingress_allow"` + IngressDeny int `json:"ingress_deny"` + EgressAllow int `json:"egress_allow"` + EgressDeny int `json:"egress_deny"` +} + +func lessSummaryEntry(x, y *summaryEntry) bool { + ret := strings.Compare(x.Namespace, y.Namespace) + if ret == 0 { + ret = strings.Compare(x.Name, y.Name) + } + return ret < 0 +} + +func runSummary(ctx context.Context, w io.Writer) error { + clientset, dynamicClient, err := createK8sClients() + if err != nil { + return err + } + + summary := make([]summaryEntry, 0) + pods, err := clientset.CoreV1().Pods(rootOptions.namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + return err + } + + for _, p := range pods.Items { + var entry summaryEntry + entry.Namespace = p.Namespace + entry.Name = p.Name + + if p.Spec.HostNetwork { + entry.EgressDeny = -1 + entry.EgressAllow = -1 + entry.IngressDeny = -1 + entry.IngressAllow = -1 + } else { + policies, err := queryPolicyMap(ctx, clientset, dynamicClient, rootOptions.namespace, p.Name) + if err != nil { + return err + } + + for _, p := range policies { + switch { + case p.IsEgressRule() && p.IsDenyRule(): + entry.EgressDeny++ + case p.IsEgressRule() && !p.IsDenyRule(): + entry.EgressAllow++ + case !p.IsEgressRule() && p.IsDenyRule(): + entry.IngressDeny++ + case !p.IsEgressRule() && !p.IsDenyRule(): + entry.IngressAllow++ + } + } + } + summary = append(summary, entry) + } + sort.Slice(summary, func(i, j int) bool { return lessSummaryEntry(&summary[i], &summary[j]) }) + + switch rootOptions.output { + case OutputJson: + text, err := json.MarshalIndent(summary, "", " ") + if err != nil { + return err + } + _, err = w.Write(text) + return err + case OutputSimple: + tw := tabwriter.NewWriter(w, 0, 1, 1, ' ', 0) + if !rootOptions.noHeaders { + if _, err := tw.Write([]byte("NAMESPACE\tNAME\tINGRESS-ALLOW\tINGRESS-DENY\tEGRESS-ALLOW\tEGRESS-DENY\n")); err != nil { + return err + } + } + for _, p := range summary { + if _, err := tw.Write([]byte(fmt.Sprintf("%v\t%v\t%v\t%v\t%v\t%v\n", p.Namespace, p.Name, p.IngressAllow, p.IngressDeny, p.EgressAllow, p.EgressDeny))); err != nil { + return err + } + } + return tw.Flush() + default: + return fmt.Errorf("unknown format: %s", rootOptions.output) + } +} diff --git a/e2e/testdata/ubuntu.yaml b/e2e/testdata/ubuntu.yaml index 2319064..24c5392 100644 --- a/e2e/testdata/ubuntu.yaml +++ b/e2e/testdata/ubuntu.yaml @@ -21,6 +21,7 @@ rules: - cilium.io resources: - ciliumendpoints + - ciliumidentities verbs: - get - list diff --git a/go.mod b/go.mod index 0904b62..a3a9435 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/onsi/gomega v1.34.2 github.com/spf13/cobra v1.8.1 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 + k8s.io/api v0.29.3 k8s.io/apimachinery v0.29.3 k8s.io/client-go v0.29.3 ) @@ -92,7 +93,6 @@ require ( gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.29.3 // indirect k8s.io/klog/v2 v2.120.0 // indirect k8s.io/kube-openapi v0.0.0-20240105020646-a37d4de58910 // indirect k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect