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 `