add code and tests to detect vboxnet collision with host interfaces

+ exclude existing host-only nets from list of host interfaces
+ interface and mocks for system calls to net.*

Signed-off-by: Paul Callahan <paul.callahan@gmail.com>
This commit is contained in:
Paul Callahan 2016-03-08 09:43:22 -08:00
parent 5a270c9751
commit 0db41d0a92
4 changed files with 313 additions and 17 deletions

View File

@ -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 {

View File

@ -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")
}

View File

@ -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) {

View File

@ -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},