542 lines
18 KiB
Python
542 lines
18 KiB
Python
"""
|
|
Pico W backend network bridge — full unit + integration coverage.
|
|
|
|
The bridge is split across:
|
|
backend/app/services/picow_net/
|
|
bridge.py — orchestrator
|
|
protocols.py — Ethernet/IPv4/TCP/UDP/ICMP/ARP/DHCP/DNS codecs
|
|
checksums.py — RFC 1071 + pseudo-header
|
|
arp.py / dhcp.py / dns.py / icmp.py / tcp_nat.py / udp_nat.py
|
|
|
|
These tests run *without* the frontend — we synthesise the chip's
|
|
side of every interaction (ARP requests, DHCP DISCOVERs, TCP segments,
|
|
UDP datagrams, ICMP echo) and assert the bridge produces correct
|
|
replies. Where real network I/O is involved (TCP open, DNS lookup,
|
|
UDP datagram), we stand up a tiny local server inside the test.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import socket
|
|
import struct
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[3]
|
|
sys.path.insert(0, str(ROOT / 'backend'))
|
|
|
|
from app.services.picow_net.checksums import ( # noqa: E402
|
|
internet_checksum,
|
|
tcp_udp_checksum,
|
|
)
|
|
from app.services.picow_net.consts import ( # noqa: E402
|
|
ETHERTYPE_ARP,
|
|
ETHERTYPE_IPV4,
|
|
GATEWAY_IP,
|
|
GATEWAY_MAC,
|
|
IPPROTO_ICMP,
|
|
IPPROTO_TCP,
|
|
IPPROTO_UDP,
|
|
STA_IP,
|
|
STA_MAC,
|
|
TCP_ACK,
|
|
TCP_FIN,
|
|
TCP_PSH,
|
|
TCP_SYN,
|
|
ip_to_bytes,
|
|
)
|
|
from app.services.picow_net.protocols import ( # noqa: E402
|
|
Arp,
|
|
Dhcp,
|
|
DnsMessage,
|
|
Ethernet,
|
|
ICMP,
|
|
IPv4,
|
|
TCP,
|
|
UDP,
|
|
DHCP_MAGIC,
|
|
parse_tcp_options,
|
|
)
|
|
from app.services.picow_net.bridge import PicowNetBridge # noqa: E402
|
|
|
|
|
|
# ─── checksum primitives ────────────────────────────────────────────
|
|
|
|
class TestChecksums:
|
|
"""Hand-computed RFC 1071 examples + a real TCP segment."""
|
|
|
|
def test_internet_checksum_zero_input(self):
|
|
# Sum of nothing is 0; one's complement of 0 is 0xffff.
|
|
assert internet_checksum(b'\x00\x00') == 0xffff
|
|
|
|
def test_internet_checksum_all_ones_cancels(self):
|
|
# 0xffff + 0xffff carries to 1, end-around adds → 0xffff.
|
|
# complement → 0.
|
|
assert internet_checksum(b'\xff\xff\xff\xff') == 0
|
|
|
|
def test_ipv4_header_checksum_known_value(self):
|
|
# IPv4 header with checksum=0:
|
|
# 45 00 00 28 00 00 40 00 40 06 00 00 7f 00 00 01 7f 00 00 01
|
|
# Per RFC 1071 worked example: cksum = 0xb96a-ish (actually 0xb961).
|
|
hdr = bytes.fromhex('4500002800004000400600007f0000017f000001')
|
|
c = internet_checksum(hdr)
|
|
# Re-inserting and re-summing should yield 0xffff.
|
|
verified = (hdr[:10] + struct.pack('!H', c) + hdr[12:])
|
|
s = internet_checksum(verified)
|
|
assert s == 0
|
|
|
|
def test_tcp_checksum_round_trip(self):
|
|
src_ip = ip_to_bytes('10.0.0.1')
|
|
dst_ip = ip_to_bytes('10.0.0.2')
|
|
# Bare TCP segment: SYN, src 1234, dst 80, seq=1, ack=0
|
|
seg = struct.pack('!HHIIHHHH',
|
|
1234, 80, 1, 0, (5 << 12) | TCP_SYN, 65535, 0, 0,
|
|
)
|
|
c = tcp_udp_checksum(src_ip, dst_ip, IPPROTO_TCP, seg)
|
|
seg_with = seg[:16] + struct.pack('!H', c) + seg[18:]
|
|
# Rolling checksum over the whole thing should verify.
|
|
from app.services.picow_net.checksums import (
|
|
tcp_udp_pseudo_header, ones_complement_sum,
|
|
)
|
|
pseudo = tcp_udp_pseudo_header(src_ip, dst_ip, IPPROTO_TCP, len(seg_with))
|
|
assert ones_complement_sum(pseudo + seg_with) == 0xffff
|
|
|
|
|
|
# ─── protocol parsers ───────────────────────────────────────────────
|
|
|
|
class TestProtocols:
|
|
def test_ethernet_round_trip(self):
|
|
f = Ethernet(dst=STA_MAC, src=GATEWAY_MAC, ethertype=ETHERTYPE_IPV4,
|
|
payload=b'hello')
|
|
assert Ethernet.parse(f.to_bytes()) == f
|
|
|
|
def test_ipv4_round_trip(self):
|
|
ip = IPv4(
|
|
protocol=IPPROTO_TCP,
|
|
src=ip_to_bytes('1.2.3.4'),
|
|
dst=ip_to_bytes('5.6.7.8'),
|
|
payload=b'\x00' * 20,
|
|
)
|
|
out = ip.to_bytes()
|
|
ip2 = IPv4.parse(out)
|
|
assert ip2.src == ip.src and ip2.dst == ip.dst
|
|
assert ip2.protocol == IPPROTO_TCP
|
|
# Header checksum verifies.
|
|
from app.services.picow_net.checksums import internet_checksum
|
|
assert internet_checksum(out[:20]) == 0
|
|
|
|
def test_tcp_options_parser(self):
|
|
# MSS=1460, NOP, SACK-permitted (kind=4 length=2)
|
|
opts = b'\x02\x04\x05\xb4\x01\x04\x02'
|
|
parsed = parse_tcp_options(opts)
|
|
assert parsed['mss'] == 1460
|
|
assert 4 in parsed
|
|
|
|
def test_arp_round_trip(self):
|
|
a = Arp(
|
|
opcode=1,
|
|
sha=STA_MAC,
|
|
spa=ip_to_bytes(STA_IP),
|
|
tha=b'\x00' * 6,
|
|
tpa=ip_to_bytes(GATEWAY_IP),
|
|
)
|
|
b = a.to_bytes()
|
|
a2 = Arp.parse(b)
|
|
assert a2.opcode == 1
|
|
assert bytes(a2.spa) == ip_to_bytes(STA_IP)
|
|
|
|
def test_dhcp_round_trip(self):
|
|
d = Dhcp(
|
|
op=1, xid=0xdeadbeef,
|
|
chaddr=STA_MAC + b'\x00' * 10,
|
|
options={53: bytes([1])},
|
|
)
|
|
out = d.to_bytes()
|
|
assert DHCP_MAGIC in out
|
|
d2 = Dhcp.parse(out)
|
|
assert d2.xid == 0xdeadbeef
|
|
assert d2.options.get(53) == bytes([1])
|
|
|
|
def test_dns_round_trip(self):
|
|
m = DnsMessage(
|
|
txid=0x4242,
|
|
flags=0x0100,
|
|
qd=[('example.com', 1, 1)],
|
|
)
|
|
b = m.to_bytes()
|
|
m2 = DnsMessage.parse(b)
|
|
assert m2.txid == 0x4242
|
|
assert m2.qd[0][0] == 'example.com'
|
|
|
|
|
|
# ─── bridge — ARP ───────────────────────────────────────────────────
|
|
|
|
class _Capture:
|
|
"""Helper that captures every (kind, data) emit() call."""
|
|
def __init__(self):
|
|
self.events: list[tuple[str, dict]] = []
|
|
|
|
async def __call__(self, kind: str, data: dict) -> None:
|
|
self.events.append((kind, data))
|
|
|
|
def latest_packet_in(self) -> bytes | None:
|
|
for k, d in reversed(self.events):
|
|
if k == 'picow_packet_in':
|
|
import base64
|
|
return base64.b64decode(d['ether_b64'])
|
|
return None
|
|
|
|
def all_packets_in(self) -> list[bytes]:
|
|
out: list[bytes] = []
|
|
import base64
|
|
for k, d in self.events:
|
|
if k == 'picow_packet_in':
|
|
out.append(base64.b64decode(d['ether_b64']))
|
|
return out
|
|
|
|
|
|
def _make_arp_request(sender_mac: bytes, sender_ip: str, target_ip: str) -> bytes:
|
|
arp = Arp(
|
|
opcode=1,
|
|
sha=sender_mac,
|
|
spa=ip_to_bytes(sender_ip),
|
|
tha=b'\x00' * 6,
|
|
tpa=ip_to_bytes(target_ip),
|
|
)
|
|
return Ethernet(
|
|
dst=b'\xff' * 6,
|
|
src=sender_mac,
|
|
ethertype=ETHERTYPE_ARP,
|
|
payload=arp.to_bytes(),
|
|
).to_bytes()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_arp_responder_replies_for_gateway():
|
|
cap = _Capture()
|
|
bridge = PicowNetBridge('test', cap, wifi_enabled=True)
|
|
await bridge.start()
|
|
|
|
await bridge.deliver_packet_out(_make_arp_request(STA_MAC, STA_IP, GATEWAY_IP))
|
|
pkt = cap.latest_packet_in()
|
|
assert pkt is not None
|
|
eth = Ethernet.parse(pkt)
|
|
assert eth.ethertype == ETHERTYPE_ARP
|
|
arp = Arp.parse(eth.payload)
|
|
assert arp.opcode == 2
|
|
assert bytes(arp.sha) == GATEWAY_MAC
|
|
assert bytes(arp.spa) == ip_to_bytes(GATEWAY_IP)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_arp_responder_answers_for_arbitrary_host():
|
|
"""Slirp-style: every IP outside the LAN resolves to the gateway MAC."""
|
|
cap = _Capture()
|
|
bridge = PicowNetBridge('test', cap, wifi_enabled=True)
|
|
await bridge.start()
|
|
await bridge.deliver_packet_out(_make_arp_request(STA_MAC, STA_IP, '8.8.8.8'))
|
|
pkt = cap.latest_packet_in()
|
|
assert pkt is not None
|
|
arp = Arp.parse(Ethernet.parse(pkt).payload)
|
|
assert arp.opcode == 2
|
|
assert bytes(arp.sha) == GATEWAY_MAC
|
|
assert bytes(arp.spa) == ip_to_bytes('8.8.8.8')
|
|
|
|
|
|
# ─── bridge — DHCP ──────────────────────────────────────────────────
|
|
|
|
def _make_dhcp_msg(msg_type: int, xid: int = 0xdeadbeef) -> bytes:
|
|
dhcp = Dhcp(
|
|
op=1, xid=xid,
|
|
chaddr=STA_MAC + b'\x00' * 10,
|
|
options={53: bytes([msg_type])},
|
|
)
|
|
udp = UDP(src_port=68, dst_port=67, payload=dhcp.to_bytes())
|
|
src_ip = ip_to_bytes('0.0.0.0')
|
|
dst_ip = ip_to_bytes('255.255.255.255')
|
|
ip = IPv4(
|
|
protocol=IPPROTO_UDP, src=src_ip, dst=dst_ip,
|
|
payload=udp.to_bytes(src_ip, dst_ip),
|
|
)
|
|
return Ethernet(
|
|
dst=b'\xff' * 6, src=STA_MAC,
|
|
ethertype=ETHERTYPE_IPV4, payload=ip.to_bytes(),
|
|
).to_bytes()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dhcp_offer_then_ack():
|
|
cap = _Capture()
|
|
bridge = PicowNetBridge('test', cap, wifi_enabled=True)
|
|
await bridge.start()
|
|
|
|
await bridge.deliver_packet_out(_make_dhcp_msg(1)) # DISCOVER
|
|
await bridge.deliver_packet_out(_make_dhcp_msg(3)) # REQUEST
|
|
|
|
pkts = cap.all_packets_in()
|
|
assert len(pkts) == 2
|
|
for raw in pkts:
|
|
eth = Ethernet.parse(raw)
|
|
ip = IPv4.parse(eth.payload)
|
|
udp = UDP.parse(ip.payload)
|
|
d = Dhcp.parse(udp.payload)
|
|
assert d.op == 2 # BOOTREPLY
|
|
assert bytes(d.yiaddr) == ip_to_bytes(STA_IP)
|
|
# Server-id option (54) must be the gateway IP.
|
|
assert d.options[54] == ip_to_bytes(GATEWAY_IP)
|
|
|
|
|
|
# ─── bridge — ICMP echo ─────────────────────────────────────────────
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_icmp_echo_reply():
|
|
cap = _Capture()
|
|
bridge = PicowNetBridge('test', cap, wifi_enabled=True)
|
|
await bridge.start()
|
|
|
|
icmp = ICMP(
|
|
type=8, code=0,
|
|
rest=struct.pack('!HH', 0x1234, 0x0001),
|
|
payload=b'velxio-ping-payload',
|
|
)
|
|
ip = IPv4(
|
|
protocol=IPPROTO_ICMP,
|
|
src=ip_to_bytes(STA_IP),
|
|
dst=ip_to_bytes('1.1.1.1'),
|
|
payload=icmp.to_bytes(),
|
|
)
|
|
frame = Ethernet(GATEWAY_MAC, STA_MAC, ETHERTYPE_IPV4, ip.to_bytes()).to_bytes()
|
|
await bridge.deliver_packet_out(frame)
|
|
|
|
pkt = cap.latest_packet_in()
|
|
assert pkt is not None
|
|
rip = IPv4.parse(Ethernet.parse(pkt).payload)
|
|
rcmp = ICMP.parse(rip.payload)
|
|
assert rcmp.type == 0 # ECHO REPLY
|
|
assert rcmp.payload == b'velxio-ping-payload'
|
|
assert rcmp.rest == struct.pack('!HH', 0x1234, 0x0001)
|
|
|
|
|
|
# ─── bridge — UDP NAT (full round-trip via local echo server) ───────
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_udp_nat_round_trip():
|
|
"""Stand up a local UDP echo server and verify chip→host→chip flow."""
|
|
loop = asyncio.get_event_loop()
|
|
received: list[bytes] = []
|
|
|
|
class EchoProto(asyncio.DatagramProtocol):
|
|
def __init__(self):
|
|
self.transport: asyncio.DatagramTransport | None = None
|
|
def connection_made(self, t):
|
|
self.transport = t
|
|
def datagram_received(self, data, addr):
|
|
received.append(data)
|
|
assert self.transport is not None
|
|
self.transport.sendto(b'echo:' + data, addr)
|
|
|
|
server_transport, _ = await loop.create_datagram_endpoint(
|
|
EchoProto, local_addr=('127.0.0.1', 0),
|
|
)
|
|
server_host, server_port = server_transport.get_extra_info('sockname')
|
|
|
|
cap = _Capture()
|
|
bridge = PicowNetBridge('test', cap, wifi_enabled=True)
|
|
await bridge.start()
|
|
|
|
udp = UDP(src_port=33333, dst_port=server_port, payload=b'velxio-udp')
|
|
ip = IPv4(
|
|
protocol=IPPROTO_UDP,
|
|
src=ip_to_bytes(STA_IP),
|
|
dst=ip_to_bytes(server_host),
|
|
payload=udp.to_bytes(ip_to_bytes(STA_IP), ip_to_bytes(server_host)),
|
|
)
|
|
frame = Ethernet(GATEWAY_MAC, STA_MAC, ETHERTYPE_IPV4, ip.to_bytes()).to_bytes()
|
|
await bridge.deliver_packet_out(frame)
|
|
|
|
# Wait up to 2 s for the echo to arrive back.
|
|
for _ in range(40):
|
|
if cap.latest_packet_in() is not None:
|
|
break
|
|
await asyncio.sleep(0.05)
|
|
|
|
server_transport.close()
|
|
await bridge.stop()
|
|
|
|
assert received == [b'velxio-udp']
|
|
pkt = cap.latest_packet_in()
|
|
assert pkt is not None
|
|
rip = IPv4.parse(Ethernet.parse(pkt).payload)
|
|
rudp = UDP.parse(rip.payload)
|
|
assert rudp.payload == b'echo:velxio-udp'
|
|
assert rudp.dst_port == 33333
|
|
|
|
|
|
# ─── bridge — TCP NAT (full RFC 793 handshake + data + close) ──────
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tcp_nat_full_round_trip():
|
|
"""Spin up a local HTTP-shaped server and walk through the whole TCP
|
|
state machine: SYN → SYN+ACK → ACK → data → FIN."""
|
|
|
|
async def handle(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
|
|
data = await reader.readuntil(b'\r\n\r\n')
|
|
assert b'GET /hello' in data
|
|
writer.write(b'HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello')
|
|
await writer.drain()
|
|
writer.close()
|
|
await writer.wait_closed()
|
|
|
|
server = await asyncio.start_server(handle, '127.0.0.1', 0)
|
|
server_host, server_port = server.sockets[0].getsockname()
|
|
|
|
cap = _Capture()
|
|
bridge = PicowNetBridge('test', cap, wifi_enabled=True)
|
|
await bridge.start()
|
|
|
|
chip_port = 44444
|
|
chip_isn = 0x1000
|
|
|
|
async def send_tcp(seq: int, ack: int, flags: int, payload: bytes = b''):
|
|
tcp = TCP(
|
|
src_port=chip_port, dst_port=server_port,
|
|
seq=seq, ack=ack, flags=flags, window=65535,
|
|
payload=payload,
|
|
)
|
|
ip = IPv4(
|
|
protocol=IPPROTO_TCP,
|
|
src=ip_to_bytes(STA_IP),
|
|
dst=ip_to_bytes(server_host),
|
|
payload=tcp.to_bytes(ip_to_bytes(STA_IP), ip_to_bytes(server_host)),
|
|
)
|
|
frame = Ethernet(GATEWAY_MAC, STA_MAC, ETHERTYPE_IPV4, ip.to_bytes()).to_bytes()
|
|
await bridge.deliver_packet_out(frame)
|
|
|
|
async def wait_for_segment_with_flags(
|
|
expected_flags: int, start_idx: int = 0, timeout: float = 2.0,
|
|
) -> tuple[TCP, int]:
|
|
"""Scan from start_idx onward; return (tcp, index_after_match)."""
|
|
deadline = asyncio.get_event_loop().time() + timeout
|
|
while asyncio.get_event_loop().time() < deadline:
|
|
for i in range(start_idx, len(cap.events)):
|
|
k, d = cap.events[i]
|
|
if k != 'picow_packet_in':
|
|
continue
|
|
import base64
|
|
raw = base64.b64decode(d['ether_b64'])
|
|
eth = Ethernet.parse(raw)
|
|
if eth.ethertype != ETHERTYPE_IPV4:
|
|
continue
|
|
ip = IPv4.parse(eth.payload)
|
|
if ip.protocol != IPPROTO_TCP:
|
|
continue
|
|
tcp = TCP.parse(ip.payload)
|
|
if (tcp.flags & expected_flags) == expected_flags:
|
|
return tcp, i + 1
|
|
await asyncio.sleep(0.02)
|
|
raise AssertionError(f'no TCP segment with flags 0x{expected_flags:x} arrived')
|
|
|
|
# 1. SYN
|
|
await send_tcp(seq=chip_isn, ack=0, flags=TCP_SYN)
|
|
syn_ack, idx = await wait_for_segment_with_flags(TCP_SYN | TCP_ACK)
|
|
assert syn_ack.ack == chip_isn + 1
|
|
server_isn = syn_ack.seq
|
|
|
|
# 2. ACK the SYN+ACK and immediately PSH a GET request.
|
|
request = b'GET /hello HTTP/1.1\r\nHost: x\r\n\r\n'
|
|
await send_tcp(
|
|
seq=chip_isn + 1, ack=server_isn + 1,
|
|
flags=TCP_ACK | TCP_PSH, payload=request,
|
|
)
|
|
|
|
# 3. Read the response. We expect:
|
|
# - an ACK for our request
|
|
# - a PSH+ACK with HTTP/1.1 200 OK + body
|
|
# - a FIN+ACK from the server
|
|
response_segments = []
|
|
deadline = asyncio.get_event_loop().time() + 3.0
|
|
fin_seen = False
|
|
while asyncio.get_event_loop().time() < deadline and not fin_seen:
|
|
for i in range(idx, len(cap.events)):
|
|
k, d = cap.events[i]
|
|
if k != 'picow_packet_in':
|
|
continue
|
|
import base64
|
|
raw = base64.b64decode(d['ether_b64'])
|
|
eth = Ethernet.parse(raw)
|
|
if eth.ethertype != ETHERTYPE_IPV4:
|
|
continue
|
|
ip = IPv4.parse(eth.payload)
|
|
if ip.protocol != IPPROTO_TCP:
|
|
continue
|
|
tcp = TCP.parse(ip.payload)
|
|
if tcp.payload:
|
|
response_segments.append(tcp.payload)
|
|
if tcp.flags & TCP_FIN:
|
|
fin_seen = True
|
|
idx = len(cap.events)
|
|
await asyncio.sleep(0.02)
|
|
|
|
assert fin_seen
|
|
body = b''.join(response_segments)
|
|
assert b'HTTP/1.1 200 OK' in body
|
|
assert body.endswith(b'hello')
|
|
|
|
# 4. Send our own FIN to close the chip side cleanly.
|
|
await send_tcp(
|
|
seq=chip_isn + 1 + len(request),
|
|
ack=server_isn + 1 + len(body) + 1,
|
|
flags=TCP_ACK | TCP_FIN,
|
|
)
|
|
|
|
server.close()
|
|
await server.wait_closed()
|
|
await bridge.stop()
|
|
|
|
|
|
# ─── bridge — DNS proxy (uses host resolver) ────────────────────────
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dns_proxy_localhost():
|
|
"""Resolve 'localhost' through the bridge — should return 127.0.0.1."""
|
|
cap = _Capture()
|
|
bridge = PicowNetBridge('test', cap, wifi_enabled=True)
|
|
await bridge.start()
|
|
|
|
query = DnsMessage(txid=0x1234, flags=0x0100, qd=[('localhost', 1, 1)])
|
|
udp = UDP(src_port=12345, dst_port=53, payload=query.to_bytes())
|
|
src_ip = ip_to_bytes(STA_IP)
|
|
dst_ip = ip_to_bytes(GATEWAY_IP)
|
|
ip = IPv4(
|
|
protocol=IPPROTO_UDP, src=src_ip, dst=dst_ip,
|
|
payload=udp.to_bytes(src_ip, dst_ip),
|
|
)
|
|
frame = Ethernet(GATEWAY_MAC, STA_MAC, ETHERTYPE_IPV4, ip.to_bytes()).to_bytes()
|
|
await bridge.deliver_packet_out(frame)
|
|
|
|
pkt = cap.latest_packet_in()
|
|
assert pkt is not None
|
|
rip = IPv4.parse(Ethernet.parse(pkt).payload)
|
|
rudp = UDP.parse(rip.payload)
|
|
resp = DnsMessage.parse(rudp.payload)
|
|
assert resp.txid == 0x1234
|
|
assert resp.flags & 0x8000 # response bit
|
|
# localhost should always resolve, regardless of host config.
|
|
assert any(rdata == b'\x7f\x00\x00\x01' for (_, _, _, _, rdata) in resp.an)
|
|
|
|
|
|
# ─── bridge — wifi disabled = silent drop ──────────────────────────
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wifi_disabled_silently_drops_packets():
|
|
cap = _Capture()
|
|
bridge = PicowNetBridge('test', cap, wifi_enabled=False)
|
|
await bridge.start()
|
|
|
|
await bridge.deliver_packet_out(_make_arp_request(STA_MAC, STA_IP, GATEWAY_IP))
|
|
# No packets should have been injected.
|
|
assert all(k != 'picow_packet_in' for k, _ in cap.events)
|