From 7f3100d21e62da449ea41f505a149649fabefb06 Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Mon, 28 Oct 2024 09:46:57 +0000 Subject: [PATCH] add flowtable object Add flowtable object based on https://www.netfilter.org/projects/nftables/manpage.html --- README.md | 1 + fake.go | 71 +++++++++++++++++++++++++++++++----- fake_test.go | 9 ++++- nftables_test.go | 13 ++++--- objects.go | 77 +++++++++++++++++++++++++++++++++++++++ objects_test.go | 95 ++++++++++++++++++++++++++++++++++++++++++++++++ types.go | 27 ++++++++++++++ 7 files changed, 277 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 794b15b..0cb6313 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,7 @@ The `Transaction` methods take arguments of type `knftables.Object`. The currently-supported objects are: - `Table` +- `Flowtable` - `Chain` - `Rule` - `Set` diff --git a/fake.go b/fake.go index 4e87713..6f209f9 100644 --- a/fake.go +++ b/fake.go @@ -51,6 +51,9 @@ type Fake struct { type FakeTable struct { Table + // Flowtables contains the table's flowtables, keyed by name + Flowtables map[string]*FakeFlowtable + // Chains contains the table's chains, keyed by name Chains map[string]*FakeChain @@ -61,6 +64,11 @@ type FakeTable struct { Maps map[string]*FakeMap } +// FakeFlowtable wraps Flowtable for the Fake implementation +type FakeFlowtable struct { + Flowtable +} + // FakeChain wraps Chain for the Fake implementation type FakeChain struct { Chain @@ -110,6 +118,10 @@ func (fake *Fake) List(_ context.Context, objectType string) ([]string, error) { var result []string switch objectType { + case "flowtable", "flowtables": + for name := range fake.Table.Flowtables { + result = append(result, name) + } case "chain", "chains": for name := range fake.Table.Chains { result = append(result, name) @@ -236,10 +248,11 @@ func (fake *Fake) run(tx *Transaction) (*FakeTable, error) { table := *obj table.Handle = PtrTo(fake.nextHandle) updatedTable = &FakeTable{ - Table: table, - Chains: make(map[string]*FakeChain), - Sets: make(map[string]*FakeSet), - Maps: make(map[string]*FakeMap), + Table: table, + Flowtables: make(map[string]*FakeFlowtable), + Chains: make(map[string]*FakeChain), + Sets: make(map[string]*FakeSet), + Maps: make(map[string]*FakeMap), } case deleteVerb: updatedTable = nil @@ -247,6 +260,29 @@ func (fake *Fake) run(tx *Transaction) (*FakeTable, error) { return nil, fmt.Errorf("unhandled operation %q", op.verb) } + case *Flowtable: + existingFlowtable := updatedTable.Flowtables[obj.Name] + err := checkExists(op.verb, "flowtable", obj.Name, existingFlowtable != nil) + if err != nil { + return nil, err + } + switch op.verb { + case addVerb, createVerb: + if existingFlowtable != nil { + continue + } + flowtable := *obj + flowtable.Handle = PtrTo(fake.nextHandle) + updatedTable.Flowtables[obj.Name] = &FakeFlowtable{ + Flowtable: flowtable, + } + case deleteVerb: + // FIXME delete-by-handle + delete(updatedTable.Flowtables, obj.Name) + default: + return nil, fmt.Errorf("unhandled operation %q", op.verb) + } + case *Chain: existingChain := updatedTable.Chains[obj.Name] err := checkExists(op.verb, "chain", obj.Name, existingChain != nil) @@ -461,10 +497,14 @@ func checkRuleRefs(rule *Rule, table *FakeTable) error { for i, word := range words { if strings.HasPrefix(word, "@") { name := word[1:] - if i > 0 && (words[i] == "map" || words[i] == "vmap") { + if i > 0 && (words[i-1] == "map" || words[i-1] == "vmap") { if table.Maps[name] == nil { return notFoundError("no such map %q", name) } + } else if i > 0 && words[i-1] == "offload" { + if table.Flowtables[name] == nil { + return notFoundError("no such flowtable %q", name) + } } else { // recent nft lets you use a map in a set lookup if table.Sets[name] == nil && table.Maps[name] == nil { @@ -507,6 +547,7 @@ func (fake *Fake) Dump() string { buf := &strings.Builder{} table := fake.Table + flowtables := sortKeys(table.Flowtables) chains := sortKeys(table.Chains) sets := sortKeys(table.Sets) maps := sortKeys(table.Maps) @@ -514,6 +555,10 @@ func (fake *Fake) Dump() string { // Write out all of the object adds first. table.writeOperation(addVerb, &fake.nftContext, buf) + for _, fname := range flowtables { + ft := table.Flowtables[fname] + ft.writeOperation(addVerb, &fake.nftContext, buf) + } for _, cname := range chains { ch := table.Chains[cname] ch.writeOperation(addVerb, &fake.nftContext, buf) @@ -585,6 +630,8 @@ func (fake *Fake) ParseDump(data string) (err error) { switch match[1] { case "table": obj = &Table{} + case "flowtable": + obj = &Flowtable{} case "chain": obj = &Chain{} case "rule": @@ -643,10 +690,16 @@ func (table *FakeTable) copy() *FakeTable { } tcopy := &FakeTable{ - Table: table.Table, - Chains: make(map[string]*FakeChain), - Sets: make(map[string]*FakeSet), - Maps: make(map[string]*FakeMap), + Table: table.Table, + Flowtables: make(map[string]*FakeFlowtable), + Chains: make(map[string]*FakeChain), + Sets: make(map[string]*FakeSet), + Maps: make(map[string]*FakeMap), + } + for name, flowtable := range table.Flowtables { + tcopy.Flowtables[name] = &FakeFlowtable{ + Flowtable: flowtable.Flowtable, + } } for name, chain := range table.Chains { tcopy.Chains[name] = &FakeChain{ diff --git a/fake_test.go b/fake_test.go index c9a44e2..a51c103 100644 --- a/fake_test.go +++ b/fake_test.go @@ -88,6 +88,10 @@ func TestFakeRun(t *testing.T) { Key: []string{"192.168.0.1", "tcp", "80"}, Value: []string{"drop"}, }) + tx.Add(&Flowtable{ + Name: "myflowtable", + Devices: []string{"eth0", "eth1"}, + }) // The transaction should contain exactly those commands, in order expected := strings.TrimPrefix(dedent.Dedent(` @@ -102,6 +106,7 @@ func TestFakeRun(t *testing.T) { add element ip kube-proxy map1 { 192.168.0.1 . tcp . 80 : goto chain } add element ip kube-proxy map1 { 192.168.0.2 . tcp . 443 comment "with a comment" : goto anotherchain } add element ip kube-proxy map1 { 192.168.0.1 . tcp . 80 : drop } + add flowtable ip kube-proxy myflowtable { devices = { eth0, eth1 } ; } `), "\n") diff := cmp.Diff(expected, tx.String()) if diff != "" { @@ -168,6 +173,7 @@ func TestFakeRun(t *testing.T) { // be seen. expected = strings.TrimPrefix(dedent.Dedent(` add table ip kube-proxy + add flowtable ip kube-proxy myflowtable { devices = { eth0, eth1 } ; } add chain ip kube-proxy anotherchain add chain ip kube-proxy chain { comment "foo" ; } add map ip kube-proxy map1 { type ipv4_addr . inet_proto . inet_service : verdict ; } @@ -208,6 +214,7 @@ func TestFakeRun(t *testing.T) { } expected = strings.TrimPrefix(dedent.Dedent(` add table ip kube-proxy + add flowtable ip kube-proxy myflowtable { devices = { eth0, eth1 } ; } add chain ip kube-proxy anotherchain add chain ip kube-proxy chain { comment "foo" ; } add map ip kube-proxy map1 { type ipv4_addr . inet_proto . inet_service : verdict ; } @@ -594,6 +601,7 @@ func TestFakeParseDump(t *testing.T) { ipFamily: IPv4Family, dump: ` add table ip kube-proxy + add flowtable ip kube-proxy myflowtable { hook ingress priority filter ; devices = { eth0, eth1 } ; } add chain ip kube-proxy anotherchain add chain ip kube-proxy chain { comment "foo" ; } add map ip kube-proxy map1 { type ipv4_addr . inet_proto . inet_service ; } @@ -610,7 +618,6 @@ func TestFakeParseDump(t *testing.T) { ipFamily: IPv4Family, dump: ` add table ip kube-proxy { comment "rules for kube-proxy" ; } - add chain ip kube-proxy mark-for-masquerade add chain ip kube-proxy masquerading add chain ip kube-proxy services diff --git a/nftables_test.go b/nftables_test.go index c27cf62..2d4556e 100644 --- a/nftables_test.go +++ b/nftables_test.go @@ -29,10 +29,6 @@ import ( func newTestInterface(t *testing.T, family Family, tableName string) (Interface, *fakeExec, error) { fexec := newFakeExec(t) - ip := "ip" - if family == IPv6Family { - ip = "ip6" - } fexec.expected = append(fexec.expected, expectedCmd{ args: []string{"/nft", "--version"}, @@ -40,7 +36,7 @@ func newTestInterface(t *testing.T, family Family, tableName string) (Interface, }, expectedCmd{ args: []string{"/nft", "--check", "-f", "-"}, - stdin: fmt.Sprintf("add table %s %s { comment \"test\" ; }\n", ip, tableName), + stdin: fmt.Sprintf("add table %s %s { comment \"test\" ; }\n", family, tableName), }, ) nft, err := newInternal(family, tableName, fexec) @@ -187,11 +183,16 @@ func TestRun(t *testing.T) { Chain: "chain", Rule: "ip daddr 10.0.0.0/8 drop", }) - + tx.Add(&Flowtable{ + Name: "flowtable", + Priority: PtrTo(FilterIngressPriority), + Devices: []string{"eth0", "eth1"}, + }) expected := strings.TrimPrefix(dedent.Dedent(` add table ip kube-proxy add chain ip kube-proxy chain { comment "foo" ; } add rule ip kube-proxy chain ip daddr 10.0.0.0/8 drop + add flowtable ip kube-proxy flowtable { hook ingress priority filter ; devices = { eth0, eth1 } ; } `), "\n") fexec.expected = append(fexec.expected, expectedCmd{ diff --git a/objects.go b/objects.go index 6a62879..ed11db2 100644 --- a/objects.go +++ b/objects.go @@ -579,3 +579,80 @@ func (element *Element) parse(line string) error { } return nil } + +// Object implementation for Flowtable +func (flowtable *Flowtable) validate(verb verb) error { + switch verb { + case addVerb, createVerb: + if flowtable.Name == "" { + return fmt.Errorf("no name specified for flowtable") + } + if flowtable.Handle != nil { + return fmt.Errorf("cannot specify Handle in %s operation", verb) + } + case deleteVerb: + if flowtable.Name == "" && flowtable.Handle == nil { + return fmt.Errorf("must specify either name or handle") + } + default: + return fmt.Errorf("%s is not implemented for flowtables", verb) + } + + return nil +} + +func (flowtable *Flowtable) writeOperation(verb verb, ctx *nftContext, writer io.Writer) { + // Special case for delete-by-handle + if verb == deleteVerb && flowtable.Handle != nil { + fmt.Fprintf(writer, "delete flowtable %s %s handle %d", ctx.family, ctx.table, *flowtable.Handle) + return + } + + fmt.Fprintf(writer, "%s flowtable %s %s %s", verb, ctx.family, ctx.table, flowtable.Name) + if verb == addVerb || verb == createVerb { + fmt.Fprintf(writer, " {") + + if flowtable.Priority != nil { + // since there is only one priority value allowed "filter" just use the value + // provided and not try to parse it. + fmt.Fprintf(writer, " hook ingress priority %s ;", *flowtable.Priority) + } + + if len(flowtable.Devices) > 0 { + fmt.Fprintf(writer, " devices = { %s } ;", strings.Join(flowtable.Devices, ", ")) + } + + fmt.Fprintf(writer, " }") + } + + fmt.Fprintf(writer, "\n") +} + +// nft add flowtable inet example_table example_flowtable { hook ingress priority filter ; devices = { eth0 }; } +var flowtableRegexp = regexp.MustCompile(fmt.Sprintf( + `%s(?: {(?: hook ingress priority %s ;)(?: devices = {(.*)} ;) })?`, + noSpaceGroup, noSpaceGroup)) + +func (flowtable *Flowtable) parse(line string) error { + match := flowtableRegexp.FindStringSubmatch(line) + if match == nil { + return fmt.Errorf("failed parsing flowtableRegexp add command") + } + flowtable.Name = match[1] + if match[2] != "" { + flowtable.Priority = (*FlowtableIngressPriority)(&match[2]) + } + // to avoid complex regular expressions the regex match everything between the brackets + // to match a single interface or a comma separated list of interfaces, and it is postprocessed + // here to remove the whitespaces. + if match[3] != "" { + devices := strings.Split(strings.TrimSpace(match[3]), ",") + for i := range devices { + devices[i] = strings.TrimSpace(devices[i]) + } + if len(devices) > 0 { + flowtable.Devices = devices + } + } + return nil +} diff --git a/objects_test.go b/objects_test.go index 6e032b9..6a8fc93 100644 --- a/objects_test.go +++ b/objects_test.go @@ -95,6 +95,101 @@ func TestObjects(t *testing.T) { err: "cannot specify Handle", }, + // Flowtables + { + name: "add flowtable", + verb: addVerb, + object: &Flowtable{ + Name: "myflowtable", + }, + out: `add flowtable ip mytable myflowtable { }`, + }, + { + name: "create flowtable", + verb: createVerb, + object: &Flowtable{ + Name: "myflowtable", + }, + out: `create flowtable ip mytable myflowtable { }`, + }, + { + name: "create flowtable with priority math", + verb: createVerb, + object: &Flowtable{ + Name: "myflowtable", + Priority: PtrTo(FilterIngressPriority + "+5"), + }, + out: `create flowtable ip mytable myflowtable { hook ingress priority filter+5 ; }`, + }, + { + name: "create flowtable with devices", + verb: createVerb, + object: &Flowtable{ + Name: "myflowtable", + Devices: []string{"eth0", "eth1"}, + }, + out: `create flowtable ip mytable myflowtable { devices = { eth0, eth1 } ; }`, + }, + { + name: "create flowtable with devices and default priority", + verb: createVerb, + object: &Flowtable{ + Name: "myflowtable", + Priority: PtrTo(FilterIngressPriority), + Devices: []string{"eth0", "eth1"}, + }, + out: `create flowtable ip mytable myflowtable { hook ingress priority filter ; devices = { eth0, eth1 } ; }`, + }, + { + name: "flush flowtable", + verb: flushVerb, + object: &Flowtable{ + Name: "myflowtable", + }, + err: "not implemented", + }, + { + name: "delete flowtable", + verb: deleteVerb, + object: &Flowtable{ + Name: "myflowtable", + }, + out: `delete flowtable ip mytable myflowtable`, + }, + { + name: "delete flowtable by handle", + verb: deleteVerb, + object: &Flowtable{ + Name: "myflowtable", + Handle: PtrTo(5), + }, + out: `delete flowtable ip mytable handle 5`, + }, + { + name: "invalid insert flowtable", + verb: insertVerb, + object: &Flowtable{ + Name: "myflowtable", + }, err: "not implemented", + }, + { + name: "invalid replace flowtable", + verb: replaceVerb, + object: &Flowtable{ + Name: "myflowtable", + }, + err: "not implemented", + }, + { + name: "invalid add flowtable with Handle", + verb: addVerb, + object: &Flowtable{ + Name: "myflowtable", + Handle: PtrTo(5), + }, + err: "cannot specify Handle", + }, + // Chains { name: "add chain", diff --git a/types.go b/types.go index d8202bc..1d0da2c 100644 --- a/types.go +++ b/types.go @@ -382,3 +382,30 @@ type Element struct { // Comment is an optional comment for the element Comment *string } + +type FlowtableIngressPriority string + +const ( + // FilterIngressPriority is the priority for the filter value in the Ingress hook + // that stands for 0. + FilterIngressPriority FlowtableIngressPriority = "filter" +) + +// Flowtable represents an nftables flowtable. +// https://wiki.nftables.org/wiki-nftables/index.php/Flowtables +type Flowtable struct { + // Name is the name of the flowtable. + Name string + + // The Priority can be a signed integer or FlowtableIngressPriority which stands for 0. + // Addition and subtraction can be used to set relative priority, e.g. filter + 5 equals to 5. + Priority *FlowtableIngressPriority + + // The Devices are specified as iifname(s) of the input interface(s) of the traffic + // that should be offloaded. + Devices []string + + // Handle is an identifier that can be used to uniquely identify an object when + // deleting it. When adding a new object, this must be nil + Handle *int +}