package virtualbox import ( "errors" "fmt" "net" "regexp" "strings" "time" "github.com/docker/machine/libmachine/log" ) const ( buggyNetmask = "0f000000" dhcpPrefix = "HostInterfaceNetworking-" ) var ( reHostOnlyAdapterCreated = regexp.MustCompile(`Interface '(.+)' was successfully created`) errNewHostOnlyAdapterNotVisible = errors.New("The host-only adapter we just created is not visible. This is a well known VirtualBox bug. You might want to uninstall it and reinstall at least version 5.0.12 that is is supposed to fix this issue") ) // Host-only network. type hostOnlyNetwork struct { Name string GUID string DHCP bool IPv4 net.IPNet HwAddr net.HardwareAddr Medium string Status string NetworkName string // referenced in DHCP.NetworkName } // Save changes the configuration of the host-only network. func (n *hostOnlyNetwork) Save(vbox VBoxManager) error { if err := n.SaveIPv4(vbox); err != nil { return err } if n.DHCP { vbox.vbm("hostonlyif", "ipconfig", n.Name, "--dhcp") // not implemented as of VirtualBox 4.3 } return nil } // SaveIPv4 changes the ipv4 configuration of the host-only network. func (n *hostOnlyNetwork) SaveIPv4(vbox VBoxManager) error { if n.IPv4.IP != nil && n.IPv4.Mask != nil { if err := vbox.vbm("hostonlyif", "ipconfig", n.Name, "--ip", n.IPv4.IP.String(), "--netmask", net.IP(n.IPv4.Mask).String()); err != nil { return err } } return nil } // createHostonlyAdapter creates a new host-only network. func createHostonlyAdapter(vbox VBoxManager) (*hostOnlyNetwork, error) { out, err := vbox.vbmOut("hostonlyif", "create") if err != nil { return nil, err } res := reHostOnlyAdapterCreated.FindStringSubmatch(string(out)) if res == nil { return nil, errors.New("Failed to create host-only adapter") } return &hostOnlyNetwork{Name: res[1]}, nil } // listHostOnlyAdapters gets all host-only adapters in a map keyed by NetworkName. func listHostOnlyAdapters(vbox VBoxManager) (map[string]*hostOnlyNetwork, error) { out, err := vbox.vbmOut("list", "hostonlyifs") if err != nil { return nil, err } byName := map[string]*hostOnlyNetwork{} byIP := map[string]*hostOnlyNetwork{} n := &hostOnlyNetwork{} err = parseKeyValues(out, reColonLine, func(key, val string) error { switch key { case "Name": n.Name = val case "GUID": n.GUID = val case "DHCP": n.DHCP = (val != "Disabled") case "IPAddress": n.IPv4.IP = net.ParseIP(val) case "NetworkMask": n.IPv4.Mask = parseIPv4Mask(val) case "HardwareAddress": mac, err := net.ParseMAC(val) if err != nil { return err } n.HwAddr = mac case "MediumType": n.Medium = val case "Status": n.Status = val case "VBoxNetworkName": n.NetworkName = val if _, present := byName[n.NetworkName]; present { return fmt.Errorf("VirtualBox is configured with multiple host-only adapters with the same name %q. Please remove one.", n.NetworkName) } byName[n.NetworkName] = n if len(n.IPv4.IP) != 0 { if _, present := byIP[n.IPv4.IP.String()]; present { return fmt.Errorf("VirtualBox is configured with multiple host-only adapters with the same IP %q. Please remove one.", n.IPv4.IP) } byIP[n.IPv4.IP.String()] = n } n = &hostOnlyNetwork{} } return nil }) if err != nil { return nil, err } return byName, nil } func getHostOnlyAdapter(nets map[string]*hostOnlyNetwork, hostIP net.IP, netmask net.IPMask) *hostOnlyNetwork { for _, n := range nets { // Second part of this conditional handles a race where // VirtualBox returns us the incorrect netmask value for the // newly created adapter. if hostIP.Equal(n.IPv4.IP) && (netmask.String() == n.IPv4.Mask.String() || n.IPv4.Mask.String() == buggyNetmask) { log.Debugf("Found: %s", n.Name) return n } } log.Debug("Not found") return nil } func getOrCreateHostOnlyNetwork(hostIP net.IP, netmask net.IPMask, vbox VBoxManager) (*hostOnlyNetwork, error) { nets, err := listHostOnlyAdapters(vbox) if err != nil { return nil, err } hostOnlyAdapter := getHostOnlyAdapter(nets, hostIP, netmask) if hostOnlyAdapter != nil { return hostOnlyAdapter, nil } // No existing host-only adapter found. Create a new one. hostOnlyAdapter, 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 log.Warnf("Creating a new host-only adapter produced an error: %s", err) log.Warn("This is a known VirtualBox bug. Let's try to recover anyway...") hostOnlyAdapter, err = waitForNewHostOnlyNetwork(nets, vbox) if err != nil { return nil, err } log.Warnf("Found a new host-only adapter: %q", hostOnlyAdapter.Name) } hostOnlyAdapter.IPv4.IP = hostIP hostOnlyAdapter.IPv4.Mask = netmask if err := hostOnlyAdapter.Save(vbox); err != nil { return nil, err } // Check that the adapter still exists. // Sometimes, Vbox says it created it but then it cannot be found... nets, err = listHostOnlyAdapters(vbox) if err != nil { return nil, err } found := getHostOnlyAdapter(nets, hostIP, netmask) if found == nil { return nil, errNewHostOnlyAdapterNotVisible } return hostOnlyAdapter, nil } func waitForNewHostOnlyNetwork(oldNets map[string]*hostOnlyNetwork, vbox VBoxManager) (*hostOnlyNetwork, error) { for i := 0; i < 10; i++ { time.Sleep(1 * time.Second) newNets, err := listHostOnlyAdapters(vbox) if err != nil { return nil, err } for name, latestNet := range newNets { if _, present := oldNets[name]; !present { return latestNet, nil } } } return nil, errors.New("Failed to find a new host-only adapter") } // DHCP server info. type dhcpServer struct { NetworkName string IPv4 net.IPNet LowerIP net.IP UpperIP net.IP Enabled bool } // removeOrphanDHCPServers removed the DHCP servers linked to no host-only adapter func removeOrphanDHCPServers(vbox VBoxManager) error { dhcps, err := listDHCPServers(vbox) if err != nil { return err } if len(dhcps) == 0 { return nil } nets, err := listHostOnlyAdapters(vbox) if err != nil { return err } for name := range dhcps { if strings.HasPrefix(name, dhcpPrefix) { if _, present := nets[name]; !present { if err := vbox.vbm("dhcpserver", "remove", "--netname", name); err != nil { log.Warnf("Unable to remove orphan dhcp server %q: %s", name, err) } } } } return nil } // addHostOnlyDHCPServer adds a DHCP server to a host-only network. func addHostOnlyDHCPServer(ifname string, d dhcpServer, vbox VBoxManager) error { name := dhcpPrefix + ifname dhcps, err := listDHCPServers(vbox) if err != nil { return err } // On some platforms (OSX), creating a host-only adapter adds a default dhcpserver, // while on others (Windows?) it does not. command := "add" if dhcp, ok := dhcps[name]; ok { command = "modify" if (dhcp.IPv4.IP.Equal(d.IPv4.IP)) && (dhcp.IPv4.Mask.String() == d.IPv4.Mask.String()) && (dhcp.LowerIP.Equal(d.LowerIP)) && (dhcp.UpperIP.Equal(d.UpperIP)) && dhcp.Enabled { // dhcp is up to date return nil } } args := []string{"dhcpserver", command, "--netname", name, "--ip", d.IPv4.IP.String(), "--netmask", net.IP(d.IPv4.Mask).String(), "--lowerip", d.LowerIP.String(), "--upperip", d.UpperIP.String(), } if d.Enabled { args = append(args, "--enable") } else { args = append(args, "--disable") } return vbox.vbm(args...) } // listDHCPServers lists all DHCP server settings in a map keyed by DHCP.NetworkName. func listDHCPServers(vbox VBoxManager) (map[string]*dhcpServer, error) { out, err := vbox.vbmOut("list", "dhcpservers") if err != nil { return nil, err } m := map[string]*dhcpServer{} dhcp := &dhcpServer{} err = parseKeyValues(out, reColonLine, func(key, val string) error { switch key { case "NetworkName": dhcp = &dhcpServer{} m[val] = dhcp dhcp.NetworkName = val case "IP": dhcp.IPv4.IP = net.ParseIP(val) case "upperIPAddress": dhcp.UpperIP = net.ParseIP(val) case "lowerIPAddress": dhcp.LowerIP = net.ParseIP(val) case "NetworkMask": dhcp.IPv4.Mask = parseIPv4Mask(val) case "Enabled": dhcp.Enabled = (val == "Yes") } return nil }) if err != nil { return nil, err } return m, 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 { mask := net.ParseIP(s) if mask == nil { return nil } return net.IPv4Mask(mask[12], mask[13], mask[14], mask[15]) }