...

Source file src/k8s.io/kubernetes/pkg/util/iptables/testing/parse.go

Documentation: k8s.io/kubernetes/pkg/util/iptables/testing

     1  //go:build linux
     2  // +build linux
     3  
     4  /*
     5  Copyright 2022 The Kubernetes Authors.
     6  
     7  Licensed under the Apache License, Version 2.0 (the "License");
     8  you may not use this file except in compliance with the License.
     9  You may obtain a copy of the License at
    10  
    11      http://www.apache.org/licenses/LICENSE-2.0
    12  
    13  Unless required by applicable law or agreed to in writing, software
    14  distributed under the License is distributed on an "AS IS" BASIS,
    15  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    16  See the License for the specific language governing permissions and
    17  limitations under the License.
    18  */
    19  
    20  package testing
    21  
    22  import (
    23  	"fmt"
    24  	"reflect"
    25  	"regexp"
    26  	"strconv"
    27  	"strings"
    28  
    29  	"k8s.io/kubernetes/pkg/util/iptables"
    30  )
    31  
    32  // IPTablesDump represents a parsed IPTables rules dump (ie, the output of
    33  // "iptables-save" or input to "iptables-restore")
    34  type IPTablesDump struct {
    35  	Tables []Table
    36  }
    37  
    38  // Table represents an IPTables table
    39  type Table struct {
    40  	Name   iptables.Table
    41  	Chains []Chain
    42  }
    43  
    44  // Chain represents an IPTables chain
    45  type Chain struct {
    46  	Name    iptables.Chain
    47  	Packets uint64
    48  	Bytes   uint64
    49  	Rules   []*Rule
    50  
    51  	// Deleted is set if the input contained a "-X Name" line; this would never
    52  	// appear in iptables-save output but it could appear in iptables-restore *input*.
    53  	Deleted bool
    54  }
    55  
    56  var declareTableRegex = regexp.MustCompile(`^\*(.*)$`)
    57  var declareChainRegex = regexp.MustCompile(`^:([^ ]*) - \[([0-9]*):([0-9]*)\]$`)
    58  var addRuleRegex = regexp.MustCompile(`^-A ([^ ]*) (.*)$`)
    59  var deleteChainRegex = regexp.MustCompile(`^-X (.*)$`)
    60  
    61  type parseState int
    62  
    63  const (
    64  	parseTableDeclaration parseState = iota
    65  	parseChainDeclarations
    66  	parseChains
    67  )
    68  
    69  // ParseIPTablesDump parses an IPTables rules dump. Note: this may ignore some bad data.
    70  func ParseIPTablesDump(data string) (*IPTablesDump, error) {
    71  	dump := &IPTablesDump{}
    72  	state := parseTableDeclaration
    73  	lines := strings.Split(strings.Trim(data, "\n"), "\n")
    74  	var t *Table
    75  
    76  	for _, line := range lines {
    77  	retry:
    78  		line = strings.TrimSpace(line)
    79  		if line == "" || line[0] == '#' {
    80  			continue
    81  		}
    82  
    83  		switch state {
    84  		case parseTableDeclaration:
    85  			// Parse table declaration line ("*filter").
    86  			match := declareTableRegex.FindStringSubmatch(line)
    87  			if match == nil {
    88  				return nil, fmt.Errorf("could not parse iptables data (table %d starts with %q not a table name)", len(dump.Tables)+1, line)
    89  			}
    90  			dump.Tables = append(dump.Tables, Table{Name: iptables.Table(match[1])})
    91  			t = &dump.Tables[len(dump.Tables)-1]
    92  			state = parseChainDeclarations
    93  
    94  		case parseChainDeclarations:
    95  			match := declareChainRegex.FindStringSubmatch(line)
    96  			if match == nil {
    97  				state = parseChains
    98  				goto retry
    99  			}
   100  
   101  			chain := iptables.Chain(match[1])
   102  			packets, _ := strconv.ParseUint(match[2], 10, 64)
   103  			bytes, _ := strconv.ParseUint(match[3], 10, 64)
   104  
   105  			t.Chains = append(t.Chains,
   106  				Chain{
   107  					Name:    chain,
   108  					Packets: packets,
   109  					Bytes:   bytes,
   110  				},
   111  			)
   112  
   113  		case parseChains:
   114  			if match := addRuleRegex.FindStringSubmatch(line); match != nil {
   115  				chain := iptables.Chain(match[1])
   116  
   117  				c, err := dump.GetChain(t.Name, chain)
   118  				if err != nil {
   119  					return nil, fmt.Errorf("error parsing rule %q: %v", line, err)
   120  				}
   121  				if c.Deleted {
   122  					return nil, fmt.Errorf("cannot add rules to deleted chain %q", chain)
   123  				}
   124  
   125  				rule, err := ParseRule(line, false)
   126  				if err != nil {
   127  					return nil, err
   128  				}
   129  				c.Rules = append(c.Rules, rule)
   130  			} else if match := deleteChainRegex.FindStringSubmatch(line); match != nil {
   131  				chain := iptables.Chain(match[1])
   132  
   133  				c, err := dump.GetChain(t.Name, chain)
   134  				if err != nil {
   135  					return nil, fmt.Errorf("error parsing rule %q: %v", line, err)
   136  				}
   137  				if len(c.Rules) != 0 {
   138  					return nil, fmt.Errorf("cannot delete chain %q after adding rules", chain)
   139  				}
   140  				c.Deleted = true
   141  			} else if line == "COMMIT" {
   142  				state = parseTableDeclaration
   143  			} else {
   144  				return nil, fmt.Errorf("error parsing rule %q", line)
   145  			}
   146  		}
   147  	}
   148  
   149  	if state != parseTableDeclaration {
   150  		return nil, fmt.Errorf("could not parse iptables data (no COMMIT line?)")
   151  	}
   152  
   153  	return dump, nil
   154  }
   155  
   156  func (dump *IPTablesDump) String() string {
   157  	buffer := &strings.Builder{}
   158  	for _, t := range dump.Tables {
   159  		fmt.Fprintf(buffer, "*%s\n", t.Name)
   160  		for _, c := range t.Chains {
   161  			fmt.Fprintf(buffer, ":%s - [%d:%d]\n", c.Name, c.Packets, c.Bytes)
   162  		}
   163  		for _, c := range t.Chains {
   164  			for _, r := range c.Rules {
   165  				fmt.Fprintf(buffer, "%s\n", r.Raw)
   166  			}
   167  		}
   168  		for _, c := range t.Chains {
   169  			if c.Deleted {
   170  				fmt.Fprintf(buffer, "-X %s\n", c.Name)
   171  			}
   172  		}
   173  		fmt.Fprintf(buffer, "COMMIT\n")
   174  	}
   175  	return buffer.String()
   176  }
   177  
   178  func (dump *IPTablesDump) GetTable(table iptables.Table) (*Table, error) {
   179  	for i := range dump.Tables {
   180  		if dump.Tables[i].Name == table {
   181  			return &dump.Tables[i], nil
   182  		}
   183  	}
   184  	return nil, fmt.Errorf("no such table %q", table)
   185  }
   186  
   187  func (dump *IPTablesDump) GetChain(table iptables.Table, chain iptables.Chain) (*Chain, error) {
   188  	t, err := dump.GetTable(table)
   189  	if err != nil {
   190  		return nil, err
   191  	}
   192  	for i := range t.Chains {
   193  		if t.Chains[i].Name == chain {
   194  			return &t.Chains[i], nil
   195  		}
   196  	}
   197  	return nil, fmt.Errorf("no such chain %q", chain)
   198  }
   199  
   200  // Rule represents a single parsed IPTables rule. (This currently covers all of the rule
   201  // types that we actually use in pkg/proxy/iptables or pkg/proxy/ipvs.)
   202  //
   203  // The parsing is mostly-automated based on type reflection. The `param` tag on a field
   204  // indicates the parameter whose value will be placed into that field. (The code assumes
   205  // that we don't use both the short and long forms of any parameter names (eg, "-s" vs
   206  // "--source"), which is currently true, but it could be extended if necessary.) The
   207  // `negatable` tag indicates if a parameter is allowed to be preceded by "!".
   208  //
   209  // Parameters that take a value are stored as type `*IPTablesValue`, which encapsulates a
   210  // string value and whether the rule was negated (ie, whether the rule requires that we
   211  // *match* or *don't match* that value). But string-valued parameters that can't be
   212  // negated use `IPTablesValue` rather than `string` too, just for API consistency.
   213  //
   214  // Parameters that don't take a value are stored as `*bool`, where the value is `nil` if
   215  // the parameter was not present, `&true` if the parameter was present, or `&false` if the
   216  // parameter was present but negated.
   217  //
   218  // Parsing skips over "-m MODULE" parameters because most parameters have unique names
   219  // anyway even ignoring the module name, and in the cases where they don't (eg "-m tcp
   220  // --sport" vs "-m udp --sport") the parameters are mutually-exclusive and it's more
   221  // convenient to store them in the same struct field anyway.
   222  type Rule struct {
   223  	// Raw contains the original raw rule string
   224  	Raw string
   225  
   226  	Chain   iptables.Chain `param:"-A"`
   227  	Comment *IPTablesValue `param:"--comment"`
   228  
   229  	Protocol *IPTablesValue `param:"-p" negatable:"true"`
   230  
   231  	SourceAddress *IPTablesValue `param:"-s" negatable:"true"`
   232  	SourceType    *IPTablesValue `param:"--src-type" negatable:"true"`
   233  	SourcePort    *IPTablesValue `param:"--sport" negatable:"true"`
   234  
   235  	DestinationAddress *IPTablesValue `param:"-d" negatable:"true"`
   236  	DestinationType    *IPTablesValue `param:"--dst-type" negatable:"true"`
   237  	DestinationPort    *IPTablesValue `param:"--dport" negatable:"true"`
   238  
   239  	MatchSet *IPTablesValue `param:"--match-set" negatable:"true"`
   240  
   241  	Jump            *IPTablesValue `param:"-j"`
   242  	RandomFully     *bool          `param:"--random-fully"`
   243  	Probability     *IPTablesValue `param:"--probability"`
   244  	DNATDestination *IPTablesValue `param:"--to-destination"`
   245  
   246  	// We don't actually use the values of these, but we care if they are present
   247  	AffinityCheck *bool          `param:"--rcheck" negatable:"true"`
   248  	MarkCheck     *IPTablesValue `param:"--mark" negatable:"true"`
   249  	CTStateCheck  *IPTablesValue `param:"--ctstate" negatable:"true"`
   250  
   251  	// We don't currently care about any of these in the unit tests, but we expect
   252  	// them to be present in some rules that we parse, so we define how to parse them.
   253  	AffinityName    *IPTablesValue `param:"--name"`
   254  	AffinitySeconds *IPTablesValue `param:"--seconds"`
   255  	AffinitySet     *bool          `param:"--set" negatable:"true"`
   256  	AffinityReap    *bool          `param:"--reap"`
   257  	StatisticMode   *IPTablesValue `param:"--mode"`
   258  }
   259  
   260  // IPTablesValue is a value of a parameter in an Rule, where the parameter is
   261  // possibly negated.
   262  type IPTablesValue struct {
   263  	Negated bool
   264  	Value   string
   265  }
   266  
   267  // for debugging; otherwise %v will just print the pointer value
   268  func (v *IPTablesValue) String() string {
   269  	if v.Negated {
   270  		return fmt.Sprintf("NOT %q", v.Value)
   271  	} else {
   272  		return fmt.Sprintf("%q", v.Value)
   273  	}
   274  }
   275  
   276  // Matches returns true if cmp equals / doesn't equal v.Value (depending on
   277  // v.Negated).
   278  func (v *IPTablesValue) Matches(cmp string) bool {
   279  	if v.Negated {
   280  		return v.Value != cmp
   281  	} else {
   282  		return v.Value == cmp
   283  	}
   284  }
   285  
   286  // findParamField finds a field in value with the struct tag "param:${param}" and if found,
   287  // returns a pointer to the Value of that field, and the value of its "negatable" tag.
   288  func findParamField(value reflect.Value, param string) (*reflect.Value, bool) {
   289  	typ := value.Type()
   290  	for i := 0; i < typ.NumField(); i++ {
   291  		field := typ.Field(i)
   292  		if field.Tag.Get("param") == param {
   293  			fValue := value.Field(i)
   294  			return &fValue, field.Tag.Get("negatable") == "true"
   295  		}
   296  	}
   297  	return nil, false
   298  }
   299  
   300  // wordRegex matches a single word or a quoted string (at the start of the string, or
   301  // preceded by whitespace)
   302  var wordRegex = regexp.MustCompile(`(?:^|\s)("[^"]*"|[^"]\S*)`)
   303  
   304  // Used by ParseRule
   305  var boolPtrType = reflect.PointerTo(reflect.TypeOf(true))
   306  var ipTablesValuePtrType = reflect.TypeOf((*IPTablesValue)(nil))
   307  
   308  // ParseRule parses rule. If strict is false, it will parse the recognized
   309  // parameters and ignore unrecognized ones. If it is true, parsing will fail if there are
   310  // unrecognized parameters.
   311  func ParseRule(rule string, strict bool) (*Rule, error) {
   312  	parsed := &Rule{Raw: rule}
   313  
   314  	// Split rule into "words" (where a quoted string is a single word).
   315  	var words []string
   316  	for _, match := range wordRegex.FindAllStringSubmatch(rule, -1) {
   317  		words = append(words, strings.Trim(match[1], `"`))
   318  	}
   319  
   320  	// The chain name must come first (and can't be the only thing there)
   321  	if len(words) < 2 || words[0] != "-A" {
   322  		return nil, fmt.Errorf(`bad iptables rule (does not start with "-A CHAIN")`)
   323  	} else if len(words) < 3 {
   324  		return nil, fmt.Errorf("bad iptables rule (no match rules)")
   325  	}
   326  
   327  	// For each word, see if it is a known iptables parameter, based on the struct
   328  	// field tags in Rule. Note that in the non-strict case we implicitly assume that
   329  	// no unrecognized parameter will take an argument that could be mistaken for
   330  	// another parameter.
   331  	v := reflect.ValueOf(parsed).Elem()
   332  	negated := false
   333  	for w := 0; w < len(words); {
   334  		if words[w] == "-m" && w < len(words)-1 {
   335  			// Skip "-m MODULE"; we don't pay attention to that since the
   336  			// parameter names are unique enough anyway.
   337  			w += 2
   338  			continue
   339  		}
   340  
   341  		if words[w] == "!" {
   342  			negated = true
   343  			w++
   344  			continue
   345  		}
   346  
   347  		// For everything else, see if it corresponds to a field of Rule
   348  		if field, negatable := findParamField(v, words[w]); field != nil {
   349  			if negated && !negatable {
   350  				return nil, fmt.Errorf("cannot negate parameter %q", words[w])
   351  			}
   352  			if field.Type() != boolPtrType && w == len(words)-1 {
   353  				return nil, fmt.Errorf("parameter %q requires an argument", words[w])
   354  			}
   355  			switch field.Type() {
   356  			case boolPtrType:
   357  				boolVal := !negated
   358  				field.Set(reflect.ValueOf(&boolVal))
   359  				w++
   360  			case ipTablesValuePtrType:
   361  				field.Set(reflect.ValueOf(&IPTablesValue{Negated: negated, Value: words[w+1]}))
   362  				w += 2
   363  			default:
   364  				field.SetString(words[w+1])
   365  				w += 2
   366  			}
   367  		} else if strict {
   368  			return nil, fmt.Errorf("unrecognized parameter %q", words[w])
   369  		} else {
   370  			// skip
   371  			w++
   372  		}
   373  
   374  		negated = false
   375  	}
   376  
   377  	return parsed, nil
   378  }
   379  

View as plain text