mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-27 13:37:30 +00:00
376 lines
12 KiB
Go
376 lines
12 KiB
Go
/*
|
|
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.PointerTo(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
|
|
}
|