Files
2025-04-06 03:14:47 +02:00

1901 lines
63 KiB
Python
Executable File

# -*- coding: utf-8 -*-
# http://web.archive.org/web/20140718071917/http://multivax.com/last_question.html
"""
Get the MAC address of remote hosts or network interfaces.
It provides a platform-independent interface to get the MAC addresses of:
- System network interfaces (by interface name)
- Remote hosts on the local network (by IPv4/IPv6 address or hostname)
It provides one function: ``get_mac_address()``
.. code-block:: python
:caption: Examples
from getmac import get_mac_address
eth_mac = get_mac_address(interface="eth0")
win_mac = get_mac_address(interface="Ethernet 3")
ip_mac = get_mac_address(ip="192.168.0.1")
ip6_mac = get_mac_address(ip6="::1")
host_mac = get_mac_address(hostname="localhost")
updated_mac = get_mac_address(ip="10.0.0.1", network_request=True)
"""
import ctypes
import logging
import os
import platform
import re
import shlex
import socket
import struct
import sys
import traceback
import warnings
from subprocess import CalledProcessError, check_output
try: # Python 3
from subprocess import DEVNULL # type: ignore
except ImportError: # Python 2
DEVNULL = open(os.devnull, "wb") # type: ignore
# Used for mypy (a data type analysis tool)
# If you're copying the code, this section can be safely removed
try:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Dict, List, Optional, Set, Tuple, Type, Union
except ImportError:
pass
# Configure logging
log = logging.getLogger("getmac") # type: logging.Logger
if not log.handlers:
log.addHandler(logging.NullHandler())
__version__ = "0.9.5"
PY2 = sys.version_info[0] == 2 # type: bool
# Configurable settings
DEBUG = 0 # type: int
PORT = 55555 # type: int
# Monkeypatch shutil.which for python 2.7 (TODO(python3): remove shutilwhich.py)
if PY2:
from .shutilwhich import which
else:
from shutil import which
# Platform identifiers
if PY2:
_UNAME = platform.uname() # type: Tuple[str, str, str, str, str, str]
_SYST = _UNAME[0] # type: str
else:
_UNAME = platform.uname() # type: platform.uname_result
_SYST = _UNAME.system # type: str
if _SYST == "Java":
try:
import java.lang
_SYST = str(java.lang.System.getProperty("os.name"))
except ImportError:
_java_err_msg = "Can't determine OS: couldn't import java.lang on Jython"
log.critical(_java_err_msg)
warnings.warn(_java_err_msg, RuntimeWarning)
WINDOWS = _SYST == "Windows" # type: bool
DARWIN = _SYST == "Darwin" # type: bool
OPENBSD = _SYST == "OpenBSD" # type: bool
FREEBSD = _SYST == "FreeBSD" # type: bool
NETBSD = _SYST == "NetBSD" # type: bool
SOLARIS = _SYST == "SunOS" # type: bool
# Not including Darwin or Solaris as a "BSD"
BSD = OPENBSD or FREEBSD or NETBSD # type: bool
# Windows Subsystem for Linux (WSL)
WSL = False # type: bool
LINUX = False # type: bool
if _SYST == "Linux":
if "Microsoft" in platform.version():
WSL = True
else:
LINUX = True
# NOTE: "Linux" methods apply to Android without modifications
# If there's Android-specific stuff then we can add a platform
# identifier for it.
ANDROID = (
hasattr(sys, "getandroidapilevel") or "ANDROID_STORAGE" in os.environ
) # type: bool
# Generic platform identifier used for filtering methods
PLATFORM = _SYST.lower() # type: str
if PLATFORM == "linux" and "Microsoft" in platform.version():
PLATFORM = "wsl"
# User-configurable override to force a specific platform
# This will change to a function argument in 1.0.0
OVERRIDE_PLATFORM = "" # type: str
# Force a specific method to be used for all lookups
# Used for debugging and testing
FORCE_METHOD = "" # type: str
# Get and cache the configured system PATH on import
# The process environment does not change after a process is started
PATH = os.environ.get("PATH", os.defpath).split(os.pathsep) # type: List[str]
if not WINDOWS:
PATH.extend(("/sbin", "/usr/sbin"))
else:
# TODO: Prevent edge case on Windows where our script "getmac.exe"
# gets added to the path ahead of the actual Windows getmac.exe
# This just handles case where it's in a virtualenv, won't work /w global scripts
PATH = [p for p in PATH if "\\getmac\\Scripts" not in p]
# Build the str after modifications are made
PATH_STR = os.pathsep.join(PATH) # type: str
# Use a copy of the environment so we don't
# modify the process's current environment.
ENV = dict(os.environ) # type: Dict[str, str]
ENV["LC_ALL"] = "C" # Ensure ASCII output so we parse correctly
# Constants
IP4 = 0
IP6 = 1
INTERFACE = 2
HOSTNAME = 3
MAC_RE_COLON = r"([0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5})"
MAC_RE_DASH = r"([0-9a-fA-F]{2}(?:-[0-9a-fA-F]{2}){5})"
# On OSX, some MACs in arp output may have a single digit instead of two
# Examples: "18:4f:32:5a:64:5", "14:cc:20:1a:99:0"
# This can also happen on other platforms, like Solaris
MAC_RE_SHORT = r"([0-9a-fA-F]{1,2}(?::[0-9a-fA-F]{1,2}){5})"
# Ensure we only log the Python 2 warning once
WARNED_UNSUPPORTED_PYTHONS = False
# Cache of commands that have been checked for existence by check_command()
CHECK_COMMAND_CACHE = {} # type: Dict[str, bool]
def check_command(command):
# type: (str) -> bool
if command not in CHECK_COMMAND_CACHE:
CHECK_COMMAND_CACHE[command] = bool(which(command, path=PATH_STR))
return CHECK_COMMAND_CACHE[command]
def check_path(filepath):
# type: (str) -> bool
return os.path.exists(filepath) and os.access(filepath, os.R_OK)
def _clean_mac(mac):
# type: (Optional[str]) -> Optional[str]
"""Check and format a string result to be lowercase colon-separated MAC."""
if mac is None:
return None
# Handle cases where it's bytes (which are the same as str in PY2)
mac = str(mac)
if not PY2: # Strip bytestring conversion artifacts
# TODO(python3): check for bytes and decode instead of this weird hack
for garbage_string in ["b'", "'", "\\n", "\\r"]:
mac = mac.replace(garbage_string, "")
# Remove trailing whitespace, make lowercase, remove spaces,
# and replace dashes '-' with colons ':'.
mac = mac.strip().lower().replace(" ", "").replace("-", ":")
# Fix cases where there are no colons
if ":" not in mac and len(mac) == 12:
log.debug("Adding colons to MAC %s", mac)
mac = ":".join(mac[i : i + 2] for i in range(0, len(mac), 2))
# Pad single-character octets with a leading zero (e.g. Darwin's ARP output)
elif len(mac) < 17:
log.debug(
"Length of MAC %s is %d, padding single-character octets with zeros",
mac,
len(mac),
)
parts = mac.split(":")
new_mac = []
for part in parts:
if len(part) == 1:
new_mac.append("0" + part)
else:
new_mac.append(part)
mac = ":".join(new_mac)
# MAC address should ALWAYS be 17 characters before being returned
if len(mac) != 17:
log.warning("MAC address %s is not 17 characters long!", mac)
mac = None
elif mac.count(":") != 5:
log.warning("MAC address %s is missing colon (':') characters", mac)
mac = None
return mac
def _read_file(filepath):
# type: (str) -> Optional[str]
try:
with open(filepath) as f:
return f.read()
# This is IOError on Python 2.7
except (OSError, IOError): # noqa: B014
log.debug("Could not find file: '%s'", filepath)
return None
def _search(regex, text, group_index=0, flags=0):
# type: (str, str, int, int) -> Optional[str]
if not text:
if DEBUG:
log.debug("No text to _search()")
return None
match = re.search(regex, text, flags)
if match:
return match.groups()[group_index]
return None
def _popen(command, args):
# type: (str, str) -> str
for directory in PATH:
executable = os.path.join(directory, command)
if (
os.path.exists(executable)
and os.access(executable, os.F_OK | os.X_OK)
and not os.path.isdir(executable)
):
break
else:
executable = command
if DEBUG >= 3:
log.debug("Running: '%s %s'", executable, args)
return _call_proc(executable, args)
def _call_proc(executable, args):
# type: (str, str) -> str
if WINDOWS:
cmd = executable + " " + args # type: ignore
else:
cmd = [executable] + shlex.split(args) # type: ignore
output = check_output(cmd, stderr=DEVNULL, env=ENV)
if DEBUG >= 4:
log.debug("Output from '%s' command: %s", executable, str(output))
if not PY2 and isinstance(output, bytes):
return str(output, "utf-8")
else:
return str(output)
def _uuid_convert(mac):
# type: (int) -> str
return ":".join(("%012X" % mac)[i : i + 2] for i in range(0, 12, 2))
def _fetch_ip_using_dns():
# type: () -> str
"""
Determines the IP address of the default network interface.
Sends a UDP packet to Cloudflare's DNS (``1.1.1.1``), which should go through
the default interface. This populates the source address of the socket,
which we then inspect and return.
"""
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("1.1.1.1", 53))
ip = s.getsockname()[0]
s.close() # NOTE: sockets don't have context manager in 2.7 :(
return ip
class Method:
#: Valid platform identifier strings
VALID_PLATFORM_NAMES = {
"android",
"darwin",
"linux",
"windows",
"wsl",
"openbsd",
"freebsd",
"sunos",
"other",
}
#: Platforms supported by a method
platforms = set() # type: Set[str]
#: The type of method, e.g. does it get the MAC of a interface?
#: Allowed values: {ip, ip4, ip6, iface, default_iface}
method_type = "" # type: str
#: If the method makes a network request as part of the check
network_request = False # type: bool
#: Marks the method as unable to be used, e.g. if there was a runtime
#: error indicating the method won't work on the current platform.
unusable = False # type: bool
def test(self): # type: () -> bool # noqa: T484
"""Low-impact test that the method is feasible, e.g. command exists."""
pass # pragma: no cover
# TODO: automatically clean MAC on return
def get(self, arg): # type: (str) -> Optional[str]
"""
Core logic of the method that performs the lookup.
.. warning::
If the method itself fails to function an exception will be raised!
(for instance, if some command arguments are invalid, or there's an
internal error with the command, or a bug in the code).
Args:
arg (str): What the method should get, such as an IP address
or interface name. In the case of default_iface methods,
this is not used and defaults to an empty string.
Returns:
Lowercase colon-separated MAC address, or None if one could
not be found.
"""
pass # pragma: no cover
@classmethod
def __str__(cls): # type: () -> str
return cls.__name__
# TODO(python3): do we want to keep this around? It calls 3 commands and is
# quite inefficient. We should just take the methods and use directly.
class UuidArpGetNode(Method):
platforms = {"linux", "darwin", "sunos", "other"}
method_type = "ip"
def test(self): # type: () -> bool
try:
from uuid import _arp_getnode # type: ignore
return True
except Exception:
return False
def get(self, arg): # type: (str) -> Optional[str]
from uuid import _arp_getnode # type: ignore
backup = socket.gethostbyname
try:
socket.gethostbyname = lambda x: arg # noqa: F841
mac1 = _arp_getnode()
if mac1 is not None:
mac1 = _uuid_convert(mac1)
mac2 = _arp_getnode()
mac2 = _uuid_convert(mac2)
if mac1 == mac2:
return mac1
except Exception:
raise
finally:
socket.gethostbyname = backup
return None
class ArpFile(Method):
platforms = {"linux"}
method_type = "ip4"
_path = os.environ.get("ARP_PATH", "/proc/net/arp") # type: str
def test(self): # type: () -> bool
return check_path(self._path)
def get(self, arg): # type: (str) -> Optional[str]
if not arg:
return None
data = _read_file(self._path)
if data is None:
self.unusable = True
return None
if data is not None and len(data) > 1:
# Need a space, otherwise a search for 192.168.16.2
# will match 192.168.16.254 if it comes first!
return _search(re.escape(arg) + r" .+" + MAC_RE_COLON, data)
return None
class ArpFreebsd(Method):
platforms = {"freebsd"}
method_type = "ip"
def test(self): # type: () -> bool
return check_command("arp")
def get(self, arg): # type: (str) -> Optional[str]
regex = r"\(" + re.escape(arg) + r"\)\s+at\s+" + MAC_RE_COLON
return _search(regex, _popen("arp", arg))
class ArpOpenbsd(Method):
platforms = {"openbsd"}
method_type = "ip"
_regex = r"[ ]+" + MAC_RE_COLON # type: str
def test(self): # type: () -> bool
return check_command("arp")
def get(self, arg): # type: (str) -> Optional[str]
return _search(re.escape(arg) + self._regex, _popen("arp", "-an"))
class ArpVariousArgs(Method):
platforms = {"linux", "darwin", "freebsd", "sunos", "other"}
method_type = "ip"
_regex_std = r"\)\s+at\s+" + MAC_RE_COLON # type: str
_regex_darwin = r"\)\s+at\s+" + MAC_RE_SHORT # type: str
_args = (
("", True), # "arp 192.168.1.1"
# Linux
("-an", False), # "arp -an"
("-an", True), # "arp -an 192.168.1.1"
# Darwin, WSL, Linux distros???
("-a", False), # "arp -a"
("-a", True), # "arp -a 192.168.1.1"
)
_args_tested = False # type: bool
_good_pair = () # type: Union[Tuple, Tuple[str, bool]]
def test(self): # type: () -> bool
return check_command("arp")
def get(self, arg): # type: (str) -> Optional[str]
if not arg:
return None
# Ensure output from testing command on first call isn't wasted
command_output = ""
# Test which arguments are valid to the command
# This will NOT test which regex is valid
if not self._args_tested:
for pair_to_test in self._args:
try:
cmd_args = [pair_to_test[0]]
# if True, then include IP as a command argument
if pair_to_test[1]:
cmd_args.append(arg)
command_output = _popen("arp", " ".join(cmd_args))
self._good_pair = pair_to_test
break
except CalledProcessError as ex:
if DEBUG:
log.debug(
"ArpVariousArgs pair test failed for (%s, %s): %s",
pair_to_test[0],
pair_to_test[1],
str(ex),
)
if not self._good_pair:
self.unusable = True
return None
self._args_tested = True
if not command_output:
# if True, then include IP as a command argument
cmd_args = [self._good_pair[0]]
if self._good_pair[1]:
cmd_args.append(arg)
command_output = _popen("arp", " ".join(cmd_args))
escaped = re.escape(arg)
_good_regex = (
self._regex_darwin if DARWIN or SOLARIS else self._regex_std
) # type: str
return _search(r"\(" + escaped + _good_regex, command_output)
class ArpExe(Method):
"""
Query the Windows ARP table using ``arp.exe`` to find the MAC address of a remote host.
This only works for IPv4, since the ARP table is IPv4-only.
Microsoft Documentation: `arp <https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/arp>`
""" # noqa: E501
platforms = {"windows", "wsl"}
method_type = "ip4"
def test(self): # type: () -> bool
# NOTE: specifying "arp.exe" instead of "arp" lets this work
# seamlessly on WSL1 as well. On WSL2 it doesn't matter, since
# it's basically just a Linux VM with some lipstick.
return check_command("arp.exe")
def get(self, arg): # type: (str) -> Optional[str]
return _search(MAC_RE_DASH, _popen("arp.exe", "-a %s" % arg))
class ArpingHost(Method):
"""
Use ``arping`` command to determine the MAC of a host.
Supports three variants of ``arping``
- "habets" arping by Thomas Habets
(`GitHub <https://github.com/ThomasHabets/arping>`__)
On Debian-based distros, ``apt install arping`` will install
Habets arping.
- "iputils" arping, from the ``iputils-arping``
`package <https://packages.debian.org/sid/iputils-arping>`__
- "busybox" arping, included with BusyBox (a small executable "distro")
(`further reading <https://boxmatrix.info/wiki/Property:arping>`__)
BusyBox's arping quite similar to iputils-arping. The arguments for
our purposes are the same, and the output is also the same.
There's even a TODO in busybox's arping code referencing iputils arping.
There are several differences:
- The return code from bad arguments is 1, not 2 like for iputils-arping
- The MAC address in output is lowercase (vs. uppercase in iputils-arping)
This was a pain to obtain samples for busybox on Windows. I recommend
using WSL and arping'ing the Docker gateway (for WSL2 distros).
NOTE: it must be run as root using ``sudo busybox arping``.
"""
platforms = {"linux", "darwin"}
method_type = "ip4"
network_request = True
_is_iputils = True # type: bool
_habets_args = "-r -C 1 -c 1 %s" # type: str
_iputils_args = "-f -c 1 %s" # type: str
def test(self): # type: () -> bool
return check_command("arping")
def get(self, arg): # type: (str) -> Optional[str]
# If busybox or iputils, this will just work, and if host ping fails,
# then it'll exit with code 1 and this function will return None.
#
# If it's Habets, then it'll exit code 1 and have "invalid option"
# and/or the help message in the output.
# In the case of Habets, set self._is_iputils to False,
# then re-try with Habets args.
try:
if self._is_iputils:
command_output = _popen("arping", self._iputils_args % arg)
if command_output:
return _search(
r" from %s \[(%s)\]" % (re.escape(arg), MAC_RE_COLON),
command_output,
)
else:
return self._call_habets(arg)
except CalledProcessError as ex:
if ex.output and self._is_iputils:
if not PY2 and isinstance(ex.output, bytes):
output = str(ex.output, "utf-8").lower()
else:
output = str(ex.output).lower()
if "habets" in output or "invalid option" in output:
if DEBUG:
log.debug("Falling back to Habets arping")
self._is_iputils = False
try:
return self._call_habets(arg)
except CalledProcessError:
pass
return None
def _call_habets(self, arg): # type: (str) -> Optional[str]
command_output = _popen("arping", self._habets_args % arg)
if command_output:
return command_output.strip()
else:
return None
class CtypesHost(Method):
"""
Uses ``SendARP`` from the Windows ``Iphlpapi`` to get the MAC address
of a remote IPv4 host.
Microsoft Documentation: `SendARP function (iphlpapi.h) <https://learn.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-sendarp>`__
""" # noqa: E501
platforms = {"windows"}
method_type = "ip4"
network_request = True
def test(self): # type: () -> bool
try:
return ctypes.windll.wsock32.inet_addr(b"127.0.0.1") > 0 # noqa: T484
except Exception:
return False
def get(self, arg): # type: (str) -> Optional[str]
if not PY2: # Convert to bytes on Python 3+ (Fixes GitHub issue #7)
arg = arg.encode() # type: ignore
try:
inetaddr = ctypes.windll.wsock32.inet_addr(arg) # type: ignore
if inetaddr in (0, -1):
raise Exception
except Exception:
# TODO: this assumes failure is due to arg being a hostname
# We should be explicit about only accepting ipv4 addresses
# and handle any hostname resolution in calling code
hostip = socket.gethostbyname(arg)
inetaddr = ctypes.windll.wsock32.inet_addr(hostip) # type: ignore
buffer = ctypes.c_buffer(6)
addlen = ctypes.c_ulong(ctypes.sizeof(buffer))
# https://docs.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-sendarp
send_arp = ctypes.windll.Iphlpapi.SendARP # type: ignore
if send_arp(inetaddr, 0, ctypes.byref(buffer), ctypes.byref(addlen)) != 0:
return None
# Convert binary data into a string.
macaddr = ""
for intval in struct.unpack("BBBBBB", buffer): # type: ignore
if intval > 15:
replacestr = "0x"
else:
replacestr = "x"
macaddr = "".join([macaddr, hex(intval).replace(replacestr, "")])
return macaddr
class IpNeighborShow(Method):
platforms = {"linux", "other"}
method_type = "ip" # IPv6 and IPv4
def test(self): # type: () -> bool
return check_command("ip")
def get(self, arg): # type: (str) -> Optional[str]
output = _popen("ip", "neighbor show %s" % arg)
if not output:
return None
try:
# NOTE: the space prevents accidental matching of partial IPs
return (
output.partition(arg + " ")[2].partition("lladdr")[2].strip().split()[0]
)
except IndexError as ex:
log.debug("IpNeighborShow failed with exception: %s", str(ex))
return None
class SysIfaceFile(Method):
platforms = {"linux", "wsl"}
method_type = "iface"
_path = "/sys/class/net/" # type: str
def test(self): # type: () -> bool
# Imperfect, but should work well enough
return check_path(self._path)
def get(self, arg): # type: (str) -> Optional[str]
data = _read_file(self._path + arg + "/address")
# NOTE: if "/sys/class/net/" exists, but interface file doesn't,
# then that means the interface doesn't exist
# Sometimes this can be empty or a single newline character
return None if data is not None and len(data) < 17 else data
class UuidLanscan(Method):
platforms = {"other"}
method_type = "iface"
def test(self): # type: () -> bool
try:
from uuid import _find_mac # noqa: T484
return check_command("lanscan")
except Exception:
return False
def get(self, arg): # type: (str) -> Optional[str]
from uuid import _find_mac # type: ignore
if not PY2:
arg = bytes(arg, "utf-8") # type: ignore
mac = _find_mac("lanscan", "-ai", [arg], lambda i: 0)
if mac:
return _uuid_convert(mac)
return None
class FcntlIface(Method):
platforms = {"linux", "wsl"}
method_type = "iface"
def test(self): # type: () -> bool
try:
import fcntl
return True
except Exception: # Broad except to handle unknown effects
return False
def get(self, arg): # type: (str) -> Optional[str]
import fcntl
if not PY2:
arg = arg.encode() # type: ignore
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 0x8927 = SIOCGIFADDR
info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack("256s", arg[:15]))
if PY2:
return ":".join(["%02x" % ord(char) for char in info[18:24]])
else:
return ":".join(["%02x" % ord(chr(char)) for char in info[18:24]])
class GetmacExe(Method):
"""
Uses Windows-builtin ``getmac.exe`` to get a interface's MAC address.
Microsoft Documentation: `getmac <https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/getmac>`__
""" # noqa: E501
platforms = {"windows"}
method_type = "iface"
_regexes = [
# Connection Name
(r"\r\n", r".*" + MAC_RE_DASH + r".*\r\n"),
# Network Adapter (the human-readable name)
(r"\r\n.*", r".*" + MAC_RE_DASH + r".*\r\n"),
] # type: List[Tuple[str, str]]
_champ = () # type: Union[tuple, Tuple[str, str]]
def test(self): # type: () -> bool
# NOTE: the scripts from this library (getmac) are excluded from the
# path used for checking variables, in getmac.getmac.PATH (defined
# at the top of this file). Otherwise, this would get messy quickly :)
return check_command("getmac.exe")
def get(self, arg): # type: (str) -> Optional[str]
try:
# /nh: Suppresses table headers
# /v: Verbose
command_output = _popen("getmac.exe", "/NH /V")
except CalledProcessError as ex:
# This shouldn't cause an exception if it's valid command
log.error("getmac.exe failed, marking unusable. Exception: %s", str(ex))
self.unusable = True
return None
if self._champ:
return _search(self._champ[0] + arg + self._champ[1], command_output)
for pair in self._regexes:
result = _search(pair[0] + arg + pair[1], command_output)
if result:
self._champ = pair
return result
return None
class IpconfigExe(Method):
"""
Uses ``ipconfig.exe`` to find interface MAC addresses on Windows.
This is generally pretty reliable and works across a wide array of
versions and releases. I'm not sure if it works pre-XP though.
Microsoft Documentation: `ipconfig <https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/ipconfig>`__
""" # noqa: E501
platforms = {"windows"}
method_type = "iface"
_regex = (
r"(?:\n?[^\n]*){1,8}Physical Address[ .:]+" + MAC_RE_DASH + r"\r\n"
) # type: str
def test(self): # type: () -> bool
return check_command("ipconfig.exe")
def get(self, arg): # type: (str) -> Optional[str]
return _search(arg + self._regex, _popen("ipconfig.exe", "/all"))
class WmicExe(Method):
"""
Use ``wmic.exe`` on Windows to find the MAC address of a network interface.
Microsoft Documentation: `wmic <https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/wmic>`__
.. warning::
WMIC is deprecated as of Windows 10 21H1. This method may not work on
Windows 11 and may stop working at some point on Windows 10 (unlikely,
but possible).
""" # noqa: E501
platforms = {"windows"}
method_type = "iface"
def test(self): # type: () -> bool
return check_command("wmic.exe")
def get(self, arg): # type: (str) -> Optional[str]
command_output = _popen(
"wmic.exe",
'nic where "NetConnectionID = \'%s\'" get "MACAddress" /value' % arg,
)
# Negative: "No Instance(s) Available"
# Positive: "MACAddress=00:FF:E7:78:95:A0"
# NOTE: .partition() always returns 3 parts,
# therefore it won't cause an IndexError
return command_output.strip().partition("=")[2]
class DarwinNetworksetupIface(Method):
"""
Use ``networksetup`` on MacOS (Darwin) to get the MAC address of a specific interface.
I think that this is or was a BSD utility, but I haven't seen it on other BSDs
(FreeBSD, OpenBSD, etc.). So, I'm treating it as a Darwin-specific utility
until further notice. If you know otherwise, please open a PR :)
If the command is present, it should always work, though naturally that is contingent
upon the whims of Apple in newer MacOS releases.
Man page: `networksetup (8) <https://www.manpagez.com/man/8/networksetup/>`__
"""
platforms = {"darwin"}
method_type = "iface"
def test(self): # type: () -> bool
return check_command("networksetup")
def get(self, arg): # type: (str) -> Optional[str]
command_output = _popen("networksetup", "-getmacaddress %s" % arg)
return _search(MAC_RE_COLON, command_output)
# This only took 15-20 hours of throwing my brain against a wall multiple times
# over the span of 1-2 years to figure out. It works for almost all conceivable
# output from "ifconfig", and probably netstat too. It can probably be made more
# efficient by someone who actually knows how to write regex.
# [: ]\s?(?:flags=|\s).*?(?:(?:\w+[: ]\s?flags=)|\s(?:ether|address|HWaddr|hwaddr|lladdr)[ :]?\s?([0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5})) # noqa: E501
IFCONFIG_REGEX = (
r"[: ]\s?(?:flags=|\s).*?(?:"
r"(?:\w+[: ]\s?flags=)|" # Prevent interfaces w/o a MAC from matching
r"\s(?:ether|address|HWaddr|hwaddr|lladdr)[ :]?\s?" # Handle various prefixes
r"([0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5}))" # Match the MAC
)
def _parse_ifconfig(iface, command_output):
# type: (str, str) -> Optional[str]
if not iface or not command_output:
return None
# Sanity check on input e.g. if user does "eth0:" as argument
iface = iface.strip(":")
# "(?:^|\s)": prevent an input of "h0" from matching on "eth0"
search_re = r"(?:^|\s)" + iface + IFCONFIG_REGEX
return _search(search_re, command_output, flags=re.DOTALL)
class IfconfigWithIfaceArg(Method):
"""
``ifconfig`` command with the interface name as an argument
(e.g. ``ifconfig eth0``).
"""
platforms = {"linux", "wsl", "freebsd", "openbsd", "other"}
method_type = "iface"
def test(self): # type: () -> bool
return check_command("ifconfig")
def get(self, arg): # type: (str) -> Optional[str]
try:
command_output = _popen("ifconfig", arg)
except CalledProcessError as err:
# Return code of 1 means interface doesn't exist
if err.returncode == 1:
return None
else:
raise err # this will cause another method to be used
return _parse_ifconfig(arg, command_output)
# TODO: combine this with IfconfigWithArg/IfconfigNoArg
# (need to do live testing on Darwin)
class IfconfigEther(Method):
platforms = {"darwin"}
method_type = "iface"
_tested_arg = False # type: bool
_iface_arg = False # type: bool
def test(self): # type: () -> bool
return check_command("ifconfig")
def get(self, arg): # type: (str) -> Optional[str]
# Check if this version of "ifconfig" accepts an interface argument
command_output = ""
if not self._tested_arg:
try:
command_output = _popen("ifconfig", arg)
self._iface_arg = True
except CalledProcessError:
self._iface_arg = False
self._tested_arg = True
if self._iface_arg and not command_output: # Don't repeat work on first run
command_output = _popen("ifconfig", arg)
else:
command_output = _popen("ifconfig", "")
return _parse_ifconfig(arg, command_output)
# TODO: create new methods, IfconfigNoArgs and IfconfigVariousArgs
# TODO: unit tests
class IfconfigOther(Method):
"""
Wild 'Shot in the Dark' attempt at ``ifconfig`` for unknown platforms.
"""
platforms = {"linux", "other"}
method_type = "iface"
# "-av": Tru64 system?
_args = (
("", (r"(?::| ).*?\sether\s", r"(?::| ).*?\sHWaddr\s")),
("-a", r".*?HWaddr\s"),
("-v", r".*?HWaddr\s"),
("-av", r".*?Ether\s"),
)
_args_tested = False # type: bool
_good_pair = [] # type: List[Union[str, Tuple[str, str]]]
def test(self): # type: () -> bool
return check_command("ifconfig")
def get(self, arg): # type: (str) -> Optional[str]
if not arg:
return None
# Cache output from testing command so first call isn't wasted
command_output = ""
# Test which arguments are valid to the command
if not self._args_tested:
for pair_to_test in self._args:
try:
command_output = _popen("ifconfig", pair_to_test[0])
self._good_pair = list(pair_to_test) # noqa: T484
if isinstance(self._good_pair[1], str):
self._good_pair[1] += MAC_RE_COLON
break
except CalledProcessError as ex:
if DEBUG:
log.debug(
"IfconfigOther pair test failed for (%s, %s): %s",
pair_to_test[0],
pair_to_test[1],
str(ex),
)
if not self._good_pair:
self.unusable = True
return None
self._args_tested = True
if not command_output and isinstance(self._good_pair[0], str):
command_output = _popen("ifconfig", self._good_pair[0])
# Handle the two possible search terms
if isinstance(self._good_pair[1], tuple):
for term in self._good_pair[1]:
regex = term + MAC_RE_COLON
result = _search(re.escape(arg) + regex, command_output)
if result:
# changes type from tuple to str, so the else statement
# will be hit on the next call to this method
self._good_pair[1] = regex
return result
return None
else:
return _search(re.escape(arg) + self._good_pair[1], command_output)
class NetstatIface(Method):
platforms = {"linux", "wsl", "other"}
method_type = "iface"
# ".*?": non-greedy
# https://docs.python.org/3/howto/regex.html#greedy-versus-non-greedy
_regexes = [
r": .*?ether " + MAC_RE_COLON,
r": .*?HWaddr " + MAC_RE_COLON,
# Ubuntu 12.04 and other older kernels
r" .*?Link encap:Ethernet HWaddr " + MAC_RE_COLON,
] # type: List[str]
_working_regex = "" # type: str
def test(self): # type: () -> bool
return check_command("netstat")
# TODO: consolidate the parsing logic between IfconfigOther and netstat
def get(self, arg): # type: (str) -> Optional[str]
# NOTE: netstat and ifconfig pull from the same kernel source and
# therefore have the same output format on the same platform.
command_output = _popen("netstat", "-iae")
if not command_output:
log.warning("no netstat output, marking unusable")
self.unusable = True
return None
if self._working_regex:
# Use regex that worked previously. This can still return None in
# the case of interface not existing, but at least it's a bit faster.
return _search(arg + self._working_regex, command_output, flags=re.DOTALL)
# See if either regex matches
for regex in self._regexes:
result = _search(arg + regex, command_output, flags=re.DOTALL)
if result:
self._working_regex = regex
return result
return None
# TODO: Add to IpLinkIface
# TODO: New method for "ip addr"? (this would be useful for CentOS and others as a fallback)
# (r"state UP.*\n.*ether " + MAC_RE_COLON, 0, "ip", ["link","addr"]),
# (r"wlan.*\n.*ether " + MAC_RE_COLON, 0, "ip", ["link","addr"]),
# (r"ether " + MAC_RE_COLON, 0, "ip", ["link","addr"]),
# _regexes = (
# r".*\n.*link/ether " + MAC_RE_COLON,
# # Android 6.0.1+ (and likely other platforms as well)
# r"state UP.*\n.*ether " + MAC_RE_COLON,
# r"wlan.*\n.*ether " + MAC_RE_COLON,
# r"ether " + MAC_RE_COLON,
# ) # type: Tuple[str, str, str, str]
class IpLinkIface(Method):
platforms = {"linux", "wsl", "android", "other"}
method_type = "iface"
_regex = r".*\n.*link/ether " + MAC_RE_COLON # type: str
_tested_arg = False # type: bool
_iface_arg = False # type: bool
def test(self): # type: () -> bool
return check_command("ip")
def get(self, arg): # type: (str) -> Optional[str]
# Check if this version of "ip link" accepts an interface argument
# Not accepting one is a quirk of older versions of 'iproute2'
# TODO: is it "ip link <arg>" on some platforms and "ip link show <arg>" on others?
command_output = ""
if not self._tested_arg:
try:
command_output = _popen("ip", "link show " + arg)
self._iface_arg = True
except CalledProcessError as err:
# Output: 'Command "eth0" is unknown, try "ip link help"'
if err.returncode != 255:
raise err
self._tested_arg = True
if self._iface_arg:
if not command_output: # Don't repeat work on first run
command_output = _popen("ip", "link show " + arg)
return _search(arg + self._regex, command_output)
else:
# TODO: improve this regex to not need extra portion for no arg
command_output = _popen("ip", "link")
return _search(arg + r":" + self._regex, command_output)
class DefaultIfaceLinuxRouteFile(Method):
"""
Get the default interface by parsing the ``/proc/net/route`` file.
This is the same source as the ``route`` command, however it's much
faster to read this file than to call ``route``. If it fails for whatever
reason, we can fall back on the system commands (e.g for a platform that
has a route command, but doesn't use ``/proc``, such as BSD-based platforms).
"""
platforms = {"linux", "wsl"}
method_type = "default_iface"
def test(self): # type: () -> bool
return check_path("/proc/net/route")
def get(self, arg=""): # type: (str) -> Optional[str]
data = _read_file("/proc/net/route")
if data is not None and len(data) > 1:
for line in data.split("\n")[1:-1]:
line = line.strip()
if not line:
continue
# Some have tab separators, some have spaces
if "\t" in line:
sep = "\t"
else:
sep = " "
iface_name, dest = line.split(sep)[:2]
if dest == "00000000":
return iface_name
if DEBUG:
log.debug(
"Failed to find default interface in data from "
"'/proc/net/route', no destination of '00000000' was found"
)
elif DEBUG:
log.warning("No data from /proc/net/route")
return None
class DefaultIfaceRouteCommand(Method):
platforms = {"linux", "wsl", "other"}
method_type = "default_iface"
def test(self): # type: () -> bool
return check_command("route")
def get(self, arg=""): # type: (str) -> Optional[str]
output = _popen("route", "-n")
try:
return output.partition("0.0.0.0")[2].partition("\n")[0].split()[-1]
except IndexError as ex: # index errors means no default route in output?
log.debug("DefaultIfaceRouteCommand failed for %s: %s", arg, str(ex))
return None
class DefaultIfaceRouteGetCommand(Method):
platforms = {"darwin", "freebsd", "other"}
method_type = "default_iface"
def test(self): # type: () -> bool
return check_command("route")
def get(self, arg=""): # type: (str) -> Optional[str]
output = _popen("route", "get default")
if not output:
return None
try:
return output.partition("interface: ")[2].strip().split()[0].strip()
except IndexError as ex:
log.debug("DefaultIfaceRouteCommand failed for %s: %s", arg, str(ex))
return None
class DefaultIfaceIpRoute(Method):
# NOTE: this is slightly faster than "route" since
# there is less output than "route -n"
platforms = {"linux", "wsl", "other"}
method_type = "default_iface"
def test(self): # type: () -> bool
return check_command("ip")
def get(self, arg=""): # type: (str) -> Optional[str]
output = _popen("ip", "route list 0/0")
if not output:
if DEBUG:
log.debug("DefaultIfaceIpRoute failed: no output")
return None
return output.partition("dev")[2].partition("proto")[0].strip()
class DefaultIfaceOpenBsd(Method):
platforms = {"openbsd"}
method_type = "default_iface"
def test(self): # type: () -> bool
return check_command("route")
def get(self, arg=""): # type: (str) -> Optional[str]
output = _popen("route", "-nq show -inet -gateway -priority 1")
return output.partition("127.0.0.1")[0].strip().rpartition(" ")[2]
class DefaultIfaceFreeBsd(Method):
platforms = {"freebsd"}
method_type = "default_iface"
def test(self): # type: () -> bool
return check_command("netstat")
def get(self, arg=""): # type: (str) -> Optional[str]
output = _popen("netstat", "-r")
return _search(r"default[ ]+\S+[ ]+\S+[ ]+(\S+)[\r\n]+", output)
# TODO: order methods by effectiveness/reliability
# Use a class attribute maybe? e.g. "score", then sort by score in cache
METHODS = [
# NOTE: CtypesHost is faster than ArpExe because of sub-process startup times :)
CtypesHost,
ArpFile,
ArpingHost,
SysIfaceFile,
FcntlIface,
UuidLanscan,
GetmacExe,
IpconfigExe,
WmicExe,
ArpExe,
DarwinNetworksetupIface,
ArpFreebsd,
ArpOpenbsd,
IfconfigWithIfaceArg,
IfconfigEther,
IfconfigOther,
IpLinkIface,
NetstatIface,
IpNeighborShow,
ArpVariousArgs,
UuidArpGetNode,
DefaultIfaceLinuxRouteFile,
DefaultIfaceIpRoute,
DefaultIfaceRouteCommand,
DefaultIfaceRouteGetCommand,
DefaultIfaceOpenBsd,
DefaultIfaceFreeBsd,
] # type: List[Type[Method]]
# Primary method to use for a given method type
METHOD_CACHE = {
"ip4": None,
"ip6": None,
"iface": None,
"default_iface": None,
} # type: Dict[str, Optional[Method]]
# Order of methods is determined by:
# Platform + version
# Performance (file read > command)
# Reliability (how well I know/understand the command to work)
FALLBACK_CACHE = {
"ip4": [],
"ip6": [],
"iface": [],
"default_iface": [],
} # type: Dict[str, List[Method]]
DEFAULT_IFACE = "" # type: str
def get_method_by_name(method_name):
# type: (str) -> Optional[Type[Method]]
for method in METHODS:
if method.__name__.lower() == method_name.lower():
return method
return None
def get_instance_from_cache(method_type, method_name):
# type: (str, str) -> Optional[Method]
"""
Get the class for a named Method from the caches.
METHOD_CACHE is checked first, and if that fails,
then any entries in FALLBACK_CACHE are checked.
If both fail, None is returned.
Args:
method_type: method type to initialize the cache for.
Allowed values are: ``ip4`` | ``ip6`` | ``iface`` | ``default_iface``
"""
if str(METHOD_CACHE[method_type]) == method_name:
return METHOD_CACHE[method_type]
for f_meth in FALLBACK_CACHE[method_type]:
if str(f_meth) == method_name:
return f_meth
return None
def _swap_method_fallback(method_type, swap_with):
# type: (str, str) -> bool
if str(METHOD_CACHE[method_type]) == swap_with:
return True
found = None # type: Optional[Method]
for f_meth in FALLBACK_CACHE[method_type]:
if str(f_meth) == swap_with:
found = f_meth
break
if not found:
return False
curr = METHOD_CACHE[method_type]
FALLBACK_CACHE[method_type].remove(found)
METHOD_CACHE[method_type] = found
FALLBACK_CACHE[method_type].insert(0, curr) # noqa: T484
return True
def _warn_critical(err_msg):
# type: (str) -> None
log.critical(err_msg)
warnings.warn(
"%s. NOTICE: this warning will likely turn into a raised exception in getmac 1.0.0!"
% err_msg,
RuntimeWarning,
)
def initialize_method_cache(
method_type, network_request=True
): # type: (str, bool) -> bool
"""
Initialize the method cache for the given method type.
Args:
method_type: method type to initialize the cache for.
Allowed values are: ``ip4`` | ``ip6`` | ``iface`` | ``default_iface``
network_request: if methods that make network requests should be included
(those methods that have the attribute ``network_request`` set to ``True``)
"""
if METHOD_CACHE.get(method_type):
if DEBUG:
log.debug(
"Method cache already initialized for method type '%s'", method_type
)
return True
log.debug("Initializing '%s' method cache (platform: '%s')", method_type, PLATFORM)
if OVERRIDE_PLATFORM:
log.warning(
"Platform override is set, using '%s' as platform "
"instead of detected platform '%s'",
OVERRIDE_PLATFORM,
PLATFORM,
)
platform = OVERRIDE_PLATFORM
else:
platform = PLATFORM
if DEBUG >= 4:
meth_strs = ", ".join(m.__name__ for m in METHODS) # type: str
log.debug("%d methods available: %s", len(METHODS), meth_strs)
# Filter methods by the type of MAC we're looking for, such as "ip"
# for remote host methods or "iface" for local interface methods.
type_methods = [
method
for method in METHODS
if (method.method_type != "ip" and method.method_type == method_type)
# Methods with a type of "ip" can handle both IPv4 and IPv6
or (method.method_type == "ip" and method_type in ["ip4", "ip6"])
] # type: List[Type[Method]]
if not type_methods:
_warn_critical("No valid methods matching MAC type '%s'" % method_type)
return False
if DEBUG >= 2:
type_strs = ", ".join(tm.__name__ for tm in type_methods) # type: str
log.debug(
"%d type-filtered methods for '%s': %s",
len(type_methods),
method_type,
type_strs,
)
# Filter methods by the platform we're running on
platform_methods = [
method for method in type_methods if platform in method.platforms
] # type: List[Type[Method]]
if not platform_methods:
# If there isn't a method for the current platform,
# then fallback to the generic platform "other".
warn_msg = (
"No methods for platform '%s'! Your system may not be supported. "
"Falling back to platform 'other'." % platform
)
log.warning(warn_msg)
warnings.warn(warn_msg, RuntimeWarning)
platform_methods = [
method for method in type_methods if "other" in method.platforms
]
if DEBUG >= 2:
plat_strs = ", ".join(pm.__name__ for pm in platform_methods) # type: str
log.debug(
"%d platform-filtered methods for '%s' (method_type='%s'): %s",
len(platform_methods),
platform,
method_type,
plat_strs,
)
if not platform_methods:
_warn_critical(
"No valid methods found for MAC type '%s' and platform '%s'"
% (method_type, platform)
)
return False
filtered_methods = platform_methods # type: List[Type[Method]]
# If network_request is False, then remove any methods that have network_request=True
if not network_request:
filtered_methods = [m for m in platform_methods if not m.network_request]
# Determine which methods work on the current system
tested_methods = [] # type: List[Method]
for method_class in filtered_methods:
method_instance = method_class() # type: Method
try:
test_result = method_instance.test() # type: bool
except Exception:
test_result = False
if test_result:
tested_methods.append(method_instance)
# First successful test goes in the cache
if not METHOD_CACHE[method_type]:
METHOD_CACHE[method_type] = method_instance
elif DEBUG:
log.debug("Test failed for method '%s'", str(method_instance))
if not tested_methods:
_warn_critical(
"All %d '%s' methods failed to test!" % (len(filtered_methods), method_type)
)
return False
if DEBUG >= 2:
tested_strs = ", ".join(str(ts) for ts in tested_methods) # type: str
log.debug(
"%d tested methods for '%s': %s",
len(tested_methods),
method_type,
tested_strs,
)
# Populate fallback cache with all the tested methods, minus the currently active method
if METHOD_CACHE[method_type] and METHOD_CACHE[method_type] in tested_methods:
tested_methods.remove(METHOD_CACHE[method_type]) # noqa: T484
FALLBACK_CACHE[method_type] = tested_methods
if DEBUG:
log.debug(
"Current method cache: %s",
str({k: str(v) for k, v in METHOD_CACHE.items()}),
)
log.debug(
"Current fallback cache: %s",
str({k: str(v) for k, v in FALLBACK_CACHE.items()}),
)
log.debug("Finished initializing '%s' method cache", method_type)
return True
def _remove_unusable(method, method_type): # type: (Method, str) -> Optional[Method]
if not FALLBACK_CACHE[method_type]:
log.warning("No fallback method for unusable method '%s'!", str(method))
METHOD_CACHE[method_type] = None
else:
METHOD_CACHE[method_type] = FALLBACK_CACHE[method_type].pop(0)
log.warning(
"Falling back to '%s' for unusable method '%s'",
str(METHOD_CACHE[method_type]),
str(method),
)
return METHOD_CACHE[method_type]
def _attempt_method_get(
method, method_type, arg
): # type: (Method, str, str) -> Optional[str]
"""
Attempt to use methods, and if they fail, fallback to the next method in the cache.
"""
if not METHOD_CACHE[method_type] and not FALLBACK_CACHE[method_type]:
_warn_critical("No usable methods found for MAC type '%s'" % method_type)
return None
if DEBUG:
log.debug(
"Attempting get() (method='%s', method_type='%s', arg='%s')",
str(method),
method_type,
arg,
)
result = None
try:
result = method.get(arg)
except CalledProcessError as ex:
# Don't mark return code 1 on a process as unusable!
# Example of return code 1 on ifconfig from WSL:
# Blake:goesc$ ifconfig eth8
# eth8: error fetching interface information: Device not found
# Blake:goesc$ echo $?
# 1
# Methods where an exit code of 1 makes it invalid should handle the
# CalledProcessError, inspect the return code, and set self.unusable = True
if ex.returncode != 1:
log.warning(
"Cached Method '%s' failed for '%s' lookup with process exit "
"code '%d' != 1, marking unusable. Exception: %s",
str(method),
method_type,
ex.returncode,
str(ex),
)
method.unusable = True
except Exception as ex:
log.warning(
"Cached Method '%s' failed for '%s' lookup with unhandled exception: %s",
str(method),
method_type,
str(ex),
)
method.unusable = True
# When an unhandled exception occurs (or exit code other than 1), remove
# the method from the cache and reinitialize with next candidate.
if not result and method.unusable:
new_method = _remove_unusable(method, method_type)
if not new_method:
return None
return _attempt_method_get(new_method, method_type, arg)
return result
def get_by_method(method_type, arg="", network_request=True):
# type: (str, str, bool) -> Optional[str]
"""
Query for a MAC using a specific method.
Args:
method_type: the type of lookup being performed.
Allowed values are: ``ip4``, ``ip6``, ``iface``, ``default_iface``
arg: Argument to pass to the method, e.g. an interface name or IP address
network_request: if methods that make network requests should be included
(those methods that have the attribute ``network_request`` set to ``True``)
"""
if not arg and method_type != "default_iface":
log.error("Empty arg for method '%s' (raw value: %s)", method_type, repr(arg))
return None
if FORCE_METHOD:
log.warning(
"Forcing method '%s' to be used for '%s' lookup (arg: '%s')",
FORCE_METHOD,
method_type,
arg,
)
forced_method = get_method_by_name(FORCE_METHOD)
if not forced_method:
log.error("Invalid FORCE_METHOD method name '%s'", FORCE_METHOD)
return None
return forced_method().get(arg)
method = METHOD_CACHE.get(method_type) # type: Optional[Method]
if not method:
# Initialize the cache if it hasn't been already
if not initialize_method_cache(method_type, network_request):
log.error(
"Failed to initialize method cache for method '%s' (arg: '%s')",
method_type,
arg,
)
return None
method = METHOD_CACHE[method_type]
if not method:
log.error(
"Initialization failed for method '%s'. It may not be supported "
"on this platform or another issue occurred.",
method_type,
)
return None
# TODO: add a "net_ok" argument, check network_request attribute
# on method in CACHE, if not then keep checking for method in
# FALLBACK_CACHE that has network_request.
result = _attempt_method_get(method, method_type, arg)
# Log normal get() failures if debugging is enabled
if DEBUG and not result:
log.debug("Method '%s' failed for '%s' lookup", str(method), method_type)
return result
def get_mac_address( # noqa: C901
interface=None, ip=None, ip6=None, hostname=None, network_request=True
):
# type: (Optional[str], Optional[str], Optional[str], Optional[str], bool) -> Optional[str]
"""
Get an Unicast IEEE 802 MAC-48 address from a local interface or remote host.
Only ONE of the first four arguments may be used
(``interface``,``ip``, ``ip6``, or ``hostname``).
If none of the arguments are selected, the default network interface for
the system will be used.
.. warning::
In getmac 1.0.0, exceptions will be raised if the method cache initialization fails
(in other words, if there are no valid methods found for the type of MAC requested).
.. warning::
You MUST provide :class:`str` typed arguments, REGARDLESS of Python version
.. note::
``"localhost"`` or ``"127.0.0.1"`` will always return ``"00:00:00:00:00:00"``
.. note::
It is assumed that you are using Ethernet or Wi-Fi. While other protocols
such as Bluetooth may work, this has not been tested and should not be
relied upon. If you need this functionality, please open an issue
(or better yet, a Pull Request ;))!
.. note::
Exceptions raised by methods are handled silently and returned as :obj:`None`.
Args:
interface (str): Name of a local network interface (e.g "Ethernet 3", "eth0", "ens32")
ip (str): Canonical dotted decimal IPv4 address of a remote host (e.g ``192.168.0.1``)
ip6 (str): Canonical shortened IPv6 address of a remote host (e.g ``ff02::1:ffe7:7f19``)
hostname (str): DNS hostname of a remote host (e.g "router1.mycorp.com", "localhost")
network_request (bool): If network requests should be made when attempting to find the
MAC of a remote host. If the ``arping`` command is available, this will be used.
If not, a UDP packet will be sent to the remote host to populate
the ARP/NDP tables for IPv4/IPv6. The port this packet is sent to can
be configured using the module variable ``getmac.PORT``.
Returns:
Lowercase colon-separated MAC address, or :obj:`None` if one could not be
found or there was an error.
""" # noqa: E501
if DEBUG:
import timeit
start_time = timeit.default_timer()
if PY2 or (sys.version_info[0] == 3 and sys.version_info[1] < 7):
global WARNED_UNSUPPORTED_PYTHONS
if not WARNED_UNSUPPORTED_PYTHONS:
warning_string = (
"Support for Python versions < 3.7 is deprecated and will be "
"removed in getmac 1.0.0. If you are stuck on an unsupported "
"Python, considor loosely pinning the version of this package "
'in your dependency list, e.g. "getmac<1.0.0" or "getmac~=0.9.0".'
)
warnings.warn(warning_string, DeprecationWarning)
log.warning(warning_string) # Ensure it appears in any logs
WARNED_UNSUPPORTED_PYTHONS = True
if (hostname and hostname == "localhost") or (ip and ip == "127.0.0.1"):
return "00:00:00:00:00:00"
# Resolve hostname to an IP address
if hostname:
# Exceptions will be handled silently and returned as a None
try:
# TODO: can this return a IPv6 address? If so, handle that!
ip = socket.gethostbyname(hostname)
except Exception as ex:
log.error("Could not resolve hostname '%s': %s", hostname, ex)
if DEBUG:
log.debug(traceback.format_exc())
return None
if ip6:
if not socket.has_ipv6:
log.error(
"Cannot get the MAC address of a IPv6 host: "
"IPv6 is not supported on this system"
)
return None
elif ":" not in ip6:
log.error("Invalid IPv6 address (no ':'): %s", ip6)
return None
mac = None
if network_request and (ip or ip6):
send_udp_packet = True # type: bool
# If IPv4, use ArpingHost or CtypesHost if they're available instead
# of populating the ARP table. This provides more reliable results
# and a ARP packet is lower impact than a UDP packet.
if ip:
if not METHOD_CACHE["ip4"]:
initialize_method_cache("ip4", network_request)
# If ArpFile succeeds, just use that, since it's
# significantly faster than arping (file read vs.
# spawning a process).
if not FORCE_METHOD or FORCE_METHOD.lower() == "arpfile":
af_meth = get_instance_from_cache("ip4", "ArpFile")
if af_meth:
mac = _attempt_method_get(af_meth, "ip4", ip)
# TODO: add tests for this logic (arpfile => fallback)
# This seems to be a common course of GitHub issues,
# so fixing it for good and adding robust tests is
# probably a good idea.
if not mac:
for arp_meth in ["CtypesHost", "ArpingHost"]:
if FORCE_METHOD and FORCE_METHOD.lower() != arp_meth:
continue
if arp_meth == str(METHOD_CACHE["ip4"]):
send_udp_packet = False
break
elif any(
arp_meth == str(x) for x in FALLBACK_CACHE["ip4"]
) and _swap_method_fallback("ip4", arp_meth):
send_udp_packet = False
break
# Populate the ARP table by sending an empty UDP packet to a high port
if send_udp_packet and not mac:
if DEBUG:
log.debug(
"Attempting to populate ARP table with UDP packet to %s:%d",
ip if ip else ip6,
PORT,
)
if ip:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
else:
sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
try:
if ip:
sock.sendto(b"", (ip, PORT))
else:
sock.sendto(b"", (ip6, PORT))
except Exception:
log.error("Failed to send ARP table population packet")
if DEBUG:
log.debug(traceback.format_exc())
finally:
sock.close()
elif DEBUG:
log.debug(
"Not sending UDP packet, using network request method '%s' instead",
str(METHOD_CACHE["ip4"]),
)
# Setup the address hunt based on the arguments specified
if not mac:
if ip6:
mac = get_by_method("ip6", ip6)
elif ip:
mac = get_by_method("ip4", ip)
elif interface:
mac = get_by_method("iface", interface)
else: # Default to searching for interface
# Default to finding MAC of the interface with the default route
if WINDOWS and network_request:
default_iface_ip = _fetch_ip_using_dns()
mac = get_by_method("ip4", default_iface_ip)
elif WINDOWS:
# TODO: implement proper default interface detection on windows
# (add a Method subclass to implement DefaultIface on Windows)
mac = get_by_method("iface", "Ethernet")
else:
global DEFAULT_IFACE
if not DEFAULT_IFACE:
DEFAULT_IFACE = get_by_method("default_iface") # noqa: T484
if DEFAULT_IFACE:
DEFAULT_IFACE = str(DEFAULT_IFACE).strip()
# TODO: better fallback if default iface lookup fails
if not DEFAULT_IFACE and BSD:
DEFAULT_IFACE = "em0"
elif not DEFAULT_IFACE and DARWIN: # OSX, maybe?
DEFAULT_IFACE = "en0"
elif not DEFAULT_IFACE:
DEFAULT_IFACE = "eth0"
mac = get_by_method("iface", DEFAULT_IFACE)
# TODO: hack to fallback to loopback if lookup fails
if not mac:
mac = get_by_method("iface", "lo")
log.debug("Raw MAC found: %s", mac)
# Log how long it took
if DEBUG:
duration = timeit.default_timer() - start_time
log.debug("getmac took %.4f seconds", duration)
return _clean_mac(mac)