github.com/cilium/cilium@v1.16.2/pkg/hubble/metrics/flows-to-world/handler_test.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package flows_to_world
     5  
     6  import (
     7  	"context"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/prometheus/client_golang/prometheus"
    12  	"github.com/prometheus/client_golang/prometheus/testutil"
    13  	"github.com/stretchr/testify/assert"
    14  	"google.golang.org/protobuf/types/known/wrapperspb"
    15  
    16  	flowpb "github.com/cilium/cilium/api/v1/flow"
    17  	"github.com/cilium/cilium/pkg/hubble/metrics/api"
    18  	monitorAPI "github.com/cilium/cilium/pkg/monitor/api"
    19  )
    20  
    21  func TestFlowsToWorldHandler_MatchingFlow(t *testing.T) {
    22  	registry := prometheus.NewRegistry()
    23  	opts := api.Options{"sourceContext": "namespace", "destinationContext": "dns|ip"}
    24  	h := &flowsToWorldHandler{}
    25  	assert.NoError(t, h.Init(registry, opts))
    26  	assert.NoError(t, testutil.CollectAndCompare(h.flowsToWorld, strings.NewReader("")))
    27  	flow := flowpb.Flow{
    28  		Verdict:        flowpb.Verdict_DROPPED,
    29  		DropReasonDesc: flowpb.DropReason_POLICY_DENIED,
    30  		EventType:      &flowpb.CiliumEventType{Type: monitorAPI.MessageTypeDrop},
    31  		L4: &flowpb.Layer4{
    32  			Protocol: &flowpb.Layer4_TCP{
    33  				TCP: &flowpb.TCP{DestinationPort: 80},
    34  			},
    35  		},
    36  		Source: &flowpb.Endpoint{Namespace: "src-a"},
    37  		Destination: &flowpb.Endpoint{
    38  			Labels: []string{"reserved:world"},
    39  		},
    40  		DestinationNames: []string{"cilium.io"},
    41  	}
    42  
    43  	h.ProcessFlow(context.Background(), &flow)
    44  	flow.L4 = &flowpb.Layer4{
    45  		Protocol: &flowpb.Layer4_UDP{UDP: &flowpb.UDP{DestinationPort: 53}},
    46  	}
    47  	h.ProcessFlow(context.Background(), &flow)
    48  	flow.L4 = &flowpb.Layer4{
    49  		Protocol: &flowpb.Layer4_ICMPv4{ICMPv4: &flowpb.ICMPv4{}},
    50  	}
    51  	h.ProcessFlow(context.Background(), &flow)
    52  	flow.L4 = &flowpb.Layer4{
    53  		Protocol: &flowpb.Layer4_ICMPv6{ICMPv6: &flowpb.ICMPv6{}},
    54  	}
    55  	h.ProcessFlow(context.Background(), &flow)
    56  	expected := strings.NewReader(`# HELP hubble_flows_to_world_total Total number of flows to reserved:world
    57  # TYPE hubble_flows_to_world_total counter
    58  hubble_flows_to_world_total{destination="cilium.io",protocol="ICMPv4",source="src-a",verdict="DROPPED"} 1
    59  hubble_flows_to_world_total{destination="cilium.io",protocol="ICMPv6",source="src-a",verdict="DROPPED"} 1
    60  hubble_flows_to_world_total{destination="cilium.io",protocol="UDP",source="src-a",verdict="DROPPED"} 1
    61  hubble_flows_to_world_total{destination="cilium.io",protocol="TCP",source="src-a",verdict="DROPPED"} 1
    62  `)
    63  	assert.NoError(t, testutil.CollectAndCompare(h.flowsToWorld, expected))
    64  }
    65  
    66  func TestFlowsToWorldHandler_NonMatchingFlows(t *testing.T) {
    67  	registry := prometheus.NewRegistry()
    68  	opts := api.Options{"sourceContext": "namespace", "destinationContext": "dns|ip"}
    69  	h := &flowsToWorldHandler{}
    70  	assert.NoError(t, h.Init(registry, opts))
    71  
    72  	// destination is missing.
    73  	h.ProcessFlow(context.Background(), &flowpb.Flow{
    74  		Verdict: flowpb.Verdict_FORWARDED,
    75  		Source:  &flowpb.Endpoint{Namespace: "src-a"},
    76  	})
    77  	// destination is not reserved:world
    78  	h.ProcessFlow(context.Background(), &flowpb.Flow{
    79  		Verdict: flowpb.Verdict_FORWARDED,
    80  		Source:  &flowpb.Endpoint{Namespace: "src-a"},
    81  		Destination: &flowpb.Endpoint{
    82  			Labels: []string{"reserved:host"},
    83  		},
    84  	})
    85  	// L4 information is missing.
    86  	h.ProcessFlow(context.Background(), &flowpb.Flow{
    87  		Verdict: flowpb.Verdict_FORWARDED,
    88  		Source:  &flowpb.Endpoint{Namespace: "src-a"},
    89  		Destination: &flowpb.Endpoint{
    90  			Labels: []string{"reserved:world"},
    91  		},
    92  	})
    93  	// EventType is missing.
    94  	h.ProcessFlow(context.Background(), &flowpb.Flow{
    95  		Verdict: flowpb.Verdict_FORWARDED,
    96  		Source:  &flowpb.Endpoint{Namespace: "src-a"},
    97  		Destination: &flowpb.Endpoint{
    98  			Labels: []string{"reserved:world"},
    99  		},
   100  		L4: &flowpb.Layer4{
   101  			Protocol: &flowpb.Layer4_TCP{
   102  				TCP: &flowpb.TCP{DestinationPort: 80},
   103  			},
   104  		},
   105  	})
   106  	// Drop reason is not "Policy denied".
   107  	h.ProcessFlow(context.Background(), &flowpb.Flow{
   108  		Verdict:        flowpb.Verdict_DROPPED,
   109  		EventType:      &flowpb.CiliumEventType{Type: monitorAPI.MessageTypeDrop},
   110  		DropReasonDesc: flowpb.DropReason_STALE_OR_UNROUTABLE_IP,
   111  		L4: &flowpb.Layer4{
   112  			Protocol: &flowpb.Layer4_TCP{
   113  				TCP: &flowpb.TCP{DestinationPort: 80},
   114  			},
   115  		},
   116  		Source: &flowpb.Endpoint{Namespace: "src-a"},
   117  		Destination: &flowpb.Endpoint{
   118  			Labels: []string{"reserved:world"},
   119  		},
   120  		DestinationNames: []string{"cilium.io"},
   121  	})
   122  	// Flow is a reply.
   123  	h.ProcessFlow(context.Background(), &flowpb.Flow{
   124  		Verdict:   flowpb.Verdict_FORWARDED,
   125  		EventType: &flowpb.CiliumEventType{Type: monitorAPI.MessageTypeTrace},
   126  		L4: &flowpb.Layer4{
   127  			Protocol: &flowpb.Layer4_TCP{
   128  				TCP: &flowpb.TCP{DestinationPort: 80},
   129  			},
   130  		},
   131  		Source: &flowpb.Endpoint{Namespace: "src-a"},
   132  		Destination: &flowpb.Endpoint{
   133  			Labels: []string{"reserved:world"},
   134  		},
   135  		DestinationNames: []string{"cilium.io"},
   136  		IsReply:          wrapperspb.Bool(true),
   137  	})
   138  	assert.NoError(t, testutil.CollectAndCompare(h.flowsToWorld, strings.NewReader("")))
   139  }
   140  
   141  func TestFlowsToWorldHandler_AnyDrop(t *testing.T) {
   142  	registry := prometheus.NewRegistry()
   143  	opts := api.Options{"sourceContext": "namespace", "destinationContext": "dns|ip", "any-drop": ""}
   144  	h := &flowsToWorldHandler{}
   145  	assert.NoError(t, h.Init(registry, opts))
   146  	assert.NoError(t, testutil.CollectAndCompare(h.flowsToWorld, strings.NewReader("")))
   147  	flow := flowpb.Flow{
   148  		Verdict:        flowpb.Verdict_DROPPED,
   149  		DropReasonDesc: flowpb.DropReason_STALE_OR_UNROUTABLE_IP,
   150  		EventType:      &flowpb.CiliumEventType{Type: monitorAPI.MessageTypeDrop},
   151  		L4: &flowpb.Layer4{
   152  			Protocol: &flowpb.Layer4_TCP{
   153  				TCP: &flowpb.TCP{DestinationPort: 80},
   154  			},
   155  		},
   156  		Source: &flowpb.Endpoint{Namespace: "src-a"},
   157  		Destination: &flowpb.Endpoint{
   158  			Labels: []string{"reserved:world"},
   159  		},
   160  		DestinationNames: []string{"cilium.io"},
   161  	}
   162  	h.ProcessFlow(context.Background(), &flow)
   163  	expected := strings.NewReader(`# HELP hubble_flows_to_world_total Total number of flows to reserved:world
   164  # TYPE hubble_flows_to_world_total counter
   165  hubble_flows_to_world_total{destination="cilium.io",protocol="TCP",source="src-a",verdict="DROPPED"} 1
   166  `)
   167  	assert.NoError(t, testutil.CollectAndCompare(h.flowsToWorld, expected))
   168  }
   169  
   170  func TestFlowsToWorldHandler_IncludePort(t *testing.T) {
   171  	registry := prometheus.NewRegistry()
   172  	opts := api.Options{"sourceContext": "namespace", "destinationContext": "dns|ip", "port": ""}
   173  	h := &flowsToWorldHandler{}
   174  	assert.NoError(t, h.Init(registry, opts))
   175  	assert.NoError(t, testutil.CollectAndCompare(h.flowsToWorld, strings.NewReader("")))
   176  	flow := flowpb.Flow{
   177  		Verdict:   flowpb.Verdict_FORWARDED,
   178  		EventType: &flowpb.CiliumEventType{Type: monitorAPI.MessageTypeTrace},
   179  		L4: &flowpb.Layer4{
   180  			Protocol: &flowpb.Layer4_TCP{
   181  				TCP: &flowpb.TCP{DestinationPort: 80},
   182  			},
   183  		},
   184  		Source: &flowpb.Endpoint{Namespace: "src-a"},
   185  		Destination: &flowpb.Endpoint{
   186  			Labels: []string{"reserved:world"},
   187  		},
   188  		DestinationNames: []string{"cilium.io"},
   189  		IsReply:          wrapperspb.Bool(false),
   190  	}
   191  	h.ProcessFlow(context.Background(), &flow)
   192  	flow.L4 = &flowpb.Layer4{
   193  		Protocol: &flowpb.Layer4_UDP{
   194  			UDP: &flowpb.UDP{DestinationPort: 53},
   195  		},
   196  	}
   197  	h.ProcessFlow(context.Background(), &flow)
   198  	flow.L4 = &flowpb.Layer4{
   199  		Protocol: &flowpb.Layer4_SCTP{
   200  			SCTP: &flowpb.SCTP{DestinationPort: 2905},
   201  		},
   202  	}
   203  	h.ProcessFlow(context.Background(), &flow)
   204  	expected := strings.NewReader(`# HELP hubble_flows_to_world_total Total number of flows to reserved:world
   205  # TYPE hubble_flows_to_world_total counter
   206  hubble_flows_to_world_total{destination="cilium.io",port="2905",protocol="SCTP",source="src-a",verdict="FORWARDED"} 1
   207  hubble_flows_to_world_total{destination="cilium.io",port="80",protocol="TCP",source="src-a",verdict="FORWARDED"} 1
   208  hubble_flows_to_world_total{destination="cilium.io",port="53",protocol="UDP",source="src-a",verdict="FORWARDED"} 1
   209  `)
   210  	assert.NoError(t, testutil.CollectAndCompare(h.flowsToWorld, expected))
   211  }
   212  
   213  func TestFlowsToWorldHandler_SynOnly(t *testing.T) {
   214  	registry := prometheus.NewRegistry()
   215  	opts := api.Options{"sourceContext": "namespace", "destinationContext": "dns|ip", "syn-only": ""}
   216  	h := &flowsToWorldHandler{}
   217  	assert.NoError(t, h.Init(registry, opts))
   218  	assert.NoError(t, testutil.CollectAndCompare(h.flowsToWorld, strings.NewReader("")))
   219  	flow := flowpb.Flow{
   220  		Verdict:        flowpb.Verdict_DROPPED,
   221  		DropReasonDesc: flowpb.DropReason_POLICY_DENIED,
   222  		EventType:      &flowpb.CiliumEventType{Type: monitorAPI.MessageTypeDrop},
   223  		L4: &flowpb.Layer4{
   224  			Protocol: &flowpb.Layer4_TCP{
   225  				TCP: &flowpb.TCP{DestinationPort: 80, Flags: &flowpb.TCPFlags{SYN: true}},
   226  			},
   227  		},
   228  		Source: &flowpb.Endpoint{Namespace: "src-a"},
   229  		Destination: &flowpb.Endpoint{
   230  			Labels: []string{"reserved:world"},
   231  		},
   232  		DestinationNames: []string{"cilium.io"},
   233  		IsReply:          wrapperspb.Bool(false),
   234  	}
   235  	h.ProcessFlow(context.Background(), &flow)
   236  
   237  	// flows without is_reply field should be counted.
   238  	flow.IsReply = nil
   239  	h.ProcessFlow(context.Background(), &flow)
   240  
   241  	// reply flows should not be counted
   242  	flow.IsReply = wrapperspb.Bool(true)
   243  	h.ProcessFlow(context.Background(), &flow)
   244  
   245  	// Non-SYN should not be counted
   246  	flow.IsReply = wrapperspb.Bool(false)
   247  	flow.L4.GetTCP().Flags = &flowpb.TCPFlags{ACK: true}
   248  	h.ProcessFlow(context.Background(), &flow)
   249  
   250  	expected := strings.NewReader(`# HELP hubble_flows_to_world_total Total number of flows to reserved:world
   251  # TYPE hubble_flows_to_world_total counter
   252  hubble_flows_to_world_total{destination="cilium.io",protocol="TCP",source="src-a",verdict="DROPPED"} 2
   253  `)
   254  	assert.NoError(t, testutil.CollectAndCompare(h.flowsToWorld, expected))
   255  }
   256  
   257  func Test_flowsToWorldHandler_Status(t *testing.T) {
   258  	h := &flowsToWorldHandler{
   259  		context: &api.ContextOptions{
   260  			Destination: api.ContextIdentifierList{api.ContextNamespace},
   261  			Source:      api.ContextIdentifierList{api.ContextReservedIdentity},
   262  		},
   263  		anyDrop: true,
   264  		port:    true,
   265  		synOnly: true,
   266  	}
   267  	assert.Equal(t, "any-drop,port,syn-only,destination=namespace,source=reserved-identity", h.Status())
   268  	h.anyDrop = false
   269  	h.port = false
   270  	assert.Equal(t, "syn-only,destination=namespace,source=reserved-identity", h.Status())
   271  }