This is in progress, and documentation will follow..main
| @@ -0,0 +1,17 @@ | |||
| MODULES=testeth | |||
| VIRTUALENV?=python3.8 -m virtualenv | |||
| test: | |||
| (ls kvm.py $(MODULES).py | entr sh -c 'python -m coverage run -m unittest -f $(MODULES) && coverage report --omit=p/\* -m -i') | |||
| env: | |||
| ($(VIRTUALENV) p && . ./p/bin/activate && pip install -r requirements.txt) | |||
| sockio.py: ioctlgen | |||
| ./ioctlgen > $@ || rm -- $@ | |||
| ioctlgen: ioctlgen.awk Makefile | |||
| awk -f ioctlgen.awk < /usr/include/sys/sockio.h| cc -x c -o ioctlgen - | |||
| testioctlgen: | |||
| ls ioctlgen.awk Makefile | entr sh -c 'clear; make sockio.py 2>&1 | head -n 20 && cat sockio.py' | |||
| @@ -0,0 +1,139 @@ | |||
| # git+https://github.com/secdev/scapy.git | |||
| import fcntl | |||
| import os | |||
| import struct | |||
| import sys | |||
| from scapy.all import * | |||
| from scapy.arch.bpf.consts import BIOCGBLEN, BIOCGDLT, BIOCSDLT, BIOCSETIF, BIOCIMMEDIATE | |||
| from ctypes import Structure, c_char, c_long, c_int, c_uint, c_ushort, c_uint16, c_uint32, sizeof | |||
| class ReprStructureMixin: | |||
| def format_field(self, k): | |||
| try: | |||
| bits = getattr(self, '_bits_%s' % k) | |||
| return str(makebits(getattr(self, k), bits)) | |||
| except AttributeError: | |||
| return repr(getattr(self, k)) | |||
| def __repr__(self): | |||
| args = tuple(self.format_field(x[0]) for x in self._fields_) | |||
| return '%s(%s)' % (self.__class__.__name__, ', '.join(args)) | |||
| class Timeval(Structure, ReprStructureMixin): | |||
| _fields_ = [ | |||
| ('tv_sec', c_long), | |||
| ('tv_usec', c_long), | |||
| ] | |||
| class bpf_hdr(Structure, ReprStructureMixin): | |||
| _fields_ = [ | |||
| ('bh_tstamp', Timeval), | |||
| ('bh_caplen', c_uint32), | |||
| ('bh_datalen', c_uint32), | |||
| ('bh_hdrlen', c_ushort), | |||
| ] | |||
| pkthdrbits = { 1: 'CSUM', 2: 'VLAN_TAG', 4: 'TSTMP' } | |||
| # awk '{ print $3 ": '"'"'" $2 "'"'"'," }' | |||
| csumflagbits = { | |||
| 0x00000001: 'CSUM_IP', | |||
| 0x00000002: 'CSUM_IP_UDP', | |||
| 0x00000004: 'CSUM_IP_TCP', | |||
| 0x00000008: 'CSUM_IP_SCTP', | |||
| 0x00000010: 'CSUM_IP_TSO', | |||
| 0x00000020: 'CSUM_IP_ISCSI', | |||
| 0x00000040: 'CSUM_INNER_IP6_UDP', | |||
| 0x00000080: 'CSUM_INNER_IP6_TCP', | |||
| 0x00000100: 'CSUM_INNER_IP6_TSO', | |||
| 0x00000200: 'CSUM_IP6_UDP', | |||
| 0x00000400: 'CSUM_IP6_TCP', | |||
| 0x00000800: 'CSUM_IP6_SCTP', | |||
| 0x00001000: 'CSUM_IP6_TSO', | |||
| 0x00002000: 'CSUM_IP6_ISCSI', | |||
| 0x00004000: 'CSUM_INNER_IP', | |||
| 0x00008000: 'CSUM_INNER_IP_UDP', | |||
| 0x00010000: 'CSUM_INNER_IP_TCP', | |||
| 0x00020000: 'CSUM_INNER_IP_TSO', | |||
| 0x00040000: 'CSUM_ENCAP_VXLAN', | |||
| 0x00080000: 'CSUM_ENCAP_RSVD1', | |||
| 0x00100000: 'CSUM_INNER_L3_CALC', | |||
| 0x00200000: 'CSUM_INNER_L3_VALID', | |||
| 0x00400000: 'CSUM_INNER_L4_CALC', | |||
| 0x00800000: 'CSUM_INNER_L4_VALID', | |||
| 0x01000000: 'CSUM_L3_CALC', | |||
| 0x02000000: 'CSUM_L3_VALID', | |||
| 0x04000000: 'CSUM_L4_CALC', | |||
| 0x08000000: 'CSUM_L4_VALID', | |||
| 0x10000000: 'CSUM_L5_CALC', | |||
| 0x20000000: 'CSUM_L5_VALID', | |||
| 0x40000000: 'CSUM_COALESCED', | |||
| 0x80000000: 'CSUM_SND_TAG', | |||
| } | |||
| class fbsdpkthdr(Structure, ReprStructureMixin): | |||
| _bits_fph_bits = pkthdrbits | |||
| _bits_fph_csum_flags = csumflagbits | |||
| _fields_ = [ | |||
| ('fph_magic', c_char * 4), | |||
| ('fph_hdrlen', c_uint32), | |||
| ('fph_recvif', c_char * 16), | |||
| ('fph_bits', c_uint32), | |||
| ('fph_flowid', c_uint32), | |||
| ('fph_csum_flags', c_uint32), | |||
| ('fph_csum_data', c_uint32), | |||
| ('fph_vlan_tag', c_uint16), | |||
| ] | |||
| class BPF: | |||
| def __init__(self, iface): | |||
| f = self._fp = os.open('/dev/bpf', os.O_RDONLY) | |||
| l = fcntl.ioctl(f, BIOCGBLEN, b'\x00'*4, True) | |||
| sz = c_int.from_buffer_copy(l) | |||
| self._blen = sz.value | |||
| fcntl.ioctl(f, BIOCIMMEDIATE, struct.pack('I', 1)) | |||
| fcntl.ioctl(f, BIOCSETIF, struct.pack('16s16x', iface.encode())) | |||
| fcntl.ioctl(f, BIOCSDLT, struct.pack('I', 154)) | |||
| def getpkt(self): | |||
| bytes = os.read(self._fp, self._blen) | |||
| bpfhdr = bpf_hdr.from_buffer_copy(bytes) | |||
| off = bpfhdr.bh_hdrlen | |||
| fbsdhdr = fbsdpkthdr.from_buffer_copy(bytes, off) | |||
| off += fbsdhdr.fph_hdrlen | |||
| pkt = bytes[off:] | |||
| return fbsdhdr, pkt | |||
| def makebits(val, bits): | |||
| r = [ v for k, v in bits.items() if k & val ] | |||
| if not r: | |||
| return 0 | |||
| return '|'.join(r) | |||
| if __name__ == '__main__': | |||
| bpf = BPF(sys.argv[1]) | |||
| while True: | |||
| hdr, pkt = bpf.getpkt() | |||
| print(repr(hdr)) | |||
| #pkthdr = struct.unpack(fbsdpkthdr, pkt[hdrlen:hdrlen + fbsdpkthdrlen]) | |||
| #pkthdr = list(pkthdr) | |||
| #pkthdr[2] = pkthdr[2].strip(b'\x00').decode('us-ascii') | |||
| #print(repr(pkthdr), fbsdpkthdrlen) | |||
| if hdr.fph_magic != b'FBSD': | |||
| print('magic wrong') | |||
| continue | |||
| # XXX calcsize is wrong here | |||
| if sizeof(hdr) != hdr.fph_hdrlen: | |||
| print('length mismatch') | |||
| continue | |||
| print(repr(Ether(pkt))[:300]) | |||
| @@ -0,0 +1,27 @@ | |||
| #!/usr/bin/awk | |||
| BEGIN { | |||
| print "#include <sys/types.h>" | |||
| print "#include <sys/ioctl.h>" | |||
| print "#include <sys/time.h>" | |||
| print "#include <sys/socket.h>" | |||
| print "#include <net/if.h>" | |||
| print "#include <net/route.h>" | |||
| print "#include <netinet/in.h>" | |||
| print "#include <netinet/ip_mroute.h>" | |||
| print "#include <sys/sockio.h>" | |||
| print "#include <stdio.h>" | |||
| print "int" | |||
| print "main()" | |||
| print "{" | |||
| } | |||
| $1 == "#define" && $3 ~ "^_IO[WR]" { | |||
| printf("\tprintf(\"%s = %%ld\\n\", %s);\n", $2, $2) | |||
| } | |||
| END { | |||
| print " return 0;" | |||
| print "}" | |||
| } | |||
| @@ -0,0 +1,157 @@ | |||
| import os | |||
| import sys | |||
| import unittest | |||
| from ctypes import * | |||
| __all__ = [ 'KVM' ] | |||
| _kvm = CDLL('libkvm.so') | |||
| class kvm_t(Structure): | |||
| pass | |||
| class timespec(Structure): | |||
| _fields_ = [ | |||
| ('tv_sec', c_ulong), | |||
| ('tv_nsec', c_long), | |||
| ] | |||
| KVM_K_UNSIGNED_INT = 1 | |||
| KVM_K_SIGNED_INT = 2 | |||
| _bytesizes = [ 1, 2, 4, 8 ] | |||
| _ttvlookup = [ ((KVM_K_UNSIGNED_INT, x), globals()['c_uint%d' % (x * 8)]) for x | |||
| in _bytesizes ] + [ ((KVM_K_SIGNED_INT, x), | |||
| globals()['c_int%d' % (x * 8)]) for x in _bytesizes ] | |||
| _ttvlookup = { k: POINTER(v) for k, v in _ttvlookup } | |||
| kvm_t_p = POINTER(kvm_t) | |||
| kvm_iter_struct_t = CFUNCTYPE(c_int, c_char_p, c_int, c_size_t, | |||
| POINTER(c_char), c_void_p) | |||
| _funs = dict( | |||
| kvm_open=(kvm_t_p, (c_char_p, c_char_p, c_char_p, c_int, c_char_p)), | |||
| kvm_close=(c_int, (kvm_t_p,)), | |||
| kvm_geterr=(c_char_p, (kvm_t_p,)), | |||
| kvm_structsize=(c_ssize_t, (kvm_t_p, c_char_p)), | |||
| kvm_iterstruct=(c_int, (kvm_t_p, c_char_p, c_void_p, | |||
| kvm_iter_struct_t, c_void_p)), | |||
| ) | |||
| for k, v in _funs.items(): | |||
| f = getattr(_kvm, k) | |||
| f.restype, f.argtypes = v | |||
| def _fetchmembers(kd, typ, obj): | |||
| res = [] | |||
| def func(memb, type, len, buf, arg): | |||
| t = _ttvlookup[(type, len)] | |||
| res.append((memb, cast(buf, t)[0])) | |||
| return 0 | |||
| cbfun = kvm_iter_struct_t(func) | |||
| r = _kvm.kvm_iterstruct(kd, typ, byref(obj), cbfun, None) | |||
| if r == -1: | |||
| err = _kvm.kvm_geterr(kd) | |||
| raise RuntimeError(err.decode('us-ascii')) | |||
| return res | |||
| class KVM(object): | |||
| def __init__(self): | |||
| self.kd = _kvm.kvm_open(None, None, None, os.O_RDONLY, None) | |||
| def __enter__(self): | |||
| return self | |||
| def close(self): | |||
| if self.kd is not None: | |||
| _kvm.kvm_close(self.kd) | |||
| self.kd = None | |||
| def __exit__(self, a, b, c): | |||
| self.close() | |||
| def _iferr(self, fun, *args): | |||
| res = fun(*args) | |||
| if res == -1: | |||
| err = _kvm.kvm_geterr(self.kd) | |||
| raise RuntimeError(err.decode('us-ascii')) | |||
| return res | |||
| def structsize(self, typ): | |||
| return self._iferr(_kvm.kvm_structsize, self.kd, | |||
| typ.encode('us-ascii')) | |||
| def getstruct(self, typ, obj): | |||
| res = _fetchmembers(self.kd, typ.encode('us-ascii'), obj) | |||
| return { k.decode('us-ascii'): v for k, v in res } | |||
| def __del__(self): | |||
| self.close() | |||
| def deb(*args): | |||
| if True: #pragma: no cover | |||
| print(*args) | |||
| sys.stdout.flush() | |||
| class _TestCase(unittest.TestCase): | |||
| def setUp(self): | |||
| self.kd = _kvm.kvm_open(None, None, None, os.O_RDONLY, None) | |||
| def tearDown(self): | |||
| _kvm.kvm_close(self.kd) | |||
| self.kd = None | |||
| def test_ss(self): | |||
| self.assertEqual(_kvm.kvm_structsize(self.kd, | |||
| b'struct timespec'), 16) | |||
| def test_iter(self): | |||
| exp = [ | |||
| (b'tv_sec', 0x1234), | |||
| (b'tv_nsec', 0xabcd), | |||
| ] | |||
| ts = timespec(0x1234, 0xabcd) | |||
| res = _fetchmembers(self.kd, b'struct timespec', ts) | |||
| self.assertEqual(res, exp) | |||
| def test_class_errs(self): | |||
| kd = KVM() | |||
| self.assertEqual(kd.structsize('struct timespec'), 16) | |||
| sec = 1839238 | |||
| nsec = 19849873 | |||
| ts = timespec(sec, nsec) | |||
| res = dict( | |||
| tv_sec=sec, | |||
| tv_nsec=nsec, | |||
| ) | |||
| self.assertEqual(kd.getstruct('struct timespec', ts), res) | |||
| def test_class(self): | |||
| kd = KVM() | |||
| with KVM() as kd: | |||
| with self.assertRaisesRegex(RuntimeError, | |||
| 'unable to find kernel type: struct flksjdi'): | |||
| kd.structsize('struct flksjdi') | |||
| ts = timespec(0, 0) | |||
| with self.assertRaisesRegex(RuntimeError, | |||
| 'unable to find kernel type: struct weoiud'): | |||
| kd.getstruct('struct weoiud', ts) | |||
| @@ -0,0 +1,47 @@ | |||
| # !!fetch -q -o - https://www.funkthat.com/gitea/jmg/bitelab/raw/branch/main/bitelab/mocks.py | |||
| # | |||
| # Copyright (c) 2020 The FreeBSD Foundation | |||
| # | |||
| # This software1 was developed by John-Mark Gurney under sponsorship | |||
| # from the FreeBSD Foundation. | |||
| # | |||
| # 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 AUTHOR 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 AUTHOR 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 unittest.mock import AsyncMock, Mock | |||
| __all__ = [ 'wrap_subprocess_exec' ] | |||
| def wrap_subprocess_exec(mockobj, stdout=b'', stderr=b'', retcode=0): | |||
| assert isinstance(stdout, bytes) | |||
| assert isinstance(stderr, bytes) | |||
| proc = Mock() | |||
| proc.communicate = AsyncMock() | |||
| proc.communicate.return_value = (stdout, stderr) | |||
| proc.stdout.read = AsyncMock() | |||
| proc.stdout.read.side_effect = [ stdout, b'' ] | |||
| proc.stdin.drain = AsyncMock() | |||
| proc.stdin.wait_closed = AsyncMock() | |||
| proc.wait = AsyncMock() | |||
| proc.wait.return_value = retcode | |||
| proc.returncode = retcode | |||
| mockobj.return_value = proc | |||
| @@ -0,0 +1,2 @@ | |||
| git+https://github.com/secdev/scapy.git | |||
| coverage | |||
| @@ -0,0 +1,641 @@ | |||
| # | |||
| # Copyright 2021 John-Mark Gurney. | |||
| # | |||
| # 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 AUTHOR 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 AUTHOR 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: https://en.wikipedia.org/wiki/Reserved_IP_addresses | |||
| # TEST-NET-1: 192.0.2.0/24 | |||
| # TEST-NET-2: 198.51.100.0/24 | |||
| # TEST-NET-3: 203.0.113.0/24 | |||
| from unittest.mock import patch | |||
| from mocks import * | |||
| import unittest | |||
| # Silence useless warning: WARNING: No IPv4 address found on | |||
| import scapy.error | |||
| scapy.error.warning = lambda *a, **kw: None | |||
| from scapy.all import * | |||
| from kvm import _TestCase as _KVMTestCase | |||
| from kvm import KVM | |||
| from bpf import Timeval | |||
| from ctypes import Structure, Union, POINTER, sizeof, create_string_buffer, byref | |||
| from ctypes import c_char, c_void_p, c_uint8, c_uint16, c_uint32, c_int64, c_uint64 | |||
| import asyncio | |||
| import fcntl | |||
| import functools | |||
| import itertools | |||
| import re | |||
| import sockio | |||
| import string | |||
| import sys | |||
| class if_data(Structure): | |||
| _fields_ = [ | |||
| ('ifi_type', c_uint8), | |||
| ('ifi_physical', c_uint8), | |||
| ('ifi_addrlen', c_uint8), | |||
| ('ifi_hdrlen', c_uint8), | |||
| ('ifi_link_state', c_uint8), | |||
| ('ifi_vhid', c_uint8), | |||
| ('ifi_datalen', c_uint16), | |||
| ('ifi_mtu', c_uint32), | |||
| ('ifi_metric', c_uint32), | |||
| ('ifi_baudrate', c_uint64), | |||
| ('ifi_ipackets', c_uint64), | |||
| ('ifi_ierrors', c_uint64), | |||
| ('ifi_opackets', c_uint64), | |||
| ('ifi_collisions', c_uint64), | |||
| ('ifi_ibytes', c_uint64), | |||
| ('ifi_obytes', c_uint64), | |||
| ('ifi_imcasts', c_uint64), | |||
| ('ifi_omcasts', c_uint64), | |||
| ('ifi_iqdrops', c_uint64), | |||
| ('ifi_oqdrops', c_uint64), | |||
| ('ifi_noproto', c_uint64), | |||
| ('ifi_hwassist', c_uint64), | |||
| ('ifi_epoch', c_int64), # XXX - broken on i386 | |||
| ('ifi_lastchange', Timeval), # XXX - broken on 32-bit platforms | |||
| ] | |||
| class ifr_ifru(Union): | |||
| _fields_ = [ | |||
| ('ifru_data', POINTER(c_char)), | |||
| ] | |||
| class ifreq(Structure): | |||
| _fields_ = [ | |||
| ('ifr_name', c_char * 16), | |||
| ('ifr_ifru', ifr_ifru), | |||
| ] | |||
| def _makeflags(s): | |||
| return set(s.split(',')) | |||
| def _parsemedia(s): | |||
| reg = '^Ethernet autoselect (1000baseT <full-duplex>)$' | |||
| m = re.match(reg, s) | |||
| return dict(ethernet='autoselect', media=dict(medium='1000baseT', options={ 'full-duplex' })) | |||
| __ifreqsock = None | |||
| def get_ifreqsock(): | |||
| global __ifreqsock | |||
| if __ifreqsock == None: | |||
| __ifreqsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | |||
| return __ifreqsock | |||
| __kvm = None | |||
| def get_kvm(): | |||
| global __kvm | |||
| if __kvm == None: | |||
| __kvm = KVM() | |||
| return __kvm | |||
| def if_data(iface): | |||
| s = get_ifreqsock() | |||
| kd = get_kvm() | |||
| ifdatalen = kd.structsize('struct if_data') | |||
| ifdata = create_string_buffer(ifdatalen) | |||
| ifq = ifreq() | |||
| ifq.ifr_name = iface.encode('us-ascii') | |||
| ifq.ifr_ifru.ifru_data = ifdata | |||
| r = fcntl.ioctl(s, sockio.SIOCGIFDATA, ifq) | |||
| if r != 0: | |||
| raise RuntimeError('ioctl returned %d') | |||
| return kd.getstruct('struct if_data', ifdata) | |||
| async def waitcarrier(iface): | |||
| while True: | |||
| res = await ifconfig(iface) | |||
| if res[iface]['status'] == 'active': | |||
| return | |||
| await asyncio.sleep(.5) | |||
| async def ifconfig(iface, *args, **kwargs): | |||
| preargs = () | |||
| args = sum([ [k, str(v)] for k, v in kwargs.items() ], list(args)) | |||
| # Get detailed info about the interface if nothing is specified | |||
| if not args: | |||
| preargs = ('-m',) | |||
| proc = await asyncio.create_subprocess_exec('/sbin/ifconfig', *preargs, | |||
| iface, *args, stdout=asyncio.subprocess.PIPE, | |||
| stderr=asyncio.subprocess.PIPE) | |||
| stdout, stderr = await proc.communicate() | |||
| if proc.returncode != 0: | |||
| raise RuntimeError(stderr.decode('us-ascii').strip()) | |||
| if not stdout: | |||
| return None | |||
| # got something, parse it | |||
| #print('ifc:', repr(stdout)) | |||
| stdout = stdout.decode('us-ascii') | |||
| lines = stdout.split('\n') | |||
| reg = '^(?P<iface>^.+): flags=[0-9a-f]{4,4}<(?P<flags>([A-Z0-9_]+(,[A-Z0-9_]+)*)?)> metric (?P<metric>[0-9]+) mtu (?P<mtu>[0-9]+)$' | |||
| m = re.match(reg, lines[0]) | |||
| iface = m.group('iface') | |||
| res = { iface: dict(mtu=int(m.group('mtu')), metric=int(m.group('metric')), flags=_makeflags(m.group('flags'))) } | |||
| obj = res[iface] | |||
| for i in lines[1:]: | |||
| reg = '^\\toptions=[0-9a-f]{5,5}<(?P<options>([A-Z0-9_]+(,[A-Z0-9_]+)*)?)>$' | |||
| m = re.match(reg, i) | |||
| if m: | |||
| obj['options'] = _makeflags(m.group('options')) | |||
| continue | |||
| reg = '^\\tcapabilities=[0-9a-f]{5,6}<(?P<capabilities>([A-Z0-9_]+(,[A-Z0-9_]+)*)?)>$' | |||
| m = re.match(reg, i) | |||
| if m: | |||
| obj['capabilities'] = _makeflags(m.group('capabilities')) | |||
| continue | |||
| reg = '^\\tether (?P<ether>[0-9a-f]{2,2}(:[0-9a-f]{2,2}){5,5})$' | |||
| m = re.match(reg, i) | |||
| if m: | |||
| obj['ether'] = m.group('ether') | |||
| continue | |||
| reg = '^\\tmedia: (?P<media>.+)$' | |||
| m = re.match(reg, i) | |||
| if m: | |||
| obj['media'] = _parsemedia(m.group('media')) | |||
| continue | |||
| reg = '^\\tstatus: (?P<status>.+)$' | |||
| m = re.match(reg, i) | |||
| if m: | |||
| obj['status'] = m.group('status') | |||
| continue | |||
| reg = '^\\tnd6 options=[0-9a-f]{2,2}<(?P<nd6options>([A-Z0-9_]+(,[A-Z0-9_]+)*)?)>$' | |||
| m = re.match(reg, i) | |||
| if m: | |||
| obj['nd6'] = _makeflags(m.group('nd6options')) | |||
| continue | |||
| return res | |||
| async def asendp(*args, **kwargs): | |||
| '''a coroutine wrapping scapy sendp. All the arguments are | |||
| the same, but you must await to get the results.''' | |||
| loop = asyncio.get_running_loop() | |||
| return await loop.run_in_executor(None, functools.partial(sendp, *args, **kwargs)) | |||
| class PacketManager(object): | |||
| def __init__(self, iface): | |||
| '''Create a context manager for sniffing packets on the | |||
| interface specified by iface. | |||
| Sniffing will only begin once used as a context manager. | |||
| Example: | |||
| async with PacketManager(iface) as pm: | |||
| pkt = await pm.getpkt() | |||
| ''' | |||
| self._iface = iface | |||
| self._sniffer = None | |||
| def enqueuepkt(self, pkt): | |||
| '''Internal function to enqueue a received packet.''' | |||
| # hopefully these routines are executed in order they are | |||
| # scheduled. The Python docs does not assure that this this | |||
| # will happen. | |||
| #print('enq:', repr(pkt)) | |||
| asyncio.run_coroutine_threadsafe(self._queue.put(pkt), self._loop) | |||
| async def __aenter__(self): | |||
| if self._sniffer is not None: | |||
| raise RuntimeError('already in a context') | |||
| self._loop = asyncio.get_running_loop() | |||
| self._queue = asyncio.Queue() | |||
| self._sniffer = AsyncSniffer(iface=self._iface, prn=self.enqueuepkt) | |||
| self._sniffer.start() | |||
| return self | |||
| async def __aexit__(self, exc_type, exc_value, traceback): | |||
| await self._loop.run_in_executor(None, self._sniffer.stop) | |||
| def getpkt(self, timeout=None): | |||
| '''coroutine. Return the next available packet. If timeout is | |||
| specified (in seconds), if a packet is not available in the timeout | |||
| specified, the exception asyncio.TimeoutError is raised.''' | |||
| return asyncio.wait_for(self._queue.get(), timeout) | |||
| _mkpktdata = lambda: itertools.cycle(string.printable) | |||
| _getpktdata = lambda *args: ''.join(itertools.islice(_mkpktdata(), *args)).encode() | |||
| # Convert capability flags to their respective ifconfig command argument | |||
| flagtoopt = dict( | |||
| TXCSUM_IPV6='txcsum6', | |||
| RXCSUM_IPV6='rxcsum6', | |||
| TXCSUM='txcsum', | |||
| RXCSUM='rxcsum', | |||
| VLAN_HWTAGGING='vlanhwtag', | |||
| ) | |||
| async def csuminternal(flag, testiface, checkiface): | |||
| # XXX - I can't figure out how to get checksum to ensure that | |||
| # the hardcoded check will ALWAYS be invalid | |||
| if flag[0] == 'T': | |||
| txiface, rxiface = testiface, checkiface | |||
| else: | |||
| return | |||
| txiface, rxiface = checkiface, testiface | |||
| # base packet | |||
| p = Ether(src=get_if_hwaddr(txiface), dst=get_if_hwaddr(rxiface)) | |||
| # https://en.wikipedia.org/wiki/Reserved_IP_addresses | |||
| if flag.endswith('_IPV6'): | |||
| p = p / IP(src='192.168.0.1', dst='192.168.0.2', | |||
| chksum=0xbadc, flags='DF') | |||
| else: | |||
| p = p / IPv6(src='fc00:0b5d:041c:7e37::7e37', | |||
| dst='fc00:0b5d:041c:7e37::c43c') | |||
| tcp = p / TCP(dport=443, flags='S', chksum=0xbadc) | |||
| udp = p / UDP(dport=53, chksum=0xbadc) / \ | |||
| b'this is a udp checksum test packet' | |||
| await ifconfig(checkiface, '-' + flagtoopt[flag]) | |||
| for pref in [ '-', '' ]: | |||
| await ifconfig(testiface, pref + flagtoopt[flag]) | |||
| await ifconfig(testiface, 'up') | |||
| await ifconfig(checkiface, 'up') | |||
| await waitcarrier(testiface) | |||
| await waitcarrier(checkiface) | |||
| async with PacketManager(rxiface) as pm: | |||
| await asendp(tcp, iface=txiface, verbose=False) | |||
| try: | |||
| rpkt = await pm.getpkt(timeout=.5) | |||
| except asyncio.TimeoutError: | |||
| raise RuntimeError('failed to receive checksum test') | |||
| print('recv:', repr(rpkt)) | |||
| async def csumtest(testiface, checkiface): | |||
| if False: | |||
| raise ValueError('cannot be implemented this way, need the host' | |||
| 'stack to set special mbuf flags, etc') | |||
| csumtests = { 'RXCSUM', 'TXCSUM', 'RXCSUM_IPV6', 'TXCSUM_IPV6' } | |||
| ifc = await ifconfig(testiface) | |||
| print(ifc) | |||
| for csum in csumtests.intersection(ifc[testiface]['capabilities']): | |||
| print(repr(csum)) | |||
| await csuminternal(csum, testiface, checkiface) | |||
| async def mtucheck(sndiface, rcviface, mtusize): | |||
| # make sure packet is padded out to full frame size | |||
| p = Ether(src=get_if_hwaddr(sndiface), dst=get_if_hwaddr(rcviface)) / \ | |||
| _getpktdata(mtusize - 14) | |||
| #print('pktlen:', repr(p.build())) | |||
| #print('sndiface:', repr(sndiface)) | |||
| #print('rcviface:', repr(rcviface)) | |||
| async with PacketManager(rcviface) as pm: | |||
| try: | |||
| await asendp(p, iface=sndiface, verbose=False) | |||
| except OSError: | |||
| raise RuntimeError('failed to send mtu size %d' % | |||
| mtusize) | |||
| try: | |||
| rpkt = await pm.getpkt(timeout=.5) | |||
| except asyncio.TimeoutError: | |||
| raise RuntimeError('failed to receive mtu size %d' % | |||
| mtusize) | |||
| #print('got pkt:', repr(rpkt)) | |||
| if rpkt != p: | |||
| raise RuntimeError( | |||
| 'received packet did not match sent: %s != %s' % | |||
| (repr(rpkt), repr(p))) | |||
| async def mtutest(testiface, checkiface): | |||
| for mtusize in [ 512, 1500, 1504, 2025, 4074, 7000, 8000, 9000, 9216 ]: | |||
| try: | |||
| await ifconfig(testiface, mtu=mtusize) | |||
| await ifconfig(checkiface, mtu=mtusize) | |||
| except RuntimeError: | |||
| print('failed to set mtu %d, skipping...' % mtusize) | |||
| continue | |||
| print('mtu size:', mtusize) | |||
| await ifconfig(testiface, 'up') | |||
| await ifconfig(checkiface, 'up') | |||
| await waitcarrier(testiface) | |||
| await waitcarrier(checkiface) | |||
| # if other way around ure won't work | |||
| await mtucheck(testiface, checkiface, mtusize) | |||
| await mtucheck(checkiface, testiface, mtusize) | |||
| async def hwassisttest(testiface): | |||
| '''Check to make sure that the hwassist flags follow the | |||
| ifconfig options. | |||
| ''' | |||
| ifconf = await ifconfig(testiface) | |||
| caps = ifconf[testiface]['capabilities'] | |||
| basecsumflags = { 'RXCSUM', 'TXCSUM', 'RXCSUM_IPV6', 'TXCSUM_IPV6' } | |||
| # get the supported flags | |||
| supcsumflags = caps & basecsumflags | |||
| # turn everything off | |||
| await ifconfig(testiface, *('-%s' % flagtoopt[x] for x in supcsumflags)) | |||
| # make sure it is off | |||
| ifconf = await ifconfig(testiface) | |||
| if ifconf[testiface]['flags'] & basecsumflags: | |||
| raise RuntimeError('failed to clear all the csum flags.') | |||
| # make sure _hwassist is 0 | |||
| raise NotImplementedError('tbd') | |||
| def main(): | |||
| testiface, checkiface = sys.argv[1:3] | |||
| print('running...') | |||
| if sys.argv[3] == 'mtu': | |||
| asyncio.run(mtutest(testiface, checkiface)) | |||
| elif sys.argv[3] == 'csum': | |||
| asyncio.run(csumtest(testiface, checkiface)) | |||
| elif sys.argv[3] == 'hwassist': | |||
| asyncio.run(hwassisttest(testiface)) | |||
| print('done') | |||
| if __name__ == '__main__': | |||
| main() | |||
| class _TestCase(unittest.IsolatedAsyncioTestCase): | |||
| @patch('asyncio.sleep') | |||
| @patch(__name__ + '.ifconfig') | |||
| async def test_waitcarrier(self, ifc, asleep): | |||
| cnt = [ 0 ] | |||
| async def se(iface): | |||
| cnt[0] += 1 | |||
| if cnt[0] < 5: | |||
| return { iface: dict(status='no carrier') } | |||
| return { iface: dict(status='active') } | |||
| ifc.side_effect = se | |||
| await waitcarrier('foo') | |||
| ifc.assert_called_with('foo') | |||
| # Better to have an event to wait on, but that isn't available | |||
| asleep.assert_called_with(.5) | |||
| @patch('asyncio.create_subprocess_exec') | |||
| async def test_ifconf_err(self, cse): | |||
| errmsg = \ | |||
| b'ifconfig: ioctl SIOCSIFMTU (set mtu): Invalid argument' | |||
| wrap_subprocess_exec(cse, stderr=errmsg, retcode=1) | |||
| with self.assertRaisesRegex(RuntimeError, re.escape(errmsg.decode())): | |||
| await ifconfig('foobar', mtu=7000) | |||
| @patch('asyncio.create_subprocess_exec') | |||
| async def test_ifconf(self, cse): | |||
| wrap_subprocess_exec(cse, b'') | |||
| self.assertIsNone(await ifconfig('foobar', mtu=4000)) | |||
| cse.assert_called_with('/sbin/ifconfig', 'foobar', | |||
| 'mtu', '4000', stdout=asyncio.subprocess.PIPE, | |||
| stderr=asyncio.subprocess.PIPE) | |||
| wrap_subprocess_exec(cse, b'') | |||
| self.assertIsNone(await ifconfig('foobar', 'up')) | |||
| cse.assert_called_with('/sbin/ifconfig', 'foobar', | |||
| 'up', stdout=asyncio.subprocess.PIPE, | |||
| stderr=asyncio.subprocess.PIPE) | |||
| output = b'''foobar: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500 | |||
| options=80188<VLAN_MTU,VLAN_HWCSUM,TSO4,LINKSTATE> | |||
| capabilities=c019b<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,VLAN_HWCSUM,TSO4,VLAN_HWTSO,LINKSTATE> | |||
| ether 52:12:34:56:78:90 | |||
| media: Ethernet autoselect (1000baseT <full-duplex>) | |||
| status: active | |||
| nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL> | |||
| ''' | |||
| res = dict(foobar=dict( | |||
| flags={ 'UP','BROADCAST', 'RUNNING', | |||
| 'SIMPLEX', 'MULTICAST' }, | |||
| metric=0, mtu=1500, | |||
| capabilities={ 'RXCSUM', 'TXCSUM', 'VLAN_MTU', | |||
| 'VLAN_HWTAGGING', 'VLAN_HWCSUM', 'TSO4', | |||
| 'VLAN_HWTSO', 'LINKSTATE' }, | |||
| options={ 'VLAN_MTU', 'VLAN_HWCSUM', 'TSO4', | |||
| 'LINKSTATE' }, | |||
| ether='52:12:34:56:78:90', | |||
| media=dict(ethernet='autoselect', | |||
| media=dict(medium='1000baseT', | |||
| options={ 'full-duplex' })), | |||
| status='active', | |||
| nd6={ 'PERFORMNUD', 'IFDISABLED', 'AUTO_LINKLOCAL' }, | |||
| ) | |||
| ) | |||
| wrap_subprocess_exec(cse, output) | |||
| ret = await ifconfig('foobar') | |||
| cse.assert_called_with('/sbin/ifconfig', '-m', 'foobar', | |||
| stdout=asyncio.subprocess.PIPE, | |||
| stderr=asyncio.subprocess.PIPE) | |||
| self.assertEqual(ret, res) | |||
| output = b'''foobar: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500 | |||
| options=80188<VLAN_MTU,VLAN_HWCSUM,TSO4,LINKSTATE> | |||
| capabilities=68009b<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,VLAN_HWCSUM,LINKSTATE,RXCSUM_IPV6,TXCSUM_IPV6> | |||
| ether 52:12:34:56:78:90 | |||
| media: Ethernet autoselect (1000baseT <full-duplex>) | |||
| status: active | |||
| nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL> | |||
| ''' | |||
| res = dict(foobar=dict( | |||
| flags={ 'UP','BROADCAST', 'RUNNING', | |||
| 'SIMPLEX', 'MULTICAST' }, | |||
| metric=0, mtu=1500, | |||
| capabilities={ 'RXCSUM', 'TXCSUM', 'VLAN_MTU', | |||
| 'VLAN_HWTAGGING', 'VLAN_HWCSUM', 'LINKSTATE', | |||
| 'RXCSUM_IPV6', 'TXCSUM_IPV6', }, | |||
| options={ 'VLAN_MTU', 'VLAN_HWCSUM', 'TSO4', | |||
| 'LINKSTATE' }, | |||
| ether='52:12:34:56:78:90', | |||
| media=dict(ethernet='autoselect', | |||
| media=dict(medium='1000baseT', | |||
| options={ 'full-duplex' })), | |||
| status='active', | |||
| nd6={ 'PERFORMNUD', 'IFDISABLED', 'AUTO_LINKLOCAL' }, | |||
| ) | |||
| ) | |||
| wrap_subprocess_exec(cse, output) | |||
| ret = await ifconfig('foobar') | |||
| cse.assert_called_with('/sbin/ifconfig', '-m', 'foobar', | |||
| stdout=asyncio.subprocess.PIPE, | |||
| stderr=asyncio.subprocess.PIPE) | |||
| self.assertEqual(ret, res) | |||
| output = b'''foobar: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500 | |||
| options=80188<VLAN_MTU,VLAN_HWCSUM,TSO4,LINKSTATE> | |||
| ether 52:12:34:56:78:90 | |||
| media: Ethernet autoselect (1000baseT <full-duplex>) | |||
| status: no carrier | |||
| nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL> | |||
| ''' | |||
| res = dict(foobar=dict( | |||
| flags={ 'UP','BROADCAST', 'RUNNING', | |||
| 'SIMPLEX', 'MULTICAST' }, | |||
| metric=0, mtu=1500, | |||
| options={ 'VLAN_MTU', 'VLAN_HWCSUM', 'TSO4', | |||
| 'LINKSTATE' }, | |||
| ether='52:12:34:56:78:90', | |||
| media=dict(ethernet='autoselect', | |||
| media=dict(medium='1000baseT', | |||
| options={ 'full-duplex' })), | |||
| status='no carrier', | |||
| nd6={ 'PERFORMNUD', 'IFDISABLED', 'AUTO_LINKLOCAL' }, | |||
| ) | |||
| ) | |||
| wrap_subprocess_exec(cse, output) | |||
| ret = await ifconfig('foobar') | |||
| self.assertEqual(ret, res) | |||
| @patch(__name__ + '.sendp') | |||
| async def test_asendp(self, sndp): | |||
| kwargs = dict(a=5, b='foo') | |||
| pkt = object() | |||
| retv = object() | |||
| sndp.return_value = retv | |||
| r = await asendp(pkt, **kwargs) | |||
| self.assertIs(r, retv) | |||
| sndp.assert_called_with(pkt, **kwargs) | |||
| @patch(__name__ + '.AsyncSniffer') | |||
| async def test_pktmgr(self, asyncs): | |||
| # That it can be a context manager | |||
| async with PacketManager('iface') as pm: | |||
| # that a RuntimeError is raised | |||
| with self.assertRaises(RuntimeError): | |||
| # when it's used as a context manager again | |||
| async with pm as foo: | |||
| pass | |||
| # That it created a queue | |||
| self.assertIsInstance(pm._queue, asyncio.Queue) | |||
| # that it called asyncsniffer with the correct arguments | |||
| asyncs.assert_called_with(iface='iface', prn=pm.enqueuepkt) | |||
| # that it was started | |||
| asyncs().start.assert_called() | |||
| # that when a pkt | |||
| pkt = object() | |||
| # is enqueued | |||
| pm.enqueuepkt(pkt) | |||
| # that getpkt returns it | |||
| self.assertIs(await pm.getpkt(), pkt) | |||
| # that a timeout parameter can be provided | |||
| with self.assertRaises(asyncio.TimeoutError): | |||
| await pm.getpkt(timeout=0) | |||
| # that when the context manager stops, stop is called | |||
| asyncs().stop.assert_called() | |||
| def test_ifdata(self): | |||
| r = if_data('ue0') | |||
| print('if:', repr(r)) | |||
| @@ -0,0 +1,616 @@ | |||
| #!/bin/sh - | |||
| # | |||
| # Copyright 2021 John-Mark Gurney. | |||
| # | |||
| # 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 AUTHOR 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 AUTHOR 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. | |||
| # | |||
| # | |||
| # ls testinterfaces.sh | entr sh -c 'fsync testinterfaces.sh && sh testinterfaces.sh run ue0 ue1' | |||
| # ifconfig ue0 -vnet testjail; ifconfig ue1 -vnet checkjail | |||
| # | |||
| # Tests to add: | |||
| # Multicast filter programming | |||
| # bad checksum filtering | |||
| # TSO/LRO functionality | |||
| # polling? | |||
| # TOE? | |||
| # WOL | |||
| # vlan hw tso | |||
| # vlan hwfilter | |||
| # hardware timestamp (may not be testable) | |||
| # | |||
| testjail=testjail | |||
| checkjail=checkjail | |||
| # Function to setup jails as needed for tests. | |||
| setup() | |||
| { | |||
| if ! jail -c persist=1 path=/ name=testjail vnet=new vnet.interface="$testiface"; then | |||
| echo failed to start test jail | |||
| exit 1 | |||
| fi | |||
| if ! jail -c persist=1 path=/ name=checkjail vnet=new vnet.interface="$checkiface"; then | |||
| echo failed to start check jail | |||
| exit 1 | |||
| fi | |||
| run test ifconfig lo0 up | |||
| run check ifconfig lo0 up | |||
| } | |||
| # Verify that jails are present | |||
| checkjails() | |||
| { | |||
| if ! jls -j "$testjail" >/dev/null 2>/dev/null; then | |||
| echo "test jail not present." | |||
| return 1 | |||
| fi | |||
| if ! jls -j "$checkjail" >/dev/null 2>/dev/null; then | |||
| echo "check jail not present." | |||
| return 1 | |||
| fi | |||
| } | |||
| # Destroy jails | |||
| cleanup() | |||
| { | |||
| jail -r "$testjail" 2>/dev/null | |||
| jail -r "$checkjail" 2>/dev/null | |||
| } | |||
| # Subroutin for down'ing the interface, and removing any addresses from it. | |||
| # There are issues where if the addresses are not cleared, the same address | |||
| # won't always work. | |||
| cleaniface() | |||
| { | |||
| jailname="$1" | |||
| ifacename="$2" | |||
| run "$jailname" ifconfig $ifacename down | |||
| run "$jailname" ifconfig $ifacename | grep inet | while read a b c; do | |||
| if [ x"$a" = x"inet" -o x"$a" = x"inet6" ]; then | |||
| #if [ x"$a" = x"inet" ]; then | |||
| run "$jailname" ifconfig $ifacename "$a" "$b" remove | |||
| fi | |||
| done | |||
| run "$jailname" ifconfig $ifacename | |||
| } | |||
| # Setup the temp directory, switch to it, and more | |||
| starttest() | |||
| { | |||
| tmpdir=$(mktemp -d -t testiface) | |||
| if [ $? != 0 ]; then | |||
| echo "failed to make temp directory" | |||
| exit 2 | |||
| fi | |||
| oldpwd=$(pwd) | |||
| cd "$tmpdir" | |||
| run test tcpdump -n -p -i $(getiface test) -w "$tmpdir"/test.tcpdump 2> /dev/null & | |||
| testdumppid="$!" | |||
| # XXX - -p should be used on both interfaces | |||
| run check tcpdump -n -i $(getiface check) -w "$tmpdir"/check.tcpdump 2> /dev/null & | |||
| checkdumppid="$!" | |||
| echo "$testdumppid $checkdumppid" | |||
| } | |||
| endtest() | |||
| { | |||
| echo "$testdumppid" "$checkdumppid" | |||
| if ! kill "$testdumppid" "$checkdumppid"; then | |||
| echo "unable to kill off tcpdumps" | |||
| fi | |||
| if ! wait "$testdumppid" "$checkdumppid"; then | |||
| echo "tcpdumps failed to exit" | |||
| fi | |||
| cd "$oldpwd" | |||
| # only attempt to clean up when successful | |||
| if [ "$1" -eq 0 ]; then | |||
| echo rm -rf "$tmpdir" | |||
| else | |||
| echo "test failed, data in $tmpdir" | |||
| fi | |||
| } | |||
| # Run a command in a jail. The first arg is either check or test. | |||
| # Remainder of the args are passed for execution. | |||
| run() | |||
| { | |||
| if [ x"$1" = x"check" ]; then | |||
| jid="$checkjail" | |||
| elif [ x"$1" = x"test" ]; then | |||
| jid="$testjail" | |||
| else | |||
| echo Invalid: "$1" >&2 | |||
| exit 1 | |||
| fi | |||
| shift | |||
| jexec "$jid" "$@" | |||
| } | |||
| # Return the interface that is in the specified jail. | |||
| # The first arg is either check or test. | |||
| getiface() | |||
| { | |||
| if [ x"$1" = x"check" ]; then | |||
| echo "$checkiface" | |||
| elif [ x"$1" = x"test" ]; then | |||
| echo "$testiface" | |||
| else | |||
| echo Invalid: "$1" >&2 | |||
| exit 1 | |||
| fi | |||
| } | |||
| # Run ifconfig on the base interface in a jail. | |||
| # The first arg is either check or test. | |||
| # Remainder of the args are passed to ifconfig for execution. | |||
| ifjail() | |||
| { | |||
| j="$1" | |||
| shift | |||
| iface=$(getiface "$j") | |||
| run "$j" ifconfig "$iface" "$@" | |||
| } | |||
| # Convert an ifconfig option to the options returned by ifconfig | |||
| iftoopt() | |||
| { | |||
| case "$1" in | |||
| -txcsum6|txcsum6) | |||
| echo TXCSUM_IPV6 | |||
| ;; | |||
| -rxcsum6|rxcsum6) | |||
| echo RXCSUM_IPV6 | |||
| ;; | |||
| -txcsum|txcsum) | |||
| echo TXCSUM | |||
| ;; | |||
| -rxcsum|rxcsum) | |||
| echo RXCSUM | |||
| ;; | |||
| -vlanhwtag|vlanhwtag) | |||
| echo VLAN_HWTAGGING | |||
| ;; | |||
| *) | |||
| echo "Unknown option: $1" >&2 | |||
| exit 5 | |||
| ;; | |||
| esac | |||
| } | |||
| # Get the list of capabilities or options for the interface in the jail. | |||
| # The first arg is either check or test. | |||
| # If the second arg is specified, it must be one of cap or opt, | |||
| # specifying which of the capabilities or options to be returned | |||
| # respectively. If this arg is not specified, it defaults to | |||
| # capabilities. | |||
| getcaps() | |||
| { | |||
| if [ x"$2" = x"" -o x"$2" = x"cap" ]; then | |||
| ifoptcap=capabilities | |||
| elif [ x"$2" = x"opt" ]; then | |||
| ifoptcap=options | |||
| else | |||
| echo "Invalid getcaps arg: $2" >&2 | |||
| exit 1 | |||
| fi | |||
| run "$1" ifconfig -m $(getiface "$1") | | |||
| awk '$1 ~ /^'"$ifoptcap"'=/ { split($0, a, "<"); split(a[2], b, ">"); split(b[1], caps, ","); for (i in caps) print caps[i] }' | | |||
| sort | |||
| } | |||
| # Verify that the interface in a jail is capable of the spefified feature. | |||
| # The first arg is either check or test. | |||
| # The second arg is the capability as printed in the options line. | |||
| # This is checked against the capabilities line printed by -m. | |||
| # Return 0 if present, return 1 if not present | |||
| hascap() | |||
| { | |||
| getcaps "$1" | grep "^$(iftoopt "$2")$" >/dev/null | |||
| } | |||
| # Verify that the interface in a jail has a specific capability enabled. | |||
| # The first arg is either check or test. | |||
| # The second arg is the capability as printed in the options line. | |||
| # This is checked against the options line. | |||
| # Return 0 if present, return 1 if not present | |||
| verifycap() | |||
| { | |||
| if [ x"${2#-}" = x"${2}" ]; then | |||
| # without leading hyphen, present == success | |||
| presentretval="0" | |||
| absentretval="1" | |||
| else | |||
| # with leading hyphen, not present == success | |||
| presentretval="1" | |||
| absentretval="0" | |||
| fi | |||
| if getcaps "$1" opt | grep "^$(iftoopt "$2")$" >/dev/null; then | |||
| return $presentretval | |||
| else | |||
| return $absentretval | |||
| fi | |||
| } | |||
| # Make sure that the carrier on both interface is up before returning. | |||
| # There is currently no timeout. | |||
| waitcarrier() | |||
| { | |||
| local i | |||
| for i in test check; do | |||
| while :; do | |||
| # make sure carrier is present and any IPv6 addresses | |||
| # are no longer tentative | |||
| if ifjail "$i" | grep 'status: active' >/dev/null && | |||
| ( ! ifjail "$i" | grep 'tentative' >/dev/null); then | |||
| break | |||
| fi | |||
| sleep .5 | |||
| done | |||
| done | |||
| } | |||
| ################################## | |||
| ######### START OF TESTS ######### | |||
| ################################## | |||
| # | |||
| # Run tests verify that vlan hardware tagging works (or at least doesn't | |||
| # break anything. There isn't an easy way to verify that packets in the | |||
| # kernel get/do not get the mbuf tag. (dtrace MAY be an option) | |||
| hwvlantest() | |||
| { | |||
| local i err | |||
| err=0 | |||
| for i in "-vlanhwtag" "vlanhwtag"; do | |||
| if ! hascap test "$i"; then | |||
| echo "skipping: $i" | |||
| continue | |||
| fi | |||
| echo "testing: $i" | |||
| ifjail test "$i" | |||
| verifycap test "$i" || return 1 | |||
| ifjail check -vlanhwtag | |||
| verifycap check -vlanhwtag || return 1 | |||
| cleaniface test $testiface | |||
| cleaniface check $checkiface | |||
| sleep .5 | |||
| ifjail test up | |||
| ifjail check up | |||
| # make sure they don't exist | |||
| run test ifconfig $testiface.42 destroy 2>/dev/null | |||
| run check ifconfig $checkiface.42 destroy 2>/dev/null | |||
| run test ifconfig $testiface.42 create 172.30.5.5/24 | |||
| run check ifconfig $checkiface.42 create 172.30.5.4/24 | |||
| waitcarrier | |||
| #run check tcpdump -XXX -p -q -c 2 -n -i "$checkiface" ip & | |||
| #run test tcpdump -XXX -p -q -c 2 -n -i "$testiface" ip & | |||
| #run test ifconfig -a | |||
| if ! run test ping -o -c 4 -t 5 -i .5 172.30.5.4 >/dev/null; then | |||
| echo FAILED on "$i"!!!! | |||
| #run test ifconfig -a | |||
| err=1 | |||
| fi | |||
| #run test ifconfig $testiface.42 destroy | |||
| #run check ifconfig $checkiface.42 destroy | |||
| if [ x"$err" != x"0" ]; then | |||
| return 1 | |||
| fi | |||
| done | |||
| } | |||
| # Internal function for testing checksum. | |||
| # The first argument is the flag for the interface under test. | |||
| # If it ends with a 6, then IPv6 will be used for testing. | |||
| csuminternal() | |||
| { | |||
| local i | |||
| i="$1" | |||
| #if ! hascap test "$i"; then | |||
| # echo "skipping $i, test interface not capable" | |||
| # continue | |||
| #fi | |||
| echo "testing: $i" | |||
| if [ x"$i" = x"${i%6}" ]; then | |||
| # IPv4 | |||
| testnet="172.29.5.5/24" | |||
| checknet="172.29.5.4/24" | |||
| checkaddr="172.29.5.4" | |||
| pingcmd="ping" | |||
| ncopt="" | |||
| else | |||
| # IPv6 | |||
| testnet="inet6 fc00:0b5d:041c:7e37::7e37/64" | |||
| checknet="inet6 fc00:0b5d:041c:7e37::c43c/64" | |||
| checkaddr="fc00:0b5d:041c:7e37::c43c" | |||
| pingcmd="ping6" | |||
| ncopt="-6" | |||
| fi | |||
| cleaniface test $testiface | |||
| cleaniface check $checkiface | |||
| ifjail test "$i" | |||
| verifycap test "$i" || return 1 | |||
| ifjail check -txcsum -rxcsum -txcsum6 -rxcsum6 | |||
| verifycap check "-txcsum" || return 1 | |||
| verifycap check "-rxcsum" || return 1 | |||
| sleep .5 | |||
| ifjail test up | |||
| ifjail check up | |||
| run test ifconfig $testiface $testnet | |||
| run check ifconfig $checkiface $checknet | |||
| waitcarrier | |||
| sleep .5 | |||
| #run check tcpdump -XXX -p -q -n -i "$checkiface" not ip6 & | |||
| #run test tcpdump -XXX -p -q -n -i "$testiface" not ip6 & | |||
| #run test ifconfig -a | |||
| #run check ifconfig -a | |||
| # make sure connectivity works | |||
| if ! run test "$pingcmd" -o -c 4 -t 5 -i .5 "$checkaddr" >/dev/null; then | |||
| ifjail test | |||
| ifjail check | |||
| echo ping FAILED on "$i"!!!! | |||
| return 1 | |||
| fi | |||
| mkfifo /tmp/tififo.$$ | |||
| run check sh -c 'cat /tmp/tififo.'$$' | nc '"$ncopt"' -l 3333 | cat > /tmp/tififo.'$$ 2>/dev/null & | |||
| sleep .5 | |||
| value="foobar $$ TCP $i" | |||
| res=$( (echo "$value"; sleep .5) | run test nc -w 2 -N "$checkaddr" 3333) | |||
| # We cannot use kill %% to kill the nc -l process as it only | |||
| # kills jexec, and NOT the jail process | |||
| # The following is heavy, but should be the only one running | |||
| run check killall nc >/dev/null 2>/dev/null | |||
| rm /tmp/tififo.$$ | |||
| if [ x"$res" != x"$value" ]; then | |||
| echo TCP FAILED on "$i"!!!! | |||
| return 1 | |||
| fi | |||
| mkfifo /tmp/tififo.$$ | |||
| run check sh -c 'cat /tmp/tififo.'$$' | nc '"$ncopt"' -u -l 3333 | cat > /tmp/tififo.'$$ 2>/dev/null & | |||
| sleep .5 | |||
| value="foobar $$ UDP $i" | |||
| res=$( (echo "$value"; sleep .5) | run test nc -u -w 2 -N "$checkaddr" 3333) | |||
| # We cannot use kill %% to kill the nc -l process as it only | |||
| # kills jexec, and NOT the jail process | |||
| # The following is heavy, but should be the only one running | |||
| run check killall nc >/dev/null 2>/dev/null | |||
| rm /tmp/tififo.$$ | |||
| if [ x"$res" != x"$value" ]; then | |||
| echo UDP FAILED on "$i"!!!! | |||
| return 1 | |||
| fi | |||
| # make sure that checksum validation works by sending invalid packets | |||
| # this includes using dtrace to verify the results from the driver | |||
| } | |||
| # Iterate through the various hardware checksum off loading flags. | |||
| csumtest() | |||
| { | |||
| local i | |||
| #for i in "-rxcsum6" "rxcsum6" "-txcsum6" "txcsum6" "-rxcsum" "rxcsum" "-txcsum" "txcsum"; do | |||
| for i in "-rxcsum" "rxcsum" "-txcsum" "txcsum"; do | |||
| if ! csuminternal "$i"; then | |||
| return 1 | |||
| fi | |||
| done | |||
| } | |||
| # Verify that packets can be sent and received w/ a larger MTU. | |||
| # This test is a bit tricky in that it sets both interfaces to the | |||
| # larger MTU. Better option would be to set both sides to the larger | |||
| # MTU, but add a route w/ the -mtu specified for the return (or transmit) | |||
| # side so that we aren't testing both sides at the same time. | |||
| mtutest() | |||
| { | |||
| local i err | |||
| oldtestmtu=$(ifjail test | awk '{ print $6; exit 0 }') | |||
| oldcheckmtu=$(ifjail check | awk '{ print $6; exit 0 }') | |||
| err=0 | |||
| for i in 1500 1504 2025 4074 7000 8000 9000 9216; do | |||
| echo "testing mtu: $i" | |||
| if ! ifjail test mtu "$i" 2>/dev/null; then | |||
| echo "Failed to set mtu $i on test interface, skipping..." | |||
| continue | |||
| fi | |||
| if ! ifjail check mtu "$i" 2>/dev/null; then | |||
| echo "Failed to set mtu $i on check interface." | |||
| echo "Please use different check interface." | |||
| return 1 | |||
| fi | |||
| cleaniface test $testiface | |||
| cleaniface check $checkiface | |||
| #run test arp -an | |||
| #run check arp -an | |||
| #run test ifconfig $testiface | |||
| #run check ifconfig $checkiface | |||
| sleep .5 | |||
| ifjail test up | |||
| ifjail check up | |||
| waitcarrier | |||
| run test ifconfig $testiface 172.29.5.5/24 | |||
| run check ifconfig $checkiface 172.29.5.4/24 | |||
| # Subtract off ethernet header, ICMP header and ethernet CRC | |||
| if ! run test ping -D -s $(($i - 8 - 20)) -o -c 4 -t 5 -i .5 172.29.5.4 >/dev/null; then | |||
| echo failed for MTU size $i | |||
| err=1 | |||
| break | |||
| fi | |||
| done | |||
| # restore MTU | |||
| ifjail test mtu $oldtestmtu | |||
| ifjail check mtu $oldcheckmtu | |||
| if [ x"$err" != x"0" ]; then | |||
| return 1 | |||
| fi | |||
| } | |||
| runtest() | |||
| { | |||
| test="$1" | |||
| starttest | |||
| eval "$test"test | |||
| res="$?" | |||
| endtest "$res" | |||
| if [ "$res" -ne 0 ]; then | |||
| echo "$test"test failed. | |||
| exit 1 | |||
| fi | |||
| } | |||
| usage() | |||
| { | |||
| echo "Usage: $0 {init,run,deinit,$(echo "$alltests" | sed -e 's/ /,/g')} <ifaceundertest> <checkinterface>" | |||
| echo "" | |||
| echo "The ifaceundertest is the name of the interface which featres are being" | |||
| echo "tested." | |||
| echo "" | |||
| echo "The checinterface is the name of the interface used to validate that the" | |||
| echo "test interface operatates correctly. Features of this interface will" | |||
| echo "be turned off to prevent any of it's hardware offloading capabilities" | |||
| echo "interfering with the test." | |||
| } | |||
| cmd="$1" | |||
| testiface="$2" | |||
| checkiface="$3" | |||
| if [ -z "$cmd" ]; then | |||
| echo "command must be specified!" | |||
| echo "" | |||
| usage | |||
| exit 1 | |||
| fi | |||
| if [ -z "$testiface" -o -z "$checkiface" ]; then | |||
| echo "both interfaces must be specified!" | |||
| echo "" | |||
| usage | |||
| exit 1 | |||
| fi | |||
| alltests="mtu csum hwvlan" | |||
| case "$cmd" in | |||
| init) | |||
| setup | |||
| ;; | |||
| mtu|csum|hwvlan) | |||
| if ! checkjails; then | |||
| exit 1 | |||
| fi | |||
| echo starting $cmd, test $testiface, check $checkiface | |||
| runtest "$cmd" | |||
| echo "done" | |||
| ;; | |||
| run) | |||
| if ! checkjails; then | |||
| exit 1 | |||
| fi | |||
| echo starting, test $testiface, check $checkiface | |||
| for i in $alltests; do | |||
| runtest "$i" | |||
| done | |||
| echo "done" | |||
| ;; | |||
| deinit) | |||
| cleanup | |||
| ;; | |||
| *) | |||
| echo "invalid cmd: $cmd" | |||
| echo | |||
| usage | |||
| exit 1 | |||
| ;; | |||
| esac | |||