github.com/docker/engine@v22.0.0-20211208180946-d456264580cf+incompatible/libnetwork/iptables/iptables.go (about) 1 //go:build linux 2 // +build linux 3 4 package iptables 5 6 import ( 7 "errors" 8 "fmt" 9 "net" 10 "os/exec" 11 "regexp" 12 "strconv" 13 "strings" 14 "sync" 15 "time" 16 17 "github.com/sirupsen/logrus" 18 ) 19 20 // Action signifies the iptable action. 21 type Action string 22 23 // Policy is the default iptable policies 24 type Policy string 25 26 // Table refers to Nat, Filter or Mangle. 27 type Table string 28 29 // IPVersion refers to IP version, v4 or v6 30 type IPVersion string 31 32 const ( 33 // Append appends the rule at the end of the chain. 34 Append Action = "-A" 35 // Delete deletes the rule from the chain. 36 Delete Action = "-D" 37 // Insert inserts the rule at the top of the chain. 38 Insert Action = "-I" 39 // Nat table is used for nat translation rules. 40 Nat Table = "nat" 41 // Filter table is used for filter rules. 42 Filter Table = "filter" 43 // Mangle table is used for mangling the packet. 44 Mangle Table = "mangle" 45 // Drop is the default iptables DROP policy 46 Drop Policy = "DROP" 47 // Accept is the default iptables ACCEPT policy 48 Accept Policy = "ACCEPT" 49 // IPv4 is version 4 50 IPv4 IPVersion = "IPV4" 51 // IPv6 is version 6 52 IPv6 IPVersion = "IPV6" 53 ) 54 55 var ( 56 iptablesPath string 57 ip6tablesPath string 58 supportsXlock = false 59 supportsCOpt = false 60 xLockWaitMsg = "Another app is currently holding the xtables lock" 61 // used to lock iptables commands if xtables lock is not supported 62 bestEffortLock sync.Mutex 63 // ErrIptablesNotFound is returned when the rule is not found. 64 ErrIptablesNotFound = errors.New("Iptables not found") 65 initOnce sync.Once 66 ) 67 68 // IPTable defines struct with IPVersion 69 type IPTable struct { 70 Version IPVersion 71 } 72 73 // ChainInfo defines the iptables chain. 74 type ChainInfo struct { 75 Name string 76 Table Table 77 HairpinMode bool 78 IPTable IPTable 79 } 80 81 // ChainError is returned to represent errors during ip table operation. 82 type ChainError struct { 83 Chain string 84 Output []byte 85 } 86 87 func (e ChainError) Error() string { 88 return fmt.Sprintf("Error iptables %s: %s", e.Chain, string(e.Output)) 89 } 90 91 func probe() { 92 path, err := exec.LookPath("iptables") 93 if err != nil { 94 logrus.Warnf("Failed to find iptables: %v", err) 95 return 96 } 97 if out, err := exec.Command(path, "--wait", "-t", "nat", "-L", "-n").CombinedOutput(); err != nil { 98 logrus.Warnf("Running iptables --wait -t nat -L -n failed with message: `%s`, error: %v", strings.TrimSpace(string(out)), err) 99 } 100 _, err = exec.LookPath("ip6tables") 101 if err != nil { 102 logrus.Warnf("Failed to find ip6tables: %v", err) 103 return 104 } 105 } 106 107 func initFirewalld() { 108 if err := FirewalldInit(); err != nil { 109 logrus.Debugf("Fail to initialize firewalld: %v, using raw iptables instead", err) 110 } 111 } 112 113 func detectIptables() { 114 path, err := exec.LookPath("iptables") 115 if err != nil { 116 return 117 } 118 iptablesPath = path 119 path, err = exec.LookPath("ip6tables") 120 if err != nil { 121 return 122 } 123 ip6tablesPath = path 124 supportsXlock = exec.Command(iptablesPath, "--wait", "-L", "-n").Run() == nil 125 mj, mn, mc, err := GetVersion() 126 if err != nil { 127 logrus.Warnf("Failed to read iptables version: %v", err) 128 return 129 } 130 supportsCOpt = supportsCOption(mj, mn, mc) 131 } 132 133 func initDependencies() { 134 probe() 135 initFirewalld() 136 detectIptables() 137 } 138 139 func initCheck() error { 140 initOnce.Do(initDependencies) 141 142 if iptablesPath == "" { 143 return ErrIptablesNotFound 144 } 145 return nil 146 } 147 148 // GetIptable returns an instance of IPTable with specified version 149 func GetIptable(version IPVersion) *IPTable { 150 return &IPTable{Version: version} 151 } 152 153 // NewChain adds a new chain to ip table. 154 func (iptable IPTable) NewChain(name string, table Table, hairpinMode bool) (*ChainInfo, error) { 155 c := &ChainInfo{ 156 Name: name, 157 Table: table, 158 HairpinMode: hairpinMode, 159 IPTable: iptable, 160 } 161 if string(c.Table) == "" { 162 c.Table = Filter 163 } 164 165 // Add chain if it doesn't exist 166 if _, err := iptable.Raw("-t", string(c.Table), "-n", "-L", c.Name); err != nil { 167 if output, err := iptable.Raw("-t", string(c.Table), "-N", c.Name); err != nil { 168 return nil, err 169 } else if len(output) != 0 { 170 return nil, fmt.Errorf("Could not create %s/%s chain: %s", c.Table, c.Name, output) 171 } 172 } 173 return c, nil 174 } 175 176 // LoopbackByVersion returns loopback address by version 177 func (iptable IPTable) LoopbackByVersion() string { 178 if iptable.Version == IPv6 { 179 return "::1/128" 180 } 181 return "127.0.0.0/8" 182 } 183 184 // ProgramChain is used to add rules to a chain 185 func (iptable IPTable) ProgramChain(c *ChainInfo, bridgeName string, hairpinMode, enable bool) error { 186 if c.Name == "" { 187 return errors.New("Could not program chain, missing chain name") 188 } 189 190 // Either add or remove the interface from the firewalld zone 191 if firewalldRunning { 192 if enable { 193 if err := AddInterfaceFirewalld(bridgeName); err != nil { 194 return err 195 } 196 } else { 197 if err := DelInterfaceFirewalld(bridgeName); err != nil { 198 return err 199 } 200 } 201 } 202 203 switch c.Table { 204 case Nat: 205 preroute := []string{ 206 "-m", "addrtype", 207 "--dst-type", "LOCAL", 208 "-j", c.Name} 209 if !iptable.Exists(Nat, "PREROUTING", preroute...) && enable { 210 if err := c.Prerouting(Append, preroute...); err != nil { 211 return fmt.Errorf("Failed to inject %s in PREROUTING chain: %s", c.Name, err) 212 } 213 } else if iptable.Exists(Nat, "PREROUTING", preroute...) && !enable { 214 if err := c.Prerouting(Delete, preroute...); err != nil { 215 return fmt.Errorf("Failed to remove %s in PREROUTING chain: %s", c.Name, err) 216 } 217 } 218 output := []string{ 219 "-m", "addrtype", 220 "--dst-type", "LOCAL", 221 "-j", c.Name} 222 if !hairpinMode { 223 output = append(output, "!", "--dst", iptable.LoopbackByVersion()) 224 } 225 if !iptable.Exists(Nat, "OUTPUT", output...) && enable { 226 if err := c.Output(Append, output...); err != nil { 227 return fmt.Errorf("Failed to inject %s in OUTPUT chain: %s", c.Name, err) 228 } 229 } else if iptable.Exists(Nat, "OUTPUT", output...) && !enable { 230 if err := c.Output(Delete, output...); err != nil { 231 return fmt.Errorf("Failed to inject %s in OUTPUT chain: %s", c.Name, err) 232 } 233 } 234 case Filter: 235 if bridgeName == "" { 236 return fmt.Errorf("Could not program chain %s/%s, missing bridge name", 237 c.Table, c.Name) 238 } 239 link := []string{ 240 "-o", bridgeName, 241 "-j", c.Name} 242 if !iptable.Exists(Filter, "FORWARD", link...) && enable { 243 insert := append([]string{string(Insert), "FORWARD"}, link...) 244 if output, err := iptable.Raw(insert...); err != nil { 245 return err 246 } else if len(output) != 0 { 247 return fmt.Errorf("Could not create linking rule to %s/%s: %s", c.Table, c.Name, output) 248 } 249 } else if iptable.Exists(Filter, "FORWARD", link...) && !enable { 250 del := append([]string{string(Delete), "FORWARD"}, link...) 251 if output, err := iptable.Raw(del...); err != nil { 252 return err 253 } else if len(output) != 0 { 254 return fmt.Errorf("Could not delete linking rule from %s/%s: %s", c.Table, c.Name, output) 255 } 256 257 } 258 establish := []string{ 259 "-o", bridgeName, 260 "-m", "conntrack", 261 "--ctstate", "RELATED,ESTABLISHED", 262 "-j", "ACCEPT"} 263 if !iptable.Exists(Filter, "FORWARD", establish...) && enable { 264 insert := append([]string{string(Insert), "FORWARD"}, establish...) 265 if output, err := iptable.Raw(insert...); err != nil { 266 return err 267 } else if len(output) != 0 { 268 return fmt.Errorf("Could not create establish rule to %s: %s", c.Table, output) 269 } 270 } else if iptable.Exists(Filter, "FORWARD", establish...) && !enable { 271 del := append([]string{string(Delete), "FORWARD"}, establish...) 272 if output, err := iptable.Raw(del...); err != nil { 273 return err 274 } else if len(output) != 0 { 275 return fmt.Errorf("Could not delete establish rule from %s: %s", c.Table, output) 276 } 277 } 278 } 279 return nil 280 } 281 282 // RemoveExistingChain removes existing chain from the table. 283 func (iptable IPTable) RemoveExistingChain(name string, table Table) error { 284 c := &ChainInfo{ 285 Name: name, 286 Table: table, 287 IPTable: iptable, 288 } 289 if string(c.Table) == "" { 290 c.Table = Filter 291 } 292 return c.Remove() 293 } 294 295 // Forward adds forwarding rule to 'filter' table and corresponding nat rule to 'nat' table. 296 func (c *ChainInfo) Forward(action Action, ip net.IP, port int, proto, destAddr string, destPort int, bridgeName string) error { 297 298 iptable := GetIptable(c.IPTable.Version) 299 daddr := ip.String() 300 if ip.IsUnspecified() { 301 // iptables interprets "0.0.0.0" as "0.0.0.0/32", whereas we 302 // want "0.0.0.0/0". "0/0" is correctly interpreted as "any 303 // value" by both iptables and ip6tables. 304 daddr = "0/0" 305 } 306 307 args := []string{ 308 "-p", proto, 309 "-d", daddr, 310 "--dport", strconv.Itoa(port), 311 "-j", "DNAT", 312 "--to-destination", net.JoinHostPort(destAddr, strconv.Itoa(destPort))} 313 314 if !c.HairpinMode { 315 args = append(args, "!", "-i", bridgeName) 316 } 317 if err := iptable.ProgramRule(Nat, c.Name, action, args); err != nil { 318 return err 319 } 320 321 args = []string{ 322 "!", "-i", bridgeName, 323 "-o", bridgeName, 324 "-p", proto, 325 "-d", destAddr, 326 "--dport", strconv.Itoa(destPort), 327 "-j", "ACCEPT", 328 } 329 if err := iptable.ProgramRule(Filter, c.Name, action, args); err != nil { 330 return err 331 } 332 333 args = []string{ 334 "-p", proto, 335 "-s", destAddr, 336 "-d", destAddr, 337 "--dport", strconv.Itoa(destPort), 338 "-j", "MASQUERADE", 339 } 340 341 if err := iptable.ProgramRule(Nat, "POSTROUTING", action, args); err != nil { 342 return err 343 } 344 345 if proto == "sctp" { 346 // Linux kernel v4.9 and below enables NETIF_F_SCTP_CRC for veth by 347 // the following commit. 348 // This introduces a problem when conbined with a physical NIC without 349 // NETIF_F_SCTP_CRC. As for a workaround, here we add an iptables entry 350 // to fill the checksum. 351 // 352 // https://github.com/torvalds/linux/commit/c80fafbbb59ef9924962f83aac85531039395b18 353 args = []string{ 354 "-p", proto, 355 "--sport", strconv.Itoa(destPort), 356 "-j", "CHECKSUM", 357 "--checksum-fill", 358 } 359 if err := iptable.ProgramRule(Mangle, "POSTROUTING", action, args); err != nil { 360 return err 361 } 362 } 363 364 return nil 365 } 366 367 // Link adds reciprocal ACCEPT rule for two supplied IP addresses. 368 // Traffic is allowed from ip1 to ip2 and vice-versa 369 func (c *ChainInfo) Link(action Action, ip1, ip2 net.IP, port int, proto string, bridgeName string) error { 370 iptable := GetIptable(c.IPTable.Version) 371 // forward 372 args := []string{ 373 "-i", bridgeName, "-o", bridgeName, 374 "-p", proto, 375 "-s", ip1.String(), 376 "-d", ip2.String(), 377 "--dport", strconv.Itoa(port), 378 "-j", "ACCEPT", 379 } 380 381 if err := iptable.ProgramRule(Filter, c.Name, action, args); err != nil { 382 return err 383 } 384 // reverse 385 args[7], args[9] = args[9], args[7] 386 args[10] = "--sport" 387 return iptable.ProgramRule(Filter, c.Name, action, args) 388 } 389 390 // ProgramRule adds the rule specified by args only if the 391 // rule is not already present in the chain. Reciprocally, 392 // it removes the rule only if present. 393 func (iptable IPTable) ProgramRule(table Table, chain string, action Action, args []string) error { 394 if iptable.Exists(table, chain, args...) != (action == Delete) { 395 return nil 396 } 397 return iptable.RawCombinedOutput(append([]string{"-t", string(table), string(action), chain}, args...)...) 398 } 399 400 // Prerouting adds linking rule to nat/PREROUTING chain. 401 func (c *ChainInfo) Prerouting(action Action, args ...string) error { 402 iptable := GetIptable(c.IPTable.Version) 403 a := []string{"-t", string(Nat), string(action), "PREROUTING"} 404 if len(args) > 0 { 405 a = append(a, args...) 406 } 407 if output, err := iptable.Raw(a...); err != nil { 408 return err 409 } else if len(output) != 0 { 410 return ChainError{Chain: "PREROUTING", Output: output} 411 } 412 return nil 413 } 414 415 // Output adds linking rule to an OUTPUT chain. 416 func (c *ChainInfo) Output(action Action, args ...string) error { 417 iptable := GetIptable(c.IPTable.Version) 418 a := []string{"-t", string(c.Table), string(action), "OUTPUT"} 419 if len(args) > 0 { 420 a = append(a, args...) 421 } 422 if output, err := iptable.Raw(a...); err != nil { 423 return err 424 } else if len(output) != 0 { 425 return ChainError{Chain: "OUTPUT", Output: output} 426 } 427 return nil 428 } 429 430 // Remove removes the chain. 431 func (c *ChainInfo) Remove() error { 432 iptable := GetIptable(c.IPTable.Version) 433 // Ignore errors - This could mean the chains were never set up 434 if c.Table == Nat { 435 c.Prerouting(Delete, "-m", "addrtype", "--dst-type", "LOCAL", "-j", c.Name) 436 c.Output(Delete, "-m", "addrtype", "--dst-type", "LOCAL", "!", "--dst", iptable.LoopbackByVersion(), "-j", c.Name) 437 c.Output(Delete, "-m", "addrtype", "--dst-type", "LOCAL", "-j", c.Name) // Created in versions <= 0.1.6 438 439 c.Prerouting(Delete) 440 c.Output(Delete) 441 } 442 iptable.Raw("-t", string(c.Table), "-F", c.Name) 443 iptable.Raw("-t", string(c.Table), "-X", c.Name) 444 return nil 445 } 446 447 // Exists checks if a rule exists 448 func (iptable IPTable) Exists(table Table, chain string, rule ...string) bool { 449 return iptable.exists(false, table, chain, rule...) 450 } 451 452 // ExistsNative behaves as Exists with the difference it 453 // will always invoke `iptables` binary. 454 func (iptable IPTable) ExistsNative(table Table, chain string, rule ...string) bool { 455 return iptable.exists(true, table, chain, rule...) 456 } 457 458 func (iptable IPTable) exists(native bool, table Table, chain string, rule ...string) bool { 459 f := iptable.Raw 460 if native { 461 f = iptable.raw 462 } 463 464 if string(table) == "" { 465 table = Filter 466 } 467 468 if err := initCheck(); err != nil { 469 // The exists() signature does not allow us to return an error, but at least 470 // we can skip the (likely invalid) exec invocation. 471 return false 472 } 473 474 if supportsCOpt { 475 // if exit status is 0 then return true, the rule exists 476 _, err := f(append([]string{"-t", string(table), "-C", chain}, rule...)...) 477 return err == nil 478 } 479 480 // parse "iptables -S" for the rule (it checks rules in a specific chain 481 // in a specific table and it is very unreliable) 482 return iptable.existsRaw(table, chain, rule...) 483 } 484 485 func (iptable IPTable) existsRaw(table Table, chain string, rule ...string) bool { 486 path := iptablesPath 487 if iptable.Version == IPv6 { 488 path = ip6tablesPath 489 } 490 ruleString := fmt.Sprintf("%s %s\n", chain, strings.Join(rule, " ")) 491 existingRules, _ := exec.Command(path, "-t", string(table), "-S", chain).Output() 492 493 return strings.Contains(string(existingRules), ruleString) 494 } 495 496 // Maximum duration that an iptables operation can take 497 // before flagging a warning. 498 const opWarnTime = 2 * time.Second 499 500 func filterOutput(start time.Time, output []byte, args ...string) []byte { 501 // Flag operations that have taken a long time to complete 502 opTime := time.Since(start) 503 if opTime > opWarnTime { 504 logrus.Warnf("xtables contention detected while running [%s]: Waited for %.2f seconds and received %q", strings.Join(args, " "), float64(opTime)/float64(time.Second), string(output)) 505 } 506 // ignore iptables' message about xtables lock: 507 // it is a warning, not an error. 508 if strings.Contains(string(output), xLockWaitMsg) { 509 output = []byte("") 510 } 511 // Put further filters here if desired 512 return output 513 } 514 515 // Raw calls 'iptables' system command, passing supplied arguments. 516 func (iptable IPTable) Raw(args ...string) ([]byte, error) { 517 if firewalldRunning { 518 // select correct IP version for firewalld 519 ipv := Iptables 520 if iptable.Version == IPv6 { 521 ipv = IP6Tables 522 } 523 524 startTime := time.Now() 525 output, err := Passthrough(ipv, args...) 526 if err == nil || !strings.Contains(err.Error(), "was not provided by any .service files") { 527 return filterOutput(startTime, output, args...), err 528 } 529 } 530 return iptable.raw(args...) 531 } 532 533 func (iptable IPTable) raw(args ...string) ([]byte, error) { 534 if err := initCheck(); err != nil { 535 return nil, err 536 } 537 if supportsXlock { 538 args = append([]string{"--wait"}, args...) 539 } else { 540 bestEffortLock.Lock() 541 defer bestEffortLock.Unlock() 542 } 543 544 path := iptablesPath 545 commandName := "iptables" 546 if iptable.Version == IPv6 { 547 path = ip6tablesPath 548 commandName = "ip6tables" 549 } 550 551 logrus.Debugf("%s, %v", path, args) 552 553 startTime := time.Now() 554 output, err := exec.Command(path, args...).CombinedOutput() 555 if err != nil { 556 return nil, fmt.Errorf("iptables failed: %s %v: %s (%s)", commandName, strings.Join(args, " "), output, err) 557 } 558 559 return filterOutput(startTime, output, args...), err 560 } 561 562 // RawCombinedOutput internally calls the Raw function and returns a non nil 563 // error if Raw returned a non nil error or a non empty output 564 func (iptable IPTable) RawCombinedOutput(args ...string) error { 565 if output, err := iptable.Raw(args...); err != nil || len(output) != 0 { 566 return fmt.Errorf("%s (%v)", string(output), err) 567 } 568 return nil 569 } 570 571 // RawCombinedOutputNative behave as RawCombinedOutput with the difference it 572 // will always invoke `iptables` binary 573 func (iptable IPTable) RawCombinedOutputNative(args ...string) error { 574 if output, err := iptable.raw(args...); err != nil || len(output) != 0 { 575 return fmt.Errorf("%s (%v)", string(output), err) 576 } 577 return nil 578 } 579 580 // ExistChain checks if a chain exists 581 func (iptable IPTable) ExistChain(chain string, table Table) bool { 582 if _, err := iptable.Raw("-t", string(table), "-nL", chain); err == nil { 583 return true 584 } 585 return false 586 } 587 588 // GetVersion reads the iptables version numbers during initialization 589 func GetVersion() (major, minor, micro int, err error) { 590 out, err := exec.Command(iptablesPath, "--version").CombinedOutput() 591 if err == nil { 592 major, minor, micro = parseVersionNumbers(string(out)) 593 } 594 return 595 } 596 597 // SetDefaultPolicy sets the passed default policy for the table/chain 598 func (iptable IPTable) SetDefaultPolicy(table Table, chain string, policy Policy) error { 599 if err := iptable.RawCombinedOutput("-t", string(table), "-P", chain, string(policy)); err != nil { 600 return fmt.Errorf("setting default policy to %v in %v chain failed: %v", policy, chain, err) 601 } 602 return nil 603 } 604 605 func parseVersionNumbers(input string) (major, minor, micro int) { 606 re := regexp.MustCompile(`v\d*.\d*.\d*`) 607 line := re.FindString(input) 608 fmt.Sscanf(line, "v%d.%d.%d", &major, &minor, µ) 609 return 610 } 611 612 // iptables -C, --check option was added in v.1.4.11 613 // http://ftp.netfilter.org/pub/iptables/changes-iptables-1.4.11.txt 614 func supportsCOption(mj, mn, mc int) bool { 615 return mj > 1 || (mj == 1 && (mn > 4 || (mn == 4 && mc >= 11))) 616 } 617 618 // AddReturnRule adds a return rule for the chain in the filter table 619 func (iptable IPTable) AddReturnRule(chain string) error { 620 var ( 621 table = Filter 622 args = []string{"-j", "RETURN"} 623 ) 624 625 if iptable.Exists(table, chain, args...) { 626 return nil 627 } 628 629 err := iptable.RawCombinedOutput(append([]string{"-A", chain}, args...)...) 630 if err != nil { 631 return fmt.Errorf("unable to add return rule in %s chain: %s", chain, err.Error()) 632 } 633 634 return nil 635 } 636 637 // EnsureJumpRule ensures the jump rule is on top 638 func (iptable IPTable) EnsureJumpRule(fromChain, toChain string) error { 639 var ( 640 table = Filter 641 args = []string{"-j", toChain} 642 ) 643 644 if iptable.Exists(table, fromChain, args...) { 645 err := iptable.RawCombinedOutput(append([]string{"-D", fromChain}, args...)...) 646 if err != nil { 647 return fmt.Errorf("unable to remove jump to %s rule in %s chain: %s", toChain, fromChain, err.Error()) 648 } 649 } 650 651 err := iptable.RawCombinedOutput(append([]string{"-I", fromChain}, args...)...) 652 if err != nil { 653 return fmt.Errorf("unable to insert jump to %s rule in %s chain: %s", toChain, fromChain, err.Error()) 654 } 655 656 return nil 657 }