kubernetes/pkg/util/iptables/testing/parse_test.go

//go:build linux
// +build linux

/*
Copyright 2022 The Kubernetes Authors.

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 testing

import (
	"fmt"
	"reflect"
	"strings"
	"testing"

	"github.com/lithammer/dedent"

	"k8s.io/kubernetes/pkg/util/iptables"
	utilpointer "k8s.io/utils/pointer"
)

func TestParseRule(t *testing.T) {
	testCases := []struct {
		name      string
		rule      string
		parsed    *Rule
		nonStrict bool
		err       string
	}{
		{
			name: "basic rule",
			rule: `-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT`,
			parsed: &Rule{
				Raw:             `-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT`,
				Chain:           iptables.Chain("KUBE-NODEPORTS"),
				Comment:         &IPTablesValue{Value: "ns2/svc2:p80 health check node port"},
				Protocol:        &IPTablesValue{Value: "tcp"},
				DestinationPort: &IPTablesValue{Value: "30000"},
				Jump:            &IPTablesValue{Value: "ACCEPT"},
			},
		},
		{
			name: "addRuleToChainRegex requires an actual rule, not just a chain name",
			rule: `-A KUBE-NODEPORTS`,
			err:  `(no match rules)`,
		},
		{
			name: "ParseRule only parses adds",
			rule: `-D KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT`,
			err:  `(does not start with "-A CHAIN")`,
		},
		{
			name: "unquoted comment",
			rule: `-A KUBE-SVC-XPGD46QRK7WJZT7O -m comment --comment ns1/svc1:p80 -j KUBE-SEP-SXIVWICOYRO3J4NJ`,
			parsed: &Rule{
				Raw:     `-A KUBE-SVC-XPGD46QRK7WJZT7O -m comment --comment ns1/svc1:p80 -j KUBE-SEP-SXIVWICOYRO3J4NJ`,
				Chain:   iptables.Chain("KUBE-SVC-XPGD46QRK7WJZT7O"),
				Comment: &IPTablesValue{Value: "ns1/svc1:p80"},
				Jump:    &IPTablesValue{Value: "KUBE-SEP-SXIVWICOYRO3J4NJ"},
			},
		},
		{
			name: "local source",
			rule: `-A KUBE-XLB-GNZBNJ2PO5MGZ6GT -m comment --comment "masquerade LOCAL traffic for ns2/svc2:p80 LB IP" -m addrtype --src-type LOCAL -j KUBE-MARK-MASQ`,
			parsed: &Rule{
				Raw:        `-A KUBE-XLB-GNZBNJ2PO5MGZ6GT -m comment --comment "masquerade LOCAL traffic for ns2/svc2:p80 LB IP" -m addrtype --src-type LOCAL -j KUBE-MARK-MASQ`,
				Chain:      iptables.Chain("KUBE-XLB-GNZBNJ2PO5MGZ6GT"),
				Comment:    &IPTablesValue{Value: "masquerade LOCAL traffic for ns2/svc2:p80 LB IP"},
				SourceType: &IPTablesValue{Value: "LOCAL"},
				Jump:       &IPTablesValue{Value: "KUBE-MARK-MASQ"},
			},
		},
		{
			name: "not local destination",
			rule: `-A RULE-TYPE-NOT-CURRENTLY-USED-BY-KUBE-PROXY -m addrtype ! --dst-type LOCAL -j KUBE-MARK-MASQ`,
			parsed: &Rule{
				Raw:             `-A RULE-TYPE-NOT-CURRENTLY-USED-BY-KUBE-PROXY -m addrtype ! --dst-type LOCAL -j KUBE-MARK-MASQ`,
				Chain:           iptables.Chain("RULE-TYPE-NOT-CURRENTLY-USED-BY-KUBE-PROXY"),
				DestinationType: &IPTablesValue{Negated: true, Value: "LOCAL"},
				Jump:            &IPTablesValue{Value: "KUBE-MARK-MASQ"},
			},
		},
		{
			name: "destination IP/port",
			rule: `-A KUBE-SERVICES -m comment --comment "ns1/svc1:p80 cluster IP" -m tcp -p tcp -d 172.30.0.41 --dport 80 -j KUBE-SVC-XPGD46QRK7WJZT7O`,
			parsed: &Rule{
				Raw:                `-A KUBE-SERVICES -m comment --comment "ns1/svc1:p80 cluster IP" -m tcp -p tcp -d 172.30.0.41 --dport 80 -j KUBE-SVC-XPGD46QRK7WJZT7O`,
				Chain:              iptables.Chain("KUBE-SERVICES"),
				Comment:            &IPTablesValue{Value: "ns1/svc1:p80 cluster IP"},
				Protocol:           &IPTablesValue{Value: "tcp"},
				DestinationAddress: &IPTablesValue{Value: "172.30.0.41"},
				DestinationPort:    &IPTablesValue{Value: "80"},
				Jump:               &IPTablesValue{Value: "KUBE-SVC-XPGD46QRK7WJZT7O"},
			},
		},
		{
			name: "source IP",
			rule: `-A KUBE-SEP-SXIVWICOYRO3J4NJ -m comment --comment ns1/svc1:p80 -s 10.180.0.1 -j KUBE-MARK-MASQ`,
			parsed: &Rule{
				Raw:           `-A KUBE-SEP-SXIVWICOYRO3J4NJ -m comment --comment ns1/svc1:p80 -s 10.180.0.1 -j KUBE-MARK-MASQ`,
				Chain:         iptables.Chain("KUBE-SEP-SXIVWICOYRO3J4NJ"),
				Comment:       &IPTablesValue{Value: "ns1/svc1:p80"},
				SourceAddress: &IPTablesValue{Value: "10.180.0.1"},
				Jump:          &IPTablesValue{Value: "KUBE-MARK-MASQ"},
			},
		},
		{
			name: "not source IP",
			rule: `-A KUBE-SVC-XPGD46QRK7WJZT7O -m comment --comment "ns1/svc1:p80 cluster IP" -m tcp -p tcp -d 172.30.0.41 --dport 80 ! -s 10.0.0.0/8 -j KUBE-MARK-MASQ`,
			parsed: &Rule{
				Raw:                `-A KUBE-SVC-XPGD46QRK7WJZT7O -m comment --comment "ns1/svc1:p80 cluster IP" -m tcp -p tcp -d 172.30.0.41 --dport 80 ! -s 10.0.0.0/8 -j KUBE-MARK-MASQ`,
				Chain:              iptables.Chain("KUBE-SVC-XPGD46QRK7WJZT7O"),
				Comment:            &IPTablesValue{Value: "ns1/svc1:p80 cluster IP"},
				Protocol:           &IPTablesValue{Value: "tcp"},
				DestinationAddress: &IPTablesValue{Value: "172.30.0.41"},
				DestinationPort:    &IPTablesValue{Value: "80"},
				SourceAddress:      &IPTablesValue{Negated: true, Value: "10.0.0.0/8"},
				Jump:               &IPTablesValue{Value: "KUBE-MARK-MASQ"},
			},
		},
		{
			name: "affinity",
			rule: `-A KUBE-SVC-XPGD46QRK7WJZT7O -m comment --comment ns1/svc1:p80 -m recent --name KUBE-SEP-SXIVWICOYRO3J4NJ --rcheck --seconds 10800 --reap -j KUBE-SEP-SXIVWICOYRO3J4NJ`,
			parsed: &Rule{
				Raw:             `-A KUBE-SVC-XPGD46QRK7WJZT7O -m comment --comment ns1/svc1:p80 -m recent --name KUBE-SEP-SXIVWICOYRO3J4NJ --rcheck --seconds 10800 --reap -j KUBE-SEP-SXIVWICOYRO3J4NJ`,
				Chain:           iptables.Chain("KUBE-SVC-XPGD46QRK7WJZT7O"),
				Comment:         &IPTablesValue{Value: "ns1/svc1:p80"},
				AffinityName:    &IPTablesValue{Value: "KUBE-SEP-SXIVWICOYRO3J4NJ"},
				AffinitySeconds: &IPTablesValue{Value: "10800"},
				AffinityCheck:   utilpointer.Bool(true),
				AffinityReap:    utilpointer.Bool(true),
				Jump:            &IPTablesValue{Value: "KUBE-SEP-SXIVWICOYRO3J4NJ"},
			},
		},
		{
			name: "jump to DNAT",
			rule: `-A KUBE-SEP-SXIVWICOYRO3J4NJ -m comment --comment ns1/svc1:p80 -m tcp -p tcp -j DNAT --to-destination 10.180.0.1:80`,
			parsed: &Rule{
				Raw:             `-A KUBE-SEP-SXIVWICOYRO3J4NJ -m comment --comment ns1/svc1:p80 -m tcp -p tcp -j DNAT --to-destination 10.180.0.1:80`,
				Chain:           iptables.Chain("KUBE-SEP-SXIVWICOYRO3J4NJ"),
				Comment:         &IPTablesValue{Value: "ns1/svc1:p80"},
				Protocol:        &IPTablesValue{Value: "tcp"},
				Jump:            &IPTablesValue{Value: "DNAT"},
				DNATDestination: &IPTablesValue{Value: "10.180.0.1:80"},
			},
		},
		{
			name: "jump to endpoint",
			rule: `-A KUBE-SVC-4SW47YFZTEDKD3PK -m comment --comment ns4/svc4:p80 -m statistic --mode random --probability 0.5000000000 -j KUBE-SEP-UKSFD7AGPMPPLUHC`,
			parsed: &Rule{
				Raw:           `-A KUBE-SVC-4SW47YFZTEDKD3PK -m comment --comment ns4/svc4:p80 -m statistic --mode random --probability 0.5000000000 -j KUBE-SEP-UKSFD7AGPMPPLUHC`,
				Chain:         iptables.Chain("KUBE-SVC-4SW47YFZTEDKD3PK"),
				Comment:       &IPTablesValue{Value: "ns4/svc4:p80"},
				Probability:   &IPTablesValue{Value: "0.5000000000"},
				StatisticMode: &IPTablesValue{Value: "random"},
				Jump:          &IPTablesValue{Value: "KUBE-SEP-UKSFD7AGPMPPLUHC"},
			},
		},
		{
			name: "unrecognized arguments",
			rule: `-A KUBE-SVC-4SW47YFZTEDKD3PK -m comment --comment ns4/svc4:p80 -i eth0 -j KUBE-SEP-UKSFD7AGPMPPLUHC`,
			err:  `unrecognized parameter "-i"`,
		},
		{
			name:      "unrecognized arguments with strict=false",
			rule:      `-A KUBE-SVC-4SW47YFZTEDKD3PK -m comment --comment ns4/svc4:p80 -i eth0 -j KUBE-SEP-UKSFD7AGPMPPLUHC`,
			nonStrict: true,
			parsed: &Rule{
				Raw:     `-A KUBE-SVC-4SW47YFZTEDKD3PK -m comment --comment ns4/svc4:p80 -i eth0 -j KUBE-SEP-UKSFD7AGPMPPLUHC`,
				Chain:   iptables.Chain("KUBE-SVC-4SW47YFZTEDKD3PK"),
				Comment: &IPTablesValue{Value: "ns4/svc4:p80"},
				Jump:    &IPTablesValue{Value: "KUBE-SEP-UKSFD7AGPMPPLUHC"},
			},
		},
		{
			name: "bad use of !",
			rule: `-A KUBE-SVC-4SW47YFZTEDKD3PK -m comment --comment ns4/svc4:p80 ! -j KUBE-SEP-UKSFD7AGPMPPLUHC`,
			err:  `cannot negate parameter "-j"`,
		},
		{
			name: "missing argument",
			rule: `-A KUBE-SVC-4SW47YFZTEDKD3PK -m comment --comment ns4/svc4:p80 -j`,
			err:  `parameter "-j" requires an argument`,
		},
		{
			name: "negated bool arg",
			rule: `-A TEST -m recent ! --rcheck -j KUBE-SEP-SXIVWICOYRO3J4NJ`,
			parsed: &Rule{
				Raw:           `-A TEST -m recent ! --rcheck -j KUBE-SEP-SXIVWICOYRO3J4NJ`,
				Chain:         iptables.Chain("TEST"),
				AffinityCheck: utilpointer.Bool(false),
				Jump:          &IPTablesValue{Value: "KUBE-SEP-SXIVWICOYRO3J4NJ"},
			},
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			rule, err := ParseRule(testCase.rule, !testCase.nonStrict)
			if err != nil {
				if testCase.err == "" {
					t.Errorf("expected %+v, got error %q", testCase.parsed, err)
				} else if !strings.Contains(err.Error(), testCase.err) {
					t.Errorf("wrong error, expected %q got %q", testCase.err, err)
				}
			} else {
				if testCase.err != "" {
					t.Errorf("expected error %q, got %+v", testCase.err, rule)
				} else if !reflect.DeepEqual(rule, testCase.parsed) {
					t.Errorf("bad match: expected\n%+v\ngot\n%+v", testCase.parsed, rule)
				}
			}
		})
	}
}

// Helper for TestParseIPTablesDump. Obviously it should not be used in TestParseRule...
func mustParseRule(rule string) *Rule {
	parsed, err := ParseRule(rule, false)
	if err != nil {
		panic(fmt.Sprintf("failed to parse test case rule %q: %v", rule, err))
	}
	return parsed
}

func TestParseIPTablesDump(t *testing.T) {
	for _, tc := range []struct {
		name   string
		input  string
		output *IPTablesDump
		error  string
	}{
		{
			name: "basic test",
			input: dedent.Dedent(`
				*filter
				:KUBE-SERVICES - [0:0]
				:KUBE-EXTERNAL-SERVICES - [0:0]
				:KUBE-FORWARD - [0:0]
				:KUBE-NODEPORTS - [0:0]
				-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT
				-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
				COMMIT
				*nat
				:KUBE-SERVICES - [0:0]
				:KUBE-NODEPORTS - [0:0]
				:KUBE-POSTROUTING - [0:0]
				:KUBE-MARK-MASQ - [0:0]
				:KUBE-SVC-XPGD46QRK7WJZT7O - [0:0]
				:KUBE-SEP-SXIVWICOYRO3J4NJ - [0:0]
				-A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN
				-A KUBE-POSTROUTING -j MARK --xor-mark 0x4000
				-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE
				-A KUBE-MARK-MASQ -j MARK --or-mark 0x4000
				-A KUBE-SERVICES -m comment --comment "ns1/svc1:p80 cluster IP" -m tcp -p tcp -d 10.20.30.41 --dport 80 -j KUBE-SVC-XPGD46QRK7WJZT7O
				-A KUBE-SVC-XPGD46QRK7WJZT7O -m comment --comment "ns1/svc1:p80 cluster IP" -m tcp -p tcp -d 10.20.30.41 --dport 80 ! -s 10.0.0.0/24 -j KUBE-MARK-MASQ
				-A KUBE-SVC-XPGD46QRK7WJZT7O -m comment --comment ns1/svc1:p80 -j KUBE-SEP-SXIVWICOYRO3J4NJ
				-A KUBE-SEP-SXIVWICOYRO3J4NJ -m comment --comment ns1/svc1:p80 -s 10.180.0.1 -j KUBE-MARK-MASQ
				-A KUBE-SEP-SXIVWICOYRO3J4NJ -m comment --comment ns1/svc1:p80 -m tcp -p tcp -j DNAT --to-destination 10.180.0.1:80
				-A KUBE-SERVICES -m comment --comment "kubernetes service nodeports; NOTE: this must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS
				COMMIT
				`),
			output: &IPTablesDump{
				Tables: []Table{{
					Name: iptables.TableFilter,
					Chains: []Chain{{
						Name: iptables.Chain("KUBE-SERVICES"),
					}, {
						Name: iptables.Chain("KUBE-EXTERNAL-SERVICES"),
					}, {
						Name: iptables.Chain("KUBE-FORWARD"),
						Rules: []*Rule{
							mustParseRule(`-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP`),
							mustParseRule(`-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT`),
							mustParseRule(`-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT`),
						},
					}, {
						Name: iptables.Chain("KUBE-NODEPORTS"),
						Rules: []*Rule{
							mustParseRule(`-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT`),
						},
					}},
				}, {
					Name: iptables.TableNAT,
					Chains: []Chain{{
						Name: iptables.Chain("KUBE-SERVICES"),
						Rules: []*Rule{
							mustParseRule(`-A KUBE-SERVICES -m comment --comment "ns1/svc1:p80 cluster IP" -m tcp -p tcp -d 10.20.30.41 --dport 80 -j KUBE-SVC-XPGD46QRK7WJZT7O`),
							mustParseRule(`-A KUBE-SERVICES -m comment --comment "kubernetes service nodeports; NOTE: this must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS`),
						},
					}, {
						Name: iptables.Chain("KUBE-NODEPORTS"),
					}, {
						Name: iptables.Chain("KUBE-POSTROUTING"),
						Rules: []*Rule{
							mustParseRule(`-A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN`),
							mustParseRule(`-A KUBE-POSTROUTING -j MARK --xor-mark 0x4000`),
							mustParseRule(`-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE`),
						},
					}, {
						Name: iptables.Chain("KUBE-MARK-MASQ"),
						Rules: []*Rule{
							mustParseRule(`-A KUBE-MARK-MASQ -j MARK --or-mark 0x4000`),
						},
					}, {
						Name: iptables.Chain("KUBE-SVC-XPGD46QRK7WJZT7O"),
						Rules: []*Rule{
							mustParseRule(`-A KUBE-SVC-XPGD46QRK7WJZT7O -m comment --comment "ns1/svc1:p80 cluster IP" -m tcp -p tcp -d 10.20.30.41 --dport 80 ! -s 10.0.0.0/24 -j KUBE-MARK-MASQ`),
							mustParseRule(`-A KUBE-SVC-XPGD46QRK7WJZT7O -m comment --comment ns1/svc1:p80 -j KUBE-SEP-SXIVWICOYRO3J4NJ`),
						},
					}, {
						Name: iptables.Chain("KUBE-SEP-SXIVWICOYRO3J4NJ"),
						Rules: []*Rule{
							mustParseRule(`-A KUBE-SEP-SXIVWICOYRO3J4NJ -m comment --comment ns1/svc1:p80 -s 10.180.0.1 -j KUBE-MARK-MASQ`),
							mustParseRule(`-A KUBE-SEP-SXIVWICOYRO3J4NJ -m comment --comment ns1/svc1:p80 -m tcp -p tcp -j DNAT --to-destination 10.180.0.1:80`),
						},
					}},
				}},
			},
		},
		{
			name: "deletion",
			input: dedent.Dedent(`
				*nat
				:KUBE-SERVICES - [0:0]
				:KUBE-SVC-XPGD46QRK7WJZT7O - [0:0]
				:KUBE-SEP-SXIVWICOYRO3J4NJ - [0:0]
				-X KUBE-SVC-XPGD46QRK7WJZT7O
				-X KUBE-SEP-SXIVWICOYRO3J4NJ
				-A KUBE-SERVICES -m comment --comment "kubernetes service nodeports; NOTE: this must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS
				COMMIT
				`),
			output: &IPTablesDump{
				Tables: []Table{{
					Name: iptables.TableNAT,
					Chains: []Chain{{
						Name: iptables.Chain("KUBE-SERVICES"),
						Rules: []*Rule{
							mustParseRule(`-A KUBE-SERVICES -m comment --comment "kubernetes service nodeports; NOTE: this must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS`),
						},
					}, {
						Name:    iptables.Chain("KUBE-SVC-XPGD46QRK7WJZT7O"),
						Deleted: true,
					}, {
						Name:    iptables.Chain("KUBE-SEP-SXIVWICOYRO3J4NJ"),
						Deleted: true,
					}},
				}},
			},
		},
		{
			name: "whitespace and comments",
			input: dedent.Dedent(`
				# Generated by iptables-save v1.8.7 on Mon May  9 11:22:21 2022
				# (not really...)
				*filter
				:KUBE-SERVICES - [0:0]
				:KUBE-EXTERNAL-SERVICES - [0:0]

				:KUBE-FORWARD - [0:0]
				:KUBE-NODEPORTS - [0:0]
				-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT
				  -A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
				# This rule does a thing
				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
				COMMIT
				# Completed on Mon May  9 11:22:21 2022
				`),
			output: &IPTablesDump{
				Tables: []Table{{
					Name: iptables.TableFilter,
					Chains: []Chain{{
						Name: iptables.Chain("KUBE-SERVICES"),
					}, {
						Name: iptables.Chain("KUBE-EXTERNAL-SERVICES"),
					}, {
						Name: iptables.Chain("KUBE-FORWARD"),
						Rules: []*Rule{
							mustParseRule(`-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP`),
							mustParseRule(`-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT`),
							mustParseRule(`-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT`),
						},
					}, {
						Name: iptables.Chain("KUBE-NODEPORTS"),
						Rules: []*Rule{
							mustParseRule(`-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT`),
						},
					}},
				}},
			},
		},
		{
			name: "no COMMIT line",
			input: dedent.Dedent(`
				*filter
				:KUBE-SERVICES - [0:0]
				:KUBE-EXTERNAL-SERVICES - [0:0]
				:KUBE-FORWARD - [0:0]
				:KUBE-NODEPORTS - [0:0]
				-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT
				-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
				`),
			error: "no COMMIT line?",
		},
		{
			name: "two tables, no second COMMIT line",
			input: dedent.Dedent(`
				*filter
				:KUBE-SERVICES - [0:0]
				:KUBE-EXTERNAL-SERVICES - [0:0]
				:KUBE-FORWARD - [0:0]
				:KUBE-NODEPORTS - [0:0]
				-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT
				-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
				COMMIT
				*nat
				:KUBE-SERVICES - [0:0]
				:KUBE-NODEPORTS - [0:0]
				:KUBE-POSTROUTING - [0:0]
				:KUBE-MARK-MASQ - [0:0]
				:KUBE-SVC-XPGD46QRK7WJZT7O - [0:0]
				:KUBE-SEP-SXIVWICOYRO3J4NJ - [0:0]
				-A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN
				-A KUBE-POSTROUTING -j MARK --xor-mark 0x4000
				-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE
				-A KUBE-MARK-MASQ -j MARK --or-mark 0x4000
				-A KUBE-SERVICES -m comment --comment "ns1/svc1:p80 cluster IP" -m tcp -p tcp -d 10.20.30.41 --dport 80 -j KUBE-SVC-XPGD46QRK7WJZT7O
				-A KUBE-SVC-XPGD46QRK7WJZT7O -m comment --comment "ns1/svc1:p80 cluster IP" -m tcp -p tcp -d 10.20.30.41 --dport 80 ! -s 10.0.0.0/24 -j KUBE-MARK-MASQ
				-A KUBE-SVC-XPGD46QRK7WJZT7O -m comment --comment ns1/svc1:p80 -j KUBE-SEP-SXIVWICOYRO3J4NJ
				-A KUBE-SEP-SXIVWICOYRO3J4NJ -m comment --comment ns1/svc1:p80 -s 10.180.0.1 -j KUBE-MARK-MASQ
				-A KUBE-SEP-SXIVWICOYRO3J4NJ -m comment --comment ns1/svc1:p80 -m tcp -p tcp -j DNAT --to-destination 10.180.0.1:80
				-A KUBE-SERVICES -m comment --comment "kubernetes service nodeports; NOTE: this must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS
				`),
			error: "no COMMIT line?",
		},
		{
			name: "two tables, no second header line",
			input: dedent.Dedent(`
				*filter
				:KUBE-SERVICES - [0:0]
				:KUBE-EXTERNAL-SERVICES - [0:0]
				:KUBE-FORWARD - [0:0]
				:KUBE-NODEPORTS - [0:0]
				-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT
				-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
				COMMIT
				:KUBE-SERVICES - [0:0]
				:KUBE-NODEPORTS - [0:0]
				:KUBE-POSTROUTING - [0:0]
				:KUBE-MARK-MASQ - [0:0]
				:KUBE-SVC-XPGD46QRK7WJZT7O - [0:0]
				:KUBE-SEP-SXIVWICOYRO3J4NJ - [0:0]
				-A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN
				-A KUBE-POSTROUTING -j MARK --xor-mark 0x4000
				-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE
				-A KUBE-MARK-MASQ -j MARK --or-mark 0x4000
				-A KUBE-SERVICES -m comment --comment "ns1/svc1:p80 cluster IP" -m tcp -p tcp -d 10.20.30.41 --dport 80 -j KUBE-SVC-XPGD46QRK7WJZT7O
				-A KUBE-SVC-XPGD46QRK7WJZT7O -m comment --comment "ns1/svc1:p80 cluster IP" -m tcp -p tcp -d 10.20.30.41 --dport 80 ! -s 10.0.0.0/24 -j KUBE-MARK-MASQ
				-A KUBE-SVC-XPGD46QRK7WJZT7O -m comment --comment ns1/svc1:p80 -j KUBE-SEP-SXIVWICOYRO3J4NJ
				-A KUBE-SEP-SXIVWICOYRO3J4NJ -m comment --comment ns1/svc1:p80 -s 10.180.0.1 -j KUBE-MARK-MASQ
				-A KUBE-SEP-SXIVWICOYRO3J4NJ -m comment --comment ns1/svc1:p80 -m tcp -p tcp -j DNAT --to-destination 10.180.0.1:80
				-A KUBE-SERVICES -m comment --comment "kubernetes service nodeports; NOTE: this must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS
				COMMIT
				`),
			error: "not a table name",
		},
		{
			name: "trailing junk",
			input: dedent.Dedent(`
				*filter
				:KUBE-SERVICES - [0:0]
				:KUBE-EXTERNAL-SERVICES - [0:0]
				:KUBE-FORWARD - [0:0]
				:KUBE-NODEPORTS - [0:0]
				-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT
				-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
				COMMIT
				*nat
				:KUBE-SERVICES - [0:0]
				:KUBE-EXTERNAL-SERVICES - [0:0]
				:KUBE-FORWARD - [0:0]
				:KUBE-NODEPORTS - [0:0]
				-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT
				-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
				COMMIT
				junk
				`),
			error: `table 3 starts with "junk"`,
		},
		{
			name: "add to missing chain",
			input: dedent.Dedent(`
				*filter
				:KUBE-SERVICES - [0:0]
				:KUBE-EXTERNAL-SERVICES - [0:0]
				:KUBE-NODEPORTS - [0:0]
				-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT
				-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
				COMMIT
				`),
			error: `no such chain "KUBE-FORWARD"`,
		},
		{
			name: "add to deleted chain",
			input: dedent.Dedent(`
				*filter
				:KUBE-SERVICES - [0:0]
				:KUBE-EXTERNAL-SERVICES - [0:0]
				:KUBE-FORWARD - [0:0]
				:KUBE-NODEPORTS - [0:0]
				-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT
				-X KUBE-FORWARD
				-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
				COMMIT
				`),
			error: `cannot add rules to deleted chain`,
		},
		{
			name: "deleted non-empty chain",
			input: dedent.Dedent(`
				*filter
				:KUBE-SERVICES - [0:0]
				:KUBE-EXTERNAL-SERVICES - [0:0]
				:KUBE-FORWARD - [0:0]
				:KUBE-NODEPORTS - [0:0]
				-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT
				-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
				-X KUBE-FORWARD
				COMMIT
				`),
			error: `cannot delete chain "KUBE-FORWARD" after adding rules`,
		},
		{
			name: "junk rule",
			input: dedent.Dedent(`
				*filter
				:KUBE-SERVICES - [0:0]
				:KUBE-EXTERNAL-SERVICES - [0:0]
				:KUBE-FORWARD - [0:0]
				:KUBE-NODEPORTS - [0:0]
				-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT
				-Q KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
				COMMIT
				`),
			error: `"-Q KUBE-FORWARD`,
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			dump, err := ParseIPTablesDump(tc.input)
			if err == nil {
				if tc.error != "" {
					t.Errorf("unexpectedly did not get error")
				} else if !reflect.DeepEqual(tc.output, dump) {
					t.Errorf("bad output: expected %#v got %#v", tc.output, dump)
				}
			} else {
				if tc.error == "" {
					t.Errorf("got unexpected error: %v", err)
				} else if !strings.Contains(err.Error(), tc.error) {
					t.Errorf("got wrong error: %v (expected %q)", err, tc.error)
				}
			}
		})
	}
}