mirror of
https://github.com/moby/moby.git
synced 2026-08-04 15:11:00 +00:00
SetBaseChainPolicy read the table's Chains map and modified the chain it found without holding applyLock, only taking the lock when it called Apply to make the change. Racing with an Apply that adds a chain, the map read is a fatal "concurrent map read and map write". Nothing calls it yet - the nftabler doesn't implement filterForwardDrop - so this isn't a live bug, but the API shouldn't come with the race attached. Hold applyLock for the whole read-modify-apply. That needs an unexported apply which assumes the lock is held, so split the body out of Table.Apply, leaving the exported method as the wrapper that checks the table and takes the lock, much like Reload and table.reload. While here, make Reload report a closed table the same way as the other two, rather than relying on the check in table.nftApply and reporting a generic "invalid table" for a table that raced with Close. The happy path of SetBaseChainPolicy had no test coverage at all, which now matters more because it applies the table with applyLock held - a deadlock would be silent. Add one. Signed-off-by: Cory Snider <csnider@mirantis.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
865 lines
28 KiB
Go
865 lines
28 KiB
Go
package nftables
|
|
|
|
import (
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/moby/moby/v2/internal/testutil/netnsutils"
|
|
"gotest.tools/v3/assert"
|
|
is "gotest.tools/v3/assert/cmp"
|
|
"gotest.tools/v3/golden"
|
|
"gotest.tools/v3/icmd"
|
|
)
|
|
|
|
func testSetup(t *testing.T) func() {
|
|
t.Helper()
|
|
if err := Enable(); err != nil {
|
|
// Make sure it didn't fail because of a bug in the text/template.
|
|
assert.NilError(t, parseTemplate())
|
|
// If this is not CI, skip.
|
|
if _, ok := os.LookupEnv("CI"); !ok {
|
|
t.Skip("Cannot enable nftables, no 'nft' command in $PATH ?")
|
|
}
|
|
// In CI, nft should always be installed, fail the test.
|
|
t.Fatalf("Failed to enable nftables: %s", err)
|
|
}
|
|
cleanupContext := netnsutils.SetupTestOSContext(t)
|
|
return func() {
|
|
cleanupContext()
|
|
Disable()
|
|
}
|
|
}
|
|
|
|
func applyAndCheck(t *testing.T, goldenFilename string, tbl *Table, tm ...Modifier) {
|
|
t.Helper()
|
|
err := tbl.Apply(t.Context(), tm...)
|
|
assert.Check(t, err)
|
|
res := icmd.RunCommand("nft", "list", "table", string(tbl.Family()), tbl.Name())
|
|
res.Assert(t, icmd.Success)
|
|
golden.Assert(t, res.Combined(), goldenFilename)
|
|
}
|
|
|
|
func reloadAndCheck(t *testing.T, goldenFilename string, tbl *Table) {
|
|
t.Helper()
|
|
err := tbl.Reload(t.Context())
|
|
assert.Check(t, err)
|
|
res := icmd.RunCommand("nft", "list", "table", string(tbl.Family()), tbl.Name())
|
|
res.Assert(t, icmd.Success)
|
|
golden.Assert(t, res.Combined(), goldenFilename)
|
|
}
|
|
|
|
func TestTable(t *testing.T) {
|
|
defer testSetup(t)()
|
|
|
|
tbl4, err := NewTable(IPv4, "ipv4_table")
|
|
assert.NilError(t, err)
|
|
defer tbl4.Close()
|
|
tbl6, err := NewTable(IPv6, "ipv6_table")
|
|
assert.NilError(t, err)
|
|
defer tbl6.Close()
|
|
|
|
// Update nftables and check what happened.
|
|
applyAndCheck(t, t.Name()+"/created4.golden", tbl4, Modifier{})
|
|
applyAndCheck(t, t.Name()+"/created6.golden", tbl6, Modifier{})
|
|
}
|
|
|
|
func TestTableClose(t *testing.T) {
|
|
// No nftables setup needed, closing a table that's never been applied doesn't
|
|
// run any "nft" commands.
|
|
tbl, err := NewTable(IPv4, "this_is_a_table")
|
|
assert.NilError(t, err)
|
|
assert.Assert(t, tbl.IsValid())
|
|
|
|
// Consumers hold references to the table, closing it must invalidate them all.
|
|
ref := tbl
|
|
|
|
assert.NilError(t, tbl.Close())
|
|
assert.Check(t, !tbl.IsValid(), "closed table should not be valid")
|
|
assert.Check(t, !ref.IsValid(), "reference to a closed table should not be valid")
|
|
|
|
// Close is idempotent, and a nil *Table can be closed.
|
|
assert.NilError(t, tbl.Close())
|
|
var nilTbl *Table
|
|
assert.NilError(t, nilTbl.Close())
|
|
assert.Check(t, !nilTbl.IsValid())
|
|
|
|
// A Table that didn't come from NewTable is not usable either.
|
|
var zeroVal Table
|
|
assert.Check(t, !zeroVal.IsValid())
|
|
assert.NilError(t, zeroVal.Close())
|
|
|
|
assert.Check(t, is.Equal(tbl.Name(), ""))
|
|
assert.Check(t, is.Equal(tbl.Family(), Family("")))
|
|
}
|
|
|
|
func TestTableUseAfterClose(t *testing.T) {
|
|
defer testSetup(t)()
|
|
|
|
tbl, err := NewTable(IPv4, "this_is_a_table")
|
|
assert.NilError(t, err)
|
|
|
|
const chainName = "this_is_a_chain"
|
|
var tm Modifier
|
|
tm.Create(Chain{Name: chainName})
|
|
assert.NilError(t, tbl.Apply(t.Context(), tm))
|
|
|
|
ref := tbl
|
|
assert.NilError(t, tbl.Close())
|
|
|
|
// Operations on a closed table must be refused, rather than transparently
|
|
// opening a new handle to the underlying nftables table.
|
|
var tm2 Modifier
|
|
tm2.Create(Rule{Chain: chainName, Rule: []string{"counter"}})
|
|
assert.Check(t, is.ErrorIs(tbl.Apply(t.Context(), tm2), errTableClosed))
|
|
assert.Check(t, is.ErrorIs(ref.Apply(t.Context(), tm2), errTableClosed))
|
|
assert.Check(t, is.ErrorIs(tbl.Reload(t.Context()), errTableClosed))
|
|
assert.Check(t, is.ErrorIs(tbl.SetBaseChainPolicy(t.Context(), chainName, BaseChainPolicyDrop),
|
|
errTableClosed))
|
|
// applyLock must be held to read nftHandle.
|
|
tbl.t.applyLock.Lock()
|
|
handle := tbl.t.nftHandle
|
|
tbl.t.applyLock.Unlock()
|
|
assert.Check(t, is.Nil(handle), "no nftables handle should have been opened")
|
|
|
|
// The table itself is left alone by Close, but the rejected update must not
|
|
// have reached it. (The table's name and family can't be read from the closed
|
|
// handle.)
|
|
res := icmd.RunCommand("nft", "list", "table", string(IPv4), "this_is_a_table")
|
|
res.Assert(t, icmd.Success)
|
|
golden.Assert(t, res.Combined(), t.Name()+".golden")
|
|
|
|
// A nil *Table, and a Table that didn't come from NewTable, must be refused
|
|
// rather than panicking on their unusable innards.
|
|
var nilTbl *Table
|
|
assert.Check(t, is.ErrorIs(nilTbl.Apply(t.Context(), tm2), errInvalidTable))
|
|
assert.Check(t, is.ErrorIs(nilTbl.Reload(t.Context()), errInvalidTable))
|
|
assert.Check(t, is.ErrorIs(nilTbl.SetBaseChainPolicy(t.Context(), chainName, BaseChainPolicyDrop),
|
|
errInvalidTable))
|
|
|
|
unusable := &Table{}
|
|
assert.Check(t, is.ErrorIs(unusable.Apply(t.Context(), tm2), errInvalidTable))
|
|
assert.Check(t, is.ErrorIs(unusable.Reload(t.Context()), errInvalidTable))
|
|
assert.Check(t, is.ErrorIs(unusable.SetBaseChainPolicy(t.Context(), chainName, BaseChainPolicyDrop),
|
|
errInvalidTable))
|
|
}
|
|
|
|
func TestSetBaseChainPolicy(t *testing.T) {
|
|
defer testSetup(t)()
|
|
|
|
tbl, err := NewTable(IPv4, "this_is_a_table")
|
|
assert.NilError(t, err)
|
|
defer tbl.Close()
|
|
|
|
const bcName = "this_is_a_base_chain"
|
|
var tm Modifier
|
|
tm.Create(BaseChain{
|
|
Name: bcName,
|
|
ChainType: BaseChainTypeFilter,
|
|
Hook: BaseChainHookForward,
|
|
Priority: BaseChainPriorityFilter,
|
|
Policy: BaseChainPolicyAccept,
|
|
})
|
|
const cName = "this_is_a_regular_chain"
|
|
tm.Create(Chain{Name: cName})
|
|
assert.NilError(t, tbl.Apply(t.Context(), tm))
|
|
|
|
// The new policy is applied immediately. (Which must not deadlock - the table
|
|
// is applied with applyLock held.)
|
|
assert.NilError(t, tbl.SetBaseChainPolicy(t.Context(), bcName, BaseChainPolicyDrop))
|
|
res := icmd.RunCommand("nft", "list", "table", string(tbl.Family()), tbl.Name())
|
|
res.Assert(t, icmd.Success)
|
|
golden.Assert(t, res.Combined(), t.Name()+".golden")
|
|
|
|
assert.Check(t, is.ErrorContains(
|
|
tbl.SetBaseChainPolicy(t.Context(), cName, BaseChainPolicyDrop), "it is not a base chain"))
|
|
assert.Check(t, is.ErrorContains(
|
|
tbl.SetBaseChainPolicy(t.Context(), "no_such_chain", BaseChainPolicyDrop), "it does not exist"))
|
|
}
|
|
|
|
func TestChain(t *testing.T) {
|
|
defer testSetup(t)()
|
|
|
|
// Create a table.
|
|
tbl, err := NewTable(IPv4, "this_is_a_table")
|
|
assert.NilError(t, err)
|
|
defer tbl.Close()
|
|
|
|
// Create a base chain.
|
|
const bcName = "this_is_a_base_chain"
|
|
tm := Modifier{}
|
|
bcDesc := BaseChain{
|
|
Name: bcName,
|
|
ChainType: BaseChainTypeFilter,
|
|
Hook: BaseChainHookForward,
|
|
Priority: BaseChainPriorityFilter + 10,
|
|
Policy: "accept",
|
|
}
|
|
tm.Create(bcDesc)
|
|
// Add a rule to the base chain.
|
|
bcCounterRule := Rule{Chain: bcName, Group: 0, Rule: []string{"counter"}}
|
|
tm.Create(bcCounterRule)
|
|
|
|
// Add a regular chain.
|
|
const regularChainName = "this_is_a_regular_chain"
|
|
cDesc := Chain{Name: regularChainName}
|
|
tm.Create(cDesc)
|
|
// Add a rule to the regular chain.
|
|
cRule := Rule{Chain: regularChainName, Group: 0, Rule: []string{"counter", "accept"}}
|
|
tm.Create(cRule)
|
|
|
|
// Add another rule to the base chain.
|
|
bcJumpRule := Rule{Chain: bcName, Group: 0, Rule: []string{"jump", regularChainName}}
|
|
tm.Create(bcJumpRule)
|
|
|
|
// Update nftables and check what happened.
|
|
applyAndCheck(t, t.Name()+"/created.golden", tbl, tm)
|
|
|
|
// Delete a rule from the base chain.
|
|
tm = Modifier{}
|
|
tm.Delete(bcCounterRule)
|
|
|
|
// Update nftables and check what happened.
|
|
applyAndCheck(t, t.Name()+"/modified.golden", tbl, tm)
|
|
|
|
// Delete the base chain.
|
|
tm = Modifier{}
|
|
tm.Delete(bcJumpRule)
|
|
tm.Delete(bcDesc)
|
|
tm.Delete(cRule)
|
|
tm.Delete(cDesc)
|
|
|
|
// Update nftables and check what happened.
|
|
applyAndCheck(t, t.Name()+"/deleted.golden", tbl, tm)
|
|
}
|
|
|
|
func TestChainRuleGroups(t *testing.T) {
|
|
defer testSetup(t)()
|
|
|
|
tbl, err := NewTable(IPv4, "testtable")
|
|
assert.NilError(t, err)
|
|
defer tbl.Close()
|
|
tm := Modifier{}
|
|
chainName := "testchain"
|
|
tm.Create(Chain{Name: chainName})
|
|
tm.Create(Rule{Chain: chainName, Group: 100, Rule: []string{"iifname hello100 counter"}})
|
|
tm.Create(Rule{Chain: chainName, Group: 200, Rule: []string{"iifname hello200 counter"}})
|
|
tm.Create(Rule{Chain: chainName, Group: 100, Rule: []string{"iifname hello101 counter"}})
|
|
tm.Create(Rule{Chain: chainName, Group: 200, Rule: []string{"iifname hello201 counter"}})
|
|
tm.Create(Rule{Chain: chainName, Group: 100, Rule: []string{"iifname hello102 counter"}})
|
|
applyAndCheck(t, t.Name()+".golden", tbl, tm)
|
|
}
|
|
|
|
func TestIgnoreExist(t *testing.T) {
|
|
defer testSetup(t)()
|
|
tbl, err := NewTable(IPv4, "this_is_a_table")
|
|
assert.NilError(t, err)
|
|
defer tbl.Close()
|
|
tm := Modifier{}
|
|
|
|
// Create a chain with a single rule, add the rule again but drop the duplicate.
|
|
const chainName = "this_is_a_chain"
|
|
tm.Create(Chain{Name: chainName})
|
|
tm.Create(Rule{Chain: chainName, Rule: []string{"counter"}})
|
|
tm.Create(Rule{Chain: chainName, Rule: []string{"counter"}, IgnoreExist: true})
|
|
applyAndCheck(t, t.Name()+"/created.golden", tbl, tm)
|
|
|
|
// Add the rule again, ignoring the duplicate, but in a modifier that has an
|
|
// error - check that the existing rule isn't removed by rollback of this modifier.
|
|
tmErr := Modifier{}
|
|
tmErr.Create(Rule{Chain: chainName, Rule: []string{"counter"}, IgnoreExist: true})
|
|
tmErr.Create(Rule{Chain: chainName})
|
|
err = tbl.Apply(t.Context(), tm)
|
|
assert.Check(t, err != nil, "Expected an error")
|
|
|
|
// Reload, to flush table state.
|
|
reloadAndCheck(t, t.Name()+"/created.golden", tbl)
|
|
|
|
// Delete the rule.
|
|
tmDel := Modifier{}
|
|
tmDel.Delete(Rule{Chain: chainName, Rule: []string{"counter"}})
|
|
applyAndCheck(t, t.Name()+"/deleted.golden", tbl, tmDel)
|
|
|
|
// Delete it again, in another chain that will roll back, to check it's not resurrected.
|
|
tmReDel := Modifier{}
|
|
tmReDel.Delete(Rule{Chain: chainName, Rule: []string{"counter"}, IgnoreExist: true})
|
|
tmReDel.Create(Rule{Chain: chainName})
|
|
err = tbl.Apply(t.Context(), tmReDel)
|
|
assert.Check(t, err != nil, "Expected an error")
|
|
|
|
// Reload, to flush table state.
|
|
reloadAndCheck(t, t.Name()+"/deleted.golden", tbl)
|
|
}
|
|
|
|
func TestVMap(t *testing.T) {
|
|
defer testSetup(t)()
|
|
|
|
// Create a table.
|
|
tbl, err := NewTable(IPv6, "this_is_a_table")
|
|
assert.NilError(t, err)
|
|
defer tbl.Close()
|
|
tm := Modifier{}
|
|
|
|
// Create a verdict map.
|
|
const mapName = "this_is_a_vmap"
|
|
tm.Create(Map{
|
|
Name: mapName,
|
|
ElementType: Ifname.VMap(),
|
|
Flags: []string{"dynamic", "timeout"},
|
|
})
|
|
tm.Create(MapElement{MapName: mapName, Key: "eth0", Value: "return"})
|
|
tm.Create(MapElement{MapName: mapName, Key: "eth1", Value: "drop", Comment: `/// this is a comment on a map element \\\`})
|
|
|
|
// Update nftables and check what happened.
|
|
applyAndCheck(t, t.Name()+"/created.golden", tbl, tm)
|
|
|
|
// Undo those changes by reversing the commands.
|
|
tmRev := tm.Reverse()
|
|
|
|
// Update nftables and check what happened.
|
|
applyAndCheck(t, t.Name()+"/deleted.golden", tbl, tmRev)
|
|
}
|
|
|
|
func TestSet(t *testing.T) {
|
|
defer testSetup(t)()
|
|
|
|
// Create v4 and v6 tables.
|
|
tbl4, err := NewTable(IPv4, "table4")
|
|
assert.NilError(t, err)
|
|
defer tbl4.Close()
|
|
tbl6, err := NewTable(IPv6, "table6")
|
|
assert.NilError(t, err)
|
|
defer tbl6.Close()
|
|
|
|
// Create a set in each table.
|
|
const set4Name = "set4"
|
|
tm4 := Modifier{}
|
|
tm4.Create(Set{Name: set4Name, ElementType: IPv4Addr, Flags: []string{"interval"}})
|
|
const set6Name = "set6"
|
|
tm6 := Modifier{}
|
|
tm6.Create(Set{Name: set6Name, ElementType: IPv6Addr, Flags: []string{"interval", "timeout"}})
|
|
|
|
// Add elements to each set.
|
|
tm4.Create(SetElement{SetName: set4Name, Element: "192.0.2.0/24"})
|
|
tm6.Create(SetElement{SetName: set6Name, Element: "2001:db8::/64", Comment: `/// this is a comment on a set element \\\`})
|
|
|
|
// Update nftables and check what happened.
|
|
applyAndCheck(t, t.Name()+"/created4.golden", tbl4, tm4)
|
|
applyAndCheck(t, t.Name()+"/created6.golden", tbl6, tm6)
|
|
|
|
// Delete elements.
|
|
applyAndCheck(t, t.Name()+"/deleted4.golden", tbl4, tm4.Reverse())
|
|
applyAndCheck(t, t.Name()+"/deleted6.golden", tbl6, tm6.Reverse())
|
|
}
|
|
|
|
func TestReload(t *testing.T) {
|
|
defer testSetup(t)()
|
|
|
|
// Create a table with some stuff in it.
|
|
const tableName = "this_is_a_table"
|
|
tbl, err := NewTable(IPv4, tableName)
|
|
assert.NilError(t, err)
|
|
defer tbl.Close()
|
|
tm := Modifier{}
|
|
|
|
const bcName = "a_base_chain"
|
|
tm.Create(BaseChain{
|
|
Name: bcName,
|
|
ChainType: BaseChainTypeFilter,
|
|
Hook: BaseChainHookForward,
|
|
Priority: BaseChainPriorityFilter,
|
|
Policy: "accept",
|
|
})
|
|
tm.Create(Rule{Chain: bcName, Group: 0, Rule: []string{"counter"}})
|
|
|
|
const vmapName = "this_is_a_vmap"
|
|
tm.Create(Map{Name: vmapName, ElementType: Ifname.VMap()})
|
|
tm.Create(MapElement{MapName: vmapName, Key: "eth0", Value: "return"})
|
|
tm.Create(MapElement{MapName: vmapName, Key: "eth1", Value: "return", Comment: "{foo}"})
|
|
|
|
const setName = "this_is_a_set"
|
|
tm.Create(Set{Name: setName, ElementType: IPv4Addr, Flags: []string{"interval"}})
|
|
tm.Create(SetElement{SetName: setName, Element: "192.0.2.0/24", Comment: "}bar{"})
|
|
|
|
tm.Create(Map{Name: "dynamic_map", ElementType: IPv4Addr.MapTo(EtherAddr), Flags: []string{"dynamic", "timeout"}, Size: 1024, Timeout: 2*time.Minute + 30*time.Second + 500*time.Millisecond + 654*time.Microsecond})
|
|
tm.Create(Set{Name: "dynamic_set", ElementType: IPv4Addr, Flags: []string{"dynamic", "timeout"}, Size: 4096, Timeout: 5*time.Minute + 10*time.Second + 250*time.Millisecond + 123*time.Microsecond})
|
|
|
|
applyAndCheck(t, t.Name()+"/created.golden", tbl, tm)
|
|
|
|
// Delete the underlying nftables table.
|
|
deleteTable := func() {
|
|
t.Helper()
|
|
res := icmd.RunCommand("nft", "delete", "table", string(IPv4), tableName)
|
|
res.Assert(t, icmd.Success)
|
|
res = icmd.RunCommand("nft", "list", "ruleset")
|
|
res.Assert(t, icmd.Success)
|
|
assert.Check(t, is.Equal(res.Combined(), ""))
|
|
}
|
|
deleteTable()
|
|
|
|
// Reconstruct the nftables table.
|
|
err = tbl.Reload(t.Context())
|
|
assert.Check(t, err)
|
|
res := icmd.RunCommand("nft", "list", "table", string(tbl.Family()), tbl.Name())
|
|
res.Assert(t, icmd.Success)
|
|
golden.Assert(t, res.Combined(), t.Name()+"/created.golden")
|
|
|
|
// Delete again.
|
|
deleteTable()
|
|
|
|
// Check implicit/recovery reload - only deleting something that's gone missing
|
|
// from a vmap/set will trigger this.
|
|
tm = Modifier{}
|
|
tm.Delete(SetElement{SetName: setName, Element: "192.0.2.0/24"})
|
|
applyAndCheck(t, t.Name()+"/recovered.golden", tbl, tm)
|
|
}
|
|
|
|
func TestApplyMultipleModifiers(t *testing.T) {
|
|
defer testSetup(t)()
|
|
|
|
tbl, err := NewTable(IPv4, "this_is_a_table")
|
|
assert.NilError(t, err)
|
|
defer tbl.Close()
|
|
|
|
const chainName = "this_is_a_chain"
|
|
var tm1, tm2 Modifier
|
|
tm1.Create(Chain{Name: chainName})
|
|
tm1.Create(Rule{Chain: chainName, Rule: []string{"counter"}})
|
|
|
|
tm2.Create(Rule{Chain: chainName, Rule: []string{"drop"}})
|
|
// This rule should fail validation and trigger rollback.
|
|
tm2.Create(Rule{Chain: "bogus", Rule: []string{"counter"}})
|
|
tm2.Create(Rule{Chain: chainName, Rule: []string{"reject"}})
|
|
|
|
err = tbl.Apply(t.Context(), tm1, tm2)
|
|
assert.Check(t, err != nil, "Expected an error")
|
|
|
|
// Verify the apply was a no-op: the table should not exist yet.
|
|
res := icmd.RunCommand("nft", "list", "table", string(tbl.Family()), tbl.Name())
|
|
res.Assert(t, icmd.Expected{ExitCode: 1})
|
|
|
|
// Verify no traces of the failed apply remain in memory.
|
|
reloadAndCheck(t, t.Name()+"/empty.golden", tbl)
|
|
|
|
// A subsequent valid apply should still succeed.
|
|
var tm3 Modifier
|
|
tm3.Create(Rule{Chain: chainName, Rule: []string{"accept"}})
|
|
applyAndCheck(t, t.Name()+"/created.golden", tbl, tm1, tm3)
|
|
}
|
|
|
|
func TestNetdevChain(t *testing.T) {
|
|
defer testSetup(t)()
|
|
|
|
tbl, err := NewTable(Netdev, "testtable")
|
|
assert.NilError(t, err)
|
|
defer tbl.Close()
|
|
tm := Modifier{}
|
|
|
|
const bcName = "this_is_a_netdev_chain"
|
|
tm.Create(BaseChain{
|
|
Name: bcName,
|
|
ChainType: BaseChainTypeFilter,
|
|
Hook: BaseChainHookIngress,
|
|
Device: "lo",
|
|
Priority: -123,
|
|
Policy: BaseChainPolicyAccept,
|
|
})
|
|
tm.Create(Rule{Chain: bcName, Rule: []string{"accept"}})
|
|
applyAndCheck(t, t.Name()+"/created.golden", tbl, tm)
|
|
|
|
icmd.RunCommand("nft", "flush", "ruleset").Assert(t, icmd.Success)
|
|
err = tbl.Reload(t.Context())
|
|
assert.Check(t, err)
|
|
res := icmd.RunCommand("nft", "list", "table", string(tbl.Family()), tbl.Name())
|
|
res.Assert(t, icmd.Success)
|
|
golden.Assert(t, res.Combined(), t.Name()+"/created.golden")
|
|
}
|
|
|
|
func TestValidation(t *testing.T) {
|
|
testcases := []struct {
|
|
name string
|
|
cmds []command
|
|
expErr string
|
|
}{
|
|
// BaseChain
|
|
{
|
|
name: "create with missing base chain name",
|
|
cmds: []command{
|
|
{obj: BaseChain{ChainType: BaseChainTypeNAT, Hook: BaseChainHookPostrouting, Priority: BaseChainPrioritySrcNAT}},
|
|
},
|
|
expErr: "base chain must have a name",
|
|
},
|
|
{
|
|
name: "create with missing base chain type",
|
|
cmds: []command{
|
|
{obj: BaseChain{Name: "achain", Hook: BaseChainHookPostrouting, Priority: BaseChainPrioritySrcNAT}},
|
|
},
|
|
expErr: "chain 'achain': fields ChainType and Hook are required",
|
|
},
|
|
{
|
|
name: "create with missing base chain hook",
|
|
cmds: []command{
|
|
{obj: BaseChain{Name: "achain", ChainType: BaseChainTypeNAT, Priority: BaseChainPrioritySrcNAT}},
|
|
},
|
|
expErr: "chain 'achain': fields ChainType and Hook are required",
|
|
},
|
|
{
|
|
name: "delete non-empty base chain",
|
|
cmds: []command{
|
|
{obj: BaseChain{
|
|
Name: "achain", ChainType: BaseChainTypeNAT, Hook: BaseChainHookPostrouting, Priority: BaseChainPrioritySrcNAT,
|
|
}},
|
|
{obj: Rule{Chain: "achain", Group: 0, Rule: []string{"counter"}}},
|
|
{
|
|
obj: BaseChain{
|
|
Name: "achain", ChainType: BaseChainTypeNAT, Hook: BaseChainHookPostrouting, Priority: BaseChainPrioritySrcNAT,
|
|
},
|
|
delete: true,
|
|
},
|
|
},
|
|
expErr: "cannot delete chain 'achain', it is not empty",
|
|
},
|
|
// Chain
|
|
{
|
|
name: "duplicate chain",
|
|
cmds: []command{
|
|
{obj: Chain{Name: "achain"}},
|
|
{obj: Chain{Name: "achain"}},
|
|
},
|
|
expErr: "already exists",
|
|
},
|
|
{
|
|
name: "delete missing chain",
|
|
cmds: []command{
|
|
{obj: Chain{Name: "achain"}, delete: true},
|
|
},
|
|
expErr: "does not exist",
|
|
},
|
|
{
|
|
name: "missing chain name",
|
|
cmds: []command{
|
|
{obj: Chain{}},
|
|
},
|
|
expErr: "chain must have a name",
|
|
},
|
|
{
|
|
name: "delete non-empty chain",
|
|
cmds: []command{
|
|
{obj: Chain{Name: "achain"}},
|
|
{obj: Rule{Chain: "achain", Rule: []string{"counter"}}},
|
|
{obj: Chain{Name: "achain"}, delete: true},
|
|
},
|
|
expErr: "cannot delete chain 'achain', it is not empty",
|
|
},
|
|
// Rule
|
|
{
|
|
name: "bad rule",
|
|
cmds: []command{
|
|
{obj: Chain{Name: "achain"}},
|
|
{obj: Rule{Chain: "achain", Rule: []string{"this is nonsense"}}},
|
|
},
|
|
expErr: "syntax error",
|
|
},
|
|
{
|
|
name: "duplicate rule",
|
|
cmds: []command{
|
|
{obj: Chain{Name: "achain"}},
|
|
{obj: Rule{Chain: "achain", Rule: []string{"counter"}}},
|
|
{obj: Rule{Chain: "achain", Rule: []string{"counter"}}},
|
|
},
|
|
expErr: "rule exists",
|
|
},
|
|
{
|
|
name: "delete missing rule",
|
|
cmds: []command{
|
|
{obj: Chain{Name: "achain"}},
|
|
{obj: Rule{Chain: "achain", Rule: []string{"counter"}}, delete: true},
|
|
},
|
|
expErr: "does not exist",
|
|
},
|
|
{
|
|
name: "duplicate rule delete",
|
|
cmds: []command{
|
|
{obj: Chain{Name: "achain"}},
|
|
{obj: Rule{Chain: "achain", Rule: []string{"counter"}}},
|
|
{obj: Rule{Chain: "achain", Rule: []string{"counter"}}, delete: true},
|
|
{obj: Rule{Chain: "achain", Rule: []string{"counter"}}, delete: true},
|
|
},
|
|
expErr: "does not exist",
|
|
},
|
|
{
|
|
name: "create rule with missing chain name",
|
|
cmds: []command{
|
|
{obj: Chain{Name: "achain"}},
|
|
{obj: Rule{Rule: []string{"counter"}}},
|
|
},
|
|
expErr: "chain '' does not exist",
|
|
},
|
|
{
|
|
name: "delete rule with missing chain name",
|
|
cmds: []command{
|
|
{obj: Chain{Name: "achain"}},
|
|
{obj: Rule{Rule: []string{"counter"}}, delete: true},
|
|
},
|
|
expErr: "chain '' does not exist",
|
|
},
|
|
{
|
|
name: "create rule with nonexistent chain",
|
|
cmds: []command{
|
|
{obj: Rule{Chain: "achain", Rule: []string{"counter"}}},
|
|
},
|
|
expErr: "chain 'achain' does not exist",
|
|
},
|
|
{
|
|
name: "delete rule with nonexistent chain",
|
|
cmds: []command{
|
|
{obj: Rule{Chain: "achain", Rule: []string{"counter"}}, delete: true},
|
|
},
|
|
expErr: "chain 'achain' does not exist",
|
|
},
|
|
{
|
|
name: "create rule with no rule",
|
|
cmds: []command{
|
|
{obj: Chain{Name: "achain"}},
|
|
{obj: Rule{Chain: "achain"}},
|
|
},
|
|
expErr: "cannot add empty rule",
|
|
},
|
|
{
|
|
name: "delete rule with no rule",
|
|
cmds: []command{
|
|
{obj: Chain{Name: "achain"}},
|
|
{obj: Rule{Chain: "achain"}, delete: true},
|
|
},
|
|
expErr: "cannot delete empty rule",
|
|
},
|
|
{
|
|
name: "bad rule mid-sequence",
|
|
cmds: []command{
|
|
{obj: Chain{Name: "achain"}},
|
|
{obj: Rule{Chain: "achain", Rule: []string{"counter"}}},
|
|
{obj: Rule{Chain: "achain", Rule: []string{"counter"}}, delete: true},
|
|
{obj: Rule{Chain: "achain"}},
|
|
{obj: Rule{Chain: "achain", Rule: []string{"counter"}}},
|
|
},
|
|
expErr: "chain 'achain', cannot add empty rule",
|
|
},
|
|
// Map (verdict)
|
|
{
|
|
name: "duplicate map",
|
|
cmds: []command{
|
|
{obj: Map{Name: "avmap", ElementType: Ifname.VMap()}},
|
|
{obj: Map{Name: "avmap", ElementType: Ifname.VMap()}},
|
|
},
|
|
expErr: "map 'avmap' already exists",
|
|
},
|
|
{
|
|
name: "delete nonexistent map",
|
|
cmds: []command{
|
|
{obj: Map{Name: "avmap", ElementType: Ifname.VMap()}, delete: true},
|
|
},
|
|
expErr: "cannot delete map 'avmap', it does not exist",
|
|
},
|
|
{
|
|
name: "missing map name",
|
|
cmds: []command{{obj: Map{ElementType: Ifname.VMap()}}},
|
|
expErr: "map must have a name",
|
|
},
|
|
{
|
|
name: "missing map element type",
|
|
cmds: []command{{obj: Map{Name: "avmap"}}},
|
|
expErr: "map 'avmap' has no element type",
|
|
},
|
|
{
|
|
name: "delete non-empty map",
|
|
cmds: []command{
|
|
{obj: Map{Name: "avmap", ElementType: Ifname.VMap()}},
|
|
{obj: MapElement{MapName: "avmap", Key: "eth0", Value: "drop"}},
|
|
{obj: Map{Name: "avmap", ElementType: Ifname.VMap()}, delete: true},
|
|
},
|
|
expErr: "cannot delete map 'avmap', it contains 1 elements",
|
|
},
|
|
// MapElement
|
|
{
|
|
name: "duplicate map element",
|
|
cmds: []command{
|
|
{obj: Map{Name: "avmap", ElementType: Ifname.VMap()}},
|
|
{obj: MapElement{MapName: "avmap", Key: "eth0", Value: "drop"}},
|
|
{obj: MapElement{MapName: "avmap", Key: "eth0", Value: "drop"}},
|
|
},
|
|
expErr: "map 'avmap' already contains element 'eth0'",
|
|
},
|
|
{
|
|
name: "add to map that does not exist",
|
|
cmds: []command{
|
|
{obj: MapElement{MapName: "avmap", Key: "eth0", Value: "drop"}},
|
|
},
|
|
expErr: "cannot add to map 'avmap', it does not exist",
|
|
},
|
|
{
|
|
name: "delete nonexistent map element",
|
|
cmds: []command{
|
|
{obj: Map{Name: "avmap", ElementType: Ifname.VMap()}},
|
|
{obj: MapElement{MapName: "avmap", Key: "eth0", Value: "drop"}, delete: true},
|
|
},
|
|
expErr: "map 'avmap' does not contain element 'eth0'",
|
|
},
|
|
{
|
|
name: "map element with no named map",
|
|
cmds: []command{
|
|
{obj: Map{Name: "avmap", ElementType: Ifname.VMap()}},
|
|
{obj: MapElement{Key: "eth0", Value: "drop"}},
|
|
},
|
|
expErr: "cannot add element to unnamed map",
|
|
},
|
|
{
|
|
name: "map element with no key",
|
|
cmds: []command{
|
|
{obj: Map{Name: "avmap", ElementType: Ifname.VMap()}},
|
|
{obj: MapElement{MapName: "avmap", Value: "drop"}},
|
|
},
|
|
expErr: "cannot add to map 'avmap', element must have key and value",
|
|
},
|
|
{
|
|
name: "map element with no value",
|
|
cmds: []command{
|
|
{obj: Map{Name: "avmap", ElementType: Ifname.VMap()}},
|
|
{obj: MapElement{MapName: "avmap", Key: "eth0"}},
|
|
},
|
|
expErr: "cannot add to map 'avmap', element must have key and value",
|
|
},
|
|
{
|
|
name: "map element with newline in comment",
|
|
cmds: []command{
|
|
{obj: Map{Name: "avmap", ElementType: Ifname.VMap()}},
|
|
{obj: MapElement{MapName: "avmap", Key: "eth0", Value: "drop", Comment: "new\nline"}},
|
|
},
|
|
expErr: `map 'avmap' element 'eth0' comment contains "\n"`,
|
|
},
|
|
{
|
|
name: "map element with quote char in comment",
|
|
cmds: []command{
|
|
{obj: Map{Name: "avmap", ElementType: Ifname.VMap()}},
|
|
{obj: MapElement{MapName: "avmap", Key: "eth0", Value: "drop", Comment: `"quoted"`}},
|
|
},
|
|
expErr: `map 'avmap' element 'eth0' comment contains "\""`,
|
|
},
|
|
// Set
|
|
{
|
|
name: "duplicate set",
|
|
cmds: []command{
|
|
{obj: Set{Name: "aset", ElementType: IPv4Addr, Flags: []string{"interval"}}},
|
|
{obj: Set{Name: "aset", ElementType: IPv4Addr, Flags: []string{"interval"}}},
|
|
},
|
|
expErr: "set 'aset' already exists",
|
|
},
|
|
{
|
|
name: "delete nonexistent set",
|
|
cmds: []command{
|
|
{obj: Set{Name: "aset", ElementType: IPv4Addr, Flags: []string{"interval"}}, delete: true},
|
|
},
|
|
expErr: "cannot delete set 'aset', it does not exist",
|
|
},
|
|
{
|
|
name: "missing set name",
|
|
cmds: []command{
|
|
{obj: Set{ElementType: IPv4Addr, Flags: []string{"interval"}}},
|
|
},
|
|
expErr: "set must have a name",
|
|
},
|
|
{
|
|
name: "missing set element type",
|
|
cmds: []command{
|
|
{obj: Set{Name: "aset", Flags: []string{"interval"}}},
|
|
},
|
|
expErr: "set 'aset' must have a type",
|
|
},
|
|
{
|
|
name: "delete non-empty set",
|
|
cmds: []command{
|
|
{obj: Set{Name: "aset", ElementType: IPv4Addr, Flags: []string{"interval"}}},
|
|
{obj: SetElement{SetName: "aset", Element: "192.0.2.0/24"}},
|
|
{obj: Set{Name: "aset", ElementType: IPv4Addr, Flags: []string{"interval"}}, delete: true},
|
|
},
|
|
expErr: "cannot delete set 'aset', it contains 1 elements",
|
|
},
|
|
// SetElement
|
|
{
|
|
name: "duplicate set element",
|
|
cmds: []command{
|
|
{obj: Set{Name: "aset", ElementType: IPv4Addr, Flags: []string{"interval"}}},
|
|
{obj: SetElement{SetName: "aset", Element: "192.0.2.0/24"}},
|
|
{obj: SetElement{SetName: "aset", Element: "192.0.2.0/24"}},
|
|
},
|
|
expErr: "set 'aset' already contains element '192.0.2.0/24'",
|
|
},
|
|
{
|
|
name: "delete nonexistent set element",
|
|
cmds: []command{
|
|
{obj: Set{Name: "aset", ElementType: IPv4Addr, Flags: []string{"interval"}}},
|
|
{obj: SetElement{SetName: "aset", Element: "192.0.2.0/24"}, delete: true},
|
|
},
|
|
expErr: "cannot delete '192.0.2.0/24' from set 'aset', it does not exist",
|
|
},
|
|
{
|
|
name: "add set element to unnamed set",
|
|
cmds: []command{
|
|
{obj: Set{Name: "aset", ElementType: IPv4Addr, Flags: []string{"interval"}}},
|
|
{obj: SetElement{Element: "192.0.2.0/24"}},
|
|
},
|
|
expErr: "cannot add to set '', it does not exist",
|
|
},
|
|
{
|
|
name: "add set element with no element",
|
|
cmds: []command{
|
|
{obj: Set{Name: "aset", ElementType: IPv4Addr, Flags: []string{"interval"}}},
|
|
{obj: SetElement{SetName: "aset"}},
|
|
},
|
|
expErr: "cannot add to set 'aset', element not specified",
|
|
},
|
|
{
|
|
name: "mismatched set element type",
|
|
cmds: []command{
|
|
{obj: Set{Name: "aset", ElementType: IPv4Addr, Flags: []string{"interval"}}},
|
|
{obj: SetElement{SetName: "aset", Element: "2001:db8::/64"}},
|
|
},
|
|
expErr: "Address family for hostname not supported",
|
|
},
|
|
{
|
|
name: "set element with newline in comment",
|
|
cmds: []command{
|
|
{obj: Set{Name: "aset", ElementType: IPv4Addr, Flags: []string{"interval"}}},
|
|
{obj: SetElement{SetName: "aset", Element: "192.0.2.0/24", Comment: "new\nline"}},
|
|
},
|
|
expErr: `set 'aset' element '192.0.2.0/24' comment contains "\n"`,
|
|
},
|
|
{
|
|
name: "set element with quote char in comment",
|
|
cmds: []command{
|
|
{obj: Set{Name: "aset", ElementType: IPv4Addr, Flags: []string{"interval"}}},
|
|
{obj: SetElement{SetName: "aset", Element: "192.0.2.0/24", Comment: `"quoted"`}},
|
|
},
|
|
expErr: `set 'aset' element '192.0.2.0/24' comment contains "\""`,
|
|
},
|
|
}
|
|
|
|
testName := t.Name()
|
|
for _, tc := range testcases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
defer testSetup(t)()
|
|
tbl, err := NewTable(IPv4, "tablename")
|
|
assert.NilError(t, err)
|
|
defer tbl.Close()
|
|
tm := Modifier{cmds: tc.cmds}
|
|
err = tbl.Apply(t.Context(), tm)
|
|
assert.Check(t, err != nil, "expected error containing '%s'", tc.expErr)
|
|
assert.Check(t, is.ErrorContains(err, tc.expErr))
|
|
// Check the table wasn't created.
|
|
res := icmd.RunCommand("nft", "list", "table", string(IPv4), "tablename")
|
|
res.Assert(t, icmd.Expected{ExitCode: 1})
|
|
// Check the empty table can be created (the Table structure is still healthy).
|
|
applyAndCheck(t, testName+"/empty.golden", tbl, Modifier{})
|
|
})
|
|
}
|
|
}
|