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 |