istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tools/istio-iptables/pkg/dependencies/implementation.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package dependencies
    16  
    17  import (
    18  	"fmt"
    19  	"io"
    20  	"os/exec"
    21  	"regexp"
    22  	"strings"
    23  
    24  	utilversion "k8s.io/apimachinery/pkg/util/version"
    25  
    26  	"istio.io/istio/pkg/log"
    27  	"istio.io/istio/tools/istio-iptables/pkg/constants"
    28  )
    29  
    30  // XTablesExittype is the exit type of xtables commands.
    31  type XTablesExittype int
    32  
    33  // Learn from `xtables_exittype` of iptables.
    34  // `XTF_ONLY_ONCE`, `XTF_NO_INVERT`, `XTF_BAD_VALUE`, `XTF_ONE_ACTION` will eventually turned out to be a
    35  // parameter problem with explicit error message. Thus, we do not need to support them here.
    36  const (
    37  	// XTablesOtherProblem indicates a problem of other type in xtables
    38  	XTablesOtherProblem XTablesExittype = iota + 1
    39  	// XTablesParameterProblem indicates a parameter problem in xtables
    40  	XTablesParameterProblem
    41  	// XTablesVersionProblem indicates a version problem in xtables
    42  	XTablesVersionProblem
    43  	// XTablesResourceProblem indicates a resource problem in xtables
    44  	XTablesResourceProblem
    45  )
    46  
    47  var exittypeToString = map[XTablesExittype]string{
    48  	XTablesOtherProblem:     "xtables other problem",
    49  	XTablesParameterProblem: "xtables parameter problem",
    50  	XTablesVersionProblem:   "xtables version problem",
    51  	XTablesResourceProblem:  "xtables resource problem",
    52  }
    53  
    54  // RealDependencies implementation of interface Dependencies, which is used in production
    55  type RealDependencies struct {
    56  	NetworkNamespace string
    57  	CNIMode          bool
    58  }
    59  
    60  const iptablesVersionPattern = `v([0-9]+(\.[0-9]+)+)`
    61  
    62  type IptablesVersion struct {
    63  	DetectedBinary        string
    64  	DetectedSaveBinary    string
    65  	DetectedRestoreBinary string
    66  	// the actual version
    67  	Version *utilversion.Version
    68  	// true if legacy mode, false if nf_tables
    69  	Legacy bool
    70  	// true if we detected that existing rules are present for this variant (legacy, nft, v6)
    71  	ExistingRules bool
    72  }
    73  
    74  func (v IptablesVersion) CmdToString(cmd constants.IptablesCmd) string {
    75  	switch cmd {
    76  	case constants.IPTables:
    77  		return v.DetectedBinary
    78  	case constants.IPTablesSave:
    79  		return v.DetectedSaveBinary
    80  	case constants.IPTablesRestore:
    81  		return v.DetectedRestoreBinary
    82  	default:
    83  		return ""
    84  	}
    85  }
    86  
    87  // IsWriteCmd returns true for all command types that do write actions (and thus need a lock)
    88  func (v IptablesVersion) IsWriteCmd(cmd constants.IptablesCmd) bool {
    89  	switch cmd {
    90  	case constants.IPTables:
    91  		return true
    92  	case constants.IPTablesRestore:
    93  		return true
    94  	default:
    95  		return false
    96  	}
    97  }
    98  
    99  // Constants for iptables commands
   100  // These should not be used directly/assumed to be present, but should be contextually detected
   101  const (
   102  	iptablesBin         = "iptables"
   103  	iptablesNftBin      = "iptables-nft"
   104  	iptablesLegacyBin   = "iptables-legacy"
   105  	ip6tablesBin        = "ip6tables"
   106  	ip6tablesNftBin     = "ip6tables-nft"
   107  	ip6tablesLegacyBin  = "ip6tables-legacy"
   108  	iptablesRestoreBin  = "iptables-restore"
   109  	ip6tablesRestoreBin = "ip6tables-restore"
   110  )
   111  
   112  // It is not sufficient to check for the presence of one binary or the other in $PATH -
   113  // we must choose a binary that is
   114  // 1. Available in our $PATH
   115  // 2. Matches where rules are actually defined in the netns we're operating in
   116  // (legacy or nft, with a preference for the latter if both present)
   117  //
   118  // This is designed to handle situations where, for instance, the host has nft-defined rules, and our default container
   119  // binary is `legacy`, or vice-versa - we must match the binaries we have in our $PATH to what rules are actually defined
   120  // in our current netns context.
   121  //
   122  // Q: Why not simply "use the host default binary" at $PATH/iptables?
   123  // A: Because we are running in our own container and do not have access to the host default binary.
   124  // We are using our local binaries to update host rules, and we must pick the right match.
   125  //
   126  // Basic selection logic is as follows:
   127  // 1. see if we have `nft` binary set in our $PATH
   128  // 2. see if we have existing rules in `nft` in our netns
   129  // 3. If so, use `nft` binary set
   130  // 4. Otherwise, see if we have `legacy` binary set, and use that.
   131  // 5. Otherwise, see if we have `iptables` binary set, and use that (detecting whether it's nft or legacy).
   132  func (r *RealDependencies) DetectIptablesVersion(ipV6 bool) (IptablesVersion, error) {
   133  	// Begin detecting
   134  	//
   135  	// iptables variants all have ipv6 variants, so decide which set we're looking for
   136  	var nftBin, legacyBin, plainBin string
   137  	if ipV6 {
   138  		nftBin = ip6tablesNftBin
   139  		legacyBin = ip6tablesLegacyBin
   140  		plainBin = ip6tablesBin
   141  	} else {
   142  		nftBin = iptablesNftBin
   143  		legacyBin = iptablesLegacyBin
   144  		plainBin = iptablesBin
   145  	}
   146  
   147  	// 1. What binaries we have
   148  	// 2. What binary we should use, based on existing rules defined in our current context.
   149  	// does the nft binary set exist, and are nft rules present?
   150  	nftVer, err := shouldUseBinaryForCurrentContext(nftBin)
   151  	if err == nil && nftVer.ExistingRules {
   152  		// if so, immediately use it.
   153  		return nftVer, nil
   154  	}
   155  	// not critical, may find another.
   156  	log.Debugf("did not find (or cannot use) iptables binary, error was %w: %+v", err, nftVer)
   157  
   158  	// Check again
   159  	// does the legacy binary set exist, and are legacy rules present?
   160  	legVer, err := shouldUseBinaryForCurrentContext(legacyBin)
   161  	if err == nil && legVer.ExistingRules {
   162  		// if so, immediately use it
   163  		return legVer, nil
   164  	}
   165  	// not critical, may find another.
   166  	log.Debugf("did not find (or cannot use) iptables binary, error was %w: %+v", err, legVer)
   167  
   168  	// regular non-suffixed binary set is our last resort.
   169  	//
   170  	// If it's there, and rules do not already exist for a specific variant,
   171  	// we should use the default non-suffixed binary.
   172  	// If it's NOT there, just propagate the error, we can't do anything, no iptables here
   173  	return shouldUseBinaryForCurrentContext(plainBin)
   174  }
   175  
   176  // TODO BML verify this won't choke on "-save" binaries having slightly diff. version string prefixes
   177  func parseIptablesVer(rawVer string) (*utilversion.Version, error) {
   178  	versionMatcher := regexp.MustCompile(iptablesVersionPattern)
   179  	match := versionMatcher.FindStringSubmatch(rawVer)
   180  	if match == nil {
   181  		return nil, fmt.Errorf("no iptables version found for: %q", rawVer)
   182  	}
   183  	version, err := utilversion.ParseGeneric(match[1])
   184  	if err != nil {
   185  		return nil, fmt.Errorf("iptables version %q is not a valid version string: %v", match[1], err)
   186  	}
   187  	return version, nil
   188  }
   189  
   190  // transformToXTablesErrorMessage returns an updated error message with explicit xtables error hints, if applicable.
   191  func transformToXTablesErrorMessage(stderr string, err error) string {
   192  	ee, ok := err.(*exec.ExitError)
   193  	if !ok {
   194  		// Not common, but can happen if file not found error, etc
   195  		return err.Error()
   196  	}
   197  	exitcode := ee.ExitCode()
   198  	if errtypeStr, ok := exittypeToString[XTablesExittype(exitcode)]; ok {
   199  		// The original stderr is something like:
   200  		// `prog_name + prog_vers: error hints`
   201  		// `(optional) try help information`.
   202  		// e.g.,
   203  		// `iptables 1.8.4 (legacy): Couldn't load target 'ISTIO_OUTPUT':No such file or directory`
   204  		// `Try 'iptables -h' or 'iptables --help' for more information.`
   205  		// Reusing the `error hints` and optional `try help information` parts of the original stderr to form
   206  		// an error message with explicit xtables error information.
   207  		errStrParts := strings.SplitN(stderr, ":", 2)
   208  		errStr := stderr
   209  		if len(errStrParts) > 1 {
   210  			errStr = errStrParts[1]
   211  		}
   212  		return fmt.Sprintf("%v: %v", errtypeStr, strings.TrimSpace(errStr))
   213  	}
   214  
   215  	return stderr
   216  }
   217  
   218  // Run runs a command
   219  func (r *RealDependencies) Run(cmd constants.IptablesCmd, iptVer *IptablesVersion, stdin io.ReadSeeker, args ...string) error {
   220  	return r.executeXTables(cmd, iptVer, false, stdin, args...)
   221  }
   222  
   223  // RunQuietlyAndIgnore runs a command quietly and ignores errors
   224  func (r *RealDependencies) RunQuietlyAndIgnore(cmd constants.IptablesCmd, iptVer *IptablesVersion, stdin io.ReadSeeker, args ...string) {
   225  	_ = r.executeXTables(cmd, iptVer, true, stdin, args...)
   226  }