81e1cdca21
We encountered a situation where concurrent invocations of the docker daemon on a machine with an older version of iptables led to nondeterministic errors related to simultaenous invocations of iptables. While this is best resolved by upgrading iptables itself, the particular situation would have been avoided if the docker daemon simply took care not to concurrently invoke iptables. Of course, external processes could also cause iptables to fail in this way, but invoking docker in parallel seems like a pretty common case. Signed-off-by: Aaron Davidson <aaron@databricks.com>
312 lines
8 KiB
Go
312 lines
8 KiB
Go
package iptables
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os/exec"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/Sirupsen/logrus"
|
|
)
|
|
|
|
type Action string
|
|
type Table string
|
|
|
|
const (
|
|
Append Action = "-A"
|
|
Delete Action = "-D"
|
|
Insert Action = "-I"
|
|
Nat Table = "nat"
|
|
Filter Table = "filter"
|
|
Mangle Table = "mangle"
|
|
)
|
|
|
|
var (
|
|
iptablesPath string
|
|
supportsXlock = false
|
|
// used to lock iptables commands if xtables lock is not supported
|
|
bestEffortLock sync.Mutex
|
|
ErrIptablesNotFound = errors.New("Iptables not found")
|
|
)
|
|
|
|
type Chain struct {
|
|
Name string
|
|
Bridge string
|
|
Table Table
|
|
}
|
|
|
|
type ChainError struct {
|
|
Chain string
|
|
Output []byte
|
|
}
|
|
|
|
func (e ChainError) Error() string {
|
|
return fmt.Sprintf("Error iptables %s: %s", e.Chain, string(e.Output))
|
|
}
|
|
|
|
func initCheck() error {
|
|
|
|
if iptablesPath == "" {
|
|
path, err := exec.LookPath("iptables")
|
|
if err != nil {
|
|
return ErrIptablesNotFound
|
|
}
|
|
iptablesPath = path
|
|
supportsXlock = exec.Command(iptablesPath, "--wait", "-L", "-n").Run() == nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func NewChain(name, bridge string, table Table) (*Chain, error) {
|
|
c := &Chain{
|
|
Name: name,
|
|
Bridge: bridge,
|
|
Table: table,
|
|
}
|
|
|
|
if string(c.Table) == "" {
|
|
c.Table = Filter
|
|
}
|
|
|
|
// Add chain if it doesn't exist
|
|
if _, err := Raw("-t", string(c.Table), "-n", "-L", c.Name); err != nil {
|
|
if output, err := Raw("-t", string(c.Table), "-N", c.Name); err != nil {
|
|
return nil, err
|
|
} else if len(output) != 0 {
|
|
return nil, fmt.Errorf("Could not create %s/%s chain: %s", c.Table, c.Name, output)
|
|
}
|
|
}
|
|
|
|
switch table {
|
|
case Nat:
|
|
preroute := []string{
|
|
"-m", "addrtype",
|
|
"--dst-type", "LOCAL"}
|
|
if !Exists(Nat, "PREROUTING", preroute...) {
|
|
if err := c.Prerouting(Append, preroute...); err != nil {
|
|
return nil, fmt.Errorf("Failed to inject docker in PREROUTING chain: %s", err)
|
|
}
|
|
}
|
|
output := []string{
|
|
"-m", "addrtype",
|
|
"--dst-type", "LOCAL",
|
|
"!", "--dst", "127.0.0.0/8"}
|
|
if !Exists(Nat, "OUTPUT", output...) {
|
|
if err := c.Output(Append, output...); err != nil {
|
|
return nil, fmt.Errorf("Failed to inject docker in OUTPUT chain: %s", err)
|
|
}
|
|
}
|
|
case Filter:
|
|
link := []string{
|
|
"-o", c.Bridge,
|
|
"-j", c.Name}
|
|
if !Exists(Filter, "FORWARD", link...) {
|
|
insert := append([]string{string(Insert), "FORWARD"}, link...)
|
|
if output, err := Raw(insert...); err != nil {
|
|
return nil, err
|
|
} else if len(output) != 0 {
|
|
return nil, fmt.Errorf("Could not create linking rule to %s/%s: %s", c.Table, c.Name, output)
|
|
}
|
|
}
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
func RemoveExistingChain(name string, table Table) error {
|
|
c := &Chain{
|
|
Name: name,
|
|
Table: table,
|
|
}
|
|
if string(c.Table) == "" {
|
|
c.Table = Filter
|
|
}
|
|
return c.Remove()
|
|
}
|
|
|
|
// Add forwarding rule to 'filter' table and corresponding nat rule to 'nat' table
|
|
func (c *Chain) Forward(action Action, ip net.IP, port int, proto, destAddr string, destPort int) error {
|
|
daddr := ip.String()
|
|
if ip.IsUnspecified() {
|
|
// iptables interprets "0.0.0.0" as "0.0.0.0/32", whereas we
|
|
// want "0.0.0.0/0". "0/0" is correctly interpreted as "any
|
|
// value" by both iptables and ip6tables.
|
|
daddr = "0/0"
|
|
}
|
|
if output, err := Raw("-t", string(Nat), string(action), c.Name,
|
|
"-p", proto,
|
|
"-d", daddr,
|
|
"--dport", strconv.Itoa(port),
|
|
"!", "-i", c.Bridge,
|
|
"-j", "DNAT",
|
|
"--to-destination", net.JoinHostPort(destAddr, strconv.Itoa(destPort))); err != nil {
|
|
return err
|
|
} else if len(output) != 0 {
|
|
return ChainError{Chain: "FORWARD", Output: output}
|
|
}
|
|
|
|
if output, err := Raw("-t", string(Filter), string(action), c.Name,
|
|
"!", "-i", c.Bridge,
|
|
"-o", c.Bridge,
|
|
"-p", proto,
|
|
"-d", destAddr,
|
|
"--dport", strconv.Itoa(destPort),
|
|
"-j", "ACCEPT"); err != nil {
|
|
return err
|
|
} else if len(output) != 0 {
|
|
return ChainError{Chain: "FORWARD", Output: output}
|
|
}
|
|
|
|
if output, err := Raw("-t", string(Nat), string(action), "POSTROUTING",
|
|
"-p", proto,
|
|
"-s", destAddr,
|
|
"-d", destAddr,
|
|
"--dport", strconv.Itoa(destPort),
|
|
"-j", "MASQUERADE"); err != nil {
|
|
return err
|
|
} else if len(output) != 0 {
|
|
return ChainError{Chain: "FORWARD", Output: output}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Add reciprocal ACCEPT rule for two supplied IP addresses.
|
|
// Traffic is allowed from ip1 to ip2 and vice-versa
|
|
func (c *Chain) Link(action Action, ip1, ip2 net.IP, port int, proto string) error {
|
|
if output, err := Raw("-t", string(Filter), string(action), c.Name,
|
|
"-i", c.Bridge, "-o", c.Bridge,
|
|
"-p", proto,
|
|
"-s", ip1.String(),
|
|
"-d", ip2.String(),
|
|
"--dport", strconv.Itoa(port),
|
|
"-j", "ACCEPT"); err != nil {
|
|
return err
|
|
} else if len(output) != 0 {
|
|
return fmt.Errorf("Error iptables forward: %s", output)
|
|
}
|
|
if output, err := Raw("-t", string(Filter), string(action), c.Name,
|
|
"-i", c.Bridge, "-o", c.Bridge,
|
|
"-p", proto,
|
|
"-s", ip2.String(),
|
|
"-d", ip1.String(),
|
|
"--sport", strconv.Itoa(port),
|
|
"-j", "ACCEPT"); err != nil {
|
|
return err
|
|
} else if len(output) != 0 {
|
|
return fmt.Errorf("Error iptables forward: %s", output)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Add linking rule to nat/PREROUTING chain.
|
|
func (c *Chain) Prerouting(action Action, args ...string) error {
|
|
a := []string{"-t", string(Nat), string(action), "PREROUTING"}
|
|
if len(args) > 0 {
|
|
a = append(a, args...)
|
|
}
|
|
if output, err := Raw(append(a, "-j", c.Name)...); err != nil {
|
|
return err
|
|
} else if len(output) != 0 {
|
|
return ChainError{Chain: "PREROUTING", Output: output}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Add linking rule to an OUTPUT chain
|
|
func (c *Chain) Output(action Action, args ...string) error {
|
|
a := []string{"-t", string(c.Table), string(action), "OUTPUT"}
|
|
if len(args) > 0 {
|
|
a = append(a, args...)
|
|
}
|
|
if output, err := Raw(append(a, "-j", c.Name)...); err != nil {
|
|
return err
|
|
} else if len(output) != 0 {
|
|
return ChainError{Chain: "OUTPUT", Output: output}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Chain) Remove() error {
|
|
// Ignore errors - This could mean the chains were never set up
|
|
if c.Table == Nat {
|
|
c.Prerouting(Delete, "-m", "addrtype", "--dst-type", "LOCAL")
|
|
c.Output(Delete, "-m", "addrtype", "--dst-type", "LOCAL", "!", "--dst", "127.0.0.0/8")
|
|
c.Output(Delete, "-m", "addrtype", "--dst-type", "LOCAL") // Created in versions <= 0.1.6
|
|
|
|
c.Prerouting(Delete)
|
|
c.Output(Delete)
|
|
}
|
|
Raw("-t", string(c.Table), "-F", c.Name)
|
|
Raw("-t", string(c.Table), "-X", c.Name)
|
|
return nil
|
|
}
|
|
|
|
// Check if a rule exists
|
|
func Exists(table Table, chain string, rule ...string) bool {
|
|
if string(table) == "" {
|
|
table = Filter
|
|
}
|
|
|
|
// iptables -C, --check option was added in v.1.4.11
|
|
// http://ftp.netfilter.org/pub/iptables/changes-iptables-1.4.11.txt
|
|
|
|
// try -C
|
|
// if exit status is 0 then return true, the rule exists
|
|
if _, err := Raw(append([]string{
|
|
"-t", string(table), "-C", chain}, rule...)...); err == nil {
|
|
return true
|
|
}
|
|
|
|
// parse "iptables -S" for the rule (this checks rules in a specific chain
|
|
// in a specific table)
|
|
ruleString := strings.Join(rule, " ")
|
|
existingRules, _ := exec.Command("iptables", "-t", string(table), "-S", chain).Output()
|
|
|
|
// regex to replace ips in rule
|
|
// because MASQUERADE rule will not be exactly what was passed
|
|
re := regexp.MustCompile(`[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\/[0-9]{1,2}`)
|
|
|
|
return strings.Contains(
|
|
re.ReplaceAllString(string(existingRules), "?"),
|
|
re.ReplaceAllString(ruleString, "?"),
|
|
)
|
|
}
|
|
|
|
// Call 'iptables' system command, passing supplied arguments
|
|
func Raw(args ...string) ([]byte, error) {
|
|
if firewalldRunning {
|
|
output, err := Passthrough(Iptables, args...)
|
|
if err == nil || !strings.Contains(err.Error(), "was not provided by any .service files") {
|
|
return output, err
|
|
}
|
|
|
|
}
|
|
|
|
if err := initCheck(); err != nil {
|
|
return nil, err
|
|
}
|
|
if supportsXlock {
|
|
args = append([]string{"--wait"}, args...)
|
|
} else {
|
|
bestEffortLock.Lock()
|
|
defer bestEffortLock.Unlock()
|
|
}
|
|
|
|
logrus.Debugf("%s, %v", iptablesPath, args)
|
|
|
|
output, err := exec.Command(iptablesPath, args...).CombinedOutput()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("iptables failed: iptables %v: %s (%s)", strings.Join(args, " "), output, err)
|
|
}
|
|
|
|
// ignore iptables' message about xtables lock
|
|
if strings.Contains(string(output), "waiting for it to exit") {
|
|
output = []byte("")
|
|
}
|
|
|
|
return output, err
|
|
}
|