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 `