k8s.io/kubernetes@v1.29.3/pkg/util/iptables/testing/parse_test.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package testing
    18  
    19  import (
    20  	"fmt"
    21  	"reflect"
    22  	"strings"
    23  	"testing"
    24  
    25  	"github.com/lithammer/dedent"
    26  
    27  	"k8s.io/kubernetes/pkg/util/iptables"
    28  	utilpointer "k8s.io/utils/pointer"
    29  )
    30  
    31  func TestParseRule(t *testing.T) {
    32  	testCases := []struct {
    33  		name      string
    34  		rule      string
    35  		parsed    *Rule
    36  		nonStrict bool
    37  		err       string
    38  	}{
    39  		{
    40  			name: "basic rule",
    41  			rule: `-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT`,
    42  			parsed: &Rule{
    43  				Raw:             `-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT`,
    44  				Chain:           iptables.Chain("KUBE-NODEPORTS"),
    45  				Comment:         &IPTablesValue{Value: "ns2/svc2:p80 health check node port"},
    46  				Protocol:        &IPTablesValue{Value: "tcp"},
    47  				DestinationPort: &IPTablesValue{Value: "30000"},
    48  				Jump:            &IPTablesValue{Value: "ACCEPT"},
    49  			},
    50  		},
    51  		{
    52  			name: "addRuleToChainRegex requires an actual rule, not just a chain name",
    53  			rule: `-A KUBE-NODEPORTS`,
    54  			err:  `(no match rules)`,
    55  		},
    56  		{
    57  			name: "ParseRule only parses adds",
    58  			rule: `-D KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT`,
    59  			err:  `(does not start with "-A CHAIN")`,
    60  		},
    61  		{
    62  			name: "unquoted comment",
    63  			rule: `-A KUBE-SVC-XPGD46QRK7WJZT7O -m comment --comment ns1/svc1:p80 -j KUBE-SEP-SXIVWICOYRO3J4NJ`,
    64  			parsed: &Rule{
    65  				Raw:     `-A KUBE-SVC-XPGD46QRK7WJZT7O -m comment --comment ns1/svc1:p80 -j KUBE-SEP-SXIVWICOYRO3J4NJ`,
    66  				Chain:   iptables.Chain("KUBE-SVC-XPGD46QRK7WJZT7O"),
    67  				Comment: &IPTablesValue{Value: "ns1/svc1:p80"},
    68  				Jump:    &IPTablesValue{Value: "KUBE-SEP-SXIVWICOYRO3J4NJ"},
    69  			},
    70  		},
    71  		{
    72  			name: "local source",
    73  			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`,
    74  			parsed: &Rule{
    75  				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`,
    76  				Chain:      iptables.Chain("KUBE-XLB-GNZBNJ2PO5MGZ6GT"),
    77  				Comment:    &IPTablesValue{Value: "masquerade LOCAL traffic for ns2/svc2:p80 LB IP"},
    78  				SourceType: &IPTablesValue{Value: "LOCAL"},
    79  				Jump:       &IPTablesValue{Value: "KUBE-MARK-MASQ"},
    80  			},
    81  		},
    82  		{
    83  			name: "not local destination",
    84  			rule: `-A RULE-TYPE-NOT-CURRENTLY-USED-BY-KUBE-PROXY -m addrtype ! --dst-type LOCAL -j KUBE-MARK-MASQ`,
    85  			parsed: &Rule{
    86  				Raw:             `-A RULE-TYPE-NOT-CURRENTLY-USED-BY-KUBE-PROXY -m addrtype ! --dst-type LOCAL -j KUBE-MARK-MASQ`,
    87  				Chain:           iptables.Chain("RULE-TYPE-NOT-CURRENTLY-USED-BY-KUBE-PROXY"),
    88  				DestinationType: &IPTablesValue{Negated: true, Value: "LOCAL"},
    89  				Jump:            &IPTablesValue{Value: "KUBE-MARK-MASQ"},
    90  			},
    91  		},
    92  		{
    93  			name: "destination IP/port",
    94  			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`,
    95  			parsed: &Rule{
    96  				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`,
    97  				Chain:              iptables.Chain("KUBE-SERVICES"),
    98  				Comment:            &IPTablesValue{Value: "ns1/svc1:p80 cluster IP"},
    99  				Protocol:           &IPTablesValue{Value: "tcp"},
   100  				DestinationAddress: &IPTablesValue{Value: "172.30.0.41"},
   101  				DestinationPort:    &IPTablesValue{Value: "80"},
   102  				Jump:               &IPTablesValue{Value: "KUBE-SVC-XPGD46QRK7WJZT7O"},
   103  			},
   104  		},
   105  		{
   106  			name: "source IP",
   107  			rule: `-A KUBE-SEP-SXIVWICOYRO3J4NJ -m comment --comment ns1/svc1:p80 -s 10.180.0.1 -j KUBE-MARK-MASQ`,
   108  			parsed: &Rule{
   109  				Raw:           `-A KUBE-SEP-SXIVWICOYRO3J4NJ -m comment --comment ns1/svc1:p80 -s 10.180.0.1 -j KUBE-MARK-MASQ`,
   110  				Chain:         iptables.Chain("KUBE-SEP-SXIVWICOYRO3J4NJ"),
   111  				Comment:       &IPTablesValue{Value: "ns1/svc1:p80"},
   112  				SourceAddress: &IPTablesValue{Value: "10.180.0.1"},
   113  				Jump:          &IPTablesValue{Value: "KUBE-MARK-MASQ"},
   114  			},
   115  		},
   116  		{
   117  			name: "not source IP",
   118  			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`,
   119  			parsed: &Rule{
   120  				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`,
   121  				Chain:              iptables.Chain("KUBE-SVC-XPGD46QRK7WJZT7O"),
   122  				Comment:            &IPTablesValue{Value: "ns1/svc1:p80 cluster IP"},
   123  				Protocol:           &IPTablesValue{Value: "tcp"},
   124  				DestinationAddress: &IPTablesValue{Value: "172.30.0.41"},
   125  				DestinationPort:    &IPTablesValue{Value: "80"},
   126  				SourceAddress:      &IPTablesValue{Negated: true, Value: "10.0.0.0/8"},
   127  				Jump:               &IPTablesValue{Value: "KUBE-MARK-MASQ"},
   128  			},
   129  		},
   130  		{
   131  			name: "affinity",
   132  			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`,
   133  			parsed: &Rule{
   134  				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`,
   135  				Chain:           iptables.Chain("KUBE-SVC-XPGD46QRK7WJZT7O"),
   136  				Comment:         &IPTablesValue{Value: "ns1/svc1:p80"},
   137  				AffinityName:    &IPTablesValue{Value: "KUBE-SEP-SXIVWICOYRO3J4NJ"},
   138  				AffinitySeconds: &IPTablesValue{Value: "10800"},
   139  				AffinityCheck:   utilpointer.Bool(true),
   140  				AffinityReap:    utilpointer.Bool(true),
   141  				Jump:            &IPTablesValue{Value: "KUBE-SEP-SXIVWICOYRO3J4NJ"},
   142  			},
   143  		},
   144  		{
   145  			name: "jump to DNAT",
   146  			rule: `-A KUBE-SEP-SXIVWICOYRO3J4NJ -m comment --comment ns1/svc1:p80 -m tcp -p tcp -j DNAT --to-destination 10.180.0.1:80`,
   147  			parsed: &Rule{
   148  				Raw:             `-A KUBE-SEP-SXIVWICOYRO3J4NJ -m comment --comment ns1/svc1:p80 -m tcp -p tcp -j DNAT --to-destination 10.180.0.1:80`,
   149  				Chain:           iptables.Chain("KUBE-SEP-SXIVWICOYRO3J4NJ"),
   150  				Comment:         &IPTablesValue{Value: "ns1/svc1:p80"},
   151  				Protocol:        &IPTablesValue{Value: "tcp"},
   152  				Jump:            &IPTablesValue{Value: "DNAT"},
   153  				DNATDestination: &IPTablesValue{Value: "10.180.0.1:80"},
   154  			},
   155  		},
   156  		{
   157  			name: "jump to endpoint",
   158  			rule: `-A KUBE-SVC-4SW47YFZTEDKD3PK -m comment --comment ns4/svc4:p80 -m statistic --mode random --probability 0.5000000000 -j KUBE-SEP-UKSFD7AGPMPPLUHC`,
   159  			parsed: &Rule{
   160  				Raw:           `-A KUBE-SVC-4SW47YFZTEDKD3PK -m comment --comment ns4/svc4:p80 -m statistic --mode random --probability 0.5000000000 -j KUBE-SEP-UKSFD7AGPMPPLUHC`,
   161  				Chain:         iptables.Chain("KUBE-SVC-4SW47YFZTEDKD3PK"),
   162  				Comment:       &IPTablesValue{Value: "ns4/svc4:p80"},
   163  				Probability:   &IPTablesValue{Value: "0.5000000000"},
   164  				StatisticMode: &IPTablesValue{Value: "random"},
   165  				Jump:          &IPTablesValue{Value: "KUBE-SEP-UKSFD7AGPMPPLUHC"},
   166  			},
   167  		},
   168  		{
   169  			name: "unrecognized arguments",
   170  			rule: `-A KUBE-SVC-4SW47YFZTEDKD3PK -m comment --comment ns4/svc4:p80 -i eth0 -j KUBE-SEP-UKSFD7AGPMPPLUHC`,
   171  			err:  `unrecognized parameter "-i"`,
   172  		},
   173  		{
   174  			name:      "unrecognized arguments with strict=false",
   175  			rule:      `-A KUBE-SVC-4SW47YFZTEDKD3PK -m comment --comment ns4/svc4:p80 -i eth0 -j KUBE-SEP-UKSFD7AGPMPPLUHC`,
   176  			nonStrict: true,
   177  			parsed: &Rule{
   178  				Raw:     `-A KUBE-SVC-4SW47YFZTEDKD3PK -m comment --comment ns4/svc4:p80 -i eth0 -j KUBE-SEP-UKSFD7AGPMPPLUHC`,
   179  				Chain:   iptables.Chain("KUBE-SVC-4SW47YFZTEDKD3PK"),
   180  				Comment: &IPTablesValue{Value: "ns4/svc4:p80"},
   181  				Jump:    &IPTablesValue{Value: "KUBE-SEP-UKSFD7AGPMPPLUHC"},
   182  			},
   183  		},
   184  		{
   185  			name: "bad use of !",
   186  			rule: `-A KUBE-SVC-4SW47YFZTEDKD3PK -m comment --comment ns4/svc4:p80 ! -j KUBE-SEP-UKSFD7AGPMPPLUHC`,
   187  			err:  `cannot negate parameter "-j"`,
   188  		},
   189  		{
   190  			name: "missing argument",
   191  			rule: `-A KUBE-SVC-4SW47YFZTEDKD3PK -m comment --comment ns4/svc4:p80 -j`,
   192  			err:  `parameter "-j" requires an argument`,
   193  		},
   194  		{
   195  			name: "negated bool arg",
   196  			rule: `-A TEST -m recent ! --rcheck -j KUBE-SEP-SXIVWICOYRO3J4NJ`,
   197  			parsed: &Rule{
   198  				Raw:           `-A TEST -m recent ! --rcheck -j KUBE-SEP-SXIVWICOYRO3J4NJ`,
   199  				Chain:         iptables.Chain("TEST"),
   200  				AffinityCheck: utilpointer.Bool(false),
   201  				Jump:          &IPTablesValue{Value: "KUBE-SEP-SXIVWICOYRO3J4NJ"},
   202  			},
   203  		},
   204  	}
   205  
   206  	for _, testCase := range testCases {
   207  		t.Run(testCase.name, func(t *testing.T) {
   208  			rule, err := ParseRule(testCase.rule, !testCase.nonStrict)
   209  			if err != nil {
   210  				if testCase.err == "" {
   211  					t.Errorf("expected %+v, got error %q", testCase.parsed, err)
   212  				} else if !strings.Contains(err.Error(), testCase.err) {
   213  					t.Errorf("wrong error, expected %q got %q", testCase.err, err)
   214  				}
   215  			} else {
   216  				if testCase.err != "" {
   217  					t.Errorf("expected error %q, got %+v", testCase.err, rule)
   218  				} else if !reflect.DeepEqual(rule, testCase.parsed) {
   219  					t.Errorf("bad match: expected\n%+v\ngot\n%+v", testCase.parsed, rule)
   220  				}
   221  			}
   222  		})
   223  	}
   224  }
   225  
   226  // Helper for TestParseIPTablesDump. Obviously it should not be used in TestParseRule...
   227  func mustParseRule(rule string) *Rule {
   228  	parsed, err := ParseRule(rule, false)
   229  	if err != nil {
   230  		panic(fmt.Sprintf("failed to parse test case rule %q: %v", rule, err))
   231  	}
   232  	return parsed
   233  }
   234  
   235  func TestParseIPTablesDump(t *testing.T) {
   236  	for _, tc := range []struct {
   237  		name   string
   238  		input  string
   239  		output *IPTablesDump
   240  		error  string
   241  	}{
   242  		{
   243  			name: "basic test",
   244  			input: dedent.Dedent(`
   245  				*filter
   246  				:KUBE-SERVICES - [0:0]
   247  				:KUBE-EXTERNAL-SERVICES - [0:0]
   248  				:KUBE-FORWARD - [0:0]
   249  				:KUBE-NODEPORTS - [0:0]
   250  				-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT
   251  				-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
   252  				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
   253  				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
   254  				COMMIT
   255  				*nat
   256  				:KUBE-SERVICES - [0:0]
   257  				:KUBE-NODEPORTS - [0:0]
   258  				:KUBE-POSTROUTING - [0:0]
   259  				:KUBE-MARK-MASQ - [0:0]
   260  				:KUBE-SVC-XPGD46QRK7WJZT7O - [0:0]
   261  				:KUBE-SEP-SXIVWICOYRO3J4NJ - [0:0]
   262  				-A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN
   263  				-A KUBE-POSTROUTING -j MARK --xor-mark 0x4000
   264  				-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE
   265  				-A KUBE-MARK-MASQ -j MARK --or-mark 0x4000
   266  				-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
   267  				-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
   268  				-A KUBE-SVC-XPGD46QRK7WJZT7O -m comment --comment ns1/svc1:p80 -j KUBE-SEP-SXIVWICOYRO3J4NJ
   269  				-A KUBE-SEP-SXIVWICOYRO3J4NJ -m comment --comment ns1/svc1:p80 -s 10.180.0.1 -j KUBE-MARK-MASQ
   270  				-A KUBE-SEP-SXIVWICOYRO3J4NJ -m comment --comment ns1/svc1:p80 -m tcp -p tcp -j DNAT --to-destination 10.180.0.1:80
   271  				-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
   272  				COMMIT
   273  				`),
   274  			output: &IPTablesDump{
   275  				Tables: []Table{{
   276  					Name: iptables.TableFilter,
   277  					Chains: []Chain{{
   278  						Name: iptables.Chain("KUBE-SERVICES"),
   279  					}, {
   280  						Name: iptables.Chain("KUBE-EXTERNAL-SERVICES"),
   281  					}, {
   282  						Name: iptables.Chain("KUBE-FORWARD"),
   283  						Rules: []*Rule{
   284  							mustParseRule(`-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP`),
   285  							mustParseRule(`-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT`),
   286  							mustParseRule(`-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT`),
   287  						},
   288  					}, {
   289  						Name: iptables.Chain("KUBE-NODEPORTS"),
   290  						Rules: []*Rule{
   291  							mustParseRule(`-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT`),
   292  						},
   293  					}},
   294  				}, {
   295  					Name: iptables.TableNAT,
   296  					Chains: []Chain{{
   297  						Name: iptables.Chain("KUBE-SERVICES"),
   298  						Rules: []*Rule{
   299  							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`),
   300  							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`),
   301  						},
   302  					}, {
   303  						Name: iptables.Chain("KUBE-NODEPORTS"),
   304  					}, {
   305  						Name: iptables.Chain("KUBE-POSTROUTING"),
   306  						Rules: []*Rule{
   307  							mustParseRule(`-A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN`),
   308  							mustParseRule(`-A KUBE-POSTROUTING -j MARK --xor-mark 0x4000`),
   309  							mustParseRule(`-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE`),
   310  						},
   311  					}, {
   312  						Name: iptables.Chain("KUBE-MARK-MASQ"),
   313  						Rules: []*Rule{
   314  							mustParseRule(`-A KUBE-MARK-MASQ -j MARK --or-mark 0x4000`),
   315  						},
   316  					}, {
   317  						Name: iptables.Chain("KUBE-SVC-XPGD46QRK7WJZT7O"),
   318  						Rules: []*Rule{
   319  							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`),
   320  							mustParseRule(`-A KUBE-SVC-XPGD46QRK7WJZT7O -m comment --comment ns1/svc1:p80 -j KUBE-SEP-SXIVWICOYRO3J4NJ`),
   321  						},
   322  					}, {
   323  						Name: iptables.Chain("KUBE-SEP-SXIVWICOYRO3J4NJ"),
   324  						Rules: []*Rule{
   325  							mustParseRule(`-A KUBE-SEP-SXIVWICOYRO3J4NJ -m comment --comment ns1/svc1:p80 -s 10.180.0.1 -j KUBE-MARK-MASQ`),
   326  							mustParseRule(`-A KUBE-SEP-SXIVWICOYRO3J4NJ -m comment --comment ns1/svc1:p80 -m tcp -p tcp -j DNAT --to-destination 10.180.0.1:80`),
   327  						},
   328  					}},
   329  				}},
   330  			},
   331  		},
   332  		{
   333  			name: "deletion",
   334  			input: dedent.Dedent(`
   335  				*nat
   336  				:KUBE-SERVICES - [0:0]
   337  				:KUBE-SVC-XPGD46QRK7WJZT7O - [0:0]
   338  				:KUBE-SEP-SXIVWICOYRO3J4NJ - [0:0]
   339  				-X KUBE-SVC-XPGD46QRK7WJZT7O
   340  				-X KUBE-SEP-SXIVWICOYRO3J4NJ
   341  				-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
   342  				COMMIT
   343  				`),
   344  			output: &IPTablesDump{
   345  				Tables: []Table{{
   346  					Name: iptables.TableNAT,
   347  					Chains: []Chain{{
   348  						Name: iptables.Chain("KUBE-SERVICES"),
   349  						Rules: []*Rule{
   350  							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`),
   351  						},
   352  					}, {
   353  						Name:    iptables.Chain("KUBE-SVC-XPGD46QRK7WJZT7O"),
   354  						Deleted: true,
   355  					}, {
   356  						Name:    iptables.Chain("KUBE-SEP-SXIVWICOYRO3J4NJ"),
   357  						Deleted: true,
   358  					}},
   359  				}},
   360  			},
   361  		},
   362  		{
   363  			name: "whitespace and comments",
   364  			input: dedent.Dedent(`
   365  				# Generated by iptables-save v1.8.7 on Mon May  9 11:22:21 2022
   366  				# (not really...)
   367  				*filter
   368  				:KUBE-SERVICES - [0:0]
   369  				:KUBE-EXTERNAL-SERVICES - [0:0]
   370  
   371  				:KUBE-FORWARD - [0:0]
   372  				:KUBE-NODEPORTS - [0:0]
   373  				-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT
   374  				  -A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
   375  				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
   376  				# This rule does a thing
   377  				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
   378  				COMMIT
   379  				# Completed on Mon May  9 11:22:21 2022
   380  				`),
   381  			output: &IPTablesDump{
   382  				Tables: []Table{{
   383  					Name: iptables.TableFilter,
   384  					Chains: []Chain{{
   385  						Name: iptables.Chain("KUBE-SERVICES"),
   386  					}, {
   387  						Name: iptables.Chain("KUBE-EXTERNAL-SERVICES"),
   388  					}, {
   389  						Name: iptables.Chain("KUBE-FORWARD"),
   390  						Rules: []*Rule{
   391  							mustParseRule(`-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP`),
   392  							mustParseRule(`-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT`),
   393  							mustParseRule(`-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT`),
   394  						},
   395  					}, {
   396  						Name: iptables.Chain("KUBE-NODEPORTS"),
   397  						Rules: []*Rule{
   398  							mustParseRule(`-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT`),
   399  						},
   400  					}},
   401  				}},
   402  			},
   403  		},
   404  		{
   405  			name: "no COMMIT line",
   406  			input: dedent.Dedent(`
   407  				*filter
   408  				:KUBE-SERVICES - [0:0]
   409  				:KUBE-EXTERNAL-SERVICES - [0:0]
   410  				:KUBE-FORWARD - [0:0]
   411  				:KUBE-NODEPORTS - [0:0]
   412  				-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT
   413  				-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
   414  				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
   415  				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
   416  				`),
   417  			error: "no COMMIT line?",
   418  		},
   419  		{
   420  			name: "two tables, no second COMMIT line",
   421  			input: dedent.Dedent(`
   422  				*filter
   423  				:KUBE-SERVICES - [0:0]
   424  				:KUBE-EXTERNAL-SERVICES - [0:0]
   425  				:KUBE-FORWARD - [0:0]
   426  				:KUBE-NODEPORTS - [0:0]
   427  				-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT
   428  				-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
   429  				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
   430  				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
   431  				COMMIT
   432  				*nat
   433  				:KUBE-SERVICES - [0:0]
   434  				:KUBE-NODEPORTS - [0:0]
   435  				:KUBE-POSTROUTING - [0:0]
   436  				:KUBE-MARK-MASQ - [0:0]
   437  				:KUBE-SVC-XPGD46QRK7WJZT7O - [0:0]
   438  				:KUBE-SEP-SXIVWICOYRO3J4NJ - [0:0]
   439  				-A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN
   440  				-A KUBE-POSTROUTING -j MARK --xor-mark 0x4000
   441  				-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE
   442  				-A KUBE-MARK-MASQ -j MARK --or-mark 0x4000
   443  				-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
   444  				-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
   445  				-A KUBE-SVC-XPGD46QRK7WJZT7O -m comment --comment ns1/svc1:p80 -j KUBE-SEP-SXIVWICOYRO3J4NJ
   446  				-A KUBE-SEP-SXIVWICOYRO3J4NJ -m comment --comment ns1/svc1:p80 -s 10.180.0.1 -j KUBE-MARK-MASQ
   447  				-A KUBE-SEP-SXIVWICOYRO3J4NJ -m comment --comment ns1/svc1:p80 -m tcp -p tcp -j DNAT --to-destination 10.180.0.1:80
   448  				-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
   449  				`),
   450  			error: "no COMMIT line?",
   451  		},
   452  		{
   453  			name: "two tables, no second header line",
   454  			input: dedent.Dedent(`
   455  				*filter
   456  				:KUBE-SERVICES - [0:0]
   457  				:KUBE-EXTERNAL-SERVICES - [0:0]
   458  				:KUBE-FORWARD - [0:0]
   459  				:KUBE-NODEPORTS - [0:0]
   460  				-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT
   461  				-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
   462  				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
   463  				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
   464  				COMMIT
   465  				:KUBE-SERVICES - [0:0]
   466  				:KUBE-NODEPORTS - [0:0]
   467  				:KUBE-POSTROUTING - [0:0]
   468  				:KUBE-MARK-MASQ - [0:0]
   469  				:KUBE-SVC-XPGD46QRK7WJZT7O - [0:0]
   470  				:KUBE-SEP-SXIVWICOYRO3J4NJ - [0:0]
   471  				-A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN
   472  				-A KUBE-POSTROUTING -j MARK --xor-mark 0x4000
   473  				-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE
   474  				-A KUBE-MARK-MASQ -j MARK --or-mark 0x4000
   475  				-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
   476  				-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
   477  				-A KUBE-SVC-XPGD46QRK7WJZT7O -m comment --comment ns1/svc1:p80 -j KUBE-SEP-SXIVWICOYRO3J4NJ
   478  				-A KUBE-SEP-SXIVWICOYRO3J4NJ -m comment --comment ns1/svc1:p80 -s 10.180.0.1 -j KUBE-MARK-MASQ
   479  				-A KUBE-SEP-SXIVWICOYRO3J4NJ -m comment --comment ns1/svc1:p80 -m tcp -p tcp -j DNAT --to-destination 10.180.0.1:80
   480  				-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
   481  				COMMIT
   482  				`),
   483  			error: "not a table name",
   484  		},
   485  		{
   486  			name: "trailing junk",
   487  			input: dedent.Dedent(`
   488  				*filter
   489  				:KUBE-SERVICES - [0:0]
   490  				:KUBE-EXTERNAL-SERVICES - [0:0]
   491  				:KUBE-FORWARD - [0:0]
   492  				:KUBE-NODEPORTS - [0:0]
   493  				-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT
   494  				-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
   495  				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
   496  				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
   497  				COMMIT
   498  				*nat
   499  				:KUBE-SERVICES - [0:0]
   500  				:KUBE-EXTERNAL-SERVICES - [0:0]
   501  				:KUBE-FORWARD - [0:0]
   502  				:KUBE-NODEPORTS - [0:0]
   503  				-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT
   504  				-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
   505  				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
   506  				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
   507  				COMMIT
   508  				junk
   509  				`),
   510  			error: `table 3 starts with "junk"`,
   511  		},
   512  		{
   513  			name: "add to missing chain",
   514  			input: dedent.Dedent(`
   515  				*filter
   516  				:KUBE-SERVICES - [0:0]
   517  				:KUBE-EXTERNAL-SERVICES - [0:0]
   518  				:KUBE-NODEPORTS - [0:0]
   519  				-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT
   520  				-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
   521  				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
   522  				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
   523  				COMMIT
   524  				`),
   525  			error: `no such chain "KUBE-FORWARD"`,
   526  		},
   527  		{
   528  			name: "add to deleted chain",
   529  			input: dedent.Dedent(`
   530  				*filter
   531  				:KUBE-SERVICES - [0:0]
   532  				:KUBE-EXTERNAL-SERVICES - [0:0]
   533  				:KUBE-FORWARD - [0:0]
   534  				:KUBE-NODEPORTS - [0:0]
   535  				-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT
   536  				-X KUBE-FORWARD
   537  				-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
   538  				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
   539  				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
   540  				COMMIT
   541  				`),
   542  			error: `cannot add rules to deleted chain`,
   543  		},
   544  		{
   545  			name: "deleted non-empty chain",
   546  			input: dedent.Dedent(`
   547  				*filter
   548  				:KUBE-SERVICES - [0:0]
   549  				:KUBE-EXTERNAL-SERVICES - [0:0]
   550  				:KUBE-FORWARD - [0:0]
   551  				:KUBE-NODEPORTS - [0:0]
   552  				-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT
   553  				-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
   554  				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
   555  				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
   556  				-X KUBE-FORWARD
   557  				COMMIT
   558  				`),
   559  			error: `cannot delete chain "KUBE-FORWARD" after adding rules`,
   560  		},
   561  		{
   562  			name: "junk rule",
   563  			input: dedent.Dedent(`
   564  				*filter
   565  				:KUBE-SERVICES - [0:0]
   566  				:KUBE-EXTERNAL-SERVICES - [0:0]
   567  				:KUBE-FORWARD - [0:0]
   568  				:KUBE-NODEPORTS - [0:0]
   569  				-A KUBE-NODEPORTS -m comment --comment "ns2/svc2:p80 health check node port" -m tcp -p tcp --dport 30000 -j ACCEPT
   570  				-Q KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
   571  				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
   572  				-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
   573  				COMMIT
   574  				`),
   575  			error: `"-Q KUBE-FORWARD`,
   576  		},
   577  	} {
   578  		t.Run(tc.name, func(t *testing.T) {
   579  			dump, err := ParseIPTablesDump(tc.input)
   580  			if err == nil {
   581  				if tc.error != "" {
   582  					t.Errorf("unexpectedly did not get error")
   583  				} else if !reflect.DeepEqual(tc.output, dump) {
   584  					t.Errorf("bad output: expected %#v got %#v", tc.output, dump)
   585  				}
   586  			} else {
   587  				if tc.error == "" {
   588  					t.Errorf("got unexpected error: %v", err)
   589  				} else if !strings.Contains(err.Error(), tc.error) {
   590  					t.Errorf("got wrong error: %v (expected %q)", err, tc.error)
   591  				}
   592  			}
   593  		})
   594  	}
   595  }