Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,14 @@ import (
"math"

"github.com/google/nftables/binaryutil"
"github.com/google/nftables/userdata"
"github.com/mdlayher/netlink"
"golang.org/x/sys/unix"
)

// https://github.com/torvalds/linux/blob/cbd2257dc96e3e46217540fcb095a757ffa20d96/include/uapi/linux/netfilter/nf_tables.h#L253
const nftaChainUserdata = 0x0c

// ChainHook specifies at which step in packet processing the Chain should be
// executed. See also
// https://wiki.nftables.org/wiki-nftables/index.php/Configuring_chains#Base_chain_hooks
Expand Down Expand Up @@ -103,6 +107,11 @@ type Chain struct {
Type ChainType
Policy *ChainPolicy
Device string
// Comment is stored in NFTA_CHAIN_USERDATA in the format used by nft,
// so "nft list ruleset" shows it. It requires Linux 5.10 and is ignored
// by older kernels. The kernel does not update the comment of an
// existing chain, changing it requires deleting the chain.
Comment string
}

// AddChain adds the specified Chain. See also
Expand Down Expand Up @@ -140,6 +149,12 @@ func (cc *Conn) AddChain(c *Chain) *Chain {
{Type: unix.NFTA_CHAIN_TYPE, Data: []byte(c.Type + "\x00")},
})...)
}
if c.Comment != "" {
udata := userdata.AppendString(nil, userdata.NFTNL_UDATA_CHAIN_COMMENT, c.Comment)
data = append(data, cc.marshalAttr([]netlink.Attribute{
{Type: nftaChainUserdata, Data: udata},
})...)
}
cc.messages = append(cc.messages, netlinkMessage{
Header: netlink.Header{
Type: nftMsgNewChain.HeaderType(),
Expand Down Expand Up @@ -314,6 +329,10 @@ func chainFromMsg(msg netlink.Message) (*Chain, error) {
c.Hooknum, c.Priority, err = hookFromMsg(b)
return err
})
case nftaChainUserdata:
if comment, ok := userdata.GetString(ad.Bytes(), userdata.NFTNL_UDATA_CHAIN_COMMENT); ok {
c.Comment = comment
}
}
}

Expand Down
74 changes: 74 additions & 0 deletions integration/nft_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,39 @@ func TestNFTables(t *testing.T) {
}
},
},
{
name: "AddComments",
scriptPath: "testdata/add_comments.nft",
goCommands: func(t *testing.T, c *nftables.Conn) {
c.FlushRuleset()

table := c.AddTable(&nftables.Table{
Name: "test-table",
Family: nftables.TableFamilyINet,
Comment: "table comment",
})

c.AddChain(&nftables.Chain{
Name: "base-chain",
Table: table,
Hooknum: nftables.ChainHookInput,
Priority: nftables.ChainPriorityFilter,
Type: nftables.ChainTypeFilter,
Comment: "base chain comment",
})

c.AddChain(&nftables.Chain{
Name: "regular-chain",
Table: table,
Comment: "regular chain comment",
})

err := c.Flush()
if err != nil {
t.Fatalf("Error creating table: %v", err)
}
},
},
{
name: "AddFlowtables",
scriptPath: "testdata/add_flowtables.nft",
Expand Down Expand Up @@ -228,6 +261,47 @@ func TestNFTables(t *testing.T) {
}
}

// TestListCommentsFromNFT checks that the comments nft stores on tables and
// chains are read back by the Go library. nft adds more userdata than the
// comment, which the library has to skip.
func TestListCommentsFromNFT(t *testing.T) {
c, newNS := nftest.OpenSystemConn(t, *enableSysTests)
defer nftest.CleanupSystemConn(t, newNS)

if out, err := applyNFTRuleset("testdata/add_comments.nft"); err != nil {
t.Fatalf("Failed to apply nftables script: %v\noutput:%s", err, out)
}
defer func() {
if err := flushNFTRuleset(); err != nil {
t.Fatalf("Failed to flush nftables ruleset: %v", err)
}
}()

table, err := c.ListTableOfFamily("test-table", nftables.TableFamilyINet)
if err != nil {
t.Fatalf("ListTableOfFamily() failed: %v", err)
}
if got, want := table.Comment, "table comment"; got != want {
t.Errorf("table comment = %q, want %q", got, want)
}

chains, err := c.ListChainsOfTableFamily(nftables.TableFamilyINet)
if err != nil {
t.Fatalf("ListChainsOfTableFamily() failed: %v", err)
}
got := map[string]string{}
for _, chain := range chains {
got[chain.Name] = chain.Comment
}
want := map[string]string{
"base-chain": "base chain comment",
"regular-chain": "regular chain comment",
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("chain comments mismatch (-want +got):\n%s", diff)
}
}

func applyNFTRuleset(scriptPath string) (string, error) {
cmd := exec.Command("nft", "--debug=all", "-f", scriptPath)
out, err := cmd.CombinedOutput()
Expand Down
10 changes: 10 additions & 0 deletions integration/testdata/add_comments.nft
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
table inet test-table {
comment "table comment"
chain base-chain {
comment "base chain comment"
type filter hook input priority filter; policy accept;
}
chain regular-chain {
comment "regular chain comment"
}
}
177 changes: 177 additions & 0 deletions nftables_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5852,6 +5852,183 @@ func TestSetComment(t *testing.T) {
}
}

func TestTableComment(t *testing.T) {
want := [][]byte{
// batch begin
[]byte("\x00\x00\x00\x0a"),
// nft add table inet filter { comment "test comment" \; }
[]byte("\x01\x00\x00\x00\x0b\x00\x01\x00\x66\x69\x6c\x74\x65\x72\x00\x00\x08\x00\x02\x00\x00\x00\x00\x00\x13\x00\x06\x00\x00\x0d\x74\x65\x73\x74\x20\x63\x6f\x6d\x6d\x65\x6e\x74\x00\x00"),
// batch end
[]byte("\x00\x00\x00\x0a"),
}

c, err := nftables.New(expectMessages(t, want))
if err != nil {
t.Fatal(err)
}

c.AddTable(&nftables.Table{
Family: nftables.TableFamilyINet,
Name: "filter",
Comment: "test comment",
})

if err := c.Flush(); err != nil {
t.Fatal(err)
}
}

func TestChainComment(t *testing.T) {
want := [][]byte{
// batch begin
[]byte("\x00\x00\x00\x0a"),
// nft add table inet filter
[]byte("\x01\x00\x00\x00\x0b\x00\x01\x00\x66\x69\x6c\x74\x65\x72\x00\x00\x08\x00\x02\x00\x00\x00\x00\x00"),
// nft add chain inet filter base-chain { type filter hook prerouting priority 0 \; comment "test comment" \; }
[]byte("\x01\x00\x00\x00\x0b\x00\x01\x00\x66\x69\x6c\x74\x65\x72\x00\x00\x0f\x00\x03\x00\x62\x61\x73\x65\x2d\x63\x68\x61\x69\x6e\x00\x00\x14\x00\x04\x80\x08\x00\x01\x00\x00\x00\x00\x00\x08\x00\x02\x00\x00\x00\x00\x00\x0b\x00\x07\x00\x66\x69\x6c\x74\x65\x72\x00\x00\x13\x00\x0c\x00\x00\x0d\x74\x65\x73\x74\x20\x63\x6f\x6d\x6d\x65\x6e\x74\x00\x00"),
// batch end
[]byte("\x00\x00\x00\x0a"),
}

c, err := nftables.New(expectMessages(t, want))
if err != nil {
t.Fatal(err)
}

filter := c.AddTable(&nftables.Table{
Family: nftables.TableFamilyINet,
Name: "filter",
})
c.AddChain(&nftables.Chain{
Name: "base-chain",
Table: filter,
Hooknum: nftables.ChainHookPrerouting,
Priority: nftables.ChainPriorityRef(0),
Type: nftables.ChainTypeFilter,
Comment: "test comment",
})

if err := c.Flush(); err != nil {
t.Fatal(err)
}
}

// TestTableAndChainCommentRoundTrip checks that the comments of tables and
// chains are stored by the kernel and read back by the list functions, and
// that objects without a comment list an empty one.
func TestTableAndChainCommentRoundTrip(t *testing.T) {
conn, newNS := nftest.OpenSystemConn(t, *enableSysTests)
defer nftest.CleanupSystemConn(t, newNS)
conn.FlushRuleset()
defer conn.FlushRuleset()

commented := conn.AddTable(&nftables.Table{
Name: "commented",
Family: nftables.TableFamilyINet,
Comment: "table comment",
})
plain := conn.AddTable(&nftables.Table{
Name: "plain",
Family: nftables.TableFamilyINet,
})
conn.AddChain(&nftables.Chain{
Name: "base",
Table: commented,
Hooknum: nftables.ChainHookInput,
Priority: nftables.ChainPriorityFilter,
Type: nftables.ChainTypeFilter,
Comment: "base chain comment",
})
conn.AddChain(&nftables.Chain{
Name: "regular",
Table: commented,
Comment: "regular chain comment",
})
conn.AddChain(&nftables.Chain{
Name: "plain",
Table: plain,
})
if err := conn.Flush(); err != nil {
t.Fatalf("conn.Flush() failed: %v", err)
}

tables, err := conn.ListTablesOfFamily(nftables.TableFamilyINet)
if err != nil {
t.Fatalf("conn.ListTablesOfFamily() failed: %v", err)
}
gotTables := map[string]string{}
for _, tb := range tables {
gotTables[tb.Name] = tb.Comment
}
wantTables := map[string]string{"commented": "table comment", "plain": ""}
if !reflect.DeepEqual(wantTables, gotTables) {
t.Errorf("table comments = %v, want %v", gotTables, wantTables)
}

tb, err := conn.ListTableOfFamily("commented", nftables.TableFamilyINet)
if err != nil {
t.Fatalf("conn.ListTableOfFamily() failed: %v", err)
}
if got, want := tb.Comment, "table comment"; got != want {
t.Errorf("ListTableOfFamily() comment = %q, want %q", got, want)
}

chains, err := conn.ListChainsOfTableFamily(nftables.TableFamilyINet)
if err != nil {
t.Fatalf("conn.ListChainsOfTableFamily() failed: %v", err)
}
gotChains := map[string]string{}
for _, ch := range chains {
gotChains[ch.Table.Name+"/"+ch.Name] = ch.Comment
}
wantChains := map[string]string{
"commented/base": "base chain comment",
"commented/regular": "regular chain comment",
"plain/plain": "",
}
if !reflect.DeepEqual(wantChains, gotChains) {
t.Errorf("chain comments = %v, want %v", gotChains, wantChains)
}

ch, err := conn.ListChain(commented, "base")
if err != nil {
t.Fatalf("conn.ListChain() failed: %v", err)
}
if got, want := ch.Comment, "base chain comment"; got != want {
t.Errorf("ListChain() comment = %q, want %q", got, want)
}

// Adding an existing table or chain again with another comment succeeds
// but the kernel keeps the original comment.
conn.AddTable(&nftables.Table{
Name: "commented",
Family: nftables.TableFamilyINet,
Comment: "changed",
})
conn.AddChain(&nftables.Chain{
Name: "regular",
Table: commented,
Comment: "changed",
})
if err := conn.Flush(); err != nil {
t.Fatalf("conn.Flush() failed: %v", err)
}
tb, err = conn.ListTableOfFamily("commented", nftables.TableFamilyINet)
if err != nil {
t.Fatalf("conn.ListTableOfFamily() failed: %v", err)
}
if got, want := tb.Comment, "table comment"; got != want {
t.Errorf("table comment after re-adding = %q, want %q", got, want)
}
ch, err = conn.ListChain(commented, "regular")
if err != nil {
t.Fatalf("conn.ListChain() failed: %v", err)
}
if got, want := ch.Comment, "regular chain comment"; got != want {
t.Errorf("chain comment after re-adding = %q, want %q", got, want)
}
}

func TestMasq(t *testing.T) {
tests := []struct {
name string
Expand Down
16 changes: 16 additions & 0 deletions table.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"fmt"

"github.com/google/nftables/binaryutil"
"github.com/google/nftables/userdata"
"github.com/mdlayher/netlink"
"golang.org/x/sys/unix"
)
Expand All @@ -44,6 +45,8 @@ const (
)

const (
// https://github.com/torvalds/linux/blob/cbd2257dc96e3e46217540fcb095a757ffa20d96/include/uapi/linux/netfilter/nf_tables.h#L210
nftaTableUserdata = 0x06
// https://github.com/torvalds/linux/blob/cbd2257dc96e3e46217540fcb095a757ffa20d96/include/uapi/linux/netfilter/nf_tables.h#L211
nftaTableOwner = 0x07

Expand Down Expand Up @@ -79,6 +82,11 @@ type Table struct {
Flags TableFlags // NFTA_TABLE_FLAGS
Owner uint32 // NFTA_TABLE_OWNER
Family TableFamily
// Comment is stored in NFTA_TABLE_USERDATA in the format used by nft,
// so "nft list ruleset" shows it. It requires Linux 5.10 and is ignored
// by older kernels. The kernel does not update the comment of an
// existing table, changing it requires deleting the table.
Comment string
}

// DelTable deletes a specific table, along with all chains/rules it contains.
Expand Down Expand Up @@ -124,6 +132,10 @@ func (cc *Conn) addTable(t *Table, flag netlink.HeaderFlags) *Table {
}

attrs = append(attrs, netlink.Attribute{Type: unix.NFTA_TABLE_FLAGS, Data: binaryutil.BigEndian.PutUint32(uint32(t.Flags))})
if t.Comment != "" {
udata := userdata.AppendString(nil, userdata.NFTNL_UDATA_TABLE_COMMENT, t.Comment)
attrs = append(attrs, netlink.Attribute{Type: nftaTableUserdata, Data: udata})
}
data := cc.marshalAttr(attrs)

cc.messages = append(cc.messages, netlinkMessage{
Expand Down Expand Up @@ -262,6 +274,10 @@ func tableFromMsg(msg netlink.Message) (*Table, error) {
t.Flags = TableFlags(ad.Uint32())
case nftaTableOwner:
t.Owner = ad.Uint32()
case nftaTableUserdata:
if comment, ok := userdata.GetString(ad.Bytes(), userdata.NFTNL_UDATA_TABLE_COMMENT); ok {
t.Comment = comment
}
}
}

Expand Down
Loading
Loading