Chris Johns deb54b6145 misc/tftpproxy: Add a proxy TFTP server.
- Uses a config INI file to map clients to servers

- Handle a number of requests to a single server's TFTP port (69)
  and multiplex to a non-su ports or different servers.

- Supports running rtems-test to more than one hardware device using
  TFTP at once.
2019-08-19 16:44:11 +10:00

604 lines
20 KiB
Python

# -*- coding: utf-8 -*-
# http://multivax.com/last_question.html
"""Get the MAC address of remote hosts or network interfaces.
It provides a platform-independent interface to get the MAC addresses of:
* System network interfaces (by interface name)
* Remote hosts on the local network (by IPv4/IPv6 address or hostname)
It provides one function: `get_mac_address()`
Examples:
from getmac import get_mac_address
eth_mac = get_mac_address(interface="eth0")
win_mac = get_mac_address(interface="Ethernet 3")
ip_mac = get_mac_address(ip="192.168.0.1")
ip6_mac = get_mac_address(ip6="::1")
host_mac = get_mac_address(hostname="localhost")
updated_mac = get_mac_address(ip="10.0.0.1", network_request=True)
"""
import ctypes
import logging
import os
import platform
import re
import shlex
import socket
import struct
import sys
import traceback
from subprocess import check_output
try: # Python 3
from subprocess import DEVNULL # type: ignore
except ImportError: # Python 2
DEVNULL = open(os.devnull, 'wb') # type: ignore
# Configure logging
log = logging.getLogger('getmac')
log.addHandler(logging.NullHandler())
__version__ = '0.8.1'
PY2 = sys.version_info[0] == 2
# Configurable settings
DEBUG = 0
PORT = 55555
# Platform identifiers
_SYST = platform.system()
if _SYST == 'Java':
try:
import java.lang
_SYST = str(java.lang.System.getProperty("os.name"))
except ImportError:
log.critical("Can't determine OS: couldn't import java.lang on Jython")
WINDOWS = _SYST == 'Windows'
DARWIN = _SYST == 'Darwin'
OPENBSD = _SYST == 'OpenBSD'
FREEBSD = _SYST == 'FreeBSD'
BSD = OPENBSD or FREEBSD # Not including Darwin for now
WSL = False # Windows Subsystem for Linux (WSL)
LINUX = False
if _SYST == 'Linux':
if 'Microsoft' in platform.version():
WSL = True
else:
LINUX = True
PATH = os.environ.get('PATH', os.defpath).split(os.pathsep)
if not WINDOWS:
PATH.extend(('/sbin', '/usr/sbin'))
# Use a copy of the environment so we don't
# modify the process's current environment.
ENV = dict(os.environ)
ENV['LC_ALL'] = 'C' # Ensure ASCII output so we parse correctly
# Constants
IP4 = 0
IP6 = 1
INTERFACE = 2
HOSTNAME = 3
MAC_RE_COLON = r'([0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5})'
MAC_RE_DASH = r'([0-9a-fA-F]{2}(?:-[0-9a-fA-F]{2}){5})'
MAC_RE_DARWIN = r'([0-9a-fA-F]{1,2}(?::[0-9a-fA-F]{1,2}){5})'
# Used for mypy (a data type analysis tool)
# If you're copying the code, this section can be safely removed
try:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Optional
except ImportError:
pass
def get_mac_address(
interface=None, ip=None, ip6=None,
hostname=None, network_request=True
):
# type: (Optional[str], Optional[str], Optional[str], Optional[str], bool) -> Optional[str]
"""Get a Unicast IEEE 802 MAC-48 address from a local interface or remote host.
You must only use one of the first four arguments. If none of the arguments
are selected, the default network interface for the system will be used.
Exceptions will be handled silently and returned as a None.
For the time being, it assumes you are using Ethernet.
NOTES:
* You MUST provide str-typed arguments, REGARDLESS of Python version.
* localhost/127.0.0.1 will always return '00:00:00:00:00:00'
Args:
interface (str): Name of a local network interface (e.g "Ethernet 3", "eth0", "ens32")
ip (str): Canonical dotted decimal IPv4 address of a remote host (e.g 192.168.0.1)
ip6 (str): Canonical shortened IPv6 address of a remote host (e.g ff02::1:ffe7:7f19)
hostname (str): DNS hostname of a remote host (e.g "router1.mycorp.com", "localhost")
network_request (bool): Send a UDP packet to a remote host to populate
the ARP/NDP tables for IPv4/IPv6. The port this packet is sent to can
be configured using the module variable `getmac.PORT`.
Returns:
Lowercase colon-separated MAC address, or None if one could not be
found or there was an error.
"""
if (hostname and hostname == 'localhost') or (ip and ip == '127.0.0.1'):
return '00:00:00:00:00:00'
# Resolve hostname to an IP address
if hostname:
ip = socket.gethostbyname(hostname)
# Populate the ARP table by sending a empty UDP packet to a high port
if network_request and (ip or ip6):
if ip:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
else:
s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
try:
if ip:
s.sendto(b'', (ip, PORT))
else:
s.sendto(b'', (ip6, PORT))
except Exception:
log.error("Failed to send ARP table population packet")
if DEBUG:
log.debug(traceback.format_exc())
finally:
s.close()
# Setup the address hunt based on the arguments specified
if ip6:
if not socket.has_ipv6:
log.error("Cannot get the MAC address of a IPv6 host: "
"IPv6 is not supported on this system")
return None
elif ':' not in ip6:
log.error("Invalid IPv6 address: %s", ip6)
return None
to_find = ip6
typ = IP6
elif ip:
to_find = ip
typ = IP4
else: # Default to searching for interface
typ = INTERFACE
if interface:
to_find = interface
else:
# Default to finding MAC of the interface with the default route
if WINDOWS and network_request:
to_find = _fetch_ip_using_dns()
typ = IP4
elif WINDOWS:
to_find = 'Ethernet'
elif BSD:
if OPENBSD:
to_find = _get_default_iface_openbsd() # type: ignore
else:
to_find = _get_default_iface_freebsd() # type: ignore
if not to_find:
to_find = 'em0'
else:
to_find = _hunt_linux_default_iface() # type: ignore
if not to_find:
to_find = 'en0'
mac = _hunt_for_mac(to_find, typ, network_request)
log.debug("Raw MAC found: %s", mac)
# Check and format the result to be lowercase, colon-separated
if mac is not None:
mac = str(mac)
if not PY2: # Strip bytestring conversion artifacts
mac = mac.replace("b'", '').replace("'", '')\
.replace('\\n', '').replace('\\r', '')
mac = mac.strip().lower().replace(' ', '').replace('-', ':')
# Fix cases where there are no colons
if ':' not in mac and len(mac) == 12:
log.debug("Adding colons to MAC %s", mac)
mac = ':'.join(mac[i:i + 2] for i in range(0, len(mac), 2))
# Pad single-character octets with a leading zero (e.g Darwin's ARP output)
elif len(mac) < 17:
log.debug("Length of MAC %s is %d, padding single-character "
"octets with zeros", mac, len(mac))
parts = mac.split(':')
new_mac = []
for part in parts:
if len(part) == 1:
new_mac.append('0' + part)
else:
new_mac.append(part)
mac = ':'.join(new_mac)
# MAC address should ALWAYS be 17 characters before being returned
if len(mac) != 17:
log.warning("MAC address %s is not 17 characters long!", mac)
mac = None
elif mac.count(':') != 5:
log.warning("MAC address %s is missing ':' characters", mac)
mac = None
return mac
def _search(regex, text, group_index=0):
# type: (str, str, int) -> Optional[str]
match = re.search(regex, text)
if match:
return match.groups()[group_index]
return None
def _popen(command, args):
# type: (str, str) -> str
for directory in PATH:
executable = os.path.join(directory, command)
if (os.path.exists(executable)
and os.access(executable, os.F_OK | os.X_OK)
and not os.path.isdir(executable)):
break
else:
executable = command
if DEBUG >= 3:
log.debug("Running: '%s %s'", executable, args)
return _call_proc(executable, args)
def _call_proc(executable, args):
# type: (str, str) -> str
if WINDOWS:
cmd = executable + ' ' + args # type: ignore
else:
cmd = [executable] + shlex.split(args) # type: ignore
output = check_output(cmd, stderr=DEVNULL, env=ENV)
if DEBUG >= 4:
log.debug("Output from '%s' command: %s", executable, str(output))
if not PY2 and isinstance(output, bytes):
return str(output, 'utf-8')
else:
return str(output)
def _windows_ctypes_host(host):
# type: (str) -> Optional[str]
if not PY2: # Convert to bytes on Python 3+ (Fixes GitHub issue #7)
host = host.encode() # type: ignore
try:
inetaddr = ctypes.windll.wsock32.inet_addr(host) # type: ignore
if inetaddr in (0, -1):
raise Exception
except Exception:
hostip = socket.gethostbyname(host)
inetaddr = ctypes.windll.wsock32.inet_addr(hostip) # type: ignore
buffer = ctypes.c_buffer(6)
addlen = ctypes.c_ulong(ctypes.sizeof(buffer))
send_arp = ctypes.windll.Iphlpapi.SendARP # type: ignore
if send_arp(inetaddr, 0, ctypes.byref(buffer), ctypes.byref(addlen)) != 0:
return None
# Convert binary data into a string.
macaddr = ''
for intval in struct.unpack('BBBBBB', buffer): # type: ignore
if intval > 15:
replacestr = '0x'
else:
replacestr = 'x'
macaddr = ''.join([macaddr, hex(intval).replace(replacestr, '')])
return macaddr
def _fcntl_iface(iface):
# type: (str) -> str
import fcntl
if not PY2:
iface = iface.encode() # type: ignore
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 0x8927 = SIOCGIFADDR
info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', iface[:15]))
if PY2:
return ':'.join(['%02x' % ord(char) for char in info[18:24]])
else:
return ':'.join(['%02x' % ord(chr(char)) for char in info[18:24]])
def _uuid_ip(ip):
# type: (str) -> Optional[str]
from uuid import _arp_getnode # type: ignore
backup = socket.gethostbyname
try:
socket.gethostbyname = lambda x: ip
mac1 = _arp_getnode()
if mac1 is not None:
mac1 = _uuid_convert(mac1)
mac2 = _arp_getnode()
mac2 = _uuid_convert(mac2)
if mac1 == mac2:
return mac1
except Exception:
raise
finally:
socket.gethostbyname = backup
return None
def _uuid_lanscan_iface(iface):
# type: (str) -> Optional[str]
from uuid import _find_mac # type: ignore
if not PY2:
iface = bytes(iface, 'utf-8') # type: ignore
mac = _find_mac('lanscan', '-ai', [iface], lambda i: 0)
if mac:
return _uuid_convert(mac)
return None
def _uuid_convert(mac):
# type: (int) -> str
return ':'.join(('%012X' % mac)[i:i+2] for i in range(0, 12, 2))
def _read_sys_iface_file(iface):
# type: (str) -> Optional[str]
data = _read_file('/sys/class/net/' + iface + '/address')
# Sometimes this can be empty or a single newline character
return None if data is not None and len(data) < 17 else data
def _read_arp_file(host):
# type: (str) -> Optional[str]
data = _read_file('/proc/net/arp')
if data is not None and len(data) > 1:
# Need a space, otherwise a search for 192.168.16.2
# will match 192.168.16.254 if it comes first!
return _search(re.escape(host) + r' .+' + MAC_RE_COLON, data)
return None
def _read_file(filepath):
# type: (str) -> Optional[str]
try:
with open(filepath) as f:
return f.read()
except (OSError, IOError): # This is IOError on Python 2.7
log.debug("Could not find file: '%s'", filepath)
return None
def _hunt_for_mac(to_find, type_of_thing, net_ok=True):
# type: (Optional[str], int, bool) -> Optional[str]
"""Tries a variety of methods to get a MAC address.
Format of method lists:
Tuple: (regex, regex index, command, command args)
Command args is a list of strings to attempt to use as arguments
lambda: Function to call
"""
if to_find is None:
log.warning("_hunt_for_mac() failed: to_find is None")
return None
if not PY2 and isinstance(to_find, bytes):
to_find = str(to_find, 'utf-8')
if WINDOWS and type_of_thing == INTERFACE:
methods = [
# getmac - Connection Name
(r'\r\n' + to_find + r'.*' + MAC_RE_DASH + r'.*\r\n',
0, 'getmac.exe', ['/NH /V']),
# ipconfig
(to_find + r'(?:\n?[^\n]*){1,8}Physical Address[ .:]+' + MAC_RE_DASH + r'\r\n',
0, 'ipconfig.exe', ['/all']),
# getmac - Network Adapter (the human-readable name)
(r'\r\n.*' + to_find + r'.*' + MAC_RE_DASH + r'.*\r\n',
0, 'getmac.exe', ['/NH /V']),
# wmic - WMI command line utility
lambda x: _popen('wmic.exe', 'nic where "NetConnectionID = \'%s\'" get '
'MACAddress /value' % x).strip().partition('=')[2],
]
elif (WINDOWS or WSL) and type_of_thing in [IP4, IP6, HOSTNAME]:
methods = [
# arp -a - Parsing result with a regex
(MAC_RE_DASH, 0, 'arp.exe', ['-a %s' % to_find]),
]
# Add methods that make network requests
# Insert it *after* arp.exe since that's probably faster.
if net_ok and type_of_thing != IP6 and not WSL:
methods.insert(1, _windows_ctypes_host)
elif (DARWIN or FREEBSD) and type_of_thing == INTERFACE:
methods = [
(r'ether ' + MAC_RE_COLON,
0, 'ifconfig', [to_find]),
# Alternative match for ifconfig if it fails
(to_find + r'.*ether ' + MAC_RE_COLON,
0, 'ifconfig', ['']),
(MAC_RE_COLON,
0, 'networksetup', ['-getmacaddress %s' % to_find]),
]
elif FREEBSD and type_of_thing in [IP4, IP6, HOSTNAME]:
methods = [
(r'\(' + re.escape(to_find) + r'\)\s+at\s+' + MAC_RE_COLON,
0, 'arp', [to_find])
]
elif OPENBSD and type_of_thing == INTERFACE:
methods = [
(r'lladdr ' + MAC_RE_COLON,
0, 'ifconfig', [to_find]),
]
elif OPENBSD and type_of_thing in [IP4, IP6, HOSTNAME]:
methods = [
(re.escape(to_find) + r'[ ]+' + MAC_RE_COLON,
0, 'arp', ['-an']),
]
elif type_of_thing == INTERFACE:
methods = [
_read_sys_iface_file,
_fcntl_iface,
# Fast modern Ubuntu ifconfig
(r'ether ' + MAC_RE_COLON,
0, 'ifconfig', [to_find]),
# Fast ifconfig
(r'HWaddr ' + MAC_RE_COLON,
0, 'ifconfig', [to_find]),
# ip link (Don't use 'list' due to SELinux [Android 24+])
(to_find + r'.*\n.*link/ether ' + MAC_RE_COLON,
0, 'ip', ['link %s' % to_find, 'link']),
# netstat
(to_find + r'.*HWaddr ' + MAC_RE_COLON,
0, 'netstat', ['-iae']),
# More variations of ifconfig
(to_find + r'.*ether ' + MAC_RE_COLON,
0, 'ifconfig', ['']),
(to_find + r'.*HWaddr ' + MAC_RE_COLON,
0, 'ifconfig', ['', '-a', '-v']),
# Tru64 ('-av')
(to_find + r'.*Ether ' + MAC_RE_COLON,
0, 'ifconfig', ['-av']),
_uuid_lanscan_iface,
]
elif type_of_thing in [IP4, IP6, HOSTNAME]:
esc = re.escape(to_find)
methods = [
_read_arp_file,
lambda x: _popen('ip', 'neighbor show %s' % x)
.partition(x)[2].partition('lladdr')[2].strip().split()[0],
(r'\(' + esc + r'\)\s+at\s+' + MAC_RE_COLON,
0, 'arp', [to_find, '-an', '-an %s' % to_find]),
# Darwin oddness
(r'\(' + esc + r'\)\s+at\s+' + MAC_RE_DARWIN,
0, 'arp', [to_find, '-a', '-a %s' % to_find]),
_uuid_ip,
]
else:
log.critical("Reached end of _hunt_for_mac() if-else chain!")
return None
return _try_methods(methods, to_find)
def _try_methods(methods, to_find=None):
# type: (list, Optional[str]) -> Optional[str]
"""Runs the methods specified by _hunt_for_mac().
We try every method and see if it returned a MAC address. If it returns
None or raises an exception, we continue and try the next method.
"""
found = None
for m in methods:
try:
if isinstance(m, tuple):
for arg in m[3]: # list(str)
if DEBUG:
log.debug("Trying: '%s %s'", m[2], arg)
# Arguments: (regex, _popen(command, arg), regex index)
found = _search(m[0], _popen(m[2], arg), m[1])
if DEBUG:
log.debug("Result: %s\n", found)
if found: # Skip remaining args AND remaining methods
break
elif callable(m):
if DEBUG:
log.debug("Trying: '%s' (to_find: '%s')", m.__name__, str(to_find))
if to_find is not None:
found = m(to_find)
else:
found = m()
if DEBUG:
log.debug("Result: %s\n", found)
else:
log.critical("Invalid type '%s' for method '%s'", type(m), str(m))
except Exception as ex:
if DEBUG:
log.debug("Exception: %s", str(ex))
if DEBUG >= 2:
log.debug(traceback.format_exc())
continue
if found: # Skip remaining methods
break
return found
def _get_default_iface_linux():
# type: () -> Optional[str]
"""Get the default interface by reading /proc/net/route.
This is the same source as the `route` command, however it's much
faster to read this file than to call `route`. If it fails for whatever
reason, we can fall back on the system commands (e.g for a platform
that has a route command, but maybe doesn't use /proc?).
"""
data = _read_file('/proc/net/route')
if data is not None and len(data) > 1:
for line in data.split('\n')[1:-1]:
iface_name, dest = line.split('\t')[:2]
if dest == '00000000':
return iface_name
return None
def _hunt_linux_default_iface():
# type: () -> Optional[str]
# NOTE: for now, we check the default interface for WSL using the
# same methods as POSIX, since those parts of the net stack work fine.
methods = [
_get_default_iface_linux,
lambda: _popen('route', '-n').partition('0.0.0.0')[2].partition('\n')[0].split()[-1],
lambda: _popen('ip', 'route list 0/0').partition('dev')[2].partition('proto')[0].strip(),
]
return _try_methods(methods)
def _get_default_iface_openbsd():
# type: () -> Optional[str]
methods = [
lambda: _popen('route', '-nq show -inet -gateway -priority 1')
.partition('127.0.0.1')[0].strip().rpartition(' ')[2],
]
return _try_methods(methods)
def _get_default_iface_freebsd():
# type: () -> Optional[str]
methods = [
(r'default[ ]+\S+[ ]+\S+[ ]+(\S+)\n',
0, 'netstat', ['-r']),
]
return _try_methods(methods)
def _fetch_ip_using_dns():
# type: () -> str
"""Determines the IP address of the default network interface.
Sends a UDP packet to Cloudflare's DNS (1.1.1.1), which should go through
the default interface. This populates the source address of the socket,
which we then inspect and return.
"""
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(('1.1.1.1', 53))
ip = s.getsockname()[0]
s.close() # NOTE: sockets don't have context manager in 2.7 :(
return ip