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.
- zscams/__init__.py +0 -0
- zscams/__main__.py +101 -0
- zscams/agent/__init__.py +0 -0
- zscams/agent/certificates/.gitkeep +0 -0
- zscams/agent/config.yaml +121 -0
- zscams/agent/keys/autoport.key +27 -0
- zscams/agent/src/__init__.py +0 -0
- zscams/agent/src/core/__init__.py +1 -0
- zscams/agent/src/core/api/backend/bootstrap.py +21 -0
- zscams/agent/src/core/api/backend/client.py +242 -0
- zscams/agent/src/core/api/backend/exceptions.py +10 -0
- zscams/agent/src/core/api/backend/update_machine_info.py +16 -0
- zscams/agent/src/core/prerequisites.py +36 -0
- zscams/agent/src/core/service_health_check.py +49 -0
- zscams/agent/src/core/services.py +86 -0
- zscams/agent/src/core/tunnel/__init__.py +144 -0
- zscams/agent/src/core/tunnel/tls.py +56 -0
- zscams/agent/src/core/tunnels.py +55 -0
- zscams/agent/src/services/__init__.py +0 -0
- zscams/agent/src/services/reverse_ssh.py +73 -0
- zscams/agent/src/services/ssh_forwarder.py +75 -0
- zscams/agent/src/services/system_monitor.py +264 -0
- zscams/agent/src/support/__init__.py +0 -0
- zscams/agent/src/support/configuration.py +53 -0
- zscams/agent/src/support/filesystem.py +41 -0
- zscams/agent/src/support/logger.py +40 -0
- zscams/agent/src/support/mac.py +18 -0
- zscams/agent/src/support/network.py +49 -0
- zscams/agent/src/support/openssl.py +100 -0
- zscams/libs/getmac/__init__.py +3 -0
- zscams/libs/getmac/__main__.py +123 -0
- zscams/libs/getmac/getmac.py +1900 -0
- zscams/libs/getmac/shutilwhich.py +67 -0
- zscams-2.0.1.dist-info/METADATA +118 -0
- zscams-2.0.1.dist-info/RECORD +37 -0
- zscams-2.0.1.dist-info/WHEEL +4 -0
- 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)
|