[PATCH] misc/tftpproxy: Add a proxy TFTP server.
chrisj at rtems.org
chrisj at rtems.org
Mon Aug 19 06:50:01 UTC 2019
From: Chris Johns <chrisj at 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 (chrisj at 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 (chrisj at 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 at 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
More information about the devel
mailing list