Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip #5

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func runDump(ctx context.Context, w io.Writer, name string) error {
return err
}

endpointID, err := getPodEndpointID(ctx, dynamicClient, rootOptions.namespace, name)
endpointID, _, err := getPodEndpointID(ctx, dynamicClient, rootOptions.namespace, name)
if err != nil {
return err
}
Expand Down
66 changes: 54 additions & 12 deletions cmd/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"strconv"

"github.com/cilium/cilium/pkg/client"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -14,13 +15,17 @@ import (
"k8s.io/client-go/rest"
)

const (
directionEgress = "EGRESS"
directionIngress = "INGRESS"
)

func createClients(ctx context.Context, name string) (*kubernetes.Clientset, *dynamic.DynamicClient, *client.Client, error) {
config, err := rest.InClusterConfig()
if err != nil {
return nil, nil, nil, err
}

// Create Kubernetes Clients
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, nil, nil, err
Expand All @@ -31,19 +36,26 @@ func createClients(ctx context.Context, name string) (*kubernetes.Clientset, *dy
return nil, nil, nil, err
}

// Create Cilium Client
endpoint, err := getProxyEndpoint(ctx, clientset, rootOptions.namespace, name)
if err != nil {
return nil, nil, nil, err
}
ciliumClient, err := client.NewClient(endpoint)
ciliumClient, err := createCiliumClient(ctx, clientset, rootOptions.namespace, name)
if err != nil {
return nil, nil, nil, err
}

return clientset, dynamicClient, ciliumClient, err
}

func createCiliumClient(ctx context.Context, clientset *kubernetes.Clientset, namespace, name string) (*client.Client, error) {
endpoint, err := getProxyEndpoint(ctx, clientset, namespace, name)
if err != nil {
return nil, err
}
client, err := client.NewClient(endpoint)
if err != nil {
return nil, err
}
return client, nil
}

func getProxyEndpoint(ctx context.Context, c *kubernetes.Clientset, namespace, name string) (string, error) {
targetPod, err := c.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
Expand All @@ -67,7 +79,7 @@ func getProxyEndpoint(ctx context.Context, c *kubernetes.Clientset, namespace, n
return fmt.Sprintf("http://%s:%d", podIP, rootOptions.proxyPort), nil
}

func getPodEndpointID(ctx context.Context, d *dynamic.DynamicClient, namespace, name string) (int64, error) {
func getPodEndpointID(ctx context.Context, d *dynamic.DynamicClient, namespace, name string) (int64, int64, error) {
gvr := schema.GroupVersionResource{
Group: "cilium.io",
Version: "v2",
Expand All @@ -76,16 +88,46 @@ func getPodEndpointID(ctx context.Context, d *dynamic.DynamicClient, namespace,

ep, err := d.Resource(gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return 0, err
return 0, 0, err
}

endpointID, found, err := unstructured.NestedInt64(ep.Object, "status", "id")
if err != nil {
return 0, err
return 0, 0, err
}
if !found {
return 0, 0, errors.New("CiliumEndpoint does not have .status.id")
}

endpointIdentity, found, err := unstructured.NestedInt64(ep.Object, "status", "identity", "id")
if err != nil {
return 0, 0, err
}
if !found {
return 0, errors.New("endpoint resource is broken")
return 0, 0, errors.New("CiliumEndpoint does not have .status.identity.id")
}

return endpointID, nil
return endpointID, endpointIdentity, nil
}

func listCiliumIDs(ctx context.Context, d *dynamic.DynamicClient) (*unstructured.UnstructuredList, error) {
gvr := schema.GroupVersionResource{
Group: "cilium.io",
Version: "v2",
Resource: "ciliumidentities",
}
return d.Resource(gvr).List(ctx, metav1.ListOptions{})
}

func findCiliumID(dict *unstructured.UnstructuredList, id int64) *unstructured.Unstructured {
if dict == nil {
return nil
}
name := strconv.FormatInt(id, 10)
for _, item := range dict.Items {
if item.GetName() == name {
return &item
}
}
return nil
}
13 changes: 13 additions & 0 deletions cmd/l3.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package cmd

import "github.com/spf13/cobra"

var l3Cmd = &cobra.Command{
Use: "l3",
Short: "inspect l3 rules",
Long: `inspect l3 rules`,
}

func init() {
rootCmd.AddCommand(l3Cmd)
}
221 changes: 221 additions & 0 deletions cmd/l3_inspect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package cmd

import (
"context"
"encoding/json"
"fmt"
"io"
"slices"
"sort"
"strconv"
"strings"
"text/tabwriter"

"github.com/cilium/cilium/api/v1/client/endpoint"
"github.com/spf13/cobra"
"golang.org/x/exp/maps"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

func init() {
l3Cmd.AddCommand(l3InspectCmd)
}

var l3InspectCmd = &cobra.Command{
Use: "inspect",
Short: "",
Long: ``,

Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runL3Inspect(context.Background(), cmd.OutOrStdout(), args[0])
},
}

type l3InspectEntry struct {
Direction string `json:"direction"`
Allowed bool `json:"allowed"`
Namespace string `json:"namespace"`
Identity int64 `json:"identity"`
Labels map[string]string `json:"labels"`
}

func buildL3InspectEntry(dict *unstructured.UnstructuredList, id int64, direction string, allowed bool) (l3InspectEntry, error) {
val := l3InspectEntry{
Direction: direction,
Allowed: allowed,
Identity: id,
}

// https://docs.cilium.io/en/latest/gettingstarted/terminology/#special-identities
switch id {
case 0:
val.Labels = map[string]string{"!! reserved": "unknown"}
return val, nil
case 1:
val.Labels = map[string]string{"!! reserved": "host"}
return val, nil
case 2:
val.Labels = map[string]string{"!! reserved": "world"}
return val, nil
case 3:
val.Labels = map[string]string{"!! reserved": "unmanaged"}
return val, nil
case 4:
val.Labels = map[string]string{"!! reserved": "health"}
return val, nil
case 5:
val.Labels = map[string]string{"!! reserved": "init"}
return val, nil
case 6:
val.Labels = map[string]string{"!! reserved": "remote-node"}
return val, nil
case 7:
val.Labels = map[string]string{"!! reserved": "kube-apiserver"}
return val, nil
case 8:
val.Labels = map[string]string{"!! reserved": "ingress"}
return val, nil
}

obj := findCiliumID(dict, id)
if obj == nil {
return l3InspectEntry{}, fmt.Errorf("CiliumID is not found for ID: %d", id)
}

labels, found, err := unstructured.NestedStringMap(obj.Object, "security-labels")
if !found {
return l3InspectEntry{}, fmt.Errorf("security label is missing for CiliumID: %d", id)
}
if err != nil {
return l3InspectEntry{}, err
}
val.Labels = labels

ns, ok := labels["k8s:io.kubernetes.pod.namespace"]
if !ok {
return l3InspectEntry{}, fmt.Errorf("namespace label is missing for CiliumID: %d", id)
}
val.Namespace = ns
return val, nil
}

func compareL3InspectEntry(x, y *l3InspectEntry) bool {
if x.Direction != y.Direction {
return strings.Compare(x.Direction, y.Direction) < 0
}
if x.Allowed != y.Allowed {
return x.Allowed
}
if x.Namespace != y.Namespace {
return strings.Compare(x.Namespace, y.Namespace) < 0
}
if x.Identity != y.Identity {
return x.Identity < y.Identity
}
// Labels should differ between identities
return false
}

func runL3Inspect(ctx context.Context, w io.Writer, name string) error {
_, dynamicClient, client, err := createClients(ctx, name)
if err != nil {
return err
}

ciliumIDs, err := listCiliumIDs(ctx, dynamicClient)
if err != nil {
return err
}

endpointID, _, err := getPodEndpointID(ctx, dynamicClient, rootOptions.namespace, name)
if err != nil {
return err
}

params := endpoint.GetEndpointIDParams{
Context: ctx,
ID: strconv.FormatInt(endpointID, 10),
}
response, err := client.Endpoint.GetEndpointID(&params)
if err != nil {
return err
}

policyList := make([]l3InspectEntry, 0)

allowedEgress := response.Payload.Status.Policy.Realized.AllowedEgressIdentities
for _, id := range allowedEgress {
entry, err := buildL3InspectEntry(ciliumIDs, id, directionEgress, true)
if err != nil {
return err
}
policyList = append(policyList, entry)
}

deniedEgress := response.Payload.Status.Policy.Realized.DeniedEgressIdentities
for _, id := range deniedEgress {
entry, err := buildL3InspectEntry(ciliumIDs, id, directionEgress, false)
if err != nil {
return err
}
policyList = append(policyList, entry)
}

allowedIngress := response.Payload.Status.Policy.Realized.AllowedIngressIdentities
for _, id := range allowedIngress {
entry, err := buildL3InspectEntry(ciliumIDs, id, directionIngress, true)
if err != nil {
return err
}
policyList = append(policyList, entry)
}

deniedIngress := response.Payload.Status.Policy.Realized.DeniedIngressIdentities
for _, id := range deniedIngress {
entry, err := buildL3InspectEntry(ciliumIDs, id, directionIngress, false)
if err != nil {
return err
}
policyList = append(policyList, entry)
}

sort.Slice(policyList, func(i, j int) bool { return compareL3InspectEntry(&policyList[i], &policyList[j]) })

switch rootOptions.output {
case OutputJson:
text, err := json.MarshalIndent(policyList, "", " ")
if err != nil {
return err
}
_, err = w.Write(text)
return err
case OutputSimple:
tw := tabwriter.NewWriter(w, 0, 1, 1, ' ', 0)
_, err := tw.Write([]byte("DIRECTION\tALLOWED\tNAMESPACE\tIDENTITY\tLABELS\n"))
if err != nil {
return err
}
for _, p := range policyList {
keys := maps.Keys(p.Labels)
slices.Sort(keys)
for i, k := range keys {
switch i {
case 0:
_, err := tw.Write([]byte(fmt.Sprintf("%v\t%v\t%v\t%v\t%v=%v\n", p.Direction, p.Allowed, p.Namespace, p.Identity, k, p.Labels[k])))
if err != nil {
return err
}
default:
_, err := tw.Write([]byte(fmt.Sprintf("\t\t\t\t%v=%v\n", k, p.Labels[k])))
if err != nil {
return err
}
}
}
}
return tw.Flush()
default:
return fmt.Errorf("unknown format: %s", rootOptions.output)
}
}
Loading
Loading