mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 01:13:00 +02:00
test: Add Wi-Fi tests
Should map on physical as well, but only tested on virtual.
This commit is contained in:
@@ -654,10 +654,17 @@ def main():
|
||||
err = 1
|
||||
|
||||
os.umask(0o337)
|
||||
# Write atomically: confd reads system.json as soon as ixinit signals
|
||||
# done, so a truncated in-place write could be read half-written. Write
|
||||
# to a temp file in the same dir, then rename (atomic on the same fs).
|
||||
# pylint: disable=invalid-name
|
||||
with open(SYSTEM_JSON, "w", encoding="ascii") as f:
|
||||
tmp = SYSTEM_JSON + ".tmp"
|
||||
with open(tmp, "w", encoding="ascii") as f:
|
||||
json.dump(out, f)
|
||||
shutil.chown(SYSTEM_JSON, user="root", group="wheel")
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
shutil.chown(tmp, user="root", group="wheel")
|
||||
os.replace(tmp, SYSTEM_JSON)
|
||||
return err
|
||||
|
||||
|
||||
|
||||
@@ -146,6 +146,7 @@ INFIX_DOC="https://www.kernelkit.org/infix/"
|
||||
INFIX_SUPPORT="mailto:kernelkit@googlegroups.com"
|
||||
BR2_PACKAGE_FEATURE_GPS=y
|
||||
BR2_PACKAGE_FEATURE_WIFI=y
|
||||
BR2_PACKAGE_FEATURE_WIFI_HWSIM=y
|
||||
BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y
|
||||
BR2_PACKAGE_FEATURE_WIFI_QUALCOMM=y
|
||||
BR2_PACKAGE_FEATURE_WIFI_REALTEK=y
|
||||
|
||||
@@ -297,6 +297,36 @@ image. The device will boot into test mode on first power-on.
|
||||
> unless `startup_override()` is called first (or the marker is removed
|
||||
> and a normal startup config is saved).
|
||||
|
||||
Simulated Wi-Fi
|
||||
---------------
|
||||
|
||||
The virtual topology has no real radios, so Wi-Fi tests run against
|
||||
`mac80211_hwsim` devices and the "air" between guests is faked. A small
|
||||
relay, `wifimedium` (`package/feature-wifi/wifimedium`), takes every frame
|
||||
a radio transmits, forwards it to the other guests, and injects what
|
||||
arrives back into the local radios.
|
||||
|
||||
Each radio (`radio0`, `radio1`, ...) is paired with a carrier NIC of the
|
||||
same name, and that carrier joins the multicast group (a *cell*) the
|
||||
topology wires it to. Two radios hear each other only when their carriers
|
||||
share a cell. Radios on the same cell that tune to different channels are
|
||||
still kept apart, as on real hardware.
|
||||
|
||||
By convention the two general cells carry `radio0` and `radio1` across all
|
||||
DUTs, so most tests put communicating radios on the same index: a station
|
||||
joins an AP on the same `radioN`. The mesh-roaming test, for instance,
|
||||
uses `radio1` for both the gateway APs and the client. That is only a
|
||||
convention, though. A cell can carry any set of radios, including two of
|
||||
the *same* DUT. The band-steering test relies on that: it wires one DUT's
|
||||
`radio2` (2.4GHz) and `radio3` (5GHz) onto a dedicated cell so a single
|
||||
client radio hears the same SSID on both bands.
|
||||
|
||||
How many radios each DUT gets depends on the topology. The number of
|
||||
`radioN` ports wired to a node is passed in via the `opt/wifi` fw_cfg and
|
||||
loaded at boot by the `00-hwsim` script, so a node with no wifi links has
|
||||
no radios at all. The wiring for the standard suite lives in
|
||||
`test/virt/quad`.
|
||||
|
||||
[9PM]: https://github.com/rical/9pm
|
||||
[Qeneth]: https://github.com/wkz/qeneth
|
||||
[TAP]: https://testanything.org/
|
||||
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/bin/sh
|
||||
# Load mac80211_hwsim virtual radios sized to this DUT's topology, then wait
|
||||
# for udev to rename the phys (phyN -> radioN) before returning.
|
||||
#
|
||||
# The radio count comes from a dedicated fw_cfg file (opt/wifi) that qeneth
|
||||
# writes per node from the number of radioN carrier ports the topology wired
|
||||
# to it. A node with no wifi links gets 0 (or no file), so hwsim is not loaded
|
||||
# at all -- no phys, nothing clutters its interface view.
|
||||
#
|
||||
# Runs early in ixinit, before 00-probe, so the system probe records the radios
|
||||
# by their final radioN names. ixinit is past the <pid/syslogd> / udev-settle
|
||||
# barrier, so 60-rename-wifi-phy.rules (phyN -> radioN) and
|
||||
# 70-remove-virtual-wifi-interfaces.rules (drop the stray wlanN) are loaded and
|
||||
# fire on the phy add events. On success it sets the <usr/hwsim> condition so
|
||||
# wifimedium starts only where radios exist. Installed only with
|
||||
# BR2_PACKAGE_FEATURE_WIFI_HWSIM.
|
||||
|
||||
wifi=/sys/firmware/qemu_fw_cfg/by_name/opt/wifi/raw
|
||||
ieee=/sys/class/ieee80211
|
||||
|
||||
# Number of radios to create, from the topology (via fw_cfg).
|
||||
radios=0
|
||||
[ -r "$wifi" ] && radios=$(cat "$wifi" 2>/dev/null)
|
||||
|
||||
# No radios wanted -> don't load hwsim, keep the view wifi-free.
|
||||
[ "$radios" -gt 0 ] 2>/dev/null || exit 0
|
||||
|
||||
modprobe mac80211_hwsim radios="$radios" || exit 0
|
||||
|
||||
# Wait (bounded ~10s) for the phyN -> radioN rename to finish: the class has
|
||||
# entries and none are still named phyN. The rename is a udev RUN, so it lags
|
||||
# modprobe's return. On timeout the radios never settled -- fail (and log) so
|
||||
# it is visible why 00-probe may record stale phyN names.
|
||||
TIMEOUT=100
|
||||
i=0
|
||||
while [ "$i" -lt "$TIMEOUT" ]; do
|
||||
names=$(ls "$ieee" 2>/dev/null)
|
||||
if [ -n "$names" ] && ! echo "$names" | grep -q '^phy'; then
|
||||
initctl -bq cond set hwsim
|
||||
exit 0
|
||||
fi
|
||||
i=$((i + 1))
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
logger -p user.err -t 00-hwsim "timed out waiting for hwsim phys to be renamed radioN"
|
||||
exit 1
|
||||
@@ -18,6 +18,18 @@ config BR2_PACKAGE_FEATURE_WIFI
|
||||
help
|
||||
Enables WiFi in Infix. Enables all requried applications.
|
||||
|
||||
config BR2_PACKAGE_FEATURE_WIFI_HWSIM
|
||||
bool "Virtual Wi-Fi radios (mac80211_hwsim)"
|
||||
depends on BR2_PACKAGE_FEATURE_WIFI
|
||||
help
|
||||
Build the mac80211_hwsim virtual radio driver (as a module) for the
|
||||
QEMU test target, to exercise AP, station, and mesh modes without
|
||||
physical WiFi adapters. The module is autoloaded at boot (see the
|
||||
x86_64 /etc/modules-load.d) and frames are bridged between guests by
|
||||
the wifimedium relay.
|
||||
|
||||
Leave disabled on real hardware; it only provides simulated radios.
|
||||
|
||||
config BR2_PACKAGE_FEATURE_WIFI_MEDIATEK
|
||||
bool "Mediatek WiFi Devices"
|
||||
depends on BR2_PACKAGE_FEATURE_WIFI
|
||||
|
||||
@@ -14,6 +14,15 @@ define FEATURE_WIFI_LINUX_CONFIG_FIXUPS
|
||||
$(call KCONFIG_SET_OPT,CONFIG_CFG80211,m)
|
||||
$(call KCONFIG_ENABLE_OPT,CONFIG_MAC80211_MESH)
|
||||
|
||||
# Virtual radio for automated testing (mac80211_hwsim), built as a
|
||||
# module and gated behind BR2_PACKAGE_FEATURE_WIFI_HWSIM so it is not
|
||||
# built on real boards. When selected, the module is loaded early in
|
||||
# ixinit by 00-hwsim -- sized to the DUT's topology, so radio-less nodes
|
||||
# load nothing -- and bridged between QEMU guests by the wifimedium relay
|
||||
# (both installed by FEATURE_WIFI_INSTALL_HWSIM below).
|
||||
$(if $(BR2_PACKAGE_FEATURE_WIFI_HWSIM),
|
||||
$(call KCONFIG_SET_OPT,CONFIG_MAC80211_HWSIM,m))
|
||||
|
||||
$(if $(filter y,$(BR2_PACKAGE_FEATURE_WIFI_MEDIATEK)),
|
||||
$(call KCONFIG_ENABLE_OPT,CONFIG_MT7601U)
|
||||
$(call KCONFIG_ENABLE_OPT,CONFIG_MT76x0U)
|
||||
@@ -76,5 +85,20 @@ define FEATURE_WIFI_INSTALL_IN_ROMFS
|
||||
endef
|
||||
FEATURE_WIFI_POST_INSTALL_TARGET_HOOKS += FEATURE_WIFI_INSTALL_IN_ROMFS
|
||||
|
||||
# Virtual-radio (mac80211_hwsim) runtime, installed only when the option is
|
||||
# selected: the 00-hwsim ixinit script loads the module sized to the opt/wifi
|
||||
# fw_cfg qeneth writes per node (no radios -> not loaded), and the wifimedium
|
||||
# relay + its Finit service bridge hwsim frames between QEMU guests so they can
|
||||
# associate "wirelessly".
|
||||
ifeq ($(BR2_PACKAGE_FEATURE_WIFI_HWSIM),y)
|
||||
define FEATURE_WIFI_INSTALL_HWSIM
|
||||
$(INSTALL) -D -m 0755 $(FEATURE_WIFI_PKGDIR)/00-hwsim $(TARGET_DIR)/usr/libexec/infix/init.d/00-hwsim
|
||||
$(INSTALL) -D -m 0755 $(FEATURE_WIFI_PKGDIR)/wifimedium $(TARGET_DIR)/usr/libexec/infix/wifimedium
|
||||
$(INSTALL) -D -m 0644 $(FEATURE_WIFI_PKGDIR)/wifimedium.conf $(TARGET_DIR)/etc/finit.d/available/wifimedium.conf
|
||||
ln -sf ../available/wifimedium.conf $(TARGET_DIR)/etc/finit.d/enabled/wifimedium.conf
|
||||
endef
|
||||
FEATURE_WIFI_POST_INSTALL_TARGET_HOOKS += FEATURE_WIFI_INSTALL_HWSIM
|
||||
endif
|
||||
|
||||
|
||||
$(eval $(generic-package))
|
||||
|
||||
Executable
+401
@@ -0,0 +1,401 @@
|
||||
#!/usr/bin/env python3
|
||||
"""wifimedium - a virtual WiFi medium for mac80211_hwsim, extended over Ethernet.
|
||||
|
||||
mac80211_hwsim simulates 802.11 radios in software. By default all radios in
|
||||
one kernel share an in-kernel medium; radios in *different* kernels (i.e.
|
||||
different QEMU guests) cannot hear each other. The moment a userspace process
|
||||
registers on the hwsim generic-netlink family, the kernel stops delivering
|
||||
frames itself and hands every transmitted frame to that process, which becomes
|
||||
responsible for delivery.
|
||||
|
||||
This daemon is that process. On each DUT it:
|
||||
|
||||
* registers on the "mac80211_hwsim" genl family and receives every frame the
|
||||
local radios transmit (HWSIM_CMD_FRAME),
|
||||
* acknowledges each transmit back to the kernel (HWSIM_CMD_TX_INFO_FRAME) so
|
||||
mac80211's TX path completes, and
|
||||
* relays the frame per radio: each radio (phy radioN) is paired by name with a
|
||||
carrier NIC (netdev radioN) that joins one multicast "cell" -- a QEMU socket
|
||||
multicast group shared by every DUT's radioN (see test/virt/quad and
|
||||
test/qeneth). A radio's frame is sent only onto its own carrier, and a
|
||||
carrier's frames are injected only into its own radio
|
||||
(HWSIM_CMD_FRAME with HWSIM_ATTR_ADDR_RECEIVER).
|
||||
|
||||
Each cell is a shared medium: every DUT's radioN is "in range" of every other
|
||||
radioN. Radios on different indices are on different cells, so two radios
|
||||
communicate only if they share an index -- the convention the test topologies
|
||||
follow (a station uses the same radio index as the AP it joins). Channel
|
||||
filtering in mac80211 still separates radios that share a cell but tune to
|
||||
different frequencies.
|
||||
|
||||
This is intended for the QEMU/qeneth test topology only. On real hardware the
|
||||
radios use the air, there are no radioN carrier netdevs, and this daemon idles.
|
||||
See doc/wifi.md.
|
||||
"""
|
||||
import os
|
||||
import socket
|
||||
import struct
|
||||
import time
|
||||
import subprocess
|
||||
import sys
|
||||
import select
|
||||
|
||||
# ---- generic netlink / hwsim ABI (drivers/net/wireless/virtual/mac80211_hwsim.h)
|
||||
NETLINK_GENERIC = 16
|
||||
GENL_ID_CTRL = 16
|
||||
CTRL_CMD_GETFAMILY = 3
|
||||
CTRL_ATTR_FAMILY_ID = 1
|
||||
CTRL_ATTR_FAMILY_NAME = 2
|
||||
CTRL_ATTR_MCAST_GROUPS = 7
|
||||
CTRL_ATTR_MCAST_GRP_NAME = 1
|
||||
CTRL_ATTR_MCAST_GRP_ID = 2
|
||||
|
||||
SOL_NETLINK = 270
|
||||
NETLINK_ADD_MEMBERSHIP = 1
|
||||
|
||||
HWSIM_CMD_REGISTER = 1
|
||||
HWSIM_CMD_FRAME = 2
|
||||
HWSIM_CMD_TX_INFO_FRAME = 3
|
||||
|
||||
HWSIM_ATTR_ADDR_RECEIVER = 1
|
||||
HWSIM_ATTR_ADDR_TRANSMITTER = 2
|
||||
HWSIM_ATTR_FRAME = 3
|
||||
HWSIM_ATTR_FLAGS = 4
|
||||
HWSIM_ATTR_RX_RATE = 5
|
||||
HWSIM_ATTR_SIGNAL = 6
|
||||
HWSIM_ATTR_TX_INFO = 7
|
||||
HWSIM_ATTR_COOKIE = 8
|
||||
HWSIM_ATTR_FREQ = 19
|
||||
HWSIM_ATTR_TX_INFO_FLAGS = 21
|
||||
|
||||
HWSIM_TX_STAT_ACK = 1 << 2
|
||||
|
||||
# Signal/rate reported for injected (received) frames. A strong, fixed signal
|
||||
# keeps mac80211 happy; the exact value is not significant for a virtual cell.
|
||||
RX_SIGNAL = -30
|
||||
RX_RATE = 0
|
||||
|
||||
# The medium is carried as raw Ethernet frames with a custom EtherType on the
|
||||
# configured interface -- no IP addressing needed (the carrier link has none).
|
||||
# A broadcast destination means every DUT on the segment is "in range".
|
||||
ETH_P_WIFIMEDIUM = 0x88B5 # local-experimental EtherType
|
||||
ETH_BROADCAST = b"\xff\xff\xff\xff\xff\xff"
|
||||
ETYPE = struct.pack("!H", ETH_P_WIFIMEDIUM)
|
||||
SOL_PACKET = 263
|
||||
PACKET_IGNORE_OUTGOING = 23 # don't deliver our own TX back to the socket
|
||||
|
||||
NLMSG_HDR = struct.Struct("=IHHII") # len, type, flags, seq, pid
|
||||
GENL_HDR = struct.Struct("=BBH") # cmd, version, reserved
|
||||
|
||||
|
||||
DEBUG = bool(os.environ.get("WIFIMEDIUM_DEBUG"))
|
||||
|
||||
|
||||
def log(msg):
|
||||
sys.stderr.write(f"wifimedium: {msg}\n")
|
||||
sys.stderr.flush()
|
||||
|
||||
|
||||
def dbg(msg):
|
||||
if DEBUG:
|
||||
log(msg)
|
||||
|
||||
|
||||
def nla_align(n):
|
||||
return (n + 3) & ~3
|
||||
|
||||
|
||||
def put_attr(atype, payload):
|
||||
hdr = struct.pack("=HH", len(payload) + 4, atype)
|
||||
return hdr + payload + b"\0" * (nla_align(len(payload) + 4) - (len(payload) + 4))
|
||||
|
||||
|
||||
def parse_attrs(buf):
|
||||
attrs = {}
|
||||
off = 0
|
||||
while off + 4 <= len(buf):
|
||||
alen, atype = struct.unpack_from("=HH", buf, off)
|
||||
if alen < 4:
|
||||
break
|
||||
attrs[atype & 0x7fff] = buf[off + 4:off + alen]
|
||||
off += nla_align(alen)
|
||||
return attrs
|
||||
|
||||
|
||||
class Netlink:
|
||||
"""Minimal generic-netlink client for the hwsim family."""
|
||||
|
||||
def __init__(self):
|
||||
self.sock = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, NETLINK_GENERIC)
|
||||
self.sock.bind((0, 0))
|
||||
self.seq = 0
|
||||
# NB: the genl family is "MAC80211_HWSIM" (uppercase); the lowercase
|
||||
# "mac80211_hwsim" is the platform driver name, not the family.
|
||||
self.family_id, self.mcast_id = self._resolve("MAC80211_HWSIM", "config")
|
||||
|
||||
def fileno(self):
|
||||
return self.sock.fileno()
|
||||
|
||||
def _send(self, family_id, cmd, attrs=b"", flags=0):
|
||||
self.seq += 1
|
||||
payload = GENL_HDR.pack(cmd, 1, 0) + attrs
|
||||
msg = NLMSG_HDR.pack(NLMSG_HDR.size + len(payload), family_id,
|
||||
flags | 1, self.seq, 0) + payload # NLM_F_REQUEST
|
||||
self.sock.send(msg)
|
||||
|
||||
def _resolve(self, name, grpname):
|
||||
self._send(GENL_ID_CTRL, CTRL_CMD_GETFAMILY,
|
||||
put_attr(CTRL_ATTR_FAMILY_NAME, name.encode() + b"\0"))
|
||||
data = self.sock.recv(65536)
|
||||
_, mtype, _, _, _ = NLMSG_HDR.unpack_from(data, 0)
|
||||
if mtype == 2: # NLMSG_ERROR (also used for ACKs; err<0 is a failure)
|
||||
err = struct.unpack_from("=i", data, NLMSG_HDR.size)[0]
|
||||
raise RuntimeError(f"genl GETFAMILY({name}) failed: "
|
||||
f"{os.strerror(-err) if err else 'empty reply'}")
|
||||
attrs = parse_attrs(data[NLMSG_HDR.size + GENL_HDR.size:])
|
||||
family_id = struct.unpack("=H", attrs[CTRL_ATTR_FAMILY_ID][:2])[0]
|
||||
|
||||
mcast_id = None
|
||||
for _, grp in _parse_nested(attrs.get(CTRL_ATTR_MCAST_GROUPS, b"")):
|
||||
gattrs = parse_attrs(grp)
|
||||
gname = gattrs.get(CTRL_ATTR_MCAST_GRP_NAME, b"").rstrip(b"\0").decode()
|
||||
if gname == grpname:
|
||||
mcast_id = struct.unpack("=I", gattrs[CTRL_ATTR_MCAST_GRP_ID])[0]
|
||||
if mcast_id is None:
|
||||
raise RuntimeError(f"hwsim multicast group '{grpname}' not found")
|
||||
return family_id, mcast_id
|
||||
|
||||
def register(self):
|
||||
self.sock.setsockopt(SOL_NETLINK, NETLINK_ADD_MEMBERSHIP, self.mcast_id)
|
||||
self._send(self.family_id, HWSIM_CMD_REGISTER)
|
||||
|
||||
def recv(self):
|
||||
return self.sock.recv(65536)
|
||||
|
||||
def inject(self, receiver, frame, freq):
|
||||
attrs = put_attr(HWSIM_ATTR_ADDR_RECEIVER, receiver)
|
||||
attrs += put_attr(HWSIM_ATTR_FRAME, frame)
|
||||
attrs += put_attr(HWSIM_ATTR_RX_RATE, struct.pack("=I", RX_RATE))
|
||||
attrs += put_attr(HWSIM_ATTR_SIGNAL, struct.pack("=i", RX_SIGNAL))
|
||||
if freq:
|
||||
attrs += put_attr(HWSIM_ATTR_FREQ, struct.pack("=I", freq))
|
||||
self._send(self.family_id, HWSIM_CMD_FRAME, attrs)
|
||||
|
||||
def tx_ack(self, transmitter, flags, tx_info, tx_info_flags, cookie):
|
||||
attrs = put_attr(HWSIM_ATTR_ADDR_TRANSMITTER, transmitter)
|
||||
attrs += put_attr(HWSIM_ATTR_FLAGS, struct.pack("=I", flags | HWSIM_TX_STAT_ACK))
|
||||
attrs += put_attr(HWSIM_ATTR_SIGNAL, struct.pack("=i", RX_SIGNAL))
|
||||
if tx_info:
|
||||
attrs += put_attr(HWSIM_ATTR_TX_INFO, tx_info)
|
||||
if tx_info_flags:
|
||||
attrs += put_attr(HWSIM_ATTR_TX_INFO_FLAGS, tx_info_flags)
|
||||
if cookie:
|
||||
attrs += put_attr(HWSIM_ATTR_COOKIE, cookie)
|
||||
self._send(self.family_id, HWSIM_CMD_TX_INFO_FRAME, attrs)
|
||||
|
||||
|
||||
def _parse_nested(buf):
|
||||
off = 0
|
||||
while off + 4 <= len(buf):
|
||||
alen, atype = struct.unpack_from("=HH", buf, off)
|
||||
if alen < 4:
|
||||
break
|
||||
yield atype & 0x7fff, buf[off + 4:off + alen]
|
||||
off += nla_align(alen)
|
||||
|
||||
|
||||
def radio_addr1(phy):
|
||||
"""The address mac80211_hwsim identifies a radio by on the medium.
|
||||
|
||||
A hwsim radio has three addresses (kernel new_radio()). sysfs 'macaddress'
|
||||
is addresses[0]; the address the kernel stamps as HWSIM_ATTR_ADDR_TRANSMITTER
|
||||
and matches HWSIM_ATTR_ADDR_RECEIVER against (its rhashtable key) is
|
||||
addresses[1] -- addresses[0] with bit 0x40 set in the first octet, for the
|
||||
default no-perm_addr radios we use. So both the transmitter we observe and
|
||||
the receiver we must inject to are this 0x40 variant, not the sysfs value.
|
||||
(Note custom-phys-address changes the vif/netdev MAC, not these.)
|
||||
"""
|
||||
with open(f"/sys/class/ieee80211/{phy}/macaddress") as fh:
|
||||
addr = bytearray.fromhex(fh.read().strip().replace(":", ""))
|
||||
addr[0] |= 0x40
|
||||
return bytes(addr)
|
||||
|
||||
|
||||
def open_medium(ifname):
|
||||
"""Raw-Ethernet socket on the medium interface, or (None, None).
|
||||
|
||||
Returns (sock, mac); mac is the interface's own (unique) address, used as
|
||||
the Ethernet source and to drop frames we transmitted ourselves -- the
|
||||
multicast cell loops our own frames back to us."""
|
||||
if not ifname:
|
||||
return None, None
|
||||
# The carrier NIC is unconfigured (it is not a real DUT port), so Infix
|
||||
# leaves it administratively down -- a raw packet socket on a down link
|
||||
# carries no frames. Bring it up; wifimedium owns the medium, like hwsim0.
|
||||
ifup(ifname)
|
||||
sock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW,
|
||||
socket.htons(ETH_P_WIFIMEDIUM))
|
||||
sock.bind((ifname, ETH_P_WIFIMEDIUM))
|
||||
# Don't receive our own transmitted frames (avoids a self-feedback path
|
||||
# and any reliance on the source MAC, which can collide between guests).
|
||||
try:
|
||||
sock.setsockopt(SOL_PACKET, PACKET_IGNORE_OUTGOING, 1)
|
||||
except OSError:
|
||||
pass # pre-4.20 kernels; the length check below still applies
|
||||
with open(f"/sys/class/net/{ifname}/address") as fh:
|
||||
mac = bytes.fromhex(fh.read().strip().replace(":", ""))
|
||||
return sock, mac
|
||||
|
||||
|
||||
# Wire format on the medium: 6-byte transmitter MAC, 4-byte LE freq, then the
|
||||
# raw 802.11 frame.
|
||||
WIRE = struct.Struct("=6sI")
|
||||
|
||||
|
||||
def wait_radios_renamed(timeout=20):
|
||||
"""Wait until the hwsim phys have been renamed phyN -> radioN.
|
||||
|
||||
60-rename-wifi-phy.rules renames each phy *asynchronously* (a udev RUN),
|
||||
and the carrier NICs are already named radioN (mactab/nameif, early boot).
|
||||
We pair phy and carrier by name, so discovering while a phy is still 'phyN'
|
||||
would miss it -- and we discover only once. This raced our start (gated
|
||||
merely on the module load) and left some boots relaying nothing until a
|
||||
restart. Block until every phy is renamed (no 'phyN' left) and the set has
|
||||
settled, so discovery sees radioN phys that match the radioN carriers.
|
||||
"""
|
||||
base = "/sys/class/ieee80211"
|
||||
prev, stable = None, 0
|
||||
for _ in range(timeout * 4):
|
||||
phys = sorted(os.listdir(base)) if os.path.isdir(base) else []
|
||||
if phys and not any(p.startswith("phy") for p in phys):
|
||||
if phys == prev:
|
||||
stable += 1
|
||||
if stable >= 2: # unchanged for ~0.5s
|
||||
return
|
||||
else:
|
||||
stable = 0
|
||||
prev = phys
|
||||
time.sleep(0.25)
|
||||
|
||||
|
||||
def discover_radios():
|
||||
"""Pair each local hwsim radio with its multicast carrier interface.
|
||||
|
||||
A radio's phy is named radioN (60-rename-wifi-phy.rules) under
|
||||
/sys/class/ieee80211/. In the qeneth test topology each radio also has a
|
||||
carrier NIC of the SAME name under /sys/class/net/ -- the topology names the
|
||||
port radioN, so the mactab/nameif rename the carrier to match -- joined to
|
||||
one multicast cell shared by every DUT's radioN. Pairing them by name gives
|
||||
a per-radio relay: a radio's frames go onto its own carrier (its cell) and a
|
||||
carrier's frames are injected only into its own radio.
|
||||
|
||||
Returns a list of {name, addr1, sock, mac}. A phy with no matching carrier
|
||||
netdev (e.g. real hardware) is skipped, so the daemon simply idles there.
|
||||
"""
|
||||
wait_radios_renamed()
|
||||
radios = []
|
||||
base = "/sys/class/ieee80211"
|
||||
if not os.path.isdir(base):
|
||||
return radios
|
||||
for phy in sorted(os.listdir(base)):
|
||||
if not os.path.isdir(f"/sys/class/net/{phy}"):
|
||||
continue # no carrier of this name -> not a relayed radio
|
||||
sock, mac = open_medium(phy)
|
||||
if sock:
|
||||
radios.append({"name": phy, "addr1": radio_addr1(phy),
|
||||
"sock": sock, "mac": mac})
|
||||
return radios
|
||||
|
||||
|
||||
def ifup(ifname):
|
||||
"""Bring an interface up (best effort)."""
|
||||
subprocess.run(["ip", "link", "set", ifname, "up"], check=False)
|
||||
|
||||
|
||||
def main():
|
||||
# The hwsim monitor interface is down after boot; bring it up so the
|
||||
# simulated medium is observable (tcpdump -i hwsim0) and active.
|
||||
ifup("hwsim0")
|
||||
|
||||
# Discover carriers first. With none -- e.g. 'make run', which creates
|
||||
# radios but no inter-guest medium -- registering on the hwsim family would
|
||||
# make the kernel hand us every frame with nowhere to relay it, breaking
|
||||
# delivery between this guest's own radios. So leave the medium to the
|
||||
# kernel and idle instead of registering.
|
||||
radios = discover_radios()
|
||||
if not radios:
|
||||
log("no radio carriers found; leaving local hwsim medium to the kernel")
|
||||
while True:
|
||||
time.sleep(3600)
|
||||
|
||||
nl = Netlink()
|
||||
nl.register()
|
||||
log(f"registered on hwsim family {nl.family_id}; "
|
||||
f"radios={' '.join(r['name'] for r in radios)}")
|
||||
|
||||
# TX routing: a frame's transmitter (addresses[1]) -> that radio's carrier.
|
||||
by_addr1 = {r["addr1"]: r for r in radios}
|
||||
dbg(f"netlink portid={nl.sock.getsockname()[0]}; "
|
||||
+ "; ".join(f"{r['name']} addr1={r['addr1'].hex()} "
|
||||
f"carrier={r['mac'].hex()}" for r in radios))
|
||||
|
||||
socks = [nl] + [r["sock"] for r in radios]
|
||||
while True:
|
||||
ready, _, _ = select.select(socks, [], [])
|
||||
|
||||
if nl in ready:
|
||||
data = nl.recv()
|
||||
off = 0
|
||||
while off + NLMSG_HDR.size <= len(data):
|
||||
mlen, mtype, _, _, _ = NLMSG_HDR.unpack_from(data, off)
|
||||
if mlen < NLMSG_HDR.size:
|
||||
break
|
||||
if mtype == nl.family_id and \
|
||||
data[off + NLMSG_HDR.size] == HWSIM_CMD_FRAME:
|
||||
a = parse_attrs(data[off + NLMSG_HDR.size + GENL_HDR.size:
|
||||
off + mlen])
|
||||
tx = a.get(HWSIM_ATTR_ADDR_TRANSMITTER)
|
||||
frame = a.get(HWSIM_ATTR_FRAME)
|
||||
if tx and frame:
|
||||
freq = struct.unpack("=I", a[HWSIM_ATTR_FREQ])[0] \
|
||||
if HWSIM_ATTR_FREQ in a else 0
|
||||
flags = struct.unpack("=I", a[HWSIM_ATTR_FLAGS])[0] \
|
||||
if HWSIM_ATTR_FLAGS in a else 0
|
||||
# Complete the kernel TX path regardless of delivery.
|
||||
nl.tx_ack(tx, flags, a.get(HWSIM_ATTR_TX_INFO),
|
||||
a.get(HWSIM_ATTR_TX_INFO_FLAGS),
|
||||
a.get(HWSIM_ATTR_COOKIE))
|
||||
# Send only onto the transmitting radio's own carrier.
|
||||
r = by_addr1.get(tx)
|
||||
if r:
|
||||
r["sock"].send(ETH_BROADCAST + r["mac"] + ETYPE +
|
||||
WIRE.pack(tx, freq) + frame)
|
||||
dbg(f"tx {r['name']} freq={freq} len={len(frame)}")
|
||||
else:
|
||||
dbg(f"tx from unknown radio {tx.hex()} -- dropped")
|
||||
off += nla_align(mlen)
|
||||
|
||||
for r in radios:
|
||||
if r["sock"] not in ready:
|
||||
continue
|
||||
pkt = r["sock"].recv(65536)
|
||||
if len(pkt) < 14 + WIRE.size:
|
||||
continue
|
||||
# The cell multicasts our own frames back to us; PACKET_IGNORE_-
|
||||
# OUTGOING does not catch that looped copy, so drop anything whose
|
||||
# Ethernet source is our own carrier (carrier MACs are unique per
|
||||
# DUT; transmitter addrs are not, so we must key on the carrier).
|
||||
if pkt[6:12] == r["mac"]:
|
||||
continue
|
||||
tx, freq = WIRE.unpack_from(pkt, 14)
|
||||
frame = pkt[14 + WIRE.size:]
|
||||
# Inject into THIS radio only -- its carrier is its cell.
|
||||
nl.inject(r["addr1"], frame, freq)
|
||||
dbg(f"rx {r['name']} tx={tx.hex()} freq={freq} len={len(frame)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
@@ -0,0 +1,14 @@
|
||||
# Virtual WiFi medium for mac80211_hwsim (QEMU test target).
|
||||
#
|
||||
# Installed only when BR2_PACKAGE_FEATURE_WIFI_HWSIM is set. Bridges hwsim
|
||||
# frames between the local radios and -- when a medium interface is configured
|
||||
# (WIFIMEDIUM_IFACE env or 'wifimedium.iface=' on the kernel cmdline) -- to
|
||||
# peer DUTs over that Ethernet segment, so two QEMU guests can associate
|
||||
# "wirelessly". hwsim is loaded by the 00-hwsim ixinit script, which sets the
|
||||
# <usr/hwsim> condition once radios exist and are renamed -- so this service
|
||||
# starts only on nodes that actually have radios (registering on the hwsim genl
|
||||
# family would fail otherwise). On real hardware neither is shipped and the
|
||||
# radios use the air.
|
||||
service env:-/etc/default/wifimedium name:wifimedium \
|
||||
[2345] <usr/hwsim> \
|
||||
/usr/libexec/infix/wifimedium -- Virtual WiFi medium (hwsim)
|
||||
@@ -571,14 +571,6 @@ static int change_cb(sr_session_ctx_t *session, uint32_t sub_id, const char *mod
|
||||
}
|
||||
max_dep--;
|
||||
}
|
||||
#if 0
|
||||
/* Debug: print diff to file */
|
||||
FILE *f = fopen("/tmp/confd-diff.json", "w");
|
||||
if (f) {
|
||||
lyd_print_file(f, diff, LYD_JSON, LYD_PRINT_SIBLINGS);
|
||||
fclose(f);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ietf-hardware */
|
||||
@@ -728,8 +720,11 @@ int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv)
|
||||
goto err;
|
||||
|
||||
confd.root = json_load_file("/run/system.json", 0, NULL);
|
||||
if (!confd.root)
|
||||
if (!confd.root) {
|
||||
ERROR("failed loading /run/system.json, system probe incomplete");
|
||||
rc = SR_ERR_SYS;
|
||||
goto err;
|
||||
}
|
||||
|
||||
/* An optional file that contains hardware specific quirks for
|
||||
* the network interfaces on running board.
|
||||
|
||||
@@ -52,3 +52,6 @@
|
||||
|
||||
- name: Layer-2/3 Tunnel Tests
|
||||
suite: tunnels.yaml
|
||||
|
||||
- name: WiFi Tests
|
||||
suite: wifi.yaml
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
- name: WiFi AP and Station Association across two DUTs
|
||||
case: wifi_ap_station_2dut/test.py
|
||||
|
||||
- name: WiFi Access Point serving three Stations
|
||||
case: wifi_ap_multi_station/test.py
|
||||
|
||||
- name: WiFi Mesh backhaul with roaming Access Points
|
||||
case: wifi_mesh_roaming/test.py
|
||||
|
||||
- name: WiFi Band Steering across a dual-band Access Point
|
||||
case: wifi_band_steering/test.py
|
||||
@@ -0,0 +1 @@
|
||||
test.adoc
|
||||
@@ -0,0 +1,46 @@
|
||||
=== WiFi Access Point serving three Stations
|
||||
|
||||
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/wifi_ap_multi_station]
|
||||
|
||||
==== Description
|
||||
|
||||
One AP and three Stations, each on its own DUT: the ap runs a
|
||||
WPA2/WPA3-personal Access Point on radio0, and station1, station2 and
|
||||
station3 each associate to it. The radios are in separate kernels, so the
|
||||
"air" between them is realised differently per environment:
|
||||
|
||||
* On real hardware the links are real RF -- the stations just associate.
|
||||
* On QEMU the radios are mac80211_hwsim, and the `wifimedium` relay on each
|
||||
DUT bridges hwsim frames over the Ethernet segments between the guests
|
||||
(the topology's wifi-links). See doc/wifi.md.
|
||||
|
||||
This is the multi-client case: a single AP cell must serve more than one
|
||||
station at once. Each station associating with WPA2/WPA3-personal is a
|
||||
strong end-to-end check -- authentication, association and the WPA 4-way
|
||||
handshake all require frames to cross the medium in *both* directions -- so
|
||||
three successful associations prove the AP keeps several stations
|
||||
associated simultaneously over the (relayed) radio links. It then confirms
|
||||
data-plane reach: the ap serves DHCP and each station leases an address
|
||||
from the pool over the radio link.
|
||||
|
||||
Topology:
|
||||
....
|
||||
((( station1 ==(mgmt)== host
|
||||
host ==(mgmt)== ap ((( station2 ==(mgmt)== host
|
||||
((( station3 ==(mgmt)== host
|
||||
....
|
||||
|
||||
==== Topology
|
||||
|
||||
image::topology.svg[WiFi Access Point serving three Stations topology, align=center, scaledwidth=75%]
|
||||
|
||||
==== Sequence
|
||||
|
||||
. Set up topology and attach to the ap and three stations
|
||||
. Configure the ap as an Access Point on radio0
|
||||
. Configure the station on radio0
|
||||
. Verify the station associates to the ap over the wifi link
|
||||
. Verify the station's wifi0 operational status is up
|
||||
. Verify the station leases an address from the ap over wifi
|
||||
|
||||
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env python3
|
||||
r"""
|
||||
WiFi Access Point serving multiple Stations
|
||||
|
||||
One AP and three Stations, each on its own DUT: the ap runs a
|
||||
WPA2/WPA3-personal Access Point on radio0, and station1, station2 and
|
||||
station3 each associate to it. The radios are in separate kernels, so the
|
||||
"air" between them is realised differently per environment:
|
||||
|
||||
* On real hardware the links are real RF -- the stations just associate.
|
||||
* On QEMU the radios are mac80211_hwsim, and the `wifimedium` relay on each
|
||||
DUT bridges hwsim frames over the Ethernet segments between the guests
|
||||
(the topology's wifi-links). See doc/wifi.md.
|
||||
|
||||
This is the multi-client case: a single AP cell must serve more than one
|
||||
station at once. Each station associating with WPA2/WPA3-personal is a
|
||||
strong end-to-end check -- authentication, association and the WPA 4-way
|
||||
handshake all require frames to cross the medium in *both* directions -- so
|
||||
three successful associations prove the AP keeps several stations
|
||||
associated simultaneously over the (relayed) radio links. It then confirms
|
||||
data-plane reach: the ap serves DHCP and each station leases an address
|
||||
from the pool over the radio link.
|
||||
|
||||
Topology:
|
||||
....
|
||||
((( station1 ==(mgmt)== host
|
||||
host ==(mgmt)== ap ((( station2 ==(mgmt)== host
|
||||
((( station3 ==(mgmt)== host
|
||||
....
|
||||
"""
|
||||
import base64
|
||||
|
||||
import infamy
|
||||
import infamy.iface as iface
|
||||
from infamy.util import until, parallel
|
||||
|
||||
SSID = "infix-test"
|
||||
PSK = "infixinfix" # 8-63 printable ASCII, see doc/wifi.md
|
||||
PSK_B64 = base64.b64encode(PSK.encode()).decode()
|
||||
COUNTRY = "SE"
|
||||
|
||||
# hwsim defaults every radio0 to 02:00:00:00:00:00, so the AP and all the
|
||||
# stations would otherwise share one MAC -- give each DUT a unique address.
|
||||
STATIONS = [
|
||||
("station1", "02:00:00:00:00:02"),
|
||||
("station2", "02:00:00:00:00:03"),
|
||||
("station3", "02:00:00:00:00:04"),
|
||||
]
|
||||
AP_MAC = "02:00:00:00:00:01"
|
||||
|
||||
# The ap runs a DHCP server on wifi0; every station leases an address from
|
||||
# the pool. A completed lease is real bidirectional IP traffic over the
|
||||
# radio link, so a lease per station proves all three reach the ap at L3.
|
||||
SUBNET = "192.168.20.0/24"
|
||||
AP_IP = "192.168.20.1"
|
||||
POOL_START = "192.168.20.100"
|
||||
POOL_END = "192.168.20.102" # three addresses, one per station
|
||||
|
||||
|
||||
def radio(name, band=None, channel=None):
|
||||
"""ietf-hardware component for a radio."""
|
||||
wifi_radio = {"country-code": COUNTRY}
|
||||
if band:
|
||||
wifi_radio["band"] = band
|
||||
if channel is not None:
|
||||
wifi_radio["channel"] = channel
|
||||
return {
|
||||
"name": name,
|
||||
"class": "infix-hardware:wifi",
|
||||
"infix-hardware:wifi-radio": wifi_radio,
|
||||
}
|
||||
|
||||
|
||||
def keystore():
|
||||
return {
|
||||
"keystore": {
|
||||
"symmetric-keys": {
|
||||
"symmetric-key": [{
|
||||
"name": "wifi",
|
||||
"key-format": "infix-crypto-types:passphrase-key-format",
|
||||
"cleartext-symmetric-key": PSK_B64,
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def wifi_iface(mac, wifi, ipv4=None):
|
||||
ifc = {
|
||||
"name": "wifi0",
|
||||
"type": "infix-if-type:wifi",
|
||||
"infix-interfaces:custom-phys-address": {"static": mac},
|
||||
"infix-interfaces:wifi": wifi,
|
||||
}
|
||||
if ipv4:
|
||||
ifc["ietf-ip:ipv4"] = ipv4
|
||||
return {"interfaces": {"interface": [ifc]}}
|
||||
|
||||
|
||||
def leased(dut):
|
||||
"""True once wifi0 holds an address handed out by the ap's DHCP server."""
|
||||
for addr in iface.get_ipv4_address(dut, "wifi0") or []:
|
||||
if addr.get("origin") == "dhcp" and \
|
||||
POOL_START <= addr.get("ip", "") <= POOL_END:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
with infamy.Test() as test:
|
||||
with test.step("Set up topology and attach to the ap and three stations"):
|
||||
env = infamy.Env()
|
||||
# Connect to the AP and all stations concurrently to cut setup time.
|
||||
ap, *station_duts = parallel(
|
||||
lambda: env.attach("ap", "mgmt"),
|
||||
*((lambda n=name: env.attach(n, "mgmt")) for name, _ in STATIONS),
|
||||
)
|
||||
stations = [(name, mac, dut)
|
||||
for (name, mac), dut in zip(STATIONS, station_duts)]
|
||||
|
||||
for dut in [ap] + [s for _, _, s in stations]:
|
||||
if not dut.has_feature("infix-interfaces", "wifi"):
|
||||
print("DUT does not advertise the 'wifi' feature -- skipping")
|
||||
test.skip()
|
||||
|
||||
with test.step("Configure the ap as an Access Point on radio0"):
|
||||
ap.put_config_dicts({
|
||||
"ietf-hardware": {"hardware": {"component": [
|
||||
radio("radio0", band="2.4GHz", channel=1)]}},
|
||||
"ietf-keystore": keystore(),
|
||||
"ietf-interfaces": wifi_iface(AP_MAC, {
|
||||
"radio": "radio0",
|
||||
"access-point": {
|
||||
"ssid": SSID,
|
||||
"security": {"mode": "wpa2-wpa3-personal", "secret": "wifi"},
|
||||
},
|
||||
}, ipv4={"address": [{"ip": AP_IP, "prefix-length": 24}]}),
|
||||
"infix-dhcp-server": {"dhcp-server": {"subnet": [{
|
||||
"subnet": SUBNET,
|
||||
"pool": {"start-address": POOL_START, "end-address": POOL_END},
|
||||
}]}},
|
||||
})
|
||||
|
||||
for name, mac, dut in stations:
|
||||
with test.step("Configure the station on radio0"):
|
||||
print(f"Configuring {name}")
|
||||
dut.put_config_dicts({
|
||||
"ietf-hardware": {"hardware": {"component": [radio("radio0")]}},
|
||||
"ietf-keystore": keystore(),
|
||||
"ietf-interfaces": wifi_iface(mac, {
|
||||
"radio": "radio0",
|
||||
"station": {
|
||||
"ssid": SSID,
|
||||
"security": {"mode": "auto", "secret": "wifi"},
|
||||
},
|
||||
}, ipv4={"infix-dhcp-client:dhcp": {}}),
|
||||
})
|
||||
|
||||
for name, _mac, dut in stations:
|
||||
with test.step("Verify the station associates to the ap over the wifi link"):
|
||||
print(f"Verifying {name}")
|
||||
def associated(dut=dut):
|
||||
ifc = dut.get_iface("wifi0")
|
||||
if not ifc:
|
||||
return False
|
||||
wifi = ifc.get("infix-interfaces:wifi") or ifc.get("wifi") or {}
|
||||
station = wifi.get("station", {})
|
||||
return station.get("ssid") == SSID and \
|
||||
station.get("signal-strength") is not None
|
||||
until(associated, attempts=60, interval=2)
|
||||
|
||||
for name, _mac, dut in stations:
|
||||
with test.step("Verify the station's wifi0 operational status is up"):
|
||||
print(f"Verifying {name}")
|
||||
until(lambda dut=dut: iface.is_oper_up(dut, "wifi0"), attempts=30)
|
||||
|
||||
for name, _mac, dut in stations:
|
||||
with test.step("Verify the station leases an address from the ap over wifi"):
|
||||
print(f"Verifying {name}")
|
||||
until(lambda dut=dut: leased(dut), attempts=60, interval=2)
|
||||
|
||||
test.succeed()
|
||||
@@ -0,0 +1,61 @@
|
||||
graph "wifi-ap-multi-station" {
|
||||
layout="neato";
|
||||
overlap="false";
|
||||
esep="+40";
|
||||
|
||||
node [shape=record, fontname="DejaVu Sans Mono, Book"];
|
||||
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
|
||||
|
||||
host [
|
||||
label="host | { <mgmt1> mgmt1 | <mgmt2> mgmt2 | <mgmt3> mgmt3 | <mgmt4> mgmt4 }",
|
||||
pos="0,0!",
|
||||
requires="controller",
|
||||
];
|
||||
|
||||
ap [
|
||||
label="{ <mgmt> mgmt | <wifi> wifi } | ap",
|
||||
pos="6,0!",
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
station1 [
|
||||
label="{ <mgmt> mgmt | <wifi> wifi } | station1",
|
||||
pos="11,3!",
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
station2 [
|
||||
label="{ <mgmt> mgmt | <wifi> wifi } | station2",
|
||||
pos="11,0!",
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
station3 [
|
||||
label="{ <mgmt> mgmt | <wifi> wifi } | station3",
|
||||
pos="11,-3!",
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
// One AP cell shared by the ap and all three stations: a single medium
|
||||
// node every radio joins (same index -> same cell). The ap's one radio
|
||||
// must be in range of every station, which the mapper guarantees by
|
||||
// placing all four on this shared medium:
|
||||
// * physical: maps onto one over-the-air RF cell;
|
||||
// * virtual (qeneth): maps onto one multicast cell the wifimedium
|
||||
// relay bridges mac80211_hwsim frames over.
|
||||
cell [
|
||||
label="cell",
|
||||
pos="8.5,0!",
|
||||
requires="wifi-radio",
|
||||
];
|
||||
|
||||
host:mgmt1 -- ap:mgmt [requires="mgmt", color="lightgray"]
|
||||
host:mgmt2 -- station1:mgmt [requires="mgmt", color="lightgray"]
|
||||
host:mgmt3 -- station2:mgmt [requires="mgmt", color="lightgray"]
|
||||
host:mgmt4 -- station3:mgmt [requires="mgmt", color="lightgray"]
|
||||
|
||||
ap:wifi -- cell [requires="wifi2.4GHz", style="dashed"]
|
||||
station1:wifi -- cell [requires="wifi2.4GHz", style="dashed"]
|
||||
station2:wifi -- cell [requires="wifi2.4GHz", style="dashed"]
|
||||
station3:wifi -- cell [requires="wifi2.4GHz", style="dashed"]
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- Title: wifi-ap-multi-station Pages: 1 -->
|
||||
<svg width="980pt" height="519pt"
|
||||
viewBox="0.00 0.00 979.99 519.45" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 515.45)">
|
||||
<title>wifi-ap-multi-station</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-515.45 975.99,-515.45 975.99,4 -4,4"/>
|
||||
<!-- host -->
|
||||
<g id="node1" class="node">
|
||||
<title>host</title>
|
||||
<polygon fill="none" stroke="black" points="0,-209.72 0,-301.72 108,-301.72 108,-209.72 0,-209.72"/>
|
||||
<text text-anchor="middle" x="25" y="-252.02" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
|
||||
<polyline fill="none" stroke="black" points="50,-209.72 50,-301.72 "/>
|
||||
<text text-anchor="middle" x="79" y="-286.52" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt1</text>
|
||||
<polyline fill="none" stroke="black" points="50,-278.72 108,-278.72 "/>
|
||||
<text text-anchor="middle" x="79" y="-263.52" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt2</text>
|
||||
<polyline fill="none" stroke="black" points="50,-255.72 108,-255.72 "/>
|
||||
<text text-anchor="middle" x="79" y="-240.52" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt3</text>
|
||||
<polyline fill="none" stroke="black" points="50,-232.72 108,-232.72 "/>
|
||||
<text text-anchor="middle" x="79" y="-217.52" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt4</text>
|
||||
</g>
|
||||
<!-- ap -->
|
||||
<g id="node2" class="node">
|
||||
<title>ap</title>
|
||||
<polygon fill="none" stroke="black" points="476.95,-232.72 476.95,-278.72 559.95,-278.72 559.95,-232.72 476.95,-232.72"/>
|
||||
<text text-anchor="middle" x="501.95" y="-263.52" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="476.95,-255.72 526.95,-255.72 "/>
|
||||
<text text-anchor="middle" x="501.95" y="-240.52" font-family="DejaVu Sans Mono, Book" font-size="14.00">wifi</text>
|
||||
<polyline fill="none" stroke="black" points="526.95,-232.72 526.95,-278.72 "/>
|
||||
<text text-anchor="middle" x="543.45" y="-252.02" font-family="DejaVu Sans Mono, Book" font-size="14.00">ap</text>
|
||||
</g>
|
||||
<!-- host--ap -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:mgmt1--ap:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M108,-290.72C108,-290.72 476.45,-267.72 476.45,-267.72"/>
|
||||
</g>
|
||||
<!-- station1 -->
|
||||
<g id="node3" class="node">
|
||||
<title>station1</title>
|
||||
<polygon fill="none" stroke="black" points="838.99,-464.95 838.99,-510.95 971.99,-510.95 971.99,-464.95 838.99,-464.95"/>
|
||||
<text text-anchor="middle" x="863.99" y="-495.75" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="838.99,-487.95 888.99,-487.95 "/>
|
||||
<text text-anchor="middle" x="863.99" y="-472.75" font-family="DejaVu Sans Mono, Book" font-size="14.00">wifi</text>
|
||||
<polyline fill="none" stroke="black" points="888.99,-464.95 888.99,-510.95 "/>
|
||||
<text text-anchor="middle" x="930.49" y="-484.25" font-family="DejaVu Sans Mono, Book" font-size="14.00">station1</text>
|
||||
</g>
|
||||
<!-- host--station1 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>host:mgmt2--station1:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M108,-267.72C108,-267.72 838.49,-499.95 838.49,-499.95"/>
|
||||
</g>
|
||||
<!-- station2 -->
|
||||
<g id="node4" class="node">
|
||||
<title>station2</title>
|
||||
<polygon fill="none" stroke="black" points="838.99,-232.72 838.99,-278.72 971.99,-278.72 971.99,-232.72 838.99,-232.72"/>
|
||||
<text text-anchor="middle" x="863.99" y="-263.52" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="838.99,-255.72 888.99,-255.72 "/>
|
||||
<text text-anchor="middle" x="863.99" y="-240.52" font-family="DejaVu Sans Mono, Book" font-size="14.00">wifi</text>
|
||||
<polyline fill="none" stroke="black" points="888.99,-232.72 888.99,-278.72 "/>
|
||||
<text text-anchor="middle" x="930.49" y="-252.02" font-family="DejaVu Sans Mono, Book" font-size="14.00">station2</text>
|
||||
</g>
|
||||
<!-- host--station2 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>host:mgmt3--station2:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M108,-243.72C108,-243.72 838.49,-267.72 838.49,-267.72"/>
|
||||
</g>
|
||||
<!-- station3 -->
|
||||
<g id="node5" class="node">
|
||||
<title>station3</title>
|
||||
<polygon fill="none" stroke="black" points="838.99,-0.5 838.99,-46.5 971.99,-46.5 971.99,-0.5 838.99,-0.5"/>
|
||||
<text text-anchor="middle" x="863.99" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="838.99,-23.5 888.99,-23.5 "/>
|
||||
<text text-anchor="middle" x="863.99" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">wifi</text>
|
||||
<polyline fill="none" stroke="black" points="888.99,-0.5 888.99,-46.5 "/>
|
||||
<text text-anchor="middle" x="930.49" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">station3</text>
|
||||
</g>
|
||||
<!-- host--station3 -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>host:mgmt4--station3:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M108,-220.72C108,-220.72 838.49,-35.5 838.49,-35.5"/>
|
||||
</g>
|
||||
<!-- cell -->
|
||||
<g id="node6" class="node">
|
||||
<title>cell</title>
|
||||
<polygon fill="none" stroke="black" points="684.97,-237.72 684.97,-273.72 738.97,-273.72 738.97,-237.72 684.97,-237.72"/>
|
||||
<text text-anchor="middle" x="711.97" y="-252.02" font-family="DejaVu Sans Mono, Book" font-size="14.00">cell</text>
|
||||
</g>
|
||||
<!-- ap--cell -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>ap:wifi--cell</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" stroke-dasharray="5,2" d="M501.45,-232.72C501.45,-232.72 628.02,-246.55 684.88,-252.76"/>
|
||||
</g>
|
||||
<!-- station1--cell -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>station1:wifi--cell</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" stroke-dasharray="5,2" d="M838.49,-475.95C838.49,-475.95 752.54,-326.34 722.32,-273.75"/>
|
||||
</g>
|
||||
<!-- station2--cell -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>station2:wifi--cell</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" stroke-dasharray="5,2" d="M838.49,-243.72C838.49,-243.72 776.28,-249.62 739.11,-253.15"/>
|
||||
</g>
|
||||
<!-- station3--cell -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>station3:wifi--cell</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" stroke-dasharray="5,2" d="M838.49,-11.5C838.49,-11.5 750.9,-180.57 721.48,-237.37"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
@@ -0,0 +1 @@
|
||||
test.adoc
|
||||
@@ -0,0 +1,43 @@
|
||||
=== WiFi AP and Station Association across two DUTs
|
||||
|
||||
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/wifi_ap_station_2dut]
|
||||
|
||||
==== Description
|
||||
|
||||
Two DUTs, each with its own radio: the ap runs a WPA2/WPA3-personal Access
|
||||
Point, the station associates to it. The radios are in
|
||||
separate kernels, so the "air" between them is realised differently per
|
||||
environment:
|
||||
|
||||
* On real hardware the link is real RF -- the radios just associate.
|
||||
* On QEMU the radios are mac80211_hwsim, and the `wifimedium` relay on
|
||||
each DUT bridges hwsim frames over the Ethernet segment between the
|
||||
guests (the topology's wifi-link). See doc/wifi.md.
|
||||
|
||||
The test asserts that the station associates with WPA2/WPA3-personal. That
|
||||
is a strong end-to-end check: authentication, association and the WPA
|
||||
4-way handshake all require frames to cross the medium in *both*
|
||||
directions, so a successful association proves real bidirectional
|
||||
communication over the (relayed) radio link -- not just that an interface
|
||||
came up. It then confirms data-plane reach: the ap serves DHCP and the
|
||||
station leases its address over the radio link.
|
||||
|
||||
Topology:
|
||||
....
|
||||
host ==(mgmt)== ap ))) ~ wifi-link ~ ((( station ==(mgmt)== host
|
||||
....
|
||||
|
||||
==== Topology
|
||||
|
||||
image::topology.svg[WiFi AP and Station Association across two DUTs topology, align=center, scaledwidth=75%]
|
||||
|
||||
==== Sequence
|
||||
|
||||
. Set up topology and attach to the ap and the station
|
||||
. Configure the ap as an Access Point on radio0
|
||||
. Configure the station on radio0
|
||||
. Verify the station associates to the ap over the wifi link
|
||||
. Verify the station's wifi0 operational status is up
|
||||
. Verify the station leases its address from the ap over wifi
|
||||
|
||||
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
r"""
|
||||
WiFi Access Point and Station association across two DUTs
|
||||
|
||||
Two DUTs, each with its own radio: the ap runs a WPA2/WPA3-personal Access
|
||||
Point, the station associates to it. The radios are in
|
||||
separate kernels, so the "air" between them is realised differently per
|
||||
environment:
|
||||
|
||||
* On real hardware the link is real RF -- the radios just associate.
|
||||
* On QEMU the radios are mac80211_hwsim, and the `wifimedium` relay on
|
||||
each DUT bridges hwsim frames over the Ethernet segment between the
|
||||
guests (the topology's wifi-link). See doc/wifi.md.
|
||||
|
||||
The test asserts that the station associates with WPA2/WPA3-personal. That
|
||||
is a strong end-to-end check: authentication, association and the WPA
|
||||
4-way handshake all require frames to cross the medium in *both*
|
||||
directions, so a successful association proves real bidirectional
|
||||
communication over the (relayed) radio link -- not just that an interface
|
||||
came up. It then confirms data-plane reach: the ap serves DHCP and the
|
||||
station leases its address over the radio link.
|
||||
|
||||
Topology:
|
||||
....
|
||||
host ==(mgmt)== ap ))) ~ wifi-link ~ ((( station ==(mgmt)== host
|
||||
....
|
||||
"""
|
||||
import base64
|
||||
|
||||
import infamy
|
||||
import infamy.iface as iface
|
||||
from infamy.util import until, parallel
|
||||
|
||||
SSID = "infix-test"
|
||||
PSK = "infixinfix" # 8-63 printable ASCII, see doc/wifi.md
|
||||
PSK_B64 = base64.b64encode(PSK.encode()).decode()
|
||||
COUNTRY = "SE"
|
||||
|
||||
# The ap runs a DHCP server on wifi0; the station leases its address. A
|
||||
# completed lease is real bidirectional IP traffic over the radio link.
|
||||
SUBNET = "192.168.20.0/24"
|
||||
AP_IP = "192.168.20.1"
|
||||
LEASE = "192.168.20.100" # single-address pool -> deterministic lease
|
||||
|
||||
|
||||
def radio(name, band=None, channel=None):
|
||||
"""ietf-hardware component for a radio."""
|
||||
wifi_radio = {"country-code": COUNTRY}
|
||||
if band:
|
||||
wifi_radio["band"] = band
|
||||
if channel is not None:
|
||||
wifi_radio["channel"] = channel
|
||||
return {
|
||||
"name": name,
|
||||
"class": "infix-hardware:wifi",
|
||||
"infix-hardware:wifi-radio": wifi_radio,
|
||||
}
|
||||
|
||||
|
||||
def keystore():
|
||||
return {
|
||||
"keystore": {
|
||||
"symmetric-keys": {
|
||||
"symmetric-key": [{
|
||||
"name": "wifi",
|
||||
"key-format": "infix-crypto-types:passphrase-key-format",
|
||||
"cleartext-symmetric-key": PSK_B64,
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
with infamy.Test() as test:
|
||||
with test.step("Set up topology and attach to the ap and the station"):
|
||||
env = infamy.Env()
|
||||
# Connect to both nodes concurrently to cut setup time.
|
||||
ap, station = parallel(
|
||||
lambda: env.attach("ap", "mgmt"),
|
||||
lambda: env.attach("station", "mgmt"),
|
||||
)
|
||||
|
||||
for dut in (ap, station):
|
||||
if not dut.has_feature("infix-interfaces", "wifi"):
|
||||
print("DUT does not advertise the 'wifi' feature -- skipping")
|
||||
test.skip()
|
||||
|
||||
with test.step("Configure the ap as an Access Point on radio0"):
|
||||
ap.put_config_dicts({
|
||||
"ietf-hardware": {"hardware": {"component": [
|
||||
radio("radio0", band="2.4GHz", channel=1)]}},
|
||||
"ietf-keystore": keystore(),
|
||||
"ietf-interfaces": {"interfaces": {"interface": [{
|
||||
"name": "wifi0",
|
||||
"type": "infix-if-type:wifi",
|
||||
# hwsim defaults every radio0 to 02:00:00:00:00:00, so the AP
|
||||
# and the station would otherwise share a MAC -- give each a
|
||||
# unique address.
|
||||
"infix-interfaces:custom-phys-address": {"static": "02:00:00:00:00:01"},
|
||||
"infix-interfaces:wifi": {
|
||||
"radio": "radio0",
|
||||
"access-point": {
|
||||
"ssid": SSID,
|
||||
"security": {"mode": "wpa2-wpa3-personal", "secret": "wifi"},
|
||||
},
|
||||
},
|
||||
"ietf-ip:ipv4": {"address": [
|
||||
{"ip": AP_IP, "prefix-length": 24}]},
|
||||
}]}},
|
||||
"infix-dhcp-server": {"dhcp-server": {"subnet": [{
|
||||
"subnet": SUBNET,
|
||||
"pool": {"start-address": LEASE, "end-address": LEASE},
|
||||
}]}},
|
||||
})
|
||||
|
||||
with test.step("Configure the station on radio0"):
|
||||
station.put_config_dicts({
|
||||
"ietf-hardware": {"hardware": {"component": [radio("radio0")]}},
|
||||
"ietf-keystore": keystore(),
|
||||
"ietf-interfaces": {"interfaces": {"interface": [{
|
||||
"name": "wifi0",
|
||||
"type": "infix-if-type:wifi",
|
||||
"infix-interfaces:custom-phys-address": {"static": "02:00:00:00:00:02"},
|
||||
"infix-interfaces:wifi": {
|
||||
"radio": "radio0",
|
||||
"station": {
|
||||
"ssid": SSID,
|
||||
"security": {"mode": "auto", "secret": "wifi"},
|
||||
},
|
||||
},
|
||||
"ietf-ip:ipv4": {"infix-dhcp-client:dhcp": {}},
|
||||
}]}},
|
||||
})
|
||||
|
||||
with test.step("Verify the station associates to the ap over the wifi link"):
|
||||
def associated():
|
||||
ifc = station.get_iface("wifi0")
|
||||
if not ifc:
|
||||
return False
|
||||
wifi = ifc.get("infix-interfaces:wifi") or ifc.get("wifi") or {}
|
||||
sta = wifi.get("station", {})
|
||||
return sta.get("ssid") == SSID and \
|
||||
sta.get("signal-strength") is not None
|
||||
until(associated, attempts=60, interval=2)
|
||||
|
||||
with test.step("Verify the station's wifi0 operational status is up"):
|
||||
until(lambda: iface.is_oper_up(station, "wifi0"), attempts=30)
|
||||
|
||||
with test.step("Verify the station leases its address from the ap over wifi"):
|
||||
until(lambda: iface.address_exist(station, "wifi0", LEASE),
|
||||
attempts=60, interval=2)
|
||||
|
||||
test.succeed()
|
||||
@@ -0,0 +1,44 @@
|
||||
graph "wifi-ap-station-2dut" {
|
||||
layout="neato";
|
||||
overlap="false";
|
||||
esep="+40";
|
||||
|
||||
node [shape=record, fontname="DejaVu Sans Mono, Book"];
|
||||
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
|
||||
|
||||
host [
|
||||
label="host | { <mgmt1> mgmt1 | <mgmt2> mgmt2 }",
|
||||
pos="0,0!",
|
||||
requires="controller",
|
||||
];
|
||||
|
||||
ap [
|
||||
label="{ <mgmt> mgmt | <wifi> wifi } | ap",
|
||||
pos="6,2!",
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
station [
|
||||
label="{ <mgmt> mgmt | <wifi> wifi } | station",
|
||||
pos="6,-2!",
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
// The wireless cell the ap and the station share, modelled as a medium
|
||||
// node both join (one radio each, same index -> same cell):
|
||||
// * physical: maps onto a real over-the-air RF cell;
|
||||
// * virtual (qeneth): maps onto one of the multicast cells over which
|
||||
// the wifimedium relay bridges mac80211_hwsim frames.
|
||||
// The mapper guarantees the two DUTs actually share this medium.
|
||||
cell [
|
||||
label="cell",
|
||||
pos="9,0!",
|
||||
requires="wifi-radio",
|
||||
];
|
||||
|
||||
host:mgmt1 -- ap:mgmt [requires="mgmt", color="lightgray"]
|
||||
host:mgmt2 -- station:mgmt [requires="mgmt", color="lightgray"]
|
||||
|
||||
ap:wifi -- cell [requires="wifi2.4GHz", style="dashed"]
|
||||
station:wifi -- cell [requires="wifi2.4GHz", style="dashed"]
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- Title: wifi-ap-station-2dut Pages: 1 -->
|
||||
<svg width="656pt" height="307pt"
|
||||
viewBox="0.00 0.00 656.06 307.03" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 303.03)">
|
||||
<title>wifi-ap-station-2dut</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-303.03 652.06,-303.03 652.06,4 -4,4"/>
|
||||
<!-- host -->
|
||||
<g id="node1" class="node">
|
||||
<title>host</title>
|
||||
<polygon fill="none" stroke="black" points="0,-126.51 0,-172.51 108,-172.51 108,-126.51 0,-126.51"/>
|
||||
<text text-anchor="middle" x="25" y="-145.81" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
|
||||
<polyline fill="none" stroke="black" points="50,-126.51 50,-172.51 "/>
|
||||
<text text-anchor="middle" x="79" y="-157.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt1</text>
|
||||
<polyline fill="none" stroke="black" points="50,-149.51 108,-149.51 "/>
|
||||
<text text-anchor="middle" x="79" y="-134.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt2</text>
|
||||
</g>
|
||||
<!-- ap -->
|
||||
<g id="node2" class="node">
|
||||
<title>ap</title>
|
||||
<polygon fill="none" stroke="black" points="390.54,-252.53 390.54,-298.53 473.54,-298.53 473.54,-252.53 390.54,-252.53"/>
|
||||
<text text-anchor="middle" x="415.54" y="-283.33" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="390.54,-275.53 440.54,-275.53 "/>
|
||||
<text text-anchor="middle" x="415.54" y="-260.33" font-family="DejaVu Sans Mono, Book" font-size="14.00">wifi</text>
|
||||
<polyline fill="none" stroke="black" points="440.54,-252.53 440.54,-298.53 "/>
|
||||
<text text-anchor="middle" x="457.04" y="-271.83" font-family="DejaVu Sans Mono, Book" font-size="14.00">ap</text>
|
||||
</g>
|
||||
<!-- host--ap -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:mgmt1--ap:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M108,-161.51C108,-161.51 390.04,-287.53 390.04,-287.53"/>
|
||||
</g>
|
||||
<!-- station -->
|
||||
<g id="node3" class="node">
|
||||
<title>station</title>
|
||||
<polygon fill="none" stroke="black" points="370.04,-0.5 370.04,-46.5 494.04,-46.5 494.04,-0.5 370.04,-0.5"/>
|
||||
<text text-anchor="middle" x="395.04" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="370.04,-23.5 420.04,-23.5 "/>
|
||||
<text text-anchor="middle" x="395.04" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">wifi</text>
|
||||
<polyline fill="none" stroke="black" points="420.04,-0.5 420.04,-46.5 "/>
|
||||
<text text-anchor="middle" x="457.04" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">station</text>
|
||||
</g>
|
||||
<!-- host--station -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>host:mgmt2--station:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M108,-137.51C108,-137.51 370.04,-35.5 370.04,-35.5"/>
|
||||
</g>
|
||||
<!-- cell -->
|
||||
<g id="node4" class="node">
|
||||
<title>cell</title>
|
||||
<polygon fill="none" stroke="black" points="594.06,-131.51 594.06,-167.51 648.06,-167.51 648.06,-131.51 594.06,-131.51"/>
|
||||
<text text-anchor="middle" x="621.06" y="-145.81" font-family="DejaVu Sans Mono, Book" font-size="14.00">cell</text>
|
||||
</g>
|
||||
<!-- ap--cell -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>ap:wifi--cell</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" stroke-dasharray="5,2" d="M415.04,-252.53C415.04,-252.53 537.66,-191.21 593.7,-163.19"/>
|
||||
</g>
|
||||
<!-- station--cell -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>station:wifi--cell</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" stroke-dasharray="5,2" d="M395.04,-0.5C395.04,-0.5 533.68,-91.91 593.79,-131.54"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1 @@
|
||||
test.adoc
|
||||
@@ -0,0 +1,50 @@
|
||||
=== WiFi Band Steering across a dual-band Access Point
|
||||
|
||||
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/wifi_band_steering]
|
||||
|
||||
==== Description
|
||||
|
||||
One DUT is a dual-band Access Point whose two radios sit on the *same*
|
||||
wireless cell: one radio serves the SSID "campus" on 2.4GHz and the other
|
||||
serves the same SSID on 5GHz. Both BSSes are bridged into br0 with a single DHCP
|
||||
server, so the two bands are one network. Infix runs all AP radios in a
|
||||
single hostapd process, which is what lets the cross-radio band-steering
|
||||
directive resolve (see src/confd/src/hardware.c).
|
||||
|
||||
The other DUT is a client with a *single* dual-band radio. Because both
|
||||
BSSes share one cell, that one radio hears "campus" on both bands -- the
|
||||
device band steering acts on.
|
||||
|
||||
Enabling 802.11v (`roaming dot11v`) on both BSSes turns on MBO band steering
|
||||
by default: hostapd tracks which band it has seen a client on and, on the
|
||||
2.4GHz BSS, suppresses probe responses to a client it has seen on the 5GHz
|
||||
BSS (`no_probe_resp_if_seen_on`). A dual-band client is therefore answered
|
||||
only on 5GHz and associates there; a 2.4-only client, never seen on 5GHz, is
|
||||
still answered and works normally. See doc/wifi.md.
|
||||
|
||||
The test asserts the client associates to "campus", that band steering lands
|
||||
it on the 5GHz BSS (not the 2.4GHz one), and that it leases an address -- a
|
||||
completed association plus DHCP lease is real bidirectional traffic over the
|
||||
(relayed) radio link.
|
||||
|
||||
Topology:
|
||||
....
|
||||
(( 2.4GHz ))
|
||||
host ==(mgmt)== ap ~ one cell ~ client ==(mgmt)== host
|
||||
(( 5GHz )) (single dual-band radio)
|
||||
....
|
||||
|
||||
==== Topology
|
||||
|
||||
image::topology.svg[WiFi Band Steering across a dual-band Access Point topology, align=center, scaledwidth=75%]
|
||||
|
||||
==== Sequence
|
||||
|
||||
. Set up topology and attach to the ap and the client
|
||||
. Configure the dual-band AP: one BSS on 2.4GHz, one on 5GHz
|
||||
. Configure the client with a single dual-band station radio
|
||||
. Verify the client associates to the 'campus' SSID
|
||||
. Verify band steering put the client on the 5GHz BSS
|
||||
. Verify the client leases an address over 5GHz
|
||||
|
||||
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env python3
|
||||
r"""
|
||||
WiFi Band Steering: a dual-band AP nudges a client onto 5GHz
|
||||
|
||||
One DUT is a dual-band Access Point whose two radios sit on the *same*
|
||||
wireless cell: one radio serves the SSID "campus" on 2.4GHz and the other
|
||||
serves the same SSID on 5GHz. Both BSSes are bridged into br0 with a single DHCP
|
||||
server, so the two bands are one network. Infix runs all AP radios in a
|
||||
single hostapd process, which is what lets the cross-radio band-steering
|
||||
directive resolve (see src/confd/src/hardware.c).
|
||||
|
||||
The other DUT is a client with a *single* dual-band radio. Because both
|
||||
BSSes share one cell, that one radio hears "campus" on both bands -- the
|
||||
device band steering acts on.
|
||||
|
||||
Enabling 802.11v (`roaming dot11v`) on both BSSes turns on MBO band steering
|
||||
by default: hostapd tracks which band it has seen a client on and, on the
|
||||
2.4GHz BSS, suppresses probe responses to a client it has seen on the 5GHz
|
||||
BSS (`no_probe_resp_if_seen_on`). A dual-band client is therefore answered
|
||||
only on 5GHz and associates there; a 2.4-only client, never seen on 5GHz, is
|
||||
still answered and works normally. See doc/wifi.md.
|
||||
|
||||
The test asserts the client associates to "campus", that band steering lands
|
||||
it on the 5GHz BSS (not the 2.4GHz one), and that it leases an address -- a
|
||||
completed association plus DHCP lease is real bidirectional traffic over the
|
||||
(relayed) radio link.
|
||||
|
||||
Topology:
|
||||
....
|
||||
(( 2.4GHz ))
|
||||
host ==(mgmt)== ap ~ one cell ~ client ==(mgmt)== host
|
||||
(( 5GHz )) (single dual-band radio)
|
||||
....
|
||||
"""
|
||||
import base64
|
||||
|
||||
import infamy
|
||||
import infamy.iface as iface
|
||||
from infamy.util import until, parallel
|
||||
|
||||
SSID = "campus"
|
||||
PSK = "infixinfix" # 8-63 printable ASCII, see doc/wifi.md
|
||||
PSK_B64 = base64.b64encode(PSK.encode()).decode()
|
||||
COUNTRY = "SE"
|
||||
|
||||
# Both BSSes are bridged into br0 behind one DHCP server: the two bands are
|
||||
# a single subnet, so a lease proves the client reached the network. A
|
||||
# two-address pool keeps the lease deterministic.
|
||||
SUBNET = "192.168.30.0/24"
|
||||
AP_IP = "192.168.30.1"
|
||||
POOL_START = "192.168.30.100"
|
||||
POOL_END = "192.168.30.101"
|
||||
|
||||
# hwsim defaults every radioN to the same address across guests, so the AP
|
||||
# BSSIDs and the client's radio would collide without explicit addresses.
|
||||
AP_BSSID_24 = "02:00:00:00:0a:01"
|
||||
AP_BSSID_5 = "02:00:00:00:0b:01"
|
||||
CLIENT_MAC = "02:00:00:00:00:0c"
|
||||
|
||||
|
||||
def radio(name, band=None, channel=None):
|
||||
"""ietf-hardware component for a radio."""
|
||||
wifi_radio = {"country-code": COUNTRY}
|
||||
if band:
|
||||
wifi_radio["band"] = band
|
||||
if channel is not None:
|
||||
wifi_radio["channel"] = channel
|
||||
return {
|
||||
"name": name,
|
||||
"class": "infix-hardware:wifi",
|
||||
"infix-hardware:wifi-radio": wifi_radio,
|
||||
}
|
||||
|
||||
|
||||
def keystore():
|
||||
return {
|
||||
"keystore": {
|
||||
"symmetric-keys": {
|
||||
"symmetric-key": [{
|
||||
"name": "wifi",
|
||||
"key-format": "infix-crypto-types:passphrase-key-format",
|
||||
"cleartext-symmetric-key": PSK_B64,
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def ap_bss(name, radio_name, bssid):
|
||||
"""A band-steering AP BSS on the given radio, bridged into br0."""
|
||||
return {
|
||||
"name": name,
|
||||
"type": "infix-if-type:wifi",
|
||||
"enabled": True,
|
||||
"infix-interfaces:custom-phys-address": {"static": bssid},
|
||||
"infix-interfaces:wifi": {
|
||||
"radio": radio_name,
|
||||
"access-point": {
|
||||
"ssid": SSID,
|
||||
"security": {"mode": "wpa2-wpa3-personal", "secret": "wifi"},
|
||||
# dot11v enables MBO band steering by default; the two BSSes
|
||||
# share the SSID so band steering has another band to point at.
|
||||
"roaming": {"dot11v": {}},
|
||||
},
|
||||
},
|
||||
"infix-interfaces:bridge-port": {"bridge": "br0"},
|
||||
}
|
||||
|
||||
|
||||
def wifi_of(ifc):
|
||||
return (ifc or {}).get("infix-interfaces:wifi") or (ifc or {}).get("wifi") or {}
|
||||
|
||||
|
||||
def associated(dut, ifname):
|
||||
sta = wifi_of(dut.get_iface(ifname)).get("station", {})
|
||||
return sta.get("ssid") == SSID and sta.get("signal-strength") is not None
|
||||
|
||||
|
||||
def ap_clients(dut, ifname):
|
||||
"""MACs of the stations currently associated to this BSS."""
|
||||
ap = wifi_of(dut.get_iface(ifname)).get("access-point") or {}
|
||||
stations = (ap.get("stations") or {}).get("station") or []
|
||||
return {s.get("mac-address", "").lower() for s in stations}
|
||||
|
||||
|
||||
with infamy.Test() as test:
|
||||
with test.step("Set up topology and attach to the ap and the client"):
|
||||
env = infamy.Env()
|
||||
ap, client = parallel(
|
||||
lambda: env.attach("ap", "mgmt"),
|
||||
lambda: env.attach("client", "mgmt"),
|
||||
)
|
||||
|
||||
for dut in (ap, client):
|
||||
if not dut.has_feature("infix-interfaces", "wifi"):
|
||||
print("DUT does not advertise the 'wifi' feature -- skipping")
|
||||
test.skip()
|
||||
|
||||
with test.step("Configure the dual-band AP: one BSS on 2.4GHz, one on 5GHz"):
|
||||
# radio2 and radio3 are dut1's two extra radios, both wired to the
|
||||
# dedicated band-steering cell (cell2) in test/virt/quad.
|
||||
ap.put_config_dicts({
|
||||
"ietf-hardware": {"hardware": {"component": [
|
||||
radio("radio2", band="2.4GHz", channel=1),
|
||||
radio("radio3", band="5GHz", channel=36),
|
||||
]}},
|
||||
"ietf-keystore": keystore(),
|
||||
"ietf-interfaces": {"interfaces": {"interface": [
|
||||
{"name": "br0", "type": "infix-if-type:bridge", "enabled": True,
|
||||
"ietf-ip:ipv4": {"address": [
|
||||
{"ip": AP_IP, "prefix-length": 24}]}},
|
||||
ap_bss("wifi0", "radio2", AP_BSSID_24),
|
||||
ap_bss("wifi1", "radio3", AP_BSSID_5),
|
||||
]}},
|
||||
"infix-dhcp-server": {"dhcp-server": {"subnet": [{
|
||||
"subnet": SUBNET,
|
||||
"pool": {"start-address": POOL_START, "end-address": POOL_END},
|
||||
}]}},
|
||||
})
|
||||
|
||||
with test.step("Configure the client with a single dual-band station radio"):
|
||||
# radio2 is the client DUT's extra radio on the band-steering cell
|
||||
# (cell2). No band/channel pinned: the one radio scans both bands and
|
||||
# lets band steering decide where it lands.
|
||||
client.put_config_dicts({
|
||||
"ietf-hardware": {"hardware": {"component": [radio("radio2")]}},
|
||||
"ietf-keystore": keystore(),
|
||||
"ietf-interfaces": {"interfaces": {"interface": [{
|
||||
"name": "wifi0",
|
||||
"type": "infix-if-type:wifi",
|
||||
"enabled": True,
|
||||
"infix-interfaces:custom-phys-address": {"static": CLIENT_MAC},
|
||||
"infix-interfaces:wifi": {
|
||||
"radio": "radio2",
|
||||
"station": {
|
||||
"ssid": SSID,
|
||||
"security": {"mode": "auto", "secret": "wifi"},
|
||||
},
|
||||
},
|
||||
"ietf-ip:ipv4": {"infix-dhcp-client:dhcp": {}},
|
||||
}]}},
|
||||
})
|
||||
|
||||
with test.step("Verify the client associates to the 'campus' SSID"):
|
||||
until(lambda: associated(client, "wifi0"), attempts=60, interval=2)
|
||||
|
||||
with test.step("Verify band steering put the client on the 5GHz BSS"):
|
||||
# Steered: the client shows up on the 5GHz BSS (wifi1) and not on the
|
||||
# 2.4GHz BSS (wifi0) -- the 2.4GHz BSS suppressed its probe responses
|
||||
# once it had been seen on 5GHz.
|
||||
until(lambda: CLIENT_MAC in ap_clients(ap, "wifi1"), attempts=90, interval=2)
|
||||
assert CLIENT_MAC not in ap_clients(ap, "wifi0"), \
|
||||
"client associated on 2.4GHz; band steering did not steer it to 5GHz"
|
||||
|
||||
with test.step("Verify the client leases an address over 5GHz"):
|
||||
until(lambda: iface.is_oper_up(client, "wifi0"), attempts=30)
|
||||
until(lambda: iface.address_exist(client, "wifi0", POOL_START) or
|
||||
iface.address_exist(client, "wifi0", POOL_END),
|
||||
attempts=60, interval=2)
|
||||
|
||||
test.succeed()
|
||||
@@ -0,0 +1,54 @@
|
||||
graph "wifi-band-steering" {
|
||||
layout="neato";
|
||||
overlap="false";
|
||||
esep="+40";
|
||||
|
||||
node [shape=record, fontname="DejaVu Sans Mono, Book"];
|
||||
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
|
||||
|
||||
host [
|
||||
label="host | { <mgmt1> mgmt1 | <mgmt2> mgmt2 }",
|
||||
pos="0,0!",
|
||||
requires="controller",
|
||||
];
|
||||
|
||||
// One dual-band Access Point with two radios on the *same* cell: one
|
||||
// serves the SSID on 2.4GHz, the other serves the same SSID on 5GHz.
|
||||
// "wifi-dualband" pins it to the quad DUT that wires two extra radios to
|
||||
// the dedicated band-steering cell, the only node where both BSSes share
|
||||
// one cell (see test/virt/quad).
|
||||
ap [
|
||||
label="{ <mgmt> mgmt | <lo> lo | <hi> hi } | ap",
|
||||
pos="6,0!",
|
||||
requires="infix wifi-dualband",
|
||||
];
|
||||
|
||||
// One client with a single dual-band radio. Because both AP BSSes live
|
||||
// on the one cell, this single radio hears the SSID on both bands and is
|
||||
// the device band steering nudges onto 5GHz.
|
||||
client [
|
||||
label="{ <mgmt> mgmt | <wifi> wifi } | client",
|
||||
pos="12,0!",
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
// A single wireless cell carrying both bands. The medium is
|
||||
// frequency-aware (the wifimedium relay preserves each frame's frequency
|
||||
// and mac80211 channel-filters), so 2.4GHz and 5GHz coexist on one cell:
|
||||
// * physical: one over-the-air neighbourhood the client scans;
|
||||
// * virtual (qeneth): one multicast cell the AP's two radios and the
|
||||
// client's radio all join.
|
||||
cell [
|
||||
label="cell\n(2.4GHz + 5GHz)",
|
||||
pos="9,0!",
|
||||
requires="wifi-radio",
|
||||
];
|
||||
|
||||
host:mgmt1 -- ap:mgmt [requires="mgmt", color="lightgray"]
|
||||
host:mgmt2 -- client:mgmt [requires="mgmt", color="lightgray"]
|
||||
|
||||
// Both AP radios and the client's single radio share the one cell.
|
||||
ap:lo -- cell [requires="wifi2.4GHz", style="dashed"]
|
||||
ap:hi -- cell [requires="wifi5GHz", style="dashed"]
|
||||
client:wifi -- cell [requires="wifi2.4GHz wifi5GHz", style="dashed"]
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- Title: wifi-band-steering Pages: 1 -->
|
||||
<svg width="1032pt" height="78pt"
|
||||
viewBox="0.00 0.00 1032.09 78.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 74)">
|
||||
<title>wifi-band-steering</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-74 1028.09,-74 1028.09,4 -4,4"/>
|
||||
<!-- host -->
|
||||
<g id="node1" class="node">
|
||||
<title>host</title>
|
||||
<polygon fill="none" stroke="black" points="0,-12 0,-58 108,-58 108,-12 0,-12"/>
|
||||
<text text-anchor="middle" x="25" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
|
||||
<polyline fill="none" stroke="black" points="50,-12 50,-58 "/>
|
||||
<text text-anchor="middle" x="79" y="-42.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt1</text>
|
||||
<polyline fill="none" stroke="black" points="50,-35 108,-35 "/>
|
||||
<text text-anchor="middle" x="79" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt2</text>
|
||||
</g>
|
||||
<!-- ap -->
|
||||
<g id="node2" class="node">
|
||||
<title>ap</title>
|
||||
<polygon fill="none" stroke="black" points="468.55,-0.5 468.55,-69.5 551.55,-69.5 551.55,-0.5 468.55,-0.5"/>
|
||||
<text text-anchor="middle" x="493.55" y="-54.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="468.55,-46.5 518.55,-46.5 "/>
|
||||
<text text-anchor="middle" x="493.55" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">lo</text>
|
||||
<polyline fill="none" stroke="black" points="468.55,-23.5 518.55,-23.5 "/>
|
||||
<text text-anchor="middle" x="493.55" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">hi</text>
|
||||
<polyline fill="none" stroke="black" points="518.55,-0.5 518.55,-69.5 "/>
|
||||
<text text-anchor="middle" x="535.05" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">ap</text>
|
||||
</g>
|
||||
<!-- host--ap -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:mgmt1--ap:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M108,-47C108,-47 468.05,-58 468.05,-58"/>
|
||||
</g>
|
||||
<!-- client -->
|
||||
<g id="node3" class="node">
|
||||
<title>client</title>
|
||||
<polygon fill="none" stroke="black" points="908.09,-12 908.09,-58 1024.09,-58 1024.09,-12 908.09,-12"/>
|
||||
<text text-anchor="middle" x="933.09" y="-42.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="908.09,-35 958.09,-35 "/>
|
||||
<text text-anchor="middle" x="933.09" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">wifi</text>
|
||||
<polyline fill="none" stroke="black" points="958.09,-12 958.09,-58 "/>
|
||||
<text text-anchor="middle" x="991.09" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">client</text>
|
||||
</g>
|
||||
<!-- host--client -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>host:mgmt2--client:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M108,-23C108,-23 908.09,-47 908.09,-47"/>
|
||||
</g>
|
||||
<!-- cell -->
|
||||
<g id="node4" class="node">
|
||||
<title>cell</title>
|
||||
<polygon fill="none" stroke="black" points="668.07,-16 668.07,-54 808.07,-54 808.07,-16 668.07,-16"/>
|
||||
<text text-anchor="middle" x="738.07" y="-38.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">cell</text>
|
||||
<text text-anchor="middle" x="738.07" y="-23.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">(2.4GHz + 5GHz)</text>
|
||||
</g>
|
||||
<!-- ap--cell -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>ap:lo--cell</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" stroke-dasharray="5,2" d="M468.05,-35C468.05,-35 587.82,-35 667.83,-35"/>
|
||||
</g>
|
||||
<!-- ap--cell -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>ap:hi--cell</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" stroke-dasharray="5,2" d="M493.05,0C493.05,0 595.45,-14.63 667.85,-24.97"/>
|
||||
</g>
|
||||
<!-- client--cell -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>client:wifi--cell</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" stroke-dasharray="5,2" d="M908.09,-23C908.09,-23 855.04,-26.74 808.2,-30.05"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
@@ -0,0 +1 @@
|
||||
test.adoc
|
||||
@@ -0,0 +1,62 @@
|
||||
=== WiFi Mesh backhaul with roaming Access Points
|
||||
|
||||
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/wifi_mesh_roaming]
|
||||
|
||||
==== Description
|
||||
|
||||
The worked example from doc/whitepaper-wifi-mesh-roaming.md, as a test.
|
||||
|
||||
Three gateway nodes (gw1, gw2, gw3) each do two jobs on two radios:
|
||||
|
||||
* radio0 -- an 802.11s mesh point. The three join one mesh ("backhaul")
|
||||
on 5GHz; this carries traffic between the nodes so only gw1 needs a wire
|
||||
to the rest of the LAN.
|
||||
* radio1 -- a WPA2/WPA3-personal Access Point on 2.4GHz. All three share
|
||||
the same SSID ("campus") and 802.11r mobility domain, so a client roams
|
||||
between them as one network.
|
||||
|
||||
Each node bridges its mesh and AP (and, on gw1, the wired uplink) into br0,
|
||||
so the mesh is a transparent layer-2 backhaul. A fourth node is the client.
|
||||
|
||||
The test checks the claims the whitepaper makes:
|
||||
|
||||
1. the three nodes form a mesh (each sees its two peers);
|
||||
2. the client associates to the "campus" SSID;
|
||||
3. traffic reaches the client across the mesh backhaul (host behind gw1
|
||||
pings the client, which is attached to some gw's AP);
|
||||
4. roaming: the client reports the BSSID it is connected to, which is one
|
||||
of the three gw APs; when that AP is taken down the client moves to
|
||||
another node -- same SSID, same mobility domain -- so the reported
|
||||
BSSID changes to a different gw, and traffic recovers.
|
||||
|
||||
In simulation every radio hears every other at one fixed strength, so there
|
||||
is no signal gradient to drift the client between APs. Step 4 forces the
|
||||
move instead, which is a stronger check: it proves a second AP accepts the
|
||||
client and the backhaul re-converges.
|
||||
|
||||
Topology:
|
||||
....
|
||||
host ==(mgmt)== gw1 (mesh+AP) )) ~ mesh ~ (( gw2 (mesh+AP)
|
||||
\\__(lan, wired)__/ \ /
|
||||
) ~ mesh ~ (( gw3 (mesh+AP)
|
||||
client (((roams between the gw APs)))
|
||||
....
|
||||
|
||||
==== Topology
|
||||
|
||||
image::topology.svg[WiFi Mesh backhaul with roaming Access Points topology, align=center, scaledwidth=75%]
|
||||
|
||||
==== Sequence
|
||||
|
||||
. Set up topology and attach to gw1, gw2, gw3 and the client
|
||||
. Configure gw1, gw2, gw3 as mesh nodes with a roaming AP
|
||||
. Configure the client as a station for the 'campus' SSID
|
||||
. Verify the three nodes form the mesh backhaul
|
||||
. Verify the client associates to the 'campus' SSID
|
||||
. Verify the client is connected to one of the campus APs
|
||||
. Verify the client is reachable across the mesh
|
||||
. Take down the client's current AP to force a roam
|
||||
. Verify the client roams to another node's AP
|
||||
. Verify connectivity is restored after roaming
|
||||
|
||||
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
#!/usr/bin/env python3
|
||||
r"""
|
||||
WiFi Mesh backhaul with roaming Access Points
|
||||
|
||||
The worked example from doc/whitepaper-wifi-mesh-roaming.md, as a test.
|
||||
|
||||
Three gateway nodes (gw1, gw2, gw3) each do two jobs on two radios:
|
||||
|
||||
* radio0 -- an 802.11s mesh point. The three join one mesh ("backhaul")
|
||||
on 5GHz; this carries traffic between the nodes so only gw1 needs a wire
|
||||
to the rest of the LAN.
|
||||
* radio1 -- a WPA2/WPA3-personal Access Point on 2.4GHz. All three share
|
||||
the same SSID ("campus") and 802.11r mobility domain, so a client roams
|
||||
between them as one network.
|
||||
|
||||
Each node bridges its mesh and AP (and, on gw1, the wired uplink) into br0,
|
||||
so the mesh is a transparent layer-2 backhaul. A fourth node is the client.
|
||||
|
||||
The test checks the claims the whitepaper makes:
|
||||
|
||||
1. the three nodes form a mesh (each sees its two peers);
|
||||
2. the client associates to the "campus" SSID;
|
||||
3. traffic reaches the client across the mesh backhaul (host behind gw1
|
||||
pings the client, which is attached to some gw's AP);
|
||||
4. roaming: the client reports the BSSID it is connected to, which is one
|
||||
of the three gw APs; when that AP is taken down the client moves to
|
||||
another node -- same SSID, same mobility domain -- so the reported
|
||||
BSSID changes to a different gw, and traffic recovers.
|
||||
|
||||
In simulation every radio hears every other at one fixed strength, so there
|
||||
is no signal gradient to drift the client between APs. Step 4 forces the
|
||||
move instead, which is a stronger check: it proves a second AP accepts the
|
||||
client and the backhaul re-converges.
|
||||
|
||||
Topology:
|
||||
....
|
||||
host ==(mgmt)== gw1 (mesh+AP) )) ~ mesh ~ (( gw2 (mesh+AP)
|
||||
\\__(lan, wired)__/ \ /
|
||||
) ~ mesh ~ (( gw3 (mesh+AP)
|
||||
client (((roams between the gw APs)))
|
||||
....
|
||||
"""
|
||||
import base64
|
||||
|
||||
import infamy
|
||||
from infamy.util import until, parallel
|
||||
|
||||
SSID = "campus"
|
||||
MESH_ID = "backhaul"
|
||||
MESH_PSK = base64.b64encode(b"meshmeshmesh").decode()
|
||||
WIFI_PSK = base64.b64encode(b"infixinfix").decode()
|
||||
COUNTRY = "SE"
|
||||
|
||||
CLIENT_IP = "10.0.0.9"
|
||||
HOST_IP = "10.0.0.1"
|
||||
CLIENT_MAC = "02:00:00:00:00:09"
|
||||
|
||||
# Unique MACs: hwsim defaults every radioN to the same address across guests,
|
||||
# so the mesh peers and the AP BSSIDs would collide without these. The AP MAC
|
||||
# is the BSSID the client reports when associated to that node.
|
||||
GWS = [
|
||||
# name, mesh radio0 MAC, AP radio1 MAC (BSSID)
|
||||
("gw1", "02:00:00:00:00:01", "02:00:00:00:0a:01"),
|
||||
("gw2", "02:00:00:00:00:02", "02:00:00:00:0a:02"),
|
||||
("gw3", "02:00:00:00:00:03", "02:00:00:00:0a:03"),
|
||||
]
|
||||
|
||||
|
||||
def radio(name, band, channel):
|
||||
return {
|
||||
"name": name,
|
||||
"class": "infix-hardware:wifi",
|
||||
"infix-hardware:wifi-radio": {
|
||||
"country-code": COUNTRY, "band": band, "channel": channel,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def keystore():
|
||||
return {"keystore": {"symmetric-keys": {"symmetric-key": [
|
||||
{"name": "mesh-secret",
|
||||
"key-format": "infix-crypto-types:passphrase-key-format",
|
||||
"cleartext-symmetric-key": MESH_PSK},
|
||||
{"name": "wifi-secret",
|
||||
"key-format": "infix-crypto-types:passphrase-key-format",
|
||||
"cleartext-symmetric-key": WIFI_PSK},
|
||||
]}}}
|
||||
|
||||
|
||||
def gw_config(mesh_mac, ap_mac, uplink=None):
|
||||
interfaces = [
|
||||
{"name": "br0", "type": "infix-if-type:bridge", "enabled": True},
|
||||
{
|
||||
"name": "wifi0", "type": "infix-if-type:wifi", "enabled": True,
|
||||
"infix-interfaces:custom-phys-address": {"static": mesh_mac},
|
||||
"infix-interfaces:wifi": {
|
||||
"radio": "radio0",
|
||||
"mesh-point": {
|
||||
"mesh-id": MESH_ID,
|
||||
"security": {"secret": "mesh-secret"},
|
||||
},
|
||||
},
|
||||
"infix-interfaces:bridge-port": {"bridge": "br0"},
|
||||
},
|
||||
{
|
||||
"name": "wifi1", "type": "infix-if-type:wifi", "enabled": True,
|
||||
"infix-interfaces:custom-phys-address": {"static": ap_mac},
|
||||
"infix-interfaces:wifi": {
|
||||
"radio": "radio1",
|
||||
"access-point": {
|
||||
"ssid": SSID,
|
||||
"security": {"mode": "wpa2-wpa3-personal", "secret": "wifi-secret"},
|
||||
"roaming": {
|
||||
"dot11r": {"mobility-domain": "hash"},
|
||||
"dot11k": {},
|
||||
"dot11v": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
"infix-interfaces:bridge-port": {"bridge": "br0"},
|
||||
},
|
||||
]
|
||||
if uplink:
|
||||
interfaces.append({
|
||||
"name": uplink, "enabled": True,
|
||||
"infix-interfaces:bridge-port": {"bridge": "br0"},
|
||||
})
|
||||
return {
|
||||
"ietf-hardware": {"hardware": {"component": [
|
||||
radio("radio0", "5GHz", 36),
|
||||
radio("radio1", "2.4GHz", 1),
|
||||
]}},
|
||||
"ietf-keystore": keystore(),
|
||||
"ietf-interfaces": {"interfaces": {"interface": interfaces}},
|
||||
}
|
||||
|
||||
|
||||
def wifi_of(ifc):
|
||||
return (ifc or {}).get("infix-interfaces:wifi") or (ifc or {}).get("wifi") or {}
|
||||
|
||||
|
||||
def mesh_peers(dut):
|
||||
mp = wifi_of(dut.get_iface("wifi0")).get("mesh-point") or {}
|
||||
return (mp.get("peers") or {}).get("peer") or []
|
||||
|
||||
|
||||
def station(dut, ifname="wifi0"):
|
||||
return wifi_of(dut.get_iface(ifname)).get("station", {})
|
||||
|
||||
|
||||
def station_bssid(dut):
|
||||
"""The BSSID the client's station is currently associated to."""
|
||||
return (station(dut).get("bssid") or "").lower()
|
||||
|
||||
|
||||
with infamy.Test() as test:
|
||||
with test.step("Set up topology and attach to gw1, gw2, gw3 and the client"):
|
||||
env = infamy.Env()
|
||||
# Connect to all four nodes concurrently -- each attach probes the
|
||||
# node and downloads its YANG models, so doing them in parallel cuts
|
||||
# the setup time roughly four-fold.
|
||||
gw1, gw2, gw3, client = parallel(
|
||||
lambda: env.attach("gw1", "mgmt"),
|
||||
lambda: env.attach("gw2", "mgmt"),
|
||||
lambda: env.attach("gw3", "mgmt"),
|
||||
lambda: env.attach("client", "mgmt"),
|
||||
)
|
||||
gw_duts = [gw1, gw2, gw3]
|
||||
gws = [(name, dut, mesh, ap) for (name, mesh, ap), dut in zip(GWS, gw_duts)]
|
||||
|
||||
for dut in gw_duts + [client]:
|
||||
if not dut.has_feature("infix-interfaces", "wifi"):
|
||||
print("DUT does not advertise the 'wifi' feature -- skipping")
|
||||
test.skip()
|
||||
|
||||
with test.step("Configure gw1, gw2, gw3 as mesh nodes with a roaming AP"):
|
||||
_, gw1_uplink = env.ltop.xlate("gw1", "uplink")
|
||||
for name, dut, mesh_mac, ap_mac in gws:
|
||||
dut.put_config_dicts(gw_config(mesh_mac, ap_mac,
|
||||
uplink=gw1_uplink if name == "gw1" else None))
|
||||
|
||||
with test.step("Configure the client as a station for the 'campus' SSID"):
|
||||
# The client joins the gw APs, which run on radio1 (2.4GHz). In the
|
||||
# virtual topology a wireless cell is shared per radio index, so the
|
||||
# client's station must use radio1 too -- a station and the AP it
|
||||
# associates to live in the same cell only when they share an index.
|
||||
# See doc/wifi.md and test/virt/quad.
|
||||
client.put_config_dicts({
|
||||
"ietf-hardware": {"hardware": {"component": [radio("radio1", "2.4GHz", 1)]}},
|
||||
"ietf-keystore": keystore(),
|
||||
"ietf-interfaces": {"interfaces": {"interface": [{
|
||||
"name": "wifi0", "type": "infix-if-type:wifi", "enabled": True,
|
||||
"infix-interfaces:custom-phys-address": {"static": CLIENT_MAC},
|
||||
"infix-interfaces:wifi": {
|
||||
"radio": "radio1",
|
||||
"station": {
|
||||
"ssid": SSID,
|
||||
"security": {"mode": "auto", "secret": "wifi-secret"},
|
||||
},
|
||||
},
|
||||
"ietf-ip:ipv4": {"address": [{"ip": CLIENT_IP, "prefix-length": 24}]},
|
||||
}]}},
|
||||
})
|
||||
|
||||
with test.step("Verify the three nodes form the mesh backhaul"):
|
||||
for name, dut, _, _ in gws:
|
||||
until(lambda dut=dut: len(mesh_peers(dut)) >= 2, attempts=60, interval=2)
|
||||
|
||||
with test.step("Verify the client associates to the 'campus' SSID"):
|
||||
def associated():
|
||||
sta = station(client)
|
||||
return sta.get("ssid") == SSID and sta.get("signal-strength") is not None
|
||||
until(associated, attempts=60, interval=2)
|
||||
|
||||
# The client reports the BSSID it is on; with all three APs sharing the
|
||||
# SSID, that BSSID is what tells them apart.
|
||||
aps = {ap_mac.lower(): (name, dut) for name, dut, _, ap_mac in gws}
|
||||
|
||||
with test.step("Verify the client is connected to one of the campus APs"):
|
||||
until(lambda: station_bssid(client) in aps, attempts=60, interval=2)
|
||||
first_bssid = station_bssid(client)
|
||||
first_ap, first_dut = aps[first_bssid]
|
||||
print(f"client is on {first_ap} ({first_bssid})")
|
||||
|
||||
# The host sits on the wired LAN behind gw1; reaching the client proves
|
||||
# the frames cross the mesh backhaul (the client may be on any gw's AP).
|
||||
_, hlan = env.ltop.xlate("host", "lan")
|
||||
with infamy.IsolatedMacVlan(hlan) as ns:
|
||||
ns.addip(HOST_IP)
|
||||
|
||||
with test.step("Verify the client is reachable across the mesh"):
|
||||
ns.must_reach(CLIENT_IP)
|
||||
|
||||
with test.step("Take down the client's current AP to force a roam"):
|
||||
first_dut.put_config_dicts({"ietf-interfaces": {"interfaces": {
|
||||
"interface": [{"name": "wifi1", "enabled": False}]}}})
|
||||
|
||||
with test.step("Verify the client roams to another node's AP"):
|
||||
until(lambda: station_bssid(client) in aps and station_bssid(client) != first_bssid,
|
||||
attempts=90, interval=2)
|
||||
new_ap, _ = aps[station_bssid(client)]
|
||||
print(f"client roamed from {first_ap} to {new_ap}")
|
||||
|
||||
with test.step("Verify connectivity is restored after roaming"):
|
||||
ns.must_reach(CLIENT_IP)
|
||||
|
||||
test.succeed()
|
||||
@@ -0,0 +1,79 @@
|
||||
graph "wifi-mesh-roaming" {
|
||||
layout="neato";
|
||||
overlap="false";
|
||||
esep="+40";
|
||||
|
||||
node [shape=record, fontname="DejaVu Sans Mono, Book"];
|
||||
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
|
||||
|
||||
host [
|
||||
label="host | { <mgmt1> mgmt1 | <mgmt2> mgmt2 | <mgmt3> mgmt3 | <mgmt4> mgmt4 | <lan> lan }",
|
||||
pos="0,0!",
|
||||
requires="controller",
|
||||
];
|
||||
|
||||
gw1 [
|
||||
label="{ <mgmt> mgmt | <uplink> uplink | <mesh> mesh | <ap> ap } | gw1",
|
||||
pos="6,4!",
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
gw2 [
|
||||
label="{ <mgmt> mgmt | <mesh> mesh | <ap> ap } | gw2",
|
||||
pos="9,0!",
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
gw3 [
|
||||
label="{ <mgmt> mgmt | <mesh> mesh | <ap> ap } | gw3",
|
||||
pos="6,-4!",
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
client [
|
||||
label="{ <mgmt> mgmt | <wifi> wifi } | client",
|
||||
pos="13,0!",
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
// Two wireless cells, each a medium node the relevant radios join. The
|
||||
// three gw nodes form the 802.11s mesh backhaul on their radio0 (the
|
||||
// "mesh" cell); their roaming AP and the client all live on radio1 (the
|
||||
// "ap" cell), so the client is in range of every gw's AP and can roam.
|
||||
// Communicating radios share an index -> the same cell (see doc/wifi.md):
|
||||
// * physical: maps onto two over-the-air RF cells;
|
||||
// * virtual (qeneth): maps onto two multicast cells the wifimedium
|
||||
// relay bridges mac80211_hwsim frames over.
|
||||
mesh [
|
||||
label="mesh-cell",
|
||||
pos="6,0!",
|
||||
requires="wifi-radio",
|
||||
];
|
||||
|
||||
ap [
|
||||
label="ap-cell",
|
||||
pos="11,0!",
|
||||
requires="wifi-radio",
|
||||
];
|
||||
|
||||
host:mgmt1 -- gw1:mgmt [requires="mgmt", color="lightgray"]
|
||||
host:mgmt2 -- gw2:mgmt [requires="mgmt", color="lightgray"]
|
||||
host:mgmt3 -- gw3:mgmt [requires="mgmt", color="lightgray"]
|
||||
host:mgmt4 -- client:mgmt [requires="mgmt", color="lightgray"]
|
||||
|
||||
// Wired uplink: the rest of the LAN sits behind gw1. gw2/gw3 reach it
|
||||
// only over the mesh backhaul, so traffic to a client on their APs must
|
||||
// cross the mesh.
|
||||
host:lan -- gw1:uplink [color="black"]
|
||||
|
||||
// Mesh backhaul (radio0, 5GHz)
|
||||
gw1:mesh -- mesh [requires="wifi5GHz", style="dashed"]
|
||||
gw2:mesh -- mesh [requires="wifi5GHz", style="dashed"]
|
||||
gw3:mesh -- mesh [requires="wifi5GHz", style="dashed"]
|
||||
|
||||
// Roaming AP cell (radio1, 2.4GHz): the gw APs and the client
|
||||
gw1:ap -- ap [requires="wifi2.4GHz", style="dashed"]
|
||||
gw2:ap -- ap [requires="wifi2.4GHz", style="dashed"]
|
||||
gw3:ap -- ap [requires="wifi2.4GHz", style="dashed"]
|
||||
client:wifi -- ap [requires="wifi2.4GHz", style="dashed"]
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- Title: wifi-mesh-roaming Pages: 1 -->
|
||||
<svg width="1388pt" height="870pt"
|
||||
viewBox="0.00 0.00 1387.63 869.58" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 865.58)">
|
||||
<title>wifi-mesh-roaming</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-865.58 1383.63,-865.58 1383.63,4 -4,4"/>
|
||||
<!-- host -->
|
||||
<g id="node1" class="node">
|
||||
<title>host</title>
|
||||
<polygon fill="none" stroke="black" points="0,-367.54 0,-482.54 108,-482.54 108,-367.54 0,-367.54"/>
|
||||
<text text-anchor="middle" x="25" y="-421.34" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
|
||||
<polyline fill="none" stroke="black" points="50,-367.54 50,-482.54 "/>
|
||||
<text text-anchor="middle" x="79" y="-467.34" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt1</text>
|
||||
<polyline fill="none" stroke="black" points="50,-459.54 108,-459.54 "/>
|
||||
<text text-anchor="middle" x="79" y="-444.34" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt2</text>
|
||||
<polyline fill="none" stroke="black" points="50,-436.54 108,-436.54 "/>
|
||||
<text text-anchor="middle" x="79" y="-421.34" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt3</text>
|
||||
<polyline fill="none" stroke="black" points="50,-413.54 108,-413.54 "/>
|
||||
<text text-anchor="middle" x="79" y="-398.34" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt4</text>
|
||||
<polyline fill="none" stroke="black" points="50,-390.54 108,-390.54 "/>
|
||||
<text text-anchor="middle" x="79" y="-375.34" font-family="DejaVu Sans Mono, Book" font-size="14.00">lan</text>
|
||||
</g>
|
||||
<!-- gw1 -->
|
||||
<g id="node2" class="node">
|
||||
<title>gw1</title>
|
||||
<polygon fill="none" stroke="black" points="585.56,-769.08 585.56,-861.08 692.56,-861.08 692.56,-769.08 585.56,-769.08"/>
|
||||
<text text-anchor="middle" x="618.56" y="-845.88" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="585.56,-838.08 651.56,-838.08 "/>
|
||||
<text text-anchor="middle" x="618.56" y="-822.88" font-family="DejaVu Sans Mono, Book" font-size="14.00">uplink</text>
|
||||
<polyline fill="none" stroke="black" points="585.56,-815.08 651.56,-815.08 "/>
|
||||
<text text-anchor="middle" x="618.56" y="-799.88" font-family="DejaVu Sans Mono, Book" font-size="14.00">mesh</text>
|
||||
<polyline fill="none" stroke="black" points="585.56,-792.08 651.56,-792.08 "/>
|
||||
<text text-anchor="middle" x="618.56" y="-776.88" font-family="DejaVu Sans Mono, Book" font-size="14.00">ap</text>
|
||||
<polyline fill="none" stroke="black" points="651.56,-769.08 651.56,-861.08 "/>
|
||||
<text text-anchor="middle" x="672.06" y="-811.38" font-family="DejaVu Sans Mono, Book" font-size="14.00">gw1</text>
|
||||
</g>
|
||||
<!-- host--gw1 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:mgmt1--gw1:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M108,-471.04C108,-471.04 585.06,-850.08 585.06,-850.08"/>
|
||||
</g>
|
||||
<!-- host--gw1 -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>host:lan--gw1:uplink</title>
|
||||
<path fill="none" stroke="black" stroke-width="2" d="M108,-379.04C108,-379.04 585.06,-827.08 585.06,-827.08"/>
|
||||
</g>
|
||||
<!-- gw2 -->
|
||||
<g id="node3" class="node">
|
||||
<title>gw2</title>
|
||||
<polygon fill="none" stroke="black" points="886.09,-390.54 886.09,-459.54 977.09,-459.54 977.09,-390.54 886.09,-390.54"/>
|
||||
<text text-anchor="middle" x="911.09" y="-444.34" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="886.09,-436.54 936.09,-436.54 "/>
|
||||
<text text-anchor="middle" x="911.09" y="-421.34" font-family="DejaVu Sans Mono, Book" font-size="14.00">mesh</text>
|
||||
<polyline fill="none" stroke="black" points="886.09,-413.54 936.09,-413.54 "/>
|
||||
<text text-anchor="middle" x="911.09" y="-398.34" font-family="DejaVu Sans Mono, Book" font-size="14.00">ap</text>
|
||||
<polyline fill="none" stroke="black" points="936.09,-390.54 936.09,-459.54 "/>
|
||||
<text text-anchor="middle" x="956.59" y="-421.34" font-family="DejaVu Sans Mono, Book" font-size="14.00">gw2</text>
|
||||
</g>
|
||||
<!-- host--gw2 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>host:mgmt2--gw2:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M108,-448.04C108,-448.04 885.59,-448.04 885.59,-448.04"/>
|
||||
</g>
|
||||
<!-- gw3 -->
|
||||
<g id="node4" class="node">
|
||||
<title>gw3</title>
|
||||
<polygon fill="none" stroke="black" points="593.56,-0.5 593.56,-69.5 684.56,-69.5 684.56,-0.5 593.56,-0.5"/>
|
||||
<text text-anchor="middle" x="618.56" y="-54.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="593.56,-46.5 643.56,-46.5 "/>
|
||||
<text text-anchor="middle" x="618.56" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mesh</text>
|
||||
<polyline fill="none" stroke="black" points="593.56,-23.5 643.56,-23.5 "/>
|
||||
<text text-anchor="middle" x="618.56" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">ap</text>
|
||||
<polyline fill="none" stroke="black" points="643.56,-0.5 643.56,-69.5 "/>
|
||||
<text text-anchor="middle" x="664.06" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">gw3</text>
|
||||
</g>
|
||||
<!-- host--gw3 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>host:mgmt3--gw3:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M108,-425.04C108,-425.04 593.06,-58 593.06,-58"/>
|
||||
</g>
|
||||
<!-- client -->
|
||||
<g id="node5" class="node">
|
||||
<title>client</title>
|
||||
<polygon fill="none" stroke="black" points="1263.63,-402.04 1263.63,-448.04 1379.63,-448.04 1379.63,-402.04 1263.63,-402.04"/>
|
||||
<text text-anchor="middle" x="1288.63" y="-432.84" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="1263.63,-425.04 1313.63,-425.04 "/>
|
||||
<text text-anchor="middle" x="1288.63" y="-409.84" font-family="DejaVu Sans Mono, Book" font-size="14.00">wifi</text>
|
||||
<polyline fill="none" stroke="black" points="1313.63,-402.04 1313.63,-448.04 "/>
|
||||
<text text-anchor="middle" x="1346.63" y="-421.34" font-family="DejaVu Sans Mono, Book" font-size="14.00">client</text>
|
||||
</g>
|
||||
<!-- host--client -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>host:mgmt4--client:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M108,-402.04C108,-402.04 1263.63,-437.04 1263.63,-437.04"/>
|
||||
</g>
|
||||
<!-- mesh -->
|
||||
<g id="node6" class="node">
|
||||
<title>mesh</title>
|
||||
<polygon fill="none" stroke="black" points="593.56,-407.04 593.56,-443.04 684.56,-443.04 684.56,-407.04 593.56,-407.04"/>
|
||||
<text text-anchor="middle" x="639.06" y="-421.34" font-family="DejaVu Sans Mono, Book" font-size="14.00">mesh-cell</text>
|
||||
</g>
|
||||
<!-- gw1--mesh -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>gw1:mesh--mesh</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" stroke-dasharray="5,2" d="M585.06,-803.08C585.06,-803.08 625.67,-518.79 636.45,-443.27"/>
|
||||
</g>
|
||||
<!-- ap -->
|
||||
<g id="node7" class="node">
|
||||
<title>ap</title>
|
||||
<polygon fill="none" stroke="black" points="1089.61,-407.04 1089.61,-443.04 1163.61,-443.04 1163.61,-407.04 1089.61,-407.04"/>
|
||||
<text text-anchor="middle" x="1126.61" y="-421.34" font-family="DejaVu Sans Mono, Book" font-size="14.00">ap-cell</text>
|
||||
</g>
|
||||
<!-- gw1--ap -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>gw1:ap--ap</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" stroke-dasharray="5,2" d="M618.06,-769.08C618.06,-769.08 995.34,-513.84 1099.98,-443.05"/>
|
||||
</g>
|
||||
<!-- gw2--mesh -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>gw2:mesh--mesh</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" stroke-dasharray="5,2" d="M885.59,-425.04C885.59,-425.04 755.8,-425.04 684.56,-425.04"/>
|
||||
</g>
|
||||
<!-- gw2--ap -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>gw2:ap--ap</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" stroke-dasharray="5,2" d="M910.59,-390.04C910.59,-390.04 1027.71,-409.02 1089.47,-419.02"/>
|
||||
</g>
|
||||
<!-- gw3--mesh -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>gw3:mesh--mesh</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" stroke-dasharray="5,2" d="M593.06,-35C593.06,-35 627.89,-330.3 636.93,-407.01"/>
|
||||
</g>
|
||||
<!-- gw3--ap -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>gw3:ap--ap</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" stroke-dasharray="5,2" d="M618.06,0C618.06,0 1007.42,-325.42 1104.76,-406.78"/>
|
||||
</g>
|
||||
<!-- client--ap -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>client:wifi--ap</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" stroke-dasharray="5,2" d="M1263.63,-413.04C1263.63,-413.04 1204.27,-418.24 1163.69,-421.79"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.6 KiB |
@@ -44,10 +44,14 @@ class Topology:
|
||||
self.g.add_node(name, **attrs)
|
||||
|
||||
for e in self.dotg.get_edges():
|
||||
sn, sp = e.get_source().split(":")
|
||||
dn, dp = e.get_destination().split(":")
|
||||
|
||||
attrs = {_qstrip(k): _qstrip(v) for k, v in e.get_attributes().items()}
|
||||
|
||||
# An endpoint may be port-less, e.g. a medium node that models a
|
||||
# shared wireless cell ("dut1:radio0 -- cell0"): the cell is just a
|
||||
# node every radio joins, it carries no port of its own.
|
||||
sn, _, sp = e.get_source().partition(":")
|
||||
dn, _, dp = e.get_destination().partition(":")
|
||||
|
||||
attrs[sn] = sp
|
||||
attrs[dn] = dp
|
||||
|
||||
|
||||
+1
-1
Submodule test/qeneth updated: 332a376bc0...e501ffedcc
@@ -15,6 +15,15 @@ truncate -s 0 $imgdir/{{name}}.mactab
|
||||
echo "{{qn_name}} {{qn_mac}}" >>$imgdir/{{name}}.mactab
|
||||
{{/links}}
|
||||
|
||||
# Number of mac80211_hwsim radios for this node = radioN carrier ports the
|
||||
# topology wired to it. Read at boot by 00-hwsim (fw_cfg opt/wifi); 0 means
|
||||
# no radios, so the node stays wifi-free.
|
||||
radios=0
|
||||
{{#links}}
|
||||
case "{{qn_name}}" in radio[0-9]*) radios=$((radios + 1));; esac
|
||||
{{/links}}
|
||||
echo "$radios" > $imgdir/{{name}}.wifi
|
||||
|
||||
{{#qn_gps}}
|
||||
mkfifo /tmp/{{name}}-gps.in /tmp/{{name}}-gps.out 2>/dev/null
|
||||
mkfifo /tmp/{{name}}-gps1.in /tmp/{{name}}-gps1.out 2>/dev/null
|
||||
@@ -24,6 +33,7 @@ exec qemu-system-x86_64 -M pc,accel=kvm:tcg -cpu max \
|
||||
-m {{#qn_mem}}{{qn_mem}}{{/qn_mem}}{{^qn_mem}}256M{{/qn_mem}} \
|
||||
{{> ../qeneth/templates/inc/qemu-links}}
|
||||
-fw_cfg name=opt/mactab,file=$imgdir/{{name}}.mactab \
|
||||
-fw_cfg name=opt/wifi,file=$imgdir/{{name}}.wifi \
|
||||
-bios $bios \
|
||||
-drive file={{name}}.qcow2,if=virtio,format=qcow2,bus=0,unit=1 \
|
||||
$usb_cmd \
|
||||
|
||||
@@ -18,6 +18,15 @@ truncate -s 0 $imgdir/{{name}}.mactab
|
||||
echo "{{qn_name}} {{qn_mac}}" >>$imgdir/{{name}}.mactab
|
||||
{{/links}}
|
||||
|
||||
# Number of mac80211_hwsim radios for this node = radioN carrier ports the
|
||||
# topology wired to it. Read at boot by 00-hwsim (fw_cfg opt/wifi); 0 means
|
||||
# no radios, so the node stays wifi-free.
|
||||
radios=0
|
||||
{{#links}}
|
||||
case "{{qn_name}}" in radio[0-9]*) radios=$((radios + 1));; esac
|
||||
{{/links}}
|
||||
echo "$radios" > $imgdir/{{name}}.wifi
|
||||
|
||||
exec qemu-system-x86_64 -M pc,accel=kvm:tcg -cpu max \
|
||||
-m {{#qn_mem}}{{qn_mem}}{{/qn_mem}}{{^qn_mem}}384M{{/qn_mem}} \
|
||||
-kernel "bzImage" -initrd $img \
|
||||
@@ -25,6 +34,7 @@ exec qemu-system-x86_64 -M pc,accel=kvm:tcg -cpu max \
|
||||
-drive file={{name}}.disk,if=virtio,format=raw,bus=0,unit=1 \
|
||||
{{> ../qeneth/templates/inc/qemu-links}}
|
||||
-fw_cfg name=opt/mactab,file=$imgdir/{{name}}.mactab \
|
||||
-fw_cfg name=opt/wifi,file=$imgdir/{{name}}.wifi \
|
||||
$usb_cmd \
|
||||
{{> ../qeneth/templates/inc/infix-common}}
|
||||
{{> ../qeneth/templates/inc/qemu-console}}
|
||||
|
||||
@@ -18,6 +18,15 @@ truncate -s 0 $imgdir/{{name}}.mactab
|
||||
echo "{{qn_name}} {{qn_mac}}" >>$imgdir/{{name}}.mactab
|
||||
{{/links}}
|
||||
|
||||
# Number of mac80211_hwsim radios for this node = radioN carrier ports the
|
||||
# topology wired to it. Read at boot by 00-hwsim (fw_cfg opt/wifi); 0 means
|
||||
# no radios, so the node stays wifi-free.
|
||||
radios=0
|
||||
{{#links}}
|
||||
case "{{qn_name}}" in radio[0-9]*) radios=$((radios + 1));; esac
|
||||
{{/links}}
|
||||
echo "$radios" > $imgdir/{{name}}.wifi
|
||||
|
||||
exec qemu-system-x86_64 -M pc,accel=kvm:tcg -cpu max \
|
||||
-m {{#qn_mem}}{{qn_mem}}{{/qn_mem}}{{^qn_mem}}384M{{/qn_mem}} \
|
||||
-kernel $imgdir/boot/bzImage -initrd $img \
|
||||
@@ -25,5 +34,6 @@ exec qemu-system-x86_64 -M pc,accel=kvm:tcg -cpu max \
|
||||
-drive file={{name}}.disk,if=virtio,format=raw,bus=0,unit=1 \
|
||||
{{> inc/qemu-links}}
|
||||
-fw_cfg name=opt/mactab,file=$imgdir/{{name}}.mactab \
|
||||
-fw_cfg name=opt/wifi,file=$imgdir/{{name}}.wifi \
|
||||
$usb_cmd \
|
||||
{{> inc/infix-common}}
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ xpaths_all_csv := $(test-dir)/.log/xpaths_all.csv
|
||||
base := -b $(base-dir)
|
||||
|
||||
TEST_MODE ?= qeneth
|
||||
mode-qeneth := -q $(test-dir)/virt/quad
|
||||
mode-qeneth := -q $(or $(QTOPOLOGY),$(test-dir)/virt/quad)
|
||||
mode-host := -t $(or $(TOPOLOGY),/etc/infamy.dot)
|
||||
mode-run := -t $(BINARIES_DIR)/qemu.dot
|
||||
mode := $(mode-$(TEST_MODE))
|
||||
|
||||
@@ -21,28 +21,34 @@ graph "quad" {
|
||||
];
|
||||
|
||||
dut1 [
|
||||
label="{ <e1> e1 | <e2> e2 | <e3> e3 | <e4> e4 } | dut1 | { <e5> e5 | <e6> e6 | <e7> e7 | <e8> e8}",
|
||||
label="{ <e1> e1 | <e2> e2 | <e3> e3 | <e4> e4 } | dut1 | { <e5> e5 | <e6> e6 | <e7> e7 | <e8> e8 } | { <radio0> radio0 | <radio1> radio1 | <radio2> radio2 | <radio3> radio3 }",
|
||||
pos="10,30!",
|
||||
provides="infix watchdog gps",
|
||||
provides="infix watchdog gps wifi wifi-dualband",
|
||||
expected_boot="primary",
|
||||
qn_console=9001,
|
||||
qn_mem="384M",
|
||||
qn_usb="dut1.usb",
|
||||
qn_gps="true"
|
||||
// NB: dut1 has two extra radios, radio2 and radio3, wired to the
|
||||
// dedicated cell2 (below). With both on one cell this DUT runs a
|
||||
// dual-band AP whose 2.4GHz and 5GHz BSSes share a single cell -- the
|
||||
// only way a single-radio client can hear both bands and be band-
|
||||
// steered (wifi_band_steering). radio0/radio1 stay on the general
|
||||
// cells. The hwsim radio count is raised in package/feature-wifi.
|
||||
];
|
||||
dut2 [
|
||||
label="{ <e1> e1 | <e2> e2 | <e3> e3 | <e4> e4 } | dut2 | { <e5> e5 | <e6> e6 | <e7> e7 | <e8> e8}",
|
||||
label="{ <e1> e1 | <e2> e2 | <e3> e3 | <e4> e4 } | dut2 | { <e5> e5 | <e6> e6 | <e7> e7 | <e8> e8 } | { <radio0> radio0 | <radio1> radio1 | <radio2> radio2 }",
|
||||
pos="0,20!",
|
||||
provides="infix watchdog",
|
||||
provides="infix watchdog wifi",
|
||||
expected_boot="primary",
|
||||
qn_console=9002,
|
||||
qn_mem="384M",
|
||||
qn_usb="dut2.usb"
|
||||
];
|
||||
dut3 [
|
||||
label="{ <e1> e1 | <e2> e2 | <e3> e3 | <e4> e4 } | dut3 | { <e5> e5 | <e6> e6 | <e7> e7 | <e8> e8}",
|
||||
label="{ <e1> e1 | <e2> e2 | <e3> e3 | <e4> e4 } | dut3 | { <e5> e5 | <e6> e6 | <e7> e7 | <e8> e8 } | { <radio0> radio0 | <radio1> radio1 }",
|
||||
pos="0,10!",
|
||||
provides="infix watchdog",
|
||||
provides="infix watchdog wifi",
|
||||
expected_boot="primary",
|
||||
qn_console=9003,
|
||||
qn_mem="384M",
|
||||
@@ -50,15 +56,30 @@ graph "quad" {
|
||||
];
|
||||
|
||||
dut4 [
|
||||
label="{ <e1> e1 | <e2> e2 | <e3> e3 | <e4> e4 } | dut4 | { <e5> e5 | <e6> e6 | <e7> e7 | <e8> e8}",
|
||||
label="{ <e1> e1 | <e2> e2 | <e3> e3 | <e4> e4 } | dut4 | { <e5> e5 | <e6> e6 | <e7> e7 | <e8> e8 } | { <radio0> radio0 | <radio1> radio1 }",
|
||||
pos="10,0!",
|
||||
provides="infix watchdog",
|
||||
provides="infix watchdog wifi",
|
||||
expected_boot="primary",
|
||||
qn_console=9004,
|
||||
qn_mem="384M",
|
||||
qn_usb="dut4.usb"
|
||||
];
|
||||
|
||||
// Wireless cells. A node with qn_hub="true" is a shared multicast
|
||||
// medium (one QEMU socket multicast group), not a guest: every radio
|
||||
// wired to it is "in range" of every other, like a real RF cell.
|
||||
|
||||
cell0 [label="cell0\n(radio0)", provides="wifi-radio", qn_hub="true",
|
||||
pos="-6,22!", color="cornflowerblue", fontcolor="cornflowerblue", style="dashed"];
|
||||
cell1 [label="cell1\n(radio1)", provides="wifi-radio", qn_hub="true",
|
||||
pos="-6,8!", color="cornflowerblue", fontcolor="cornflowerblue", style="dashed"];
|
||||
// Dedicated band-steering cell, separate from the general cells above so
|
||||
// the AP's shared SSID never leaks onto cell0/cell1. dut1 wires two
|
||||
// radios here for a dual-band AP (radio2=2.4GHz, radio3=5GHz) and dut2
|
||||
// joins with a single dual-band radio (radio2) as the client.
|
||||
cell2 [label="cell2\n(band steering)", provides="wifi-radio", qn_hub="true",
|
||||
pos="-6,15!", color="mediumseagreen", fontcolor="mediumseagreen", style="dashed"];
|
||||
|
||||
host:d1a -- dut1:e1 [provides="mgmt", color="lightgray"]
|
||||
host:d1b -- dut1:e2 [provides="ieee-mc"]
|
||||
host:d1c -- dut1:e3 [provides="ieee-mc"]
|
||||
@@ -79,7 +100,10 @@ graph "quad" {
|
||||
host:d4c -- dut4:e3 [provides="ieee-mc"]
|
||||
host:d4d -- dut4:e4 [provides="ieee-mc"]
|
||||
|
||||
// Lag
|
||||
// Wired inter-DUT fabric (full mesh): ring + cross-links + a two-link
|
||||
// lag between dut2 and dut3. General-purpose links the non-wifi
|
||||
// multi-DUT tests map onto; kept plain (wifi rides the cells above).
|
||||
// Lag
|
||||
dut2:e7 -- dut3:e6 [color="black", penwidth="3"]
|
||||
dut2:e8 -- dut3:e5 [color="black", penwidth="3"]
|
||||
|
||||
@@ -91,4 +115,31 @@ graph "quad" {
|
||||
// Cross-links
|
||||
dut1:e6 -- dut3:e7 [color="red"]
|
||||
dut2:e6 -- dut4:e7 [color="red"]
|
||||
|
||||
// Wireless adjacency: every radio joins its cell, so all radios on the
|
||||
// same index hear each other (full mesh per cell). The carrier port is
|
||||
// named after the radio, so it becomes the Linux netdev radioN that the
|
||||
// wifimedium relay pairs with phy radioN.
|
||||
// provides lists the bands each radio supports, one wifi<band> token per
|
||||
// band (mac80211_hwsim is tri-band). On real hardware a band-specific
|
||||
// radio advertises only its own bands, so a test requiring e.g. wifi5GHz
|
||||
// maps only onto a 5GHz-capable radio.
|
||||
dut1:radio0 -- cell0 [provides="wifi2.4GHz wifi5GHz wifi6GHz"]
|
||||
dut2:radio0 -- cell0 [provides="wifi2.4GHz wifi5GHz wifi6GHz"]
|
||||
dut3:radio0 -- cell0 [provides="wifi2.4GHz wifi5GHz wifi6GHz"]
|
||||
dut4:radio0 -- cell0 [provides="wifi2.4GHz wifi5GHz wifi6GHz"]
|
||||
|
||||
dut1:radio1 -- cell1 [provides="wifi2.4GHz wifi5GHz wifi6GHz"]
|
||||
dut2:radio1 -- cell1 [provides="wifi2.4GHz wifi5GHz wifi6GHz"]
|
||||
dut3:radio1 -- cell1 [provides="wifi2.4GHz wifi5GHz wifi6GHz"]
|
||||
dut4:radio1 -- cell1 [provides="wifi2.4GHz wifi5GHz wifi6GHz"]
|
||||
|
||||
// Band-steering cell: dut1's two extra radios form a dual-band AP
|
||||
// (radio2=2.4GHz, radio3=5GHz) whose BSSes share this one cell, and
|
||||
// dut2's extra radio (radio2) is the single-radio client that hears
|
||||
// both bands. Only dut1 has two radios here, so it is the sole node the
|
||||
// dual-band AP can map onto (wifi_band_steering).
|
||||
dut1:radio2 -- cell2 [provides="wifi2.4GHz wifi5GHz wifi6GHz"]
|
||||
dut1:radio3 -- cell2 [provides="wifi2.4GHz wifi5GHz wifi6GHz"]
|
||||
dut2:radio2 -- cell2 [provides="wifi2.4GHz wifi5GHz wifi6GHz"]
|
||||
}
|
||||
|
||||
+231
-133
@@ -4,251 +4,349 @@
|
||||
<!-- Generated by graphviz version 2.43.0 (0)
|
||||
-->
|
||||
<!-- Title: quad Pages: 1 -->
|
||||
<svg width="542pt" height="560pt"
|
||||
viewBox="0.00 0.00 541.97 560.05" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 556.05)">
|
||||
<svg width="1001pt" height="1199pt"
|
||||
viewBox="0.00 0.00 1000.71 1199.35" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1195.35)">
|
||||
<title>quad</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-556.05 537.97,-556.05 537.97,4 -4,4"/>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-1195.35 996.71,-1195.35 996.71,4 -4,4"/>
|
||||
<!-- host -->
|
||||
<g id="node1" class="node">
|
||||
<title>host</title>
|
||||
<polygon fill="none" stroke="grey" points="0,-92.02 0,-460.02 91,-460.02 91,-92.02 0,-92.02"/>
|
||||
<text text-anchor="middle" x="25" y="-272.32" font-family="monospace" font-size="14.00" fill="grey">host</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-92.02 50,-460.02 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-444.82" font-family="monospace" font-size="14.00" fill="grey">d1a</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-437.02 91,-437.02 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-421.82" font-family="monospace" font-size="14.00" fill="grey">d1b</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-414.02 91,-414.02 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-398.82" font-family="monospace" font-size="14.00" fill="grey">d1c</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-391.02 91,-391.02 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-375.82" font-family="monospace" font-size="14.00" fill="grey">d1d</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-368.02 91,-368.02 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-352.82" font-family="monospace" font-size="14.00" fill="grey">d2a</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-345.02 91,-345.02 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-329.82" font-family="monospace" font-size="14.00" fill="grey">d2b</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-322.02 91,-322.02 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-306.82" font-family="monospace" font-size="14.00" fill="grey">d2c</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-299.02 91,-299.02 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-283.82" font-family="monospace" font-size="14.00" fill="grey">d2d</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-276.02 91,-276.02 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-260.82" font-family="monospace" font-size="14.00" fill="grey">d3a</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-253.02 91,-253.02 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-237.82" font-family="monospace" font-size="14.00" fill="grey">d3b</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-230.02 91,-230.02 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-214.82" font-family="monospace" font-size="14.00" fill="grey">d3c</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-207.02 91,-207.02 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-191.82" font-family="monospace" font-size="14.00" fill="grey">d3d</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-184.02 91,-184.02 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-168.82" font-family="monospace" font-size="14.00" fill="grey">d4a</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-161.02 91,-161.02 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-145.82" font-family="monospace" font-size="14.00" fill="grey">d4b</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-138.02 91,-138.02 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-122.82" font-family="monospace" font-size="14.00" fill="grey">d4c</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-115.02 91,-115.02 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-99.82" font-family="monospace" font-size="14.00" fill="grey">d4d</text>
|
||||
<polygon fill="none" stroke="grey" points="0,-404.69 0,-772.69 91,-772.69 91,-404.69 0,-404.69"/>
|
||||
<text text-anchor="middle" x="25" y="-584.99" font-family="monospace" font-size="14.00" fill="grey">host</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-404.69 50,-772.69 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-757.49" font-family="monospace" font-size="14.00" fill="grey">d1a</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-749.69 91,-749.69 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-734.49" font-family="monospace" font-size="14.00" fill="grey">d1b</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-726.69 91,-726.69 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-711.49" font-family="monospace" font-size="14.00" fill="grey">d1c</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-703.69 91,-703.69 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-688.49" font-family="monospace" font-size="14.00" fill="grey">d1d</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-680.69 91,-680.69 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-665.49" font-family="monospace" font-size="14.00" fill="grey">d2a</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-657.69 91,-657.69 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-642.49" font-family="monospace" font-size="14.00" fill="grey">d2b</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-634.69 91,-634.69 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-619.49" font-family="monospace" font-size="14.00" fill="grey">d2c</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-611.69 91,-611.69 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-596.49" font-family="monospace" font-size="14.00" fill="grey">d2d</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-588.69 91,-588.69 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-573.49" font-family="monospace" font-size="14.00" fill="grey">d3a</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-565.69 91,-565.69 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-550.49" font-family="monospace" font-size="14.00" fill="grey">d3b</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-542.69 91,-542.69 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-527.49" font-family="monospace" font-size="14.00" fill="grey">d3c</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-519.69 91,-519.69 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-504.49" font-family="monospace" font-size="14.00" fill="grey">d3d</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-496.69 91,-496.69 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-481.49" font-family="monospace" font-size="14.00" fill="grey">d4a</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-473.69 91,-473.69 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-458.49" font-family="monospace" font-size="14.00" fill="grey">d4b</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-450.69 91,-450.69 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-435.49" font-family="monospace" font-size="14.00" fill="grey">d4c</text>
|
||||
<polyline fill="none" stroke="grey" points="50,-427.69 91,-427.69 "/>
|
||||
<text text-anchor="middle" x="70.5" y="-412.49" font-family="monospace" font-size="14.00" fill="grey">d4d</text>
|
||||
</g>
|
||||
<!-- dut1 -->
|
||||
<g id="node2" class="node">
|
||||
<title>dut1</title>
|
||||
<polygon fill="none" stroke="black" points="370.04,-459.55 370.04,-551.55 486.04,-551.55 486.04,-459.55 370.04,-459.55"/>
|
||||
<text text-anchor="middle" x="386.54" y="-536.35" font-family="monospace" font-size="14.00">e1</text>
|
||||
<polyline fill="none" stroke="black" points="370.04,-528.55 403.04,-528.55 "/>
|
||||
<text text-anchor="middle" x="386.54" y="-513.35" font-family="monospace" font-size="14.00">e2</text>
|
||||
<polyline fill="none" stroke="black" points="370.04,-505.55 403.04,-505.55 "/>
|
||||
<text text-anchor="middle" x="386.54" y="-490.35" font-family="monospace" font-size="14.00">e3</text>
|
||||
<polyline fill="none" stroke="black" points="370.04,-482.55 403.04,-482.55 "/>
|
||||
<text text-anchor="middle" x="386.54" y="-467.35" font-family="monospace" font-size="14.00">e4</text>
|
||||
<polyline fill="none" stroke="black" points="403.04,-459.55 403.04,-551.55 "/>
|
||||
<text text-anchor="middle" x="428.04" y="-501.85" font-family="monospace" font-size="14.00">dut1</text>
|
||||
<polyline fill="none" stroke="black" points="453.04,-459.55 453.04,-551.55 "/>
|
||||
<text text-anchor="middle" x="469.54" y="-536.35" font-family="monospace" font-size="14.00">e5</text>
|
||||
<polyline fill="none" stroke="black" points="453.04,-528.55 486.04,-528.55 "/>
|
||||
<text text-anchor="middle" x="469.54" y="-513.35" font-family="monospace" font-size="14.00">e6</text>
|
||||
<polyline fill="none" stroke="black" points="453.04,-505.55 486.04,-505.55 "/>
|
||||
<text text-anchor="middle" x="469.54" y="-490.35" font-family="monospace" font-size="14.00">e7</text>
|
||||
<polyline fill="none" stroke="black" points="453.04,-482.55 486.04,-482.55 "/>
|
||||
<text text-anchor="middle" x="469.54" y="-467.35" font-family="monospace" font-size="14.00">e8</text>
|
||||
<polygon fill="none" stroke="black" points="756.66,-1023.99 756.66,-1115.99 938.66,-1115.99 938.66,-1023.99 756.66,-1023.99"/>
|
||||
<text text-anchor="middle" x="773.16" y="-1100.79" font-family="monospace" font-size="14.00">e1</text>
|
||||
<polyline fill="none" stroke="black" points="756.66,-1092.99 789.66,-1092.99 "/>
|
||||
<text text-anchor="middle" x="773.16" y="-1077.79" font-family="monospace" font-size="14.00">e2</text>
|
||||
<polyline fill="none" stroke="black" points="756.66,-1069.99 789.66,-1069.99 "/>
|
||||
<text text-anchor="middle" x="773.16" y="-1054.79" font-family="monospace" font-size="14.00">e3</text>
|
||||
<polyline fill="none" stroke="black" points="756.66,-1046.99 789.66,-1046.99 "/>
|
||||
<text text-anchor="middle" x="773.16" y="-1031.79" font-family="monospace" font-size="14.00">e4</text>
|
||||
<polyline fill="none" stroke="black" points="789.66,-1023.99 789.66,-1115.99 "/>
|
||||
<text text-anchor="middle" x="814.66" y="-1066.29" font-family="monospace" font-size="14.00">dut1</text>
|
||||
<polyline fill="none" stroke="black" points="839.66,-1023.99 839.66,-1115.99 "/>
|
||||
<text text-anchor="middle" x="856.16" y="-1100.79" font-family="monospace" font-size="14.00">e5</text>
|
||||
<polyline fill="none" stroke="black" points="839.66,-1092.99 872.66,-1092.99 "/>
|
||||
<text text-anchor="middle" x="856.16" y="-1077.79" font-family="monospace" font-size="14.00">e6</text>
|
||||
<polyline fill="none" stroke="black" points="839.66,-1069.99 872.66,-1069.99 "/>
|
||||
<text text-anchor="middle" x="856.16" y="-1054.79" font-family="monospace" font-size="14.00">e7</text>
|
||||
<polyline fill="none" stroke="black" points="839.66,-1046.99 872.66,-1046.99 "/>
|
||||
<text text-anchor="middle" x="856.16" y="-1031.79" font-family="monospace" font-size="14.00">e8</text>
|
||||
<polyline fill="none" stroke="black" points="872.66,-1023.99 872.66,-1115.99 "/>
|
||||
<text text-anchor="middle" x="905.66" y="-1100.79" font-family="monospace" font-size="14.00">radio0</text>
|
||||
<polyline fill="none" stroke="black" points="872.66,-1092.99 938.66,-1092.99 "/>
|
||||
<text text-anchor="middle" x="905.66" y="-1077.79" font-family="monospace" font-size="14.00">radio1</text>
|
||||
<polyline fill="none" stroke="black" points="872.66,-1069.99 938.66,-1069.99 "/>
|
||||
<text text-anchor="middle" x="905.66" y="-1054.79" font-family="monospace" font-size="14.00">radio2</text>
|
||||
<polyline fill="none" stroke="black" points="872.66,-1046.99 938.66,-1046.99 "/>
|
||||
<text text-anchor="middle" x="905.66" y="-1031.79" font-family="monospace" font-size="14.00">radio3</text>
|
||||
</g>
|
||||
<!-- host--dut1 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:d1a--dut1:e1</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M91.5,-449.02C91.5,-449.02 370.04,-540.55 370.04,-540.55"/>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M91.5,-761.69C91.5,-761.69 307.4,-665.13 405.78,-764.3 405.82,-764.33 405.78,-825.07 405.8,-825.12 449.08,-914.98 756.66,-1104.99 756.66,-1104.99"/>
|
||||
</g>
|
||||
<!-- host--dut1 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>host:d1b--dut1:e2</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M91.5,-426.02C91.5,-426.02 370.04,-517.55 370.04,-517.55"/>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M91.5,-738.69C91.5,-738.69 310.78,-661.27 405.78,-764.3 405.81,-764.33 405.77,-825.07 405.8,-825.12 493.25,-997.46 756.66,-1081.99 756.66,-1081.99"/>
|
||||
</g>
|
||||
<!-- host--dut1 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>host:d1c--dut1:e3</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M91.5,-403.02C91.5,-403.02 370.04,-493.55 370.04,-493.55"/>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M91.5,-715.69C91.5,-715.69 313.77,-657 405.78,-764.3 405.81,-764.34 405.77,-825.08 405.8,-825.12 494.31,-990.03 756.66,-1057.99 756.66,-1057.99"/>
|
||||
</g>
|
||||
<!-- host--dut1 -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>host:d1d--dut1:e4</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M91.5,-380.02C91.5,-380.02 142.26,-412.3 187.02,-428.53 265.48,-456.97 370.04,-470.55 370.04,-470.55"/>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M91.5,-692.69C91.5,-692.69 316.42,-652.32 405.78,-764.3 405.81,-764.34 405.77,-825.08 405.8,-825.12 495.46,-983.17 756.66,-1034.99 756.66,-1034.99"/>
|
||||
</g>
|
||||
<!-- dut2 -->
|
||||
<g id="node3" class="node">
|
||||
<title>dut2</title>
|
||||
<polygon fill="none" stroke="black" points="217.02,-306.53 217.02,-398.53 333.02,-398.53 333.02,-306.53 217.02,-306.53"/>
|
||||
<text text-anchor="middle" x="233.52" y="-383.33" font-family="monospace" font-size="14.00">e1</text>
|
||||
<polyline fill="none" stroke="black" points="217.02,-375.53 250.02,-375.53 "/>
|
||||
<text text-anchor="middle" x="233.52" y="-360.33" font-family="monospace" font-size="14.00">e2</text>
|
||||
<polyline fill="none" stroke="black" points="217.02,-352.53 250.02,-352.53 "/>
|
||||
<text text-anchor="middle" x="233.52" y="-337.33" font-family="monospace" font-size="14.00">e3</text>
|
||||
<polyline fill="none" stroke="black" points="217.02,-329.53 250.02,-329.53 "/>
|
||||
<text text-anchor="middle" x="233.52" y="-314.33" font-family="monospace" font-size="14.00">e4</text>
|
||||
<polyline fill="none" stroke="black" points="250.02,-306.53 250.02,-398.53 "/>
|
||||
<text text-anchor="middle" x="275.02" y="-348.83" font-family="monospace" font-size="14.00">dut2</text>
|
||||
<polyline fill="none" stroke="black" points="300.02,-306.53 300.02,-398.53 "/>
|
||||
<text text-anchor="middle" x="316.52" y="-383.33" font-family="monospace" font-size="14.00">e5</text>
|
||||
<polyline fill="none" stroke="black" points="300.02,-375.53 333.02,-375.53 "/>
|
||||
<text text-anchor="middle" x="316.52" y="-360.33" font-family="monospace" font-size="14.00">e6</text>
|
||||
<polyline fill="none" stroke="black" points="300.02,-352.53 333.02,-352.53 "/>
|
||||
<text text-anchor="middle" x="316.52" y="-337.33" font-family="monospace" font-size="14.00">e7</text>
|
||||
<polyline fill="none" stroke="black" points="300.02,-329.53 333.02,-329.53 "/>
|
||||
<text text-anchor="middle" x="316.52" y="-314.33" font-family="monospace" font-size="14.00">e8</text>
|
||||
<polygon fill="none" stroke="black" points="435.8,-703.12 435.8,-795.12 617.8,-795.12 617.8,-703.12 435.8,-703.12"/>
|
||||
<text text-anchor="middle" x="452.3" y="-779.92" font-family="monospace" font-size="14.00">e1</text>
|
||||
<polyline fill="none" stroke="black" points="435.8,-772.12 468.8,-772.12 "/>
|
||||
<text text-anchor="middle" x="452.3" y="-756.92" font-family="monospace" font-size="14.00">e2</text>
|
||||
<polyline fill="none" stroke="black" points="435.8,-749.12 468.8,-749.12 "/>
|
||||
<text text-anchor="middle" x="452.3" y="-733.92" font-family="monospace" font-size="14.00">e3</text>
|
||||
<polyline fill="none" stroke="black" points="435.8,-726.12 468.8,-726.12 "/>
|
||||
<text text-anchor="middle" x="452.3" y="-710.92" font-family="monospace" font-size="14.00">e4</text>
|
||||
<polyline fill="none" stroke="black" points="468.8,-703.12 468.8,-795.12 "/>
|
||||
<text text-anchor="middle" x="493.8" y="-745.42" font-family="monospace" font-size="14.00">dut2</text>
|
||||
<polyline fill="none" stroke="black" points="518.8,-703.12 518.8,-795.12 "/>
|
||||
<text text-anchor="middle" x="535.3" y="-779.92" font-family="monospace" font-size="14.00">e5</text>
|
||||
<polyline fill="none" stroke="black" points="518.8,-772.12 551.8,-772.12 "/>
|
||||
<text text-anchor="middle" x="535.3" y="-756.92" font-family="monospace" font-size="14.00">e6</text>
|
||||
<polyline fill="none" stroke="black" points="518.8,-749.12 551.8,-749.12 "/>
|
||||
<text text-anchor="middle" x="535.3" y="-733.92" font-family="monospace" font-size="14.00">e7</text>
|
||||
<polyline fill="none" stroke="black" points="518.8,-726.12 551.8,-726.12 "/>
|
||||
<text text-anchor="middle" x="535.3" y="-710.92" font-family="monospace" font-size="14.00">e8</text>
|
||||
<polyline fill="none" stroke="black" points="551.8,-703.12 551.8,-795.12 "/>
|
||||
<text text-anchor="middle" x="584.8" y="-776.42" font-family="monospace" font-size="14.00">radio0</text>
|
||||
<polyline fill="none" stroke="black" points="551.8,-765.12 617.8,-765.12 "/>
|
||||
<text text-anchor="middle" x="584.8" y="-745.92" font-family="monospace" font-size="14.00">radio1</text>
|
||||
<polyline fill="none" stroke="black" points="551.8,-734.12 617.8,-734.12 "/>
|
||||
<text text-anchor="middle" x="584.8" y="-714.92" font-family="monospace" font-size="14.00">radio2</text>
|
||||
</g>
|
||||
<!-- host--dut2 -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>host:d2a--dut2:e1</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M91.5,-357.02C91.5,-357.02 217.02,-387.53 217.02,-387.53"/>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M91.5,-669.69C91.5,-669.69 273.68,-702.42 405.78,-764.3 420.26,-771.08 435.8,-784.12 435.8,-784.12"/>
|
||||
</g>
|
||||
<!-- host--dut2 -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>host:d2b--dut2:e2</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M91.5,-334.02C91.5,-334.02 217.02,-364.53 217.02,-364.53"/>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M91.5,-646.69C91.5,-646.69 435.8,-761.12 435.8,-761.12"/>
|
||||
</g>
|
||||
<!-- host--dut2 -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>host:d2c--dut2:e3</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M91.5,-311.02C91.5,-311.02 217.02,-340.53 217.02,-340.53"/>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M91.5,-623.69C91.5,-623.69 435.8,-737.12 435.8,-737.12"/>
|
||||
</g>
|
||||
<!-- host--dut2 -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>host:d2d--dut2:e4</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M91.5,-288.02C91.5,-288.02 217.02,-317.53 217.02,-317.53"/>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M91.5,-600.69C91.5,-600.69 435.8,-714.12 435.8,-714.12"/>
|
||||
</g>
|
||||
<!-- dut3 -->
|
||||
<g id="node4" class="node">
|
||||
<title>dut3</title>
|
||||
<polygon fill="none" stroke="black" points="217.02,-153.52 217.02,-245.52 333.02,-245.52 333.02,-153.52 217.02,-153.52"/>
|
||||
<text text-anchor="middle" x="233.52" y="-230.32" font-family="monospace" font-size="14.00">e1</text>
|
||||
<polyline fill="none" stroke="black" points="217.02,-222.52 250.02,-222.52 "/>
|
||||
<text text-anchor="middle" x="233.52" y="-207.32" font-family="monospace" font-size="14.00">e2</text>
|
||||
<polyline fill="none" stroke="black" points="217.02,-199.52 250.02,-199.52 "/>
|
||||
<text text-anchor="middle" x="233.52" y="-184.32" font-family="monospace" font-size="14.00">e3</text>
|
||||
<polyline fill="none" stroke="black" points="217.02,-176.52 250.02,-176.52 "/>
|
||||
<text text-anchor="middle" x="233.52" y="-161.32" font-family="monospace" font-size="14.00">e4</text>
|
||||
<polyline fill="none" stroke="black" points="250.02,-153.52 250.02,-245.52 "/>
|
||||
<text text-anchor="middle" x="275.02" y="-195.82" font-family="monospace" font-size="14.00">dut3</text>
|
||||
<polyline fill="none" stroke="black" points="300.02,-153.52 300.02,-245.52 "/>
|
||||
<text text-anchor="middle" x="316.52" y="-230.32" font-family="monospace" font-size="14.00">e5</text>
|
||||
<polyline fill="none" stroke="black" points="300.02,-222.52 333.02,-222.52 "/>
|
||||
<text text-anchor="middle" x="316.52" y="-207.32" font-family="monospace" font-size="14.00">e6</text>
|
||||
<polyline fill="none" stroke="black" points="300.02,-199.52 333.02,-199.52 "/>
|
||||
<text text-anchor="middle" x="316.52" y="-184.32" font-family="monospace" font-size="14.00">e7</text>
|
||||
<polyline fill="none" stroke="black" points="300.02,-176.52 333.02,-176.52 "/>
|
||||
<text text-anchor="middle" x="316.52" y="-161.32" font-family="monospace" font-size="14.00">e8</text>
|
||||
<polygon fill="none" stroke="black" points="435.8,-382.26 435.8,-474.26 617.8,-474.26 617.8,-382.26 435.8,-382.26"/>
|
||||
<text text-anchor="middle" x="452.3" y="-459.06" font-family="monospace" font-size="14.00">e1</text>
|
||||
<polyline fill="none" stroke="black" points="435.8,-451.26 468.8,-451.26 "/>
|
||||
<text text-anchor="middle" x="452.3" y="-436.06" font-family="monospace" font-size="14.00">e2</text>
|
||||
<polyline fill="none" stroke="black" points="435.8,-428.26 468.8,-428.26 "/>
|
||||
<text text-anchor="middle" x="452.3" y="-413.06" font-family="monospace" font-size="14.00">e3</text>
|
||||
<polyline fill="none" stroke="black" points="435.8,-405.26 468.8,-405.26 "/>
|
||||
<text text-anchor="middle" x="452.3" y="-390.06" font-family="monospace" font-size="14.00">e4</text>
|
||||
<polyline fill="none" stroke="black" points="468.8,-382.26 468.8,-474.26 "/>
|
||||
<text text-anchor="middle" x="493.8" y="-424.56" font-family="monospace" font-size="14.00">dut3</text>
|
||||
<polyline fill="none" stroke="black" points="518.8,-382.26 518.8,-474.26 "/>
|
||||
<text text-anchor="middle" x="535.3" y="-459.06" font-family="monospace" font-size="14.00">e5</text>
|
||||
<polyline fill="none" stroke="black" points="518.8,-451.26 551.8,-451.26 "/>
|
||||
<text text-anchor="middle" x="535.3" y="-436.06" font-family="monospace" font-size="14.00">e6</text>
|
||||
<polyline fill="none" stroke="black" points="518.8,-428.26 551.8,-428.26 "/>
|
||||
<text text-anchor="middle" x="535.3" y="-413.06" font-family="monospace" font-size="14.00">e7</text>
|
||||
<polyline fill="none" stroke="black" points="518.8,-405.26 551.8,-405.26 "/>
|
||||
<text text-anchor="middle" x="535.3" y="-390.06" font-family="monospace" font-size="14.00">e8</text>
|
||||
<polyline fill="none" stroke="black" points="551.8,-382.26 551.8,-474.26 "/>
|
||||
<text text-anchor="middle" x="584.8" y="-447.56" font-family="monospace" font-size="14.00">radio0</text>
|
||||
<polyline fill="none" stroke="black" points="551.8,-428.26 617.8,-428.26 "/>
|
||||
<text text-anchor="middle" x="584.8" y="-401.56" font-family="monospace" font-size="14.00">radio1</text>
|
||||
</g>
|
||||
<!-- host--dut3 -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>host:d3a--dut3:e1</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M91.5,-264.02C91.5,-264.02 217.02,-234.52 217.02,-234.52"/>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M91.5,-576.69C91.5,-576.69 435.8,-463.26 435.8,-463.26"/>
|
||||
</g>
|
||||
<!-- host--dut3 -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>host:d3b--dut3:e2</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M91.5,-241.02C91.5,-241.02 217.02,-211.52 217.02,-211.52"/>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M91.5,-553.69C91.5,-553.69 435.8,-440.26 435.8,-440.26"/>
|
||||
</g>
|
||||
<!-- host--dut3 -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>host:d3c--dut3:e3</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M91.5,-218.02C91.5,-218.02 217.02,-187.52 217.02,-187.52"/>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M91.5,-530.69C91.5,-530.69 435.8,-416.26 435.8,-416.26"/>
|
||||
</g>
|
||||
<!-- host--dut3 -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>host:d3d--dut3:e4</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M91.5,-195.02C91.5,-195.02 217.02,-164.52 217.02,-164.52"/>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M91.5,-507.69C91.5,-507.69 273.68,-474.96 405.78,-413.08 420.26,-406.3 435.8,-393.26 435.8,-393.26"/>
|
||||
</g>
|
||||
<!-- dut4 -->
|
||||
<g id="node5" class="node">
|
||||
<title>dut4</title>
|
||||
<polygon fill="none" stroke="black" points="370.04,-0.5 370.04,-92.5 486.04,-92.5 486.04,-0.5 370.04,-0.5"/>
|
||||
<text text-anchor="middle" x="386.54" y="-77.3" font-family="monospace" font-size="14.00">e1</text>
|
||||
<polyline fill="none" stroke="black" points="370.04,-69.5 403.04,-69.5 "/>
|
||||
<text text-anchor="middle" x="386.54" y="-54.3" font-family="monospace" font-size="14.00">e2</text>
|
||||
<polyline fill="none" stroke="black" points="370.04,-46.5 403.04,-46.5 "/>
|
||||
<text text-anchor="middle" x="386.54" y="-31.3" font-family="monospace" font-size="14.00">e3</text>
|
||||
<polyline fill="none" stroke="black" points="370.04,-23.5 403.04,-23.5 "/>
|
||||
<text text-anchor="middle" x="386.54" y="-8.3" font-family="monospace" font-size="14.00">e4</text>
|
||||
<polyline fill="none" stroke="black" points="403.04,-0.5 403.04,-92.5 "/>
|
||||
<text text-anchor="middle" x="428.04" y="-42.8" font-family="monospace" font-size="14.00">dut4</text>
|
||||
<polyline fill="none" stroke="black" points="453.04,-0.5 453.04,-92.5 "/>
|
||||
<text text-anchor="middle" x="469.54" y="-77.3" font-family="monospace" font-size="14.00">e5</text>
|
||||
<polyline fill="none" stroke="black" points="453.04,-69.5 486.04,-69.5 "/>
|
||||
<text text-anchor="middle" x="469.54" y="-54.3" font-family="monospace" font-size="14.00">e6</text>
|
||||
<polyline fill="none" stroke="black" points="453.04,-46.5 486.04,-46.5 "/>
|
||||
<text text-anchor="middle" x="469.54" y="-31.3" font-family="monospace" font-size="14.00">e7</text>
|
||||
<polyline fill="none" stroke="black" points="453.04,-23.5 486.04,-23.5 "/>
|
||||
<text text-anchor="middle" x="469.54" y="-8.3" font-family="monospace" font-size="14.00">e8</text>
|
||||
<polygon fill="none" stroke="black" points="756.66,-61.39 756.66,-153.39 938.66,-153.39 938.66,-61.39 756.66,-61.39"/>
|
||||
<text text-anchor="middle" x="773.16" y="-138.19" font-family="monospace" font-size="14.00">e1</text>
|
||||
<polyline fill="none" stroke="black" points="756.66,-130.39 789.66,-130.39 "/>
|
||||
<text text-anchor="middle" x="773.16" y="-115.19" font-family="monospace" font-size="14.00">e2</text>
|
||||
<polyline fill="none" stroke="black" points="756.66,-107.39 789.66,-107.39 "/>
|
||||
<text text-anchor="middle" x="773.16" y="-92.19" font-family="monospace" font-size="14.00">e3</text>
|
||||
<polyline fill="none" stroke="black" points="756.66,-84.39 789.66,-84.39 "/>
|
||||
<text text-anchor="middle" x="773.16" y="-69.19" font-family="monospace" font-size="14.00">e4</text>
|
||||
<polyline fill="none" stroke="black" points="789.66,-61.39 789.66,-153.39 "/>
|
||||
<text text-anchor="middle" x="814.66" y="-103.69" font-family="monospace" font-size="14.00">dut4</text>
|
||||
<polyline fill="none" stroke="black" points="839.66,-61.39 839.66,-153.39 "/>
|
||||
<text text-anchor="middle" x="856.16" y="-138.19" font-family="monospace" font-size="14.00">e5</text>
|
||||
<polyline fill="none" stroke="black" points="839.66,-130.39 872.66,-130.39 "/>
|
||||
<text text-anchor="middle" x="856.16" y="-115.19" font-family="monospace" font-size="14.00">e6</text>
|
||||
<polyline fill="none" stroke="black" points="839.66,-107.39 872.66,-107.39 "/>
|
||||
<text text-anchor="middle" x="856.16" y="-92.19" font-family="monospace" font-size="14.00">e7</text>
|
||||
<polyline fill="none" stroke="black" points="839.66,-84.39 872.66,-84.39 "/>
|
||||
<text text-anchor="middle" x="856.16" y="-69.19" font-family="monospace" font-size="14.00">e8</text>
|
||||
<polyline fill="none" stroke="black" points="872.66,-61.39 872.66,-153.39 "/>
|
||||
<text text-anchor="middle" x="905.66" y="-126.69" font-family="monospace" font-size="14.00">radio0</text>
|
||||
<polyline fill="none" stroke="black" points="872.66,-107.39 938.66,-107.39 "/>
|
||||
<text text-anchor="middle" x="905.66" y="-80.69" font-family="monospace" font-size="14.00">radio1</text>
|
||||
</g>
|
||||
<!-- host--dut4 -->
|
||||
<g id="edge13" class="edge">
|
||||
<title>host:d4a--dut4:e1</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M91.5,-172.02C91.5,-172.02 142.26,-139.74 187.02,-123.52 265.48,-95.07 370.04,-81.5 370.04,-81.5"/>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M91.5,-484.69C91.5,-484.69 316.42,-525.06 405.78,-413.08 405.81,-413.04 405.77,-352.3 405.8,-352.26 495.46,-194.21 756.66,-142.39 756.66,-142.39"/>
|
||||
</g>
|
||||
<!-- host--dut4 -->
|
||||
<g id="edge14" class="edge">
|
||||
<title>host:d4b--dut4:e2</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M91.5,-149.02C91.5,-149.02 370.04,-58.5 370.04,-58.5"/>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M91.5,-461.69C91.5,-461.69 313.77,-520.38 405.78,-413.08 405.81,-413.04 405.77,-352.3 405.8,-352.26 494.31,-187.35 756.66,-119.39 756.66,-119.39"/>
|
||||
</g>
|
||||
<!-- host--dut4 -->
|
||||
<g id="edge15" class="edge">
|
||||
<title>host:d4c--dut4:e3</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M91.5,-126.02C91.5,-126.02 370.04,-34.5 370.04,-34.5"/>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M91.5,-438.69C91.5,-438.69 310.78,-516.11 405.78,-413.08 405.81,-413.04 405.77,-352.3 405.8,-352.26 493.25,-179.91 756.66,-95.39 756.66,-95.39"/>
|
||||
</g>
|
||||
<!-- host--dut4 -->
|
||||
<g id="edge16" class="edge">
|
||||
<title>host:d4d--dut4:e4</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M91.5,-103.02C91.5,-103.02 370.04,-11.5 370.04,-11.5"/>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M91.5,-415.69C91.5,-415.69 307.4,-512.25 405.78,-413.08 405.82,-413.05 405.78,-352.3 405.8,-352.26 449.08,-262.4 756.66,-72.39 756.66,-72.39"/>
|
||||
</g>
|
||||
<!-- dut1--dut2 -->
|
||||
<g id="edge19" class="edge">
|
||||
<title>dut1:e8--dut2:e5</title>
|
||||
<path fill="none" stroke="black" stroke-width="2" d="M470.04,-459.55C470.04,-459.55 333.02,-387.53 333.02,-387.53"/>
|
||||
<path fill="none" stroke="black" stroke-width="2" d="M856.66,-1023.99C856.66,-1023.99 535.8,-795.12 535.8,-795.12"/>
|
||||
</g>
|
||||
<!-- dut1--dut3 -->
|
||||
<g id="edge22" class="edge">
|
||||
<title>dut1:e6--dut3:e7</title>
|
||||
<path fill="none" stroke="red" stroke-width="2" d="M486.13,-517.51C486.13,-517.51 522.61,-470.31 516.04,-429.55 494.57,-296.4 333.02,-187.52 333.02,-187.52"/>
|
||||
<path fill="none" stroke="red" stroke-width="2" d="M850.82,-1069.86C811.11,-987.48 581.28,-510.61 541.62,-428.34"/>
|
||||
</g>
|
||||
<!-- cell0 -->
|
||||
<g id="node6" class="node">
|
||||
<title>cell0</title>
|
||||
<polygon fill="none" stroke="cornflowerblue" stroke-dasharray="5,2" points="292.78,-794.3 292.78,-832.3 375.78,-832.3 375.78,-794.3 292.78,-794.3"/>
|
||||
<text text-anchor="middle" x="334.28" y="-817.1" font-family="monospace" font-size="14.00" fill="cornflowerblue">cell0</text>
|
||||
<text text-anchor="middle" x="334.28" y="-802.1" font-family="monospace" font-size="14.00" fill="cornflowerblue">(radio0)</text>
|
||||
</g>
|
||||
<!-- dut1--cell0 -->
|
||||
<g id="edge24" class="edge">
|
||||
<title>dut1:radio0--cell0</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M905.57,-1116.03C905.57,-1116.03 804.39,-1167.39 726.66,-1145.99 541.96,-1095.13 391.3,-895.93 347.31,-832.68"/>
|
||||
</g>
|
||||
<!-- cell1 -->
|
||||
<g id="node7" class="node">
|
||||
<title>cell1</title>
|
||||
<polygon fill="none" stroke="cornflowerblue" stroke-dasharray="5,2" points="292.78,-345.08 292.78,-383.08 375.78,-383.08 375.78,-345.08 292.78,-345.08"/>
|
||||
<text text-anchor="middle" x="334.28" y="-367.88" font-family="monospace" font-size="14.00" fill="cornflowerblue">cell1</text>
|
||||
<text text-anchor="middle" x="334.28" y="-352.88" font-family="monospace" font-size="14.00" fill="cornflowerblue">(radio1)</text>
|
||||
</g>
|
||||
<!-- dut1--cell1 -->
|
||||
<g id="edge28" class="edge">
|
||||
<title>dut1:radio1--cell1</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M938.76,-1081.95C938.76,-1081.95 985.53,-1119.49 968.66,-1145.99 939.78,-1191.35 778.43,-1160.56 726.66,-1145.99 494.04,-1080.49 394.02,-1065.22 262.78,-862.3 184.61,-741.43 218.38,-682.75 234.28,-539.69 240.65,-482.37 230.43,-460.84 262.78,-413.08 271.04,-400.89 283.44,-390.85 295.6,-383.1"/>
|
||||
</g>
|
||||
<!-- cell2 -->
|
||||
<g id="node8" class="node">
|
||||
<title>cell2</title>
|
||||
<polygon fill="none" stroke="mediumseagreen" stroke-dasharray="5,2" points="264.28,-569.69 264.28,-607.69 404.28,-607.69 404.28,-569.69 264.28,-569.69"/>
|
||||
<text text-anchor="middle" x="334.28" y="-592.49" font-family="monospace" font-size="14.00" fill="mediumseagreen">cell2</text>
|
||||
<text text-anchor="middle" x="334.28" y="-577.49" font-family="monospace" font-size="14.00" fill="mediumseagreen">(band steering)</text>
|
||||
</g>
|
||||
<!-- dut1--cell2 -->
|
||||
<g id="edge32" class="edge">
|
||||
<title>dut1:radio2--cell2</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M938.76,-1058.02C938.76,-1058.02 992.71,-1112.42 968.66,-1145.99 937.35,-1189.71 778.43,-1160.56 726.66,-1145.99 494.04,-1080.49 394.02,-1065.22 262.78,-862.3 208.13,-777.8 186.54,-726.27 234.28,-637.69 241.38,-624.51 253.34,-614.82 266.56,-607.71"/>
|
||||
</g>
|
||||
<!-- dut1--cell2 -->
|
||||
<g id="edge33" class="edge">
|
||||
<title>dut1:radio3--cell2</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M905.66,-1023.99C905.66,-1023.99 472.93,-924.04 405.8,-825.12 405.77,-825.08 405.79,-764.35 405.78,-764.3 394.07,-704.53 361.94,-639.44 345.03,-607.93"/>
|
||||
</g>
|
||||
<!-- dut2--dut3 -->
|
||||
<g id="edge17" class="edge">
|
||||
<title>dut2:e7--dut3:e6</title>
|
||||
<path fill="none" stroke="black" stroke-width="3" d="M333.12,-340.57C333.12,-340.57 369.92,-306.58 363.02,-275.52 356.22,-244.85 333.12,-211.48 333.12,-211.48"/>
|
||||
<path fill="none" stroke="black" stroke-width="3" d="M535.8,-725.85C535.8,-678.82 535.8,-498.59 535.8,-451.54"/>
|
||||
</g>
|
||||
<!-- dut2--dut3 -->
|
||||
<g id="edge18" class="edge">
|
||||
<title>dut2:e8--dut3:e5</title>
|
||||
<path fill="none" stroke="black" stroke-width="3" d="M317.02,-306.53C317.02,-306.53 317.02,-245.52 317.02,-245.52"/>
|
||||
<path fill="none" stroke="black" stroke-width="3" d="M535.8,-703.12C535.8,-703.12 535.8,-474.26 535.8,-474.26"/>
|
||||
</g>
|
||||
<!-- dut2--dut4 -->
|
||||
<g id="edge23" class="edge">
|
||||
<title>dut2:e6--dut4:e7</title>
|
||||
<path fill="none" stroke="red" stroke-width="2" d="M333.02,-364.53C333.02,-364.53 494.57,-255.64 516.04,-122.5 522.61,-81.73 486.13,-34.54 486.13,-34.54"/>
|
||||
<path fill="none" stroke="red" stroke-width="2" d="M541.64,-749C581.35,-666.61 811.19,-189.75 850.84,-107.48"/>
|
||||
</g>
|
||||
<!-- dut2--cell0 -->
|
||||
<g id="edge25" class="edge">
|
||||
<title>dut2:radio0--cell0</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M584.71,-795.17C584.71,-795.17 486.42,-825.04 405.8,-825.12 396.11,-825.13 385.78,-824.07 376.07,-822.57"/>
|
||||
</g>
|
||||
<!-- dut2--cell1 -->
|
||||
<g id="edge29" class="edge">
|
||||
<title>dut2:radio1--cell1</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M617.9,-750.12C617.9,-750.12 658.49,-708.24 647.8,-673.12 609.59,-547.66 491.95,-603.14 405.8,-504.26 373.32,-466.97 351.01,-412.34 340.73,-383.53"/>
|
||||
</g>
|
||||
<!-- dut2--cell2 -->
|
||||
<g id="edge34" class="edge">
|
||||
<title>dut2:radio2--cell2</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M584.8,-703.12C584.8,-703.12 500.51,-668.26 434.28,-637.69 413.57,-628.13 390.71,-617 372.08,-607.77"/>
|
||||
</g>
|
||||
<!-- dut3--dut4 -->
|
||||
<g id="edge20" class="edge">
|
||||
<title>dut3:e8--dut4:e5</title>
|
||||
<path fill="none" stroke="black" stroke-width="2" d="M333.02,-164.52C333.02,-164.52 470.04,-92.5 470.04,-92.5"/>
|
||||
<path fill="none" stroke="black" stroke-width="2" d="M535.8,-382.26C535.8,-382.26 856.66,-153.39 856.66,-153.39"/>
|
||||
</g>
|
||||
<!-- dut3--cell0 -->
|
||||
<g id="edge26" class="edge">
|
||||
<title>dut3:radio0--cell0</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M584.8,-474.26C584.8,-474.26 422.64,-648.62 405.8,-673.12 377.94,-713.64 353.9,-766.48 342.08,-794.28"/>
|
||||
</g>
|
||||
<!-- dut3--cell1 -->
|
||||
<g id="edge30" class="edge">
|
||||
<title>dut3:radio1--cell1</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M584.71,-382.21C584.71,-382.21 486.42,-352.34 405.8,-352.26 396.11,-352.25 385.78,-353.31 376.07,-354.81"/>
|
||||
</g>
|
||||
<!-- dut4--dut1 -->
|
||||
<g id="edge21" class="edge">
|
||||
<title>dut4:e8--dut1:e5</title>
|
||||
<path fill="none" stroke="black" stroke-width="2" d="M486.1,-11.58C486.1,-11.58 509.33,-71.88 516.04,-122.5 533.97,-257.78 533.97,-294.26 516.04,-429.55 509.33,-480.17 486.1,-540.47 486.1,-540.47"/>
|
||||
<path fill="none" stroke="black" stroke-width="2" d="M856.63,-61.3C856.63,-61.3 927.79,0 968.66,-31.39 968.66,-31.39 856.63,-1116.08 856.63,-1116.08"/>
|
||||
</g>
|
||||
<!-- dut4--cell0 -->
|
||||
<g id="edge27" class="edge">
|
||||
<title>dut4:radio0--cell0</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M905.66,-153.39C905.66,-153.39 786.31,-369.1 647.8,-504.26 553.93,-595.85 491.95,-574.23 405.8,-673.12 373.32,-710.41 351.01,-765.04 340.73,-793.85"/>
|
||||
</g>
|
||||
<!-- dut4--cell1 -->
|
||||
<g id="edge31" class="edge">
|
||||
<title>dut4:radio1--cell1</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M905.57,-61.35C905.57,-61.35 804.39,-9.99 726.66,-31.39 541.96,-82.25 391.3,-281.45 347.31,-344.7"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 23 KiB |
Reference in New Issue
Block a user