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