github.com/aporeto-inc/trireme-lib@v10.358.0+incompatible/controller/pkg/aclprovider/iptablesprovider.go (about) 1 // +build linux darwin 2 3 package provider 4 5 import ( 6 "bytes" 7 "errors" 8 "fmt" 9 "os/exec" 10 "strings" 11 "sync" 12 13 "go.uber.org/zap" 14 ) 15 16 // IptablesProvider is an abstraction of all the methods an implementation of userspace 17 // iptables need to provide. 18 type IptablesProvider interface { 19 BaseIPTables 20 // Commit will commit changes if it is a batch provider. 21 Commit() error 22 // RetrieveTable allows a caller to retrieve the final table. 23 RetrieveTable() map[string]map[string][]string 24 // ResetRules resets the rules to a state where rules with the substring subs are removed 25 ResetRules(subs string) error 26 } 27 28 // BaseIPTables is the base interface of iptables functions. 29 type BaseIPTables interface { 30 // Append apends a rule to chain of table 31 Append(table, chain string, rulespec ...string) error 32 // Insert inserts a rule to a chain of table at the required pos 33 Insert(table, chain string, pos int, rulespec ...string) error 34 // Delete deletes a rule of a chain in the given table 35 Delete(table, chain string, rulespec ...string) error 36 // ListChains lists all the chains associated with a table 37 ListChains(table string) ([]string, error) 38 // ClearChain clears a chain in a table 39 ClearChain(table, chain string) error 40 // DeleteChain deletes a chain in the table. There should be no references to this chain 41 DeleteChain(table, chain string) error 42 // NewChain creates a new chain 43 NewChain(table, chain string) error 44 // ListRules lists the rules in the table/chain passed to it 45 ListRules(table, chain string) ([]string, error) 46 } 47 48 // BatchProvider uses iptables-restore to program ACLs 49 type BatchProvider struct { 50 ipt BaseIPTables 51 52 // TABLE CHAIN RULES 53 rules map[string]map[string][]string 54 batchTables map[string]bool 55 56 // Allowing for custom commit functions for testing 57 commitFunc func(buf *bytes.Buffer) error 58 customChain string 59 sync.Mutex 60 cmd string 61 restoreCmd string 62 saveCmd string 63 quote bool 64 } 65 66 const ( 67 cmdV4 = "iptables --wait" 68 cmdV6 = "ip6tables --wait" 69 restoreCmdV4 = "iptables-restore" 70 restoreCmdV6 = "ip6tables-restore" 71 saveCmdV4 = "iptables-save" 72 saveCmdV6 = "ip6tables-save" 73 ) 74 75 // TestIptablesPinned returns error if the kernel doesn't support bpf pinning in iptables 76 func TestIptablesPinned(bpf string) error { 77 cmd := exec.Command("aporeto-iptables", strings.Fields("iptables --wait -t mangle -I OUTPUT -m bpf --object-pinned "+bpf+" -j LOG")...) 78 if _, err := cmd.CombinedOutput(); err != nil { 79 return err 80 } 81 82 cmd = exec.Command("aporeto-iptables", strings.Fields("iptables --wait -t mangle -D OUTPUT -m bpf --object-pinned "+bpf+" -j LOG")...) 83 if _, err := cmd.CombinedOutput(); err != nil { 84 zap.L().Error("Error removing rule", zap.Error(err)) 85 } 86 87 return nil 88 } 89 90 // NewGoIPTablesProviderV4 returns an IptablesProvider interface based on the go-iptables 91 // external package. 92 func NewGoIPTablesProviderV4(batchTables []string, customChain string) (IptablesProvider, error) { 93 94 batchTablesMap := map[string]bool{} 95 for _, t := range batchTables { 96 batchTablesMap[t] = true 97 } 98 99 b := &BatchProvider{ 100 cmd: cmdV4, 101 rules: map[string]map[string][]string{}, 102 batchTables: batchTablesMap, 103 restoreCmd: restoreCmdV4, 104 saveCmd: saveCmdV4, 105 customChain: customChain, 106 quote: true, 107 } 108 109 b.commitFunc = b.restore 110 111 return b, nil 112 } 113 114 // NewGoIPTablesProviderV6 returns an IptablesProvider interface based on the go-iptables 115 // external package. 116 func NewGoIPTablesProviderV6(batchTables []string, customChain string) (IptablesProvider, error) { 117 118 batchTablesMap := map[string]bool{} 119 for _, t := range batchTables { 120 batchTablesMap[t] = true 121 } 122 123 b := &BatchProvider{ 124 cmd: cmdV6, 125 rules: map[string]map[string][]string{}, 126 batchTables: batchTablesMap, 127 customChain: customChain, 128 restoreCmd: restoreCmdV6, 129 saveCmd: saveCmdV6, 130 quote: true, 131 } 132 133 b.commitFunc = b.restore 134 135 return b, nil 136 } 137 138 // NewCustomBatchProvider is a custom batch provider wher the downstream 139 // iptables utility is provided by the caller. Very useful for testing 140 // the ACL functions with a mock. 141 func NewCustomBatchProvider(ipt BaseIPTables, commit func(buf *bytes.Buffer) error, batchTables []string) *BatchProvider { 142 143 batchTablesMap := map[string]bool{} 144 145 for _, t := range batchTables { 146 batchTablesMap[t] = true 147 } 148 149 return &BatchProvider{ 150 ipt: ipt, 151 rules: map[string]map[string][]string{}, 152 batchTables: batchTablesMap, 153 commitFunc: commit, 154 } 155 } 156 157 func createIPtablesCommand(iptablesCmd, table, chain, action string, rulespec ...string) []string { 158 cmd := strings.Fields(iptablesCmd) 159 cmd = append(cmd, "-t") 160 cmd = append(cmd, table) 161 cmd = append(cmd, action) 162 cmd = append(cmd, chain) 163 cmd = append(cmd, rulespec...) 164 return cmd 165 } 166 167 // Append will append the provided rule to the local cache or call 168 // directly the iptables command depending on the table. 169 func (b *BatchProvider) Append(table, chain string, rulespec ...string) error { 170 b.Lock() 171 defer b.Unlock() 172 173 if len(rulespec) == 0 { 174 return nil 175 } 176 177 if _, ok := b.batchTables[table]; !ok { 178 cmd := createIPtablesCommand(b.cmd, table, chain, "-A", rulespec...) 179 execCmd := exec.Command("aporeto-iptables", cmd...) 180 s, err := execCmd.CombinedOutput() 181 if err != nil { 182 return errors.New(string(s)) 183 } 184 185 return nil 186 } 187 188 if _, ok := b.rules[table]; !ok { 189 b.rules[table] = map[string][]string{} 190 } 191 192 if _, ok := b.rules[table][chain]; !ok { 193 b.rules[table][chain] = []string{} 194 } 195 196 b.quoteRulesSpec(rulespec) 197 198 rule := strings.Join(rulespec, " ") 199 b.rules[table][chain] = append(b.rules[table][chain], rule) 200 201 return nil 202 } 203 204 // Insert will insert the rule in the corresponding position in the local 205 // cache or call the corresponding iptables command, depending on the table. 206 func (b *BatchProvider) Insert(table, chain string, pos int, rulespec ...string) error { 207 208 b.Lock() 209 defer b.Unlock() 210 211 if _, ok := b.batchTables[table]; !ok { 212 cmd := createIPtablesCommand(b.cmd, table, chain, "-I", rulespec...) 213 execCmd := exec.Command("aporeto-iptables", cmd...) 214 s, err := execCmd.CombinedOutput() 215 if err != nil { 216 return errors.New(string(s)) 217 } 218 return nil 219 } 220 221 if _, ok := b.rules[table]; !ok { 222 b.rules[table] = map[string][]string{} 223 } 224 225 if _, ok := b.rules[table][chain]; !ok { 226 b.rules[table][chain] = []string{} 227 } 228 229 b.quoteRulesSpec(rulespec) 230 231 rule := strings.Join(rulespec, " ") 232 233 if pos == 1 { 234 b.rules[table][chain] = append([]string{rule}, b.rules[table][chain]...) 235 } else if pos > len(b.rules[table][chain]) { 236 b.rules[table][chain] = append(b.rules[table][chain], rule) 237 } else { 238 b.rules[table][chain] = append(b.rules[table][chain], "newvalue") 239 copy(b.rules[table][chain][pos-1:], b.rules[table][chain][pos-2:]) 240 b.rules[table][chain][pos-1] = rule 241 } 242 243 return nil 244 } 245 246 // Delete will delete the rule from the local cache or the system. 247 func (b *BatchProvider) Delete(table, chain string, rulespec ...string) error { 248 b.Lock() 249 defer b.Unlock() 250 251 if _, ok := b.batchTables[table]; !ok { 252 cmd := createIPtablesCommand(b.cmd, table, chain, "-D", rulespec...) 253 execCmd := exec.Command("aporeto-iptables", cmd...) 254 s, err := execCmd.CombinedOutput() 255 if err != nil { 256 return errors.New(string(s)) 257 } 258 return nil 259 } 260 261 if _, ok := b.rules[table]; !ok { 262 return nil 263 } 264 265 if _, ok := b.rules[table][chain]; !ok { 266 return nil 267 } 268 269 b.quoteRulesSpec(rulespec) 270 271 rule := strings.Join(rulespec, " ") 272 for index, r := range b.rules[table][chain] { 273 if rule == r { 274 switch index { 275 case 0: 276 if len(b.rules[table][chain]) == 1 { 277 b.rules[table][chain] = []string{} 278 } else { 279 b.rules[table][chain] = b.rules[table][chain][1:] 280 } 281 case len(b.rules[table][chain]) - 1: 282 b.rules[table][chain] = b.rules[table][chain][:index] 283 default: 284 b.rules[table][chain] = append(b.rules[table][chain][:index], b.rules[table][chain][index+1:]...) 285 } 286 break 287 } 288 } 289 290 return nil 291 } 292 293 // ListChains returns a slice containing the name of each chain in the specified table. 294 func listChains(iptablesCmd, table string) ([]string, error) { 295 cmd := strings.Fields(iptablesCmd) 296 cmd = append(cmd, []string{"-t", table, "-S"}...) 297 298 execCmd := exec.Command("aporeto-iptables", cmd...) 299 out, err := execCmd.CombinedOutput() 300 if err != nil { 301 return nil, errors.New(string(out)) 302 } 303 304 result := strings.Split(string(out), "\n") 305 306 // Iterate over rules to find all default (-P) and user-specified (-N) chains. 307 // Chains definition always come before rules. 308 // Format is the following: 309 // -P OUTPUT ACCEPT 310 // -N Custom 311 var chains []string 312 for _, val := range result { 313 if strings.HasPrefix(val, "-P") || strings.HasPrefix(val, "-N") { 314 chains = append(chains, strings.Fields(val)[1]) 315 } else { 316 break 317 } 318 } 319 return chains, nil 320 } 321 322 // ListChains will provide a list of the current chains. 323 func (b *BatchProvider) ListChains(table string) ([]string, error) { 324 b.Lock() 325 defer b.Unlock() 326 327 chains, err := listChains(b.cmd, table) 328 if err != nil { 329 return []string{}, err 330 } 331 332 if _, ok := b.batchTables[table]; !ok || b.rules[table] == nil { 333 return chains, nil 334 } 335 336 for _, chain := range chains { 337 if _, ok := b.rules[table][chain]; !ok { 338 b.rules[table][chain] = []string{} 339 } 340 } 341 342 allChains := make([]string, len(b.rules[table])) 343 i := 0 344 for chain := range b.rules[table] { 345 allChains[i] = chain 346 i++ 347 } 348 349 return allChains, nil 350 } 351 352 // ClearChain will clear the chains. 353 func (b *BatchProvider) ClearChain(table, chain string) error { 354 355 b.Lock() 356 defer b.Unlock() 357 358 if _, ok := b.batchTables[table]; !ok { 359 cmd := strings.Fields(b.cmd) 360 cmd = append(cmd, []string{"-t", table, "-F", chain}...) 361 execCmd := exec.Command("aporeto-iptables", cmd...) 362 s, err := execCmd.CombinedOutput() 363 if err != nil { 364 return errors.New(string(s)) 365 } 366 return nil 367 } 368 369 if _, ok := b.rules[table]; !ok { 370 return nil 371 } 372 if _, ok := b.rules[table][chain]; !ok { 373 return nil 374 } 375 376 b.rules[table][chain] = []string{} 377 return nil 378 } 379 380 // DeleteChain will delete the chains. 381 func (b *BatchProvider) DeleteChain(table, chain string) error { 382 b.Lock() 383 defer b.Unlock() 384 385 if _, ok := b.batchTables[table]; !ok { 386 cmd := strings.Fields(b.cmd) 387 cmd = append(cmd, []string{"-t", table, "-X", chain}...) 388 execCmd := exec.Command("aporeto-iptables", cmd...) 389 s, err := execCmd.CombinedOutput() 390 if err != nil { 391 return errors.New(string(s)) 392 } 393 return nil 394 } 395 396 if _, ok := b.rules[table]; !ok { 397 return nil 398 } 399 400 delete(b.rules[table], chain) 401 return nil 402 } 403 404 // NewChain creates a new chain. 405 func (b *BatchProvider) NewChain(table, chain string) error { 406 b.Lock() 407 defer b.Unlock() 408 409 if _, ok := b.batchTables[table]; !ok { 410 cmd := strings.Fields(b.cmd) 411 cmd = append(cmd, []string{"-t", table, "-N", chain}...) 412 execCmd := exec.Command("aporeto-iptables", cmd...) 413 s, err := execCmd.CombinedOutput() 414 if err != nil { 415 return errors.New(string(s)) 416 } 417 return nil 418 } 419 420 if _, ok := b.rules[table]; !ok { 421 b.rules[table] = map[string][]string{} 422 } 423 424 b.rules[table][chain] = []string{} 425 return nil 426 } 427 428 // Commit commits the rules to the system 429 func (b *BatchProvider) Commit() error { 430 b.Lock() 431 defer b.Unlock() 432 433 // We don't commit if we don't have any tables. This is old 434 // kernel compatibility mode. 435 if len(b.batchTables) == 0 { 436 return nil 437 } 438 439 buf, err := b.createDataBuffer() 440 if err != nil { 441 return fmt.Errorf("Failed to crete buffer %s", err) 442 } 443 444 return b.commitFunc(buf) 445 } 446 447 // RetrieveTable allows a caller to retrieve the final table. Mostly 448 // needed for debuging and unit tests. 449 func (b *BatchProvider) RetrieveTable() map[string]map[string][]string { 450 b.Lock() 451 defer b.Unlock() 452 453 return b.rules 454 } 455 456 func (b *BatchProvider) createDataBuffer() (*bytes.Buffer, error) { 457 458 buf := bytes.NewBuffer([]byte{}) 459 460 for table := range b.rules { 461 if _, err := fmt.Fprintf(buf, "*%s\n", table); err != nil { 462 return nil, err 463 } 464 for chain := range b.rules[table] { 465 if _, err := fmt.Fprintf(buf, ":%s - [0:0]\n", chain); err != nil { 466 return nil, err 467 } 468 } 469 for chain := range b.rules[table] { 470 for _, rule := range b.rules[table][chain] { 471 if _, err := fmt.Fprintf(buf, "-A %s %s\n", chain, rule); err != nil { 472 return nil, err 473 } 474 } 475 } 476 customChainRules, _ := b.saveCustomChainRules() 477 fmt.Fprintf(buf, "%s\n", customChainRules.String()) 478 if _, err := fmt.Fprintf(buf, "COMMIT\n"); err != nil { 479 return nil, err 480 } 481 } 482 return buf, nil 483 } 484 485 // restore will save the current DB to iptables. 486 func (b *BatchProvider) restore(buf *bytes.Buffer) error { 487 488 cmd := exec.Command("aporeto-iptables", b.restoreCmd, "--wait") 489 cmd.Stdin = buf 490 out, err := cmd.CombinedOutput() 491 if err != nil { 492 again, _ := b.createDataBuffer() 493 zap.L().Error("Failed to execute command", zap.Error(err), 494 zap.ByteString("Output", out), 495 zap.String("Output", again.String()), 496 ) 497 return fmt.Errorf("Failed to execute iptables-restore: %s", err) 498 } 499 return nil 500 } 501 502 func (b *BatchProvider) quoteRulesSpec(rulesspec []string) { 503 504 if !b.quote { 505 return 506 } 507 508 for i, rule := range rulesspec { 509 if len(rulesspec[i]) > 0 && rulesspec[i][0] == '"' { 510 continue 511 } 512 513 rulesspec[i] = fmt.Sprintf("\"%s\"", rule) 514 } 515 } 516 517 // ResetRules resets the rules to the original form. 518 // It is implemented as "iptables-save | grep "-v" subs | iptables-restore" 519 func (b *BatchProvider) ResetRules(subs string) error { 520 521 var out []byte 522 var err error 523 524 cmd := exec.Command("aporeto-iptables", b.saveCmd) 525 if out, err = cmd.CombinedOutput(); err != nil { 526 zap.L().Error("Failed to get iptables-save command", zap.Error(err), 527 zap.String("Output", string(out))) 528 return err 529 } 530 531 s := string(out) 532 rules := strings.Split(s, "\n") 533 534 var filterRules []string 535 536 for _, rule := range rules { 537 if !strings.Contains(rule, subs) { 538 filterRules = append(filterRules, rule) 539 } 540 } 541 542 combineRules := strings.Join(filterRules, "\n") 543 buf := bytes.NewBufferString(combineRules) 544 545 return b.commitFunc(buf) 546 } 547 548 func (b *BatchProvider) saveCustomChainRules() (*bytes.Buffer, error) { 549 var out []byte 550 var err error 551 552 cmd := exec.Command("aporeto-iptables", b.saveCmd) 553 if out, err = cmd.CombinedOutput(); err != nil { 554 zap.L().Error("Failed to get iptables-save command", zap.Error(err), 555 zap.String("Output", string(out))) 556 return nil, err 557 } 558 559 s := string(out) 560 rules := strings.Split(s, "\n") 561 562 var filterRules []string 563 564 for _, rule := range rules { 565 if strings.Contains(rule, b.customChain) { 566 filterRules = append(filterRules, rule) 567 } 568 } 569 570 combineRules := strings.Join(filterRules, "\n") 571 return bytes.NewBufferString(combineRules), nil 572 573 } 574 575 // ListRules lists the rules in the table/chain passed to it 576 func (b *BatchProvider) ListRules(table, chain string) ([]string, error) { 577 var cmd *exec.Cmd 578 579 if chain != "" { 580 cmd = exec.Command("aporeto-iptables", "iptables", "--wait", "-t", table, "-L", chain) 581 } else { 582 cmd = exec.Command("aporeto-iptables", "iptables", "-wait", "-t", table, "-L") 583 } 584 out, err := cmd.CombinedOutput() 585 if err != nil { 586 zap.L().Error("Failed to get rules", zap.Error(err), zap.String("table", table), zap.String("chain", chain)) 587 return []string{}, err 588 } 589 rules := strings.Split(string(out), "\n") 590 return rules, nil 591 592 }