Bring up a two-node 802.11 link — one node as an access point (hostapd), one as a client (wpa_supplicant) — from a ten-line experiment description and one command.
Two nodes form a WiFi link: the first runs an access point, the second joins it as a client. The console reaches each node over the wired control network, while the experiment itself runs on the nodes' Atheros radios.
Both paths are shown below. The manual path spells out every command, so you can see what happens on each node and debug by hand. The Ansible path is the supported one: the framework is already on the console, so you supply only the description and it picks two WiFi-capable nodes, images them, configures both ends, verifies the link and tears down.
After completing this tutorial you will be able to:
wifi: {}) rather than by name.| Difficulty | Beginner |
| Estimated time | 30 min (most of it imaging) |
| Domain / sandbox | any WiFi-equipped domain — grid, sb7 |
| Topic group | WiFi |
| Band / channel | 2.4 GHz, channel 1, 802.11g, WPA2-PSK |
| WiFi subnet | 192.168.100.0/24 (AP .1, client .2) |
| Transmits? | Yes — 2.4 GHz, inside your reservation |
| Last verified | 2026-07-28 on grid (node19-7 / node19-13, baseline.ndz) |
| Role | Qty | Notes |
|---|---|---|
| WiFi-equipped node — access point | 1 | Atheros radio, auto-detected by driver |
| WiFi-equipped node — client | 1 | Atheros radio, auto-detected by driver |
Grid nodes commonly carry two radios — an Atheros plus an Intel or a Broadcom — and not the same second one on every node. Both the roles and the manual steps below select the radio by driver, never by interface name.
| Image | Loaded onto | Provides |
|---|---|---|
baseline.ndz |
both nodes | Ubuntu 24.04 with the Atheros drivers available |
| Component | Source |
|---|---|
| hostapd, wpasupplicant, iw, iperf3, rfkill | installed during the run |
| cosmos-orchestration | preinstalled on the console |
console.<domain>.cosmos-lab.org
| (wired control plane, ssh root@node)
+--------------------------+
| |
node A ((( 802.11g ch1 ))) node B
hostapd <---- cosmos-wifi ----> wpa_supplicant
192.168.100.1 192.168.100.2
The control plane stays on the wired NIC; the experiment lives entirely on the radios.
Log into the console:
ssh <username>@console.<domain>.cosmos-lab.org
The WPA2 passphrase never goes in the description. Put it in a vault file:
cat > secrets.yml <<'EOF'
cosmos_wifi_psk: choose-a-passphrase-at-least-8-chars
EOF
ansible-vault encrypt secrets.yml
Substitute your two reserved nodes for node19-7 (AP) and node19-13 (client).
# 1. Image and boot both nodes
omf tell -a offh -t node19-7,node19-13
omf load -i baseline.ndz -t node19-7,node19-13
omf tell -a on -t node19-7,node19-13
omf stat -t node19-7,node19-13
# 2. Install the tools on both
for n in node19-7 node19-13; do
ssh root@$n 'apt-get install -y hostapd wpasupplicant iw iperf3 rfkill'
done
# 3. Find the radio ON EACH NODE, by driver rather than by name.
# Interface names are slot-derived, so they differ between nodes.
ssh root@node19-7 'rfkill unblock wifi; iw dev'
phy#0
Interface wlp3s0
type managed
A freshly imaged node answers SSH before its radio is up — the driver loads and the interface is renamed about 36 s after boot. If iw dev is empty, wait and try again.
# 4. Access point: write the config and start hostapd
ssh root@node19-7 'cat > /etc/hostapd/cosmos.conf <<EOF
interface=wlp3s0
driver=nl80211
ssid=cosmos-wifi
hw_mode=g
channel=1
wmm_enabled=1
auth_algs=1
wpa=2
wpa_key_mgmt=WPA-PSK
rsn_pairwise=CCMP
wpa_passphrase=<your passphrase>
EOF
iw reg set US
systemd-run --unit=cosmos-hostapd --collect /usr/sbin/hostapd /etc/hostapd/cosmos.conf
ip addr replace 192.168.100.1/24 dev wlp3s0
systemd-run --unit=cosmos-iperf3 --collect /usr/bin/iperf3 --server'
# 5. Client: associate and address
ssh root@node19-13 'cat > /etc/wpa_supplicant/cosmos.conf <<EOF
ctrl_interface=/run/wpa_supplicant
network={
ssid="cosmos-wifi"
key_mgmt=WPA-PSK
proto=RSN
pairwise=CCMP
psk="<your passphrase>"
}
EOF
iw reg set US
systemd-run --unit=cosmos-wpa-supplicant --collect \
/usr/sbin/wpa_supplicant -i wlp3s0 -c /etc/wpa_supplicant/cosmos.conf
sleep 10
ip addr replace 192.168.100.2/24 dev wlp3s0
iw dev wlp3s0 link'
# 6. Verify from the client. -I binds to the radio, so this cannot
# silently succeed over the wired control network.
ssh root@node19-13 'ping -c 5 -I wlp3s0 192.168.100.1; iperf3 --client 192.168.100.1 --time 5'
# 7. Stop the radios and release
ssh root@node19-7 'systemctl stop cosmos-hostapd cosmos-iperf3'
ssh root@node19-13 'systemctl stop cosmos-wpa-supplicant'
omf tell -a offh -t node19-7,node19-13
Save this as wifi.yml. It is the whole experiment:
apiVersion: cosmos/v1
name: wifi-ap-client
description: Two-node 802.11 link, AP plus client
parameters:
ssid: {type: string, default: cosmos-wifi}
channel: {type: int, default: 1}
resources:
ap:
requires: {wifi: {}}
image: baseline.ndz
client:
requires: {wifi: {}}
image: baseline.ndz
steps:
- role: cosmos_lab.orchestration.wifi_ap
target: ap
vars:
cosmos_wifi_ssid: "{{ ssid }}"
cosmos_wifi_channel: "{{ channel }}"
- role: cosmos_lab.orchestration.wifi_sta
target: client
vars:
cosmos_wifi_ssid: "{{ ssid }}"
- role: cosmos_lab.orchestration.wifi_verify
target: client
collect: [/run/cosmos/wifi]
No node names, and no passphrase — it comes from the vault file.
Check it can run here, without booking anything:
cosmos-run wifi/ap-client.yml --resolve-only
wifi-ap-client on grid: ap=node19-7.grid.cosmos-lab.org; client=node19-13.grid.cosmos-lab.org
Then run it:
cosmos-run wifi/ap-client.yml -e @secrets.yml --ask-vault-pass
To image the nodes from scratch first, add image_policy: always to each resource and pass -e cosmos_allow_destructive=true. By default the framework images only if the node cannot already satisfy the requirements, which saves roughly 13 minutes of reservation time per pair.
The verify role prints:
node19-13.grid.cosmos-lab.org -> 192.168.100.1: 15.48 Mbit/s, 0% loss
and collects link.json to ~/cosmos-artifacts/<run-id>/:
{
"iface": "wlp3s0",
"node": "node19-13.grid.cosmos-lab.org",
"packet_loss_pct": "0",
"peer": "192.168.100.1",
"signal_dbm": "-31",
"ssid": "cosmos-wifi",
"throughput_mbps": "15.48"
}
Success = association, 0% packet loss over the radio, and a non-zero iperf3 rate. PLAY RECAP shows failed=0 on both nodes.
Throughput here is 802.11g on channel 1, matching the configuration above — not what the hardware can do.
The framework stops hostapd, wpa_supplicant and iperf3 from an always: block, so they are stopped even if the run fails. Release the nodes when finished:
omf tell -a offh -t <your nodes>
| Symptom | Likely cause | Fix |
|---|---|---|
no wireless interface on a freshly imaged node |
The radio comes up about 36 s after boot, later than sshd | The roles retry; by hand, wait and re-run iw dev |
| The two nodes pick different chipsets | Nodes carry more than one radio, and not the same second one | Select by driver, not by interface name — the roles prefer ath10k_pci, then ath9k, then ath5k |
| hostapd exits immediately | The channel is illegal for the regulatory domain in force | iw reg set US before starting it; the role does this |
| Client will not associate | SSID or passphrase mismatch, or different channels | Check both ends; the role fails with the supplicant's journal rather than a bare timeout |
ping succeeds but the radio is down |
The ping went over the wired control network | Bind to the radio with -I <iface>, as above |
| No WiFi-capable nodes found | SDR-only sandboxes have no WiFi radio | Use grid or sb7; --resolve-only tells you before you book |
Orchestration: cosmos_lab.orchestration (cosmos-run); imaging: omf. Last verified: 2026-07-28 on grid (node19-7 / node19-13, baseline.ndz, imaged during the run). Tags: wifi, hostapd, wpa_supplicant, ansible.