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 |