Skip to content

Commit c9a67cc

Browse files
aojeastapelberg
authored andcommitted
Add integration tests for nftables package
This commit adds integration tests to the nftables package to verify that the Go code correctly programs nftables rules. The tests use external nftables scripts to define the expected state and compare it with the state produced by the Go code. Change-Id: I9c8439ee462b4882b221e6244f53379b822446dc Signed-off-by: Antonio Ojea <[email protected]>
1 parent 3cae477 commit c9a67cc

File tree

7 files changed

+288
-4
lines changed

7 files changed

+288
-4
lines changed

.github/workflows/push.yml

+2
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,5 @@ jobs:
3333
go test ./...
3434
go test -c github.com/google/nftables
3535
sudo ./nftables.test -test.v -run_system_tests
36+
go test -c github.com/google/nftables/integration
37+
(cd integration && sudo ../integration.test -test.v -run_system_tests)

go.mod

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ module github.com/google/nftables
33
go 1.21
44

55
require (
6+
github.com/google/go-cmp v0.6.0
67
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42
7-
github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc
8+
github.com/vishvananda/netlink v1.3.0
9+
github.com/vishvananda/netns v0.0.4
810
golang.org/x/sys v0.28.0
911
)
1012

1113
require (
12-
github.com/google/go-cmp v0.6.0 // indirect
1314
github.com/mdlayher/socket v0.5.0 // indirect
1415
golang.org/x/net v0.33.0 // indirect
1516
golang.org/x/sync v0.6.0 // indirect

go.sum

+6-2
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@ github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0
44
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=
55
github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
66
github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI=
7-
github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc h1:R83G5ikgLMxrBvLh22JhdfI8K6YXEPHx5P03Uu3DRs4=
8-
github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI=
7+
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
8+
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
9+
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
10+
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
911
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
1012
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
1113
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
1214
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
15+
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
16+
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1317
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
1418
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

integration/nft_test.go

+252
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
// Copyright 2025 Google LLC. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package integration
16+
17+
import (
18+
"flag"
19+
"os/exec"
20+
"strings"
21+
"testing"
22+
23+
"github.com/google/go-cmp/cmp"
24+
"github.com/google/nftables"
25+
"github.com/google/nftables/binaryutil"
26+
"github.com/google/nftables/expr"
27+
"github.com/google/nftables/internal/nftest"
28+
"github.com/vishvananda/netlink"
29+
)
30+
31+
var enableSysTests = flag.Bool("run_system_tests", false, "Run tests that operate against the live kernel")
32+
33+
func ifname(n string) []byte {
34+
b := make([]byte, 16)
35+
copy(b, []byte(n+"\x00"))
36+
return b
37+
}
38+
39+
func TestNFTables(t *testing.T) {
40+
tests := []struct {
41+
name string
42+
scriptPath string
43+
goCommands func(t *testing.T, c *nftables.Conn)
44+
expectFailure bool
45+
}{
46+
{
47+
name: "AddTable",
48+
scriptPath: "testdata/add_table.nft",
49+
goCommands: func(t *testing.T, c *nftables.Conn) {
50+
c.FlushRuleset()
51+
52+
c.AddTable(&nftables.Table{
53+
Name: "test-table",
54+
Family: nftables.TableFamilyINet,
55+
})
56+
57+
err := c.Flush()
58+
if err != nil {
59+
t.Fatalf("Error creating table: %v", err)
60+
}
61+
},
62+
},
63+
{
64+
name: "AddChain",
65+
scriptPath: "testdata/add_chain.nft",
66+
goCommands: func(t *testing.T, c *nftables.Conn) {
67+
c.FlushRuleset()
68+
69+
table := c.AddTable(&nftables.Table{
70+
Name: "test-table",
71+
Family: nftables.TableFamilyINet,
72+
})
73+
74+
c.AddChain(&nftables.Chain{
75+
Name: "test-chain",
76+
Table: table,
77+
Hooknum: nftables.ChainHookOutput,
78+
Priority: nftables.ChainPriorityNATDest,
79+
Type: nftables.ChainTypeNAT,
80+
})
81+
82+
err := c.Flush()
83+
if err != nil {
84+
t.Fatalf("Error creating table: %v", err)
85+
}
86+
},
87+
},
88+
{
89+
name: "AddFlowtables",
90+
scriptPath: "testdata/add_flowtables.nft",
91+
goCommands: func(t *testing.T, c *nftables.Conn) {
92+
devices := []string{"dummy0"}
93+
c.FlushRuleset()
94+
// add + delete + add for flushing all the table
95+
table := c.AddTable(&nftables.Table{
96+
Family: nftables.TableFamilyINet,
97+
Name: "test-table",
98+
})
99+
100+
devicesSet := &nftables.Set{
101+
Table: table,
102+
Name: "test-set",
103+
KeyType: nftables.TypeIFName,
104+
KeyByteOrder: binaryutil.NativeEndian,
105+
}
106+
107+
elements := []nftables.SetElement{}
108+
for _, dev := range devices {
109+
elements = append(elements, nftables.SetElement{
110+
Key: ifname(dev),
111+
})
112+
}
113+
114+
if err := c.AddSet(devicesSet, elements); err != nil {
115+
t.Errorf("failed to add Set %s : %v", devicesSet.Name, err)
116+
}
117+
118+
flowtable := &nftables.Flowtable{
119+
Table: table,
120+
Name: "test-flowtable",
121+
Devices: devices,
122+
Hooknum: nftables.FlowtableHookIngress,
123+
Priority: nftables.FlowtablePriorityRef(5),
124+
}
125+
c.AddFlowtable(flowtable)
126+
127+
chain := c.AddChain(&nftables.Chain{
128+
Name: "test-chain",
129+
Table: table,
130+
Type: nftables.ChainTypeFilter,
131+
Hooknum: nftables.ChainHookForward,
132+
Priority: nftables.ChainPriorityMangle,
133+
})
134+
135+
c.AddRule(&nftables.Rule{
136+
Table: table,
137+
Chain: chain,
138+
Exprs: []expr.Any{
139+
&expr.Meta{Key: expr.MetaKeyIIFNAME, SourceRegister: false, Register: 0x1},
140+
&expr.Lookup{SourceRegister: 0x1, DestRegister: 0x0, IsDestRegSet: false, SetName: "test-set", Invert: true},
141+
&expr.Verdict{Kind: expr.VerdictReturn},
142+
},
143+
})
144+
145+
c.AddRule(&nftables.Rule{
146+
Table: table,
147+
Chain: chain,
148+
Exprs: []expr.Any{
149+
&expr.Meta{Key: expr.MetaKeyOIFNAME, SourceRegister: false, Register: 0x1},
150+
&expr.Lookup{SourceRegister: 0x1, DestRegister: 0x0, IsDestRegSet: false, SetName: "test-set", Invert: true},
151+
&expr.Verdict{Kind: expr.VerdictReturn},
152+
},
153+
})
154+
155+
c.AddRule(&nftables.Rule{
156+
Table: table,
157+
Chain: chain,
158+
Exprs: []expr.Any{
159+
&expr.Ct{Register: 0x1, SourceRegister: false, Key: expr.CtKeySTATE, Direction: 0x0},
160+
&expr.Bitwise{SourceRegister: 0x1, DestRegister: 0x1, Len: 0x4, Mask: binaryutil.NativeEndian.PutUint32(expr.CtStateBitESTABLISHED), Xor: binaryutil.NativeEndian.PutUint32(0)},
161+
&expr.Cmp{Op: 0x1, Register: 0x1, Data: []uint8{0x0, 0x0, 0x0, 0x0}},
162+
&expr.Ct{Register: 0x1, SourceRegister: false, Key: expr.CtKeyPKTS, Direction: 0x0},
163+
&expr.Cmp{Op: expr.CmpOpGt, Register: 0x1, Data: binaryutil.NativeEndian.PutUint64(20)},
164+
&expr.FlowOffload{Name: "test-flowtable"},
165+
&expr.Counter{},
166+
},
167+
})
168+
169+
if err := c.Flush(); err != nil {
170+
t.Fatal(err)
171+
}
172+
},
173+
},
174+
}
175+
176+
for _, tt := range tests {
177+
t.Run(tt.name, func(t *testing.T) {
178+
// Create a new network namespace to test these operations,
179+
// and tear down the namespace at test completion.
180+
c, newNS := nftest.OpenSystemConn(t, *enableSysTests)
181+
defer nftest.CleanupSystemConn(t, newNS)
182+
183+
// Real interface must exist otherwise some nftables will fail
184+
la := netlink.NewLinkAttrs()
185+
la.Name = "dummy0"
186+
dummy := &netlink.Dummy{LinkAttrs: la}
187+
if err := netlink.LinkAdd(dummy); err != nil {
188+
t.Fatal(err)
189+
}
190+
191+
scriptOutput, err := applyNFTRuleset(tt.scriptPath)
192+
if err != nil {
193+
t.Fatalf("Failed to apply nftables script: %v\noutput:%s", err, scriptOutput)
194+
}
195+
if len(scriptOutput) > 0 {
196+
t.Logf("nft output:\n%s", scriptOutput)
197+
}
198+
199+
// Retrieve nftables state using nft
200+
expectedOutput, err := listNFTRuleset()
201+
if err != nil {
202+
t.Fatalf("Failed to list nftables ruleset: %v\noutput:%s", err, expectedOutput)
203+
}
204+
t.Logf("Expected output:\n%s", expectedOutput)
205+
206+
// Program nftables using your Go code
207+
if err := flushNFTRuleset(); err != nil {
208+
t.Fatalf("Failed to flush nftables ruleset: %v", err)
209+
}
210+
tt.goCommands(t, c)
211+
212+
// Retrieve nftables state using nft
213+
actualOutput, err := listNFTRuleset()
214+
if err != nil {
215+
t.Fatalf("Failed to list nftables ruleset: %v\noutput:%s", err, actualOutput)
216+
}
217+
218+
t.Logf("Actual output:\n%s", actualOutput)
219+
220+
if expectedOutput != actualOutput {
221+
t.Errorf("nftables ruleset mismatch:\n%s", cmp.Diff(expectedOutput, actualOutput))
222+
}
223+
224+
if err := flushNFTRuleset(); err != nil {
225+
t.Fatalf("Failed to flush nftables ruleset: %v", err)
226+
}
227+
})
228+
}
229+
}
230+
231+
func applyNFTRuleset(scriptPath string) (string, error) {
232+
cmd := exec.Command("nft", "--debug=all", "-f", scriptPath)
233+
out, err := cmd.CombinedOutput()
234+
if err != nil {
235+
return string(out), err
236+
}
237+
return strings.TrimSpace(string(out)), nil
238+
}
239+
240+
func listNFTRuleset() (string, error) {
241+
cmd := exec.Command("nft", "list", "ruleset")
242+
out, err := cmd.CombinedOutput()
243+
if err != nil {
244+
return string(out), err
245+
}
246+
return strings.TrimSpace(string(out)), nil
247+
}
248+
249+
func flushNFTRuleset() error {
250+
cmd := exec.Command("nft", "flush", "ruleset")
251+
return cmd.Run()
252+
}

integration/testdata/add_chain.nft

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
table inet test-table {
2+
chain test-chain {
3+
type nat hook output priority dstnat; policy accept;
4+
}
5+
}
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
table inet test-table {
2+
set test-set {
3+
type ifname
4+
elements = { "dummy0" }
5+
}
6+
7+
flowtable test-flowtable {
8+
hook ingress priority filter + 5
9+
devices = { dummy0 }
10+
}
11+
12+
chain test-chain {
13+
type filter hook forward priority mangle; policy accept;
14+
iifname != @test-set return
15+
oifname != @test-set return
16+
ct state established ct packets > 20 flow add @test-flowtable counter packets 0 bytes 0
17+
}
18+
}

integration/testdata/add_table.nft

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
table inet test-table {
2+
}

0 commit comments

Comments
 (0)