Skip to content

Commit

Permalink
Add support for handlers and named counters.
Browse files Browse the repository at this point in the history
Signed-off-by: Tobias Buner <buner@anapaya.net>
  • Loading branch information
bunert committed Aug 2, 2023
1 parent 166e132 commit 0085ba4
Show file tree
Hide file tree
Showing 15 changed files with 418 additions and 38 deletions.
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ support in the kernel and the `nft` executable.

To run the unit-tests, use this command from the project root:
```bash
./autmation/run-tests.sh --unit-test
./automation/run-tests.sh --unit-test
```

To run the integration-tests, use this command from the project root:
```bash
./autmation/run-tests.sh --integration-test
./automation/run-tests.sh --integration-test
```

## Help utilities
Expand Down
2 changes: 1 addition & 1 deletion nft/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
"fmt"
"testing"

assert "github.com/stretchr/testify/require"
"github.com/stretchr/testify/assert"

nftconfig "github.com/networkplumbing/go-nft/nft/config"
"github.com/networkplumbing/go-nft/nft/schema"
Expand Down
54 changes: 54 additions & 0 deletions nft/config/counter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* This file is part of the go-nft project
*
* 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.
*
* Copyright 2022 Red Hat, Inc.
*
*/

package config

import (
"github.com/networkplumbing/go-nft/nft/schema"
)

// AddCounter appends the given named counter to the nftable config.
// The rule is added without an explicit action (`add`).
// Adding multiple times the same named counter has no affect when the config is applied.
func (c *Config) AddCounter(counter *schema.NamedCounter) {
nftable := schema.Nftable{Counter: counter}
c.Nftables = append(c.Nftables, nftable)
}

// DeleteCounter appends a given rule to the nftable config with the `delete` action.
// Attempting to delete a non-existing named counter, results with a failure when the config is applied.
func (c *Config) DeleteCounter(counter *schema.NamedCounter) {
nftable := schema.Nftable{Delete: &schema.Objects{Counter: counter}}
c.Nftables = append(c.Nftables, nftable)
}

// LookupCounter searches the configuration for a matching counter and returns it.
// The counter is matched first by the table.
// Mutating the returned counter will result in mutating the configuration.
func (c *Config) LookupCounter(toFind *schema.NamedCounter) *schema.NamedCounter {
for _, nftable := range c.Nftables {
if counter := nftable.Counter; counter != nil {
match := counter.Table == toFind.Table && counter.Family == toFind.Family && counter.Name == toFind.Name
if match {
return counter
}
}
}
return nil
}
97 changes: 97 additions & 0 deletions nft/config/counter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* This file is part of the go-nft project
*
* 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.
*
* Copyright 2022 Red Hat, Inc.
*
*/

package config_test

import (
"fmt"
"testing"

assert "github.com/stretchr/testify/require"

"github.com/networkplumbing/go-nft/nft"
"github.com/networkplumbing/go-nft/nft/schema"
)

type CounterAction string

// Counter Actions
const (
CounterADD CounterAction = "add"
CounterDELETE CounterAction = "delete"
)

type counterActionFunc func(*nft.Config, *schema.NamedCounter)

const counterName = "test-counter"

func TestCounter(t *testing.T) {
testCounterActions(t)
testCounterLookup(t)
}

func testCounterActions(t *testing.T) {
actions := map[CounterAction]counterActionFunc{
CounterADD: func(c *nft.Config, t *schema.NamedCounter) { c.AddCounter(t) },
CounterDELETE: func(c *nft.Config, t *schema.NamedCounter) { c.DeleteCounter(t) },
}
table := nft.NewTable(tableName, nft.FamilyIP)
counter := nft.NewCounter(table, counterName)

for action, actionFunc := range actions {
testName := fmt.Sprintf("%s counter", string(action))

t.Run(testName, func(t *testing.T) {
config := nft.NewConfig()
actionFunc(config, counter)

serializedConfig, err := config.ToJSON()
assert.NoError(t, err)

counterArgs := fmt.Sprintf(`"family":%q,"table":%q,"name":%q`, table.Family, table.Name, counterName)
var expected []byte
if action == CounterADD {
expected = []byte(fmt.Sprintf(`{"nftables":[{"counter":{%s}}]}`, counterArgs))
} else {
expected = []byte(fmt.Sprintf(`{"nftables":[{%q:{"counter":{%s}}}]}`, action, counterArgs))
}

assert.Equal(t, string(expected), string(serializedConfig))
})
}
}

func testCounterLookup(t *testing.T) {
config := nft.NewConfig()
table_inet := nft.NewTable("table-inet", nft.FamilyINET)
config.AddTable(table_inet)

counter := nft.NewCounter(table_inet, "test-counter")
config.AddCounter(counter)

t.Run("Lookup an existing named counter", func(t *testing.T) {
c := config.LookupCounter(counter)
assert.Equal(t, counter, c)
})

t.Run("Lookup a missing named counter", func(t *testing.T) {
c := nft.NewCounter(table_inet, "counter-na")
assert.Nil(t, config.LookupCounter(c))
})
}
94 changes: 80 additions & 14 deletions nft/config/rule_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func TestRule(t *testing.T) {

testAddRuleWithRowExpression(t)
testAddRuleWithCounter(t)
testAddRuleWithNamedCounter(t)
testAddRuleWithNAT(t)

testRuleLookup(t)
Expand All @@ -67,14 +68,14 @@ func testAddRuleWithRowExpression(t *testing.T) {
serializedConfig, err := config.ToJSON()
assert.NoError(t, err)

expectedConfig := buildSerializedConfig(ruleADD, serializedStatements, nil, comment)
expectedConfig := buildSerializedConfig(ruleADD, serializedStatements, nil, nil, comment)
assert.Equal(t, string(expectedConfig), string(serializedConfig))
})

t.Run("Add rule with a row expression, check deserialization", func(t *testing.T) {
statements, serializedStatements := matchWithRowExpression()

serializedConfig := buildSerializedConfig(ruleADD, serializedStatements, nil, comment)
serializedConfig := buildSerializedConfig(ruleADD, serializedStatements, nil, nil, comment)

var deserializedConfig nft.Config
assert.NoError(t, json.Unmarshal(serializedConfig, &deserializedConfig))
Expand Down Expand Up @@ -103,14 +104,14 @@ func testAddRuleWithMatchAndVerdict(t *testing.T) {
serializedConfig, err := config.ToJSON()
assert.NoError(t, err)

expectedConfig := buildSerializedConfig(ruleADD, serializedStatements, nil, comment)
expectedConfig := buildSerializedConfig(ruleADD, serializedStatements, nil, nil, comment)
assert.Equal(t, string(expectedConfig), string(serializedConfig))
})

t.Run("Add rule with match and verdict, check deserialization", func(t *testing.T) {
statements, serializedStatements := matchSrcIP4withReturnVerdict()

serializedConfig := buildSerializedConfig(ruleADD, serializedStatements, nil, comment)
serializedConfig := buildSerializedConfig(ruleADD, serializedStatements, nil, nil, comment)

var deserializedConfig nft.Config
assert.NoError(t, json.Unmarshal(serializedConfig, &deserializedConfig))
Expand All @@ -137,15 +138,21 @@ func testDeleteRule(t *testing.T) {
serializedConfig, err := config.ToJSON()
assert.NoError(t, err)

expectedConfig := buildSerializedConfig(ruleDELETE, "", &handleID, "")
expectedConfig := buildSerializedConfig(ruleDELETE, "", nil, &handleID, "")
assert.Equal(t, string(expectedConfig), string(serializedConfig))
})
}

func buildSerializedConfig(action ruleAction, serializedStatements string, handle *int, comment string) []byte {
func buildSerializedConfig(
action ruleAction,
serializedRuleStatements string,
namedCounter *schema.NamedCounter,
handle *int,
comment string,
) []byte {
ruleArgs := fmt.Sprintf(`"family":%q,"table":%q,"chain":%q`, nft.FamilyIP, tableName, chainName)
if serializedStatements != "" {
ruleArgs += "," + serializedStatements
if serializedRuleStatements != "" {
ruleArgs += "," + serializedRuleStatements
}
if handle != nil {
ruleArgs += fmt.Sprintf(`,"handle":%d`, *handle)
Expand All @@ -154,11 +161,20 @@ func buildSerializedConfig(action ruleAction, serializedStatements string, handl
ruleArgs += fmt.Sprintf(`,"comment":%q`, comment)
}

serialzedCounter := ""
if namedCounter != nil {
counterArgs := fmt.Sprintf(`"family":%q,"table":%q,"name":%q`, nft.FamilyIP, tableName, namedCounter.Name)
serialzedCounter = fmt.Sprintf(`{"counter":{%s}},`, counterArgs)
}

var config string
if action == ruleADD {
config = fmt.Sprintf(`{"nftables":[{"rule":{%s}}]}`, ruleArgs)
config = fmt.Sprintf(`{"nftables":[%s{"rule":{%s}}]}`, serialzedCounter, ruleArgs)
} else {
config = fmt.Sprintf(`{"nftables":[{%q:{"rule":{%s}}}]}`, action, ruleArgs)
if namedCounter != nil {
serialzedCounter = fmt.Sprintf(`{%q:{%s}},`, action, serialzedCounter)
}
config = fmt.Sprintf(`{"nftables":[%s{%q:{"rule":{%s}}}]}`, serialzedCounter, action, ruleArgs)
}
return []byte(config)
}
Expand Down Expand Up @@ -301,12 +317,12 @@ func testAddRuleWithCounter(t *testing.T) {
serializedConfig, err := config.ToJSON()
assert.NoError(t, err)

expectedConfig := buildSerializedConfig(ruleADD, serializedStatements, nil, comment)
expectedConfig := buildSerializedConfig(ruleADD, serializedStatements, nil, nil, comment)
assert.JSONEq(t, string(expectedConfig), string(serializedConfig))
})

t.Run("Add rule with counter, check deserialization", func(t *testing.T) {
serializedConfig := buildSerializedConfig(ruleADD, serializedStatements, nil, comment)
serializedConfig := buildSerializedConfig(ruleADD, serializedStatements, nil, nil, comment)

var deserializedConfig nft.Config
assert.NoError(t, json.Unmarshal(serializedConfig, &deserializedConfig))
Expand All @@ -332,6 +348,56 @@ func counterStatements() ([]schema.Statement, string) {
return statements, serializedStatements
}

func testAddRuleWithNamedCounter(t *testing.T) {
const comment = "mycomment"
const counterName = "mycounter"

table := nft.NewTable(tableName, nft.FamilyIP)
chain := nft.NewRegularChain(table, chainName)
counter := nft.NewCounter(table, counterName)

statements, serializedStatements := namedCounterStatements(counterName)
rule := nft.NewRule(table, chain, statements, nil, nil, comment)

t.Run("Add rule with named counter, check serialization", func(t *testing.T) {
config := nft.NewConfig()
config.AddCounter(counter)
config.AddRule(rule)

serializedConfig, err := config.ToJSON()
assert.NoError(t, err)

expectedConfig := buildSerializedConfig(ruleADD, serializedStatements, counter, nil, comment)
assert.JSONEq(t, string(expectedConfig), string(serializedConfig))
})

t.Run("Add rule with named counter, check deserialization", func(t *testing.T) {
serializedConfig := buildSerializedConfig(ruleADD, serializedStatements, counter, nil, comment)

var deserializedConfig nft.Config
assert.NoError(t, json.Unmarshal(serializedConfig, &deserializedConfig))

expectedConfig := nft.NewConfig()
expectedConfig.AddCounter(counter)
expectedConfig.AddRule(rule)

assert.Equal(t, expectedConfig, &deserializedConfig)
})
}

func namedCounterStatements(name string) ([]schema.Statement, string) {
statements := []schema.Statement{{
Counter: &schema.Counter{
Name: name,
},
}}

expectedCounter := fmt.Sprintf(`"counter": "%s"`, name)
serializedStatements := fmt.Sprintf(`"expr":[{%s}]`, expectedCounter)

return statements, serializedStatements
}

func testAddRuleWithNAT(t *testing.T) {
tableTests := []struct {
typeName string
Expand Down Expand Up @@ -367,7 +433,7 @@ func testSerializationWith(t *testing.T, createStatements func() ([]schema.State
serializedConfig, err := config.ToJSON()
assert.NoError(t, err)

expectedConfig := buildSerializedConfig(ruleADD, serializedStatements, nil, comment)
expectedConfig := buildSerializedConfig(ruleADD, serializedStatements, nil, nil, comment)
assert.Equal(t, string(expectedConfig), string(serializedConfig))
}

Expand All @@ -379,7 +445,7 @@ func testDeserializationWith(t *testing.T, createStatements func() ([]schema.Sta

statements, serializedStatements := createStatements()

serializedConfig := buildSerializedConfig(ruleADD, serializedStatements, nil, comment)
serializedConfig := buildSerializedConfig(ruleADD, serializedStatements, nil, nil, comment)

var deserializedConfig nft.Config
assert.NoError(t, json.Unmarshal(serializedConfig, &deserializedConfig))
Expand Down
35 changes: 35 additions & 0 deletions nft/counter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* This file is part of the go-nft project
*
* 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.
*
* Copyright 2022 Red Hat, Inc.
*
*/

package nft

import (
"github.com/networkplumbing/go-nft/nft/schema"
)

// NewCounter returns a new schema counter structure for a named counter.
func NewCounter(table *schema.Table, name string) *schema.NamedCounter {
c := &schema.NamedCounter{
Family: table.Family,
Table: table.Name,
Name: name,
}

return c
}
Loading

0 comments on commit 0085ba4

Please sign in to comment.