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 }