From 036dfc3ba74144cf2c33b1ef5a02f2155b947c44 Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Wed, 22 Oct 2025 21:37:47 +0000 Subject: [PATCH 1/2] add ConntrackDelete command Add a new ConntrackDelete() function that operates directly on flows, same as the ConntrackCreate() and ConntrackUpdate() functions. We already have ConntrackDeleteFilters() that is very useful to batch operations and to express the intent based on filter matches, but having the function that operate on flows allow to create much more complex filtering without having to use an additional abstraction with filters. Signed-off-by: Antonio Ojea --- conntrack_linux.go | 23 +++++ conntrack_test.go | 208 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 231 insertions(+) diff --git a/conntrack_linux.go b/conntrack_linux.go index ff20869b..066c4144 100644 --- a/conntrack_linux.go +++ b/conntrack_linux.go @@ -71,6 +71,12 @@ func ConntrackUpdate(table ConntrackTableType, family InetFamily, flow *Conntrac return pkgHandle.ConntrackUpdate(table, family, flow) } +// ConntrackDelete deletes an existing conntrack flow in the desired table +// conntrack -D [table] Delete conntrack flow +func ConntrackDelete(table ConntrackTableType, family InetFamily, flow *ConntrackFlow) error { + return pkgHandle.ConntrackDelete(table, family, flow) +} + // ConntrackDeleteFilter deletes entries on the specified table on the base of the filter // conntrack -D [table] parameters Delete conntrack or expectation // @@ -148,6 +154,23 @@ func (h *Handle) ConntrackUpdate(table ConntrackTableType, family InetFamily, fl return err } +// ConntrackDelete deletes an existing conntrack flow in the desired table using the handle +// conntrack -D [table] Delete a conntrack +func (h *Handle) ConntrackDelete(table ConntrackTableType, family InetFamily, flow *ConntrackFlow) error { + req := h.newConntrackRequest(table, family, nl.IPCTNL_MSG_CT_DELETE, unix.NLM_F_ACK) + attr, err := flow.toNlData() + if err != nil { + return err + } + + for _, a := range attr { + req.AddData(a) + } + + _, err = req.Execute(unix.NETLINK_NETFILTER, 0) + return err +} + // ConntrackDeleteFilter deletes entries on the specified table on the base of the filter using the netlink handle passed // conntrack -D [table] parameters Delete conntrack or expectation // diff --git a/conntrack_test.go b/conntrack_test.go index 48e5c4a1..1bb0816c 100644 --- a/conntrack_test.go +++ b/conntrack_test.go @@ -1511,6 +1511,214 @@ func TestConntrackCreateV6(t *testing.T) { checkProtoInfosEqual(t, flow.ProtoInfo, match.ProtoInfo) } +// TestConntrackDeleteV4 creates an IPv4 conntrack entry, verifies it exists, +// deletes it via Handle.ConntrackDelete, and verifies it was removed. +func TestConntrackDeleteV4(t *testing.T) { + // Print timestamps in UTC + os.Setenv("TZ", "") + + requiredModules := []string{"nf_conntrack", "nf_conntrack_netlink"} + k, m, err := KernelVersion() + if err != nil { + t.Fatal(err) + } + // Conntrack l3proto was unified since 4.19 + // https://github.com/torvalds/linux/commit/a0ae2562c6c4b2721d9fddba63b7286c13517d9f + if k < 4 || k == 4 && m < 19 { + requiredModules = append(requiredModules, "nf_conntrack_ipv4") + } + // Implicitly skips test if not root: + nsStr, teardown := setUpNamedNetlinkTestWithKModule(t, requiredModules...) + t.Cleanup(teardown) + + ns, err := netns.GetFromName(nsStr) + if err != nil { + t.Fatalf("couldn't get handle to generated namespace: %s", err) + } + + h, err := NewHandleAt(ns, nl.FAMILY_V4) + if err != nil { + t.Fatalf("failed to create netlink handle: %s", err) + } + + flow := ConntrackFlow{ + FamilyType: FAMILY_V4, + Forward: IPTuple{ + SrcIP: net.IP{234, 234, 234, 234}, + DstIP: net.IP{123, 123, 123, 123}, + SrcPort: 48385, + DstPort: 53, + Protocol: unix.IPPROTO_TCP, + }, + Reverse: IPTuple{ + SrcIP: net.IP{123, 123, 123, 123}, + DstIP: net.IP{234, 234, 234, 234}, + SrcPort: 53, + DstPort: 48385, + Protocol: unix.IPPROTO_TCP, + }, + TimeOut: 100, + Mark: 12, + ProtoInfo: &ProtoInfoTCP{ + State: nl.TCP_CONNTRACK_ESTABLISHED, + }, + } + + // Create the entry using the handle + if err := h.ConntrackCreate(ConntrackTable, nl.FAMILY_V4, &flow); err != nil { + t.Fatalf("failed to insert conntrack: %s", err) + } + + // Verify it exists + flows, err := h.ConntrackTableList(ConntrackTable, nl.FAMILY_V4) + if err != nil { + t.Fatalf("failed to list conntracks following successful insert: %s", err) + } + filter := ConntrackFilter{ + ipNetFilter: map[ConntrackFilterType]*net.IPNet{ + ConntrackOrigSrcIP: NewIPNet(flow.Forward.SrcIP), + ConntrackOrigDstIP: NewIPNet(flow.Forward.DstIP), + ConntrackReplySrcIP: NewIPNet(flow.Reverse.SrcIP), + ConntrackReplyDstIP: NewIPNet(flow.Reverse.DstIP), + }, + portFilter: map[ConntrackFilterType]uint16{ + ConntrackOrigSrcPort: flow.Forward.SrcPort, + ConntrackOrigDstPort: flow.Forward.DstPort, + }, + protoFilter: unix.IPPROTO_TCP, + } + var match *ConntrackFlow + for _, f := range flows { + if filter.MatchConntrackFlow(f) { + match = f + break + } + } + if match == nil { + t.Fatalf("didn't find any matching conntrack entries for original flow: %+v\n Filter used: %+v", flow, filter) + } + + // Delete using the handler + if err := h.ConntrackDelete(ConntrackTable, InetFamily(nl.FAMILY_V4), &flow); err != nil { + t.Fatalf("failed to delete conntrack via handler: %s", err) + } + + // Verify it's gone + flows, err = h.ConntrackTableList(ConntrackTable, nl.FAMILY_V4) + if err != nil { + t.Fatalf("failed to list conntracks following delete: %s", err) + } + for _, f := range flows { + if filter.MatchConntrackFlow(f) { + t.Fatalf("found flow after delete: %+v", f) + } + } +} + +// TestConntrackDeleteV6 creates an IPv6 conntrack entry, verifies it exists, +// deletes it via Handle.ConntrackDelete, and verifies it was removed. +func TestConntrackDeleteV6(t *testing.T) { + // Print timestamps in UTC + os.Setenv("TZ", "") + + requiredModules := []string{"nf_conntrack", "nf_conntrack_netlink"} + k, m, err := KernelVersion() + if err != nil { + t.Fatal(err) + } + // Conntrack l3proto was unified since 4.19 + // https://github.com/torvalds/linux/commit/a0ae2562c6c4b2721d9fddba63b7286c13517d9f + if k < 4 || k == 4 && m < 19 { + requiredModules = append(requiredModules, "nf_conntrack_ipv4") + } + // Implicitly skips test if not root: + nsStr, teardown := setUpNamedNetlinkTestWithKModule(t, requiredModules...) + t.Cleanup(teardown) + + ns, err := netns.GetFromName(nsStr) + if err != nil { + t.Fatalf("couldn't get handle to generated namespace: %s", err) + } + + h, err := NewHandleAt(ns, nl.FAMILY_V6) + if err != nil { + t.Fatalf("failed to create netlink handle: %s", err) + } + + flow := ConntrackFlow{ + FamilyType: FAMILY_V6, + Forward: IPTuple{ + SrcIP: net.ParseIP("2001:db8::68"), + DstIP: net.ParseIP("2001:db9::32"), + SrcPort: 48385, + DstPort: 53, + Protocol: unix.IPPROTO_TCP, + }, + Reverse: IPTuple{ + SrcIP: net.ParseIP("2001:db9::32"), + DstIP: net.ParseIP("2001:db8::68"), + SrcPort: 53, + DstPort: 48385, + Protocol: unix.IPPROTO_TCP, + }, + TimeOut: 100, + Mark: 12, + ProtoInfo: &ProtoInfoTCP{ + State: nl.TCP_CONNTRACK_ESTABLISHED, + }, + } + + // Create the entry using the handle + if err := h.ConntrackCreate(ConntrackTable, nl.FAMILY_V6, &flow); err != nil { + t.Fatalf("failed to insert conntrack: %s", err) + } + + // Verify it exists + flows, err := h.ConntrackTableList(ConntrackTable, nl.FAMILY_V6) + if err != nil { + t.Fatalf("failed to list conntracks following successful insert: %s", err) + } + filter := ConntrackFilter{ + ipNetFilter: map[ConntrackFilterType]*net.IPNet{ + ConntrackOrigSrcIP: NewIPNet(flow.Forward.SrcIP), + ConntrackOrigDstIP: NewIPNet(flow.Forward.DstIP), + ConntrackReplySrcIP: NewIPNet(flow.Reverse.SrcIP), + ConntrackReplyDstIP: NewIPNet(flow.Reverse.DstIP), + }, + portFilter: map[ConntrackFilterType]uint16{ + ConntrackOrigSrcPort: flow.Forward.SrcPort, + ConntrackOrigDstPort: flow.Forward.DstPort, + }, + protoFilter: unix.IPPROTO_TCP, + } + var match *ConntrackFlow + for _, f := range flows { + if filter.MatchConntrackFlow(f) { + match = f + break + } + } + if match == nil { + t.Fatalf("didn't find any matching conntrack entries for original flow: %+v\n Filter used: %+v", flow, filter) + } + + // Delete using the handler + if err := h.ConntrackDelete(ConntrackTable, InetFamily(nl.FAMILY_V6), &flow); err != nil { + t.Fatalf("failed to delete conntrack via handler: %s", err) + } + + // Verify it's gone + flows, err = h.ConntrackTableList(ConntrackTable, nl.FAMILY_V6) + if err != nil { + t.Fatalf("failed to list conntracks following delete: %s", err) + } + for _, f := range flows { + if filter.MatchConntrackFlow(f) { + t.Fatalf("found flow after delete: %+v", f) + } + } +} + // TestConntrackLabels test the conntrack table labels // Creates some flows and then checks the labels associated func TestConntrackLabels(t *testing.T) { From 0c0ea58fc7def6ddad0092a3999fb3ea41cb09cf Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Sun, 23 Nov 2025 11:31:06 +0000 Subject: [PATCH 2/2] add conntrack zone serialization this allows to use the zone for conntrack delete Signed-off-by: Antonio Ojea --- conntrack_linux.go | 7 +++++++ conntrack_test.go | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/conntrack_linux.go b/conntrack_linux.go index 066c4144..dbeb112e 100644 --- a/conntrack_linux.go +++ b/conntrack_linux.go @@ -391,6 +391,8 @@ func (s *ConntrackFlow) toNlData() ([]*nl.RtAttr, error) { // // // + // + // // // CTA_TUPLE_ORIG @@ -439,6 +441,11 @@ func (s *ConntrackFlow) toNlData() ([]*nl.RtAttr, error) { } } + if s.Zone != 0 { + ctZone := nl.NewRtAttr(nl.CTA_ZONE, nl.BEUint16Attr(s.Zone)) + payload = append(payload, ctZone) + } + return payload, nil } diff --git a/conntrack_test.go b/conntrack_test.go index 1bb0816c..101066c0 100644 --- a/conntrack_test.go +++ b/conntrack_test.go @@ -1541,6 +1541,8 @@ func TestConntrackDeleteV4(t *testing.T) { t.Fatalf("failed to create netlink handle: %s", err) } + ctZone := uint16(123) + flow := ConntrackFlow{ FamilyType: FAMILY_V4, Forward: IPTuple{ @@ -1562,6 +1564,7 @@ func TestConntrackDeleteV4(t *testing.T) { ProtoInfo: &ProtoInfoTCP{ State: nl.TCP_CONNTRACK_ESTABLISHED, }, + Zone: ctZone, } // Create the entry using the handle @@ -1586,6 +1589,7 @@ func TestConntrackDeleteV4(t *testing.T) { ConntrackOrigDstPort: flow.Forward.DstPort, }, protoFilter: unix.IPPROTO_TCP, + zoneFilter: &ctZone, } var match *ConntrackFlow for _, f := range flows { @@ -1645,6 +1649,8 @@ func TestConntrackDeleteV6(t *testing.T) { t.Fatalf("failed to create netlink handle: %s", err) } + ctZone := uint16(123) + flow := ConntrackFlow{ FamilyType: FAMILY_V6, Forward: IPTuple{ @@ -1666,6 +1672,7 @@ func TestConntrackDeleteV6(t *testing.T) { ProtoInfo: &ProtoInfoTCP{ State: nl.TCP_CONNTRACK_ESTABLISHED, }, + Zone: ctZone, } // Create the entry using the handle @@ -1690,6 +1697,7 @@ func TestConntrackDeleteV6(t *testing.T) { ConntrackOrigDstPort: flow.Forward.DstPort, }, protoFilter: unix.IPPROTO_TCP, + zoneFilter: &ctZone, } var match *ConntrackFlow for _, f := range flows {