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