The UDM RADIUS rejects the pod CIDR as an unknown client; it accepts
10.2.11.0/24. FreeRADIUS pods get net1 on VLAN 11 "(11) RADIUS" and the
udm home server moves to 10.2.11.1.
- Talos k8s-{1,2,3}: VLANConfig radius0 (VLAN 11 on uplink), no
addresses, IPv6 disabled on the host link.
- Cilium bpf.vlanBypass gains 11: radius0 is address-less like ens19, so
its tagged frames meet the VLAN filter on uplink (#592).
- NAD freeradius-udm: macvlan on radius0, DHCP IPAM requesting only
subnet-mask (skipDefault), so the lease has no router and net1 holds
only the connected /24. No sbr: it moves that route into a source
table, and the unbound proxy socket would then reach the UDM via eth0
from the pod IP. tuning disables IPv6 on net1 (VLAN 11 sends RAs).
- NAS listeners bound to eth0, so nothing listens on net1.
- CNP freeradius-udm-egress removed: net1 traffic bypasses Cilium.
- Harness: FreeRADIUS interfaces named eth0/net1 explicitly; Docker
attaches networks in no fixed order, and the eth0 bind exposed it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
437 lines
18 KiB
Bash
Executable File
437 lines
18 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Functional test of the FreeRADIUS config in kubernetes/apps/authentik/.
|
|
# See README.md. Needs: docker, openssl, yq (the jq wrapper, kislyuk/yq).
|
|
set -euo pipefail
|
|
|
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
repo="$(cd "$here/../.." && pwd)"
|
|
app="$repo/kubernetes/apps/authentik"
|
|
deploy="$app/freeradius.yaml"
|
|
config="$app/freeradius-config.yaml"
|
|
|
|
net=radtest
|
|
net_udm=radtest-udm
|
|
# test client ranges from clients.conf
|
|
subnet=10.2.50.0/24
|
|
subnet6=fd02:50::/64
|
|
ip_acct=10.2.50.249
|
|
ip_nas=10.2.50.250
|
|
ip_radius=10.2.50.251
|
|
ip_mock=10.2.50.252
|
|
ip6_acct=fd02:50::249
|
|
ip6_nas=fd02:50::250
|
|
ip6_radius=fd02:50::251
|
|
outpost_fqdn=ak-outpost-8021x-radius.authentik.svc.cluster.local
|
|
mock_image=python:3.13-alpine
|
|
eapol_image=radtest-eapol:local
|
|
password=dummy-correct-password
|
|
|
|
work="$(mktemp -d)"
|
|
fails=0
|
|
|
|
cleanup() {
|
|
local ids=()
|
|
mapfile -t ids < <(docker ps -aq --filter name='^radtest-')
|
|
if ((${#ids[@]} > 0)); then docker rm -f "${ids[@]}" >/dev/null; fi
|
|
local n
|
|
for n in "$net" "$net_udm"; do
|
|
if [[ -n "$(docker network ls -q --filter name="^${n}\$")" ]]; then docker network rm "$n" >/dev/null; fi
|
|
done
|
|
local vols=()
|
|
mapfile -t vols < <(docker volume ls -q --filter name='^radtest-')
|
|
if ((${#vols[@]} > 0)); then docker volume rm "${vols[@]}" >/dev/null; fi
|
|
rm -rf "$work"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
pass() { printf 'PASS %s\n' "$1"; }
|
|
fail() { printf 'FAIL %s\n' "$1"; fails=$((fails + 1)); }
|
|
|
|
deployment() { yq -r 'select(.kind == "Deployment")' "$deploy"; }
|
|
|
|
# --- render the production ConfigMap through the Deployment's projected volume
|
|
render() {
|
|
local raddb="$work/raddb" key path
|
|
mkdir -p "$raddb"
|
|
while read -r key path; do
|
|
mkdir -p "$raddb/$(dirname "$path")"
|
|
yq -r --arg k "$key" 'select(.kind == "ConfigMap") | .data[$k]' "$config" >"$raddb/$path"
|
|
done < <(deployment | yq -r '.spec.template.spec.volumes[] | select(.projected) | .projected.sources[]
|
|
| select(.configMap) | .configMap.items[] | "\(.key) \(.path)"')
|
|
|
|
# self-signed CA + server cert in place of the cert-manager Secret
|
|
local crt key_path
|
|
crt="$(deployment | yq -r '.spec.template.spec.volumes[] | select(.projected) | .projected.sources[]
|
|
| select(.secret) | .secret.items[] | select(.key == "tls.crt") | .path')"
|
|
key_path="$(deployment | yq -r '.spec.template.spec.volumes[] | select(.projected) | .projected.sources[]
|
|
| select(.secret) | .secret.items[] | select(.key == "tls.key") | .path')"
|
|
mkdir -p "$raddb/$(dirname "$crt")" "$work/pki" "$work/eapol"
|
|
openssl req -x509 -newkey rsa:2048 -nodes -days 1 -subj "/CN=radtest CA" \
|
|
-keyout "$work/pki/ca.key" -out "$work/eapol/ca.pem" >"$work/pki/openssl.log" 2>&1
|
|
openssl req -newkey rsa:2048 -nodes -subj "/CN=radius.renner.uno" \
|
|
-keyout "$raddb/$key_path" -out "$work/pki/server.csr" >>"$work/pki/openssl.log" 2>&1
|
|
printf 'subjectAltName=DNS:radius.renner.uno\nextendedKeyUsage=serverAuth\n' >"$work/pki/ext"
|
|
openssl x509 -req -in "$work/pki/server.csr" -CA "$work/eapol/ca.pem" -CAkey "$work/pki/ca.key" \
|
|
-CAcreateserial -days 1 -extfile "$work/pki/ext" -out "$raddb/$crt" >>"$work/pki/openssl.log" 2>&1
|
|
chmod -R a+rX "$work"
|
|
# kubelet creates the projected volume root world-writable
|
|
chmod 1777 "$raddb"
|
|
}
|
|
|
|
# Volume mounts of a pod container as docker -v options, as the pod declares
|
|
# them: the projected volume is the rendered config, other emptyDirs (except
|
|
# the /tmp tmpfs) are named docker volumes.
|
|
mount_opts() { # container name; sets mounts[]
|
|
local vol name path ro ro_suffix
|
|
vol="$(deployment | yq -r '.spec.template.spec.volumes[] | select(.projected) | .name')"
|
|
mounts=()
|
|
while read -r name path ro; do
|
|
ro_suffix=""
|
|
if [[ "$ro" == true ]]; then ro_suffix=":ro"; fi
|
|
if [[ "$name" == "$vol" ]]; then
|
|
mounts+=(-v "$work/raddb:$path${ro_suffix}")
|
|
elif [[ "$name" != tmp ]]; then
|
|
mounts+=(-v "radtest-$name:$path${ro_suffix}")
|
|
fi
|
|
done < <(deployment | yq -r --arg n "$1" '(.spec.template.spec.containers + (.spec.template.spec.initContainers // []))[]
|
|
| select(.name == $n) | .volumeMounts[] | "\(.name) \(.mountPath) \(.readOnly // false)"')
|
|
}
|
|
|
|
# emptyDir volumes other than /tmp, writable by the pod user like kubelet's
|
|
create_volumes() {
|
|
local name
|
|
while read -r name; do
|
|
docker volume create "radtest-$name" >/dev/null
|
|
docker run --rm --user 0:0 -v "radtest-$name:/v" "$mock_image" chmod 0777 /v >/dev/null
|
|
done < <(deployment | yq -r '.spec.template.spec.volumes[] | select(.emptyDir) | select(.name != "tmp") | .name')
|
|
}
|
|
|
|
# Pod restrictions: non-root, read-only root, no capabilities.
|
|
pod_opts=(--user 65534:65534 --read-only --tmpfs /tmp:rw,size=16m --cap-drop ALL --security-opt no-new-privileges)
|
|
|
|
# Run the Deployment's initContainers exactly as defined.
|
|
run_init() {
|
|
local names=() n image cmd=() args=()
|
|
mapfile -t names < <(deployment | yq -r '.spec.template.spec.initContainers // [] | .[].name')
|
|
for n in "${names[@]}"; do
|
|
image="$(deployment | yq -r --arg n "$n" '.spec.template.spec.initContainers[] | select(.name == $n) | .image')"
|
|
mapfile -t cmd < <(deployment | yq -r --arg n "$n" '.spec.template.spec.initContainers[] | select(.name == $n) | .command[]')
|
|
mapfile -t args < <(deployment | yq -r --arg n "$n" '.spec.template.spec.initContainers[] | select(.name == $n) | .args // [] | .[]')
|
|
mount_opts "$n"
|
|
if docker run --rm --name "radtest-init-$n" --network "$net" "${pod_opts[@]}" "${mounts[@]}" \
|
|
--entrypoint "${cmd[0]}" "$image" "${cmd[@]:1}" "${args[@]}" >"$work/init-$n.log" 2>&1; then
|
|
pass "initContainer $n"
|
|
else
|
|
cat "$work/init-$n.log"
|
|
fail "initContainer $n"
|
|
exit 1
|
|
fi
|
|
done
|
|
}
|
|
|
|
# --- dummy secrets for every secretKeyRef env var of the Deployment
|
|
secret_env=()
|
|
load_env() {
|
|
local name
|
|
while read -r name; do
|
|
secret_env+=(-e "$name=dummy-${name,,}")
|
|
done < <(deployment | yq -r '.spec.template.spec.containers[] | select(.name == "freeradius")
|
|
| .env[] | select(.valueFrom.secretKeyRef) | .name')
|
|
}
|
|
|
|
start() {
|
|
local image args=()
|
|
image="$(deployment | yq -r '.spec.template.spec.containers[] | select(.name == "freeradius") | .image')"
|
|
mapfile -t args < <(deployment | yq -r '.spec.template.spec.containers[] | select(.name == "freeradius") | .args[]')
|
|
if [[ "${RADTEST_DEBUG:-0}" == 1 ]]; then args+=(-xx); fi
|
|
radius_image="$image"
|
|
|
|
# Internal bridge without a host address: both test ranges are also the
|
|
# workstation LAN, so the host must get no address or route on them.
|
|
docker network create --internal --subnet "$subnet" --ipv6 --subnet "$subnet6" \
|
|
-o com.docker.network.bridge.inhibit_ipv4=true \
|
|
-o com.docker.network.bridge.gateway_mode_ipv6=isolated "$net" >/dev/null
|
|
# UDM home server address from proxy.conf; its /24 is a second internal
|
|
# bridge (gateway address moved off the UDM's).
|
|
ip_udm="$(awk '$1 == "home_server" && $2 == "udm" {f=1} f && $1 == "ipaddr" {print $3; exit}' "$work/raddb/proxy.conf")"
|
|
docker network create --internal --subnet "${ip_udm%.*}.0/24" --gateway "${ip_udm%.*}.254" \
|
|
-o com.docker.network.bridge.inhibit_ipv4=true "$net_udm" >/dev/null
|
|
local bridge n
|
|
for n in "$net" "$net_udm"; do
|
|
bridge="br-$(docker network inspect -f '{{printf "%.12s" .Id}}' "$n")"
|
|
if [[ -n "$(ip -4 -o addr show dev "$bridge")" ]] \
|
|
|| [[ -n "$(ip -6 -o addr show dev "$bridge" scope global)" ]] \
|
|
|| [[ -n "$(ip -6 route show dev "$bridge" "$subnet6")" ]]; then
|
|
echo "bridge $bridge got a host address or route in a test range; aborting" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
docker run -d --name radtest-udm --network "$net_udm" --ip "$ip_udm" \
|
|
-e MOCK_SECRET=dummy-udm_radius_secret \
|
|
-v "$here/mock_outpost.py:/mock/mock_outpost.py:ro" -v "$here/mock_udm.py:/mock/mock_udm.py:ro" \
|
|
"$mock_image" python3 -u /mock/mock_udm.py >/dev/null
|
|
|
|
docker run -d --name radtest-mock --network "$net" --ip "$ip_mock" \
|
|
--network-alias "$outpost_fqdn" \
|
|
-e MOCK_SECRET=dummy-dot1x_outpost_secret -e MOCK_PASSWORD="$password" \
|
|
-v "$here/mock_outpost.py:/mock_outpost.py:ro" \
|
|
"$mock_image" python3 -u /mock_outpost.py >/dev/null
|
|
|
|
create_volumes
|
|
run_init
|
|
|
|
# Same restrictions as the pod.
|
|
mount_opts freeradius
|
|
local run_opts=("${pod_opts[@]}" "${secret_env[@]}" "${mounts[@]}")
|
|
local confdir="" i
|
|
for i in "${!args[@]}"; do
|
|
if [[ "${args[$i]}" == -d ]]; then confdir="${args[$((i + 1))]}"; fi
|
|
done
|
|
|
|
echo "--- radiusd -XC (config check)"
|
|
if docker run --rm --network "$net" "${run_opts[@]}" "$image" radiusd -XC -d "$confdir" >"$work/configcheck.log" 2>&1 \
|
|
&& grep -q "Configuration appears to be OK" "$work/configcheck.log"; then
|
|
pass "radiusd -XC: Configuration appears to be OK"
|
|
else
|
|
cat "$work/configcheck.log"
|
|
fail "radiusd -XC"
|
|
exit 1
|
|
fi
|
|
|
|
# Interface names as in the pod: eth0 the NAS side (the listeners bind to
|
|
# it), net1 the UDM side. Docker attaches networks in no fixed order.
|
|
docker create --name radtest-freeradius "${run_opts[@]}" \
|
|
--network "name=$net,ip=$ip_radius,ip6=$ip6_radius,driver-opt=com.docker.network.endpoint.ifname=eth0" \
|
|
"$image" "${args[@]}" >/dev/null
|
|
docker network connect --driver-opt com.docker.network.endpoint.ifname=net1 "$net_udm" radtest-freeradius
|
|
docker start radtest-freeradius >/dev/null
|
|
sleep 2
|
|
if [[ "$(docker inspect -f '{{.State.Running}}' radtest-freeradius)" != true ]]; then
|
|
docker logs radtest-freeradius
|
|
fail "freeradius container running"
|
|
exit 1
|
|
fi
|
|
pass "freeradius container running with production args: ${args[*]}"
|
|
|
|
local probe=()
|
|
mapfile -t probe < <(deployment | yq -r '.spec.template.spec.containers[] | select(.name == "freeradius")
|
|
| .readinessProbe.exec.command[]')
|
|
if docker exec radtest-freeradius "${probe[@]}" >"$work/probe.out" 2>&1; then
|
|
pass "readiness probe command succeeds"
|
|
else
|
|
cat "$work/probe.out"
|
|
fail "readiness probe command succeeds"
|
|
fi
|
|
}
|
|
|
|
eapol_conf() { # case user password [outer-identity]
|
|
cat >"$work/eapol/$1.conf" <<EOF
|
|
network={
|
|
key_mgmt=WPA-EAP
|
|
eap=TTLS
|
|
identity="$2"
|
|
anonymous_identity="${4:-anonymous}"
|
|
password="$3"
|
|
phase2="auth=PAP"
|
|
ca_cert="/eapol/ca.pem"
|
|
domain_suffix_match="radius.renner.uno"
|
|
}
|
|
EOF
|
|
}
|
|
|
|
eapol() { # name timeout server
|
|
docker run --rm --network "$net" --ip "$ip_nas" --ip6 "$ip6_nas" -v "$work/eapol:/eapol:ro" "$eapol_image" \
|
|
eapol_test -c "/eapol/$1.conf" -a "$3" -s "dummy-dot1x_test_secret" -t "$2" \
|
|
>"$work/$1.out" 2>&1 || true
|
|
}
|
|
|
|
# Attribute lines of the Access-Accept as the NAS received it.
|
|
accept_attrs() {
|
|
awk '/RADIUS message: code=2 /{f=1; next} f && /^ /{print; next} f{f=0}' "$work/$1.out"
|
|
}
|
|
|
|
attr_value() { # case attr-number
|
|
accept_attrs "$1" | awk -v a="Attribute $2 " 'index($0, a){getline; sub(/^ *Value: /, ""); print; exit}'
|
|
}
|
|
|
|
check_accept() { # case user server [outer-identity]
|
|
local c="$1"
|
|
eapol_conf "$c" "$2" "$password" "${4:-}"
|
|
eapol "$c" 5 "$3"
|
|
if [[ "$(tail -n 1 "$work/$c.out")" == SUCCESS ]]; then pass "$c: SUCCESS"; else fail "$c: SUCCESS"; fi
|
|
[[ "$(attr_value "$c" 81)" =~ ^(00)?3530$ ]] && pass "$c: Tunnel-Private-Group-Id 50" \
|
|
|| fail "$c: Tunnel-Private-Group-Id 50 (got '$(attr_value "$c" 81)')"
|
|
[[ "$(attr_value "$c" 64)" == 0000000d ]] && pass "$c: Tunnel-Type VLAN" \
|
|
|| fail "$c: Tunnel-Type VLAN (got '$(attr_value "$c" 64)')"
|
|
[[ "$(attr_value "$c" 65)" == 00000006 ]] && pass "$c: Tunnel-Medium-Type IEEE-802" \
|
|
|| fail "$c: Tunnel-Medium-Type IEEE-802 (got '$(attr_value "$c" 65)')"
|
|
# Cisco-AV-Pair = VSA vendor 9; "dot1x-vlan" in hex
|
|
if accept_attrs "$c" | grep -Eq 'Value: 00000009|646f7431782d766c616e'; then
|
|
fail "$c: no Cisco-AV-Pair to the NAS"
|
|
else
|
|
pass "$c: no Cisco-AV-Pair to the NAS"
|
|
fi
|
|
}
|
|
|
|
check_reject() { # case user password timeout server [outer-identity]
|
|
local c="$1"
|
|
eapol_conf "$c" "$2" "$3" "${6:-}"
|
|
eapol "$c" "$4" "$5"
|
|
if [[ "$(tail -n 1 "$work/$c.out")" == FAILURE ]] && grep -q "RADIUS message: code=3 (Access-Reject)" "$work/$c.out" \
|
|
&& ! grep -q "EAPOL test timed out" "$work/$c.out"; then
|
|
pass "$c: FAILURE via Access-Reject within ${4}s"
|
|
else
|
|
fail "$c: FAILURE via Access-Reject within ${4}s"
|
|
fi
|
|
}
|
|
|
|
udm_log() { docker logs radtest-udm 2>&1; }
|
|
|
|
# Non-anonymous outer identity: the whole EAP conversation goes to the mock
|
|
# UDM, its Access-Accept reaches the NAS unchanged.
|
|
check_udm_accept() { # case user server
|
|
local c="$1" u="$2"
|
|
eapol_conf "$c" "$u" "$password" "$u"
|
|
eapol "$c" 3 "$3"
|
|
udm_log | grep -q "round=1 user=$u eap_identity_response=True eap_identity=$u request_ma=True -> challenge" \
|
|
&& pass "$c: EAP-Response/Identity forwarded to the UDM" || fail "$c: EAP-Response/Identity forwarded to the UDM"
|
|
udm_log | grep -q "round=2 user=$u state_ok=True eap_type=21 request_ma=True -> accept" \
|
|
&& pass "$c: second round forwarded with the UDM's State" || fail "$c: second round forwarded with the UDM's State"
|
|
grep -q "RADIUS message: code=2 (Access-Accept)" "$work/$c.out" \
|
|
&& pass "$c: UDM Access-Accept reaches the NAS" || fail "$c: UDM Access-Accept reaches the NAS"
|
|
[[ "$(attr_value "$c" 81)" =~ ^(00)?323030$ ]] && pass "$c: Tunnel-Private-Group-Id 200" \
|
|
|| fail "$c: Tunnel-Private-Group-Id 200 (got '$(attr_value "$c" 81)')"
|
|
[[ "$(attr_value "$c" 64)" == 0000000d ]] && pass "$c: Tunnel-Type VLAN" \
|
|
|| fail "$c: Tunnel-Type VLAN (got '$(attr_value "$c" 64)')"
|
|
[[ "$(attr_value "$c" 65)" == 00000006 ]] && pass "$c: Tunnel-Medium-Type IEEE-802" \
|
|
|| fail "$c: Tunnel-Medium-Type IEEE-802 (got '$(attr_value "$c" 65)')"
|
|
local k
|
|
for k in "Recv-Key (crypt)|11" "Send-Key (sign)|22"; do
|
|
grep -qF "MS-MPPE-${k%|*} - hexdump(len=32): $(printf "${k#*|} %.0s" {1..31})${k#*|}" "$work/$c.out" \
|
|
&& pass "$c: MS-MPPE-${k%% *} unchanged" || fail "$c: MS-MPPE-${k%% *} unchanged"
|
|
done
|
|
}
|
|
|
|
check_udm_reject() { # case user server
|
|
local c="$1" u="$2"
|
|
check_reject "$c" "$u" "$password" 1 "$3" "$u"
|
|
udm_log | grep -q "round=1 user=$u eap_identity_response=True eap_identity=$u request_ma=True -> reject" \
|
|
&& pass "$c: decided by the UDM" || fail "$c: decided by the UDM"
|
|
}
|
|
|
|
check_accounting() { # case server
|
|
printf 'Acct-Status-Type = Start\nUser-Name = "acct-test"\nAcct-Session-Id = "%s"\n' \
|
|
"$1" >"$work/eapol/$1.txt"
|
|
docker run --rm --network "$net" --ip "$ip_acct" --ip6 "$ip6_acct" -v "$work/eapol:/eapol:ro" "$radius_image" \
|
|
radclient -x -r 1 -t 2 -f "/eapol/$1.txt" "$2:1813" acct dummy-dot1x_test_secret \
|
|
>"$work/$1.out" 2>&1 || true
|
|
if grep -q "Received Accounting-Response" "$work/$1.out"; then
|
|
pass "$1: accounting answered locally"
|
|
else
|
|
fail "$1: accounting answered locally"
|
|
fi
|
|
}
|
|
|
|
# Outpost stalls until FreeRADIUS marks it dead, then comes back: a login must
|
|
# succeed again within the bound.
|
|
check_recovery() { # bound-seconds
|
|
local bound="$1" start now elapsed n=0
|
|
eapol_conf recovery vlan-avpair "$password"
|
|
docker pause radtest-mock >/dev/null
|
|
start=$SECONDS
|
|
# Requests during the stall: timeouts mark the home server zombie, then
|
|
# dead. Unpause right after the dead marking (worst case for recovery).
|
|
until docker logs radtest-freeradius 2>&1 | grep -q "port 1812 as dead"; do
|
|
if ((SECONDS - start > 90)); then
|
|
docker unpause radtest-mock >/dev/null
|
|
fail "recovery: home server marked dead during the stall"
|
|
return
|
|
fi
|
|
eapol recovery 4 "$ip_radius"
|
|
done
|
|
echo "recovery: home server marked dead $((SECONDS - start))s into the stall"
|
|
docker unpause radtest-mock >/dev/null
|
|
start=$SECONDS
|
|
while :; do
|
|
n=$((n + 1))
|
|
eapol recovery 5 "$ip_radius"
|
|
if [[ "$(tail -n 1 "$work/recovery.out")" == SUCCESS ]]; then break; fi
|
|
now=$SECONDS
|
|
if ((now - start > 120)); then break; fi
|
|
sleep 1
|
|
done
|
|
elapsed=$((SECONDS - start))
|
|
echo "recovery: first SUCCESS ${elapsed}s after unpause (attempt $n)"
|
|
if [[ "$(tail -n 1 "$work/recovery.out")" == SUCCESS ]] && ((elapsed <= bound)); then
|
|
pass "recovery: login succeeds within ${bound}s after the outpost returns (${elapsed}s)"
|
|
else
|
|
fail "recovery: login succeeds within ${bound}s after the outpost returns (${elapsed}s)"
|
|
local probe=()
|
|
mapfile -t probe < <(deployment | yq -r '.spec.template.spec.containers[] | select(.name == "freeradius")
|
|
| .livenessProbe.exec.command[]')
|
|
if docker exec radtest-freeradius "${probe[@]}" >"$work/recovery-probe.out" 2>&1; then
|
|
echo "recovery: liveness probe still succeeds"
|
|
else
|
|
echo "recovery: liveness probe fails"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
main() {
|
|
for f in "$deploy" "$config"; do
|
|
[[ -f "$f" ]] || { echo "missing $f" >&2; exit 1; }
|
|
done
|
|
docker build -q -t "$eapol_image" -f "$here/eapol.Dockerfile" "$here" >/dev/null
|
|
render
|
|
load_env
|
|
start
|
|
|
|
check_accept vlan-tunnel vlan-tunnel "$ip_radius"
|
|
check_accept vlan-avpair vlan-avpair "$ip_radius"
|
|
check_reject novlan novlan "$password" 5 "$ip_radius"
|
|
check_reject bad-password vlan-tunnel wrong-password 1 "$ip_radius"
|
|
check_reject unknown-user bad "$password" 1 "$ip_radius"
|
|
check_accounting acct "$ip_radius"
|
|
|
|
# Outer identity routing: anonymous[@realm] (any case) -> local TTLS,
|
|
# anything else -> UDM.
|
|
check_accept anon-realm vlan-tunnel "$ip_radius" "Anonymous@Example.Org"
|
|
check_udm_accept udm-accept endpoint-accept "$ip_radius"
|
|
check_udm_reject udm-reject endpoint-reject "$ip_radius"
|
|
check_udm_accept v6-udm-accept v6-endpoint-accept "$ip6_radius"
|
|
|
|
# IPv6 listeners and clients
|
|
check_accept v6-vlan-avpair vlan-avpair "$ip6_radius"
|
|
check_reject v6-novlan novlan "$password" 5 "$ip6_radius"
|
|
check_reject v6-bad-password vlan-tunnel wrong-password 1 "$ip6_radius"
|
|
check_accounting v6-acct "[$ip6_radius]"
|
|
|
|
# dead -> alive needs num_answers_to_alive (3) status checks, check_interval 6s
|
|
check_recovery 20
|
|
|
|
if grep -q "code=4" < <(docker logs radtest-mock 2>&1); then
|
|
fail "no accounting forwarded to the outpost"
|
|
else
|
|
pass "no accounting forwarded to the outpost"
|
|
fi
|
|
|
|
if udm_log | grep -Eiq "eap_identity=anonymous|user=(vlan-|novlan|bad|anonymous)|code=4"; then
|
|
fail "anonymous logins and accounting never reach the UDM"
|
|
else
|
|
pass "anonymous logins and accounting never reach the UDM"
|
|
fi
|
|
|
|
if ((fails > 0)) || [[ "${RADTEST_DEBUG:-0}" == 1 ]]; then
|
|
echo "--- freeradius log"; docker logs radtest-freeradius 2>&1
|
|
echo "--- mock outpost log"; docker logs radtest-mock 2>&1
|
|
echo "--- mock udm log"; udm_log
|
|
for o in "$work"/*.out; do echo "--- $(basename "$o")"; cat "$o"; done
|
|
fi
|
|
echo
|
|
if ((fails > 0)); then echo "$fails check(s) failed"; exit 1; fi
|
|
echo "all checks passed"
|
|
}
|
|
|
|
main "$@"
|