From 77c1ebd14fe7e1f3d90e9e4c37ab39ef2eaa0abd Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Wed, 23 Aug 2017 15:32:22 -0700 Subject: [PATCH] Handle interfaces that only have link-local addrs This PR changes the fingerprint handling of network interfaces that only contain link local addresses. The new behavior is to prefer globally routable addresses and if none are detected, to fall back to link local addresses if the operator hasn't disallowed it. This gives us pre 0.6 behavior for interfaces with only link local addresses but 0.6+ behavior for IPv6 interfaces that will always have a link-local address. Fixes https://github.com/hashicorp/nomad/issues/3005 /cc diptanuc --- client/fingerprint/network.go | 35 ++++- client/fingerprint/network_test.go | 145 ++++++++++++++++++ .../docs/agent/configuration/client.html.md | 15 +- 3 files changed, 188 insertions(+), 7 deletions(-) diff --git a/client/fingerprint/network.go b/client/fingerprint/network.go index bfc59c7e830..beeca77b9f6 100644 --- a/client/fingerprint/network.go +++ b/client/fingerprint/network.go @@ -13,6 +13,12 @@ const ( // defaultNetworkSpeed is the speed set if the network link speed could not // be detected. defaultNetworkSpeed = 1000 + + // networkDisallowLinkLocalOption/Default are used to allow the operator to + // decide how the fingerprinter handles an interface that only contains link + // local addresses. + networkDisallowLinkLocalOption = "fingerprint.network.disallow_link_local" + networkDisallowLinkLocalDefault = false ) // NetworkFingerprint is used to fingerprint the Network capabilities of a node @@ -84,7 +90,8 @@ func (f *NetworkFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) } // Create the network resources from the interface - nwResources, err := f.createNetworkResources(mbits, intf) + disallowLinkLocal := cfg.ReadBoolDefault(networkDisallowLinkLocalOption, networkDisallowLinkLocalDefault) + nwResources, err := f.createNetworkResources(mbits, intf, disallowLinkLocal) if err != nil { return false, err } @@ -105,13 +112,16 @@ func (f *NetworkFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) } // createNetworkResources creates network resources for every IP -func (f *NetworkFingerprint) createNetworkResources(throughput int, intf *net.Interface) ([]*structs.NetworkResource, error) { +func (f *NetworkFingerprint) createNetworkResources(throughput int, intf *net.Interface, disallowLinkLocal bool) ([]*structs.NetworkResource, error) { // Find the interface with the name addrs, err := f.interfaceDetector.Addrs(intf) if err != nil { return nil, err } + nwResources := make([]*structs.NetworkResource, 0) + linkLocals := make([]*structs.NetworkResource, 0) + for _, addr := range addrs { // Create a new network resource newNetwork := &structs.NetworkResource{ @@ -128,10 +138,6 @@ func (f *NetworkFingerprint) createNetworkResources(throughput int, intf *net.In ip = v.IP } - // If the ip is link-local then we ignore it - if ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() { - continue - } newNetwork.IP = ip.String() if ip.To4() != nil { newNetwork.CIDR = newNetwork.IP + "/32" @@ -139,8 +145,25 @@ func (f *NetworkFingerprint) createNetworkResources(throughput int, intf *net.In newNetwork.CIDR = newNetwork.IP + "/128" } + // If the ip is link-local then we ignore it unless the user allows it + // and we detect nothing else + if ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() { + linkLocals = append(linkLocals, newNetwork) + continue + } + nwResources = append(nwResources, newNetwork) } + + if len(nwResources) == 0 && len(linkLocals) != 0 { + if disallowLinkLocal { + f.logger.Printf("[DEBUG] fingerprint.network: ignoring detected link-local address on interface %v", intf.Name) + return nwResources, nil + } + + return linkLocals, nil + } + return nwResources, nil } diff --git a/client/fingerprint/network_test.go b/client/fingerprint/network_test.go index 62124a0f5ec..cb17d9e7eb6 100644 --- a/client/fingerprint/network_test.go +++ b/client/fingerprint/network_test.go @@ -46,6 +46,24 @@ var ( HardwareAddr: []byte{23, 44, 54, 70}, Flags: net.FlagUp | net.FlagBroadcast | net.FlagMulticast, } + + // One link local address + eth3 = net.Interface{ + Index: 4, + MTU: 1500, + Name: "eth3", + HardwareAddr: []byte{23, 44, 54, 71}, + Flags: net.FlagUp | net.FlagBroadcast | net.FlagMulticast, + } + + // One link local address and one globally routable address + eth4 = net.Interface{ + Index: 4, + MTU: 1500, + Name: "eth4", + HardwareAddr: []byte{23, 44, 54, 72}, + Flags: net.FlagUp | net.FlagBroadcast | net.FlagMulticast, + } ) // A fake network detector which returns no devices @@ -109,6 +127,10 @@ func (n *NetworkInterfaceDetectorMultipleInterfaces) InterfaceByName(name string intf = ð1 case "eth2": intf = ð2 + case "eth3": + intf = ð3 + case "eth4": + intf = ð4 } if intf != nil { return intf, nil @@ -140,6 +162,18 @@ func (n *NetworkInterfaceDetectorMultipleInterfaces) Addrs(intf *net.Interface) if intf.Name == "eth2" { return []net.Addr{}, nil } + + if intf.Name == "eth3" { + _, ipnet1, _ := net.ParseCIDR("169.254.155.20/32") + return []net.Addr{ipnet1}, nil + } + + if intf.Name == "eth4" { + _, ipnet1, _ := net.ParseCIDR("169.254.155.20/32") + _, ipnet2, _ := net.ParseCIDR("100.64.0.10/10") + return []net.Addr{ipnet1, ipnet2}, nil + } + return nil, fmt.Errorf("Can't find addresses for device: %v", intf.Name) } @@ -321,3 +355,114 @@ func TestNetworkFingerPrint_excludelo_down_interfaces(t *testing.T) { t.Fatalf("bad number of IPs %v", len(node.Resources.Networks)) } } + +func TestNetworkFingerPrint_LinkLocal_Allowed(t *testing.T) { + f := &NetworkFingerprint{logger: testLogger(), interfaceDetector: &NetworkInterfaceDetectorMultipleInterfaces{}} + node := &structs.Node{ + Attributes: make(map[string]string), + } + cfg := &config.Config{NetworkSpeed: 100, NetworkInterface: "eth3"} + + ok, err := f.Fingerprint(cfg, node) + if err != nil { + t.Fatalf("err: %v", err) + } + if !ok { + t.Fatalf("should apply") + } + + assertNodeAttributeContains(t, node, "unique.network.ip-address") + + ip := node.Attributes["unique.network.ip-address"] + match := net.ParseIP(ip) + if match == nil { + t.Fatalf("Bad IP match: %s", ip) + } + + if node.Resources == nil || len(node.Resources.Networks) == 0 { + t.Fatal("Expected to find Network Resources") + } + + // Test at least the first Network Resource + net := node.Resources.Networks[0] + if net.IP == "" { + t.Fatal("Expected Network Resource to not be empty") + } + if net.CIDR == "" { + t.Fatal("Expected Network Resource to have a CIDR") + } + if net.Device == "" { + t.Fatal("Expected Network Resource to have a Device Name") + } + if net.MBits == 0 { + t.Fatal("Expected Network Resource to have a non-zero bandwidth") + } +} + +func TestNetworkFingerPrint_LinkLocal_Allowed_MixedIntf(t *testing.T) { + f := &NetworkFingerprint{logger: testLogger(), interfaceDetector: &NetworkInterfaceDetectorMultipleInterfaces{}} + node := &structs.Node{ + Attributes: make(map[string]string), + } + cfg := &config.Config{NetworkSpeed: 100, NetworkInterface: "eth4"} + + ok, err := f.Fingerprint(cfg, node) + if err != nil { + t.Fatalf("err: %v", err) + } + if !ok { + t.Fatalf("should apply") + } + + assertNodeAttributeContains(t, node, "unique.network.ip-address") + + ip := node.Attributes["unique.network.ip-address"] + match := net.ParseIP(ip) + if match == nil { + t.Fatalf("Bad IP match: %s", ip) + } + + if node.Resources == nil || len(node.Resources.Networks) == 0 { + t.Fatal("Expected to find Network Resources") + } + + // Test at least the first Network Resource + net := node.Resources.Networks[0] + if net.IP == "" { + t.Fatal("Expected Network Resource to not be empty") + } + if net.IP == "169.254.155.20" { + t.Fatalf("expected non-link local address; got %v", net.IP) + } + if net.CIDR == "" { + t.Fatal("Expected Network Resource to have a CIDR") + } + if net.Device == "" { + t.Fatal("Expected Network Resource to have a Device Name") + } + if net.MBits == 0 { + t.Fatal("Expected Network Resource to have a non-zero bandwidth") + } +} + +func TestNetworkFingerPrint_LinkLocal_Disallowed(t *testing.T) { + f := &NetworkFingerprint{logger: testLogger(), interfaceDetector: &NetworkInterfaceDetectorMultipleInterfaces{}} + node := &structs.Node{ + Attributes: make(map[string]string), + } + cfg := &config.Config{ + NetworkSpeed: 100, + NetworkInterface: "eth3", + Options: map[string]string{ + networkDisallowLinkLocalOption: "true", + }, + } + + ok, err := f.Fingerprint(cfg, node) + if err != nil { + t.Fatalf("err: %v", err) + } + if !ok { + t.Fatalf("should not apply") + } +} diff --git a/website/source/docs/agent/configuration/client.html.md b/website/source/docs/agent/configuration/client.html.md index d06c05b81c2..feda9a1002c 100644 --- a/website/source/docs/agent/configuration/client.html.md +++ b/website/source/docs/agent/configuration/client.html.md @@ -92,7 +92,7 @@ client { "client", like `"/opt/nomad/client"`. This must be an absolute path. - `gc_interval` `(string: "1m")` - Specifies the interval at which Nomad - attempts to garbage collect terminal allocation directories. + attempts to garbage collect terminal allocation directories. - `gc_disk_usage_threshold` `(float: 80)` - Specifies the disk usage percent which Nomad tries to maintain by garbage collecting terminal allocations. @@ -268,6 +268,19 @@ see the [drivers documentation](/docs/drivers/index.html). } ``` +- `"fingerprint.network.disallow_link_local"` `(string: "false")` - Specifies + whether the network fingerprinter should ignore link-local addresses in the + case that no globally routable address is found. The fingerprinter will always + prefer globally routable addresses. + + ```hcl + client { + options = { + "fingerprint.network.disallow_link_local" = "true" + } + } + ``` + ### `reserved` Parameters - `cpu` `(int: 0)` - Specifies the amount of CPU to reserve, in MHz.