From 8da11b2ba7a7d9bc1ddd07392327f2056df51b97 Mon Sep 17 00:00:00 2001 From: C Anthony Risinger Date: Sat, 21 Feb 2026 13:23:07 -0600 Subject: [PATCH] fix(set): round-trip KeyByteOrder and add DataByteOrder for maps Add DataByteOrder to Set for tracking map data byte order. Emit NFTNL_UDATA_SET_DATABYTEORDER in AddSet for maps and parse both KEYBYTEORDER and DATABYTEORDER from userdata in setsFromMsg. Also fix KEYBYTEORDER emission: check NativeEndian before the anonymous/constant/interval/BigEndian catch-all so that sets with an explicit NativeEndian key order emit the correct value. Without these fixes, host-endian map data (e.g. marks) appeared byte-swapped when read back on little-endian systems, and neither KeyByteOrder nor DataByteOrder was populated when deserializing sets. --- nftables_test.go | 138 +++++++++++++++++++++++++++++++++++++++++++++++ set.go | 45 +++++++++++++--- 2 files changed, 177 insertions(+), 6 deletions(-) diff --git a/nftables_test.go b/nftables_test.go index 4e915a0..4245f2a 100644 --- a/nftables_test.go +++ b/nftables_test.go @@ -3742,6 +3742,144 @@ func TestCreateAutoMergeSet(t *testing.T) { } } +func TestSetByteOrderRoundTrip(t *testing.T) { + tests := []struct { + name string + set nftables.Set + elems []nftables.SetElement + wantKeyOrder binaryutil.ByteOrder + wantDataOrder binaryutil.ByteOrder + }{ + { + name: "constant set defaults key byteorder to big-endian", + set: nftables.Set{ + Constant: true, + KeyType: nftables.TypeInetService, + }, + elems: []nftables.SetElement{ + {Key: binaryutil.BigEndian.PutUint16(80)}, + }, + wantKeyOrder: binaryutil.BigEndian, + }, + { + name: "explicit host-endian key byteorder", + set: nftables.Set{ + KeyType: nftables.TypeMark, + KeyByteOrder: binaryutil.NativeEndian, + }, + elems: []nftables.SetElement{ + {Key: binaryutil.NativeEndian.PutUint32(1)}, + }, + wantKeyOrder: binaryutil.NativeEndian, + }, + { + name: "interval set defaults key byteorder to big-endian", + set: nftables.Set{ + Interval: true, + KeyType: nftables.TypeInetService, + }, + wantKeyOrder: binaryutil.BigEndian, + }, + { + name: "interval set with explicit host-endian key byteorder", + set: nftables.Set{ + Interval: true, + KeyType: nftables.TypeMark, + KeyByteOrder: binaryutil.NativeEndian, + }, + elems: []nftables.SetElement{ + {Key: binaryutil.NativeEndian.PutUint32(7)}, + }, + wantKeyOrder: binaryutil.NativeEndian, + }, + { + name: "map with explicit host-endian key and data byteorder", + set: nftables.Set{ + KeyType: nftables.TypeMark, + DataType: nftables.TypeMark, + KeyByteOrder: binaryutil.NativeEndian, + DataByteOrder: binaryutil.NativeEndian, + IsMap: true, + }, + elems: []nftables.SetElement{ + { + Key: binaryutil.NativeEndian.PutUint32(1), + Val: binaryutil.NativeEndian.PutUint32(2), + }, + }, + wantKeyOrder: binaryutil.NativeEndian, + wantDataOrder: binaryutil.NativeEndian, + }, + { + name: "map with explicit data byteorder only", + set: nftables.Set{ + KeyType: nftables.TypeInetService, + DataType: nftables.TypeMark, + DataByteOrder: binaryutil.NativeEndian, + IsMap: true, + }, + elems: []nftables.SetElement{ + { + Key: binaryutil.BigEndian.PutUint16(22), + Val: binaryutil.NativeEndian.PutUint32(1), + }, + }, + wantDataOrder: binaryutil.NativeEndian, + }, + } + + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conn, newNS := nftest.OpenSystemConn(t, *enableSysTests) + defer nftest.CleanupSystemConn(t, newNS) + defer conn.FlushRuleset() + + table := conn.AddTable(&nftables.Table{ + Name: fmt.Sprintf("byteorder-table-%d", i), + Family: nftables.TableFamilyIPv4, + }) + + set := tt.set + set.Table = table + set.Name = fmt.Sprintf("byteorder-set-%d", i) + + if err := conn.AddSet(&set, tt.elems); err != nil { + t.Fatalf("failed to add set: %v", err) + } + if err := conn.Flush(); err != nil { + t.Fatalf("failed to flush: %v", err) + } + + gotSet, err := conn.GetSetByName(table, set.Name) + if err != nil { + t.Fatalf("failed to find set %q: %v", set.Name, err) + } + if gotSet.KeyByteOrder != tt.wantKeyOrder { + t.Fatalf("set.KeyByteOrder = %v, want %v", gotSet.KeyByteOrder, tt.wantKeyOrder) + } + if gotSet.DataByteOrder != tt.wantDataOrder { + t.Fatalf("set.DataByteOrder = %v, want %v", gotSet.DataByteOrder, tt.wantDataOrder) + } + + gotElems, err := conn.GetSetElements(gotSet) + if err != nil { + t.Fatalf("failed to get set elements: %v", err) + } + if got, want := len(gotElems), len(tt.elems); got != want { + t.Fatalf("got %d elements, want %d", got, want) + } + for i := range tt.elems { + if !bytes.Equal(gotElems[i].Key, tt.elems[i].Key) { + t.Fatalf("element[%d].Key = %x, want %x", i, gotElems[i].Key, tt.elems[i].Key) + } + if !bytes.Equal(gotElems[i].Val, tt.elems[i].Val) { + t.Fatalf("element[%d].Val = %x, want %x", i, gotElems[i].Val, tt.elems[i].Val) + } + } + }) + } +} + func TestIP6SetAddElements(t *testing.T) { // Create a new network namespace to test these operations, // and tear down the namespace at test completion. diff --git a/set.go b/set.go index 38997e1..9dadb0f 100644 --- a/set.go +++ b/set.go @@ -267,8 +267,9 @@ type Set struct { DataType SetDatatype // Either host (binaryutil.NativeEndian) or big (binaryutil.BigEndian) endian as per // https://git.netfilter.org/nftables/tree/include/datatype.h?id=d486c9e626405e829221b82d7355558005b26d8a#n109 - KeyByteOrder binaryutil.ByteOrder - Comment string + KeyByteOrder binaryutil.ByteOrder + DataByteOrder binaryutil.ByteOrder + Comment string // Indicates that the set has "size" specifier Size uint32 } @@ -716,12 +717,27 @@ func (cc *Conn) AddSet(s *Set, vals []SetElement) error { // https://git.netfilter.org/libnftnl/tree/include/udata.h#n17 var userData []byte - if s.Anonymous || s.Constant || s.Interval || s.KeyByteOrder == binaryutil.BigEndian { - // Semantically useless - kept for binary compatability with nft - userData = userdata.AppendUint32(userData, userdata.NFTNL_UDATA_SET_KEYBYTEORDER, 2) - } else if s.KeyByteOrder == binaryutil.NativeEndian { + // Emit KEYBYTEORDER metadata matching nft C tool behavior (mnl.c:mnl_nft_set_add). + // Anonymous, constant, and interval sets always need byte order metadata. + // When KeyByteOrder is explicitly set, use it; otherwise default to big-endian + // for backward compatibility with prior library behavior. + if s.KeyByteOrder == binaryutil.NativeEndian { // Per https://git.netfilter.org/nftables/tree/src/mnl.c?id=187c6d01d35722618c2711bbc49262c286472c8f#n1165 userData = userdata.AppendUint32(userData, userdata.NFTNL_UDATA_SET_KEYBYTEORDER, 1) + } else if s.Anonymous || s.Constant || s.Interval || s.KeyByteOrder == binaryutil.BigEndian { + userData = userdata.AppendUint32(userData, userdata.NFTNL_UDATA_SET_KEYBYTEORDER, 2) + } + + // Emit DATABYTEORDER for maps, matching nft C tool behavior (mnl.c:mnl_nft_set_add). + // Without this, nft list ruleset cannot determine the data byte order and displays + // host-endian values (like marks) as byte-swapped on LE systems. + if s.IsMap { + switch s.DataByteOrder { + case binaryutil.NativeEndian: + userData = userdata.AppendUint32(userData, userdata.NFTNL_UDATA_SET_DATABYTEORDER, 1) + case binaryutil.BigEndian: + userData = userdata.AppendUint32(userData, userdata.NFTNL_UDATA_SET_DATABYTEORDER, 2) + } } if s.Interval && s.AutoMerge { @@ -862,6 +878,12 @@ func setsFromMsg(msg netlink.Message) (*Set, error) { set.DataType.Bytes = binary.BigEndian.Uint32(ad.Bytes()) case unix.NFTA_SET_USERDATA: data := ad.Bytes() + if val, ok := userdata.GetUint32(data, userdata.NFTNL_UDATA_SET_KEYBYTEORDER); ok { + set.KeyByteOrder = parseSetByteOrder(val) + } + if val, ok := userdata.GetUint32(data, userdata.NFTNL_UDATA_SET_DATABYTEORDER); ok { + set.DataByteOrder = parseSetByteOrder(val) + } if val, ok := userdata.GetString(data, userdata.NFTNL_UDATA_SET_COMMENT); ok { set.Comment = val } @@ -891,6 +913,17 @@ func setsFromMsg(msg netlink.Message) (*Set, error) { return &set, nil } +func parseSetByteOrder(v uint32) binaryutil.ByteOrder { + switch v { + case 1: + return binaryutil.NativeEndian + case 2: + return binaryutil.BigEndian + default: + return nil + } +} + func parseSetDatatype(magic uint32) (SetDatatype, error) { types := make([]SetDatatype, 0, 32/SetConcatTypeBits) for magic != 0 {