github.com/kaisenlinux/docker.io@v0.0.0-20230510090727-ea55db55fac7/libnetwork/iptables/iptables.go (about)

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