From b092650fb75e7f109fc5c076e6b29c77e2fe8fd6 Mon Sep 17 00:00:00 2001 From: Nadia Pinaeva Date: Thu, 1 Feb 2024 09:42:12 +0100 Subject: [PATCH] Add ParseDump function to allow using Fake.Dump() output as a test setup. Signed-off-by: Nadia Pinaeva --- fake.go | 54 +++++++++++++++ fake_test.go | 178 +++++++++++++++++++++++++++++++++++++++++++++++++ objects.go | 182 +++++++++++++++++++++++++++++++++++++++++++++++++++ types.go | 6 ++ 4 files changed, 420 insertions(+) diff --git a/fake.go b/fake.go index c2ba0e5..694c970 100644 --- a/fake.go +++ b/fake.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "reflect" + "regexp" "sort" "strings" ) @@ -519,6 +520,59 @@ func (fake *Fake) Dump() string { return buf.String() } +// ParseDump can parse a dump for a given nft instance. +// It expects fake's table name and family in all rules. +// The best way to verify that everything important was properly parsed is to +// compare given data with nft.Dump() output. +func (fake *Fake) ParseDump(data string) (err error) { + lines := strings.Split(data, "\n") + var i int + var line string + parsingDone := false + defer func() { + if err != nil && !parsingDone { + err = fmt.Errorf("%w (at line %v: %s", err, i+1, line) + } + }() + tx := fake.NewTransaction() + commonRegexp := regexp.MustCompile(fmt.Sprintf(`add %s %s %s (.*)`, noSpaceGroup, fake.family, fake.table)) + + for i, line = range lines { + line = strings.TrimSpace(line) + if line == "" || line[0] == '#' { + continue + } + match := commonRegexp.FindStringSubmatch(line) + if match == nil { + return fmt.Errorf("could not parse, or wrong table/family") + } + var obj Object + switch match[1] { + case "table": + obj = &Table{} + case "chain": + obj = &Chain{} + case "rule": + obj = &Rule{} + case "map": + obj = &Map{} + case "set": + obj = &Set{} + case "element": + obj = &Element{} + default: + return fmt.Errorf("unknown object %s", match[1]) + } + err = obj.parse(match[2]) + if err != nil { + return err + } + tx.Add(obj) + } + parsingDone = true + return fake.Run(context.Background(), tx) +} + func sortKeys[K ~string, V any](m map[K]V) []K { keys := make([]K, 0, len(m)) for key := range m { diff --git a/fake_test.go b/fake_test.go index 9cc5877..682ff0d 100644 --- a/fake_test.go +++ b/fake_test.go @@ -558,3 +558,181 @@ func TestFakeAddInsertReplace(t *testing.T) { assertRules(t, fake, "thirteenth", "sixth", "twelfth", "fifth", "seventh", "ninth", "eighth", "fourth", "third", "eleventh", "tenth") } + +func TestFakeParseDump(t *testing.T) { + tc1 := ` + add table ip kube-proxy { comment "" ; } + 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 ; } + add set ip kube-proxy set1 { type ipv4_addr . inet_proto . inet_service ; flags dynamic ; gc-interval 15s ; policy memory ; auto-merge ; } + add rule ip kube-proxy anotherchain ip saddr 1.2.3.4 drop comment "drop rule" + add rule ip kube-proxy anotherchain ip daddr 5.6.7.8 reject comment "reject rule" + add rule ip kube-proxy chain ip daddr 10.0.0.0/8 drop + add rule ip kube-proxy chain masquerade comment "comment" + add element ip kube-proxy map1 { 192.168.0.1 . tcp . 80 : drop } + add element ip kube-proxy map1 { 192.168.0.2 . tcp . 443 comment "with a comment" : goto anotherchain } + ` + + tc2 := ` + 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 + add chain ip kube-proxy firewall-check + add chain ip kube-proxy endpoints-check + add chain ip kube-proxy filter-prerouting { type filter hook prerouting priority -110 ; } + add chain ip kube-proxy filter-forward { type filter hook forward priority -110 ; } + add chain ip kube-proxy filter-input { type filter hook input priority -110 ; } + add chain ip kube-proxy filter-output { type filter hook output priority -110 ; } + add chain ip kube-proxy nat-output { type nat hook output priority -100 ; } + add chain ip kube-proxy nat-postrouting { type nat hook postrouting priority 100 ; } + add chain ip kube-proxy nat-prerouting { type nat hook prerouting priority -100 ; } + add chain ip kube-proxy reject-chain { comment "helper for @no-endpoint-services / @no-endpoint-nodeports" ; } + add chain ip kube-proxy service-ULMVA6XW-ns1/svc1/tcp/p80 + add chain ip kube-proxy endpoint-5OJB2KTY-ns1/svc1/tcp/p80__10.180.0.1/80 + add chain ip kube-proxy service-42NFTM6N-ns2/svc2/tcp/p80 + add chain ip kube-proxy endpoint-SGOXE6O3-ns2/svc2/tcp/p80__10.180.0.2/80 + add chain ip kube-proxy external-42NFTM6N-ns2/svc2/tcp/p80 + add chain ip kube-proxy service-4AT6LBPK-ns3/svc3/tcp/p80 + add chain ip kube-proxy endpoint-UEIP74TE-ns3/svc3/tcp/p80__10.180.0.3/80 + add chain ip kube-proxy external-4AT6LBPK-ns3/svc3/tcp/p80 + add chain ip kube-proxy service-LAUZTJTB-ns4/svc4/tcp/p80 + add chain ip kube-proxy endpoint-UNZV3OEC-ns4/svc4/tcp/p80__10.180.0.4/80 + add chain ip kube-proxy endpoint-5RFCDDV7-ns4/svc4/tcp/p80__10.180.0.5/80 + add chain ip kube-proxy external-LAUZTJTB-ns4/svc4/tcp/p80 + add chain ip kube-proxy service-HVFWP5L3-ns5/svc5/tcp/p80 + add chain ip kube-proxy external-HVFWP5L3-ns5/svc5/tcp/p80 + add chain ip kube-proxy endpoint-GTK6MW7G-ns5/svc5/tcp/p80__10.180.0.3/80 + add chain ip kube-proxy firewall-HVFWP5L3-ns5/svc5/tcp/p80 + + add rule ip kube-proxy mark-for-masquerade mark set mark or 0x4000 + add rule ip kube-proxy masquerading mark and 0x4000 == 0 return + add rule ip kube-proxy masquerading mark set mark xor 0x4000 + add rule ip kube-proxy masquerading masquerade fully-random + add rule ip kube-proxy filter-prerouting ct state new jump firewall-check + add rule ip kube-proxy filter-forward ct state new jump endpoints-check + add rule ip kube-proxy filter-input ct state new jump endpoints-check + add rule ip kube-proxy filter-output ct state new jump endpoints-check + add rule ip kube-proxy filter-output ct state new jump firewall-check + add rule ip kube-proxy nat-output jump services + add rule ip kube-proxy nat-postrouting jump masquerading + add rule ip kube-proxy nat-prerouting jump services + + add map ip kube-proxy firewall-ips { type ipv4_addr . inet_proto . inet_service : verdict ; comment "destinations that are subject to LoadBalancerSourceRanges" ; } + add rule ip kube-proxy firewall-check ip daddr . meta l4proto . th dport vmap @firewall-ips + + add rule ip kube-proxy reject-chain reject + + add map ip kube-proxy no-endpoint-services { type ipv4_addr . inet_proto . inet_service : verdict ; comment "vmap to drop or reject packets to services with no endpoints" ; } + add map ip kube-proxy no-endpoint-nodeports { type inet_proto . inet_service : verdict ; comment "vmap to drop or reject packets to service nodeports with no endpoints" ; } + + add rule ip kube-proxy endpoints-check ip daddr . meta l4proto . th dport vmap @no-endpoint-services + add rule ip kube-proxy endpoints-check fib daddr type local ip daddr != 127.0.0.0/8 meta l4proto . th dport vmap @no-endpoint-nodeports + + add map ip kube-proxy service-ips { type ipv4_addr . inet_proto . inet_service : verdict ; comment "ClusterIP, ExternalIP and LoadBalancer IP traffic" ; } + add map ip kube-proxy service-nodeports { type inet_proto . inet_service : verdict ; comment "NodePort traffic" ; } + add rule ip kube-proxy services ip daddr . meta l4proto . th dport vmap @service-ips + add rule ip kube-proxy services fib daddr type local ip daddr != 127.0.0.0/8 meta l4proto . th dport vmap @service-nodeports + + # svc1 + add rule ip kube-proxy service-ULMVA6XW-ns1/svc1/tcp/p80 ip daddr 172.30.0.41 tcp dport 80 ip saddr != 10.0.0.0/8 jump mark-for-masquerade + add rule ip kube-proxy service-ULMVA6XW-ns1/svc1/tcp/p80 numgen random mod 1 vmap { 0 : goto endpoint-5OJB2KTY-ns1/svc1/tcp/p80__10.180.0.1/80 } + + add rule ip kube-proxy endpoint-5OJB2KTY-ns1/svc1/tcp/p80__10.180.0.1/80 ip saddr 10.180.0.1 jump mark-for-masquerade + add rule ip kube-proxy endpoint-5OJB2KTY-ns1/svc1/tcp/p80__10.180.0.1/80 meta l4proto tcp dnat to 10.180.0.1:80 + + add element ip kube-proxy service-ips { 172.30.0.41 . tcp . 80 : goto service-ULMVA6XW-ns1/svc1/tcp/p80 } + + # svc2 + add rule ip kube-proxy service-42NFTM6N-ns2/svc2/tcp/p80 ip daddr 172.30.0.42 tcp dport 80 ip saddr != 10.0.0.0/8 jump mark-for-masquerade + add rule ip kube-proxy service-42NFTM6N-ns2/svc2/tcp/p80 numgen random mod 1 vmap { 0 : goto endpoint-SGOXE6O3-ns2/svc2/tcp/p80__10.180.0.2/80 } + add rule ip kube-proxy external-42NFTM6N-ns2/svc2/tcp/p80 ip saddr 10.0.0.0/8 goto service-42NFTM6N-ns2/svc2/tcp/p80 comment "short-circuit pod traffic" + add rule ip kube-proxy external-42NFTM6N-ns2/svc2/tcp/p80 fib saddr type local jump mark-for-masquerade comment "masquerade local traffic" + add rule ip kube-proxy external-42NFTM6N-ns2/svc2/tcp/p80 fib saddr type local goto service-42NFTM6N-ns2/svc2/tcp/p80 comment "short-circuit local traffic" + add rule ip kube-proxy endpoint-SGOXE6O3-ns2/svc2/tcp/p80__10.180.0.2/80 ip saddr 10.180.0.2 jump mark-for-masquerade + add rule ip kube-proxy endpoint-SGOXE6O3-ns2/svc2/tcp/p80__10.180.0.2/80 meta l4proto tcp dnat to 10.180.0.2:80 + + add element ip kube-proxy service-ips { 172.30.0.42 . tcp . 80 : goto service-42NFTM6N-ns2/svc2/tcp/p80 } + add element ip kube-proxy service-ips { 192.168.99.22 . tcp . 80 : goto external-42NFTM6N-ns2/svc2/tcp/p80 } + add element ip kube-proxy service-ips { 1.2.3.4 . tcp . 80 : goto external-42NFTM6N-ns2/svc2/tcp/p80 } + add element ip kube-proxy service-nodeports { tcp . 3001 : goto external-42NFTM6N-ns2/svc2/tcp/p80 } + + add element ip kube-proxy no-endpoint-nodeports { tcp . 3001 comment "ns2/svc2:p80" : drop } + add element ip kube-proxy no-endpoint-services { 1.2.3.4 . tcp . 80 comment "ns2/svc2:p80" : drop } + add element ip kube-proxy no-endpoint-services { 192.168.99.22 . tcp . 80 comment "ns2/svc2:p80" : drop } + + # svc3 + add rule ip kube-proxy service-4AT6LBPK-ns3/svc3/tcp/p80 ip daddr 172.30.0.43 tcp dport 80 ip saddr != 10.0.0.0/8 jump mark-for-masquerade + add rule ip kube-proxy service-4AT6LBPK-ns3/svc3/tcp/p80 numgen random mod 1 vmap { 0 : goto endpoint-UEIP74TE-ns3/svc3/tcp/p80__10.180.0.3/80 } + add rule ip kube-proxy external-4AT6LBPK-ns3/svc3/tcp/p80 jump mark-for-masquerade + add rule ip kube-proxy external-4AT6LBPK-ns3/svc3/tcp/p80 goto service-4AT6LBPK-ns3/svc3/tcp/p80 + add rule ip kube-proxy endpoint-UEIP74TE-ns3/svc3/tcp/p80__10.180.0.3/80 ip saddr 10.180.0.3 jump mark-for-masquerade + add rule ip kube-proxy endpoint-UEIP74TE-ns3/svc3/tcp/p80__10.180.0.3/80 meta l4proto tcp dnat to 10.180.0.3:80 + + add element ip kube-proxy service-ips { 172.30.0.43 . tcp . 80 : goto service-4AT6LBPK-ns3/svc3/tcp/p80 } + add element ip kube-proxy service-nodeports { tcp . 3003 : goto external-4AT6LBPK-ns3/svc3/tcp/p80 } + + # svc4 + add rule ip kube-proxy service-LAUZTJTB-ns4/svc4/tcp/p80 ip daddr 172.30.0.44 tcp dport 80 ip saddr != 10.0.0.0/8 jump mark-for-masquerade + add rule ip kube-proxy service-LAUZTJTB-ns4/svc4/tcp/p80 numgen random mod 2 vmap { 0 : goto endpoint-UNZV3OEC-ns4/svc4/tcp/p80__10.180.0.4/80 , 1 : goto endpoint-5RFCDDV7-ns4/svc4/tcp/p80__10.180.0.5/80 } + add rule ip kube-proxy external-LAUZTJTB-ns4/svc4/tcp/p80 jump mark-for-masquerade + add rule ip kube-proxy external-LAUZTJTB-ns4/svc4/tcp/p80 goto service-LAUZTJTB-ns4/svc4/tcp/p80 + add rule ip kube-proxy endpoint-5RFCDDV7-ns4/svc4/tcp/p80__10.180.0.5/80 ip saddr 10.180.0.5 jump mark-for-masquerade + add rule ip kube-proxy endpoint-5RFCDDV7-ns4/svc4/tcp/p80__10.180.0.5/80 meta l4proto tcp dnat to 10.180.0.5:80 + add rule ip kube-proxy endpoint-UNZV3OEC-ns4/svc4/tcp/p80__10.180.0.4/80 ip saddr 10.180.0.4 jump mark-for-masquerade + add rule ip kube-proxy endpoint-UNZV3OEC-ns4/svc4/tcp/p80__10.180.0.4/80 meta l4proto tcp dnat to 10.180.0.4:80 + + add element ip kube-proxy service-ips { 172.30.0.44 . tcp . 80 : goto service-LAUZTJTB-ns4/svc4/tcp/p80 } + add element ip kube-proxy service-ips { 192.168.99.33 . tcp . 80 : goto external-LAUZTJTB-ns4/svc4/tcp/p80 } + + # svc5 + add set ip kube-proxy affinity-GTK6MW7G-ns5/svc5/tcp/p80__10.180.0.3/80 { type ipv4_addr ; flags dynamic,timeout ; timeout 10800s ; } + add rule ip kube-proxy service-HVFWP5L3-ns5/svc5/tcp/p80 ip daddr 172.30.0.45 tcp dport 80 ip saddr != 10.0.0.0/8 jump mark-for-masquerade + add rule ip kube-proxy service-HVFWP5L3-ns5/svc5/tcp/p80 ip saddr @affinity-GTK6MW7G-ns5/svc5/tcp/p80__10.180.0.3/80 goto endpoint-GTK6MW7G-ns5/svc5/tcp/p80__10.180.0.3/80 + add rule ip kube-proxy service-HVFWP5L3-ns5/svc5/tcp/p80 numgen random mod 1 vmap { 0 : goto endpoint-GTK6MW7G-ns5/svc5/tcp/p80__10.180.0.3/80 } + add rule ip kube-proxy external-HVFWP5L3-ns5/svc5/tcp/p80 jump mark-for-masquerade + add rule ip kube-proxy external-HVFWP5L3-ns5/svc5/tcp/p80 goto service-HVFWP5L3-ns5/svc5/tcp/p80 + + add rule ip kube-proxy endpoint-GTK6MW7G-ns5/svc5/tcp/p80__10.180.0.3/80 ip saddr 10.180.0.3 jump mark-for-masquerade + add rule ip kube-proxy endpoint-GTK6MW7G-ns5/svc5/tcp/p80__10.180.0.3/80 update @affinity-GTK6MW7G-ns5/svc5/tcp/p80__10.180.0.3/80 { ip saddr } + add rule ip kube-proxy endpoint-GTK6MW7G-ns5/svc5/tcp/p80__10.180.0.3/80 meta l4proto tcp dnat to 10.180.0.3:80 + + add rule ip kube-proxy firewall-HVFWP5L3-ns5/svc5/tcp/p80 ip saddr != { 203.0.113.0/25 } drop + + add element ip kube-proxy service-ips { 172.30.0.45 . tcp . 80 : goto service-HVFWP5L3-ns5/svc5/tcp/p80 } + add element ip kube-proxy service-ips { 5.6.7.8 . tcp . 80 : goto external-HVFWP5L3-ns5/svc5/tcp/p80 } + add element ip kube-proxy service-nodeports { tcp . 3002 : goto external-HVFWP5L3-ns5/svc5/tcp/p80 } + add element ip kube-proxy firewall-ips { 5.6.7.8 . tcp . 80 comment "ns5/svc5:p80" : goto firewall-HVFWP5L3-ns5/svc5/tcp/p80 } + + # svc6 + add element ip kube-proxy no-endpoint-services { 172.30.0.46 . tcp . 80 comment "ns6/svc6:p80" : goto reject-chain } + ` + + for _, rules := range []string{tc1, tc2} { + rules = dedent.Dedent(rules) + fake := NewFake(IPv4Family, "kube-proxy") + err := fake.ParseDump(rules) + if err != nil { + t.Fatalf("unexpected error from ParseDump: %v", err) + } + + // Dump() will add 1 empty line, so add to rulesSlice to match + rulesSlice := []string{""} + for _, rule := range strings.Split(rules, "\n") { + if rule == "" || strings.HasPrefix(rule, "#") { + continue + } + rulesSlice = append(rulesSlice, rule) + } + sort.Strings(rulesSlice) + dumpSlice := strings.Split(fake.Dump(), "\n") + sort.Strings(dumpSlice) + + diff := cmp.Diff(rulesSlice, dumpSlice) + if diff != "" { + t.Errorf("Dump doesn't match given rules:\n%s", diff) + } + } +} diff --git a/objects.go b/objects.go index 69ed9a8..a1ab367 100644 --- a/objects.go +++ b/objects.go @@ -19,9 +19,37 @@ package knftables import ( "fmt" "io" + "regexp" + "strconv" "strings" + "time" ) +func parseInt(numbersOnly string) *int { + i64, _ := strconv.ParseInt(numbersOnly, 10, 64) + i := int(i64) + return &i +} + +func parseUint(numbersOnly string) *uint64 { + ui64, _ := strconv.ParseUint(numbersOnly, 10, 64) + return &ui64 +} + +// getComment parses a match for the commentGroup regexp (below). To distinguish between empty comment and no comment, +// we capture comment with double quotes. +func getComment(commentGroup string) *string { + if commentGroup == "" { + return nil + } + noQuotes := strings.Trim(commentGroup, "\"") + return &noQuotes +} + +var commentGroup = `(".*")` +var noSpaceGroup = `([^ ]*)` +var numberGroup = `([0-9]*)` + // Object implementation for Table func (table *Table) validate(verb verb) error { switch verb { @@ -55,6 +83,18 @@ func (table *Table) writeOperation(verb verb, ctx *nftContext, writer io.Writer) fmt.Fprintf(writer, "\n") } +var tableRegexp = regexp.MustCompile(fmt.Sprintf( + `(?:{ comment %s ; })?`, commentGroup)) + +func (table *Table) parse(line string) error { + match := tableRegexp.FindStringSubmatch(line) + if match == nil { + return fmt.Errorf("failed parsing table add command") + } + table.Comment = getComment(match[1]) + return nil +} + // Object implementation for Chain func (chain *Chain) validate(verb verb) error { if chain.Hook == nil { @@ -128,6 +168,33 @@ func (chain *Chain) writeOperation(verb verb, ctx *nftContext, writer io.Writer) fmt.Fprintf(writer, "\n") } +// groups in []: [1]%s(?: {(?: type [2]%s hook [3]%s(?: device "[4]%s")(?: priority [5]%s ;))(?: comment [6]%s ;) }) +var chainRegexp = regexp.MustCompile(fmt.Sprintf( + `%s(?: {(?: type %s hook %s(?: device "%s")?(?: priority %s ;))?(?: comment %s ;)? })?`, + noSpaceGroup, noSpaceGroup, noSpaceGroup, noSpaceGroup, noSpaceGroup, commentGroup)) + +func (chain *Chain) parse(line string) error { + match := chainRegexp.FindStringSubmatch(line) + if match == nil { + return fmt.Errorf("failed parsing chain add command") + } + chain.Name = match[1] + chain.Comment = getComment(match[6]) + if match[2] != "" { + chain.Type = (*BaseChainType)(&match[2]) + } + if match[3] != "" { + chain.Hook = (*BaseChainHook)(&match[3]) + } + if match[4] != "" { + chain.Device = &match[4] + } + if match[5] != "" { + chain.Priority = (*BaseChainPriority)(&match[5]) + } + return nil +} + // Object implementation for Rule func (rule *Rule) validate(verb verb) error { if rule.Chain == "" { @@ -181,6 +248,28 @@ func (rule *Rule) writeOperation(verb verb, ctx *nftContext, writer io.Writer) { fmt.Fprintf(writer, "\n") } +// groups in []: [1]%s(?: index [2]%s)?(?: handle [3]%s)? [4]([^"]*)(?: comment [5]%s)?$ +var ruleRegexp = regexp.MustCompile(fmt.Sprintf( + `%s(?: index %s)?(?: handle %s)? ([^"]*)(?: comment %s)?$`, + noSpaceGroup, numberGroup, numberGroup, commentGroup)) + +func (rule *Rule) parse(line string) error { + match := ruleRegexp.FindStringSubmatch(line) + if match == nil { + return fmt.Errorf("failed parsing rule add command") + } + rule.Chain = match[1] + rule.Rule = match[4] + rule.Comment = getComment(match[5]) + if match[2] != "" { + rule.Index = parseInt(match[2]) + } + if match[3] != "" { + rule.Handle = parseInt(match[3]) + } + return nil +} + // Object implementation for Set func (set *Set) validate(verb verb) error { switch verb { @@ -261,6 +350,16 @@ func (set *Set) writeOperation(verb verb, ctx *nftContext, writer io.Writer) { fmt.Fprintf(writer, "\n") } +func (set *Set) parse(line string) error { + match := setRegexp.FindStringSubmatch(line) + if match == nil { + return fmt.Errorf("failed parsing set add command") + } + set.Name, set.Type, set.TypeOf, set.Flags, set.Timeout, set.GCInterval, + set.Size, set.Policy, set.Comment, set.AutoMerge = parseMapAndSetProps(match) + return nil +} + // Object implementation for Map func (mapObj *Map) validate(verb verb) error { switch verb { @@ -338,6 +437,68 @@ func (mapObj *Map) writeOperation(verb verb, ctx *nftContext, writer io.Writer) fmt.Fprintf(writer, "\n") } +func (mapObj *Map) parse(line string) error { + match := mapRegexp.FindStringSubmatch(line) + if match == nil { + return fmt.Errorf("failed parsing map add command") + } + mapObj.Name, mapObj.Type, mapObj.TypeOf, mapObj.Flags, mapObj.Timeout, mapObj.GCInterval, + mapObj.Size, mapObj.Policy, mapObj.Comment, _ = parseMapAndSetProps(match) + return nil +} + +var autoMergeProp = `( auto-merge ;)?` + +// groups in []: [1]%s {(?: [2](type|typeof) [3]([^;]*)) ;(?: flags [4]([^;]*) ;)?(?: timeout [5]%ss ;)?(?: gc-interval [6]%ss ;)?(?: size [7]%s ;)?(?: policy [8]%s ;)?[9]%s(?: comment [10]%s ;)? } +var mapOrSet = `%s {(?: (type|typeof) ([^;]*)) ;(?: flags ([^;]*) ;)?(?: timeout %ss ;)?(?: gc-interval %ss ;)?(?: size %s ;)?(?: policy %s ;)?%s(?: comment %s ;)? }` +var mapRegexp = regexp.MustCompile(fmt.Sprintf(mapOrSet, noSpaceGroup, numberGroup, numberGroup, noSpaceGroup, noSpaceGroup, "", commentGroup)) +var setRegexp = regexp.MustCompile(fmt.Sprintf(mapOrSet, noSpaceGroup, numberGroup, numberGroup, noSpaceGroup, noSpaceGroup, autoMergeProp, commentGroup)) + +func parseMapAndSetProps(match []string) (name string, typeProp string, typeOf string, flags []SetFlag, + timeout *time.Duration, gcInterval *time.Duration, size *uint64, policy *SetPolicy, comment *string, autoMerge *bool) { + name = match[1] + // set and map have different number of match groups, but comment is always the last + comment = getComment(match[len(match)-1]) + if match[2] == "type" { + typeProp = match[3] + } else { + typeOf = match[3] + } + if match[4] != "" { + flags = parseSetFlags(match[4]) + } + if match[5] != "" { + timeoutObj, _ := time.ParseDuration(match[5] + "s") + timeout = &timeoutObj + } + if match[6] != "" { + gcIntervalObj, _ := time.ParseDuration(match[6] + "s") + gcInterval = &gcIntervalObj + } + if match[7] != "" { + size = parseUint(match[7]) + } + if match[8] != "" { + policy = (*SetPolicy)(&match[8]) + } + if len(match) > 10 { + // set + if match[9] != "" { + autoMergeObj := true + autoMerge = &autoMergeObj + } + } + return +} + +func parseSetFlags(s string) []SetFlag { + var res []SetFlag + for _, flag := range strings.Split(s, ",") { + res = append(res, SetFlag(flag)) + } + return res +} + // Object implementation for Element func (element *Element) validate(verb verb) error { if element.Map == "" && element.Set == "" { @@ -387,3 +548,24 @@ func (element *Element) writeOperation(verb verb, ctx *nftContext, writer io.Wri fmt.Fprintf(writer, " }\n") } + +// groups in []: [1]%s { [2]([^:"]*)(?: comment [3]%s)?(?: : [4](.*))? } +var elementRegexp = regexp.MustCompile(fmt.Sprintf( + `%s { ([^:"]*)(?: comment %s)?(?: : (.*))? }`, noSpaceGroup, commentGroup)) + +func (element *Element) parse(line string) error { + match := elementRegexp.FindStringSubmatch(line) + if match == nil { + return fmt.Errorf("failed parsing element add command") + } + element.Comment = getComment(match[3]) + mapOrSetName := match[1] + element.Key = append(element.Key, strings.Split(match[2], " . ")...) + if match[4] != "" { + element.Map = mapOrSetName + element.Value = append(element.Value, strings.Split(match[4], " . ")...) + } else { + element.Set = mapOrSetName + } + return nil +} diff --git a/types.go b/types.go index 3eca214..d8202bc 100644 --- a/types.go +++ b/types.go @@ -38,6 +38,12 @@ type Object interface { // writeOperation writes out an "nft" operation involving the object. It assumes // that the object has been validated. writeOperation(verb verb, ctx *nftContext, writer io.Writer) + + // parse is the opposite of writeOperation; it fills Object fields based on an "nft add" + // command. line is the part of the line after "nft add " + // (so for most types it starts with the object name). + // If error is returned, Object's fields may be partially filled, therefore Object should not be used. + parse(line string) error } // Family is an nftables family