diff --git a/drivers/virtualbox/network.go b/drivers/virtualbox/network.go index f1618001a1..0586b77daa 100644 --- a/drivers/virtualbox/network.go +++ b/drivers/virtualbox/network.go @@ -35,6 +35,27 @@ type hostOnlyNetwork struct { NetworkName string // referenced in DHCP.NetworkName } +// HostInterfaces returns host network interface info. By default delegates to net.Interfaces() +type HostInterfaces interface { + Interfaces() ([]net.Interface, error) + Addrs(iface *net.Interface) ([]net.Addr, error) +} + +func NewHostInterfaces() HostInterfaces { + return &defaultHostInterfaces{} +} + +type defaultHostInterfaces struct { +} + +func (ni *defaultHostInterfaces) Interfaces() ([]net.Interface, error) { + return net.Interfaces() +} + +func (ni *defaultHostInterfaces) Addrs(iface *net.Interface) ([]net.Addr, error) { + return iface.Addrs() +} + // Save changes the configuration of the host-only network. func (n *hostOnlyNetwork) Save(vbox VBoxManager) error { if err := n.SaveIPv4(vbox); err != nil { @@ -158,12 +179,7 @@ func getHostOnlyAdapter(nets map[string]*hostOnlyNetwork, hostIP net.IP, netmask return nil } -func getOrCreateHostOnlyNetwork(hostIP net.IP, netmask net.IPMask, vbox VBoxManager) (*hostOnlyNetwork, error) { - nets, err := listHostOnlyAdapters(vbox) - if err != nil { - return nil, err - } - +func getOrCreateHostOnlyNetwork(hostIP net.IP, netmask net.IPMask, nets map[string]*hostOnlyNetwork, vbox VBoxManager) (*hostOnlyNetwork, error) { // Search for an existing host-only adapter. hostOnlyAdapter := getHostOnlyAdapter(nets, hostIP, netmask) if hostOnlyAdapter != nil { @@ -171,7 +187,7 @@ func getOrCreateHostOnlyNetwork(hostIP net.IP, netmask net.IPMask, vbox VBoxMana } // No existing host-only adapter found. Create a new one. - _, err = createHostonlyAdapter(vbox) + _, err := createHostonlyAdapter(vbox) if err != nil { // Sometimes the host-only adapter fails to create. See https://www.virtualbox.org/ticket/14040 // BUT, it is created in fact! So let's wait until it appears last in the list @@ -333,6 +349,46 @@ func listDHCPServers(vbox VBoxManager) (map[string]*dhcpServer, error) { return m, nil } +// listHostInterfaces returns a map of net.IPNet addresses of host interfaces that are "UP" and not loopback adapters +// and not virtualbox host-only networks (given by excludeNets), keyed by CIDR string. +func listHostInterfaces(hif HostInterfaces, excludeNets map[string]*hostOnlyNetwork) (map[string]*net.IPNet, error) { + ifaces, err := hif.Interfaces() + if err != nil { + return nil, err + } + m := map[string]*net.IPNet{} + + for _, iface := range ifaces { + addrs, err := hif.Addrs(&iface) + if err != nil { + return nil, err + } + for _, a := range addrs { + switch ipnet := a.(type) { + case *net.IPNet: + _, hostOnly := excludeNets[ipnet.String()] + if !hostOnly && iface.Flags&net.FlagUp != 0 && iface.Flags&net.FlagLoopback == 0 { + m[ipnet.String()] = ipnet + } + default: + + } + } + } + return m, nil +} + +// checkIPNetCollision returns true if any host interfaces conflict with the host-only network mask passed as a parameter. +// This works with IPv4 or IPv6 ip addresses. +func checkIPNetCollision(hostonly *net.IPNet, hostIfaces map[string]*net.IPNet) (bool, error) { + for _, ifaceNet := range hostIfaces { + if hostonly.IP.Equal(ifaceNet.IP.Mask(ifaceNet.Mask)) { + return true, nil + } + } + return false, nil +} + // parseIPv4Mask parses IPv4 netmask written in IP form (e.g. 255.255.255.0). // This function should really belong to the net package. func parseIPv4Mask(s string) net.IPMask { diff --git a/drivers/virtualbox/network_test.go b/drivers/virtualbox/network_test.go index 262c343f70..ce8f4cd9e5 100644 --- a/drivers/virtualbox/network_test.go +++ b/drivers/virtualbox/network_test.go @@ -65,6 +65,37 @@ Enabled: No ` ) +type mockHostInterfaces struct { + mockIfaces []net.Interface + mockAddrs map[string]net.Addr +} + +func newMockHostInterfaces() *mockHostInterfaces { + return &mockHostInterfaces{ + mockAddrs: make(map[string]net.Addr), + } +} + +func (mhi *mockHostInterfaces) Interfaces() ([]net.Interface, error) { + return mhi.mockIfaces, nil +} + +func (mhi *mockHostInterfaces) Addrs(iface *net.Interface) ([]net.Addr, error) { + return []net.Addr{mhi.mockAddrs[iface.Name]}, nil +} + +func (mhi *mockHostInterfaces) addMockIface(ip string, mask int, iplen int, name string, flags net.Flags) (*net.IPNet, error) { + iface := &net.Interface{Name: name, Flags: flags} + mhi.mockIfaces = append(mhi.mockIfaces, *iface) + + ipnet := &net.IPNet{IP: net.ParseIP(ip), Mask: net.CIDRMask(mask, 8*iplen)} + if ipnet.IP == nil { + return nil, &net.ParseError{Type: "IP address", Text: ip} + } + mhi.mockAddrs[name] = ipnet + return ipnet, nil +} + // Tests that when we have a host only network which matches our expectations, // it gets returned correctly. func TestGetHostOnlyNetworkHappy(t *testing.T) { @@ -220,8 +251,10 @@ func TestGetHostOnlyNetwork(t *testing.T) { args: "list hostonlyifs", stdOut: stdOutOneHostOnlyNetwork, } + nets, err := listHostOnlyAdapters(vbox) + assert.NoError(t, err) - net, err := getOrCreateHostOnlyNetwork(net.ParseIP("192.168.99.1"), parseIPv4Mask("255.255.255.0"), vbox) + net, err := getOrCreateHostOnlyNetwork(net.ParseIP("192.168.99.1"), parseIPv4Mask("255.255.255.0"), nets, vbox) assert.NotNil(t, net) assert.Equal(t, "HostInterfaceNetworking-vboxnet0", net.NetworkName) @@ -240,10 +273,8 @@ IPAddress: 192.168.99.1 NetworkMask: 255.255.255.0 VBoxNetworkName: HostInterfaceNetworking-vboxnet1`, } - - net, err := getOrCreateHostOnlyNetwork(net.ParseIP("192.168.99.1"), parseIPv4Mask("255.255.255.0"), vbox) - - assert.Nil(t, net) + nets, err := listHostOnlyAdapters(vbox) + assert.Nil(t, nets) assert.EqualError(t, err, `VirtualBox is configured with multiple host-only adapters with the same IP "192.168.99.1". Please remove one.`) } @@ -255,10 +286,8 @@ VBoxNetworkName: HostInterfaceNetworking-vboxnet0 Name: vboxnet0 VBoxNetworkName: HostInterfaceNetworking-vboxnet0`, } - - net, err := getOrCreateHostOnlyNetwork(net.ParseIP("192.168.99.1"), parseIPv4Mask("255.255.255.0"), vbox) - - assert.Nil(t, net) + nets, err := listHostOnlyAdapters(vbox) + assert.Nil(t, nets) assert.EqualError(t, err, `VirtualBox is configured with multiple host-only adapters with the same name "HostInterfaceNetworking-vboxnet0". Please remove one.`) } @@ -291,3 +320,87 @@ func TestGetDHCPServers(t *testing.T) { assert.Equal(t, "ffffff00", server.IPv4.Mask.String()) assert.False(t, server.Enabled) } + +// Tests detection of a conflict between prospective vbox host-only network and an IPV6 host interface +func TestCheckIPNetCollisionIPv6(t *testing.T) { + m := map[string]*net.IPNet{} + _, vboxHostOnly, err := net.ParseCIDR("2607:f8b0:400e:c04:ffff:ffff:ffff:ffff/64") + assert.Nil(t, err) + + hostIP, hostNet, err := net.ParseCIDR("2001:4998:c:a06::2:4008/64") + assert.Nil(t, err) + m[hostIP.String()] = &net.IPNet{IP: hostIP, Mask: hostNet.Mask} + + result, err := checkIPNetCollision(vboxHostOnly, m) + assert.Nil(t, err) + assert.False(t, result) + + hostIP, hostNet, err = net.ParseCIDR("2607:f8b0:400e:c04::6a/64") + assert.Nil(t, err) + m[hostIP.String()] = &net.IPNet{IP: hostIP, Mask: hostNet.Mask} + + result, err = checkIPNetCollision(vboxHostOnly, m) + assert.Nil(t, err) + assert.True(t, result) +} + +// Tests detection of a conflict between prospective vbox host-only network and an IPV4 host interface +func TestCheckIPNetCollisionIPv4(t *testing.T) { + m := map[string]*net.IPNet{} + _, vboxHostOnly, err := net.ParseCIDR("192.168.99.1/24") + assert.NoError(t, err) + + hostIP, hostNet, err := net.ParseCIDR("10.10.10.42/24") + assert.NoError(t, err) + m[hostIP.String()] = &net.IPNet{IP: hostIP, Mask: hostNet.Mask} + + result, err := checkIPNetCollision(vboxHostOnly, m) + assert.NoError(t, err) + assert.False(t, result) + + hostIP, hostNet, err = net.ParseCIDR("192.168.99.22/24") + assert.NoError(t, err) + m[hostIP.String()] = &net.IPNet{IP: hostIP, Mask: hostNet.Mask} + + result, err = checkIPNetCollision(vboxHostOnly, m) + assert.NoError(t, err) + assert.True(t, result) +} + +// Tests functionality of listHostInterfaces and verifies only non-loopback, active and non-excluded interfaces are returned +func TestListHostInterfaces(t *testing.T) { + mhi := newMockHostInterfaces() + excludes := map[string]*hostOnlyNetwork{} + + en0, err := mhi.addMockIface("10.10.0.22", 24, net.IPv4len, "en0", net.FlagUp|net.FlagBroadcast) + assert.NoError(t, err) + _, err = mhi.addMockIface("10.10.1.11", 24, net.IPv4len, "en1", net.FlagBroadcast /*not up*/) + assert.NoError(t, err) + _, err = mhi.addMockIface("127.0.0.1", 24, net.IPv4len, "lo0", net.FlagUp|net.FlagLoopback) + assert.NoError(t, err) + en0ipv6, err := mhi.addMockIface("2001:4998:c:a06::2:4008", 64, net.IPv6len, "en0ipv6", net.FlagUp|net.FlagBroadcast) + assert.NoError(t, err) + vboxnet0, err := mhi.addMockIface("192.168.99.1", 24, net.IPv4len, "vboxnet0", net.FlagUp|net.FlagBroadcast) + assert.NoError(t, err) + notvboxnet0, err := mhi.addMockIface("192.168.99.42", 24, net.IPv4len, "en2", net.FlagUp|net.FlagBroadcast) + assert.NoError(t, err) + + excludes["192.168.99.1/24"] = &hostOnlyNetwork{IPv4: *vboxnet0, Name: "HostInterfaceNetworking-vboxnet0"} + + m, err := listHostInterfaces(mhi, excludes) + assert.NoError(t, err) + assert.NotEmpty(t, m) + + assert.Contains(t, m, "10.10.0.22/24") + assert.Equal(t, en0, m["10.10.0.22/24"]) + + assert.Contains(t, m, "2001:4998:c:a06::2:4008/64") + assert.Equal(t, en0ipv6, m["2001:4998:c:a06::2:4008/64"]) + + assert.Contains(t, m, "192.168.99.42/24") + assert.Equal(t, notvboxnet0, m["192.168.99.42/24"]) + + assert.NotContains(t, m, "10.10.1.11/24") + assert.NotContains(t, m, "127.0.0.1/24") + assert.NotContains(t, m, "192.168.99.1/24") +} diff --git a/drivers/virtualbox/virtualbox.go b/drivers/virtualbox/virtualbox.go index 15deea8373..7846d0f4fb 100644 --- a/drivers/virtualbox/virtualbox.go +++ b/drivers/virtualbox/virtualbox.go @@ -38,11 +38,13 @@ var ( ErrMustEnableVTX = errors.New("This computer doesn't have VT-X/AMD-v enabled. Enabling it in the BIOS is mandatory") ErrNotCompatibleWithHyperV = errors.New("Hyper-V is installed. VirtualBox won't boot a 64bits VM when Hyper-V is activated. If it's installed but deactivated, you can use --virtualbox-no-vtx-check to try anyways") ErrNetworkAddrCidr = errors.New("host-only cidr must be specified with a host address, not a network address") + ErrNetworkAddrCollision = errors.New("host-only cidr conflicts with the network address of a host interface") ) type Driver struct { *drivers.BaseDriver VBoxManager + HostInterfaces b2dUpdater B2DUpdater sshKeyGenerator SSHKeyGenerator diskCreator DiskCreator @@ -75,6 +77,7 @@ func NewDriver(hostName, storePath string) *Driver { ipWaiter: NewIPWaiter(), randomInter: NewRandomInter(), sleeper: NewSleeper(), + HostInterfaces: NewHostInterfaces(), Memory: defaultMemory, CPU: defaultCPU, DiskSize: defaultDiskSize, @@ -529,6 +532,11 @@ func (d *Driver) Start() error { return err } + err = validateNoIPCollisions(d.HostInterfaces, network, nets) + if err != nil { + return err + } + hostOnlyNet := getHostOnlyAdapter(nets, ip, network.Mask) if hostOnlyNet != nil { // OK, we found a valid host-only adapter @@ -768,8 +776,18 @@ func (d *Driver) setupHostOnlyNetwork(machineName string) (*hostOnlyNetwork, err return nil, err } + nets, err := listHostOnlyAdapters(d.VBoxManager) + if err != nil { + return nil, err + } + + err = validateNoIPCollisions(d.HostInterfaces, network, nets) + if err != nil { + return nil, err + } + log.Debugf("Searching for hostonly interface for IPv4: %s and Mask: %s", ip, network.Mask) - hostOnlyAdapter, err := getOrCreateHostOnlyNetwork(ip, network.Mask, d.VBoxManager) + hostOnlyAdapter, err := getOrCreateHostOnlyNetwork(ip, network.Mask, nets, d.VBoxManager) if err != nil { return nil, err } @@ -822,6 +840,32 @@ func parseAndValidateCIDR(hostOnlyCIDR string) (net.IP, *net.IPNet, error) { return ip, network, nil } +// validateNoIPCollisions ensures no conflicts between the host's network interfaces and the vbox host-only network that +// will be used for machine vm instances. +func validateNoIPCollisions(hif HostInterfaces, hostOnlyNet *net.IPNet, currHostOnlyNets map[string]*hostOnlyNetwork) error { + hostOnlyByCIDR := map[string]*hostOnlyNetwork{} + //listHostOnlyAdapters returns a map w/ virtualbox net names as key. Rekey to CIDRs + for _, n := range currHostOnlyNets { + ipnet := net.IPNet{IP: n.IPv4.IP, Mask: n.IPv4.Mask} + hostOnlyByCIDR[ipnet.String()] = n + } + + m, err := listHostInterfaces(hif, hostOnlyByCIDR) + if err != nil { + return err + } + + collision, err := checkIPNetCollision(hostOnlyNet, m) + if err != nil { + return err + } + + if collision { + return ErrNetworkAddrCollision + } + return nil +} + // Select an available port, trying the specified // port first, falling back on an OS selected port. func getAvailableTCPPort(port int) (int, error) { diff --git a/drivers/virtualbox/virtualbox_test.go b/drivers/virtualbox/virtualbox_test.go index 4dbf322875..dc935acdb1 100644 --- a/drivers/virtualbox/virtualbox_test.go +++ b/drivers/virtualbox/virtualbox_test.go @@ -241,6 +241,74 @@ func TestInvalidNetworkIpCIDR(t *testing.T) { assert.Nil(t, network) } +// Tests detection of a conflict between an existing vbox host-only network and a host network interface. This +// scenario would happen if the docker-machine was created with the host on one network, and then the host gets +// moved to another network (e.g. different wifi routers) +func TestCIDRHostIFaceCollisionExisting(t *testing.T) { + vbox := &VBoxManagerMock{ + args: "list hostonlyifs", + stdOut: stdOutTwoHostOnlyNetwork, + } + mhi := newMockHostInterfaces() + _, err := mhi.addMockIface("192.168.99.42", 24, net.IPv4len, "en0", net.FlagUp|net.FlagBroadcast) + assert.NoError(t, err) + + nets, err := listHostOnlyAdapters(vbox) + assert.NoError(t, err) + m, listErr := listHostInterfaces(mhi, nets) + assert.Nil(t, listErr) + assert.NotEmpty(t, m) + + _, network, cidrErr := net.ParseCIDR("192.168.99.1/24") + assert.Nil(t, cidrErr) + err = validateNoIPCollisions(mhi, network, nets) + assert.Equal(t, ErrNetworkAddrCollision, err) +} + +// Tests operation of validateNoIPCollisions when no conflicts exist. +func TestCIDRHostIFaceNoCollision(t *testing.T) { + vbox := &VBoxManagerMock{ + args: "list hostonlyifs", + stdOut: stdOutTwoHostOnlyNetwork, + } + mhi := newMockHostInterfaces() + _, err := mhi.addMockIface("10.10.0.22", 24, net.IPv4len, "en0", net.FlagUp|net.FlagBroadcast) + assert.NoError(t, err) + + nets, err := listHostOnlyAdapters(vbox) + assert.NoError(t, err) + m, listErr := listHostInterfaces(mhi, nets) + assert.Nil(t, listErr) + assert.NotEmpty(t, m) + + _, network, cidrErr := net.ParseCIDR("192.168.99.1/24") + assert.Nil(t, cidrErr) + err = validateNoIPCollisions(mhi, network, nets) + assert.NoError(t, err) +} + +// Tests detection of a conflict between a potential vbox host-only network and a host network interface. +func TestCIDRHostIFaceCollision(t *testing.T) { + vbox := &VBoxManagerMock{ + args: "list hostonlyifs", + stdOut: "", + } + mhi := newMockHostInterfaces() + _, err := mhi.addMockIface("192.168.99.42", 24, net.IPv4len, "en0", net.FlagUp|net.FlagBroadcast) + assert.NoError(t, err) + + nets, err := listHostOnlyAdapters(vbox) + assert.NoError(t, err) + m, listErr := listHostInterfaces(mhi, nets) + assert.Nil(t, listErr) + assert.NotEmpty(t, m) + + _, network, cidrErr := net.ParseCIDR("192.168.99.1/24") + assert.Nil(t, cidrErr) + err = validateNoIPCollisions(mhi, network, nets) + assert.Equal(t, ErrNetworkAddrCollision, err) +} + func TestSetConfigFromFlags(t *testing.T) { driver := newTestDriver("default") @@ -320,6 +388,16 @@ func (v *MockCreateOperations) Sleep(d time.Duration) { v.doCall("Sleep " + fmt.Sprintf("%v", d)) } +func (v *MockCreateOperations) Interfaces() ([]net.Interface, error) { + _, err := v.doCall("Interfaces") + return []net.Interface{}, err +} + +func (v *MockCreateOperations) Addrs(iface *net.Interface) ([]net.Addr, error) { + _, err := v.doCall("Addrs " + fmt.Sprintf("%v", iface)) + return []net.Addr{}, err +} + func (v *MockCreateOperations) expectCall(callSignature, output string, err error) { v.expectedCalls = append(v.expectedCalls, Call{ signature: callSignature, @@ -359,6 +437,7 @@ func mockCalls(t *testing.T, driver *Driver, expectedCalls []Call) { driver.ipWaiter = mockOperations driver.randomInter = mockOperations driver.sleeper = mockOperations + driver.HostInterfaces = mockOperations } func TestCreateVM(t *testing.T) { @@ -396,6 +475,7 @@ func TestStart(t *testing.T) { mockCalls(t, driver, []Call{ {"vbm showvminfo default --machinereadable", `VMState="poweroff"`, nil}, {"vbm list hostonlyifs", "", nil}, + {"Interfaces", "", nil}, {"vbm hostonlyif create", "Interface 'VirtualBox Host-Only Ethernet Adapter' was successfully created", nil}, {"vbm list hostonlyifs", ` Name: VirtualBox Host-Only Ethernet Adapter @@ -431,6 +511,7 @@ HardwareAddress: 0a:00:27:00:00:00 MediumType: Ethernet Status: Up VBoxNetworkName: HostInterfaceNetworking-VirtualBox Host-Only Ethernet Adapter`, nil}, + {"Interfaces", "", nil}, }) err := driver.Start() @@ -443,6 +524,7 @@ func TestStartWithHostOnlyAdapterCreationBug(t *testing.T) { mockCalls(t, driver, []Call{ {"vbm showvminfo default --machinereadable", `VMState="poweroff"`, nil}, {"vbm list hostonlyifs", "", nil}, + {"Interfaces", "", nil}, {"vbm hostonlyif create", "", errors.New("error: Failed to create the host-only adapter")}, {"vbm list hostonlyifs", "", nil}, {"vbm list hostonlyifs", ` @@ -479,6 +561,7 @@ HardwareAddress: 0a:00:27:00:00:00 MediumType: Ethernet Status: Up VBoxNetworkName: HostInterfaceNetworking-VirtualBox Host-Only Ethernet Adapter`, nil}, + {"Interfaces", "", nil}, {"vbm showvminfo default --machinereadable", `VMState="running"`, nil}, {"vbm controlvm default acpipowerbutton", "", nil}, {"vbm showvminfo default --machinereadable", `VMState="stopped"`, nil},