Skip to content

Commit d49e81f

Browse files
Bigdellepchaigno
authored andcommitted
feat: add IP trace ID filter to Hubble CLI
This change introduces the ability to filter Hubble flows by IP trace ID directly from the Hubble CLI. The following changes are included: - A new `--ip-trace-id` flag is added to the `hubble observe` command, which can be specified multiple times to filter for multiple trace IDs. - A new `IPTraceIDFilter` is implemented to perform the filtering logic based onthe provided trace IDs. - The `IPTraceIDFilter` is added to the list of default filters. - The help text for the `hubble observe` command is updated to include the new flag. Signed-off-by: Ben Bigdelle <bigdelle@google.com>
1 parent d30630a commit d49e81f

8 files changed

Lines changed: 209 additions & 6 deletions

File tree

hubble/cmd/cli_test.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"context"
99
_ "embed"
1010
"fmt"
11+
"strings"
1112
"testing"
1213

1314
"github.com/spf13/viper"
@@ -41,6 +42,12 @@ denylist:
4142
- '{"source_ip":["1.1.1.1"]}'
4243
`
4344

45+
func normalizeNewlines(content string) string {
46+
content = strings.ReplaceAll(content, "\r\n", "\n")
47+
content = strings.ReplaceAll(content, "\r", "\n")
48+
return content
49+
}
50+
4451
func TestTestHubbleObserve(t *testing.T) {
4552
tests := []struct {
4653
name string
@@ -116,7 +123,7 @@ Use "hubble [command] --help" for more information about a command.
116123
cli.SetArgs(tt.args)
117124
err := cli.Execute()
118125
require.Equal(t, tt.expectErr, err)
119-
output := b.String()
126+
output := normalizeNewlines(b.String())
120127
if tt.expectedOutput != "" {
121128
assert.Equal(t, tt.expectedOutput, output, "expected output does not match")
122129
}

hubble/cmd/observe/flows.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,10 @@ func newFlowsCmdHelper(usage cmdUsage, vp *viper.Viper, ofilter *flowFilter) *co
420420
"trace-id", ofilter,
421421
"Show only flows which match this trace ID"))
422422

423+
filterFlags.Var(filterVar(
424+
"ip-trace-id", ofilter,
425+
"Show only flows which match this IP trace ID"))
426+
423427
filterFlags.Var(filterVar(
424428
"from-fqdn", ofilter,
425429
`Show all flows originating at the given fully qualified domain name (e.g. "*.cilium.io").`))

hubble/cmd/observe/flows_filter.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,18 @@ func (of *flowFilter) set(f *filterTracker, name, val string, track bool) error
514514
f.TraceId = append(f.GetTraceId(), val)
515515
})
516516

517+
case "ip-trace-id":
518+
if val == "0" {
519+
return fmt.Errorf("invalid --ip-trace-id value; must be greater than 0")
520+
}
521+
traceID, err := strconv.ParseUint(val, 10, 64)
522+
if err != nil {
523+
return fmt.Errorf("invalid --ip-trace-id value: %w", err)
524+
}
525+
f.apply(func(f *flowpb.FlowFilter) {
526+
f.IpTraceId = append(f.GetIpTraceId(), traceID)
527+
})
528+
517529
case "verdict":
518530
if wipe {
519531
f.apply(func(f *flowpb.FlowFilter) {

hubble/cmd/observe/flows_filter_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,6 +1129,55 @@ func TestCluster(t *testing.T) {
11291129
}
11301130
}
11311131

1132+
func TestIpTraceId(t *testing.T) {
1133+
tt := []struct {
1134+
name string
1135+
flags []string
1136+
filters []*flowpb.FlowFilter
1137+
err string
1138+
}{
1139+
{
1140+
name: "error",
1141+
flags: []string{"--ip-trace-id", "0"},
1142+
filters: []*flowpb.FlowFilter{},
1143+
err: "invalid --ip-trace-id value; must be greater than 0",
1144+
},
1145+
{
1146+
name: "single",
1147+
flags: []string{"--ip-trace-id", "1"},
1148+
filters: []*flowpb.FlowFilter{
1149+
{IpTraceId: []uint64{1}},
1150+
},
1151+
},
1152+
{
1153+
name: "multiple",
1154+
flags: []string{"--ip-trace-id", "1", "--ip-trace-id", "2"},
1155+
filters: []*flowpb.FlowFilter{
1156+
{IpTraceId: []uint64{1, 2}},
1157+
},
1158+
},
1159+
}
1160+
for _, tc := range tt {
1161+
t.Run(tc.name, func(t *testing.T) {
1162+
f := newFlowFilter()
1163+
cmd := newFlowsCmdWithFilter(viper.New(), f)
1164+
err := cmd.Flags().Parse(tc.flags)
1165+
if tc.err != "" {
1166+
require.Errorf(t, err, tc.err)
1167+
return
1168+
} else {
1169+
require.NoError(t, err)
1170+
}
1171+
assert.Nil(t, f.blacklist)
1172+
got := f.whitelist.flowFilters()
1173+
diff := cmp.Diff(tc.filters, got, cmpopts.IgnoreUnexported(flowpb.FlowFilter{}))
1174+
if diff != "" {
1175+
t.Errorf("mismatch (-want +got):\n%s", diff)
1176+
}
1177+
})
1178+
}
1179+
}
1180+
11321181
func TestCELExpression(t *testing.T) {
11331182
tt := []struct {
11341183
name string

hubble/cmd/observe_help.txt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Filters Flags:
6262
--identity filter Show all flows related to an endpoint with the given security identity
6363
--interface filter Show all flows observed at the given interface name (e.g. eth0)
6464
--ip filter Show all flows originating or terminating at the given IP address. Each of the IPs can be specified as an exact match (e.g. '1.1.1.1') or as a CIDR range (e.g.'1.1.1.0/24').
65+
--ip-trace-id filter Show only flows which match this IP trace ID
6566
--ip-version filter Show only IPv4, IPv6 flows or non IP flows (e.g. ARP packets) (ie: "none", "v4", "v6")
6667
-4, --ipv4 filter[=v4] Show only IPv4 flows
6768
-6, --ipv6 filter[=v6] Show only IPv6 flows
@@ -156,15 +157,15 @@ Server Flags:
156157
--request-timeout duration Unary Request timeout. Only applies to non-streaming RPCs (ServerStatus, ListNodes, ListNamespaces). (default 12s)
157158
--server string Address of a Hubble server. Ignored when --input-file or --port-forward is provided. (default "localhost:4245")
158159
--timeout duration Hubble server dialing timeout (default 5s)
159-
--tls Specify that TLS must be used when establishing a connection to a Hubble server.
160+
--tls Specify that TLS must be used when establishing a connection to a Hubble server.
160161
By default, TLS is only enabled if the server address starts with 'tls://'.
161-
--tls-allow-insecure Allows the client to skip verifying the server's certificate chain and host name.
162-
This option is NOT recommended as, in this mode, TLS is susceptible to machine-in-the-middle attacks.
162+
--tls-allow-insecure Allows the client to skip verifying the server's certificate chain and host name.
163+
This option is NOT recommended as, in this mode, TLS is susceptible to machine-in-the-middle attacks.
163164
See also the 'tls-server-name' option which allows setting the server name.
164165
--tls-ca-cert-files strings Paths to custom Certificate Authority (CA) certificate files.The files must contain PEM encoded data.
165-
--tls-client-cert-file string Path to the public key file for the client certificate to connect to a Hubble server (implies TLS).
166+
--tls-client-cert-file string Path to the public key file for the client certificate to connect to a Hubble server (implies TLS).
166167
The file must contain PEM encoded data.
167-
--tls-client-key-file string Path to the private key file for the client certificate to connect a Hubble server (implies TLS).
168+
--tls-client-key-file string Path to the private key file for the client certificate to connect a Hubble server (implies TLS).
168169
The file must contain PEM encoded data.
169170
--tls-server-name string Specify a server name to verify the hostname on the returned certificate (eg: 'instance.hubble-relay.cilium.io').
170171

pkg/hubble/filters/filters.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,5 +149,6 @@ func DefaultFilters(log *slog.Logger) []OnBuildFilter {
149149
&TrafficDirectionFilter{},
150150
&CELExpressionFilter{log: log},
151151
&NetworkInterfaceFilter{},
152+
&IPTraceIDFilter{},
152153
}
153154
}

pkg/hubble/filters/ip_tracing.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright Authors of Hubble
3+
4+
package filters
5+
6+
import (
7+
"context"
8+
"slices"
9+
10+
flowpb "github.com/cilium/cilium/api/v1/flow"
11+
v1 "github.com/cilium/cilium/pkg/hubble/api/v1"
12+
)
13+
14+
func filterByIPTraceID(tids []uint64) FilterFunc {
15+
return func(ev *v1.Event) bool {
16+
trace := ev.GetFlow().GetIpTraceId().GetTraceId()
17+
return slices.Contains(tids, trace)
18+
}
19+
}
20+
21+
// TraceIDFilter implements filtering based on IP trace IDs.
22+
type IPTraceIDFilter struct{}
23+
24+
// OnBuildFilter builds a IP trace ID filter.
25+
func (t *IPTraceIDFilter) OnBuildFilter(_ context.Context, ff *flowpb.FlowFilter) ([]FilterFunc, error) {
26+
var fs []FilterFunc
27+
if ids := ff.GetIpTraceId(); len(ids) > 0 {
28+
fs = append(fs, filterByIPTraceID(ids))
29+
}
30+
return fs, nil
31+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright Authors of Hubble
3+
4+
package filters
5+
6+
import (
7+
"context"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
12+
flowpb "github.com/cilium/cilium/api/v1/flow"
13+
v1 "github.com/cilium/cilium/pkg/hubble/api/v1"
14+
)
15+
16+
func TestIPTraceIDFilter(t *testing.T) {
17+
ctx := context.Background()
18+
tests := []struct {
19+
name string
20+
f []*flowpb.FlowFilter
21+
ev *v1.Event
22+
want bool
23+
}{
24+
{
25+
name: "match_single_filter",
26+
f: []*flowpb.FlowFilter{
27+
{IpTraceId: []uint64{1}},
28+
},
29+
ev: &v1.Event{
30+
Event: &flowpb.Flow{
31+
IpTraceId: &flowpb.IPTraceID{
32+
TraceId: 1,
33+
},
34+
},
35+
},
36+
want: true,
37+
},
38+
{
39+
name: "match_multiple_filters",
40+
f: []*flowpb.FlowFilter{
41+
{IpTraceId: []uint64{1}},
42+
{IpTraceId: []uint64{2}},
43+
},
44+
ev: &v1.Event{
45+
Event: &flowpb.Flow{
46+
IpTraceId: &flowpb.IPTraceID{
47+
TraceId: 2,
48+
},
49+
},
50+
},
51+
want: true,
52+
},
53+
{
54+
name: "no_filter",
55+
ev: &v1.Event{
56+
Event: &flowpb.Flow{
57+
IpTraceId: &flowpb.IPTraceID{
58+
TraceId: 1,
59+
},
60+
},
61+
},
62+
want: true,
63+
},
64+
{
65+
name: "mismatch",
66+
f: []*flowpb.FlowFilter{
67+
{IpTraceId: []uint64{1}},
68+
},
69+
ev: &v1.Event{
70+
Event: &flowpb.Flow{
71+
IpTraceId: &flowpb.IPTraceID{
72+
TraceId: 2,
73+
},
74+
},
75+
},
76+
want: false,
77+
},
78+
{
79+
name: "no_trace_id",
80+
f: []*flowpb.FlowFilter{
81+
{IpTraceId: []uint64{1}},
82+
},
83+
ev: &v1.Event{
84+
Event: &flowpb.Flow{},
85+
},
86+
want: false,
87+
},
88+
}
89+
for _, tt := range tests {
90+
t.Run(tt.name, func(t *testing.T) {
91+
fl, err := BuildFilterList(ctx, tt.f, []OnBuildFilter{&IPTraceIDFilter{}})
92+
if err != nil {
93+
t.Fatalf("Faile to build filter: %v", err)
94+
}
95+
assert.Equal(t, tt.want, fl.MatchOne(tt.ev))
96+
})
97+
}
98+
}

0 commit comments

Comments
 (0)