Skip to content

Commit 628845b

Browse files
committed
conntrack: update conntrack labels
The logic for updating conntrack labels was missing. The conntrack labels is an slice of bytes, if is not nil we send its current value. The all zeros slice has a special meaning , that wipes out the existing labels. There is also some unexpected behavior, the conntrack table does not reserve space for the labels if there is no label set in any rule, causing the netlink calls to fail with ENOSPC Signed-off-by: Antonio Ojea <[email protected]>
1 parent 19840db commit 628845b

File tree

2 files changed

+162
-0
lines changed

2 files changed

+162
-0
lines changed

conntrack_linux.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,8 @@ func (s *ConntrackFlow) toNlData() ([]*nl.RtAttr, error) {
366366
// <BEuint64>
367367
// <len, CTA_TIMEOUT>
368368
// <BEuint64>
369+
// <len, CTA_LABELS>
370+
// <binary data>
369371
// <len, NLA_F_NESTED|CTA_PROTOINFO>
370372

371373
// CTA_TUPLE_ORIG
@@ -392,6 +394,14 @@ func (s *ConntrackFlow) toNlData() ([]*nl.RtAttr, error) {
392394
ctTimeout := nl.NewRtAttr(nl.CTA_TIMEOUT, nl.BEUint32Attr(s.TimeOut))
393395

394396
payload = append(payload, ctTupleOrig, ctTupleReply, ctMark, ctTimeout)
397+
// Labels: nil => do not send; 16 zero bytes => clear all labels.
398+
if s.Labels != nil {
399+
if len(s.Labels) != 16 {
400+
return nil, fmt.Errorf("conntrack CTA_LABELS must be 16 bytes, got %d", len(s.Labels))
401+
}
402+
ctLabels := nl.NewRtAttr(nl.CTA_LABELS, s.Labels)
403+
payload = append(payload, ctLabels)
404+
}
395405

396406
if s.ProtoInfo != nil {
397407
switch p := s.ProtoInfo.(type) {

conntrack_test.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package netlink
55

66
import (
7+
"bytes"
78
"encoding/binary"
89
"fmt"
910
"net"
@@ -72,10 +73,16 @@ func ensureCtHooksInThisNS(t *testing.T) func() {
7273
var addedInput, addedOutput bool
7374
if ipt(false, "-C", "INPUT", "-m", "conntrack", "--ctstate", "NEW,ESTABLISHED", "-j", "ACCEPT") != nil {
7475
ipt(true, "-I", "INPUT", "-m", "conntrack", "--ctstate", "NEW,ESTABLISHED", "-j", "ACCEPT")
76+
// Add a rule to set conntrack label to allocate the label space
77+
// https://lore.kernel.org/netfilter-devel/[email protected]/
78+
ipt(true, "-I", "INPUT", "-m", "connlabel", "--set", "--label", "1")
7579
addedInput = true
7680
}
7781
if ipt(false, "-C", "OUTPUT", "-m", "conntrack", "--ctstate", "ESTABLISHED", "-j", "ACCEPT") != nil {
7882
ipt(true, "-I", "OUTPUT", "-m", "conntrack", "--ctstate", "ESTABLISHED", "-j", "ACCEPT")
83+
// Add a rule to set conntrack label to allocate the label space
84+
// https://lore.kernel.org/netfilter-devel/[email protected]/
85+
ipt(true, "-I", "OUTPUT", "-m", "connlabel", "--set", "--label", "1")
7986
addedOutput = true
8087
}
8188
return func() {
@@ -98,6 +105,12 @@ func ensureCtHooksInThisNS(t *testing.T) func() {
98105
_ = exec.Command("nft", "add", "chain", "inet", "ct_test", "output",
99106
"{", "type", "filter", "hook", "output", "priority", "0", ";",
100107
"ct", "state", "established", "accept", "}").Run()
108+
// Add a rule to set conntrack label to allocate the label space
109+
// https://lore.kernel.org/netfilter-devel/[email protected]/
110+
_ = exec.Command("nft", "add", "rule", "inet", "ct_test", "output",
111+
"ct", "label", "set", "1").Run()
112+
_ = exec.Command("nft", "add", "rule", "inet", "ct_test", "input",
113+
"ct", "label", "set", "1").Run()
101114
return func() {
102115
_ = exec.Command("nft", "delete", "table", "inet", "ct_test").Run()
103116
}
@@ -1498,6 +1511,138 @@ func TestConntrackCreateV6(t *testing.T) {
14981511
checkProtoInfosEqual(t, flow.ProtoInfo, match.ProtoInfo)
14991512
}
15001513

1514+
// TestConntrackLabels test the conntrack table labels
1515+
// Creates some flows and then checks the labels associated
1516+
func TestConntrackLabels(t *testing.T) {
1517+
skipUnlessRoot(t)
1518+
t.Cleanup(setUpNetlinkTestWithKModule(t, "nf_conntrack"))
1519+
t.Cleanup(setUpNetlinkTestWithKModule(t, "nf_conntrack_netlink"))
1520+
k, m, err := KernelVersion()
1521+
if err != nil {
1522+
t.Fatal(err)
1523+
}
1524+
// conntrack l3proto was unified since 4.19
1525+
// https://github.com/torvalds/linux/commit/a0ae2562c6c4b2721d9fddba63b7286c13517d9f
1526+
if k < 4 || k == 4 && m < 19 {
1527+
t.Cleanup(setUpNetlinkTestWithKModule(t, "nf_conntrack_ipv4"))
1528+
}
1529+
// Creates a new namespace and bring up the loopback interface
1530+
origns, ns, h := nsCreateAndEnter(t)
1531+
defer netns.Set(*origns)
1532+
defer origns.Close()
1533+
defer ns.Close()
1534+
defer runtime.UnlockOSThread()
1535+
1536+
flow := ConntrackFlow{
1537+
FamilyType: FAMILY_V4,
1538+
Forward: IPTuple{
1539+
SrcIP: net.IP{234, 234, 234, 234},
1540+
DstIP: net.IP{123, 123, 123, 123},
1541+
SrcPort: 48385,
1542+
DstPort: 53,
1543+
Protocol: unix.IPPROTO_TCP,
1544+
},
1545+
Reverse: IPTuple{
1546+
SrcIP: net.IP{123, 123, 123, 123},
1547+
DstIP: net.IP{234, 234, 234, 234},
1548+
SrcPort: 53,
1549+
DstPort: 48385,
1550+
Protocol: unix.IPPROTO_TCP,
1551+
},
1552+
// No point checking equivalence of timeout, but value must
1553+
// be reasonable to allow for a potentially slow subsequent read.
1554+
TimeOut: 100,
1555+
Mark: 12,
1556+
Labels: []byte{0, 0, 0, 0, 3, 4, 61, 141, 207, 170, 2, 0, 0, 0, 0, 0},
1557+
ProtoInfo: &ProtoInfoTCP{
1558+
State: nl.TCP_CONNTRACK_SYN_SENT2,
1559+
},
1560+
}
1561+
1562+
err = h.ConntrackUpdate(ConntrackTable, nl.FAMILY_V4, &flow)
1563+
if err == nil {
1564+
t.Fatalf("expected an error to occur when trying to update a non-existant conntrack: %+v", flow)
1565+
}
1566+
1567+
err = h.ConntrackCreate(ConntrackTable, nl.FAMILY_V4, &flow)
1568+
if err != nil {
1569+
t.Fatalf("failed to insert conntrack: %s", err)
1570+
}
1571+
1572+
flows, err := h.ConntrackTableList(ConntrackTable, nl.FAMILY_V4)
1573+
if err != nil {
1574+
t.Fatalf("failed to list conntracks following successful insert: %s", err)
1575+
}
1576+
1577+
filter := ConntrackFilter{
1578+
ipNetFilter: map[ConntrackFilterType]*net.IPNet{
1579+
ConntrackOrigSrcIP: NewIPNet(flow.Forward.SrcIP),
1580+
ConntrackOrigDstIP: NewIPNet(flow.Forward.DstIP),
1581+
ConntrackReplySrcIP: NewIPNet(flow.Reverse.SrcIP),
1582+
ConntrackReplyDstIP: NewIPNet(flow.Reverse.DstIP),
1583+
},
1584+
portFilter: map[ConntrackFilterType]uint16{
1585+
ConntrackOrigSrcPort: flow.Forward.SrcPort,
1586+
ConntrackOrigDstPort: flow.Forward.DstPort,
1587+
},
1588+
protoFilter: unix.IPPROTO_TCP,
1589+
}
1590+
1591+
var match *ConntrackFlow
1592+
for _, f := range flows {
1593+
if filter.MatchConntrackFlow(f) {
1594+
match = f
1595+
break
1596+
}
1597+
}
1598+
1599+
if match == nil {
1600+
t.Fatalf("Didn't find any matching conntrack entries for original flow: %+v\n Filter used: %+v", flow, filter)
1601+
} else {
1602+
t.Logf("Found entry in conntrack table matching original flow: %+v labels=%+v", match, match.Labels)
1603+
}
1604+
checkFlowsEqual(t, &flow, match)
1605+
checkProtoInfosEqual(t, flow.ProtoInfo, match.ProtoInfo)
1606+
1607+
// Change the conntrack and update the kernel entry.
1608+
flow.Mark = 10
1609+
flow.Labels = make([]byte, 16) // zero labels
1610+
flow.ProtoInfo = &ProtoInfoTCP{
1611+
State: nl.TCP_CONNTRACK_ESTABLISHED,
1612+
}
1613+
err = h.ConntrackUpdate(ConntrackTable, nl.FAMILY_V4, &flow)
1614+
if err != nil {
1615+
t.Fatalf("failed to update conntrack with new mark: %s", err)
1616+
}
1617+
1618+
// Look for updated conntrack.
1619+
flows, err = h.ConntrackTableList(ConntrackTable, nl.FAMILY_V4)
1620+
if err != nil {
1621+
t.Fatalf("failed to list conntracks following successful update: %s", err)
1622+
}
1623+
1624+
var updatedMatch *ConntrackFlow
1625+
for _, f := range flows {
1626+
if filter.MatchConntrackFlow(f) {
1627+
updatedMatch = f
1628+
break
1629+
}
1630+
}
1631+
if updatedMatch == nil {
1632+
t.Fatalf("Didn't find any matching conntrack entries for updated flow: %+v\n Filter used: %+v", flow, filter)
1633+
} else {
1634+
t.Logf("Found entry in conntrack table matching updated flow: %+v labels=%+v", updatedMatch, updatedMatch.Labels)
1635+
}
1636+
1637+
// To clear the labels we send an empty slice, but when reading back
1638+
// from the kernel we get a nil slice.
1639+
flow.Labels = nil
1640+
checkFlowsEqual(t, &flow, updatedMatch)
1641+
checkProtoInfosEqual(t, flow.ProtoInfo, updatedMatch.ProtoInfo)
1642+
// Switch back to the original namespace
1643+
netns.Set(*origns)
1644+
}
1645+
15011646
// TestConntrackFlowToNlData generates a serialized representation of a
15021647
// ConntrackFlow and runs the resulting bytes back through `parseRawData` to validate.
15031648
func TestConntrackFlowToNlData(t *testing.T) {
@@ -1518,6 +1663,7 @@ func TestConntrackFlowToNlData(t *testing.T) {
15181663
Protocol: unix.IPPROTO_TCP,
15191664
},
15201665
Mark: 5,
1666+
Labels: []byte{0, 0, 0, 0, 3, 4, 61, 141, 207, 170, 2, 0, 0, 0, 0, 0},
15211667
TimeOut: 10,
15221668
ProtoInfo: &ProtoInfoTCP{
15231669
State: nl.TCP_CONNTRACK_ESTABLISHED,
@@ -1540,6 +1686,7 @@ func TestConntrackFlowToNlData(t *testing.T) {
15401686
Protocol: unix.IPPROTO_TCP,
15411687
},
15421688
Mark: 5,
1689+
Labels: []byte{0, 0, 0, 0, 3, 4, 61, 141, 207, 170, 2, 0, 0, 0, 0, 0},
15431690
TimeOut: 10,
15441691
ProtoInfo: &ProtoInfoTCP{
15451692
State: nl.TCP_CONNTRACK_ESTABLISHED,
@@ -1596,6 +1743,11 @@ func checkFlowsEqual(t *testing.T, f1, f2 *ConntrackFlow) {
15961743
t.Logf("Reverse tuples mismatch. Tuple1 reverse flow: %+v, Tuple2 reverse flow: %+v.\n", f1.Reverse, f2.Reverse)
15971744
t.Fail()
15981745
}
1746+
1747+
if !bytes.Equal(f1.Labels, f2.Labels) {
1748+
t.Logf("Conntrack flow Labels differ. Tuple1: %+v, Tuple2: %+v.\n", f1.Labels, f2.Labels)
1749+
t.Fail()
1750+
}
15991751
}
16001752

16011753
func checkProtoInfosEqual(t *testing.T, p1, p2 ProtoInfo) {

0 commit comments

Comments
 (0)