Given a device instance ID for a network card, I would like to know its MAC address. Example device instance ID on my system for integrated Intel Gigabit card:
PCI\VEN_8086&DEV_10CC&SUBSYS_00008086&REV_00\3&33FD14CA&0&C8
So far, the algorithm I have used works as follows:
- Call
SetupDiGetClassDevs
withDIGCF_DEVICEINTERFACE
. - Call
SetupDiEnumDeviceInfo
to get the returned device in aSP_DEVINFO_DATA
. - Call
SetupDiEnumDeviceInterfaces
withGUID_NDIS_LAN_CLASS
to get a device interface. - Call
SetupDiGetDeviceInterfaceDetail
for this returned device interface. This gets us the device path as a string:\\?\pci#ven_8086&dev_10cc&subsys_00008086&rev_00#3&33fd14ca&0&c8#{ad498944-762f-11d0-8dcb-00c04fc3358c}\{28fd5409-15bd-4c06-b62f-004d3a06f852}
- At this point we have an address to the network card driver's interface. Open it with
CreateFile
using the result from #4. - Call
DeviceIoControl
withIOCTL_NDIS_QUERY_GLOBAL_STATS
and OID ofOID_802_3_PERMANENT_ADDRESS
to get the MAC address.
This usually works, and has been used successfully on quite a large number of machines. However, it appears that a very select few machines have network drivers that aren't responding properly to the DeviceIoControl
request in step #6; the problem persists even after updating network card drivers to the latest. These are newer, Windows 7-based computers. Specifically, DeviceIoControl
completes successfully, but returns zero bytes instead of the expected six bytes containing the MAC address.
A clue seems to be on the MSDN page for IOCTL_NDIS_QUERY_GLOBAL_STATS
:
This IOCTL will be deprecated in later operating system releases. You should use WMI interfaces to query miniport driver information. For more information see, NDIS Support for WMI.
-- perhaps newer network card drivers are no longer implementing this IOCTL?
So, how should I get this working? Is it possible there's an oversight in my approach and I'm doing something slightly wrong? Or do I need to take a much more different approach? Some alternate approaches seem to include:
- Query
Win32_NetworkAdapter
WMI class: provides needed information but rejected due to horrible performance. See Fast replacement for Win32_NetworkAdapter WMI class for getting MAC address of local computer - Query
MSNdis_EthernetPermanentAddress
WMI class: appears to be the WMI replacement forIOCTL_NDIS_QUERY_GLOBAL_STATS
and queries the OID directly from the driver - and this one works on the troublesome network driver. Unfortunately, the returned class instances only provide the MAC address and theInstanceName
, which is a localized string likeIntel(R) 82567LM-2 Gigabit Network Connection
. QueryingMSNdis_EnumerateAdapter
yields a list which relates theInstanceName
to aDeviceName
, like\DEVICE\{28FD5409-15BD-4C06-B62F-004D3A06F852}
. I'm not sure how to go from theDeviceName
to the plug-and-play device instance ID (PCI\VEN_8086......
). - Call
GetAdaptersAddresses
orGetAdaptersInfo
(deprecated). The only non-localized identifier I can find in the return value is the adapter name, which is a string like{28FD5409-15BD-4C06-B62F-004D3A06F852}
- same as theDeviceName
returned by the WMI NDIS classes. So again, I can't figure out how to relate it to the device instance ID. I'm not sure if it would work 100% of the time either - e.g. for adapters without TCP/IP protocol configured. - NetBIOS method: requires specific protocols to be set up on the card so won't work 100% of time. Generally seems hack-ish, and not a way to relate to device instance ID anyway that I know of. I'd reject this approach.
- UUID generation method: rejected for reasons I won't elaborate on here.
It seems like if I could find a way to get the "GUID" for the card from the device instance ID, I'd be well on my way with one of the remaining two ways of doing things. But I haven't figured out how yet. Otherwise, the WMI NDIS approach would seem most promising.
Getting a list of network cards and MAC addresses is easy, and there are several ways of doing it. Doing it in a fast way that lets me relate it to the device instance ID is apparently hard...
EDIT: Sample code of the IOCTL call if it helps anyone (ignore the leaked hFile handle):
HANDLE hFile = CreateFile(dosDevice.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
DWORD err = GetLastError();
wcout << "GetMACAddress: CreateFile on " << dosDevice << " failed." << endl;
return MACAddress();
}
BYTE address[6];
DWORD oid = OID_802_3_PERMANENT_ADDRESS, returned = 0;
//this fails too: DWORD oid = OID_802_3_CURRENT_ADDRESS, returned = 0;
if (!DeviceIoControl(hFile, IOCTL_NDIS_QUERY_GLOBAL_STATS, &oid, sizeof(oid), address, 6, &returned, NULL)) {
DWORD err = GetLastError();
wcout << "GetMACAddress: DeviceIoControl on " << dosDevice << " failed." << endl;
return MACAddress();
}
if (returned != 6) {
wcout << "GetMACAddress: invalid address length of " << returned << "." << endl;
return MACAddress();
}
The code fails, printing:
GetMACAddress: invalid address length of 0.
So the DeviceIoControl returns non-zero indicating success, but then returns zero bytes.
I wound up using
SetupDiGetDeviceRegistryProperty
to readSPDRP_FRIENDLYNAME
. If that's not found, then I readSPDRP_DEVICEDESC
instead. Ultimately, this gets me a string like "VirtualBox Host-Only Ethernet Adapter #2". I then match this against the InstanceName property in the WMI NDIS classes (MSNdis_EthernetPermanentAddress
WMI class). Both properties must be read in case there are multiple adapters sharing the same driver (i.e. "#2", "#3", etc.) - if there's only one adapter thenSPDRP_FRIENDLYNAME
isn't available, but if there is more than one thenSPDRP_FRIENDLYNAME
is required to differentiate them.The method makes me a little nervous because I'm comparing what seems like a localized string, and there's no documentation that I've found that guarantees what I'm doing will always work. Unfortunately, I haven't found any better ways that are documented to work, either.
A couple other alternate methods involve groveling in undocumented registry locations. One method is spencercw's method, and the other would be to read
SPDRP_DRIVER
, which is the name of a subkey underHKLM\SYSTEM\CurrentControlSet\Control\Class
. Underneath the driver key, look for theLinkage\Export
value which then seems like it could be matched to theDeviceName
property of theMSNdis_EnumerateAdapter
class. But there's no documentation I could find that says these values can be legally matched. Furthermore, the only documentation I found aboutLinkage\Export
was from the Win2000 registry reference and explicitly said that applications shouldn't rely on it.Another method would be to look at my original question, step 4: "
SetupDiGetDeviceInterfaceDetail
for this returned device interface". The device interface path actually can be used to reconstruct the device path. Start with device interface path:\\?\pci#ven_8086&dev_10cc&subsys_00008086&rev_00#3&33fd14ca&0&c8#{ad498944-762f-11d0-8dcb-00c04fc3358c}\{28fd5409-15bd-4c06-b62f-004d3a06f852}
. Then, remove everything before the final slash, leaving you with:{28fd5409-15bd-4c06-b62f-004d3a06f852}
. Finally, prepend\Device\
to this string and match it against the WMI NDIS classes. Again, however, this seems to be undocumented and relying on an implementation detail of a device interface path.In the end, the other methods I investigated had their own undocumented complications that sounded at least as serious as matching the
SPDRP_FRIENDLYNAME
/SPDRP_DEVICEDESC
strings. So I opted for the simpler approach, which was to just match those strings against the WMI NDIS classes.