zscams 2.0.1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. zscams/__init__.py +0 -0
  2. zscams/__main__.py +101 -0
  3. zscams/agent/__init__.py +0 -0
  4. zscams/agent/certificates/.gitkeep +0 -0
  5. zscams/agent/config.yaml +121 -0
  6. zscams/agent/keys/autoport.key +27 -0
  7. zscams/agent/src/__init__.py +0 -0
  8. zscams/agent/src/core/__init__.py +1 -0
  9. zscams/agent/src/core/api/backend/bootstrap.py +21 -0
  10. zscams/agent/src/core/api/backend/client.py +242 -0
  11. zscams/agent/src/core/api/backend/exceptions.py +10 -0
  12. zscams/agent/src/core/api/backend/update_machine_info.py +16 -0
  13. zscams/agent/src/core/prerequisites.py +36 -0
  14. zscams/agent/src/core/service_health_check.py +49 -0
  15. zscams/agent/src/core/services.py +86 -0
  16. zscams/agent/src/core/tunnel/__init__.py +144 -0
  17. zscams/agent/src/core/tunnel/tls.py +56 -0
  18. zscams/agent/src/core/tunnels.py +55 -0
  19. zscams/agent/src/services/__init__.py +0 -0
  20. zscams/agent/src/services/reverse_ssh.py +73 -0
  21. zscams/agent/src/services/ssh_forwarder.py +75 -0
  22. zscams/agent/src/services/system_monitor.py +264 -0
  23. zscams/agent/src/support/__init__.py +0 -0
  24. zscams/agent/src/support/configuration.py +53 -0
  25. zscams/agent/src/support/filesystem.py +41 -0
  26. zscams/agent/src/support/logger.py +40 -0
  27. zscams/agent/src/support/mac.py +18 -0
  28. zscams/agent/src/support/network.py +49 -0
  29. zscams/agent/src/support/openssl.py +100 -0
  30. zscams/libs/getmac/__init__.py +3 -0
  31. zscams/libs/getmac/__main__.py +123 -0
  32. zscams/libs/getmac/getmac.py +1900 -0
  33. zscams/libs/getmac/shutilwhich.py +67 -0
  34. zscams-2.0.1.dist-info/METADATA +118 -0
  35. zscams-2.0.1.dist-info/RECORD +37 -0
  36. zscams-2.0.1.dist-info/WHEEL +4 -0
  37. zscams-2.0.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,1900 @@
1
+ # -*- coding: utf-8 -*-
2
+ # http://web.archive.org/web/20140718071917/http://multivax.com/last_question.html
3
+
4
+ """
5
+ Get the MAC address of remote hosts or network interfaces.
6
+
7
+ It provides a platform-independent interface to get the MAC addresses of:
8
+
9
+ - System network interfaces (by interface name)
10
+ - Remote hosts on the local network (by IPv4/IPv6 address or hostname)
11
+
12
+ It provides one function: ``get_mac_address()``
13
+
14
+ .. code-block:: python
15
+ :caption: Examples
16
+
17
+ from getmac import get_mac_address
18
+ eth_mac = get_mac_address(interface="eth0")
19
+ win_mac = get_mac_address(interface="Ethernet 3")
20
+ ip_mac = get_mac_address(ip="192.168.0.1")
21
+ ip6_mac = get_mac_address(ip6="::1")
22
+ host_mac = get_mac_address(hostname="localhost")
23
+ updated_mac = get_mac_address(ip="10.0.0.1", network_request=True)
24
+
25
+ """
26
+ import ctypes
27
+ import logging
28
+ import os
29
+ import platform
30
+ import re
31
+ import shlex
32
+ import socket
33
+ import struct
34
+ import sys
35
+ import traceback
36
+ import warnings
37
+ from subprocess import CalledProcessError, check_output
38
+
39
+ try: # Python 3
40
+ from subprocess import DEVNULL # type: ignore
41
+ except ImportError: # Python 2
42
+ DEVNULL = open(os.devnull, "wb") # type: ignore
43
+
44
+ # Used for mypy (a data type analysis tool)
45
+ # If you're copying the code, this section can be safely removed
46
+ try:
47
+ from typing import TYPE_CHECKING
48
+
49
+ if TYPE_CHECKING:
50
+ from typing import Dict, List, Optional, Set, Tuple, Type, Union
51
+ except ImportError:
52
+ pass
53
+
54
+ # Configure logging
55
+ log = logging.getLogger("getmac") # type: logging.Logger
56
+ if not log.handlers:
57
+ log.addHandler(logging.NullHandler())
58
+
59
+ __version__ = "0.9.5"
60
+
61
+ PY2 = sys.version_info[0] == 2 # type: bool
62
+
63
+ # Configurable settings
64
+ DEBUG = 0 # type: int
65
+ PORT = 55555 # type: int
66
+
67
+ # Monkeypatch shutil.which for python 2.7 (TODO(python3): remove shutilwhich.py)
68
+ if PY2:
69
+ from .shutilwhich import which
70
+ else:
71
+ from shutil import which
72
+
73
+ # Platform identifiers
74
+ if PY2:
75
+ _UNAME = platform.uname() # type: Tuple[str, str, str, str, str, str]
76
+ _SYST = _UNAME[0] # type: str
77
+ else:
78
+ _UNAME = platform.uname() # type: platform.uname_result
79
+ _SYST = _UNAME.system # type: str
80
+ if _SYST == "Java":
81
+ try:
82
+ import java.lang
83
+
84
+ _SYST = str(java.lang.System.getProperty("os.name"))
85
+ except ImportError:
86
+ _java_err_msg = "Can't determine OS: couldn't import java.lang on Jython"
87
+ log.critical(_java_err_msg)
88
+ warnings.warn(_java_err_msg, RuntimeWarning)
89
+
90
+ WINDOWS = _SYST == "Windows" # type: bool
91
+ DARWIN = _SYST == "Darwin" # type: bool
92
+
93
+ OPENBSD = _SYST == "OpenBSD" # type: bool
94
+ FREEBSD = _SYST == "FreeBSD" # type: bool
95
+ NETBSD = _SYST == "NetBSD" # type: bool
96
+ SOLARIS = _SYST == "SunOS" # type: bool
97
+
98
+ # Not including Darwin or Solaris as a "BSD"
99
+ BSD = OPENBSD or FREEBSD or NETBSD # type: bool
100
+
101
+ # Windows Subsystem for Linux (WSL)
102
+ WSL = False # type: bool
103
+ LINUX = False # type: bool
104
+ if _SYST == "Linux":
105
+ if "Microsoft" in platform.version():
106
+ WSL = True
107
+ else:
108
+ LINUX = True
109
+
110
+ # NOTE: "Linux" methods apply to Android without modifications
111
+ # If there's Android-specific stuff then we can add a platform
112
+ # identifier for it.
113
+ ANDROID = (
114
+ hasattr(sys, "getandroidapilevel") or "ANDROID_STORAGE" in os.environ
115
+ ) # type: bool
116
+
117
+ # Generic platform identifier used for filtering methods
118
+ PLATFORM = _SYST.lower() # type: str
119
+ if PLATFORM == "linux" and "Microsoft" in platform.version():
120
+ PLATFORM = "wsl"
121
+
122
+ # User-configurable override to force a specific platform
123
+ # This will change to a function argument in 1.0.0
124
+ OVERRIDE_PLATFORM = "" # type: str
125
+
126
+ # Force a specific method to be used for all lookups
127
+ # Used for debugging and testing
128
+ FORCE_METHOD = "" # type: str
129
+
130
+ # Get and cache the configured system PATH on import
131
+ # The process environment does not change after a process is started
132
+ PATH = os.environ.get("PATH", os.defpath).split(os.pathsep) # type: List[str]
133
+ if not WINDOWS:
134
+ PATH.extend(("/sbin", "/usr/sbin"))
135
+ else:
136
+ # TODO: Prevent edge case on Windows where our script "getmac.exe"
137
+ # gets added to the path ahead of the actual Windows getmac.exe
138
+ # This just handles case where it's in a virtualenv, won't work /w global scripts
139
+ PATH = [p for p in PATH if "\\getmac\\Scripts" not in p]
140
+ # Build the str after modifications are made
141
+ PATH_STR = os.pathsep.join(PATH) # type: str
142
+
143
+ # Use a copy of the environment so we don't
144
+ # modify the process's current environment.
145
+ ENV = dict(os.environ) # type: Dict[str, str]
146
+ ENV["LC_ALL"] = "C" # Ensure ASCII output so we parse correctly
147
+
148
+ # Constants
149
+ IP4 = 0
150
+ IP6 = 1
151
+ INTERFACE = 2
152
+ HOSTNAME = 3
153
+
154
+ MAC_RE_COLON = r"([0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5})"
155
+ MAC_RE_DASH = r"([0-9a-fA-F]{2}(?:-[0-9a-fA-F]{2}){5})"
156
+ # On OSX, some MACs in arp output may have a single digit instead of two
157
+ # Examples: "18:4f:32:5a:64:5", "14:cc:20:1a:99:0"
158
+ # This can also happen on other platforms, like Solaris
159
+ MAC_RE_SHORT = r"([0-9a-fA-F]{1,2}(?::[0-9a-fA-F]{1,2}){5})"
160
+
161
+ # Ensure we only log the Python 2 warning once
162
+ WARNED_UNSUPPORTED_PYTHONS = False
163
+
164
+ # Cache of commands that have been checked for existence by check_command()
165
+ CHECK_COMMAND_CACHE = {} # type: Dict[str, bool]
166
+
167
+
168
+ def check_command(command):
169
+ # type: (str) -> bool
170
+ if command not in CHECK_COMMAND_CACHE:
171
+ CHECK_COMMAND_CACHE[command] = bool(which(command, path=PATH_STR))
172
+ return CHECK_COMMAND_CACHE[command]
173
+
174
+
175
+ def check_path(filepath):
176
+ # type: (str) -> bool
177
+ return os.path.exists(filepath) and os.access(filepath, os.R_OK)
178
+
179
+
180
+ def _clean_mac(mac):
181
+ # type: (Optional[str]) -> Optional[str]
182
+ """Check and format a string result to be lowercase colon-separated MAC."""
183
+ if mac is None:
184
+ return None
185
+
186
+ # Handle cases where it's bytes (which are the same as str in PY2)
187
+ mac = str(mac)
188
+ if not PY2: # Strip bytestring conversion artifacts
189
+ # TODO(python3): check for bytes and decode instead of this weird hack
190
+ for garbage_string in ["b'", "'", "\\n", "\\r"]:
191
+ mac = mac.replace(garbage_string, "")
192
+
193
+ # Remove trailing whitespace, make lowercase, remove spaces,
194
+ # and replace dashes '-' with colons ':'.
195
+ mac = mac.strip().lower().replace(" ", "").replace("-", ":")
196
+
197
+ # Fix cases where there are no colons
198
+ if ":" not in mac and len(mac) == 12:
199
+ log.debug("Adding colons to MAC %s", mac)
200
+ mac = ":".join(mac[i : i + 2] for i in range(0, len(mac), 2))
201
+
202
+ # Pad single-character octets with a leading zero (e.g. Darwin's ARP output)
203
+ elif len(mac) < 17:
204
+ log.debug(
205
+ "Length of MAC %s is %d, padding single-character octets with zeros",
206
+ mac,
207
+ len(mac),
208
+ )
209
+ parts = mac.split(":")
210
+ new_mac = []
211
+ for part in parts:
212
+ if len(part) == 1:
213
+ new_mac.append("0" + part)
214
+ else:
215
+ new_mac.append(part)
216
+ mac = ":".join(new_mac)
217
+
218
+ # MAC address should ALWAYS be 17 characters before being returned
219
+ if len(mac) != 17:
220
+ log.warning("MAC address %s is not 17 characters long!", mac)
221
+ mac = None
222
+ elif mac.count(":") != 5:
223
+ log.warning("MAC address %s is missing colon (':') characters", mac)
224
+ mac = None
225
+ return mac
226
+
227
+
228
+ def _read_file(filepath):
229
+ # type: (str) -> Optional[str]
230
+ try:
231
+ with open(filepath) as f:
232
+ return f.read()
233
+ # This is IOError on Python 2.7
234
+ except (OSError, IOError): # noqa: B014
235
+ log.debug("Could not find file: '%s'", filepath)
236
+ return None
237
+
238
+
239
+ def _search(regex, text, group_index=0, flags=0):
240
+ # type: (str, str, int, int) -> Optional[str]
241
+ if not text:
242
+ if DEBUG:
243
+ log.debug("No text to _search()")
244
+ return None
245
+
246
+ match = re.search(regex, text, flags)
247
+ if match:
248
+ return match.groups()[group_index]
249
+
250
+ return None
251
+
252
+
253
+ def _popen(command, args):
254
+ # type: (str, str) -> str
255
+ for directory in PATH:
256
+ executable = os.path.join(directory, command)
257
+ if (
258
+ os.path.exists(executable)
259
+ and os.access(executable, os.F_OK | os.X_OK)
260
+ and not os.path.isdir(executable)
261
+ ):
262
+ break
263
+ else:
264
+ executable = command
265
+
266
+ if DEBUG >= 3:
267
+ log.debug("Running: '%s %s'", executable, args)
268
+
269
+ return _call_proc(executable, args)
270
+
271
+
272
+ def _call_proc(executable, args):
273
+ # type: (str, str) -> str
274
+ if WINDOWS:
275
+ cmd = executable + " " + args # type: ignore
276
+ else:
277
+ cmd = [executable] + shlex.split(args) # type: ignore
278
+
279
+ output = check_output(cmd, stderr=DEVNULL, env=ENV)
280
+
281
+ if DEBUG >= 4:
282
+ log.debug("Output from '%s' command: %s", executable, str(output))
283
+
284
+ if not PY2 and isinstance(output, bytes):
285
+ return str(output, "utf-8")
286
+ else:
287
+ return str(output)
288
+
289
+
290
+ def _uuid_convert(mac):
291
+ # type: (int) -> str
292
+ return ":".join(("%012X" % mac)[i : i + 2] for i in range(0, 12, 2))
293
+
294
+
295
+ def _fetch_ip_using_dns():
296
+ # type: () -> str
297
+ """
298
+ Determines the IP address of the default network interface.
299
+
300
+ Sends a UDP packet to Cloudflare's DNS (``1.1.1.1``), which should go through
301
+ the default interface. This populates the source address of the socket,
302
+ which we then inspect and return.
303
+ """
304
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
305
+ s.connect(("1.1.1.1", 53))
306
+ ip = s.getsockname()[0]
307
+ s.close() # NOTE: sockets don't have context manager in 2.7 :(
308
+ return ip
309
+
310
+
311
+ class Method:
312
+ #: Valid platform identifier strings
313
+ VALID_PLATFORM_NAMES = {
314
+ "android",
315
+ "darwin",
316
+ "linux",
317
+ "windows",
318
+ "wsl",
319
+ "openbsd",
320
+ "freebsd",
321
+ "sunos",
322
+ "other",
323
+ }
324
+
325
+ #: Platforms supported by a method
326
+ platforms = set() # type: Set[str]
327
+
328
+ #: The type of method, e.g. does it get the MAC of a interface?
329
+ #: Allowed values: {ip, ip4, ip6, iface, default_iface}
330
+ method_type = "" # type: str
331
+
332
+ #: If the method makes a network request as part of the check
333
+ network_request = False # type: bool
334
+
335
+ #: Marks the method as unable to be used, e.g. if there was a runtime
336
+ #: error indicating the method won't work on the current platform.
337
+ unusable = False # type: bool
338
+
339
+ def test(self): # type: () -> bool # noqa: T484
340
+ """Low-impact test that the method is feasible, e.g. command exists."""
341
+ pass # pragma: no cover
342
+
343
+ # TODO: automatically clean MAC on return
344
+ def get(self, arg): # type: (str) -> Optional[str]
345
+ """
346
+ Core logic of the method that performs the lookup.
347
+
348
+ .. warning::
349
+ If the method itself fails to function an exception will be raised!
350
+ (for instance, if some command arguments are invalid, or there's an
351
+ internal error with the command, or a bug in the code).
352
+
353
+ Args:
354
+ arg (str): What the method should get, such as an IP address
355
+ or interface name. In the case of default_iface methods,
356
+ this is not used and defaults to an empty string.
357
+
358
+ Returns:
359
+ Lowercase colon-separated MAC address, or None if one could
360
+ not be found.
361
+ """
362
+ pass # pragma: no cover
363
+
364
+ @classmethod
365
+ def __str__(cls): # type: () -> str
366
+ return cls.__name__
367
+
368
+
369
+ # TODO(python3): do we want to keep this around? It calls 3 commands and is
370
+ # quite inefficient. We should just take the methods and use directly.
371
+ class UuidArpGetNode(Method):
372
+ platforms = {"linux", "darwin", "sunos", "other"}
373
+ method_type = "ip"
374
+
375
+ def test(self): # type: () -> bool
376
+ try:
377
+ from uuid import _arp_getnode # type: ignore
378
+
379
+ return True
380
+ except Exception:
381
+ return False
382
+
383
+ def get(self, arg): # type: (str) -> Optional[str]
384
+ from uuid import _arp_getnode # type: ignore
385
+
386
+ backup = socket.gethostbyname
387
+ try:
388
+ socket.gethostbyname = lambda x: arg # noqa: F841
389
+ mac1 = _arp_getnode()
390
+ if mac1 is not None:
391
+ mac1 = _uuid_convert(mac1)
392
+ mac2 = _arp_getnode()
393
+ mac2 = _uuid_convert(mac2)
394
+ if mac1 == mac2:
395
+ return mac1
396
+ except Exception:
397
+ raise
398
+ finally:
399
+ socket.gethostbyname = backup
400
+
401
+ return None
402
+
403
+
404
+ class ArpFile(Method):
405
+ platforms = {"linux"}
406
+ method_type = "ip4"
407
+ _path = os.environ.get("ARP_PATH", "/proc/net/arp") # type: str
408
+
409
+ def test(self): # type: () -> bool
410
+ return check_path(self._path)
411
+
412
+ def get(self, arg): # type: (str) -> Optional[str]
413
+ if not arg:
414
+ return None
415
+
416
+ data = _read_file(self._path)
417
+
418
+ if data is None:
419
+ self.unusable = True
420
+ return None
421
+
422
+ if data is not None and len(data) > 1:
423
+ # Need a space, otherwise a search for 192.168.16.2
424
+ # will match 192.168.16.254 if it comes first!
425
+ return _search(re.escape(arg) + r" .+" + MAC_RE_COLON, data)
426
+
427
+ return None
428
+
429
+
430
+ class ArpFreebsd(Method):
431
+ platforms = {"freebsd"}
432
+ method_type = "ip"
433
+
434
+ def test(self): # type: () -> bool
435
+ return check_command("arp")
436
+
437
+ def get(self, arg): # type: (str) -> Optional[str]
438
+ regex = r"\(" + re.escape(arg) + r"\)\s+at\s+" + MAC_RE_COLON
439
+ return _search(regex, _popen("arp", arg))
440
+
441
+
442
+ class ArpOpenbsd(Method):
443
+ platforms = {"openbsd"}
444
+ method_type = "ip"
445
+ _regex = r"[ ]+" + MAC_RE_COLON # type: str
446
+
447
+ def test(self): # type: () -> bool
448
+ return check_command("arp")
449
+
450
+ def get(self, arg): # type: (str) -> Optional[str]
451
+ return _search(re.escape(arg) + self._regex, _popen("arp", "-an"))
452
+
453
+
454
+ class ArpVariousArgs(Method):
455
+ platforms = {"linux", "darwin", "freebsd", "sunos", "other"}
456
+ method_type = "ip"
457
+ _regex_std = r"\)\s+at\s+" + MAC_RE_COLON # type: str
458
+ _regex_darwin = r"\)\s+at\s+" + MAC_RE_SHORT # type: str
459
+ _args = (
460
+ ("", True), # "arp 192.168.1.1"
461
+ # Linux
462
+ ("-an", False), # "arp -an"
463
+ ("-an", True), # "arp -an 192.168.1.1"
464
+ # Darwin, WSL, Linux distros???
465
+ ("-a", False), # "arp -a"
466
+ ("-a", True), # "arp -a 192.168.1.1"
467
+ )
468
+ _args_tested = False # type: bool
469
+ _good_pair = () # type: Union[Tuple, Tuple[str, bool]]
470
+
471
+ def test(self): # type: () -> bool
472
+ return check_command("arp")
473
+
474
+ def get(self, arg): # type: (str) -> Optional[str]
475
+ if not arg:
476
+ return None
477
+
478
+ # Ensure output from testing command on first call isn't wasted
479
+ command_output = ""
480
+
481
+ # Test which arguments are valid to the command
482
+ # This will NOT test which regex is valid
483
+ if not self._args_tested:
484
+ for pair_to_test in self._args:
485
+ try:
486
+ cmd_args = [pair_to_test[0]]
487
+
488
+ # if True, then include IP as a command argument
489
+ if pair_to_test[1]:
490
+ cmd_args.append(arg)
491
+
492
+ command_output = _popen("arp", " ".join(cmd_args))
493
+ self._good_pair = pair_to_test
494
+ break
495
+ except CalledProcessError as ex:
496
+ if DEBUG:
497
+ log.debug(
498
+ "ArpVariousArgs pair test failed for (%s, %s): %s",
499
+ pair_to_test[0],
500
+ pair_to_test[1],
501
+ str(ex),
502
+ )
503
+
504
+ if not self._good_pair:
505
+ self.unusable = True
506
+ return None
507
+ self._args_tested = True
508
+
509
+ if not command_output:
510
+ # if True, then include IP as a command argument
511
+ cmd_args = [self._good_pair[0]]
512
+
513
+ if self._good_pair[1]:
514
+ cmd_args.append(arg)
515
+
516
+ command_output = _popen("arp", " ".join(cmd_args))
517
+
518
+ escaped = re.escape(arg)
519
+ _good_regex = (
520
+ self._regex_darwin if DARWIN or SOLARIS else self._regex_std
521
+ ) # type: str
522
+ return _search(r"\(" + escaped + _good_regex, command_output)
523
+
524
+
525
+ class ArpExe(Method):
526
+ """
527
+ Query the Windows ARP table using ``arp.exe`` to find the MAC address of a remote host.
528
+ This only works for IPv4, since the ARP table is IPv4-only.
529
+
530
+ Microsoft Documentation: `arp <https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/arp>`
531
+ """ # noqa: E501
532
+
533
+ platforms = {"windows", "wsl"}
534
+ method_type = "ip4"
535
+
536
+ def test(self): # type: () -> bool
537
+ # NOTE: specifying "arp.exe" instead of "arp" lets this work
538
+ # seamlessly on WSL1 as well. On WSL2 it doesn't matter, since
539
+ # it's basically just a Linux VM with some lipstick.
540
+ return check_command("arp.exe")
541
+
542
+ def get(self, arg): # type: (str) -> Optional[str]
543
+ return _search(MAC_RE_DASH, _popen("arp.exe", "-a %s" % arg))
544
+
545
+
546
+ class ArpingHost(Method):
547
+ """
548
+ Use ``arping`` command to determine the MAC of a host.
549
+
550
+ Supports three variants of ``arping``
551
+
552
+ - "habets" arping by Thomas Habets
553
+ (`GitHub <https://github.com/ThomasHabets/arping>`__)
554
+ On Debian-based distros, ``apt install arping`` will install
555
+ Habets arping.
556
+ - "iputils" arping, from the ``iputils-arping``
557
+ `package <https://packages.debian.org/sid/iputils-arping>`__
558
+ - "busybox" arping, included with BusyBox (a small executable "distro")
559
+ (`further reading <https://boxmatrix.info/wiki/Property:arping>`__)
560
+
561
+ BusyBox's arping quite similar to iputils-arping. The arguments for
562
+ our purposes are the same, and the output is also the same.
563
+ There's even a TODO in busybox's arping code referencing iputils arping.
564
+ There are several differences:
565
+ - The return code from bad arguments is 1, not 2 like for iputils-arping
566
+ - The MAC address in output is lowercase (vs. uppercase in iputils-arping)
567
+
568
+ This was a pain to obtain samples for busybox on Windows. I recommend
569
+ using WSL and arping'ing the Docker gateway (for WSL2 distros).
570
+ NOTE: it must be run as root using ``sudo busybox arping``.
571
+ """
572
+
573
+ platforms = {"linux", "darwin"}
574
+ method_type = "ip4"
575
+ network_request = True
576
+ _is_iputils = True # type: bool
577
+ _habets_args = "-r -C 1 -c 1 %s" # type: str
578
+ _iputils_args = "-f -c 1 %s" # type: str
579
+
580
+ def test(self): # type: () -> bool
581
+ return check_command("arping")
582
+
583
+ def get(self, arg): # type: (str) -> Optional[str]
584
+ # If busybox or iputils, this will just work, and if host ping fails,
585
+ # then it'll exit with code 1 and this function will return None.
586
+ #
587
+ # If it's Habets, then it'll exit code 1 and have "invalid option"
588
+ # and/or the help message in the output.
589
+ # In the case of Habets, set self._is_iputils to False,
590
+ # then re-try with Habets args.
591
+ try:
592
+ if self._is_iputils:
593
+ command_output = _popen("arping", self._iputils_args % arg)
594
+ if command_output:
595
+ return _search(
596
+ r" from %s \[(%s)\]" % (re.escape(arg), MAC_RE_COLON),
597
+ command_output,
598
+ )
599
+ else:
600
+ return self._call_habets(arg)
601
+ except CalledProcessError as ex:
602
+ if ex.output and self._is_iputils:
603
+ if not PY2 and isinstance(ex.output, bytes):
604
+ output = str(ex.output, "utf-8").lower()
605
+ else:
606
+ output = str(ex.output).lower()
607
+
608
+ if "habets" in output or "invalid option" in output:
609
+ if DEBUG:
610
+ log.debug("Falling back to Habets arping")
611
+ self._is_iputils = False
612
+ try:
613
+ return self._call_habets(arg)
614
+ except CalledProcessError:
615
+ pass
616
+
617
+ return None
618
+
619
+ def _call_habets(self, arg): # type: (str) -> Optional[str]
620
+ command_output = _popen("arping", self._habets_args % arg)
621
+ if command_output:
622
+ return command_output.strip()
623
+ else:
624
+ return None
625
+
626
+
627
+ class CtypesHost(Method):
628
+ """
629
+ Uses ``SendARP`` from the Windows ``Iphlpapi`` to get the MAC address
630
+ of a remote IPv4 host.
631
+
632
+ Microsoft Documentation: `SendARP function (iphlpapi.h) <https://learn.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-sendarp>`__
633
+ """ # noqa: E501
634
+
635
+ platforms = {"windows"}
636
+ method_type = "ip4"
637
+ network_request = True
638
+
639
+ def test(self): # type: () -> bool
640
+ try:
641
+ return ctypes.windll.wsock32.inet_addr(b"127.0.0.1") > 0 # noqa: T484
642
+ except Exception:
643
+ return False
644
+
645
+ def get(self, arg): # type: (str) -> Optional[str]
646
+ if not PY2: # Convert to bytes on Python 3+ (Fixes GitHub issue #7)
647
+ arg = arg.encode() # type: ignore
648
+
649
+ try:
650
+ inetaddr = ctypes.windll.wsock32.inet_addr(arg) # type: ignore
651
+ if inetaddr in (0, -1):
652
+ raise Exception
653
+ except Exception:
654
+ # TODO: this assumes failure is due to arg being a hostname
655
+ # We should be explicit about only accepting ipv4 addresses
656
+ # and handle any hostname resolution in calling code
657
+ hostip = socket.gethostbyname(arg)
658
+ inetaddr = ctypes.windll.wsock32.inet_addr(hostip) # type: ignore
659
+
660
+ buffer = ctypes.c_buffer(6)
661
+ addlen = ctypes.c_ulong(ctypes.sizeof(buffer))
662
+
663
+ # https://docs.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-sendarp
664
+ send_arp = ctypes.windll.Iphlpapi.SendARP # type: ignore
665
+ if send_arp(inetaddr, 0, ctypes.byref(buffer), ctypes.byref(addlen)) != 0:
666
+ return None
667
+
668
+ # Convert binary data into a string.
669
+ macaddr = ""
670
+ for intval in struct.unpack("BBBBBB", buffer): # type: ignore
671
+ if intval > 15:
672
+ replacestr = "0x"
673
+ else:
674
+ replacestr = "x"
675
+ macaddr = "".join([macaddr, hex(intval).replace(replacestr, "")])
676
+
677
+ return macaddr
678
+
679
+
680
+ class IpNeighborShow(Method):
681
+ platforms = {"linux", "other"}
682
+ method_type = "ip" # IPv6 and IPv4
683
+
684
+ def test(self): # type: () -> bool
685
+ return check_command("ip")
686
+
687
+ def get(self, arg): # type: (str) -> Optional[str]
688
+ output = _popen("ip", "neighbor show %s" % arg)
689
+ if not output:
690
+ return None
691
+
692
+ try:
693
+ # NOTE: the space prevents accidental matching of partial IPs
694
+ return (
695
+ output.partition(arg + " ")[2].partition("lladdr")[2].strip().split()[0]
696
+ )
697
+ except IndexError as ex:
698
+ log.debug("IpNeighborShow failed with exception: %s", str(ex))
699
+ return None
700
+
701
+
702
+ class SysIfaceFile(Method):
703
+ platforms = {"linux", "wsl"}
704
+ method_type = "iface"
705
+ _path = "/sys/class/net/" # type: str
706
+
707
+ def test(self): # type: () -> bool
708
+ # Imperfect, but should work well enough
709
+ return check_path(self._path)
710
+
711
+ def get(self, arg): # type: (str) -> Optional[str]
712
+ data = _read_file(self._path + arg + "/address")
713
+
714
+ # NOTE: if "/sys/class/net/" exists, but interface file doesn't,
715
+ # then that means the interface doesn't exist
716
+ # Sometimes this can be empty or a single newline character
717
+ return None if data is not None and len(data) < 17 else data
718
+
719
+
720
+ class UuidLanscan(Method):
721
+ platforms = {"other"}
722
+ method_type = "iface"
723
+
724
+ def test(self): # type: () -> bool
725
+ try:
726
+ from uuid import _find_mac # noqa: T484
727
+
728
+ return check_command("lanscan")
729
+ except Exception:
730
+ return False
731
+
732
+ def get(self, arg): # type: (str) -> Optional[str]
733
+ from uuid import _find_mac # type: ignore
734
+
735
+ if not PY2:
736
+ arg = bytes(arg, "utf-8") # type: ignore
737
+
738
+ mac = _find_mac("lanscan", "-ai", [arg], lambda i: 0)
739
+
740
+ if mac:
741
+ return _uuid_convert(mac)
742
+
743
+ return None
744
+
745
+
746
+ class FcntlIface(Method):
747
+ platforms = {"linux", "wsl"}
748
+ method_type = "iface"
749
+
750
+ def test(self): # type: () -> bool
751
+ try:
752
+ import fcntl
753
+
754
+ return True
755
+ except Exception: # Broad except to handle unknown effects
756
+ return False
757
+
758
+ def get(self, arg): # type: (str) -> Optional[str]
759
+ import fcntl
760
+
761
+ if not PY2:
762
+ arg = arg.encode() # type: ignore
763
+
764
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
765
+
766
+ # 0x8927 = SIOCGIFADDR
767
+ info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack("256s", arg[:15]))
768
+
769
+ if PY2:
770
+ return ":".join(["%02x" % ord(char) for char in info[18:24]])
771
+ else:
772
+ return ":".join(["%02x" % ord(chr(char)) for char in info[18:24]])
773
+
774
+
775
+ class GetmacExe(Method):
776
+ """
777
+ Uses Windows-builtin ``getmac.exe`` to get a interface's MAC address.
778
+
779
+ Microsoft Documentation: `getmac <https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/getmac>`__
780
+ """ # noqa: E501
781
+
782
+ platforms = {"windows"}
783
+ method_type = "iface"
784
+ _regexes = [
785
+ # Connection Name
786
+ (r"\r\n", r".*" + MAC_RE_DASH + r".*\r\n"),
787
+ # Network Adapter (the human-readable name)
788
+ (r"\r\n.*", r".*" + MAC_RE_DASH + r".*\r\n"),
789
+ ] # type: List[Tuple[str, str]]
790
+ _champ = () # type: Union[tuple, Tuple[str, str]]
791
+
792
+ def test(self): # type: () -> bool
793
+ # NOTE: the scripts from this library (getmac) are excluded from the
794
+ # path used for checking variables, in getmac.getmac.PATH (defined
795
+ # at the top of this file). Otherwise, this would get messy quickly :)
796
+ return check_command("getmac.exe")
797
+
798
+ def get(self, arg): # type: (str) -> Optional[str]
799
+ try:
800
+ # /nh: Suppresses table headers
801
+ # /v: Verbose
802
+ command_output = _popen("getmac.exe", "/NH /V")
803
+ except CalledProcessError as ex:
804
+ # This shouldn't cause an exception if it's valid command
805
+ log.error("getmac.exe failed, marking unusable. Exception: %s", str(ex))
806
+ self.unusable = True
807
+ return None
808
+
809
+ if self._champ:
810
+ return _search(self._champ[0] + arg + self._champ[1], command_output)
811
+
812
+ for pair in self._regexes:
813
+ result = _search(pair[0] + arg + pair[1], command_output)
814
+ if result:
815
+ self._champ = pair
816
+ return result
817
+
818
+ return None
819
+
820
+
821
+ class IpconfigExe(Method):
822
+ """
823
+ Uses ``ipconfig.exe`` to find interface MAC addresses on Windows.
824
+
825
+ This is generally pretty reliable and works across a wide array of
826
+ versions and releases. I'm not sure if it works pre-XP though.
827
+
828
+ Microsoft Documentation: `ipconfig <https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/ipconfig>`__
829
+ """ # noqa: E501
830
+
831
+ platforms = {"windows"}
832
+ method_type = "iface"
833
+ _regex = (
834
+ r"(?:\n?[^\n]*){1,8}Physical Address[ .:]+" + MAC_RE_DASH + r"\r\n"
835
+ ) # type: str
836
+
837
+ def test(self): # type: () -> bool
838
+ return check_command("ipconfig.exe")
839
+
840
+ def get(self, arg): # type: (str) -> Optional[str]
841
+ return _search(arg + self._regex, _popen("ipconfig.exe", "/all"))
842
+
843
+
844
+ class WmicExe(Method):
845
+ """
846
+ Use ``wmic.exe`` on Windows to find the MAC address of a network interface.
847
+
848
+ Microsoft Documentation: `wmic <https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/wmic>`__
849
+
850
+ .. warning::
851
+ WMIC is deprecated as of Windows 10 21H1. This method may not work on
852
+ Windows 11 and may stop working at some point on Windows 10 (unlikely,
853
+ but possible).
854
+ """ # noqa: E501
855
+
856
+ platforms = {"windows"}
857
+ method_type = "iface"
858
+
859
+ def test(self): # type: () -> bool
860
+ return check_command("wmic.exe")
861
+
862
+ def get(self, arg): # type: (str) -> Optional[str]
863
+ command_output = _popen(
864
+ "wmic.exe",
865
+ 'nic where "NetConnectionID = \'%s\'" get "MACAddress" /value' % arg,
866
+ )
867
+
868
+ # Negative: "No Instance(s) Available"
869
+ # Positive: "MACAddress=00:FF:E7:78:95:A0"
870
+ # NOTE: .partition() always returns 3 parts,
871
+ # therefore it won't cause an IndexError
872
+ return command_output.strip().partition("=")[2]
873
+
874
+
875
+ class DarwinNetworksetupIface(Method):
876
+ """
877
+ Use ``networksetup`` on MacOS (Darwin) to get the MAC address of a specific interface.
878
+
879
+ I think that this is or was a BSD utility, but I haven't seen it on other BSDs
880
+ (FreeBSD, OpenBSD, etc.). So, I'm treating it as a Darwin-specific utility
881
+ until further notice. If you know otherwise, please open a PR :)
882
+
883
+ If the command is present, it should always work, though naturally that is contingent
884
+ upon the whims of Apple in newer MacOS releases.
885
+
886
+ Man page: `networksetup (8) <https://www.manpagez.com/man/8/networksetup/>`__
887
+ """
888
+
889
+ platforms = {"darwin"}
890
+ method_type = "iface"
891
+
892
+ def test(self): # type: () -> bool
893
+ return check_command("networksetup")
894
+
895
+ def get(self, arg): # type: (str) -> Optional[str]
896
+ command_output = _popen("networksetup", "-getmacaddress %s" % arg)
897
+ return _search(MAC_RE_COLON, command_output)
898
+
899
+
900
+ # This only took 15-20 hours of throwing my brain against a wall multiple times
901
+ # over the span of 1-2 years to figure out. It works for almost all conceivable
902
+ # output from "ifconfig", and probably netstat too. It can probably be made more
903
+ # efficient by someone who actually knows how to write regex.
904
+ # [: ]\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
905
+ IFCONFIG_REGEX = (
906
+ r"[: ]\s?(?:flags=|\s).*?(?:"
907
+ r"(?:\w+[: ]\s?flags=)|" # Prevent interfaces w/o a MAC from matching
908
+ r"\s(?:ether|address|HWaddr|hwaddr|lladdr)[ :]?\s?" # Handle various prefixes
909
+ r"([0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5}))" # Match the MAC
910
+ )
911
+
912
+
913
+ def _parse_ifconfig(iface, command_output):
914
+ # type: (str, str) -> Optional[str]
915
+ if not iface or not command_output:
916
+ return None
917
+
918
+ # Sanity check on input e.g. if user does "eth0:" as argument
919
+ iface = iface.strip(":")
920
+
921
+ # "(?:^|\s)": prevent an input of "h0" from matching on "eth0"
922
+ search_re = r"(?:^|\s)" + iface + IFCONFIG_REGEX
923
+
924
+ return _search(search_re, command_output, flags=re.DOTALL)
925
+
926
+
927
+ class IfconfigWithIfaceArg(Method):
928
+ """
929
+ ``ifconfig`` command with the interface name as an argument
930
+ (e.g. ``ifconfig eth0``).
931
+ """
932
+
933
+ platforms = {"linux", "wsl", "freebsd", "openbsd", "other"}
934
+ method_type = "iface"
935
+
936
+ def test(self): # type: () -> bool
937
+ return check_command("ifconfig")
938
+
939
+ def get(self, arg): # type: (str) -> Optional[str]
940
+ try:
941
+ command_output = _popen("ifconfig", arg)
942
+ except CalledProcessError as err:
943
+ # Return code of 1 means interface doesn't exist
944
+ if err.returncode == 1:
945
+ return None
946
+ else:
947
+ raise err # this will cause another method to be used
948
+
949
+ return _parse_ifconfig(arg, command_output)
950
+
951
+
952
+ # TODO: combine this with IfconfigWithArg/IfconfigNoArg
953
+ # (need to do live testing on Darwin)
954
+ class IfconfigEther(Method):
955
+ platforms = {"darwin"}
956
+ method_type = "iface"
957
+ _tested_arg = False # type: bool
958
+ _iface_arg = False # type: bool
959
+
960
+ def test(self): # type: () -> bool
961
+ return check_command("ifconfig")
962
+
963
+ def get(self, arg): # type: (str) -> Optional[str]
964
+ # Check if this version of "ifconfig" accepts an interface argument
965
+ command_output = ""
966
+
967
+ if not self._tested_arg:
968
+ try:
969
+ command_output = _popen("ifconfig", arg)
970
+ self._iface_arg = True
971
+ except CalledProcessError:
972
+ self._iface_arg = False
973
+ self._tested_arg = True
974
+
975
+ if self._iface_arg and not command_output: # Don't repeat work on first run
976
+ command_output = _popen("ifconfig", arg)
977
+ else:
978
+ command_output = _popen("ifconfig", "")
979
+
980
+ return _parse_ifconfig(arg, command_output)
981
+
982
+
983
+ # TODO: create new methods, IfconfigNoArgs and IfconfigVariousArgs
984
+ # TODO: unit tests
985
+ class IfconfigOther(Method):
986
+ """
987
+ Wild 'Shot in the Dark' attempt at ``ifconfig`` for unknown platforms.
988
+ """
989
+
990
+ platforms = {"linux", "other"}
991
+ method_type = "iface"
992
+ # "-av": Tru64 system?
993
+ _args = (
994
+ ("", (r"(?::| ).*?\sether\s", r"(?::| ).*?\sHWaddr\s")),
995
+ ("-a", r".*?HWaddr\s"),
996
+ ("-v", r".*?HWaddr\s"),
997
+ ("-av", r".*?Ether\s"),
998
+ )
999
+ _args_tested = False # type: bool
1000
+ _good_pair = [] # type: List[Union[str, Tuple[str, str]]]
1001
+
1002
+ def test(self): # type: () -> bool
1003
+ return check_command("ifconfig")
1004
+
1005
+ def get(self, arg): # type: (str) -> Optional[str]
1006
+ if not arg:
1007
+ return None
1008
+
1009
+ # Cache output from testing command so first call isn't wasted
1010
+ command_output = ""
1011
+
1012
+ # Test which arguments are valid to the command
1013
+ if not self._args_tested:
1014
+ for pair_to_test in self._args:
1015
+ try:
1016
+ command_output = _popen("ifconfig", pair_to_test[0])
1017
+ self._good_pair = list(pair_to_test) # noqa: T484
1018
+ if isinstance(self._good_pair[1], str):
1019
+ self._good_pair[1] += MAC_RE_COLON
1020
+ break
1021
+ except CalledProcessError as ex:
1022
+ if DEBUG:
1023
+ log.debug(
1024
+ "IfconfigOther pair test failed for (%s, %s): %s",
1025
+ pair_to_test[0],
1026
+ pair_to_test[1],
1027
+ str(ex),
1028
+ )
1029
+
1030
+ if not self._good_pair:
1031
+ self.unusable = True
1032
+ return None
1033
+
1034
+ self._args_tested = True
1035
+
1036
+ if not command_output and isinstance(self._good_pair[0], str):
1037
+ command_output = _popen("ifconfig", self._good_pair[0])
1038
+
1039
+ # Handle the two possible search terms
1040
+ if isinstance(self._good_pair[1], tuple):
1041
+ for term in self._good_pair[1]:
1042
+ regex = term + MAC_RE_COLON
1043
+ result = _search(re.escape(arg) + regex, command_output)
1044
+
1045
+ if result:
1046
+ # changes type from tuple to str, so the else statement
1047
+ # will be hit on the next call to this method
1048
+ self._good_pair[1] = regex
1049
+ return result
1050
+ return None
1051
+ else:
1052
+ return _search(re.escape(arg) + self._good_pair[1], command_output)
1053
+
1054
+
1055
+ class NetstatIface(Method):
1056
+ platforms = {"linux", "wsl", "other"}
1057
+ method_type = "iface"
1058
+
1059
+ # ".*?": non-greedy
1060
+ # https://docs.python.org/3/howto/regex.html#greedy-versus-non-greedy
1061
+ _regexes = [
1062
+ r": .*?ether " + MAC_RE_COLON,
1063
+ r": .*?HWaddr " + MAC_RE_COLON,
1064
+ # Ubuntu 12.04 and other older kernels
1065
+ r" .*?Link encap:Ethernet HWaddr " + MAC_RE_COLON,
1066
+ ] # type: List[str]
1067
+ _working_regex = "" # type: str
1068
+
1069
+ def test(self): # type: () -> bool
1070
+ return check_command("netstat")
1071
+
1072
+ # TODO: consolidate the parsing logic between IfconfigOther and netstat
1073
+ def get(self, arg): # type: (str) -> Optional[str]
1074
+ # NOTE: netstat and ifconfig pull from the same kernel source and
1075
+ # therefore have the same output format on the same platform.
1076
+ command_output = _popen("netstat", "-iae")
1077
+ if not command_output:
1078
+ log.warning("no netstat output, marking unusable")
1079
+ self.unusable = True
1080
+ return None
1081
+
1082
+ if self._working_regex:
1083
+ # Use regex that worked previously. This can still return None in
1084
+ # the case of interface not existing, but at least it's a bit faster.
1085
+ return _search(arg + self._working_regex, command_output, flags=re.DOTALL)
1086
+
1087
+ # See if either regex matches
1088
+ for regex in self._regexes:
1089
+ result = _search(arg + regex, command_output, flags=re.DOTALL)
1090
+ if result:
1091
+ self._working_regex = regex
1092
+ return result
1093
+
1094
+ return None
1095
+
1096
+
1097
+ # TODO: Add to IpLinkIface
1098
+ # TODO: New method for "ip addr"? (this would be useful for CentOS and others as a fallback)
1099
+ # (r"state UP.*\n.*ether " + MAC_RE_COLON, 0, "ip", ["link","addr"]),
1100
+ # (r"wlan.*\n.*ether " + MAC_RE_COLON, 0, "ip", ["link","addr"]),
1101
+ # (r"ether " + MAC_RE_COLON, 0, "ip", ["link","addr"]),
1102
+ # _regexes = (
1103
+ # r".*\n.*link/ether " + MAC_RE_COLON,
1104
+ # # Android 6.0.1+ (and likely other platforms as well)
1105
+ # r"state UP.*\n.*ether " + MAC_RE_COLON,
1106
+ # r"wlan.*\n.*ether " + MAC_RE_COLON,
1107
+ # r"ether " + MAC_RE_COLON,
1108
+ # ) # type: Tuple[str, str, str, str]
1109
+
1110
+
1111
+ class IpLinkIface(Method):
1112
+ platforms = {"linux", "wsl", "android", "other"}
1113
+ method_type = "iface"
1114
+ _regex = r".*\n.*link/ether " + MAC_RE_COLON # type: str
1115
+ _tested_arg = False # type: bool
1116
+ _iface_arg = False # type: bool
1117
+
1118
+ def test(self): # type: () -> bool
1119
+ return check_command("ip")
1120
+
1121
+ def get(self, arg): # type: (str) -> Optional[str]
1122
+ # Check if this version of "ip link" accepts an interface argument
1123
+ # Not accepting one is a quirk of older versions of 'iproute2'
1124
+ # TODO: is it "ip link <arg>" on some platforms and "ip link show <arg>" on others?
1125
+ command_output = ""
1126
+
1127
+ if not self._tested_arg:
1128
+ try:
1129
+ command_output = _popen("ip", "link show " + arg)
1130
+ self._iface_arg = True
1131
+ except CalledProcessError as err:
1132
+ # Output: 'Command "eth0" is unknown, try "ip link help"'
1133
+ if err.returncode != 255:
1134
+ raise err
1135
+ self._tested_arg = True
1136
+
1137
+ if self._iface_arg:
1138
+ if not command_output: # Don't repeat work on first run
1139
+ command_output = _popen("ip", "link show " + arg)
1140
+ return _search(arg + self._regex, command_output)
1141
+ else:
1142
+ # TODO: improve this regex to not need extra portion for no arg
1143
+ command_output = _popen("ip", "link")
1144
+ return _search(arg + r":" + self._regex, command_output)
1145
+
1146
+
1147
+ class DefaultIfaceLinuxRouteFile(Method):
1148
+ """
1149
+ Get the default interface by parsing the ``/proc/net/route`` file.
1150
+
1151
+ This is the same source as the ``route`` command, however it's much
1152
+ faster to read this file than to call ``route``. If it fails for whatever
1153
+ reason, we can fall back on the system commands (e.g for a platform that
1154
+ has a route command, but doesn't use ``/proc``, such as BSD-based platforms).
1155
+ """
1156
+
1157
+ platforms = {"linux", "wsl"}
1158
+ method_type = "default_iface"
1159
+
1160
+ def test(self): # type: () -> bool
1161
+ return check_path("/proc/net/route")
1162
+
1163
+ def get(self, arg=""): # type: (str) -> Optional[str]
1164
+ data = _read_file("/proc/net/route")
1165
+
1166
+ if data is not None and len(data) > 1:
1167
+ for line in data.split("\n")[1:-1]:
1168
+ line = line.strip()
1169
+ if not line:
1170
+ continue
1171
+
1172
+ # Some have tab separators, some have spaces
1173
+ if "\t" in line:
1174
+ sep = "\t"
1175
+ else:
1176
+ sep = " "
1177
+
1178
+ iface_name, dest = line.split(sep)[:2]
1179
+
1180
+ if dest == "00000000":
1181
+ return iface_name
1182
+
1183
+ if DEBUG:
1184
+ log.debug(
1185
+ "Failed to find default interface in data from "
1186
+ "'/proc/net/route', no destination of '00000000' was found"
1187
+ )
1188
+ elif DEBUG:
1189
+ log.warning("No data from /proc/net/route")
1190
+
1191
+ return None
1192
+
1193
+
1194
+ class DefaultIfaceRouteCommand(Method):
1195
+ platforms = {"linux", "wsl", "other"}
1196
+ method_type = "default_iface"
1197
+
1198
+ def test(self): # type: () -> bool
1199
+ return check_command("route")
1200
+
1201
+ def get(self, arg=""): # type: (str) -> Optional[str]
1202
+ output = _popen("route", "-n")
1203
+
1204
+ try:
1205
+ return output.partition("0.0.0.0")[2].partition("\n")[0].split()[-1]
1206
+ except IndexError as ex: # index errors means no default route in output?
1207
+ log.debug("DefaultIfaceRouteCommand failed for %s: %s", arg, str(ex))
1208
+ return None
1209
+
1210
+
1211
+ class DefaultIfaceRouteGetCommand(Method):
1212
+ platforms = {"darwin", "freebsd", "other"}
1213
+ method_type = "default_iface"
1214
+
1215
+ def test(self): # type: () -> bool
1216
+ return check_command("route")
1217
+
1218
+ def get(self, arg=""): # type: (str) -> Optional[str]
1219
+ output = _popen("route", "get default")
1220
+
1221
+ if not output:
1222
+ return None
1223
+
1224
+ try:
1225
+ return output.partition("interface: ")[2].strip().split()[0].strip()
1226
+ except IndexError as ex:
1227
+ log.debug("DefaultIfaceRouteCommand failed for %s: %s", arg, str(ex))
1228
+ return None
1229
+
1230
+
1231
+ class DefaultIfaceIpRoute(Method):
1232
+ # NOTE: this is slightly faster than "route" since
1233
+ # there is less output than "route -n"
1234
+ platforms = {"linux", "wsl", "other"}
1235
+ method_type = "default_iface"
1236
+
1237
+ def test(self): # type: () -> bool
1238
+ return check_command("ip")
1239
+
1240
+ def get(self, arg=""): # type: (str) -> Optional[str]
1241
+ output = _popen("ip", "route list 0/0")
1242
+
1243
+ if not output:
1244
+ if DEBUG:
1245
+ log.debug("DefaultIfaceIpRoute failed: no output")
1246
+ return None
1247
+
1248
+ return output.partition("dev")[2].partition("proto")[0].strip()
1249
+
1250
+
1251
+ class DefaultIfaceOpenBsd(Method):
1252
+ platforms = {"openbsd"}
1253
+ method_type = "default_iface"
1254
+
1255
+ def test(self): # type: () -> bool
1256
+ return check_command("route")
1257
+
1258
+ def get(self, arg=""): # type: (str) -> Optional[str]
1259
+ output = _popen("route", "-nq show -inet -gateway -priority 1")
1260
+ return output.partition("127.0.0.1")[0].strip().rpartition(" ")[2]
1261
+
1262
+
1263
+ class DefaultIfaceFreeBsd(Method):
1264
+ platforms = {"freebsd"}
1265
+ method_type = "default_iface"
1266
+
1267
+ def test(self): # type: () -> bool
1268
+ return check_command("netstat")
1269
+
1270
+ def get(self, arg=""): # type: (str) -> Optional[str]
1271
+ output = _popen("netstat", "-r")
1272
+ return _search(r"default[ ]+\S+[ ]+\S+[ ]+(\S+)[\r\n]+", output)
1273
+
1274
+
1275
+ # TODO: order methods by effectiveness/reliability
1276
+ # Use a class attribute maybe? e.g. "score", then sort by score in cache
1277
+ METHODS = [
1278
+ # NOTE: CtypesHost is faster than ArpExe because of sub-process startup times :)
1279
+ CtypesHost,
1280
+ ArpFile,
1281
+ ArpingHost,
1282
+ SysIfaceFile,
1283
+ FcntlIface,
1284
+ UuidLanscan,
1285
+ GetmacExe,
1286
+ IpconfigExe,
1287
+ WmicExe,
1288
+ ArpExe,
1289
+ DarwinNetworksetupIface,
1290
+ ArpFreebsd,
1291
+ ArpOpenbsd,
1292
+ IfconfigWithIfaceArg,
1293
+ IfconfigEther,
1294
+ IfconfigOther,
1295
+ IpLinkIface,
1296
+ NetstatIface,
1297
+ IpNeighborShow,
1298
+ ArpVariousArgs,
1299
+ UuidArpGetNode,
1300
+ DefaultIfaceLinuxRouteFile,
1301
+ DefaultIfaceIpRoute,
1302
+ DefaultIfaceRouteCommand,
1303
+ DefaultIfaceRouteGetCommand,
1304
+ DefaultIfaceOpenBsd,
1305
+ DefaultIfaceFreeBsd,
1306
+ ] # type: List[Type[Method]]
1307
+
1308
+ # Primary method to use for a given method type
1309
+ METHOD_CACHE = {
1310
+ "ip4": None,
1311
+ "ip6": None,
1312
+ "iface": None,
1313
+ "default_iface": None,
1314
+ } # type: Dict[str, Optional[Method]]
1315
+
1316
+
1317
+ # Order of methods is determined by:
1318
+ # Platform + version
1319
+ # Performance (file read > command)
1320
+ # Reliability (how well I know/understand the command to work)
1321
+ FALLBACK_CACHE = {
1322
+ "ip4": [],
1323
+ "ip6": [],
1324
+ "iface": [],
1325
+ "default_iface": [],
1326
+ } # type: Dict[str, List[Method]]
1327
+
1328
+
1329
+ DEFAULT_IFACE = "" # type: str
1330
+
1331
+
1332
+ def get_method_by_name(method_name):
1333
+ # type: (str) -> Optional[Type[Method]]
1334
+ for method in METHODS:
1335
+ if method.__name__.lower() == method_name.lower():
1336
+ return method
1337
+
1338
+ return None
1339
+
1340
+
1341
+ def get_instance_from_cache(method_type, method_name):
1342
+ # type: (str, str) -> Optional[Method]
1343
+ """
1344
+ Get the class for a named Method from the caches.
1345
+
1346
+ METHOD_CACHE is checked first, and if that fails,
1347
+ then any entries in FALLBACK_CACHE are checked.
1348
+ If both fail, None is returned.
1349
+
1350
+ Args:
1351
+ method_type: method type to initialize the cache for.
1352
+ Allowed values are: ``ip4`` | ``ip6`` | ``iface`` | ``default_iface``
1353
+ """
1354
+
1355
+ if str(METHOD_CACHE[method_type]) == method_name:
1356
+ return METHOD_CACHE[method_type]
1357
+
1358
+ for f_meth in FALLBACK_CACHE[method_type]:
1359
+ if str(f_meth) == method_name:
1360
+ return f_meth
1361
+
1362
+ return None
1363
+
1364
+
1365
+ def _swap_method_fallback(method_type, swap_with):
1366
+ # type: (str, str) -> bool
1367
+ if str(METHOD_CACHE[method_type]) == swap_with:
1368
+ return True
1369
+
1370
+ found = None # type: Optional[Method]
1371
+ for f_meth in FALLBACK_CACHE[method_type]:
1372
+ if str(f_meth) == swap_with:
1373
+ found = f_meth
1374
+ break
1375
+
1376
+ if not found:
1377
+ return False
1378
+
1379
+ curr = METHOD_CACHE[method_type]
1380
+ FALLBACK_CACHE[method_type].remove(found)
1381
+ METHOD_CACHE[method_type] = found
1382
+ FALLBACK_CACHE[method_type].insert(0, curr) # noqa: T484
1383
+
1384
+ return True
1385
+
1386
+
1387
+ def _warn_critical(err_msg):
1388
+ # type: (str) -> None
1389
+ log.critical(err_msg)
1390
+ warnings.warn(
1391
+ "%s. NOTICE: this warning will likely turn into a raised exception in getmac 1.0.0!"
1392
+ % err_msg,
1393
+ RuntimeWarning,
1394
+ )
1395
+
1396
+
1397
+ def initialize_method_cache(
1398
+ method_type, network_request=True
1399
+ ): # type: (str, bool) -> bool
1400
+ """
1401
+ Initialize the method cache for the given method type.
1402
+
1403
+ Args:
1404
+ method_type: method type to initialize the cache for.
1405
+ Allowed values are: ``ip4`` | ``ip6`` | ``iface`` | ``default_iface``
1406
+ network_request: if methods that make network requests should be included
1407
+ (those methods that have the attribute ``network_request`` set to ``True``)
1408
+ """
1409
+ if METHOD_CACHE.get(method_type):
1410
+ if DEBUG:
1411
+ log.debug(
1412
+ "Method cache already initialized for method type '%s'", method_type
1413
+ )
1414
+ return True
1415
+
1416
+ log.debug("Initializing '%s' method cache (platform: '%s')", method_type, PLATFORM)
1417
+
1418
+ if OVERRIDE_PLATFORM:
1419
+ log.warning(
1420
+ "Platform override is set, using '%s' as platform "
1421
+ "instead of detected platform '%s'",
1422
+ OVERRIDE_PLATFORM,
1423
+ PLATFORM,
1424
+ )
1425
+ platform = OVERRIDE_PLATFORM
1426
+ else:
1427
+ platform = PLATFORM
1428
+
1429
+ if DEBUG >= 4:
1430
+ meth_strs = ", ".join(m.__name__ for m in METHODS) # type: str
1431
+ log.debug("%d methods available: %s", len(METHODS), meth_strs)
1432
+
1433
+ # Filter methods by the type of MAC we're looking for, such as "ip"
1434
+ # for remote host methods or "iface" for local interface methods.
1435
+ type_methods = [
1436
+ method
1437
+ for method in METHODS
1438
+ if (method.method_type != "ip" and method.method_type == method_type)
1439
+ # Methods with a type of "ip" can handle both IPv4 and IPv6
1440
+ or (method.method_type == "ip" and method_type in ["ip4", "ip6"])
1441
+ ] # type: List[Type[Method]]
1442
+
1443
+ if not type_methods:
1444
+ _warn_critical("No valid methods matching MAC type '%s'" % method_type)
1445
+ return False
1446
+
1447
+ if DEBUG >= 2:
1448
+ type_strs = ", ".join(tm.__name__ for tm in type_methods) # type: str
1449
+ log.debug(
1450
+ "%d type-filtered methods for '%s': %s",
1451
+ len(type_methods),
1452
+ method_type,
1453
+ type_strs,
1454
+ )
1455
+
1456
+ # Filter methods by the platform we're running on
1457
+ platform_methods = [
1458
+ method for method in type_methods if platform in method.platforms
1459
+ ] # type: List[Type[Method]]
1460
+
1461
+ if not platform_methods:
1462
+ # If there isn't a method for the current platform,
1463
+ # then fallback to the generic platform "other".
1464
+ warn_msg = (
1465
+ "No methods for platform '%s'! Your system may not be supported. "
1466
+ "Falling back to platform 'other'." % platform
1467
+ )
1468
+ log.warning(warn_msg)
1469
+ warnings.warn(warn_msg, RuntimeWarning)
1470
+ platform_methods = [
1471
+ method for method in type_methods if "other" in method.platforms
1472
+ ]
1473
+
1474
+ if DEBUG >= 2:
1475
+ plat_strs = ", ".join(pm.__name__ for pm in platform_methods) # type: str
1476
+ log.debug(
1477
+ "%d platform-filtered methods for '%s' (method_type='%s'): %s",
1478
+ len(platform_methods),
1479
+ platform,
1480
+ method_type,
1481
+ plat_strs,
1482
+ )
1483
+
1484
+ if not platform_methods:
1485
+ _warn_critical(
1486
+ "No valid methods found for MAC type '%s' and platform '%s'"
1487
+ % (method_type, platform)
1488
+ )
1489
+ return False
1490
+
1491
+ filtered_methods = platform_methods # type: List[Type[Method]]
1492
+
1493
+ # If network_request is False, then remove any methods that have network_request=True
1494
+ if not network_request:
1495
+ filtered_methods = [m for m in platform_methods if not m.network_request]
1496
+
1497
+ # Determine which methods work on the current system
1498
+ tested_methods = [] # type: List[Method]
1499
+
1500
+ for method_class in filtered_methods:
1501
+ method_instance = method_class() # type: Method
1502
+ try:
1503
+ test_result = method_instance.test() # type: bool
1504
+ except Exception:
1505
+ test_result = False
1506
+ if test_result:
1507
+ tested_methods.append(method_instance)
1508
+ # First successful test goes in the cache
1509
+ if not METHOD_CACHE[method_type]:
1510
+ METHOD_CACHE[method_type] = method_instance
1511
+ elif DEBUG:
1512
+ log.debug("Test failed for method '%s'", str(method_instance))
1513
+
1514
+ if not tested_methods:
1515
+ _warn_critical(
1516
+ "All %d '%s' methods failed to test!" % (len(filtered_methods), method_type)
1517
+ )
1518
+ return False
1519
+
1520
+ if DEBUG >= 2:
1521
+ tested_strs = ", ".join(str(ts) for ts in tested_methods) # type: str
1522
+ log.debug(
1523
+ "%d tested methods for '%s': %s",
1524
+ len(tested_methods),
1525
+ method_type,
1526
+ tested_strs,
1527
+ )
1528
+
1529
+ # Populate fallback cache with all the tested methods, minus the currently active method
1530
+ if METHOD_CACHE[method_type] and METHOD_CACHE[method_type] in tested_methods:
1531
+ tested_methods.remove(METHOD_CACHE[method_type]) # noqa: T484
1532
+
1533
+ FALLBACK_CACHE[method_type] = tested_methods
1534
+
1535
+ if DEBUG:
1536
+ log.debug(
1537
+ "Current method cache: %s",
1538
+ str({k: str(v) for k, v in METHOD_CACHE.items()}),
1539
+ )
1540
+ log.debug(
1541
+ "Current fallback cache: %s",
1542
+ str({k: str(v) for k, v in FALLBACK_CACHE.items()}),
1543
+ )
1544
+ log.debug("Finished initializing '%s' method cache", method_type)
1545
+
1546
+ return True
1547
+
1548
+
1549
+ def _remove_unusable(method, method_type): # type: (Method, str) -> Optional[Method]
1550
+ if not FALLBACK_CACHE[method_type]:
1551
+ log.warning("No fallback method for unusable method '%s'!", str(method))
1552
+ METHOD_CACHE[method_type] = None
1553
+ else:
1554
+ METHOD_CACHE[method_type] = FALLBACK_CACHE[method_type].pop(0)
1555
+ log.warning(
1556
+ "Falling back to '%s' for unusable method '%s'",
1557
+ str(METHOD_CACHE[method_type]),
1558
+ str(method),
1559
+ )
1560
+
1561
+ return METHOD_CACHE[method_type]
1562
+
1563
+
1564
+ def _attempt_method_get(
1565
+ method, method_type, arg
1566
+ ): # type: (Method, str, str) -> Optional[str]
1567
+ """
1568
+ Attempt to use methods, and if they fail, fallback to the next method in the cache.
1569
+ """
1570
+ if not METHOD_CACHE[method_type] and not FALLBACK_CACHE[method_type]:
1571
+ _warn_critical("No usable methods found for MAC type '%s'" % method_type)
1572
+ return None
1573
+
1574
+ if DEBUG:
1575
+ log.debug(
1576
+ "Attempting get() (method='%s', method_type='%s', arg='%s')",
1577
+ str(method),
1578
+ method_type,
1579
+ arg,
1580
+ )
1581
+
1582
+ result = None
1583
+ try:
1584
+ result = method.get(arg)
1585
+ except CalledProcessError as ex:
1586
+ # Don't mark return code 1 on a process as unusable!
1587
+ # Example of return code 1 on ifconfig from WSL:
1588
+ # Blake:goesc$ ifconfig eth8
1589
+ # eth8: error fetching interface information: Device not found
1590
+ # Blake:goesc$ echo $?
1591
+ # 1
1592
+ # Methods where an exit code of 1 makes it invalid should handle the
1593
+ # CalledProcessError, inspect the return code, and set self.unusable = True
1594
+ if ex.returncode != 1:
1595
+ log.warning(
1596
+ "Cached Method '%s' failed for '%s' lookup with process exit "
1597
+ "code '%d' != 1, marking unusable. Exception: %s",
1598
+ str(method),
1599
+ method_type,
1600
+ ex.returncode,
1601
+ str(ex),
1602
+ )
1603
+ method.unusable = True
1604
+ except Exception as ex:
1605
+ log.warning(
1606
+ "Cached Method '%s' failed for '%s' lookup with unhandled exception: %s",
1607
+ str(method),
1608
+ method_type,
1609
+ str(ex),
1610
+ )
1611
+ method.unusable = True
1612
+
1613
+ # When an unhandled exception occurs (or exit code other than 1), remove
1614
+ # the method from the cache and reinitialize with next candidate.
1615
+ if not result and method.unusable:
1616
+ new_method = _remove_unusable(method, method_type)
1617
+
1618
+ if not new_method:
1619
+ return None
1620
+
1621
+ return _attempt_method_get(new_method, method_type, arg)
1622
+
1623
+ return result
1624
+
1625
+
1626
+ def get_by_method(method_type, arg="", network_request=True):
1627
+ # type: (str, str, bool) -> Optional[str]
1628
+ """
1629
+ Query for a MAC using a specific method.
1630
+
1631
+ Args:
1632
+ method_type: the type of lookup being performed.
1633
+ Allowed values are: ``ip4``, ``ip6``, ``iface``, ``default_iface``
1634
+ arg: Argument to pass to the method, e.g. an interface name or IP address
1635
+ network_request: if methods that make network requests should be included
1636
+ (those methods that have the attribute ``network_request`` set to ``True``)
1637
+ """
1638
+ if not arg and method_type != "default_iface":
1639
+ log.error("Empty arg for method '%s' (raw value: %s)", method_type, repr(arg))
1640
+ return None
1641
+
1642
+ if FORCE_METHOD:
1643
+ log.warning(
1644
+ "Forcing method '%s' to be used for '%s' lookup (arg: '%s')",
1645
+ FORCE_METHOD,
1646
+ method_type,
1647
+ arg,
1648
+ )
1649
+
1650
+ forced_method = get_method_by_name(FORCE_METHOD)
1651
+
1652
+ if not forced_method:
1653
+ log.error("Invalid FORCE_METHOD method name '%s'", FORCE_METHOD)
1654
+ return None
1655
+
1656
+ return forced_method().get(arg)
1657
+
1658
+ method = METHOD_CACHE.get(method_type) # type: Optional[Method]
1659
+
1660
+ if not method:
1661
+ # Initialize the cache if it hasn't been already
1662
+ if not initialize_method_cache(method_type, network_request):
1663
+ log.error(
1664
+ "Failed to initialize method cache for method '%s' (arg: '%s')",
1665
+ method_type,
1666
+ arg,
1667
+ )
1668
+ return None
1669
+
1670
+ method = METHOD_CACHE[method_type]
1671
+
1672
+ if not method:
1673
+ log.error(
1674
+ "Initialization failed for method '%s'. It may not be supported "
1675
+ "on this platform or another issue occurred.",
1676
+ method_type,
1677
+ )
1678
+ return None
1679
+
1680
+ # TODO: add a "net_ok" argument, check network_request attribute
1681
+ # on method in CACHE, if not then keep checking for method in
1682
+ # FALLBACK_CACHE that has network_request.
1683
+ result = _attempt_method_get(method, method_type, arg)
1684
+
1685
+ # Log normal get() failures if debugging is enabled
1686
+ if DEBUG and not result:
1687
+ log.debug("Method '%s' failed for '%s' lookup", str(method), method_type)
1688
+
1689
+ return result
1690
+
1691
+
1692
+ def get_mac_address( # noqa: C901
1693
+ interface=None, ip=None, ip6=None, hostname=None, network_request=True
1694
+ ):
1695
+ # type: (Optional[str], Optional[str], Optional[str], Optional[str], bool) -> Optional[str]
1696
+ """
1697
+ Get an Unicast IEEE 802 MAC-48 address from a local interface or remote host.
1698
+
1699
+ Only ONE of the first four arguments may be used
1700
+ (``interface``,``ip``, ``ip6``, or ``hostname``).
1701
+ If none of the arguments are selected, the default network interface for
1702
+ the system will be used.
1703
+
1704
+ .. warning::
1705
+ In getmac 1.0.0, exceptions will be raised if the method cache initialization fails
1706
+ (in other words, if there are no valid methods found for the type of MAC requested).
1707
+
1708
+ .. warning::
1709
+ You MUST provide :class:`str` typed arguments, REGARDLESS of Python version
1710
+
1711
+ .. note::
1712
+ ``"localhost"`` or ``"127.0.0.1"`` will always return ``"00:00:00:00:00:00"``
1713
+
1714
+ .. note::
1715
+ It is assumed that you are using Ethernet or Wi-Fi. While other protocols
1716
+ such as Bluetooth may work, this has not been tested and should not be
1717
+ relied upon. If you need this functionality, please open an issue
1718
+ (or better yet, a Pull Request ;))!
1719
+
1720
+ .. note::
1721
+ Exceptions raised by methods are handled silently and returned as :obj:`None`.
1722
+
1723
+ Args:
1724
+ interface (str): Name of a local network interface (e.g "Ethernet 3", "eth0", "ens32")
1725
+ ip (str): Canonical dotted decimal IPv4 address of a remote host (e.g ``192.168.0.1``)
1726
+ ip6 (str): Canonical shortened IPv6 address of a remote host (e.g ``ff02::1:ffe7:7f19``)
1727
+ hostname (str): DNS hostname of a remote host (e.g "router1.mycorp.com", "localhost")
1728
+ network_request (bool): If network requests should be made when attempting to find the
1729
+ MAC of a remote host. If the ``arping`` command is available, this will be used.
1730
+ If not, a UDP packet will be sent to the remote host to populate
1731
+ the ARP/NDP tables for IPv4/IPv6. The port this packet is sent to can
1732
+ be configured using the module variable ``getmac.PORT``.
1733
+
1734
+ Returns:
1735
+ Lowercase colon-separated MAC address, or :obj:`None` if one could not be
1736
+ found or there was an error.
1737
+ """ # noqa: E501
1738
+
1739
+ if DEBUG:
1740
+ import timeit
1741
+
1742
+ start_time = timeit.default_timer()
1743
+
1744
+ if PY2 or (sys.version_info[0] == 3 and sys.version_info[1] < 7):
1745
+ global WARNED_UNSUPPORTED_PYTHONS
1746
+ if not WARNED_UNSUPPORTED_PYTHONS:
1747
+ warning_string = (
1748
+ "Support for Python versions < 3.7 is deprecated and will be "
1749
+ "removed in getmac 1.0.0. If you are stuck on an unsupported "
1750
+ "Python, considor loosely pinning the version of this package "
1751
+ 'in your dependency list, e.g. "getmac<1.0.0" or "getmac~=0.9.0".'
1752
+ )
1753
+ warnings.warn(warning_string, DeprecationWarning)
1754
+ log.warning(warning_string) # Ensure it appears in any logs
1755
+ WARNED_UNSUPPORTED_PYTHONS = True
1756
+
1757
+ if (hostname and hostname == "localhost") or (ip and ip == "127.0.0.1"):
1758
+ return "00:00:00:00:00:00"
1759
+
1760
+ # Resolve hostname to an IP address
1761
+ if hostname:
1762
+ # Exceptions will be handled silently and returned as a None
1763
+ try:
1764
+ # TODO: can this return a IPv6 address? If so, handle that!
1765
+ ip = socket.gethostbyname(hostname)
1766
+ except Exception as ex:
1767
+ log.error("Could not resolve hostname '%s': %s", hostname, ex)
1768
+ if DEBUG:
1769
+ log.debug(traceback.format_exc())
1770
+ return None
1771
+
1772
+ if ip6:
1773
+ if not socket.has_ipv6:
1774
+ log.error(
1775
+ "Cannot get the MAC address of a IPv6 host: "
1776
+ "IPv6 is not supported on this system"
1777
+ )
1778
+ return None
1779
+ elif ":" not in ip6:
1780
+ log.error("Invalid IPv6 address (no ':'): %s", ip6)
1781
+ return None
1782
+
1783
+ mac = None
1784
+
1785
+ if network_request and (ip or ip6):
1786
+ send_udp_packet = True # type: bool
1787
+
1788
+ # If IPv4, use ArpingHost or CtypesHost if they're available instead
1789
+ # of populating the ARP table. This provides more reliable results
1790
+ # and a ARP packet is lower impact than a UDP packet.
1791
+ if ip:
1792
+ if not METHOD_CACHE["ip4"]:
1793
+ initialize_method_cache("ip4", network_request)
1794
+
1795
+ # If ArpFile succeeds, just use that, since it's
1796
+ # significantly faster than arping (file read vs.
1797
+ # spawning a process).
1798
+ if not FORCE_METHOD or FORCE_METHOD.lower() == "arpfile":
1799
+ af_meth = get_instance_from_cache("ip4", "ArpFile")
1800
+ if af_meth:
1801
+ mac = _attempt_method_get(af_meth, "ip4", ip)
1802
+
1803
+ # TODO: add tests for this logic (arpfile => fallback)
1804
+ # This seems to be a common course of GitHub issues,
1805
+ # so fixing it for good and adding robust tests is
1806
+ # probably a good idea.
1807
+
1808
+ if not mac:
1809
+ for arp_meth in ["CtypesHost", "ArpingHost"]:
1810
+ if FORCE_METHOD and FORCE_METHOD.lower() != arp_meth:
1811
+ continue
1812
+
1813
+ if arp_meth == str(METHOD_CACHE["ip4"]):
1814
+ send_udp_packet = False
1815
+ break
1816
+ elif any(
1817
+ arp_meth == str(x) for x in FALLBACK_CACHE["ip4"]
1818
+ ) and _swap_method_fallback("ip4", arp_meth):
1819
+ send_udp_packet = False
1820
+ break
1821
+
1822
+ # Populate the ARP table by sending an empty UDP packet to a high port
1823
+ if send_udp_packet and not mac:
1824
+ if DEBUG:
1825
+ log.debug(
1826
+ "Attempting to populate ARP table with UDP packet to %s:%d",
1827
+ ip if ip else ip6,
1828
+ PORT,
1829
+ )
1830
+
1831
+ if ip:
1832
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1833
+ else:
1834
+ sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
1835
+
1836
+ try:
1837
+ if ip:
1838
+ sock.sendto(b"", (ip, PORT))
1839
+ else:
1840
+ sock.sendto(b"", (ip6, PORT))
1841
+ except Exception:
1842
+ log.error("Failed to send ARP table population packet")
1843
+ if DEBUG:
1844
+ log.debug(traceback.format_exc())
1845
+ finally:
1846
+ sock.close()
1847
+ elif DEBUG:
1848
+ log.debug(
1849
+ "Not sending UDP packet, using network request method '%s' instead",
1850
+ str(METHOD_CACHE["ip4"]),
1851
+ )
1852
+
1853
+ # Setup the address hunt based on the arguments specified
1854
+ if not mac:
1855
+ if ip6:
1856
+ mac = get_by_method("ip6", ip6)
1857
+ elif ip:
1858
+ mac = get_by_method("ip4", ip)
1859
+ elif interface:
1860
+ mac = get_by_method("iface", interface)
1861
+ else: # Default to searching for interface
1862
+ # Default to finding MAC of the interface with the default route
1863
+ if WINDOWS and network_request:
1864
+ default_iface_ip = _fetch_ip_using_dns()
1865
+ mac = get_by_method("ip4", default_iface_ip)
1866
+ elif WINDOWS:
1867
+ # TODO: implement proper default interface detection on windows
1868
+ # (add a Method subclass to implement DefaultIface on Windows)
1869
+ mac = get_by_method("iface", "Ethernet")
1870
+ else:
1871
+ global DEFAULT_IFACE
1872
+
1873
+ if not DEFAULT_IFACE:
1874
+ DEFAULT_IFACE = get_by_method("default_iface") # noqa: T484
1875
+
1876
+ if DEFAULT_IFACE:
1877
+ DEFAULT_IFACE = str(DEFAULT_IFACE).strip()
1878
+
1879
+ # TODO: better fallback if default iface lookup fails
1880
+ if not DEFAULT_IFACE and BSD:
1881
+ DEFAULT_IFACE = "em0"
1882
+ elif not DEFAULT_IFACE and DARWIN: # OSX, maybe?
1883
+ DEFAULT_IFACE = "en0"
1884
+ elif not DEFAULT_IFACE:
1885
+ DEFAULT_IFACE = "eth0"
1886
+
1887
+ mac = get_by_method("iface", DEFAULT_IFACE)
1888
+
1889
+ # TODO: hack to fallback to loopback if lookup fails
1890
+ if not mac:
1891
+ mac = get_by_method("iface", "lo")
1892
+
1893
+ log.debug("Raw MAC found: %s", mac)
1894
+
1895
+ # Log how long it took
1896
+ if DEBUG:
1897
+ duration = timeit.default_timer() - start_time
1898
+ log.debug("getmac took %.4f seconds", duration)
1899
+
1900
+ return _clean_mac(mac)