add generate_test_case.py
This commit is contained in:
parent
c44b856060
commit
8a5197cf04
1 changed files with 452 additions and 0 deletions
452
generate_test_case.py
Normal file
452
generate_test_case.py
Normal file
|
@ -0,0 +1,452 @@
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import typing
|
||||||
|
import ctypes
|
||||||
|
import contextlib
|
||||||
|
import struct
|
||||||
|
import binascii
|
||||||
|
import enum
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import base64
|
||||||
|
from ctypes import c_char, c_short
|
||||||
|
from typing import Tuple
|
||||||
|
import asyncio
|
||||||
|
from aioupnp.upnp import UPnP, UPnPError, get_gateway_and_lan_addresses
|
||||||
|
from aioupnp.constants import SSDP_IP_ADDRESS
|
||||||
|
import miniupnpc
|
||||||
|
|
||||||
|
|
||||||
|
_IFF_PROMISC = 0x0100
|
||||||
|
_SIOCGIFFLAGS = 0x8913 # get the active flags
|
||||||
|
_SIOCSIFFLAGS = 0x8914 # set the active flags
|
||||||
|
_ETH_P_ALL = 0x0003 # all protocols
|
||||||
|
ETHER_HEADER_LEN = 6 + 6 + 2
|
||||||
|
VLAN_HEADER_LEN = 2
|
||||||
|
|
||||||
|
printable = re.compile(b"([a-z0-9!\"#$%&'()*+,.\/:;<=>?@\[\] ^_`{|}~-]*)")
|
||||||
|
|
||||||
|
|
||||||
|
class PacketTypes(enum.Enum): # if_packet.h
|
||||||
|
HOST = 0
|
||||||
|
BROADCAST = 1
|
||||||
|
MULTICAST = 2
|
||||||
|
OTHERHOST = 3
|
||||||
|
OUTGOING = 4
|
||||||
|
LOOPBACK = 5
|
||||||
|
FASTROUTE = 6
|
||||||
|
|
||||||
|
|
||||||
|
class Layer2(enum.Enum): # https://www.iana.org/assignments/ieee-802-numbers/ieee-802-numbers.xhtml
|
||||||
|
IPv4 = 0x0800
|
||||||
|
ARP = 0x0806
|
||||||
|
VLAN = 0x8100
|
||||||
|
MVRP = 0x88f5
|
||||||
|
MMRP = 0x88f6
|
||||||
|
IPv6 = 0x86dd
|
||||||
|
GRE = 0xb7ea
|
||||||
|
|
||||||
|
|
||||||
|
class Layer3(enum.Enum): # https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml
|
||||||
|
ICMP = 1
|
||||||
|
IGMP = 2
|
||||||
|
TCP = 6
|
||||||
|
UDP = 17
|
||||||
|
|
||||||
|
|
||||||
|
class _ifreq(ctypes.Structure):
|
||||||
|
_fields_ = [("ifr_ifrn", c_char * 16),
|
||||||
|
("ifr_flags", c_short)]
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _promiscuous_posix_socket_context(interface: str):
|
||||||
|
import fcntl # posix-only
|
||||||
|
sock = socket.socket(socket.PF_PACKET, socket.SOCK_RAW, socket.htons(_ETH_P_ALL))
|
||||||
|
ifr = _ifreq()
|
||||||
|
ifr.ifr_ifrn = interface.encode()[:16]
|
||||||
|
fcntl.ioctl(sock, _SIOCGIFFLAGS, ifr) # get the flags
|
||||||
|
ifr.ifr_flags |= _IFF_PROMISC # add the promiscuous flag
|
||||||
|
fcntl.ioctl(sock, _SIOCSIFFLAGS, ifr) # update
|
||||||
|
sock.setblocking(False)
|
||||||
|
try:
|
||||||
|
yield sock
|
||||||
|
finally:
|
||||||
|
ifr.ifr_flags ^= _IFF_PROMISC # mask it off (remove)
|
||||||
|
fcntl.ioctl(sock, _SIOCSIFFLAGS, ifr) # update
|
||||||
|
print("closed posix promiscuous socket")
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _promiscuous_non_posix_socket_context():
|
||||||
|
# the public network interface
|
||||||
|
HOST = socket.gethostbyname(socket.gethostname())
|
||||||
|
# create a raw socket and bind it to the public interface
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_IP)
|
||||||
|
# prevent socket from being left in TIME_WAIT state, enabling reuse
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
sock.bind((HOST, 0))
|
||||||
|
# Include IP headers
|
||||||
|
sock.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)
|
||||||
|
# receive all packages
|
||||||
|
sock.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)
|
||||||
|
sock.setblocking(False)
|
||||||
|
try:
|
||||||
|
yield sock
|
||||||
|
finally:
|
||||||
|
# disable promiscuous mode
|
||||||
|
sock.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF)
|
||||||
|
print("closed non-posix promiscuous socket")
|
||||||
|
|
||||||
|
|
||||||
|
def promiscuous(interface: typing.Optional[str] = None) -> typing.ContextManager[socket.socket]:
|
||||||
|
if os.name == 'posix':
|
||||||
|
return _promiscuous_posix_socket_context(interface)
|
||||||
|
return _promiscuous_non_posix_socket_context()
|
||||||
|
|
||||||
|
|
||||||
|
def ipv4_to_str(addr: bytes) -> str:
|
||||||
|
return ".".join((str(b) for b in addr))
|
||||||
|
|
||||||
|
|
||||||
|
def pretty_mac(mac: bytes) -> str:
|
||||||
|
return ":".join((('0' if b < 16 else '') + hex(b)[2:] for b in mac))
|
||||||
|
|
||||||
|
|
||||||
|
def split_byte(b: int, bit=4) -> Tuple[bytes, bytes]:
|
||||||
|
return chr(((b >> (8-bit)) % 256) << (8-bit) >> (8-bit)).encode(), chr(((b << bit) % 256) >> bit).encode()
|
||||||
|
|
||||||
|
|
||||||
|
class EtherFrame:
|
||||||
|
__slots__ = [
|
||||||
|
'source_mac',
|
||||||
|
'target_mac',
|
||||||
|
'ether_type',
|
||||||
|
'vlan_id',
|
||||||
|
'tpid'
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, source_mac: bytes, target_mac: bytes, ether_type: int,
|
||||||
|
vlan_id: typing.Optional[int] = None, tpid: typing.Optional[int] = None):
|
||||||
|
self.source_mac = source_mac
|
||||||
|
self.target_mac = target_mac
|
||||||
|
self.ether_type = ether_type
|
||||||
|
self.vlan_id = vlan_id
|
||||||
|
self.tpid = tpid
|
||||||
|
|
||||||
|
def encode(self) -> bytes:
|
||||||
|
if self.vlan_id is None:
|
||||||
|
return struct.pack("6s6sH", *(getattr(self, slot) for slot in self.__slots__[:-2]))
|
||||||
|
return struct.pack("6s6sHHH", *(getattr(self, slot) for slot in self.__slots__))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def decode(cls, packet: bytes) -> Tuple['EtherFrame', bytes]:
|
||||||
|
vlan_id = None
|
||||||
|
tpid = None
|
||||||
|
if struct.unpack(f'!H', packet[12:14])[0] == Layer2.VLAN.value:
|
||||||
|
target_mac, source_mac, tpid, vlan_id, ether_type, data = struct.unpack(f'!6s6sHHH{len(packet) - ETHER_HEADER_LEN - VLAN_HEADER_LEN}s', packet)
|
||||||
|
else:
|
||||||
|
target_mac, source_mac, ether_type, data = struct.unpack(f'!6s6sH{len(packet) - ETHER_HEADER_LEN}s', packet)
|
||||||
|
return cls(source_mac, target_mac, ether_type, vlan_id, tpid), data
|
||||||
|
|
||||||
|
def debug(self) -> str:
|
||||||
|
if self.vlan_id is None:
|
||||||
|
return f"EtherFrame(source={pretty_mac(self.source_mac)}, target={pretty_mac(self.target_mac)}, " \
|
||||||
|
f"ether_type={Layer2(self.ether_type).name})"
|
||||||
|
return f"EtherFrame(source={pretty_mac(self.source_mac)}, target={pretty_mac(self.target_mac)}, " \
|
||||||
|
f"ether_type={Layer2(self.ether_type).name}, vlan={self.vlan_id})"
|
||||||
|
|
||||||
|
|
||||||
|
class IPv4Packet:
|
||||||
|
__slots__ = [
|
||||||
|
'ether_frame',
|
||||||
|
'version',
|
||||||
|
'header_length',
|
||||||
|
'dscp',
|
||||||
|
'ecn',
|
||||||
|
'total_length',
|
||||||
|
'identification',
|
||||||
|
'df',
|
||||||
|
'mf',
|
||||||
|
'flag',
|
||||||
|
'fragment_offset',
|
||||||
|
'ttl',
|
||||||
|
'protocol',
|
||||||
|
'header_checksum',
|
||||||
|
'_source_address',
|
||||||
|
'_destination_address',
|
||||||
|
'data',
|
||||||
|
'packet_type',
|
||||||
|
'interface'
|
||||||
|
]
|
||||||
|
|
||||||
|
ETHER_TYPE = Layer2.IPv4
|
||||||
|
|
||||||
|
def __init__(self, ether_frame: EtherFrame, version: int, header_length: int, dscp: int, ecn: int,
|
||||||
|
total_length: int, identification: int, mf: bool, df: bool, flag: bool, fragment_offset: int, ttl: int,
|
||||||
|
protocol: int, header_checksum: int, source_address: bytes, destination_address: bytes, data: bytes,
|
||||||
|
packet_type: int, interface: str):
|
||||||
|
self.ether_frame = ether_frame
|
||||||
|
self.version = version
|
||||||
|
self.header_length = header_length
|
||||||
|
self.dscp = dscp
|
||||||
|
self.ecn = ecn
|
||||||
|
self.total_length = total_length
|
||||||
|
self.identification = identification
|
||||||
|
self.mf = mf
|
||||||
|
self.df = df
|
||||||
|
self.flag = flag
|
||||||
|
self.fragment_offset = fragment_offset
|
||||||
|
self.ttl = ttl
|
||||||
|
self.protocol = Layer3(protocol)
|
||||||
|
self.header_checksum = header_checksum
|
||||||
|
self._source_address = source_address
|
||||||
|
self._destination_address = destination_address
|
||||||
|
self.data = data
|
||||||
|
self.packet_type = PacketTypes(packet_type)
|
||||||
|
self.interface = interface
|
||||||
|
|
||||||
|
@property
|
||||||
|
def source(self) -> str:
|
||||||
|
return ipv4_to_str(self._source_address)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def destination(self) -> str:
|
||||||
|
return ipv4_to_str(self._destination_address)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def checksum(header: bytes) -> int:
|
||||||
|
c = 0
|
||||||
|
for i in range(0, len(header), 2):
|
||||||
|
c += int.from_bytes(header[i:i + 2], 'big')
|
||||||
|
while c > 0xffff:
|
||||||
|
c %= 0xffff
|
||||||
|
while c > 0xffff:
|
||||||
|
c %= 0xffff
|
||||||
|
return c ^ 0xffff
|
||||||
|
|
||||||
|
def get_header(self) -> bytes:
|
||||||
|
version_and_hlen = (self.version << 4) + self.header_length
|
||||||
|
dscp_and_ecn = (self.dscp << 2) + self.ecn
|
||||||
|
flags = (4 if self.flag else 0) + (2 if self.df else 0) + (1 if self.mf else 0)
|
||||||
|
df_mf_and_fragment = (flags << 12) + self.fragment_offset
|
||||||
|
return struct.pack(
|
||||||
|
'!BBHHHBBH4s4s', version_and_hlen, dscp_and_ecn, self.total_length,
|
||||||
|
self.identification, df_mf_and_fragment, self.ttl, self.protocol.value,
|
||||||
|
self.header_checksum, self._source_address, self._destination_address
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def decode(cls, ether_frame: EtherFrame, packet: bytes, packet_type: int, interface: str) -> 'IPv4Packet':
|
||||||
|
if cls.checksum(packet[:20]):
|
||||||
|
raise ValueError(f'\nipv4 checksum failed, frame: {ether_frame.debug()}\n'
|
||||||
|
f'packet: {binascii.hexlify(packet).decode()}, checksum: {hex(cls.checksum(packet[:20]))}')
|
||||||
|
data_len = len(packet) - 20
|
||||||
|
version_and_hlen, dscp_and_ecn, tlen, ident, df_mf_and_fragment, ttl, proto, checksum, source, dest = \
|
||||||
|
struct.unpack(
|
||||||
|
f'!BBHHHBBH4s4s', packet[:20]
|
||||||
|
)
|
||||||
|
version, hlen = split_byte(version_and_hlen)
|
||||||
|
flags = df_mf_and_fragment >> 13
|
||||||
|
mask = (flags << 13) | df_mf_and_fragment
|
||||||
|
fragment = mask ^ df_mf_and_fragment
|
||||||
|
flag, df, mf = False, False, False
|
||||||
|
if flags % 2:
|
||||||
|
mf = True
|
||||||
|
flags -= 1
|
||||||
|
if flags % 2:
|
||||||
|
df = True
|
||||||
|
flags -= 2
|
||||||
|
if flags % 4:
|
||||||
|
flag = True
|
||||||
|
flags -= 4
|
||||||
|
dscp, ecn = split_byte(dscp_and_ecn, 6)
|
||||||
|
return cls(
|
||||||
|
ether_frame, ord(version), ord(hlen), ord(dscp), ord(ecn), tlen, ident, mf, df, flag, fragment, ttl,
|
||||||
|
proto, checksum, source, dest, packet[20:], packet_type, interface
|
||||||
|
)
|
||||||
|
|
||||||
|
def encode(self) -> bytes:
|
||||||
|
return self.ether_frame.encode() + self.get_header() + self.data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def printable_data(self) -> str:
|
||||||
|
return b".".join(printable.findall(self.data)).decode()
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"IPv4(protocol={self.protocol.name}, " \
|
||||||
|
f"iface={self.interface}, " \
|
||||||
|
f"type={self.packet_type.name}, " \
|
||||||
|
f"source={ipv4_to_str(self._source_address)}, " \
|
||||||
|
f"destination={ipv4_to_str(self._destination_address)}, " \
|
||||||
|
f"data_len={len(self.data)})"
|
||||||
|
|
||||||
|
|
||||||
|
def make_filter(l3_protocol=None, src=None, dst=None, invert=False):
|
||||||
|
def filter_packet(packet: IPv4Packet):
|
||||||
|
if l3_protocol and not Layer3(packet.protocol) == l3_protocol:
|
||||||
|
return False
|
||||||
|
if src and not packet.source == src:
|
||||||
|
return False
|
||||||
|
if dst and not packet.destination == dst:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
if invert:
|
||||||
|
return lambda packet: not filter_packet(packet)
|
||||||
|
return filter_packet
|
||||||
|
|
||||||
|
|
||||||
|
async def sniff_ipv4(filters=None, kill=None):
|
||||||
|
start = time.perf_counter()
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
async def sock_recv(sock, n):
|
||||||
|
"""Receive data from the socket.
|
||||||
|
|
||||||
|
The return value is a bytes object representing the data received.
|
||||||
|
The maximum amount of data to be received at once is specified by
|
||||||
|
nbytes.
|
||||||
|
"""
|
||||||
|
if loop._debug and sock.gettimeout() != 0:
|
||||||
|
raise ValueError("the socket must be non-blocking")
|
||||||
|
fut = loop.create_future()
|
||||||
|
_sock_recv(fut, None, sock, n)
|
||||||
|
return await fut
|
||||||
|
|
||||||
|
def _sock_recv(fut, registered_fd, sock, n):
|
||||||
|
# _sock_recv() can add itself as an I/O callback if the operation can't
|
||||||
|
# be done immediately. Don't use it directly, call sock_recv().
|
||||||
|
if registered_fd is not None:
|
||||||
|
# Remove the callback early. It should be rare that the
|
||||||
|
# selector says the fd is ready but the call still returns
|
||||||
|
# EAGAIN, and I am willing to take a hit in that case in
|
||||||
|
# order to simplify the common case.
|
||||||
|
loop.remove_reader(registered_fd)
|
||||||
|
if fut.cancelled():
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
data, flags = sock.recvfrom(n)
|
||||||
|
except (BlockingIOError, InterruptedError):
|
||||||
|
fd = sock.fileno()
|
||||||
|
loop.add_reader(fd, _sock_recv, fut, fd, sock, n)
|
||||||
|
except Exception as exc:
|
||||||
|
fut.set_exception(exc)
|
||||||
|
else:
|
||||||
|
fut.set_result((data, flags))
|
||||||
|
|
||||||
|
with promiscuous('lo') as sock:
|
||||||
|
while True:
|
||||||
|
if not kill:
|
||||||
|
data, flags = await sock_recv(sock, 9000)
|
||||||
|
else:
|
||||||
|
t = asyncio.create_task(sock_recv(sock, 9000))
|
||||||
|
await asyncio.wait([t, kill.wait()], return_when=asyncio.FIRST_COMPLETED)
|
||||||
|
if kill.is_set():
|
||||||
|
break
|
||||||
|
data, flags = await t
|
||||||
|
# https://stackoverflow.com/questions/42821309/how-to-interpret-result-of-recvfrom-raw-socket/45215859#45215859
|
||||||
|
if data:
|
||||||
|
try:
|
||||||
|
ether_frame, packet = EtherFrame.decode(data)
|
||||||
|
if ether_frame.ether_type == Layer2.IPv4.value:
|
||||||
|
interface, _, packet_type, _, _ = flags
|
||||||
|
ipv4 = IPv4Packet.decode(ether_frame, packet, packet_type, interface)
|
||||||
|
if not filters or any((f(ipv4) for f in filters)):
|
||||||
|
yield time.perf_counter() - start, ipv4
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
gateway, lan = get_gateway_and_lan_addresses('default')
|
||||||
|
|
||||||
|
done = asyncio.Event()
|
||||||
|
|
||||||
|
def discover_aioupnp():
|
||||||
|
async def _discover():
|
||||||
|
print("test aioupnp")
|
||||||
|
try:
|
||||||
|
u = await UPnP.discover()
|
||||||
|
try:
|
||||||
|
await u.get_external_ip()
|
||||||
|
except UPnPError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
await u.get_redirects()
|
||||||
|
except UPnPError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
external_port = await u.get_next_mapping(1234, 'TCP', 'aioupnp testing')
|
||||||
|
except UPnPError:
|
||||||
|
external_port = None
|
||||||
|
try:
|
||||||
|
await u.get_redirects()
|
||||||
|
except UPnPError:
|
||||||
|
pass
|
||||||
|
if external_port:
|
||||||
|
try:
|
||||||
|
await u.delete_port_mapping(external_port, 'TCP')
|
||||||
|
except UPnPError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
await u.get_redirects()
|
||||||
|
except UPnPError:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
print("done with aioupnp test")
|
||||||
|
asyncio.create_task(_discover())
|
||||||
|
|
||||||
|
def discover_miniupnpc():
|
||||||
|
def _miniupnpc_discover():
|
||||||
|
try:
|
||||||
|
u = miniupnpc.UPnP()
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
u.discover()
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
u.selectigd()
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
u.externalipaddress()
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
|
||||||
|
async def _discover():
|
||||||
|
print("test miniupnpc")
|
||||||
|
try:
|
||||||
|
await loop.run_in_executor(None, _miniupnpc_discover)
|
||||||
|
finally:
|
||||||
|
done.set()
|
||||||
|
print("done with miniupnpc test")
|
||||||
|
|
||||||
|
asyncio.create_task(_discover())
|
||||||
|
|
||||||
|
loop.call_later(2, discover_aioupnp)
|
||||||
|
loop.call_later(10, discover_miniupnpc)
|
||||||
|
start = time.perf_counter()
|
||||||
|
f = open("upnp_packet_cap.txt", "w")
|
||||||
|
try:
|
||||||
|
async for (ts, ipv4_packet) in sniff_ipv4([
|
||||||
|
make_filter(l3_protocol=Layer3.UDP, src=SSDP_IP_ADDRESS),
|
||||||
|
make_filter(l3_protocol=Layer3.UDP, dst=SSDP_IP_ADDRESS),
|
||||||
|
make_filter(l3_protocol=Layer3.UDP, src=lan, dst=gateway),
|
||||||
|
make_filter(l3_protocol=Layer3.UDP, src=gateway, dst=lan),
|
||||||
|
make_filter(l3_protocol=Layer3.TCP, src=lan, dst=gateway),
|
||||||
|
make_filter(l3_protocol=Layer3.TCP, src=gateway, dst=lan)], done):
|
||||||
|
f.write(f"{time.perf_counter() - start},{ipv4_packet.packet_type.name},{ipv4_packet.source},{ipv4_packet.destination},{base64.b64encode(ipv4_packet.data).decode()}\n")
|
||||||
|
print(ts, ipv4_packet)
|
||||||
|
print(ipv4_packet.printable_data)
|
||||||
|
print()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("stopping")
|
||||||
|
finally:
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
Loading…
Reference in a new issue