github.com/adityamillind98/moby@v23.0.0-rc.4+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  		establish := []string{
   250  			"-o", bridgeName,
   251  			"-m", "conntrack",
   252  			"--ctstate", "RELATED,ESTABLISHED",
   253  			"-j", "ACCEPT"}
   254  		if !iptable.Exists(Filter, "FORWARD", establish...) && enable {
   255  			insert := append([]string{string(Insert), "FORWARD"}, establish...)
   256  			if output, err := iptable.Raw(insert...); err != nil {
   257  				return err
   258  			} else if len(output) != 0 {
   259  				return fmt.Errorf("Could not create establish rule to %s: %s", c.Table, output)
   260  			}
   261  		} else if iptable.Exists(Filter, "FORWARD", establish...) && !enable {
   262  			del := append([]string{string(Delete), "FORWARD"}, establish...)
   263  			if output, err := iptable.Raw(del...); err != nil {
   264  				return err
   265  			} else if len(output) != 0 {
   266  				return fmt.Errorf("Could not delete establish rule from %s: %s", c.Table, output)
   267  			}
   268  		}
   269  	}
   270  	return nil
   271  }
   272  
   273  // RemoveExistingChain removes existing chain from the table.
   274  func (iptable IPTable) RemoveExistingChain(name string, table Table) error {
   275  	c := &ChainInfo{
   276  		Name:    name,
   277  		Table:   table,
   278  		IPTable: iptable,
   279  	}
   280  	if string(c.Table) == "" {
   281  		c.Table = Filter
   282  	}
   283  	return c.Remove()
   284  }
   285  
   286  // Forward adds forwarding rule to 'filter' table and corresponding nat rule to 'nat' table.
   287  func (c *ChainInfo) Forward(action Action, ip net.IP, port int, proto, destAddr string, destPort int, bridgeName string) error {
   288  	iptable := GetIptable(c.IPTable.Version)
   289  	daddr := ip.String()
   290  	if ip.IsUnspecified() {
   291  		// iptables interprets "0.0.0.0" as "0.0.0.0/32", whereas we
   292  		// want "0.0.0.0/0". "0/0" is correctly interpreted as "any
   293  		// value" by both iptables and ip6tables.
   294  		daddr = "0/0"
   295  	}
   296  
   297  	args := []string{
   298  		"-p", proto,
   299  		"-d", daddr,
   300  		"--dport", strconv.Itoa(port),
   301  		"-j", "DNAT",
   302  		"--to-destination", net.JoinHostPort(destAddr, strconv.Itoa(destPort))}
   303  
   304  	if !c.HairpinMode {
   305  		args = append(args, "!", "-i", bridgeName)
   306  	}
   307  	if err := iptable.ProgramRule(Nat, c.Name, action, args); err != nil {
   308  		return err
   309  	}
   310  
   311  	args = []string{
   312  		"!", "-i", bridgeName,
   313  		"-o", bridgeName,
   314  		"-p", proto,
   315  		"-d", destAddr,
   316  		"--dport", strconv.Itoa(destPort),
   317  		"-j", "ACCEPT",
   318  	}
   319  	if err := iptable.ProgramRule(Filter, c.Name, action, args); err != nil {
   320  		return err
   321  	}
   322  
   323  	args = []string{
   324  		"-p", proto,
   325  		"-s", destAddr,
   326  		"-d", destAddr,
   327  		"--dport", strconv.Itoa(destPort),
   328  		"-j", "MASQUERADE",
   329  	}
   330  
   331  	if err := iptable.ProgramRule(Nat, "POSTROUTING", action, args); err != nil {
   332  		return err
   333  	}
   334  
   335  	if proto == "sctp" {
   336  		// Linux kernel v4.9 and below enables NETIF_F_SCTP_CRC for veth by
   337  		// the following commit.
   338  		// This introduces a problem when conbined with a physical NIC without
   339  		// NETIF_F_SCTP_CRC. As for a workaround, here we add an iptables entry
   340  		// to fill the checksum.
   341  		//
   342  		// https://github.com/torvalds/linux/commit/c80fafbbb59ef9924962f83aac85531039395b18
   343  		args = []string{
   344  			"-p", proto,
   345  			"--sport", strconv.Itoa(destPort),
   346  			"-j", "CHECKSUM",
   347  			"--checksum-fill",
   348  		}
   349  		if err := iptable.ProgramRule(Mangle, "POSTROUTING", action, args); err != nil {
   350  			return err
   351  		}
   352  	}
   353  
   354  	return nil
   355  }
   356  
   357  // Link adds reciprocal ACCEPT rule for two supplied IP addresses.
   358  // Traffic is allowed from ip1 to ip2 and vice-versa
   359  func (c *ChainInfo) Link(action Action, ip1, ip2 net.IP, port int, proto string, bridgeName string) error {
   360  	iptable := GetIptable(c.IPTable.Version)
   361  	// forward
   362  	args := []string{
   363  		"-i", bridgeName, "-o", bridgeName,
   364  		"-p", proto,
   365  		"-s", ip1.String(),
   366  		"-d", ip2.String(),
   367  		"--dport", strconv.Itoa(port),
   368  		"-j", "ACCEPT",
   369  	}
   370  
   371  	if err := iptable.ProgramRule(Filter, c.Name, action, args); err != nil {
   372  		return err
   373  	}
   374  	// reverse
   375  	args[7], args[9] = args[9], args[7]
   376  	args[10] = "--sport"
   377  	return iptable.ProgramRule(Filter, c.Name, action, args)
   378  }
   379  
   380  // ProgramRule adds the rule specified by args only if the
   381  // rule is not already present in the chain. Reciprocally,
   382  // it removes the rule only if present.
   383  func (iptable IPTable) ProgramRule(table Table, chain string, action Action, args []string) error {
   384  	if iptable.Exists(table, chain, args...) != (action == Delete) {
   385  		return nil
   386  	}
   387  	return iptable.RawCombinedOutput(append([]string{"-t", string(table), string(action), chain}, args...)...)
   388  }
   389  
   390  // Prerouting adds linking rule to nat/PREROUTING chain.
   391  func (c *ChainInfo) Prerouting(action Action, args ...string) error {
   392  	iptable := GetIptable(c.IPTable.Version)
   393  	a := []string{"-t", string(Nat), string(action), "PREROUTING"}
   394  	if len(args) > 0 {
   395  		a = append(a, args...)
   396  	}
   397  	if output, err := iptable.Raw(a...); err != nil {
   398  		return err
   399  	} else if len(output) != 0 {
   400  		return ChainError{Chain: "PREROUTING", Output: output}
   401  	}
   402  	return nil
   403  }
   404  
   405  // Output adds linking rule to an OUTPUT chain.
   406  func (c *ChainInfo) Output(action Action, args ...string) error {
   407  	iptable := GetIptable(c.IPTable.Version)
   408  	a := []string{"-t", string(c.Table), string(action), "OUTPUT"}
   409  	if len(args) > 0 {
   410  		a = append(a, args...)
   411  	}
   412  	if output, err := iptable.Raw(a...); err != nil {
   413  		return err
   414  	} else if len(output) != 0 {
   415  		return ChainError{Chain: "OUTPUT", Output: output}
   416  	}
   417  	return nil
   418  }
   419  
   420  // Remove removes the chain.
   421  func (c *ChainInfo) Remove() error {
   422  	iptable := GetIptable(c.IPTable.Version)
   423  	// Ignore errors - This could mean the chains were never set up
   424  	if c.Table == Nat {
   425  		c.Prerouting(Delete, "-m", "addrtype", "--dst-type", "LOCAL", "-j", c.Name)
   426  		c.Output(Delete, "-m", "addrtype", "--dst-type", "LOCAL", "!", "--dst", iptable.LoopbackByVersion(), "-j", c.Name)
   427  		c.Output(Delete, "-m", "addrtype", "--dst-type", "LOCAL", "-j", c.Name) // Created in versions <= 0.1.6
   428  
   429  		c.Prerouting(Delete)
   430  		c.Output(Delete)
   431  	}
   432  	iptable.Raw("-t", string(c.Table), "-F", c.Name)
   433  	iptable.Raw("-t", string(c.Table), "-X", c.Name)
   434  	return nil
   435  }
   436  
   437  // Exists checks if a rule exists
   438  func (iptable IPTable) Exists(table Table, chain string, rule ...string) bool {
   439  	return iptable.exists(false, table, chain, rule...)
   440  }
   441  
   442  // ExistsNative behaves as Exists with the difference it
   443  // will always invoke `iptables` binary.
   444  func (iptable IPTable) ExistsNative(table Table, chain string, rule ...string) bool {
   445  	return iptable.exists(true, table, chain, rule...)
   446  }
   447  
   448  func (iptable IPTable) exists(native bool, table Table, chain string, rule ...string) bool {
   449  	f := iptable.Raw
   450  	if native {
   451  		f = iptable.raw
   452  	}
   453  
   454  	if string(table) == "" {
   455  		table = Filter
   456  	}
   457  
   458  	if err := initCheck(); err != nil {
   459  		// The exists() signature does not allow us to return an error, but at least
   460  		// we can skip the (likely invalid) exec invocation.
   461  		return false
   462  	}
   463  
   464  	// if exit status is 0 then return true, the rule exists
   465  	_, err := f(append([]string{"-t", string(table), "-C", chain}, rule...)...)
   466  	return err == nil
   467  }
   468  
   469  // Maximum duration that an iptables operation can take
   470  // before flagging a warning.
   471  const opWarnTime = 2 * time.Second
   472  
   473  func filterOutput(start time.Time, output []byte, args ...string) []byte {
   474  	// Flag operations that have taken a long time to complete
   475  	opTime := time.Since(start)
   476  	if opTime > opWarnTime {
   477  		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))
   478  	}
   479  	// ignore iptables' message about xtables lock:
   480  	// it is a warning, not an error.
   481  	if strings.Contains(string(output), xLockWaitMsg) {
   482  		output = []byte("")
   483  	}
   484  	// Put further filters here if desired
   485  	return output
   486  }
   487  
   488  // Raw calls 'iptables' system command, passing supplied arguments.
   489  func (iptable IPTable) Raw(args ...string) ([]byte, error) {
   490  	if firewalldRunning {
   491  		// select correct IP version for firewalld
   492  		ipv := Iptables
   493  		if iptable.Version == IPv6 {
   494  			ipv = IP6Tables
   495  		}
   496  
   497  		startTime := time.Now()
   498  		output, err := Passthrough(ipv, args...)
   499  		if err == nil || !strings.Contains(err.Error(), "was not provided by any .service files") {
   500  			return filterOutput(startTime, output, args...), err
   501  		}
   502  	}
   503  	return iptable.raw(args...)
   504  }
   505  
   506  func (iptable IPTable) raw(args ...string) ([]byte, error) {
   507  	if err := initCheck(); err != nil {
   508  		return nil, err
   509  	}
   510  	if supportsXlock {
   511  		args = append([]string{"--wait"}, args...)
   512  	} else {
   513  		bestEffortLock.Lock()
   514  		defer bestEffortLock.Unlock()
   515  	}
   516  
   517  	path := iptablesPath
   518  	commandName := "iptables"
   519  	if iptable.Version == IPv6 {
   520  		if ip6tablesPath == "" {
   521  			return nil, fmt.Errorf("ip6tables is missing")
   522  		}
   523  		path = ip6tablesPath
   524  		commandName = "ip6tables"
   525  	}
   526  
   527  	logrus.Debugf("%s, %v", path, args)
   528  
   529  	startTime := time.Now()
   530  	output, err := exec.Command(path, args...).CombinedOutput()
   531  	if err != nil {
   532  		return nil, fmt.Errorf("iptables failed: %s %v: %s (%s)", commandName, strings.Join(args, " "), output, err)
   533  	}
   534  
   535  	return filterOutput(startTime, output, args...), err
   536  }
   537  
   538  // RawCombinedOutput internally calls the Raw function and returns a non nil
   539  // error if Raw returned a non nil error or a non empty output
   540  func (iptable IPTable) RawCombinedOutput(args ...string) error {
   541  	if output, err := iptable.Raw(args...); err != nil || len(output) != 0 {
   542  		return fmt.Errorf("%s (%v)", string(output), err)
   543  	}
   544  	return nil
   545  }
   546  
   547  // RawCombinedOutputNative behave as RawCombinedOutput with the difference it
   548  // will always invoke `iptables` binary
   549  func (iptable IPTable) RawCombinedOutputNative(args ...string) error {
   550  	if output, err := iptable.raw(args...); err != nil || len(output) != 0 {
   551  		return fmt.Errorf("%s (%v)", string(output), err)
   552  	}
   553  	return nil
   554  }
   555  
   556  // ExistChain checks if a chain exists
   557  func (iptable IPTable) ExistChain(chain string, table Table) bool {
   558  	if _, err := iptable.Raw("-t", string(table), "-nL", chain); err == nil {
   559  		return true
   560  	}
   561  	return false
   562  }
   563  
   564  // SetDefaultPolicy sets the passed default policy for the table/chain
   565  func (iptable IPTable) SetDefaultPolicy(table Table, chain string, policy Policy) error {
   566  	if err := iptable.RawCombinedOutput("-t", string(table), "-P", chain, string(policy)); err != nil {
   567  		return fmt.Errorf("setting default policy to %v in %v chain failed: %v", policy, chain, err)
   568  	}
   569  	return nil
   570  }
   571  
   572  // AddReturnRule adds a return rule for the chain in the filter table
   573  func (iptable IPTable) AddReturnRule(chain string) error {
   574  	var (
   575  		table = Filter
   576  		args  = []string{"-j", "RETURN"}
   577  	)
   578  
   579  	if iptable.Exists(table, chain, args...) {
   580  		return nil
   581  	}
   582  
   583  	err := iptable.RawCombinedOutput(append([]string{"-A", chain}, args...)...)
   584  	if err != nil {
   585  		return fmt.Errorf("unable to add return rule in %s chain: %s", chain, err.Error())
   586  	}
   587  
   588  	return nil
   589  }
   590  
   591  // EnsureJumpRule ensures the jump rule is on top
   592  func (iptable IPTable) EnsureJumpRule(fromChain, toChain string) error {
   593  	var (
   594  		table = Filter
   595  		args  = []string{"-j", toChain}
   596  	)
   597  
   598  	if iptable.Exists(table, fromChain, args...) {
   599  		err := iptable.RawCombinedOutput(append([]string{"-D", fromChain}, args...)...)
   600  		if err != nil {
   601  			return fmt.Errorf("unable to remove jump to %s rule in %s chain: %s", toChain, fromChain, err.Error())
   602  		}
   603  	}
   604  
   605  	err := iptable.RawCombinedOutput(append([]string{"-I", fromChain}, args...)...)
   606  	if err != nil {
   607  		return fmt.Errorf("unable to insert jump to %s rule in %s chain: %s", toChain, fromChain, err.Error())
   608  	}
   609  
   610  	return nil
   611  }