diff --git a/builder/vmware/common/driver.go b/builder/vmware/common/driver.go index 6f3a40393..db50a2889 100644 --- a/builder/vmware/common/driver.go +++ b/builder/vmware/common/driver.go @@ -430,6 +430,56 @@ func (d *VmwareDriver) PotentialGuestIP(state multistep.StateBag) ([]string, err return addrs, nil } + if runtime.GOOS == "darwin" { + // We have match no vmware DHCP lease for this MAC. We'll try to match it in Apple DHCP leases. + // As a remember, VMWare is no longer able to rely on its own dhcpd server on MacOS BigSur and is + // forced to use Apple DHCPD server instead. + // https://communities.vmware.com/t5/VMware-Fusion-Discussions/Big-Sur-hosts-with-Fusion-Is-vmnet-dhcpd-vmnet8-leases-file/m-p/2298927/highlight/true#M140003 + + // set the apple dhcp leases path + appleDhcpLeasesPath := "/var/db/dhcpd_leases" + log.Printf("Trying Apple DHCP leases path: %s", appleDhcpLeasesPath) + + // open up the path to the apple dhcpd leases + fh, err := os.Open(appleDhcpLeasesPath) + if err != nil { + log.Printf("Error while reading apple DHCP lease path file %s: %s", appleDhcpLeasesPath, err.Error()) + } else { + defer fh.Close() + + // and then read its contents + leaseEntries, err := ReadAppleDhcpdLeaseEntries(fh) + if err != nil { + return []string{}, err + } + + // Parse our MAC address again. There's no need to check for an + // error because we've already parsed this successfully. + hwaddr, _ := net.ParseMAC(MACAddress) + + // Go through our available lease entries and see which ones are within + // scope, and that match to our hardware address. + available_lease_entries := make([]appleDhcpLeaseEntry, 0) + for _, entry := range leaseEntries { + // Next check for any where the hardware address matches. + if bytes.Equal(hwaddr, entry.hwAddress) { + available_lease_entries = append(available_lease_entries, entry) + } + } + + // Check if we found any lease entries that correspond to us. If so, then we + // need to map() them in order to extract the address field to return to the + // caller. + if len(available_lease_entries) > 0 { + addrs := make([]string, 0) + for _, entry := range available_lease_entries { + addrs = append(addrs, entry.ipAddress) + } + return addrs, nil + } + } + } + return []string{}, fmt.Errorf("None of the found device(s) %v has a DHCP lease for MAC %s", devices, MACAddress) } diff --git a/builder/vmware/common/driver_parser.go b/builder/vmware/common/driver_parser.go index 8b1e5ebcc..825fa7bd5 100644 --- a/builder/vmware/common/driver_parser.go +++ b/builder/vmware/common/driver_parser.go @@ -2430,3 +2430,142 @@ func ReadDhcpdLeaseEntries(fd *os.File) ([]dhcpLeaseEntry, error) { } return result, nil } + +/*** Apple Dhcp Leases */ + +// Here is what an Apple DHCPD lease entry looks like: +// { +// ip_address=192.168.111.2 +// hw_address=1,0:50:56:20:ac:33 +// identifier=1,0:50:56:20:ac:33 +// lease=0x5fd72edc +// name=vagrant-2019 +// } + +type appleDhcpLeaseEntry struct { + ipAddress string + hwAddress, id []byte + lease string + name string + extra map[string]string +} + +func readAppleDhcpdLeaseEntry(in chan byte) (entry *appleDhcpLeaseEntry, err error) { + entry = &appleDhcpLeaseEntry{extra: map[string]string{}} + mandatoryFieldCount := 0 + // Read up to the lease item and validate that it actually matches + _, ch := consumeOpenClosePair('{', '}', in) + for insideBraces := true; insideBraces; { + item, ok := consumeUntilSentinel('\n', ch) + item_s := strings.TrimSpace(string(item)) + + if !ok { + insideBraces = false + } + if strings.Contains(item_s, "{") || strings.Contains(item_s, "}") { + continue + } + splittedLine := strings.Split(item_s, "=") + var key, val string + switch len(splittedLine) { + case 0: + // should never happens as Split always returns at least 1 item + fallthrough + case 1: + log.Printf("Error parsing invalid line: `%s`", item_s) + continue + case 2: + key = strings.TrimSpace(splittedLine[0]) + val = strings.TrimSpace(splittedLine[1]) + default: + // There were more than one '=' on this line, we'll keep the part before the first '=' as the key and + // the rest will be the value + key = strings.TrimSpace(splittedLine[0]) + val = strings.TrimSpace(strings.Join(splittedLine[1:], "=")) + } + switch key { + case "ip_address": + entry.ipAddress = val + mandatoryFieldCount++ + case "identifier": + fallthrough + case "hw_address": + if strings.Count(val, ",") != 1 { + log.Printf("Error %s `%s` is not properly formatted for entry %s", key, val, entry.name) + break + } + splittedVal := strings.Split(val, ",") + mac := splittedVal[1] + splittedMac := strings.Split(mac, ":") + // Pad the retrieved hw address with '0' when necessary + for idx := range splittedMac { + if len(splittedMac[idx]) == 1 { + splittedMac[idx] = "0" + splittedMac[idx] + } + } + mac = strings.Join(splittedMac, ":") + decodedLease, err := decodeDhcpdLeaseBytes(mac) + if err != nil { + log.Printf("Error trying to parse %s (%v) for entry %s - %v", key, val, entry.name, mac) + break + } + if key == "identifier" { + entry.id = decodedLease + } else { + entry.hwAddress = decodedLease + } + mandatoryFieldCount++ + case "lease": + entry.lease = val + case "name": + entry.name = val + default: + // Just stash it for now because we have no idea what it is. + entry.extra[key] = val + } + } + // we have most likely parsed the whole file + if mandatoryFieldCount == 0 { + return nil, nil + } + // an entry is composed of 3 mandatory fields, we'll check that they all have been set during the parsing + if mandatoryFieldCount < 3 { + return entry, fmt.Errorf("Error entry `%v` is missing mandatory information", entry) + } + return entry, nil +} + +func ReadAppleDhcpdLeaseEntries(fd *os.File) ([]appleDhcpLeaseEntry, error) { + fch := consumeFile(fd) + uncommentedch := uncomment(fch) + wch := filterOutCharacters([]byte{'\r', '\v'}, uncommentedch) + + result := make([]appleDhcpLeaseEntry, 0) + errors := make([]error, 0) + + // Consume apple dhcpd lease entries from the channel until we just plain run out. + for i := 0; ; i++ { + if entry, err := readAppleDhcpdLeaseEntry(wch); entry == nil { + // If our entry is nil, then we've run out of input and finished + // parsing the file to completion. + break + + } else if err != nil { + // If we received an error, then log it and keep track of it. This + // way we can warn the user later which entries we had issues with. + log.Printf("Error parsing apple dhcpd lease entry #%d: %s", 1+i, err) + errors = append(errors, err) + + } else { + // If we've parsed an entry successfully, then aggregate it to + // our slice of results. + result = append(result, *entry) + } + } + + // If we received any errors then include alongside our results. + if len(errors) > 0 { + return result, fmt.Errorf("Errors found while parsing apple dhcpd lease entries: %v", errors) + } + return result, nil +} diff --git a/builder/vmware/common/driver_parser_test.go b/builder/vmware/common/driver_parser_test.go index 01abffdbb..dbd24fa18 100644 --- a/builder/vmware/common/driver_parser_test.go +++ b/builder/vmware/common/driver_parser_test.go @@ -865,6 +865,208 @@ func TestParserReadDhcpdLeases(t *testing.T) { } } +func consumeAppleLeaseString(s string) chan byte { + sch := consumeString(s) + uncommentedch := uncomment(sch) + return filterOutCharacters([]byte{'\r', '\v'}, uncommentedch) +} + +func TestParserReadAppleDhcpdLeaseEntry(t *testing.T) { + test_1 := `{ + ip_address=192.168.111.3 + hw_address=1,0:c:56:3c:e7:22 + identifier=1,0:c:56:3c:e7:22 + lease=0x5fd78ae2 + name=vagrant-2019 + fake=field + }` + expected_1 := map[string]string{ + "ipAddress": "192.168.111.3", + "hwAddress": "000c563ce722", + "id": "000c563ce722", + "lease": "0x5fd78ae2", + "name": "vagrant-2019", + } + expected_extra_1 := map[string]string{ + "fake": "field", + } + + result, err := readAppleDhcpdLeaseEntry(consumeAppleLeaseString(test_1)) + if err != nil { + t.Errorf("error parsing entry: %v", err) + } + if result.ipAddress != expected_1["ipAddress"] { + t.Errorf("expected ipAddress %v, got %v", expected_1["ipAddress"], result.ipAddress) + } + if hex.EncodeToString(result.hwAddress) != expected_1["hwAddress"] { + t.Errorf("expected hwAddress %v, got %v", expected_1["hwAddress"], hex.EncodeToString(result.hwAddress)) + } + if hex.EncodeToString(result.id) != expected_1["id"] { + t.Errorf("expected id %v, got %v", expected_1["id"], hex.EncodeToString(result.id)) + } + if result.lease != expected_1["lease"] { + t.Errorf("expected lease %v, got %v", expected_1["lease"], result.lease) + } + if result.name != expected_1["name"] { + t.Errorf("expected name %v, got %v", expected_1["name"], result.name) + } + if result.extra["fake"] != expected_extra_1["fake"] { + t.Errorf("expected extra %v, got %v", expected_extra_1["fake"], result.extra["fake"]) + } + + test_2 := `{ + ip_address=192.168.111.4 + hw_address=1,0:c:56:3c:e7:23 + identifier=1,0:c:56:3c:e7:23 + }` + expected_2 := map[string]string{ + "ipAddress": "192.168.111.4", + "hwAddress": "000c563ce723", + "id": "000c563ce723", + } + + result, err = readAppleDhcpdLeaseEntry(consumeAppleLeaseString(test_2)) + if err != nil { + t.Errorf("error parsing entry: %v", err) + } + if result.ipAddress != expected_2["ipAddress"] { + t.Errorf("expected ipAddress %v, got %v", expected_2["ipAddress"], result.ipAddress) + } + if hex.EncodeToString(result.hwAddress) != expected_2["hwAddress"] { + t.Errorf("expected hwAddress %v, got %v", expected_2["hwAddress"], hex.EncodeToString(result.hwAddress)) + } + if hex.EncodeToString(result.id) != expected_2["id"] { + t.Errorf("expected id %v, got %v", expected_2["id"], hex.EncodeToString(result.id)) + } +} + +func TestParserReadAppleDhcpdLeases(t *testing.T) { + f, err := os.Open(filepath.Join("testdata", "apple-dhcpd-example.leases")) + if err != nil { + t.Fatalf("Unable to open dhcpd.leases sample: %s", err) + } + defer f.Close() + + results, err := ReadAppleDhcpdLeaseEntries(f) + if err != nil { + t.Fatalf("Error reading lease: %s", err) + } + + // some simple utilities + filter_ipAddr := func(ipAddress string, items []appleDhcpLeaseEntry) (result []appleDhcpLeaseEntry) { + for _, item := range items { + if item.ipAddress == ipAddress { + result = append(result, item) + } + } + return + } + + find_id := func(id string, items []appleDhcpLeaseEntry) *appleDhcpLeaseEntry { + for _, item := range items { + if id == hex.EncodeToString(item.id) { + return &item + } + } + return nil + } + + find_hwAddr := func(hwAddr string, items []appleDhcpLeaseEntry) *appleDhcpLeaseEntry { + for _, item := range items { + if hwAddr == hex.EncodeToString(item.hwAddress) { + return &item + } + } + return nil + } + + // actual unit tests + test_1 := map[string]string{ + "ipAddress": "127.0.0.19", + "id": "0dead099aabb", + "hwAddress": "0dead099aabb", + } + test_1_findings := filter_ipAddr(test_1["ipAddress"], results) + if len(test_1_findings) != 2 { + t.Errorf("expected %d matching entries, got %d", 2, len(test_1_findings)) + } else { + res := find_hwAddr(test_1["hwAddress"], test_1_findings) + if res == nil { + t.Errorf("unable to find item with hwAddress %v", test_1["hwAddress"]) + } else if hex.EncodeToString(res.id) != test_1["id"] { + t.Errorf("expected id %s, got %s", test_1["id"], hex.EncodeToString(res.id)) + } + } + + test_2 := map[string]string{ + "ipAddress": "127.0.0.19", + "id": "0dead0667788", + "hwAddress": "0dead0667788", + } + test_2_findings := filter_ipAddr(test_2["ipAddress"], results) + if len(test_2_findings) != 2 { + t.Errorf("expected %d matching entries, got %d", 2, len(test_2_findings)) + } else { + res := find_hwAddr(test_2["hwAddress"], test_2_findings) + if res == nil { + t.Errorf("unable to find item with hwAddress %v", test_2["hwAddress"]) + } else if hex.EncodeToString(res.id) != test_2["id"] { + t.Errorf("expected id %s, got %s", test_2["id"], hex.EncodeToString(res.id)) + } + } + + test_3 := map[string]string{ + "ipAddress": "127.0.0.17", + "id": "0dead0334455", + "hwAddress": "0dead0667788", + } + test_3_findings := filter_ipAddr(test_3["ipAddress"], results) + if len(test_3_findings) != 2 { + t.Errorf("expected %d matching entries, got %d", 2, len(test_3_findings)) + } else { + res := find_id(test_3["id"], test_3_findings) + if res == nil { + t.Errorf("unable to find item with id %v", test_3["id"]) + } else if hex.EncodeToString(res.hwAddress) != test_3["hwAddress"] { + t.Errorf("expected hardware address %s, got %s", test_3["hwAddress"], hex.EncodeToString(res.hwAddress)) + } + } + + test_4 := map[string]string{ + "ipAddress": "127.0.0.17", + "id": "0dead0001122", + "hwAddress": "0dead0667788", + } + test_4_findings := filter_ipAddr(test_4["ipAddress"], results) + if len(test_4_findings) != 2 { + t.Errorf("expected %d matching entries, got %d", 2, len(test_4_findings)) + } else { + res := find_id(test_4["id"], test_4_findings) + if res == nil { + t.Errorf("unable to find item with id %v", test_4["id"]) + } else if hex.EncodeToString(res.hwAddress) != test_4["hwAddress"] { + t.Errorf("expected hardware address %s, got %s", test_4["hwAddress"], hex.EncodeToString(res.hwAddress)) + } + } + + test_5 := map[string]string{ + "ipAddress": "127.0.0.20", + "id": "0dead099aabc", + "hwAddress": "0dead099aabc", + } + test_5_findings := filter_ipAddr(test_5["ipAddress"], results) + if len(test_5_findings) != 1 { + t.Errorf("expected %d matching entries, got %d", 1, len(test_5_findings)) + } else { + res := find_id(test_5["id"], test_5_findings) + if res == nil { + t.Errorf("unable to find item with id %v", test_5["id"]) + } else if hex.EncodeToString(res.hwAddress) != test_5["hwAddress"] { + t.Errorf("expected hardware address %s, got %s", test_5["hwAddress"], hex.EncodeToString(res.hwAddress)) + } + } +} + func TestParserTokenizeNetworkingConfig(t *testing.T) { tests := []string{ "words words words", diff --git a/builder/vmware/common/testdata/apple-dhcpd-example.leases b/builder/vmware/common/testdata/apple-dhcpd-example.leases new file mode 100644 index 000000000..53aacb19c --- /dev/null +++ b/builder/vmware/common/testdata/apple-dhcpd-example.leases @@ -0,0 +1,40 @@ +# this entry is normal +{ + ip_address=127.0.0.17 + hw_address=1,d:ea:d0:66:77:88 + identifier=1,d:ea:d0:0:11:22 + lease=0x5fd78ae2 + name=vagrant-2019 +} + +# this entry has tabs +{ + ip_address=127.0.0.17 + hw_address=1,d:ea:d0:66:77:88 + identifier=1,d:ea:d0:33:44:55 + lease=0x5fd7b4e5 + name=vagrant-2019 +} + +# These next two entries have the same address, but different uids +{ + ip_address=127.0.0.19 + hw_address=1,d:ea:d0:66:77:88 + identifier=1,d:ea:d0:66:77:88 + lease=0x5fd72edc + name=vagrant-2019 +} +{ + ip_address=127.0.0.19 + hw_address=1,d:ea:d0:99:aa:bb + identifier=1,d:ea:d0:99:aa:bb + lease=0x5fd72edc + name=vagrant-2019 +} + +# this entry does not have all fields +{ + ip_address=127.0.0.20 + hw_address=1,d:ea:d0:99:aa:bc + identifier=1,d:ea:d0:99:aa:bc +} \ No newline at end of file