Compare commits

...
Author SHA1 Message Date
claude-botandClaude Opus 5.5 ed3be3156b feat(network): VLAN 11 link radius0 on the nodes, Cilium VLAN bypass
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
VLAN 11 "(11) RADIUS" carries the FreeRADIUS proxy traffic to the UDM
RADIUS, which accepts clients from its own networks only.

- Talos k8s-{1,2,3}: VLANConfig radius0 (VLAN 11 on uplink), link only,
  no addresses, IPv6 disabled (VLAN 11 sends RAs).
- Cilium bpf.vlanBypass [30, 11]: radius0 is address-less like ens19, so
  its tagged frames meet the VLAN filter on uplink (#592).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 15:05:23 +01:00
k.renner 697856cbf8 Merge pull request 'feat(radius): forward UniFi Endpoint logins to the UDM RADIUS' (#1559) from feat/radius-udm-forward into main
Reviewed-on: #1559
Reviewed-by: Review Bot <review-bot@renner.uno>
2026-11-04 12:20:57 +00:00
k.renner 4962af5ed1 Merge pull request 'docs(radius): UniFi objects and live login check for the 802.1X test' (#1558) from docs/radius-live-test into main
Reviewed-on: #1558
2026-11-04 12:15:30 +00:00
claude-botandClaude Opus 5.5 d42862e2d8 docs(radius): UniFi objects and live login check for the 802.1X test
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 10:05:55 +01:00
7 changed files with 149 additions and 7 deletions
+26
View File
@@ -94,6 +94,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
@@ -95,19 +95,20 @@ spec:
# VLAN-tagged frames, accounted as "VLAN traffic disallowed by VLAN
# filter" in cilium_drop_count_total.
#
# Only VLAN 30 (v30iot) needs listing. Cilium attaches to devices that
# carry an address, so it sees mgmt0's frames already untagged by the
# 8021q layer — nothing for a VLAN filter to match. ens19 is deliberately
# address-less, so its frames only meet the filter on the shared parent,
# still tagged. Adding 40 would change nothing and would touch the
# datapath carrying etcd and the API.
# Only the address-less macvlan parents need listing: VLAN 30 (ens19,
# v30iot) and VLAN 11 (radius0, freeradius-udm). Cilium attaches to
# devices that carry an address, so it sees mgmt0's frames already
# untagged by the 8021q layer — nothing for a VLAN filter to match.
# ens19 and radius0 are deliberately address-less, so their frames only
# meet the filter on the shared parent, still tagged. Adding 40 would
# change nothing and would touch the datapath carrying etcd and the API.
#
# The symptom when this is wrong is not a DHCP timeout: a tc-layer drop
# reaches an AF_PACKET sender as ENOBUFS, so the CNI dhcp plugin reports
# "no buffer space available" (#592).
#
# Same three-copy problem as policyMapMax above. Takes an agent restart.
vlanBypass: [30]
vlanBypass: [30, 11]
# Let pods reach the cluster's own LoadBalancer external IPs; Cilium
# redirects the socket directly to the backend pod instead of sending
# traffic out to the VIP and back (which fails with L2 announcements).
+15
View File
@@ -8,6 +8,10 @@ machine:
# 10.2.30.0/24 route makes node replies bypass the UDM and breaks every
# DNATed flow.
net.ipv6.conf.ens19.disable_ipv6: "1"
# radius0 (VLAN 11) is LINK ONLY as well: the macvlan parent for the
# FreeRADIUS attachment. VLAN 11 announces RAs; the node must not
# autoconfigure from them.
net.ipv6.conf.radius0.disable_ipv6: "1"
# These two sit on the untagged fallback network, where nothing may run.
# Left enabled they autoconfigure a ULA and an IPv6 default route from its
# RAs, which ties with mgmt0's and costs the cluster IPv6 egress.
@@ -168,6 +172,17 @@ vlanID: 30
parent: uplink
up: true
---
# VLAN 11, UniFi "(11) RADIUS": link only, no addresses, no DHCP. Macvlan
# parent of the freeradius-udm attachment
# (kubernetes/apps/authentik/freeradius-network-attachment.yaml), which names
# it as `master`; renaming this link means editing that too.
apiVersion: v1alpha1
kind: VLANConfig
name: radius0
vlanID: 11
parent: uplink
up: true
---
# Pod network, VLAN 48. Node-to-node only: the VXLAN overlay rides it once the
# kubelet node IP moves here, and nothing on it is routed off the switch.
#
+10
View File
@@ -7,6 +7,8 @@ machine:
# macvlan parent for the v30iot pod attachments; host addresses on that L2
# make node replies bypass the UDM and break every UDM-DNATed flow.
net.ipv6.conf.ens19.disable_ipv6: "1"
# radius0 (VLAN 11): link only — see k8s-1.yaml.
net.ipv6.conf.radius0.disable_ipv6: "1"
# Same for the physical ports on the untagged fallback network: left
# enabled they take an IPv6 default route from its RAs and cost the
# cluster IPv6 egress.
@@ -115,6 +117,14 @@ vlanID: 30
parent: uplink
up: true
---
# VLAN 11, "(11) RADIUS": link only, no host addressing — see k8s-1.yaml.
apiVersion: v1alpha1
kind: VLANConfig
name: radius0
vlanID: 11
parent: uplink
up: true
---
# Pod network, VLAN 48. Node-to-node only, statically addressed, no gateway
# and no DHCP — see k8s-1.yaml.
apiVersion: v1alpha1
+10
View File
@@ -4,6 +4,8 @@ machine:
sysctls:
# v30iot NIC carries no host addressing — see k8s-1.yaml.
net.ipv6.conf.ens19.disable_ipv6: "1"
# radius0 (VLAN 11): link only — see k8s-1.yaml.
net.ipv6.conf.radius0.disable_ipv6: "1"
# Same for the physical ports on the untagged fallback network — see
# k8s-1.yaml.
net.ipv6.conf.uplink.disable_ipv6: "1"
@@ -102,6 +104,14 @@ vlanID: 30
parent: uplink
up: true
---
# VLAN 11, "(11) RADIUS": link only, no host addressing — see k8s-1.yaml.
apiVersion: v1alpha1
kind: VLANConfig
name: radius0
vlanID: 11
parent: uplink
up: true
---
# Pod network, VLAN 48. Node-to-node only, statically addressed, no gateway
# and no DHCP — see k8s-1.yaml.
apiVersion: v1alpha1
+3
View File
@@ -43,3 +43,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").
+77
View File
@@ -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 <server-ip> <identity> <password-file> <secret-file> [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" <<EOF
network={
ssid="Renner Test"
key_mgmt=WPA-EAP
eap=TTLS
identity="${identity}"
anonymous_identity="anonymous"
ca_cert="/etc/ssl/certs/ca-certificates.crt"
domain_suffix_match="radius.renner.uno"
phase2="auth=PAP"
password="${password}"
}
EOF
for i in $(seq 1 "$count"); do
start=$(date +%s%N)
out=$(eapol_test -c "$work/eapol.conf" -a "$server" ${LIVE_SOURCE:+-A "$LIVE_SOURCE"} -s "$secret" -t 10 2>&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