From d42862e2d84b1fa654c57918336de4f48cd0944e Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Wed, 4 Nov 2026 10:05:55 +0100 Subject: [PATCH] docs(radius): UniFi objects and live login check for the 802.1X test Co-Authored-By: Claude Opus 5.5 --- docs/radius/dot1x.md | 26 ++++++++++++++ tests/radius/README.md | 3 ++ tests/radius/live.sh | 77 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100755 tests/radius/live.sh diff --git a/docs/radius/dot1x.md b/docs/radius/dot1x.md index 464785cb..ce041c17 100644 --- a/docs/radius/dot1x.md +++ b/docs/radius/dot1x.md @@ -69,6 +69,32 @@ The FreeRADIUS clients `test` (10.2.50.0/24) and `test6` (fd02:50::/64) and their CIDRs in `freeradius-cnp.yaml` are for the test phase only and must be removed after the test. +## UniFi objects + +Created through the UniFi Network API (site `default`): + +| Object | ID | Settings | +|---|---|---| +| Firewall policy "Allow APs RADIUS" | `62c21e63-8c15-4720-adf8-b2333c090e2f` | zone Isolated `10.2.10.0/24` → zone External `10.2.99.91`, UDP `1812-1813`, ALLOW, IPv4 | +| RADIUS profile "Cluster 802.1X" | `3e5fcfed-63dc-4e7d-ad51-17cbc074e4bb` | auth `10.2.99.91:1812`, acct `10.2.99.91:1813`, secret `DOT1X_NAS_SECRET`, VLAN assignment on (`vlan_wlan_mode: optional`) | +| WLAN "Renner Test" | `3eba27e7-ddc2-41fb-ad3f-82add317847a` | copy of "Renner UniFi" (WPA2/WPA3 Enterprise transition, fast roaming, BSS transition, same AP group and bands), network (90) Guest as fallback, RADIUS profile "Cluster 802.1X" | + +## Live login check + +`tests/radius/live.sh` runs EAP-TTLS/PAP logins with `eapol_test` against the +deployed FreeRADIUS from a test client range and prints result, duration and +the VLAN attributes of the Access-Accept (hex) per login; secrets are read from +files and never printed: + +``` +LIVE_PASSWORD_VAR=CLAUDE_BOT_PASSWORD tests/radius/live.sh 10.2.99.91 claude-bot \ + ~/.config/claude-bot/authentik ~/.config/claude-bot/radius-secret 20 +``` + +For IPv6 set `LIVE_SOURCE` to the workstation's `fd02:50::` address and use +`fd99::91`. `group-id=[3530]` is VLAN `50`, `tunnel-type=[0000000d]` VLAN, +`medium=[00000006]` IEEE-802. + ## Client setup (SSID "Renner Test") All clients use EAP-TTLS with inner PAP. The server certificate is a public diff --git a/tests/radius/README.md b/tests/radius/README.md index 8350f947..7cd1182a 100644 --- a/tests/radius/README.md +++ b/tests/radius/README.md @@ -31,3 +31,6 @@ Pieces: Requires Docker, `openssl` and `yq` (the jq wrapper). Secrets, password and certificates are generated dummies. `RADTEST_DEBUG=1` runs radiusd with `-xx` and prints all logs. Containers, network and temp files are removed on exit. + +`live.sh` logs in against the deployed FreeRADIUS instead; usage in +`docs/radius/dot1x.md` ("Live login check"). diff --git a/tests/radius/live.sh b/tests/radius/live.sh new file mode 100755 index 00000000..487a6fc4 --- /dev/null +++ b/tests/radius/live.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# Live EAP-TTLS/PAP login against the deployed FreeRADIUS from a test client +# (10.2.50.0/24 / fd02:50::/64, client "test"/"test6"). +# +# Usage: tests/radius/live.sh [count] +# password-file: the password on the first line, or `export NAME=value` lines +# (LIVE_PASSWORD_VAR selects the variable, default CLAUDE_BOT_TOKEN) +# secret-file: `bao kv get` output of apps/radius, or the bare +# DOT1X_TEST_SECRET on the first line +# count: number of logins (default 1) +# LIVE_SOURCE: optional local address for eapol_test -A (must be in a +# test client range) +# Prints one line per login: result, duration, VLAN. Secrets are never printed. +set -euo pipefail + +server="$1" +identity="$2" +password_file="$3" +secret_file="$4" +count="${5:-1}" +password_var="${LIVE_PASSWORD_VAR:-CLAUDE_BOT_TOKEN}" + +read_password() { + local line + line=$(grep -m1 -E "^export ${password_var}=" "$password_file" || true) + if [ -n "$line" ]; then + line="${line#*=}" + line="${line#\"}"; line="${line%\"}" + line="${line#\'}"; line="${line%\'}" + printf '%s' "$line" + else + head -n1 "$password_file" + fi +} + +read_secret() { + local s + s=$(sed -n 's/.*DOT1X_TEST_SECRET:\([^] ]*\).*/\1/p' "$secret_file") + if [ -n "$s" ]; then printf '%s' "$s"; else head -n1 "$secret_file"; fi +} + +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +chmod 700 "$work" + +password=$(read_password) +secret=$(read_secret) +[ -n "$password" ] || { echo "no password found in $password_file" >&2; exit 2; } +[ -n "$secret" ] || { echo "no DOT1X_TEST_SECRET found in $secret_file" >&2; exit 2; } + +umask 077 +cat > "$work/eapol.conf" <&1 || true) + end=$(date +%s%N) + ms=$(( (end - start) / 1000000 )) + result=$(tail -n1 <<<"$out") + case "$result" in SUCCESS|FAILURE) ;; *) result="NO-RESULT" ;; esac + # Attribute lines of the Access-Accept as the NAS received it (hex values). + attrs=$(awk '/RADIUS message: code=2 /{f=1; next} f && /^ /{print; next} f{f=0}' <<<"$out") + attr() { awk -v a="Attribute $1 " 'index($0, a){getline; sub(/^ *Value: /, ""); print; exit}' <<<"$attrs"; } + echo "login $i: $result ${ms}ms tunnel-type=[$(attr 64)] medium=[$(attr 65)] group-id=[$(attr 81)]" +done -- 2.54.0