From: Chris Johns <chr...@rtems.org> - 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. --- misc/rtems-tftp-proxy | 42 +++ misc/tools/cmd-tftpproxy.py | 44 +++ misc/tools/getmac/LICENSE | 21 ++ misc/tools/getmac/__init__.py | 2 + misc/tools/getmac/__main__.py | 67 ++++ misc/tools/getmac/getmac.py | 603 ++++++++++++++++++++++++++++++++++ misc/tools/tftpproxy.py | 423 ++++++++++++++++++++++++ misc/wscript | 9 +- 8 files changed, 1209 insertions(+), 2 deletions(-) create mode 100755 misc/rtems-tftp-proxy create mode 100755 misc/tools/cmd-tftpproxy.py create mode 100644 misc/tools/getmac/LICENSE create mode 100644 misc/tools/getmac/__init__.py create mode 100644 misc/tools/getmac/__main__.py create mode 100644 misc/tools/getmac/getmac.py create mode 100644 misc/tools/tftpproxy.py diff --git a/misc/rtems-tftp-proxy b/misc/rtems-tftp-proxy new file mode 100755 index 0000000..213311d --- /dev/null +++ b/misc/rtems-tftp-proxy @@ -0,0 +1,42 @@ +#! /bin/sh +# +# RTEMS Tools Project (http://www.rtems.org/) +# Copyright 2019 Chris Johns (chr...@rtems.org) +# All rights reserved. +# +# This file is part of the RTEMS Tools package in 'rtems-tools'. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +set -e +base=$(dirname $(dirname $0)) +cmd=misc/tools/cmd-tftpproxy.py +PYTHON_WRAPPER=rtemstoolkit/python-wrapper.sh +if test -f ${base}/${PYTHON_WRAPPER}; then + PYTHON_CMD=${base}/${cmd} + . ${base}/${PYTHON_WRAPPER} +elif test -f ${base}/share/rtems/${PYTHON_WRAPPER}; then + PYTHON_CMD=${base}/share/rtems/${cmd} + . ${base}/share/rtems/${PYTHON_WRAPPER} +fi +echo "error: RTEMS Toolkit python wrapper not found, please report" diff --git a/misc/tools/cmd-tftpproxy.py b/misc/tools/cmd-tftpproxy.py new file mode 100755 index 0000000..9f66a76 --- /dev/null +++ b/misc/tools/cmd-tftpproxy.py @@ -0,0 +1,44 @@ +# +# RTEMS Tools Project (http://www.rtems.org/) +# Copyright 2019 Chris Johns (chr...@rtems.org) +# All rights reserved. +# +# This file is part of the RTEMS Tools package in 'rtems-tools'. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +from __future__ import print_function + +import sys, os + +base = os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[0]))) +rtems = os.path.dirname(base) +sys.path = [rtems] + sys.path + +try: + import tftpproxy + tftpproxy.run(sys.argv[1:], command_path = base) +except ImportError: + print("Incorrect RTEMS Tools installation", file = sys.stderr) + sys.exit(1) diff --git a/misc/tools/getmac/LICENSE b/misc/tools/getmac/LICENSE new file mode 100644 index 0000000..1a71c0f --- /dev/null +++ b/misc/tools/getmac/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Christopher Goes + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/misc/tools/getmac/__init__.py b/misc/tools/getmac/__init__.py new file mode 100644 index 0000000..fec68bc --- /dev/null +++ b/misc/tools/getmac/__init__.py @@ -0,0 +1,2 @@ +from .getmac import __version__, get_mac_address +__all__ = ['get_mac_address'] diff --git a/misc/tools/getmac/__main__.py b/misc/tools/getmac/__main__.py new file mode 100644 index 0000000..5fbe0f7 --- /dev/null +++ b/misc/tools/getmac/__main__.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from __future__ import print_function + +import argparse +import logging +import sys + +from . import getmac + + +def main(): + parser = argparse.ArgumentParser( + 'getmac', description='Get the MAC address of system network ' + 'interfaces or remote hosts on the LAN') + parser.add_argument( + '--version', action='version', + version='getmac %s' % getmac.__version__) + parser.add_argument( + '-v', '--verbose', action='store_true', + help='Enable output messages') + parser.add_argument( + '-d', '--debug', action='count', + help='Enable debugging output. Add characters to ' + 'increase verbosity of output, e.g. \'-dd\'.') + parser.add_argument( + '-N', '--no-net', '--no-network-requests', + action='store_true', dest='NO_NET', + help='Do not send a UDP packet to refresh the ARP table') + + group = parser.add_mutually_exclusive_group(required=False) + group.add_argument( + '-i', '--interface', type=str, default=None, + help='Name of a network interface on the system') + group.add_argument( + '-4', '--ip', type=str, default=None, + help='IPv4 address of a remote host') + group.add_argument( + '-6', '--ip6', type=str, default=None, + help='IPv6 address of a remote host') + group.add_argument( + '-n', '--hostname', type=str, default=None, + help='Hostname of a remote host') + + args = parser.parse_args() + + if args.debug or args.verbose: + logging.basicConfig(format='%(levelname)-8s %(message)s', + level=logging.DEBUG, stream=sys.stderr) + if args.debug: + getmac.DEBUG = args.debug + + mac = getmac.get_mac_address( + interface=args.interface, ip=args.ip, + ip6=args.ip6, hostname=args.hostname, + network_request=not args.NO_NET) + + if mac is not None: + print(mac) # noqa: T001 + sys.exit(0) # Exit success! + else: + sys.exit(1) # Exit with error since it failed to find a MAC + + +if __name__ == '__main__': + main() diff --git a/misc/tools/getmac/getmac.py b/misc/tools/getmac/getmac.py new file mode 100644 index 0000000..d5555fb --- /dev/null +++ b/misc/tools/getmac/getmac.py @@ -0,0 +1,603 @@ +# -*- 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 diff --git a/misc/tools/tftpproxy.py b/misc/tools/tftpproxy.py new file mode 100644 index 0000000..a815584 --- /dev/null +++ b/misc/tools/tftpproxy.py @@ -0,0 +1,423 @@ +# +# Copyright 2019 Chris Johns (chris@contemporary.software) +# All rights reserved. +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +# +# The TFTP proxy redirects a TFTP session to another host. If you have a +# farm of boards you can configure them to point to this proxy and it will +# redirect the requests to another machine that is testing it. +# + +from __future__ import print_function + +import argparse +import os +import socket +import sys +import time +import threading + +try: + import socketserver +except: + import SocketServer as socketserver + +from rtemstoolkit import configuration +from rtemstoolkit import error +from rtemstoolkit import log +from rtemstoolkit import version + +import getmac + +def host_port_split(ip_port): + ips = ip_port.split(':') + port = 0 + if len(ips) >= 1: + ip = ips[0] + if len(ips) == 2: + port = int(ips[1]) + else: + raise error.general('invalid host:port: %s' % (ip_port)) + return ip, port + +class tftp_session(object): + + opcodes = ['nul', 'RRQ', 'WRQ', 'DATA', 'ACK', 'ERROR', 'OACK'] + + def __init__(self): + self.packets = [] + self.block = 0 + self.block_size = 512 + self.timeout = 0 + self.finished = True + + def __str__(self): + return os.linesep.join([self.decode(p[0], p[1], p[2]) for p in self.packets]) + + def data(self, host, port, data): + finished = False + self.packets += [(host, port, data)] + opcode = (data[0] << 8) | data[1] + if opcode == 1 or opcode == 2: + self.block = 0 + self.finished = False + value = self.get_option('timeout', data) + if value is not None: + self.timeout = int(value) + value = self.get_option('blksize', data) + if value is not None: + self.block_size = int(value) + else: + self.block_size = 512 + elif opcode == 3: + self.block = (data[2] << 8) | data[3] + if len(data) - 4 < self.block_size: + self.finished = True + elif opcode == 4: + self.block = (data[2] << 8) | data[3] + if self.finished: + finished = True + return finished + + def decode(self, host, port, data): + s = '' + dlen = len(data) + if dlen > 2: + opcode = (data[0] << 8) | data[1] + if opcode < len(self.opcodes): + if opcode == 1 or opcode == 2: + s += ' ' + self.opcodes[opcode] + ', ' + i = 2 + while data[i] != 0: + s += chr(data[i]) + i += 1 + while i < dlen - 1: + s += ', ' + i += 1 + while data[i] != 0: + s += chr(data[i]) + i += 1 + elif opcode == 3: + block = (data[2] << 8) | data[3] + s += ' ' + self.opcodes[opcode] + ', ' + s += '#' + str(block) + ', ' + if dlen > 4: + s += '%02x%02x..%02x%02x' % (data[4], data[5], data[-2], data[-1]) + else: + s += '%02x%02x%02x%02x' % (data[4], data[5], data[6], data[6]) + s += ',' + str(dlen - 4) + elif opcode == 4: + block = (data[2] << 8) | data[3] + s += ' ' + self.opcodes[opcode] + ' ' + str(block) + elif opcode == 5: + s += 'E ' + self.opcodes[opcode] + ', ' + s += str((data[2] << 8) | (data[3])) + i = 2 + while data[i] != 0: + s += chr(data[i]) + i += 1 + elif opcode == 6: + s += ' ' + self.opcodes[opcode] + i = 1 + while i < dlen - 1: + s += ', ' + i += 1 + while data[i] != 0: + s += chr(data[i]) + i += 1 + else: + s += 'E INV(%d)' % (opcode) + else: + s += 'E INVALID LENGTH' + return s[:2] + '[%s:%d] ' % (host, port) + s[2:] + + def get_option(self, option, data): + dlen = len(data) + opcode = (data[0] << 8) | data[1] + next_option = False + if opcode == 1 or opcode == 2: + i = 1 + while i < dlen - 1: + o = '' + i += 1 + while data[i] != 0: + o += chr(data[i]) + i += 1 + if o == option: + next_option = True + elif next_option: + return o + return None + + def get_timeout(self, default_timeout, timeout_guard): + if self.timeout == 0: + return self.timeout + timeout_guard + return default_timeout + + def get_block_size(self): + return self.block_size + +class udp_handler(socketserver.BaseRequestHandler): + + def handle(self): + client_ip = self.client_address[0] + client_port = self.client_address[1] + client = '%s:%i' % (client_ip, client_port) + session = tftp_session() + finished = session.data(client_ip, client_port, self.request[0]) + if not finished: + timeout = session.get_timeout(self.server.proxy.session_timeout, 1) + host = self.server.proxy.get_host(client_ip) + if host is not None: + session_count = self.server.proxy.get_session_count() + log.notice(' ] %6d: session: %s -> %s: start' % (session_count, + client, + host)) + host_ip, host_server_port = host_port_split(host) + host_port = host_server_port + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(timeout) + log.trace(' > ' + session.decode(client_ip, + client_port, + self.request[0])) + sock.sendto(self.request[0], (host_ip, host_port)) + while not finished: + try: + data, address = sock.recvfrom(16 * 1024) + except socket.error as se: + log.notice(' ] session: %s -> %s: error: %s' % (client, + host, + se)) + return + except socket.gaierror as se: + log.notice(' ] session: %s -> %s: error: %s' % (client, + host, + se)) + return + except: + return + finished = session.data(address[0], address[1], data) + if address[0] == host_ip: + if host_port == host_server_port: + host_port = address[1] + if address[1] == host_port: + log.trace(' < ' + session.decode(address[0], + address[1], + data)) + sock.sendto(data, (client_ip, client_port)) + elif address[0] == client_ip and address[1] == client_port: + log.trace(' > ' + session.decode(address[0], + address[1], + data)) + sock.sendto(data, (host_ip, host_port)) + log.notice(' ] %6d: session: %s -> %s: end' % (session_count, + client, + host)) + else: + mac = getmac.get_mac_address(ip = client_ip) + log.trace(' . request: host not found: %s (%s)' % (client, mac)) + +class udp_server(socketserver.ThreadingMixIn, socketserver.UDPServer): + pass + +class proxy_server(object): + def __init__(self, config, host, port): + self.lock = threading.Lock() + self.session_timeout = 10 + self.host = host + self.port = port + self.server = None + self.clients = { } + self.config = configuration.configuration() + self._load(config) + self.session_counter = 0 + + def __del__(self): + self.stop() + + def _lock(self): + self.lock.acquire() + + def _unlock(self): + self.lock.release() + + def _load_client(self, client, depth = 0): + if depth > 32: + raise error.general('\'clients\'" nesting too deep; circular?') + if not self.config.has_section(client): + raise error.general('client not found: %s' % (client)) + for c in self.config.comma_list(client, 'clients', err = False): + self._load_client(c, depth + 1) + if client in self.clients: + raise error.general('repeated client: %s' % (client)) + host = self.config.get_item(client, 'host', err = False) + if host is not None: + ips = self.config.comma_list(client, 'ip', err = False) + macs = self.config.comma_list(client, 'mac', err = False) + if len(ips) != 0 and len(macs) != 0: + raise error.general('client has ip and mac: %s' % (client)) + if len(ips) != 0: + keys = ips + elif len(macs) != 0: + keys = macs + else: + raise error.general('not client ip or mac: %s' % (client)) + for key in keys: + self.clients[key] = host + + def _load(self, config): + self.config.load(config) + clients = self.config.comma_list('default', 'clients', err = False) + if len(clients) == 0: + raise error.general('\'clients\'" entry not found in config [defaults]') + for client in clients: + self._load_client(client) + + def start(self): + log.notice('Proxy: %s:%i' % (self.host, self.port)) + if self.host == 'all': + host = '' + else: + host = self.host + try: + self.server = udp_server((host, self.port), udp_handler) + except Exception as e: + raise error.general('proxy create: %s' % (e)) + self.server.proxy = self + self._lock() + try: + self.server_thread = threading.Thread(target = self.server.serve_forever) + self.server_thread.daemon = True + self.server_thread.start() + finally: + self._unlock() + + def stop(self): + self._lock() + try: + if self.server is not None: + self.server.shutdown() + self.server.server_close() + self.server = None + finally: + self._unlock() + + def run(self): + while True: + time.sleep(1) + + def get_host(self, client): + host = None + self._lock() + try: + if client in self.clients: + host = self.clients[client] + else: + mac = getmac.get_mac_address(ip = client) + if mac in self.clients: + host = self.clients[mac] + finally: + self._unlock() + return host + + def get_session_count(self): + count = 0 + self._lock() + try: + self.session_counter += 1 + count = self.session_counter + finally: + self._unlock() + return count + + +def load_log(logfile): + if logfile is None: + log.default = log.log(streams = ['stdout']) + else: + log.default = log.log(streams = [logfile]) + +def run(args = sys.argv, command_path = None): + ec = 0 + notice = None + proxy = None + try: + description = 'Proxy TFTP sessions from the host running this proxy' + description += 'to hosts and ports defined in the configuration file. ' + description += 'The tool lets you create a farm of hardware and to run ' + description += 'more than one TFTP test session on a host or multiple ' + description += 'hosts at once. This proxy service is not considered secure' + description += 'and is for use in a secure environment.' + + argsp = argparse.ArgumentParser(prog = 'rtems-tftp-proxy', + description = description) + argsp.add_argument('-l', '--log', + help = 'log file.', + type = str, default = None) + argsp.add_argument('-v', '--trace', + help = 'enable trace logging for debugging.', + action = 'store_true', default = False) + argsp.add_argument('-c', '--config', + help = 'proxy configuation (default: %(default)s).', + type = str, default = None) + argsp.add_argument('-B', '--bind', + help = 'address to bind the proxy too (default: %(default)s).', + type = str, default = 'all') + argsp.add_argument('-P', '--port', + help = 'port to bind the proxy too(default: %(default)s).', + type = int, default = '69') + + argopts = argsp.parse_args(args[1:]) + + load_log(argopts.log) + log.notice('RTEMS Tools - TFTP Proxy, %s' % (version.string())) + log.output(log.info(args)) + log.tracing = argopts.trace + + if argopts.config is None: + raise error.general('no config file, see -h') + + proxy = proxy_server(argopts.config, argopts.bind, argopts.port) + + try: + proxy.start() + proxy.run() + except: + proxy.stop() + raise + + except error.general as gerr: + notice = str(gerr) + ec = 1 + except error.internal as ierr: + notice = str(ierr) + ec = 1 + except error.exit as eerr: + pass + except KeyboardInterrupt: + notice = 'abort: user terminated' + ec = 1 + except: + raise + notice = 'abort: unknown error' + ec = 1 + if proxy is not None: + del proxy + if notice is not None: + log.stderr(notice) + sys.exit(ec) + +if __name__ == "__main__": + run() diff --git a/misc/wscript b/misc/wscript index f83c74b..7d90968 100644 --- a/misc/wscript +++ b/misc/wscript @@ -75,11 +75,16 @@ def build(bld): # bld(features = 'py', source = ['tools/boot.py', - 'tools/cmd-boot-image.py'], + 'tools/cmd-boot-image.py', + 'tools/cmd-tftpproxy.py', + 'tools/tftpproxy.py', + 'tools/getmac/__init__.py', + 'tools/getmac/getmac.py'], install_from = '.', install_path = '${PREFIX}/share/rtems/misc') bld.install_files('${PREFIX}/bin', - ['rtems-boot-image'], + ['rtems-boot-image', + 'rtems-tftp-proxy'], chmod = 0o755) bld.install_files('${PREFIX}/share/rtems/tools/config', 'tools/config/rtems-boot.ini') -- 2.19.1 _______________________________________________ devel mailing list devel@rtems.org http://lists.rtems.org/mailman/listinfo/devel