diff --git a/userdata/userdata.go b/userdata/userdata.go new file mode 100644 index 0000000..611919a --- /dev/null +++ b/userdata/userdata.go @@ -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 +} diff --git a/userdata/userdata_cli_interop_test.go b/userdata/userdata_cli_interop_test.go new file mode 100644 index 0000000..10bcc20 --- /dev/null +++ b/userdata/userdata_cli_interop_test.go @@ -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) + } +} diff --git a/userdata/userdata_test.go b/userdata/userdata_test.go new file mode 100644 index 0000000..a2ea94a --- /dev/null +++ b/userdata/userdata_test.go @@ -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") + } +}