k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/util/iptables/testing/parse.go (about) 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 }