github.com/manicqin/nomad@v0.9.5/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  // // ensureForwardingRules ensures that a forwarding rule is added to iptables
    90  // to allow traffic inbound to the bridge network
    91  func (b *bridgeNetworkConfigurator) ensureForwardingRules() error {
    92  	ipt, err := iptables.New()
    93  	if err != nil {
    94  		return err
    95  	}
    96  
    97  	if err = ensureChain(ipt, "filter", cniAdminChainName); err != nil {
    98  		return err
    99  	}
   100  
   101  	if err := ensureFirstChainRule(ipt, cniAdminChainName, b.generateAdminChainRule()); err != nil {
   102  		return err
   103  	}
   104  
   105  	return nil
   106  }
   107  
   108  // ensureChain ensures that the given chain exists, creating it if missing
   109  func ensureChain(ipt *iptables.IPTables, table, chain string) error {
   110  	chains, err := ipt.ListChains(table)
   111  	if err != nil {
   112  		return fmt.Errorf("failed to list iptables chains: %v", err)
   113  	}
   114  	for _, ch := range chains {
   115  		if ch == chain {
   116  			return nil
   117  		}
   118  	}
   119  
   120  	err = ipt.NewChain(table, chain)
   121  
   122  	// if err is for chain already existing return as it is possible another
   123  	// goroutine created it first
   124  	if e, ok := err.(*iptables.Error); ok && e.ExitStatus() == 1 {
   125  		return nil
   126  	}
   127  
   128  	return err
   129  }
   130  
   131  // ensureFirstChainRule ensures the given rule exists as the first rule in the chain
   132  func ensureFirstChainRule(ipt *iptables.IPTables, chain string, rule []string) error {
   133  	exists, err := ipt.Exists("filter", chain, rule...)
   134  	if !exists && err == nil {
   135  		// iptables rules are 1-indexed
   136  		err = ipt.Insert("filter", chain, 1, rule...)
   137  	}
   138  	return err
   139  }
   140  
   141  // generateAdminChainRule builds the iptables rule that is inserted into the
   142  // CNI admin chain to ensure traffic forwarding to the bridge network
   143  func (b *bridgeNetworkConfigurator) generateAdminChainRule() []string {
   144  	return []string{"-o", b.bridgeName, "-d", b.allocSubnet, "-j", "ACCEPT"}
   145  }
   146  
   147  // Setup calls the CNI plugins with the add action
   148  func (b *bridgeNetworkConfigurator) Setup(ctx context.Context, alloc *structs.Allocation, spec *drivers.NetworkIsolationSpec) error {
   149  	if err := b.ensureForwardingRules(); err != nil {
   150  		return fmt.Errorf("failed to initialize table forwarding rules: %v", err)
   151  	}
   152  
   153  	if err := b.cni.Load(cni.WithConfListBytes(b.buildNomadNetConfig())); err != nil {
   154  		return err
   155  	}
   156  
   157  	// Depending on the version of bridge cni plugin used, a known race could occure
   158  	// where two alloc attempt to create the nomad bridge at the same time, resulting
   159  	// in one of them to fail. This rety attempts to overcome any
   160  	const retry = 3
   161  	for attempt := 1; ; attempt++ {
   162  		//TODO eventually returning the IP from the result would be nice to have in the alloc
   163  		if _, err := b.cni.Setup(ctx, alloc.ID, spec.Path, cni.WithCapabilityPortMap(getPortMapping(alloc))); err != nil {
   164  			b.logger.Warn("failed to configure bridge network", "err", err, "attempt", attempt)
   165  			if attempt == retry {
   166  				return fmt.Errorf("failed to configure bridge network: %v", err)
   167  			}
   168  			// Sleep for 1 second + jitter
   169  			time.Sleep(time.Second + (time.Duration(b.rand.Int63n(1000)) * time.Millisecond))
   170  			continue
   171  		}
   172  		break
   173  	}
   174  
   175  	return nil
   176  
   177  }
   178  
   179  // Teardown calls the CNI plugins with the delete action
   180  func (b *bridgeNetworkConfigurator) Teardown(ctx context.Context, alloc *structs.Allocation, spec *drivers.NetworkIsolationSpec) error {
   181  	return b.cni.Remove(ctx, alloc.ID, spec.Path, cni.WithCapabilityPortMap(getPortMapping(alloc)))
   182  }
   183  
   184  // getPortMapping builds a list of portMapping structs that are used as the
   185  // portmapping capability arguments for the portmap CNI plugin
   186  func getPortMapping(alloc *structs.Allocation) []cni.PortMapping {
   187  	ports := []cni.PortMapping{}
   188  	for _, network := range alloc.AllocatedResources.Shared.Networks {
   189  		for _, port := range append(network.DynamicPorts, network.ReservedPorts...) {
   190  			if port.To < 1 {
   191  				continue
   192  			}
   193  			for _, proto := range []string{"tcp", "udp"} {
   194  				ports = append(ports, cni.PortMapping{
   195  					HostPort:      int32(port.Value),
   196  					ContainerPort: int32(port.To),
   197  					Protocol:      proto,
   198  				})
   199  			}
   200  		}
   201  	}
   202  	return ports
   203  }
   204  
   205  func (b *bridgeNetworkConfigurator) buildNomadNetConfig() []byte {
   206  	return []byte(fmt.Sprintf(nomadCNIConfigTemplate, b.bridgeName, b.allocSubnet, cniAdminChainName))
   207  }
   208  
   209  const nomadCNIConfigTemplate = `{
   210  	"cniVersion": "0.4.0",
   211  	"name": "nomad",
   212  	"plugins": [
   213  		{
   214  			"type": "bridge",
   215  			"bridge": "%s",
   216  			"ipMasq": true,
   217  			"isGateway": true,
   218  			"forceAddress": true,
   219  			"ipam": {
   220  				"type": "host-local",
   221  				"ranges": [
   222  					[
   223  						{
   224  							"subnet": "%s"
   225  						}
   226  					]
   227  				],
   228  				"routes": [
   229  					{ "dst": "0.0.0.0/0" }
   230  				]
   231  			}
   232  		},
   233  		{
   234  			"type": "firewall",
   235  			"backend": "iptables",
   236  			"iptablesAdminChainName": "%s"
   237  		},
   238  		{
   239  			"type": "portmap",
   240  			"capabilities": {"portMappings": true},
   241  			"snat": true
   242  		}
   243  	]
   244  }
   245  `