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_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..fcdc540 100644 --- a/filter.go +++ b/filter.go @@ -8,14 +8,17 @@ 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 + // 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. func (f Filter) marshal() []netfilter.Attribute { - - return []netfilter.Attribute{ + attrs := []netfilter.Attribute{ { Type: uint16(ctaMark), Data: netfilter.Uint32Bytes(f.Mark), @@ -25,4 +28,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..9be7efc 100644 --- a/flow_integration_test.go +++ b/flow_integration_test.go @@ -401,3 +401,53 @@ 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 !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 + + 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) + 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 + 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) + require.NoError(t, err, "dumping flows with zone filter after flush") + assert.Empty(t, flows, "expected no flows in zone 100 after flush") +}