Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
Signed-off-by: Daichi Sakaue <daichi-sakaue@cybozu.co.jp>
  • Loading branch information
yokaze committed May 22, 2024
1 parent 90b7e0f commit 7d585eb
Show file tree
Hide file tree
Showing 11 changed files with 522 additions and 22 deletions.
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

0 comments on commit 7d585eb

Please sign in to comment.