From cf2a41b773722b58258426e2fc8b8dac017290e0 Mon Sep 17 00:00:00 2001 From: Antonin Bas Date: Tue, 30 Sep 2025 21:48:44 +0000 Subject: [PATCH 1/3] Add support for zone filtering for Dump and Flush Support for the CTA_ZONE attribute for dump and flush requests was added to the kernel in 6.8: https://github.com/torvalds/linux/commit/eff3c558bb7e61c41b53e4c8130e514a5a4df9ba We add an optional `Zone` field to the `Filter` struct in order to expose this support. In the case of Dump, for clients who are only interested in flows from a specific zone, this can be much more efficient than dumping all flows, unmarshalling them, and filtering based on the unmarshalled zone value, especially considering that the library has no support for partial unmarshalling. For older kernels, the attribute will be ignored. While we do not include a version check in the implementation of the Flush / Dump methods, we do make this requirement clear in their documentation. Fixes #23 Signed-off-by: Antonin Bas --- README.md | 2 +- conn.go | 4 ++- conn_integration_test.go | 42 +++++++++++++++++++++++++++++ conn_test.go | 34 +++++++++++++++++++++++ filter.go | 15 +++++++++-- filter_test.go | 42 +++++++++++++++++++++++++++++ flow_integration_test.go | 58 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 193 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2b2d111..58aa5f1 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ With this library, the user can: - Interact with conntrack connections and expectations through Flow and Expect types respectively - Create, get, update and delete Flows in an idiomatic way (and Expects, to an extent) - Listen for create/update/destroy events -- Flush (empty) and dump (display) the whole conntrack table, optionally filtering on specific connection marks +- Flush (empty) and dump (display) the whole conntrack table, optionally filtering on specific connection marks and on the zone There are many usage examples in the [godoc](https://godoc.org/github.com/ti-mo/conntrack). diff --git a/conn.go b/conn.go index c2e5b22..17c44c7 100644 --- a/conn.go +++ b/conn.go @@ -194,7 +194,8 @@ func (c *Conn) Dump(opts *DumpOptions) ([]Flow, error) { } // DumpFilter gets all Conntrack connections from the kernel in the form of a list -// of Flow objects, but only returns Flows matching the connmark specified in the Filter parameter. +// of Flow objects, but only returns Flows matching the connmark and/or zone specified in the Filter parameter. +// Zone filtering requires Linux kernel 6.8 or greater. func (c *Conn) DumpFilter(f Filter, opts *DumpOptions) ([]Flow, error) { msgType := ctGet if opts != nil && opts.ZeroCounters { @@ -272,6 +273,7 @@ func (c *Conn) Flush() error { // FlushFilter deletes all entries from the Conntrack table matching a given Filter. // Both IPv4 and IPv6 entries are considered for deletion. +// Zone filtering requires Linux kernel 6.8 or greater. func (c *Conn) FlushFilter(f Filter) error { req, err := netfilter.MarshalNetlink( diff --git a/conn_integration_test.go b/conn_integration_test.go index aedea1b..0f89416 100644 --- a/conn_integration_test.go +++ b/conn_integration_test.go @@ -8,6 +8,7 @@ import ( "log" "os" "strings" + "syscall" "testing" "github.com/stretchr/testify/require" @@ -114,3 +115,44 @@ func findKsym(sym string) bool { return false } + +// kernelVersion returns major and minor kernel version numbers parsed from the syscall.Uname's +// Release field, or (0, 0) if the version can't be obtained or parsed. +// The code was taken from src/internal/syscall/unix/kernel_version_linux.go. +func kernelVersion() (major, minor int) { + var uname syscall.Utsname + if err := syscall.Uname(&uname); err != nil { + return + } + + var ( + values [2]int + value, vi int + ) + for _, c := range uname.Release { + if '0' <= c && c <= '9' { + value = (value * 10) + int(c-'0') + } else { + // Note that we're assuming N.N.N here. + // If we see anything else, we are likely to mis-parse it. + values[vi] = value + vi++ + if vi >= len(values) { + break + } + value = 0 + } + } + + return values[0], values[1] +} + +// kernelVersionLessThan returns true if and only if the actual kernel version +// (major.minor) is less than the provided one. +func kernelVersionLessThan(major, minor int) bool { + actualMajor, actualMinor := kernelVersion() + if actualMajor != major { + return actualMajor < major + } + return actualMinor < minor +} diff --git a/conn_test.go b/conn_test.go index 661266b..21ecd64 100644 --- a/conn_test.go +++ b/conn_test.go @@ -94,6 +94,40 @@ func ExampleConn_dumpFilter() { log.Print(df) } +func ExampleConn_dumpFilterZone() { + // Open a Conntrack connection. + c, err := conntrack.Dial(nil) + if err != nil { + log.Fatal(err) + } + + // Create flows in different zones + f1 := conntrack.NewFlow( + 6, 0, netip.MustParseAddr("1.2.3.4"), netip.MustParseAddr("5.6.7.8"), + 1234, 80, 120, 0, + ) + f1.Zone = 10 + + f2 := conntrack.NewFlow( + 17, 0, netip.MustParseAddr("2a00:1450:400e:804::200e"), netip.MustParseAddr("2a00:1450:400e:804::200f"), + 1234, 80, 120, 0, + ) + f2.Zone = 20 + + _ = c.Create(f1) + _ = c.Create(f2) + + // Dump all records in the Conntrack table that match zone 20. + zone := uint16(20) + df, err := c.DumpFilter(conntrack.Filter{Zone: &zone}, nil) + if err != nil { + log.Fatal(err) + } + + // Print the result. Only f2 is displayed. + log.Print(df) +} + func ExampleConn_flush() { // Open a Conntrack connection. c, err := conntrack.Dial(nil) diff --git a/filter.go b/filter.go index e6e5027..fdecda1 100644 --- a/filter.go +++ b/filter.go @@ -8,14 +8,15 @@ import ( // based on a given connmark and mask. The mask is applied to the Mark field of // all flows in the conntrack table, the result is compared to the filter's Mark. // Each flow that matches will be returned by the kernel. +// Zone can be used to filter connections by conntrack zone. type Filter struct { Mark, Mask uint32 + Zone *uint16 } // marshal marshals a Filter into a list of netfilter.Attributes. func (f Filter) marshal() []netfilter.Attribute { - - return []netfilter.Attribute{ + attrs := []netfilter.Attribute{ { Type: uint16(ctaMark), Data: netfilter.Uint32Bytes(f.Mark), @@ -25,4 +26,14 @@ func (f Filter) marshal() []netfilter.Attribute { Data: netfilter.Uint32Bytes(f.Mask), }, } + + // Add CTA_ZONE attribute if Zone is specified + if f.Zone != nil { + attrs = append(attrs, netfilter.Attribute{ + Type: uint16(ctaZone), + Data: netfilter.Uint16Bytes(*f.Zone), + }) + } + + return attrs } diff --git a/filter_test.go b/filter_test.go index e170825..e511441 100644 --- a/filter_test.go +++ b/filter_test.go @@ -24,3 +24,45 @@ func TestFilterMarshal(t *testing.T) { assert.Equal(t, fm, f.marshal(), "unexpected Filter marshal") } + +func TestFilterMarshalZoneOnly(t *testing.T) { + zone := uint16(123) + f := Filter{Zone: &zone} + fm := []netfilter.Attribute{ + { + Type: uint16(ctaMark), + Data: []byte{0, 0, 0, 0}, + }, + { + Type: uint16(ctaMarkMask), + Data: []byte{0, 0, 0, 0}, + }, + { + Type: uint16(ctaZone), + Data: []byte{0, 123}, + }, + } + + assert.Equal(t, fm, f.marshal(), "unexpected Filter marshal") +} + +func TestFilterMarshalMarkAndZone(t *testing.T) { + zone := uint16(42) + f := Filter{Mark: 0xf0000000, Mask: 0x0000000f, Zone: &zone} + fm := []netfilter.Attribute{ + { + Type: uint16(ctaMark), + Data: []byte{0xf0, 0, 0, 0}, + }, + { + Type: uint16(ctaMarkMask), + Data: []byte{0, 0, 0, 0x0f}, + }, + { + Type: uint16(ctaZone), + Data: []byte{0, 42}, + }, + } + + assert.Equal(t, fm, f.marshal(), "unexpected Filter marshal") +} diff --git a/flow_integration_test.go b/flow_integration_test.go index a348384..574cdfd 100644 --- a/flow_integration_test.go +++ b/flow_integration_test.go @@ -401,3 +401,61 @@ func BenchmarkCreateDeleteFlow(b *testing.B) { } } } + +// Creates flows in a specific zone, dumps them using zone filter, flushes them using zone filter, +// and verifies they are removed. Requires Linux kernel 6.8 or greater for zone filtering support. +func TestZoneFilter(t *testing.T) { + if kernelVersionLessThan(6, 8) { + t.Skip("Zone filtering requires Linux kernel 6.8 or greater") + } + + if !findKsym("ctnetlink_alloc_filter") { + t.Skip("DumpFilter not supported in this kernel") + } + + if !findKsym("ctnetlink_flush_iterate") { + t.Skip("FlushFilter not supported in this kernel") + } + + c, _, err := makeNSConn() + require.NoError(t, err) + + zone := uint16(100) + + // Create two flows in zone 100 + f1 := NewFlow( + 6, 0, + netip.MustParseAddr("1.2.3.4"), + netip.MustParseAddr("5.6.7.8"), + 1234, 80, 120, 0, + ) + f1.Zone = zone + + f2 := NewFlow( + 17, 0, + netip.MustParseAddr("2a00:1450:400e:804::200e"), + netip.MustParseAddr("2a00:1450:400e:804::200f"), + 1234, 80, 120, 0, + ) + f2.Zone = zone + + err = c.Create(f1) + require.NoError(t, err, "creating IPv4 flow in zone 100") + + err = c.Create(f2) + require.NoError(t, err, "creating IPv6 flow in zone 100") + + // Dump flows using zone filter - should return 2 flows + flows, err := c.DumpFilter(Filter{Zone: &zone}, nil) + require.NoError(t, err, "dumping flows with zone filter") + assert.Len(t, flows, 2, "expected 2 flows in zone 100") + + // Flush flows using zone filter + err = c.FlushFilter(Filter{Zone: &zone}) + require.NoError(t, err, "flushing flows with zone filter") + + // Dump flows using zone filter again - should return empty list + flows, err = c.DumpFilter(Filter{Zone: &zone}, nil) + require.NoError(t, err, "dumping flows with zone filter after flush") + assert.Empty(t, flows, "expected no flows in zone 100 after flush") +} From beb2433f0b411323515f721f936c1cb054151e94 Mon Sep 17 00:00:00 2001 From: Antonin Bas Date: Mon, 13 Oct 2025 13:13:21 -0700 Subject: [PATCH 2/3] Address review comment: remove kernel ver check Signed-off-by: Antonin Bas --- conn_integration_test.go | 42 ---------------------------------------- flow_integration_test.go | 4 ---- 2 files changed, 46 deletions(-) diff --git a/conn_integration_test.go b/conn_integration_test.go index 0f89416..aedea1b 100644 --- a/conn_integration_test.go +++ b/conn_integration_test.go @@ -8,7 +8,6 @@ import ( "log" "os" "strings" - "syscall" "testing" "github.com/stretchr/testify/require" @@ -115,44 +114,3 @@ func findKsym(sym string) bool { return false } - -// kernelVersion returns major and minor kernel version numbers parsed from the syscall.Uname's -// Release field, or (0, 0) if the version can't be obtained or parsed. -// The code was taken from src/internal/syscall/unix/kernel_version_linux.go. -func kernelVersion() (major, minor int) { - var uname syscall.Utsname - if err := syscall.Uname(&uname); err != nil { - return - } - - var ( - values [2]int - value, vi int - ) - for _, c := range uname.Release { - if '0' <= c && c <= '9' { - value = (value * 10) + int(c-'0') - } else { - // Note that we're assuming N.N.N here. - // If we see anything else, we are likely to mis-parse it. - values[vi] = value - vi++ - if vi >= len(values) { - break - } - value = 0 - } - } - - return values[0], values[1] -} - -// kernelVersionLessThan returns true if and only if the actual kernel version -// (major.minor) is less than the provided one. -func kernelVersionLessThan(major, minor int) bool { - actualMajor, actualMinor := kernelVersion() - if actualMajor != major { - return actualMajor < major - } - return actualMinor < minor -} diff --git a/flow_integration_test.go b/flow_integration_test.go index 574cdfd..3b7126d 100644 --- a/flow_integration_test.go +++ b/flow_integration_test.go @@ -405,10 +405,6 @@ func BenchmarkCreateDeleteFlow(b *testing.B) { // Creates flows in a specific zone, dumps them using zone filter, flushes them using zone filter, // and verifies they are removed. Requires Linux kernel 6.8 or greater for zone filtering support. func TestZoneFilter(t *testing.T) { - if kernelVersionLessThan(6, 8) { - t.Skip("Zone filtering requires Linux kernel 6.8 or greater") - } - if !findKsym("ctnetlink_alloc_filter") { t.Skip("DumpFilter not supported in this kernel") } From fd334258d2468e0e4c5eefe4ba3076bb51a65207 Mon Sep 17 00:00:00 2001 From: Antonin Bas Date: Mon, 13 Oct 2025 13:17:22 -0700 Subject: [PATCH 3/3] Address other review comments Signed-off-by: Antonin Bas --- filter.go | 4 +++- flow_integration_test.go | 10 +++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/filter.go b/filter.go index fdecda1..fcdc540 100644 --- a/filter.go +++ b/filter.go @@ -11,7 +11,9 @@ import ( // Zone can be used to filter connections by conntrack zone. type Filter struct { Mark, Mask uint32 - Zone *uint16 + // Requires at least Linux 6.8. + // If omitted, the default behavior is to consider ALL zones. + Zone *uint16 } // marshal marshals a Filter into a list of netfilter.Attributes. diff --git a/flow_integration_test.go b/flow_integration_test.go index 3b7126d..9be7efc 100644 --- a/flow_integration_test.go +++ b/flow_integration_test.go @@ -435,11 +435,8 @@ func TestZoneFilter(t *testing.T) { ) f2.Zone = zone - err = c.Create(f1) - require.NoError(t, err, "creating IPv4 flow in zone 100") - - err = c.Create(f2) - require.NoError(t, err, "creating IPv6 flow in zone 100") + require.NoError(t, c.Create(f1), "creating IPv4 flow in zone 100") + require.NoError(t, c.Create(f2), "creating IPv6 flow in zone 100") // Dump flows using zone filter - should return 2 flows flows, err := c.DumpFilter(Filter{Zone: &zone}, nil) @@ -447,8 +444,7 @@ func TestZoneFilter(t *testing.T) { assert.Len(t, flows, 2, "expected 2 flows in zone 100") // Flush flows using zone filter - err = c.FlushFilter(Filter{Zone: &zone}) - require.NoError(t, err, "flushing flows with zone filter") + require.NoError(t, c.FlushFilter(Filter{Zone: &zone}), "flushing flows with zone filter") // Dump flows using zone filter again - should return empty list flows, err = c.DumpFilter(Filter{Zone: &zone}, nil)