Merge pull request #109844 from danwinship/iptables-tests-new

improve parsing in iptables unit tests
This commit is contained in:
Kubernetes Prow Robot 2022-06-10 14:27:44 -07:00 committed by GitHub
commit dc4e91a875
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 1874 additions and 653 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1664,11 +1664,25 @@ func TestMasqueradeRule(t *testing.T) {
makeServiceMap(fp) makeServiceMap(fp)
fp.syncProxyRules() fp.syncProxyRules()
postRoutingRules := ipt.GetRules(string(kubePostroutingChain)) buf := bytes.NewBuffer(nil)
if !hasJump(postRoutingRules, "MASQUERADE", "") { _ = ipt.SaveInto(utiliptables.TableNAT, buf)
natRules := strings.Split(string(buf.Bytes()), "\n")
var hasMasqueradeJump, hasMasqRandomFully bool
for _, line := range natRules {
rule, _ := iptablestest.ParseRule(line, false)
if rule != nil && rule.Chain == kubePostroutingChain && rule.Jump != nil && rule.Jump.Value == "MASQUERADE" {
hasMasqueradeJump = true
if rule.RandomFully != nil {
hasMasqRandomFully = true
}
break
}
}
if !hasMasqueradeJump {
t.Errorf("Failed to find -j MASQUERADE in %s chain", kubePostroutingChain) t.Errorf("Failed to find -j MASQUERADE in %s chain", kubePostroutingChain)
} }
if hasMasqRandomFully(postRoutingRules) != testcase { if hasMasqRandomFully != testcase {
probs := map[bool]string{false: "found", true: "did not find"} probs := map[bool]string{false: "found", true: "did not find"}
t.Errorf("%s --random-fully in -j MASQUERADE rule in %s chain for HasRandomFully()=%v", probs[testcase], kubePostroutingChain, testcase) t.Errorf("%s --random-fully in -j MASQUERADE rule in %s chain for HasRandomFully()=%v", probs[testcase], kubePostroutingChain, testcase)
} }
@ -3817,42 +3831,41 @@ func buildFakeProxier() (*iptablestest.FakeIPTables, *Proxier) {
return ipt, NewFakeProxier(ipt, ipvs, ipset, nil, nil, v1.IPv4Protocol) return ipt, NewFakeProxier(ipt, ipvs, ipset, nil, nil, v1.IPv4Protocol)
} }
func hasJump(rules []iptablestest.Rule, destChain, ipSet string) bool { func getRules(ipt *iptablestest.FakeIPTables, chain utiliptables.Chain) []*iptablestest.Rule {
for _, r := range rules { var rules []*iptablestest.Rule
if r[iptablestest.Jump] == destChain {
if ipSet == "" {
return true
}
if strings.Contains(r[iptablestest.MatchSet], ipSet) {
return true
}
}
}
return false
}
func hasMasqRandomFully(rules []iptablestest.Rule) bool { buf := bytes.NewBuffer(nil)
for _, r := range rules { _ = ipt.SaveInto(utiliptables.TableNAT, buf)
if r[iptablestest.Masquerade] == "--random-fully" { _ = ipt.SaveInto(utiliptables.TableFilter, buf)
return true lines := strings.Split(string(buf.Bytes()), "\n")
for _, l := range lines {
if !strings.HasPrefix(l, "-A ") {
continue
}
rule, _ := iptablestest.ParseRule(l, false)
if rule != nil && rule.Chain == chain {
rules = append(rules, rule)
} }
} }
return false return rules
} }
// checkIptables to check expected iptables chain and rules. The got rules must have same number and order as the // checkIptables to check expected iptables chain and rules. The got rules must have same number and order as the
// expected rules. // expected rules.
func checkIptables(t *testing.T, ipt *iptablestest.FakeIPTables, epIpt netlinktest.ExpectedIptablesChain) { func checkIptables(t *testing.T, ipt *iptablestest.FakeIPTables, epIpt netlinktest.ExpectedIptablesChain) {
for epChain, epRules := range epIpt { for epChain, epRules := range epIpt {
rules := ipt.GetRules(epChain) rules := getRules(ipt, utiliptables.Chain(epChain))
if len(rules) != len(epRules) { if len(rules) != len(epRules) {
t.Errorf("Expected %d iptables rule in chain %s, got %d", len(epRules), epChain, len(rules)) t.Errorf("Expected %d iptables rule in chain %s, got %d", len(epRules), epChain, len(rules))
continue continue
} }
for i, epRule := range epRules { for i, epRule := range epRules {
rule := rules[i] rule := rules[i]
if rule[iptablestest.Jump] != epRule.JumpChain || !strings.Contains(rule[iptablestest.MatchSet], epRule.MatchSet) { if rule.Jump == nil || rule.Jump.Value != epRule.JumpChain {
t.Errorf("Expected MatchSet=%s JumpChain=%s, got MatchSet=%s JumpChain=%s", epRule.MatchSet, epRule.JumpChain, rule[iptablestest.MatchSet], rule[iptablestest.Jump]) t.Errorf("Expected MatchSet=%s JumpChain=%s, got %s", epRule.MatchSet, epRule.JumpChain, rule.Raw)
}
if (epRule.MatchSet == "" && rule.MatchSet != nil) || (epRule.MatchSet != "" && (rule.MatchSet == nil || rule.MatchSet.Value != epRule.MatchSet)) {
t.Errorf("Expected MatchSet=%s JumpChain=%s, got %s", epRule.MatchSet, epRule.JumpChain, rule.Raw)
} }
} }
} }

View File

@ -25,86 +25,149 @@ import (
"k8s.io/kubernetes/pkg/util/iptables" "k8s.io/kubernetes/pkg/util/iptables"
) )
const (
// Destination represents the destination address flag
Destination = "-d "
// Source represents the source address flag
Source = "-s "
// DPort represents the destination port flag
DPort = "--dport "
// Protocol represents the protocol flag
Protocol = "-p "
// Jump represents jump flag specifies the jump target
Jump = "-j "
// Reject specifies the reject target
Reject = "REJECT"
// Accept specifies the accept target
Accept = "ACCEPT"
// ToDest represents the flag used to specify the destination address in DNAT
ToDest = "--to-destination "
// Recent represents the sub-command recent that allows to dynamically create list of IP address to match against
Recent = "recent "
// MatchSet represents the flag which match packets against the specified set
MatchSet = "--match-set "
// SrcType represents the --src-type flag which matches if the source address is of given type
SrcType = "--src-type "
// Masquerade represents the target that is used in nat table.
Masquerade = "MASQUERADE "
)
// Rule holds a map of rules.
type Rule map[string]string
// FakeIPTables is no-op implementation of iptables Interface. // FakeIPTables is no-op implementation of iptables Interface.
type FakeIPTables struct { type FakeIPTables struct {
hasRandomFully bool hasRandomFully bool
Lines []byte
protocol iptables.Protocol protocol iptables.Protocol
Dump *IPTablesDump
} }
// NewFake returns a no-op iptables.Interface // NewFake returns a no-op iptables.Interface
func NewFake() *FakeIPTables { func NewFake() *FakeIPTables {
return &FakeIPTables{protocol: iptables.ProtocolIPv4} f := &FakeIPTables{
protocol: iptables.ProtocolIPv4,
Dump: &IPTablesDump{
Tables: []Table{
{
Name: iptables.TableNAT,
Chains: []Chain{
{Name: iptables.ChainPrerouting},
{Name: iptables.ChainInput},
{Name: iptables.ChainOutput},
{Name: iptables.ChainPostrouting},
},
},
{
Name: iptables.TableFilter,
Chains: []Chain{
{Name: iptables.ChainInput},
{Name: iptables.ChainForward},
{Name: iptables.ChainOutput},
},
},
{
Name: iptables.TableMangle,
Chains: []Chain{},
},
},
},
}
return f
} }
// NewIPv6Fake returns a no-op iptables.Interface with IsIPv6() == true // NewIPv6Fake returns a no-op iptables.Interface with IsIPv6() == true
func NewIPv6Fake() *FakeIPTables { func NewIPv6Fake() *FakeIPTables {
return &FakeIPTables{protocol: iptables.ProtocolIPv6} f := NewFake()
f.protocol = iptables.ProtocolIPv6
return f
} }
// SetHasRandomFully is part of iptables.Interface // SetHasRandomFully sets f's return value for HasRandomFully()
func (f *FakeIPTables) SetHasRandomFully(can bool) *FakeIPTables { func (f *FakeIPTables) SetHasRandomFully(can bool) *FakeIPTables {
f.hasRandomFully = can f.hasRandomFully = can
return f return f
} }
// EnsureChain is part of iptables.Interface // EnsureChain is part of iptables.Interface
func (*FakeIPTables) EnsureChain(table iptables.Table, chain iptables.Chain) (bool, error) { func (f *FakeIPTables) EnsureChain(table iptables.Table, chain iptables.Chain) (bool, error) {
return true, nil t, err := f.Dump.GetTable(table)
if err != nil {
return false, err
}
if c, _ := f.Dump.GetChain(table, chain); c != nil {
return true, nil
}
t.Chains = append(t.Chains, Chain{Name: chain})
return false, nil
} }
// FlushChain is part of iptables.Interface // FlushChain is part of iptables.Interface
func (*FakeIPTables) FlushChain(table iptables.Table, chain iptables.Chain) error { func (f *FakeIPTables) FlushChain(table iptables.Table, chain iptables.Chain) error {
if c, _ := f.Dump.GetChain(table, chain); c != nil {
c.Rules = nil
}
return nil return nil
} }
// DeleteChain is part of iptables.Interface // DeleteChain is part of iptables.Interface
func (*FakeIPTables) DeleteChain(table iptables.Table, chain iptables.Chain) error { func (f *FakeIPTables) DeleteChain(table iptables.Table, chain iptables.Chain) error {
t, err := f.Dump.GetTable(table)
if err != nil {
return err
}
for i := range t.Chains {
if t.Chains[i].Name == chain {
t.Chains = append(t.Chains[:i], t.Chains[i+1:]...)
return nil
}
}
return nil return nil
} }
// ChainExists is part of iptables.Interface // ChainExists is part of iptables.Interface
func (*FakeIPTables) ChainExists(table iptables.Table, chain iptables.Chain) (bool, error) { func (f *FakeIPTables) ChainExists(table iptables.Table, chain iptables.Chain) (bool, error) {
return true, nil if _, err := f.Dump.GetTable(table); err != nil {
return false, err
}
if c, _ := f.Dump.GetChain(table, chain); c != nil {
return true, nil
}
return false, nil
} }
// EnsureRule is part of iptables.Interface // EnsureRule is part of iptables.Interface
func (*FakeIPTables) EnsureRule(position iptables.RulePosition, table iptables.Table, chain iptables.Chain, args ...string) (bool, error) { func (f *FakeIPTables) EnsureRule(position iptables.RulePosition, table iptables.Table, chain iptables.Chain, args ...string) (bool, error) {
return true, nil c, err := f.Dump.GetChain(table, chain)
if err != nil {
return false, err
}
rule := "-A " + string(chain) + " " + strings.Join(args, " ")
for _, r := range c.Rules {
if r.Raw == rule {
return true, nil
}
}
parsed, err := ParseRule(rule, false)
if err != nil {
return false, err
}
if position == iptables.Append {
c.Rules = append(c.Rules, parsed)
} else {
c.Rules = append([]*Rule{parsed}, c.Rules...)
}
return false, nil
} }
// DeleteRule is part of iptables.Interface // DeleteRule is part of iptables.Interface
func (*FakeIPTables) DeleteRule(table iptables.Table, chain iptables.Chain, args ...string) error { func (f *FakeIPTables) DeleteRule(table iptables.Table, chain iptables.Chain, args ...string) error {
c, err := f.Dump.GetChain(table, chain)
if err != nil {
return err
}
rule := "-A " + string(chain) + " " + strings.Join(args, " ")
for i, r := range c.Rules {
if r.Raw == rule {
c.Rules = append(c.Rules[:i], c.Rules[i+1:]...)
break
}
}
return nil return nil
} }
@ -118,27 +181,102 @@ func (f *FakeIPTables) Protocol() iptables.Protocol {
return f.protocol return f.protocol
} }
// Save is part of iptables.Interface func (f *FakeIPTables) saveTable(table iptables.Table, buffer *bytes.Buffer) error {
func (f *FakeIPTables) Save(table iptables.Table) ([]byte, error) { t, err := f.Dump.GetTable(table)
lines := make([]byte, len(f.Lines)) if err != nil {
copy(lines, f.Lines) return err
return lines, nil }
fmt.Fprintf(buffer, "*%s\n", table)
for _, c := range t.Chains {
fmt.Fprintf(buffer, ":%s - [%d:%d]\n", c.Name, c.Packets, c.Bytes)
}
for _, c := range t.Chains {
for _, r := range c.Rules {
fmt.Fprintf(buffer, "%s\n", r.Raw)
}
}
fmt.Fprintf(buffer, "COMMIT\n")
return nil
} }
// SaveInto is part of iptables.Interface // SaveInto is part of iptables.Interface
func (f *FakeIPTables) SaveInto(table iptables.Table, buffer *bytes.Buffer) error { func (f *FakeIPTables) SaveInto(table iptables.Table, buffer *bytes.Buffer) error {
buffer.Write(f.Lines) if table == "" {
// As a secret extension to the API, FakeIPTables treats table="" as
// meaning "all tables"
for i := range f.Dump.Tables {
err := f.saveTable(f.Dump.Tables[i].Name, buffer)
if err != nil {
return err
}
}
return nil
}
return f.saveTable(table, buffer)
}
func (f *FakeIPTables) restoreTable(newTable *Table, flush iptables.FlushFlag, counters iptables.RestoreCountersFlag) error {
oldTable, err := f.Dump.GetTable(newTable.Name)
if err != nil {
return err
}
if flush == iptables.FlushTables {
oldTable.Chains = make([]Chain, 0, len(newTable.Chains))
}
for _, newChain := range newTable.Chains {
oldChain, _ := f.Dump.GetChain(newTable.Name, newChain.Name)
switch {
case oldChain == nil && newChain.Deleted:
// no-op
case oldChain == nil && !newChain.Deleted:
oldTable.Chains = append(oldTable.Chains, newChain)
case oldChain != nil && newChain.Deleted:
// FIXME: should make sure chain is not referenced from other jumps
_ = f.DeleteChain(newTable.Name, newChain.Name)
case oldChain != nil && !newChain.Deleted:
// replace old data with new
oldChain.Rules = newChain.Rules
if counters == iptables.RestoreCounters {
oldChain.Packets = newChain.Packets
oldChain.Bytes = newChain.Bytes
}
}
}
return nil return nil
} }
// Restore is part of iptables.Interface // Restore is part of iptables.Interface
func (*FakeIPTables) Restore(table iptables.Table, data []byte, flush iptables.FlushFlag, counters iptables.RestoreCountersFlag) error { func (f *FakeIPTables) Restore(table iptables.Table, data []byte, flush iptables.FlushFlag, counters iptables.RestoreCountersFlag) error {
return nil dump, err := ParseIPTablesDump(string(data))
if err != nil {
return err
}
newTable, err := dump.GetTable(table)
if err != nil {
return err
}
return f.restoreTable(newTable, flush, counters)
} }
// RestoreAll is part of iptables.Interface // RestoreAll is part of iptables.Interface
func (f *FakeIPTables) RestoreAll(data []byte, flush iptables.FlushFlag, counters iptables.RestoreCountersFlag) error { func (f *FakeIPTables) RestoreAll(data []byte, flush iptables.FlushFlag, counters iptables.RestoreCountersFlag) error {
f.Lines = data dump, err := ParseIPTablesDump(string(data))
if err != nil {
return err
}
for i := range dump.Tables {
err = f.restoreTable(&dump.Tables[i], flush, counters)
if err != nil {
return err
}
}
return nil return nil
} }
@ -146,31 +284,6 @@ func (f *FakeIPTables) RestoreAll(data []byte, flush iptables.FlushFlag, counter
func (f *FakeIPTables) Monitor(canary iptables.Chain, tables []iptables.Table, reloadFunc func(), interval time.Duration, stopCh <-chan struct{}) { func (f *FakeIPTables) Monitor(canary iptables.Chain, tables []iptables.Table, reloadFunc func(), interval time.Duration, stopCh <-chan struct{}) {
} }
func getToken(line, separator string) string {
tokens := strings.Split(line, separator)
if len(tokens) == 2 {
return strings.Split(tokens[1], " ")[0]
}
return ""
}
// GetRules is part of iptables.Interface
func (f *FakeIPTables) GetRules(chainName string) (rules []Rule) {
for _, l := range strings.Split(string(f.Lines), "\n") {
if strings.Contains(l, fmt.Sprintf("-A %v", chainName)) {
newRule := Rule(map[string]string{})
for _, arg := range []string{Destination, Source, DPort, Protocol, Jump, ToDest, Recent, MatchSet, SrcType, Masquerade} {
tok := getToken(l, arg)
if tok != "" {
newRule[arg] = tok
}
}
rules = append(rules, newRule)
}
}
return
}
// HasRandomFully is part of iptables.Interface // HasRandomFully is part of iptables.Interface
func (f *FakeIPTables) HasRandomFully() bool { func (f *FakeIPTables) HasRandomFully() bool {
return f.hasRandomFully return f.hasRandomFully

View File

@ -0,0 +1,315 @@
/*
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 (
"bytes"
"strings"
"testing"
"github.com/lithammer/dedent"
"k8s.io/kubernetes/pkg/util/iptables"
)
func TestFakeIPTables(t *testing.T) {
fake := NewFake()
buf := bytes.NewBuffer(nil)
err := fake.SaveInto("", buf)
if err != nil {
t.Fatalf("unexpected error from SaveInto: %v", err)
}
expected := dedent.Dedent(strings.Trim(`
*nat
:PREROUTING - [0:0]
:INPUT - [0:0]
:OUTPUT - [0:0]
:POSTROUTING - [0:0]
COMMIT
*filter
:INPUT - [0:0]
:FORWARD - [0:0]
:OUTPUT - [0:0]
COMMIT
*mangle
COMMIT
`, "\n"))
if string(buf.Bytes()) != expected {
t.Fatalf("bad initial dump. expected:\n%s\n\ngot:\n%s\n", expected, buf.Bytes())
}
// EnsureChain
existed, err := fake.EnsureChain(iptables.Table("blah"), iptables.Chain("KUBE-TEST"))
if err == nil {
t.Errorf("did not get expected error creating chain in non-existent table")
} else if existed {
t.Errorf("wrong return value from EnsureChain with non-existent table")
}
existed, err = fake.EnsureChain(iptables.TableNAT, iptables.Chain("KUBE-TEST"))
if err != nil {
t.Errorf("unexpected error creating chain: %v", err)
} else if existed {
t.Errorf("wrong return value from EnsureChain with non-existent chain")
}
existed, err = fake.EnsureChain(iptables.TableNAT, iptables.Chain("KUBE-TEST"))
if err != nil {
t.Errorf("unexpected error creating chain: %v", err)
} else if !existed {
t.Errorf("wrong return value from EnsureChain with existing chain")
}
// ChainExists
exists, err := fake.ChainExists(iptables.TableNAT, iptables.Chain("KUBE-TEST"))
if err != nil {
t.Errorf("unexpected error checking chain: %v", err)
} else if !exists {
t.Errorf("wrong return value from ChainExists with existing chain")
}
exists, err = fake.ChainExists(iptables.TableNAT, iptables.Chain("KUBE-TEST-NOT"))
if err != nil {
t.Errorf("unexpected error checking chain: %v", err)
} else if exists {
t.Errorf("wrong return value from ChainExists with non-existent chain")
}
// EnsureRule
existed, err = fake.EnsureRule(iptables.Append, iptables.Table("blah"), iptables.Chain("KUBE-TEST"), "-j", "ACCEPT")
if err == nil {
t.Errorf("did not get expected error creating rule in non-existent table")
} else if existed {
t.Errorf("wrong return value from EnsureRule with non-existent table")
}
existed, err = fake.EnsureRule(iptables.Append, iptables.TableNAT, iptables.Chain("KUBE-TEST-NOT"), "-j", "ACCEPT")
if err == nil {
t.Errorf("did not get expected error creating rule in non-existent chain")
} else if existed {
t.Errorf("wrong return value from EnsureRule with non-existent chain")
}
existed, err = fake.EnsureRule(iptables.Append, iptables.TableNAT, iptables.Chain("KUBE-TEST"), "-j", "ACCEPT")
if err != nil {
t.Errorf("unexpected error creating rule: %v", err)
} else if existed {
t.Errorf("wrong return value from EnsureRule with non-existent rule")
}
existed, err = fake.EnsureRule(iptables.Prepend, iptables.TableNAT, iptables.Chain("KUBE-TEST"), "-j", "DROP")
if err != nil {
t.Errorf("unexpected error creating rule: %v", err)
} else if existed {
t.Errorf("wrong return value from EnsureRule with non-existent rule")
}
existed, err = fake.EnsureRule(iptables.Append, iptables.TableNAT, iptables.Chain("KUBE-TEST"), "-j", "DROP")
if err != nil {
t.Errorf("unexpected error creating rule: %v", err)
} else if !existed {
t.Errorf("wrong return value from EnsureRule with already-existing rule")
}
// Sanity-check...
buf.Reset()
err = fake.SaveInto("", buf)
if err != nil {
t.Fatalf("unexpected error from SaveInto: %v", err)
}
expected = dedent.Dedent(strings.Trim(`
*nat
:PREROUTING - [0:0]
:INPUT - [0:0]
:OUTPUT - [0:0]
:POSTROUTING - [0:0]
:KUBE-TEST - [0:0]
-A KUBE-TEST -j DROP
-A KUBE-TEST -j ACCEPT
COMMIT
*filter
:INPUT - [0:0]
:FORWARD - [0:0]
:OUTPUT - [0:0]
COMMIT
*mangle
COMMIT
`, "\n"))
if string(buf.Bytes()) != expected {
t.Fatalf("bad sanity-check dump. expected:\n%s\n\ngot:\n%s\n", expected, buf.Bytes())
}
// DeleteRule
err = fake.DeleteRule(iptables.Table("blah"), iptables.Chain("KUBE-TEST"), "-j", "DROP")
if err == nil {
t.Errorf("did not get expected error deleting rule in non-existent table")
}
err = fake.DeleteRule(iptables.TableNAT, iptables.Chain("KUBE-TEST-NOT"), "-j", "DROP")
if err == nil {
t.Errorf("did not get expected error deleting rule in non-existent chain")
}
err = fake.DeleteRule(iptables.TableNAT, iptables.Chain("KUBE-TEST"), "-j", "DROPLET")
if err != nil {
t.Errorf("unexpected error deleting non-existent rule: %v", err)
}
err = fake.DeleteRule(iptables.TableNAT, iptables.Chain("KUBE-TEST"), "-j", "DROP")
if err != nil {
t.Errorf("unexpected error deleting rule: %v", err)
}
// Restore
rules := dedent.Dedent(strings.Trim(`
*nat
:KUBE-RESTORED - [0:0]
:KUBE-MISC-CHAIN - [0:0]
:KUBE-EMPTY - [0:0]
-A KUBE-RESTORED -m comment --comment "restored chain" -j ACCEPT
-A KUBE-MISC-CHAIN -s 1.2.3.4 -j DROP
-A KUBE-MISC-CHAIN -d 5.6.7.8 -j MASQUERADE
COMMIT
`, "\n"))
err = fake.Restore(iptables.TableNAT, []byte(rules), iptables.NoFlushTables, iptables.NoRestoreCounters)
if err != nil {
t.Fatalf("unexpected error from Restore: %v", err)
}
// We used NoFlushTables, so this should leave KUBE-TEST unchanged
buf.Reset()
err = fake.SaveInto("", buf)
if err != nil {
t.Fatalf("unexpected error from SaveInto: %v", err)
}
expected = dedent.Dedent(strings.Trim(`
*nat
:PREROUTING - [0:0]
:INPUT - [0:0]
:OUTPUT - [0:0]
:POSTROUTING - [0:0]
:KUBE-TEST - [0:0]
:KUBE-RESTORED - [0:0]
:KUBE-MISC-CHAIN - [0:0]
:KUBE-EMPTY - [0:0]
-A KUBE-TEST -j ACCEPT
-A KUBE-RESTORED -m comment --comment "restored chain" -j ACCEPT
-A KUBE-MISC-CHAIN -s 1.2.3.4 -j DROP
-A KUBE-MISC-CHAIN -d 5.6.7.8 -j MASQUERADE
COMMIT
*filter
:INPUT - [0:0]
:FORWARD - [0:0]
:OUTPUT - [0:0]
COMMIT
*mangle
COMMIT
`, "\n"))
if string(buf.Bytes()) != expected {
t.Fatalf("bad post-restore dump. expected:\n%s\n\ngot:\n%s\n", expected, buf.Bytes())
}
// more Restore; empty out one chain and delete another, but also update its counters
rules = dedent.Dedent(strings.Trim(`
*nat
:KUBE-RESTORED - [0:0]
:KUBE-TEST - [99:9999]
-X KUBE-RESTORED
COMMIT
`, "\n"))
err = fake.Restore(iptables.TableNAT, []byte(rules), iptables.NoFlushTables, iptables.RestoreCounters)
if err != nil {
t.Fatalf("unexpected error from Restore: %v", err)
}
buf.Reset()
err = fake.SaveInto("", buf)
if err != nil {
t.Fatalf("unexpected error from SaveInto: %v", err)
}
expected = dedent.Dedent(strings.Trim(`
*nat
:PREROUTING - [0:0]
:INPUT - [0:0]
:OUTPUT - [0:0]
:POSTROUTING - [0:0]
:KUBE-TEST - [99:9999]
:KUBE-MISC-CHAIN - [0:0]
:KUBE-EMPTY - [0:0]
-A KUBE-MISC-CHAIN -s 1.2.3.4 -j DROP
-A KUBE-MISC-CHAIN -d 5.6.7.8 -j MASQUERADE
COMMIT
*filter
:INPUT - [0:0]
:FORWARD - [0:0]
:OUTPUT - [0:0]
COMMIT
*mangle
COMMIT
`, "\n"))
if string(buf.Bytes()) != expected {
t.Fatalf("bad post-second-restore dump. expected:\n%s\n\ngot:\n%s\n", expected, buf.Bytes())
}
// RestoreAll, FlushTables
rules = dedent.Dedent(strings.Trim(`
*filter
:INPUT - [0:0]
:FORWARD - [0:0]
:OUTPUT - [0:0]
:KUBE-TEST - [0:0]
-A KUBE-TEST -m comment --comment "filter table KUBE-TEST" -j ACCEPT
COMMIT
*nat
:PREROUTING - [0:0]
:INPUT - [0:0]
:OUTPUT - [0:0]
:POSTROUTING - [0:0]
:KUBE-TEST - [88:8888]
:KUBE-NEW-CHAIN - [0:0]
-A KUBE-NEW-CHAIN -d 172.30.0.1 -j DNAT --to-destination 10.0.0.1
-A KUBE-NEW-CHAIN -d 172.30.0.2 -j DNAT --to-destination 10.0.0.2
-A KUBE-NEW-CHAIN -d 172.30.0.3 -j DNAT --to-destination 10.0.0.3
COMMIT
`, "\n"))
err = fake.RestoreAll([]byte(rules), iptables.FlushTables, iptables.NoRestoreCounters)
if err != nil {
t.Fatalf("unexpected error from RestoreAll: %v", err)
}
buf.Reset()
err = fake.SaveInto("", buf)
if err != nil {
t.Fatalf("unexpected error from SaveInto: %v", err)
}
expected = dedent.Dedent(strings.Trim(`
*nat
:PREROUTING - [0:0]
:INPUT - [0:0]
:OUTPUT - [0:0]
:POSTROUTING - [0:0]
:KUBE-TEST - [88:8888]
:KUBE-NEW-CHAIN - [0:0]
-A KUBE-NEW-CHAIN -d 172.30.0.1 -j DNAT --to-destination 10.0.0.1
-A KUBE-NEW-CHAIN -d 172.30.0.2 -j DNAT --to-destination 10.0.0.2
-A KUBE-NEW-CHAIN -d 172.30.0.3 -j DNAT --to-destination 10.0.0.3
COMMIT
*filter
:INPUT - [0:0]
:FORWARD - [0:0]
:OUTPUT - [0:0]
:KUBE-TEST - [0:0]
-A KUBE-TEST -m comment --comment "filter table KUBE-TEST" -j ACCEPT
COMMIT
*mangle
COMMIT
`, "\n"))
if string(buf.Bytes()) != expected {
t.Fatalf("bad post-restore-all dump. expected:\n%s\n\ngot:\n%s\n", expected, buf.Bytes())
}
}

View File

@ -0,0 +1,375 @@
/*
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"
"regexp"
"strconv"
"strings"
"k8s.io/kubernetes/pkg/util/iptables"
)
// IPTablesDump represents a parsed IPTables rules dump (ie, the output of
// "iptables-save" or input to "iptables-restore")
type IPTablesDump struct {
Tables []Table
}
// Table represents an IPTables table
type Table struct {
Name iptables.Table
Chains []Chain
}
// Chain represents an IPTables chain
type Chain struct {
Name iptables.Chain
Packets uint64
Bytes uint64
Rules []*Rule
// Deleted is set if the input contained a "-X Name" line; this would never
// appear in iptables-save output but it could appear in iptables-restore *input*.
Deleted bool
}
var declareTableRegex = regexp.MustCompile(`^\*(.*)$`)
var declareChainRegex = regexp.MustCompile(`^:([^ ]*) - \[([0-9]*):([0-9]*)\]$`)
var addRuleRegex = regexp.MustCompile(`^-A ([^ ]*) (.*)$`)
var deleteChainRegex = regexp.MustCompile(`^-X (.*)$`)
type parseState int
const (
parseTableDeclaration parseState = iota
parseChainDeclarations
parseChains
)
// ParseIPTablesDump parses an IPTables rules dump. Note: this may ignore some bad data.
func ParseIPTablesDump(data string) (*IPTablesDump, error) {
dump := &IPTablesDump{}
state := parseTableDeclaration
lines := strings.Split(strings.Trim(data, "\n"), "\n")
var t *Table
for _, line := range lines {
retry:
line = strings.TrimSpace(line)
if line == "" || line[0] == '#' {
continue
}
switch state {
case parseTableDeclaration:
// Parse table declaration line ("*filter").
match := declareTableRegex.FindStringSubmatch(line)
if match == nil {
return nil, fmt.Errorf("could not parse iptables data (table %d starts with %q not a table name)", len(dump.Tables)+1, line)
}
dump.Tables = append(dump.Tables, Table{Name: iptables.Table(match[1])})
t = &dump.Tables[len(dump.Tables)-1]
state = parseChainDeclarations
case parseChainDeclarations:
match := declareChainRegex.FindStringSubmatch(line)
if match == nil {
state = parseChains
goto retry
}
chain := iptables.Chain(match[1])
packets, _ := strconv.ParseUint(match[2], 10, 64)
bytes, _ := strconv.ParseUint(match[3], 10, 64)
t.Chains = append(t.Chains,
Chain{
Name: chain,
Packets: packets,
Bytes: bytes,
},
)
case parseChains:
if match := addRuleRegex.FindStringSubmatch(line); match != nil {
chain := iptables.Chain(match[1])
c, err := dump.GetChain(t.Name, chain)
if err != nil {
return nil, fmt.Errorf("error parsing rule %q: %v", line, err)
}
if c.Deleted {
return nil, fmt.Errorf("cannot add rules to deleted chain %q", chain)
}
rule, err := ParseRule(line, false)
if err != nil {
return nil, err
}
c.Rules = append(c.Rules, rule)
} else if match := deleteChainRegex.FindStringSubmatch(line); match != nil {
chain := iptables.Chain(match[1])
c, err := dump.GetChain(t.Name, chain)
if err != nil {
return nil, fmt.Errorf("error parsing rule %q: %v", line, err)
}
if len(c.Rules) != 0 {
return nil, fmt.Errorf("cannot delete chain %q after adding rules", chain)
}
c.Deleted = true
} else if line == "COMMIT" {
state = parseTableDeclaration
} else {
return nil, fmt.Errorf("error parsing rule %q", line)
}
}
}
if state != parseTableDeclaration {
return nil, fmt.Errorf("could not parse iptables data (no COMMIT line?)")
}
return dump, nil
}
func (dump *IPTablesDump) String() string {
buffer := &strings.Builder{}
for _, t := range dump.Tables {
fmt.Fprintf(buffer, "*%s\n", t.Name)
for _, c := range t.Chains {
fmt.Fprintf(buffer, ":%s - [%d:%d]\n", c.Name, c.Packets, c.Bytes)
}
for _, c := range t.Chains {
for _, r := range c.Rules {
fmt.Fprintf(buffer, "%s\n", r.Raw)
}
}
for _, c := range t.Chains {
if c.Deleted {
fmt.Fprintf(buffer, "-X %s\n", c.Name)
}
}
fmt.Fprintf(buffer, "COMMIT\n")
}
return buffer.String()
}
func (dump *IPTablesDump) GetTable(table iptables.Table) (*Table, error) {
for i := range dump.Tables {
if dump.Tables[i].Name == table {
return &dump.Tables[i], nil
}
}
return nil, fmt.Errorf("no such table %q", table)
}
func (dump *IPTablesDump) GetChain(table iptables.Table, chain iptables.Chain) (*Chain, error) {
t, err := dump.GetTable(table)
if err != nil {
return nil, err
}
for i := range t.Chains {
if t.Chains[i].Name == chain {
return &t.Chains[i], nil
}
}
return nil, fmt.Errorf("no such chain %q", chain)
}
// Rule represents a single parsed IPTables rule. (This currently covers all of the rule
// types that we actually use in pkg/proxy/iptables or pkg/proxy/ipvs.)
//
// The parsing is mostly-automated based on type reflection. The `param` tag on a field
// indicates the parameter whose value will be placed into that field. (The code assumes
// that we don't use both the short and long forms of any parameter names (eg, "-s" vs
// "--source"), which is currently true, but it could be extended if necessary.) The
// `negatable` tag indicates if a parameter is allowed to be preceded by "!".
//
// Parameters that take a value are stored as type `*IPTablesValue`, which encapsulates a
// string value and whether the rule was negated (ie, whether the rule requires that we
// *match* or *don't match* that value). But string-valued parameters that can't be
// negated use `IPTablesValue` rather than `string` too, just for API consistency.
//
// Parameters that don't take a value are stored as `*bool`, where the value is `nil` if
// the parameter was not present, `&true` if the parameter was present, or `&false` if the
// parameter was present but negated.
//
// Parsing skips over "-m MODULE" parameters because most parameters have unique names
// anyway even ignoring the module name, and in the cases where they don't (eg "-m tcp
// --sport" vs "-m udp --sport") the parameters are mutually-exclusive and it's more
// convenient to store them in the same struct field anyway.
type Rule struct {
// Raw contains the original raw rule string
Raw string
Chain iptables.Chain `param:"-A"`
Comment *IPTablesValue `param:"--comment"`
Protocol *IPTablesValue `param:"-p" negatable:"true"`
SourceAddress *IPTablesValue `param:"-s" negatable:"true"`
SourceType *IPTablesValue `param:"--src-type" negatable:"true"`
SourcePort *IPTablesValue `param:"--sport" negatable:"true"`
DestinationAddress *IPTablesValue `param:"-d" negatable:"true"`
DestinationType *IPTablesValue `param:"--dst-type" negatable:"true"`
DestinationPort *IPTablesValue `param:"--dport" negatable:"true"`
MatchSet *IPTablesValue `param:"--match-set" negatable:"true"`
Jump *IPTablesValue `param:"-j"`
RandomFully *bool `param:"--random-fully"`
Probability *IPTablesValue `param:"--probability"`
DNATDestination *IPTablesValue `param:"--to-destination"`
// We don't actually use the values of these, but we care if they are present
AffinityCheck *bool `param:"--rcheck" negatable:"true"`
MarkCheck *IPTablesValue `param:"--mark" negatable:"true"`
CTStateCheck *IPTablesValue `param:"--ctstate" negatable:"true"`
// We don't currently care about any of these in the unit tests, but we expect
// them to be present in some rules that we parse, so we define how to parse them.
AffinityName *IPTablesValue `param:"--name"`
AffinitySeconds *IPTablesValue `param:"--seconds"`
AffinitySet *bool `param:"--set" negatable:"true"`
AffinityReap *bool `param:"--reap"`
StatisticMode *IPTablesValue `param:"--mode"`
}
// IPTablesValue is a value of a parameter in an Rule, where the parameter is
// possibly negated.
type IPTablesValue struct {
Negated bool
Value string
}
// for debugging; otherwise %v will just print the pointer value
func (v *IPTablesValue) String() string {
if v.Negated {
return fmt.Sprintf("NOT %q", v.Value)
} else {
return fmt.Sprintf("%q", v.Value)
}
}
// Matches returns true if cmp equals / doesn't equal v.Value (depending on
// v.Negated).
func (v *IPTablesValue) Matches(cmp string) bool {
if v.Negated {
return v.Value != cmp
} else {
return v.Value == cmp
}
}
// findParamField finds a field in value with the struct tag "param:${param}" and if found,
// returns a pointer to the Value of that field, and the value of its "negatable" tag.
func findParamField(value reflect.Value, param string) (*reflect.Value, bool) {
typ := value.Type()
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
if field.Tag.Get("param") == param {
fValue := value.Field(i)
return &fValue, field.Tag.Get("negatable") == "true"
}
}
return nil, false
}
// wordRegex matches a single word or a quoted string (at the start of the string, or
// preceded by whitespace)
var wordRegex = regexp.MustCompile(`(?:^|\s)("[^"]*"|[^"]\S*)`)
// Used by ParseRule
var boolPtrType = reflect.PtrTo(reflect.TypeOf(true))
var ipTablesValuePtrType = reflect.TypeOf((*IPTablesValue)(nil))
// ParseRule parses rule. If strict is false, it will parse the recognized
// parameters and ignore unrecognized ones. If it is true, parsing will fail if there are
// unrecognized parameters.
func ParseRule(rule string, strict bool) (*Rule, error) {
parsed := &Rule{Raw: rule}
// Split rule into "words" (where a quoted string is a single word).
var words []string
for _, match := range wordRegex.FindAllStringSubmatch(rule, -1) {
words = append(words, strings.Trim(match[1], `"`))
}
// The chain name must come first (and can't be the only thing there)
if len(words) < 2 || words[0] != "-A" {
return nil, fmt.Errorf(`bad iptables rule (does not start with "-A CHAIN")`)
} else if len(words) < 3 {
return nil, fmt.Errorf("bad iptables rule (no match rules)")
}
// For each word, see if it is a known iptables parameter, based on the struct
// field tags in Rule. Note that in the non-strict case we implicitly assume that
// no unrecognized parameter will take an argument that could be mistaken for
// another parameter.
v := reflect.ValueOf(parsed).Elem()
negated := false
for w := 0; w < len(words); {
if words[w] == "-m" && w < len(words)-1 {
// Skip "-m MODULE"; we don't pay attention to that since the
// parameter names are unique enough anyway.
w += 2
continue
}
if words[w] == "!" {
negated = true
w++
continue
}
// For everything else, see if it corresponds to a field of Rule
if field, negatable := findParamField(v, words[w]); field != nil {
if negated && !negatable {
return nil, fmt.Errorf("cannot negate parameter %q", words[w])
}
if field.Type() != boolPtrType && w == len(words)-1 {
return nil, fmt.Errorf("parameter %q requires an argument", words[w])
}
switch field.Type() {
case boolPtrType:
boolVal := !negated
field.Set(reflect.ValueOf(&boolVal))
w++
case ipTablesValuePtrType:
field.Set(reflect.ValueOf(&IPTablesValue{Negated: negated, Value: words[w+1]}))
w += 2
default:
field.SetString(words[w+1])
w += 2
}
} else if strict {
return nil, fmt.Errorf("unrecognized parameter %q", words[w])
} else {
// skip
w++
}
negated = false
}
return parsed, nil
}

View File

@ -0,0 +1,595 @@
/*
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)
}
}
})
}
}