userdata: Add TLV parser/serialized for rule user data
This TLV format is compatible with the one used by libnftables. Hence with this change, we can now de/encode comments and other user data information which is compatible with the Netfilter command line tooling. Signed-off-by: Steffen Vogel <post@steffenvogel.de>
This commit is contained in:
parent
971247e1b2
commit
1510be9a55
|
@ -0,0 +1,94 @@
|
|||
// Copyright 2018 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 userdata implements a TLV parser/serializer for libnftables-compatible comments
|
||||
package userdata
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
type Type byte
|
||||
|
||||
// TLV type values are defined in:
|
||||
// https://git.netfilter.org/iptables/tree/iptables/nft.c?id=73611d5582e72367a698faf1b5301c836e981465#n1659
|
||||
const (
|
||||
TypeComment Type = iota
|
||||
TypeEbtablesPolicy
|
||||
|
||||
TypesCount
|
||||
)
|
||||
|
||||
func Append(udata []byte, typ Type, data []byte) []byte {
|
||||
udata = append(udata, byte(typ), byte(len(data)))
|
||||
udata = append(udata, data...)
|
||||
|
||||
return udata
|
||||
}
|
||||
|
||||
func Get(udata []byte, styp Type) []byte {
|
||||
for {
|
||||
if len(udata) < 2 {
|
||||
break
|
||||
}
|
||||
|
||||
typ := Type(udata[0])
|
||||
length := int(udata[1])
|
||||
data := udata[2 : 2+length]
|
||||
|
||||
if styp == typ {
|
||||
return data
|
||||
}
|
||||
|
||||
if len(udata) < 2+length {
|
||||
break
|
||||
} else {
|
||||
udata = udata[2+length:]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func AppendUint32(udata []byte, typ Type, num uint32) []byte {
|
||||
data := binary.LittleEndian.AppendUint32(nil, num)
|
||||
|
||||
return Append(udata, typ, data)
|
||||
}
|
||||
|
||||
func GetUint32(udata []byte, typ Type) (uint32, bool) {
|
||||
data := Get(udata, typ)
|
||||
if data == nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return binary.LittleEndian.Uint32(data), true
|
||||
}
|
||||
|
||||
func AppendString(udata []byte, typ Type, str string) []byte {
|
||||
data := append([]byte(str), 0)
|
||||
return Append(udata, typ, data)
|
||||
}
|
||||
|
||||
func GetString(udata []byte, typ Type) (string, bool) {
|
||||
data := Get(udata, typ)
|
||||
if data == nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
data, _ = bytes.CutSuffix(data, []byte{0})
|
||||
|
||||
return string(data), true
|
||||
}
|
|
@ -0,0 +1,233 @@
|
|||
// Copyright 2018 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 userdata_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/google/nftables"
|
||||
"github.com/google/nftables/internal/nftest"
|
||||
"github.com/google/nftables/userdata"
|
||||
)
|
||||
|
||||
var enableSysTests = flag.Bool("run_system_tests", false, "Run tests that operate against the live kernel")
|
||||
|
||||
type nftCliMetainfo struct {
|
||||
Version string `json:"version,omitempty"`
|
||||
ReleaseName string `json:"release_name,omitempty"`
|
||||
JSONSchemaVersion int `json:"json_schema_version,omitempty"`
|
||||
}
|
||||
|
||||
type nftCliTable struct {
|
||||
Family string `json:"family,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Handle int `json:"handle,omitempty"`
|
||||
}
|
||||
|
||||
type nftCliChain struct {
|
||||
Family string `json:"family,omitempty"`
|
||||
Table string `json:"table,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Handle int `json:"handle,omitempty"`
|
||||
}
|
||||
|
||||
type nftCliExpr struct{}
|
||||
|
||||
type nftCliRule struct {
|
||||
Family string `json:"family,omitempty"`
|
||||
Table string `json:"table,omitempty"`
|
||||
Chain string `json:"chain,omitempty"`
|
||||
Handle int `json:"handle,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
Expr []nftCliExpr `json:"expr"`
|
||||
}
|
||||
|
||||
type nftCommand struct {
|
||||
Ruleset interface{} `json:"ruleset"`
|
||||
Table *nftCliTable `json:"table,omitempty"`
|
||||
Chain *nftCliChain `json:"chain,omitempty"`
|
||||
Rule *nftCliRule `json:"rule,omitempty"`
|
||||
}
|
||||
|
||||
type nftCliObject struct {
|
||||
Metainfo *nftCliMetainfo `json:"metainfo,omitempty"`
|
||||
Table *nftCliTable `json:"table,omitempty"`
|
||||
Chain *nftCliChain `json:"chain,omitempty"`
|
||||
Rule *nftCliRule `json:"rule,omitempty"`
|
||||
Add *nftCommand `json:"add,omitempty"`
|
||||
Flush *nftCommand `json:"flush,omitempty"`
|
||||
}
|
||||
|
||||
type nftCli struct {
|
||||
Nftables []nftCliObject `json:"nftables"`
|
||||
}
|
||||
|
||||
func TestCommentInteropGo2Cli(t *testing.T) {
|
||||
wantComment := "my comment"
|
||||
|
||||
// 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)
|
||||
|
||||
c.FlushRuleset()
|
||||
|
||||
table := c.AddTable(&nftables.Table{
|
||||
Name: "userdata-table",
|
||||
Family: nftables.TableFamilyIPv4,
|
||||
})
|
||||
|
||||
chain := c.AddChain(&nftables.Chain{
|
||||
Name: "userdata-chain",
|
||||
Table: table,
|
||||
})
|
||||
|
||||
c.AddRule(&nftables.Rule{
|
||||
Table: table,
|
||||
Chain: chain,
|
||||
UserData: userdata.AppendString(nil, userdata.TypeComment, wantComment),
|
||||
})
|
||||
|
||||
if err := c.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
out := bytes.NewBuffer(nil)
|
||||
d := exec.Command("nft", "-j", "list", "table", "userdata-table")
|
||||
d.Stdout = out
|
||||
if err := d.Run(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var outJson nftCli
|
||||
if err := json.Unmarshal(out.Bytes(), &outJson); err != nil {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
found := 0
|
||||
for _, e := range outJson.Nftables {
|
||||
if e.Rule == nil || e.Rule.Handle == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if e.Rule.Comment != wantComment {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
found++
|
||||
}
|
||||
|
||||
if found != 1 {
|
||||
t.Fatalf("found %d rules", found)
|
||||
}
|
||||
|
||||
c.DelTable(table)
|
||||
|
||||
if err := c.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommentInteropCli2Go(t *testing.T) {
|
||||
wantComment := "my comment"
|
||||
|
||||
inJson := nftCli{
|
||||
Nftables: []nftCliObject{
|
||||
{
|
||||
Metainfo: &nftCliMetainfo{
|
||||
JSONSchemaVersion: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Flush: &nftCommand{
|
||||
Ruleset: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
Add: &nftCommand{
|
||||
Table: &nftCliTable{
|
||||
Family: "ip",
|
||||
Name: "userdata-table",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Add: &nftCommand{
|
||||
Chain: &nftCliChain{
|
||||
Family: "ip",
|
||||
Name: "userdata-chain",
|
||||
Table: "userdata-table",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Add: &nftCommand{
|
||||
Rule: &nftCliRule{
|
||||
Family: "ip",
|
||||
Table: "userdata-table",
|
||||
Chain: "userdata-chain",
|
||||
Comment: wantComment,
|
||||
Expr: []nftCliExpr{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
in := bytes.NewBuffer(nil)
|
||||
if err := json.NewEncoder(in).Encode(inJson); err != nil {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
d := exec.Command("nft", "-j", "-f", "-")
|
||||
d.Stdin = in
|
||||
if err := d.Run(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
table := &nftables.Table{
|
||||
Name: "userdata-table",
|
||||
Family: nftables.TableFamilyIPv4,
|
||||
}
|
||||
|
||||
chain := &nftables.Chain{
|
||||
Name: "userdata-chain",
|
||||
Table: table,
|
||||
}
|
||||
|
||||
rules, err := c.GetRules(table, chain)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(rules) != 1 {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if comment, ok := userdata.GetString(rules[0].UserData, userdata.TypeComment); !ok {
|
||||
t.Fatalf("failed to find comment")
|
||||
} else if comment != wantComment {
|
||||
t.Fatalf("comment mismatch %q != %q", comment, wantComment)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
// Copyright 2018 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 userdata_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/google/nftables"
|
||||
"github.com/google/nftables/userdata"
|
||||
)
|
||||
|
||||
func TestUserDataComment(t *testing.T) {
|
||||
r := nftables.Rule{}
|
||||
|
||||
wantComment := "this is my comment"
|
||||
want := []byte{
|
||||
byte(userdata.TypeComment), // Type
|
||||
byte(len(wantComment) + 1), // Length (including terminating null byte)
|
||||
}
|
||||
want = append(want, []byte(wantComment)...) // Payload
|
||||
want = append(want, 0) // Terminating null byte
|
||||
|
||||
r.UserData = userdata.AppendString(r.UserData, userdata.TypeComment, wantComment)
|
||||
|
||||
if !bytes.Equal(r.UserData, want) {
|
||||
t.Fatalf("UserData mismatch: %s != %s",
|
||||
hex.EncodeToString(r.UserData),
|
||||
hex.EncodeToString(want))
|
||||
}
|
||||
|
||||
if comment, ok := userdata.GetString(r.UserData, userdata.TypeComment); !ok {
|
||||
t.Fatalf("failed to get comment")
|
||||
} else if comment != wantComment {
|
||||
t.Fatalf("comment does not match: %s != %s", comment, wantComment)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUint32(t *testing.T) {
|
||||
// Define a custom type for storing a rule ID
|
||||
const TypeRuleID = userdata.TypesCount
|
||||
|
||||
r := nftables.Rule{}
|
||||
|
||||
wantRuleID := uint32(1234)
|
||||
want := []byte{byte(TypeRuleID), 4, 210, 4, 0, 0}
|
||||
|
||||
r.UserData = userdata.AppendUint32(r.UserData, TypeRuleID, wantRuleID)
|
||||
|
||||
if !bytes.Equal(r.UserData, want) {
|
||||
t.Fatalf("UserData mismatch: %x != %x", r.UserData, want)
|
||||
}
|
||||
|
||||
if ruleID, ok := userdata.GetUint32(r.UserData, TypeRuleID); !ok {
|
||||
t.Fatalf("failed to get id")
|
||||
} else if ruleID != wantRuleID {
|
||||
t.Fatalf("id mismatch")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue