diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index d05791b..471411d 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -33,3 +33,5 @@ jobs: go test ./... go test -c github.com/google/nftables sudo ./nftables.test -test.v -run_system_tests + go test -c github.com/google/nftables/integration + (cd integration && sudo ../integration.test -test.v -run_system_tests) diff --git a/go.mod b/go.mod index 5b33ffc..a3c19cc 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,14 @@ module github.com/google/nftables go 1.21 require ( + github.com/google/go-cmp v0.6.0 github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 - github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc + github.com/vishvananda/netlink v1.3.0 + github.com/vishvananda/netns v0.0.4 golang.org/x/sys v0.28.0 ) require ( - github.com/google/go-cmp v0.6.0 // indirect github.com/mdlayher/socket v0.5.0 // indirect golang.org/x/net v0.33.0 // indirect golang.org/x/sync v0.6.0 // indirect diff --git a/go.sum b/go.sum index be7f332..133f8a1 100644 --- a/go.sum +++ b/go.sum @@ -4,11 +4,15 @@ github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0 github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o= github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI= github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= -github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc h1:R83G5ikgLMxrBvLh22JhdfI8K6YXEPHx5P03Uu3DRs4= -github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= +github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk= +github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= +github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= +github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/integration/nft_test.go b/integration/nft_test.go new file mode 100644 index 0000000..14c7d43 --- /dev/null +++ b/integration/nft_test.go @@ -0,0 +1,252 @@ +// Copyright 2025 Google LLC. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package integration + +import ( + "flag" + "os/exec" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/nftables" + "github.com/google/nftables/binaryutil" + "github.com/google/nftables/expr" + "github.com/google/nftables/internal/nftest" + "github.com/vishvananda/netlink" +) + +var enableSysTests = flag.Bool("run_system_tests", false, "Run tests that operate against the live kernel") + +func ifname(n string) []byte { + b := make([]byte, 16) + copy(b, []byte(n+"\x00")) + return b +} + +func TestNFTables(t *testing.T) { + tests := []struct { + name string + scriptPath string + goCommands func(t *testing.T, c *nftables.Conn) + expectFailure bool + }{ + { + name: "AddTable", + scriptPath: "testdata/add_table.nft", + goCommands: func(t *testing.T, c *nftables.Conn) { + c.FlushRuleset() + + c.AddTable(&nftables.Table{ + Name: "test-table", + Family: nftables.TableFamilyINet, + }) + + err := c.Flush() + if err != nil { + t.Fatalf("Error creating table: %v", err) + } + }, + }, + { + name: "AddChain", + scriptPath: "testdata/add_chain.nft", + goCommands: func(t *testing.T, c *nftables.Conn) { + c.FlushRuleset() + + table := c.AddTable(&nftables.Table{ + Name: "test-table", + Family: nftables.TableFamilyINet, + }) + + c.AddChain(&nftables.Chain{ + Name: "test-chain", + Table: table, + Hooknum: nftables.ChainHookOutput, + Priority: nftables.ChainPriorityNATDest, + Type: nftables.ChainTypeNAT, + }) + + err := c.Flush() + if err != nil { + t.Fatalf("Error creating table: %v", err) + } + }, + }, + { + name: "AddFlowtables", + scriptPath: "testdata/add_flowtables.nft", + goCommands: func(t *testing.T, c *nftables.Conn) { + devices := []string{"dummy0"} + c.FlushRuleset() + // add + delete + add for flushing all the table + table := c.AddTable(&nftables.Table{ + Family: nftables.TableFamilyINet, + Name: "test-table", + }) + + devicesSet := &nftables.Set{ + Table: table, + Name: "test-set", + KeyType: nftables.TypeIFName, + KeyByteOrder: binaryutil.NativeEndian, + } + + elements := []nftables.SetElement{} + for _, dev := range devices { + elements = append(elements, nftables.SetElement{ + Key: ifname(dev), + }) + } + + if err := c.AddSet(devicesSet, elements); err != nil { + t.Errorf("failed to add Set %s : %v", devicesSet.Name, err) + } + + flowtable := &nftables.Flowtable{ + Table: table, + Name: "test-flowtable", + Devices: devices, + Hooknum: nftables.FlowtableHookIngress, + Priority: nftables.FlowtablePriorityRef(5), + } + c.AddFlowtable(flowtable) + + chain := c.AddChain(&nftables.Chain{ + Name: "test-chain", + Table: table, + Type: nftables.ChainTypeFilter, + Hooknum: nftables.ChainHookForward, + Priority: nftables.ChainPriorityMangle, + }) + + c.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{Key: expr.MetaKeyIIFNAME, SourceRegister: false, Register: 0x1}, + &expr.Lookup{SourceRegister: 0x1, DestRegister: 0x0, IsDestRegSet: false, SetName: "test-set", Invert: true}, + &expr.Verdict{Kind: expr.VerdictReturn}, + }, + }) + + c.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Meta{Key: expr.MetaKeyOIFNAME, SourceRegister: false, Register: 0x1}, + &expr.Lookup{SourceRegister: 0x1, DestRegister: 0x0, IsDestRegSet: false, SetName: "test-set", Invert: true}, + &expr.Verdict{Kind: expr.VerdictReturn}, + }, + }) + + c.AddRule(&nftables.Rule{ + Table: table, + Chain: chain, + Exprs: []expr.Any{ + &expr.Ct{Register: 0x1, SourceRegister: false, Key: expr.CtKeySTATE, Direction: 0x0}, + &expr.Bitwise{SourceRegister: 0x1, DestRegister: 0x1, Len: 0x4, Mask: binaryutil.NativeEndian.PutUint32(expr.CtStateBitESTABLISHED), Xor: binaryutil.NativeEndian.PutUint32(0)}, + &expr.Cmp{Op: 0x1, Register: 0x1, Data: []uint8{0x0, 0x0, 0x0, 0x0}}, + &expr.Ct{Register: 0x1, SourceRegister: false, Key: expr.CtKeyPKTS, Direction: 0x0}, + &expr.Cmp{Op: expr.CmpOpGt, Register: 0x1, Data: binaryutil.NativeEndian.PutUint64(20)}, + &expr.FlowOffload{Name: "test-flowtable"}, + &expr.Counter{}, + }, + }) + + if err := c.Flush(); err != nil { + t.Fatal(err) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a new network namespace to test these operations, + // and tear down the namespace at test completion. + c, newNS := nftest.OpenSystemConn(t, *enableSysTests) + defer nftest.CleanupSystemConn(t, newNS) + + // Real interface must exist otherwise some nftables will fail + la := netlink.NewLinkAttrs() + la.Name = "dummy0" + dummy := &netlink.Dummy{LinkAttrs: la} + if err := netlink.LinkAdd(dummy); err != nil { + t.Fatal(err) + } + + scriptOutput, err := applyNFTRuleset(tt.scriptPath) + if err != nil { + t.Fatalf("Failed to apply nftables script: %v\noutput:%s", err, scriptOutput) + } + if len(scriptOutput) > 0 { + t.Logf("nft output:\n%s", scriptOutput) + } + + // Retrieve nftables state using nft + expectedOutput, err := listNFTRuleset() + if err != nil { + t.Fatalf("Failed to list nftables ruleset: %v\noutput:%s", err, expectedOutput) + } + t.Logf("Expected output:\n%s", expectedOutput) + + // Program nftables using your Go code + if err := flushNFTRuleset(); err != nil { + t.Fatalf("Failed to flush nftables ruleset: %v", err) + } + tt.goCommands(t, c) + + // Retrieve nftables state using nft + actualOutput, err := listNFTRuleset() + if err != nil { + t.Fatalf("Failed to list nftables ruleset: %v\noutput:%s", err, actualOutput) + } + + t.Logf("Actual output:\n%s", actualOutput) + + if expectedOutput != actualOutput { + t.Errorf("nftables ruleset mismatch:\n%s", cmp.Diff(expectedOutput, actualOutput)) + } + + if err := flushNFTRuleset(); err != nil { + t.Fatalf("Failed to flush nftables ruleset: %v", err) + } + }) + } +} + +func applyNFTRuleset(scriptPath string) (string, error) { + cmd := exec.Command("nft", "--debug=all", "-f", scriptPath) + out, err := cmd.CombinedOutput() + if err != nil { + return string(out), err + } + return strings.TrimSpace(string(out)), nil +} + +func listNFTRuleset() (string, error) { + cmd := exec.Command("nft", "list", "ruleset") + out, err := cmd.CombinedOutput() + if err != nil { + return string(out), err + } + return strings.TrimSpace(string(out)), nil +} + +func flushNFTRuleset() error { + cmd := exec.Command("nft", "flush", "ruleset") + return cmd.Run() +} diff --git a/integration/testdata/add_chain.nft b/integration/testdata/add_chain.nft new file mode 100644 index 0000000..27cbf70 --- /dev/null +++ b/integration/testdata/add_chain.nft @@ -0,0 +1,5 @@ +table inet test-table { + chain test-chain { + type nat hook output priority dstnat; policy accept; + } +} diff --git a/integration/testdata/add_flowtables.nft b/integration/testdata/add_flowtables.nft new file mode 100644 index 0000000..5a30676 --- /dev/null +++ b/integration/testdata/add_flowtables.nft @@ -0,0 +1,18 @@ +table inet test-table { + set test-set { + type ifname + elements = { "dummy0" } + } + + flowtable test-flowtable { + hook ingress priority filter + 5 + devices = { dummy0 } + } + + chain test-chain { + type filter hook forward priority mangle; policy accept; + iifname != @test-set return + oifname != @test-set return + ct state established ct packets > 20 flow add @test-flowtable counter packets 0 bytes 0 + } +} diff --git a/integration/testdata/add_table.nft b/integration/testdata/add_table.nft new file mode 100644 index 0000000..5c43f01 --- /dev/null +++ b/integration/testdata/add_table.nft @@ -0,0 +1,2 @@ +table inet test-table { +}