github.com/iqoqo/nomad@v0.11.3-0.20200911112621-d7021c74d101/client/allocrunner/networking_bridge_linux.go (about)

     1  package allocrunner
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"math/rand"
     7  	"os"
     8  	"path/filepath"
     9  	"time"
    10  
    11  	cni "github.com/containerd/go-cni"
    12  	"github.com/coreos/go-iptables/iptables"
    13  	hclog "github.com/hashicorp/go-hclog"
    14  	"github.com/hashicorp/nomad/nomad/structs"
    15  	"github.com/hashicorp/nomad/plugins/drivers"
    16  )
    17  
    18  const (
    19  	// envCNIPath is the environment variable name to use to derive the CNI path
    20  	// when it is not explicitly set by the client
    21  	envCNIPath = "CNI_PATH"
    22  
    23  	// defaultCNIPath is the CNI path to use when it is not set by the client
    24  	// and is not set by environment variable
    25  	defaultCNIPath = "/opt/cni/bin"
    26  
    27  	// defaultNomadBridgeName is the name of the bridge to use when not set by
    28  	// the client
    29  	defaultNomadBridgeName = "nomad"
    30  
    31  	// bridgeNetworkAllocIfPrefix is the prefix that is used for the interface
    32  	// name created inside of the alloc network which is connected to the bridge
    33  	bridgeNetworkAllocIfPrefix = "eth"
    34  
    35  	// defaultNomadAllocSubnet is the subnet to use for host local ip address
    36  	// allocation when not specified by the client
    37  	defaultNomadAllocSubnet = "172.26.64.0/20" // end 172.26.79.255
    38  
    39  	// cniAdminChainName is the name of the admin iptables chain used to allow
    40  	// forwarding traffic to allocations
    41  	cniAdminChainName = "NOMAD-ADMIN"
    42  )
    43  
    44  // bridgeNetworkConfigurator is a NetworkConfigurator which adds the alloc to a
    45  // shared bridge, configures masquerading for egress traffic and port mapping
    46  // for ingress
    47  type bridgeNetworkConfigurator struct {
    48  	cni         cni.CNI
    49  	allocSubnet string
    50  	bridgeName  string
    51  
    52  	rand   *rand.Rand
    53  	logger hclog.Logger
    54  }
    55  
    56  func newBridgeNetworkConfigurator(log hclog.Logger, bridgeName, ipRange, cniPath string) (*bridgeNetworkConfigurator, error) {
    57  	b := &bridgeNetworkConfigurator{
    58  		bridgeName:  bridgeName,
    59  		allocSubnet: ipRange,
    60  		rand:        rand.New(rand.NewSource(time.Now().Unix())),
    61  		logger:      log,
    62  	}
    63  	if cniPath == "" {
    64  		if cniPath = os.Getenv(envCNIPath); cniPath == "" {
    65  			cniPath = defaultCNIPath
    66  		}
    67  	}
    68  
    69  	c, err := cni.New(cni.WithPluginDir(filepath.SplitList(cniPath)),
    70  		cni.WithInterfacePrefix(bridgeNetworkAllocIfPrefix))
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  	b.cni = c
    75  
    76  	if b.bridgeName == "" {
    77  		b.bridgeName = defaultNomadBridgeName
    78  	}
    79  
    80  	if b.allocSubnet == "" {
    81  		b.allocSubnet = defaultNomadAllocSubnet
    82  	}
    83  
    84  	return b, nil
    85  }
    86  
    87  // ensureForwardingRules ensures that a forwarding rule is added to iptables
    88  // to allow traffic inbound to the bridge network
    89  func (b *bridgeNetworkConfigurator) ensureForwardingRules() error {
    90  	ipt, err := iptables.New()
    91  	if err != nil {
    92  		return err
    93  	}
    94  
    95  	if err = ensureChain(ipt, "filter", cniAdminChainName); err != nil {
    96  		return err
    97  	}
    98  
    99  	if err := ensureFirstChainRule(ipt, cniAdminChainName, b.generateAdminChainRule()); err != nil {
   100  		return err
   101  	}
   102  
   103  	return nil
   104  }
   105  
   106  // ensureChain ensures that the given chain exists, creating it if missing
   107  func ensureChain(ipt *iptables.IPTables, table, chain string) error {
   108  	chains, err := ipt.ListChains(table)
   109  	if err != nil {
   110  		return fmt.Errorf("failed to list iptables chains: %v", err)
   111  	}
   112  	for _, ch := range chains {
   113  		if ch == chain {
   114  			return nil
   115  		}
   116  	}
   117  
   118  	err = ipt.NewChain(table, chain)
   119  
   120  	// if err is for chain already existing return as it is possible another
   121  	// goroutine created it first
   122  	if e, ok := err.(*iptables.Error); ok && e.ExitStatus() == 1 {
   123  		return nil
   124  	}
   125  
   126  	return err
   127  }
   128  
   129  // ensureFirstChainRule ensures the given rule exists as the first rule in the chain
   130  func ensureFirstChainRule(ipt *iptables.IPTables, chain string, rule []string) error {
   131  	exists, err := ipt.Exists("filter", chain, rule...)
   132  	if !exists && err == nil {
   133  		// iptables rules are 1-indexed
   134  		err = ipt.Insert("filter", chain, 1, rule...)
   135  	}
   136  	return err
   137  }
   138  
   139  // generateAdminChainRule builds the iptables rule that is inserted into the
   140  // CNI admin chain to ensure traffic forwarding to the bridge network
   141  func (b *bridgeNetworkConfigurator) generateAdminChainRule() []string {
   142  	return []string{"-o", b.bridgeName, "-d", b.allocSubnet, "-j", "ACCEPT"}
   143  }
   144  
   145  // Setup calls the CNI plugins with the add action
   146  func (b *bridgeNetworkConfigurator) Setup(ctx context.Context, alloc *structs.Allocation, spec *drivers.NetworkIsolationSpec) error {
   147  	if err := b.ensureForwardingRules(); err != nil {
   148  		return fmt.Errorf("failed to initialize table forwarding rules: %v", err)
   149  	}
   150  
   151  	if err := b.ensureCNIInitialized(); err != nil {
   152  		return err
   153  	}
   154  
   155  	// Depending on the version of bridge cni plugin (< 0.8.4) a known race could occur
   156  	// where two alloc attempt to create the nomad bridge at the same time, resulting
   157  	// in one of them to fail. This retry attempts to overcome those erroneous failures.
   158  	const retry = 3
   159  	for attempt := 1; ; attempt++ {
   160  		//TODO eventually returning the IP from the result would be nice to have in the alloc
   161  		if _, err := b.cni.Setup(ctx, alloc.ID, spec.Path, cni.WithCapabilityPortMap(getPortMapping(alloc))); err != nil {
   162  			b.logger.Warn("failed to configure bridge network", "err", err, "attempt", attempt)
   163  			if attempt == retry {
   164  				return fmt.Errorf("failed to configure bridge network: %v", err)
   165  			}
   166  			// Sleep for 1 second + jitter
   167  			time.Sleep(time.Second + (time.Duration(b.rand.Int63n(1000)) * time.Millisecond))
   168  			continue
   169  		}
   170  		break
   171  	}
   172  
   173  	return nil
   174  
   175  }
   176  
   177  // Teardown calls the CNI plugins with the delete action
   178  func (b *bridgeNetworkConfigurator) Teardown(ctx context.Context, alloc *structs.Allocation, spec *drivers.NetworkIsolationSpec) error {
   179  	if err := b.ensureCNIInitialized(); err != nil {
   180  		return err
   181  	}
   182  
   183  	return b.cni.Remove(ctx, alloc.ID, spec.Path, cni.WithCapabilityPortMap(getPortMapping(alloc)))
   184  }
   185  
   186  func (b *bridgeNetworkConfigurator) ensureCNIInitialized() error {
   187  	if err := b.cni.Status(); cni.IsCNINotInitialized(err) {
   188  		return b.cni.Load(cni.WithConfListBytes(b.buildNomadNetConfig()))
   189  	} else {
   190  		return err
   191  	}
   192  }
   193  
   194  // getPortMapping builds a list of portMapping structs that are used as the
   195  // portmapping capability arguments for the portmap CNI plugin
   196  func getPortMapping(alloc *structs.Allocation) []cni.PortMapping {
   197  	ports := []cni.PortMapping{}
   198  	for _, network := range alloc.AllocatedResources.Shared.Networks {
   199  		for _, port := range append(network.DynamicPorts, network.ReservedPorts...) {
   200  			if port.To < 1 {
   201  				continue
   202  			}
   203  			for _, proto := range []string{"tcp", "udp"} {
   204  				ports = append(ports, cni.PortMapping{
   205  					HostPort:      int32(port.Value),
   206  					ContainerPort: int32(port.To),
   207  					Protocol:      proto,
   208  				})
   209  			}
   210  		}
   211  	}
   212  	return ports
   213  }
   214  
   215  func (b *bridgeNetworkConfigurator) buildNomadNetConfig() []byte {
   216  	return []byte(fmt.Sprintf(nomadCNIConfigTemplate, b.bridgeName, b.allocSubnet, cniAdminChainName))
   217  }
   218  
   219  const nomadCNIConfigTemplate = `{
   220  	"cniVersion": "0.4.0",
   221  	"name": "nomad",
   222  	"plugins": [
   223  		{
   224  			"type": "bridge",
   225  			"bridge": "%s",
   226  			"ipMasq": true,
   227  			"isGateway": true,
   228  			"forceAddress": true,
   229  			"ipam": {
   230  				"type": "host-local",
   231  				"ranges": [
   232  					[
   233  						{
   234  							"subnet": "%s"
   235  						}
   236  					]
   237  				],
   238  				"routes": [
   239  					{ "dst": "0.0.0.0/0" }
   240  				]
   241  			}
   242  		},
   243  		{
   244  			"type": "firewall",
   245  			"backend": "iptables",
   246  			"iptablesAdminChainName": "%s"
   247  		},
   248  		{
   249  			"type": "portmap",
   250  			"capabilities": {"portMappings": true},
   251  			"snat": true
   252  		}
   253  	]
   254  }
   255  `