. "$BST_LIB" # BST-TIMEOUT: 900 # freeradius 3.2.6 (meta-networking, recipes-connectivity) -- the FreeRADIUS # authentication server. What ships is the daemon /usr/sbin/radiusd, the client # and administration tools in /usr/bin (radclient, radtest, radwho, radlast), # and the configuration tree under /etc/raddb. # # READ THIS BEFORE READING THE CASES: THE SERVER CANNOT INSTANTIATE A MODULE ON # THIS ARM, AND THAT IS A PLATFORM PROPERTY RATHER THAN A DEFECT IN THIS PORT. # FreeRADIUS is not a monolith -- every authentication type, the file-backed # user database, the EAP methods and the policy helpers are rlm_* SHARED # OBJECTS, which src/main/modules.c opens at runtime with dlopen(3) # (modules.c:207,256). This image contains no shared object of any kind: # static-only is distro policy (conf/distro/brintos.conf) until the dynamic-libc # milestone lands, and that milestone is live platform work, not an unreported # bug -- the multi-library needed-graph is # brintos.io/brintos/linux-in-the-browser/issues/168 and the M5 dynamic arm is # tracked in #105, #106, #117. # # dlopen ITSELF IS PRESENT -- measured, not assumed: a four-line probe built with # this arm's own wasmcc-glibc links, and llvm-nm shows __dlopen defined in the # result beside glibc's .gnu.warning.dlopen (the standing caveat that dlopen in a # statically linked program needs the shared libraries of the glibc it was linked # against). So radiusd LINKS and STARTS. What fails is every module load, and # with it `radiusd -XC` -- the gate an authentication exchange sits behind. # # THE ONE COMPILED-IN-MODULE PATH FREERADIUS CARRIES IS COMPILED OUT HERE. # modules.c:506 guards it on RTLD_SELF, and this arm's glibc 2.43 defines # RTLD_NEXT and RTLD_DEFAULT but no RTLD_SELF. Upstream's --with-static-modules # does not close it either: it expands to -dlpreopen, and freeradius never reads # libtool's preloaded table (configure.ac mentions ltdl nowhere). # # AND ON THIS IMAGE -XC NEVER REACHES THE LOADER AT ALL -- a SECOND platform gap # sits in front of the first. main_config_init() calls switch_users(), whose very # first act is fr_set_dumpable_init() -> getrlimit(RLIMIT_CORE) (mainconfig.c:648, # debug.c:582), and no rlimit syscall is wired on wasm32: getrlimit returns ENOSYS # while /proc/self/limits reports real values, which is brintos/linux#20. So # radiusd parses raddb, then exits 1 with "Failed to get current core limit: # Function not implemented" -- before the first dlopen. Both gaps are the # platform's, and the case below names whichever one the run actually met rather # than assuming the module loader was reached. # # SO THIS SUITE COVERS WHAT IS REACHABLE AND ASSERTS THE BOUNDARY, rather than # reporting a dozen reds that all say the same thing about the platform. That is # the shape 88-openvpn uses for the TUN device and 63-tunctl for the same fact: # drive the arm as far as the machine allows, assert the DIAGNOSTIC, and gate the # real assertions on a runtime probe so they become the genuine test the day the # platform grows the capability -- rather than a permanent red every reader and # every batch has to re-attribute forever. require radiusd freeradius/present RADIUSD=/usr/sbin/radiusd AUTHPORT=18120 ACCTPORT=18121 SECRET=bst-testing123 USER=bstuser PASS=bstpass D=${TMPDIR:-/tmp}/bst-freeradius.$$ rm -rf "$D"; mkdir -p "$D" || { bst_notok freeradius/workdir "cannot create $D"; exit 0; } # has ... : true iff EVERY needle is a substring. has() { _h=$1; shift for _n in "$@"; do case "$_h" in *"$_n"*) ;; *) return 1 ;; esac; done return 0 } # ---------------------------------------------------------------- inventory -- # The daemon plus the client tools. radiusd is in sbindir and the clients in # bindir, which the recipe splits into freeradius and freeradius-utils; a name # missing here means an image list picked a subset, not that the build ran short. check freeradius/inventory-radiusd test -x /usr/sbin/radiusd check freeradius/inventory-radclient test -x /usr/bin/radclient check freeradius/inventory-radtest test -x /usr/bin/radtest # The configuration tree, installed whole even though the server cannot yet # instantiate what it names. Asserting it is what makes the boundary case below # a statement about module loading rather than about a missing config. check freeradius/raddb-dir test -d /etc/raddb check freeradius/raddb-radiusd test -f /etc/raddb/radiusd.conf check freeradius/raddb-clients test -f /etc/raddb/clients.conf check freeradius/raddb-dictionary test -f /etc/raddb/dictionary # The bundled module set is symlinked into mods-enabled at install # (raddb/all.mk:10, 30 entries). Two spot checks that the tree is complete. check freeradius/mods-available-pap test -f /etc/raddb/mods-available/pap check freeradius/mods-enabled-pap test -e /etc/raddb/mods-enabled/pap # The dictionaries the client tools need to name attributes rather than number # them. Pure data files, so these are reachable whatever the loader does. check freeradius/dict-freeradius test -f /usr/share/freeradius/dictionary # radiusd installs to sbindir, not bindir. Asserting where the bare name # resolves catches a packaging change that leaves it off the PATH the runner # has, which would otherwise surface as a skip and read like an absent package. check_eq freeradius/resolves-to-sbin "$RADIUSD" "$(command -v radiusd 2>/dev/null)" # ----------------------------------------------------------------- identity -- # The binary runs, which is the half of the port the loader does not gate. -v # pins the series: 3.2 and 4.x are different configuration languages, so a major # bump would leave every case here asserting against a tree this port does not # ship. ver=$("$RADIUSD" -v 2>&1); vrc=$? check_eq freeradius/version-rc "0" "$vrc" if has "$ver" "Version 3.2"; then bst_ok freeradius/version-is-3.2 else bst_notok freeradius/version-is-3.2 "want 'Version 3.2', got '$(_bst_1line "$ver")'"; fi # The TLS backend the binary was linked against. NOT read from the -v banner: # that string is the version, the git commit, HOSTINFO and the build date and # NOTHING ELSE (radiusd.c:67), so it names no library and "OpenSSL" is absent # from it on an OpenSSL build too -- asserting against it is red whatever the # build did. What does carry the library is version_print()'s built-with table, # gated on DEBUG_ENABLED3 (version.c:535), so -xxx is what reaches it. Still no # configuration is read and no rlimit is touched: -v exits at radiusd.c:330, # before main_config_init(). # # EVERY LINE OF THAT TABLE ARRIVES PREFIXED, and the prefix is not optional -- # it is forced by the very flag that unlocks the table. The rows print only at # rad_debug_lvl >= 3, and vradlog() stamps a ctime and a severity onto every # line whenever the level is NEITHER 1 NOR 2 (log.c:404). The two conditions # have no overlap, so a row reaches the terminal as # Wed Aug 26 09:46:59 2026 : Debug: tls : yes # and never as " tls : yes", on this arm or any other. A parse anchored to the # start of the line therefore matches nothing on EVERY build, and reports the # table as absent when it was printed in full -- which is what it did here. # The name is matched on a whitespace boundary instead, which also stops the # "ssl" row being read out of the word "openssl". vx=$("$RADIUSD" -xxxv 2>&1) # -> the row's value, wherever radlog put the row on the line. vx_field() { printf '%s\n' "$vx" | sed -n "s/^.*[[:space:]]$1[[:space:]][[:space:]]*:[[:space:]]*\(.*\)\$/\1/p" | head -1 } vx_tls=$(vx_field tls) vx_ssl=$(vx_field ssl) # "not linked" is the literal ssl_version_num() returns without # HAVE_OPENSSL_CRYPTO_H (version.c:203) -- a without-OpenSSL build spelled out, # not a missing line, so it is checked for by name rather than by emptiness. if [ "$vx_tls" = "yes" ] && [ -n "$vx_ssl" ] && [ "$vx_ssl" != "not linked" ]; then bst_ok freeradius/linked-openssl elif ! has "$vx" "Server was built with"; then # The header version_print() emits before the first row. Distinguishing its # absence from an unreadable row is what separates "the table never printed" # from "the table printed and this suite could not parse it" -- the two want # opposite fixes, and reporting the second as the first is how a green build # spent a batch round looking like a broken one. bst_notok freeradius/linked-openssl \ "radiusd -xxxv printed no built-with table at all: version_print()'s DEBUG_ENABLED3 arm did not run, so the TLS backend cannot be read -- $(_bst_1line "$vx")" else bst_notok freeradius/linked-openssl \ "the built-with table printed but did not name a linked libssl: tls='$vx_tls' ssl='$vx_ssl' -- $(_bst_1line "$(printf '%s\n' "$vx" | grep -E '[[:space:]](tls|ssl)[[:space:]]+:' | head -2)")" fi # The client tools run too, and their usage path is loader-independent: radclient # parses its arguments and exits before any module is involved. -h exits non-zero # on this package (usage() ends in exit(1)), so the status is pinned by check_rc # rather than assumed to be 0 -- a suite written to the usual convention would be # red against a correct binary. check_rc freeradius/radclient-usage-rc 1 sh -c "radclient -h > '$D/rc.out' 2>&1" check_out freeradius/radclient-usage-text "Usage: radclient" cat "$D/rc.out" # ------------------------------------------------- THE MODULE-LOADING BOUNDARY # radiusd -XC parses /etc/raddb as shipped and INSTANTIATES every module the # enabled sites reference. It is the gate the whole authentication exchange sits # behind, and on this arm it is where the platform boundary is met: the first # module dlopen() fails because no rlm_*.so exists on the image. # # The transcript is captured and the outcome branched on, rather than one arm # being pinned as correct. A green -XC means the platform grew what freeradius # needs (or the recipe found a static dispatch), and the exchange below then runs # for real; a failed -XC is reported as ONE attributed red naming the boundary, # not as a dozen reds that all say the same thing. "$RADIUSD" -XC > "$D/xc.log" 2>&1; xcrc=$? xc=$(cat "$D/xc.log") if [ "$xcrc" -eq 0 ] && has "$xc" "Configuration appears to be OK"; then bst_ok freeradius/config-check-ok XC_OK=1 else XC_OK=0 # The red is attributed by naming the cause, and the cause is asserted rather # than asserted-by-comment: a failure that is NOT about loading a module is a # different finding and must not be filed under this one. if has "$xc" "Failed to link to module"; then bst_notok freeradius/config-check-ok \ "radiusd -XC cannot instantiate a module: this image has no shared objects (distro static-only until the dynamic-libc milestone; linux-in-the-browser#168, M5 #105/#106/#117) -- $(_bst_1line "$(grep -m1 'Failed to link to module' "$D/xc.log")")" elif has "$xc" "Failed to get current core limit"; then bst_notok freeradius/config-check-ok \ "radiusd -XC dies BEFORE the module loader: switch_users() opens with fr_set_dumpable_init() (mainconfig.c:648) and getrlimit(RLIMIT_CORE) is ENOSYS here -- no rlimit syscall is wired on wasm32 (brintos/linux#20) -- $(_bst_1line "$(grep -m1 'core limit' "$D/xc.log")")" else bst_notok freeradius/config-check-ok \ "radiusd -XC failed for a reason that is NEITHER module loading NOR the rlimit gap -- this is not a known platform boundary and needs its own attribution: rc=$xcrc $(_bst_1line "$(tail -3 "$D/xc.log")")" fi fi # Whichever way -XC went, the parser ran: radiusd read radiusd.conf and reached # module instantiation rather than dying earlier. This case is what separates # "the configuration tree is intact and parseable" from "the loader is missing", # and it is green on both sides of the boundary. if has "$xc" "including configuration file /etc/raddb/radiusd.conf"; then bst_ok freeradius/config-parser-reached-raddb else bst_notok freeradius/config-parser-reached-raddb \ "radiusd -XC did not report reading /etc/raddb/radiusd.conf -- $(_bst_1line "$(head -5 "$D/xc.log")")" fi # ------------------------------------------------- THE AUTHENTICATION EXCHANGE # The ticket's pass criterion. It is gated on the probe above rather than run # unconditionally, for the reason in the header: with no module loader there is # no rlm_pap to authenticate against and no rlm_files to hold a user, so running # it anyway would report a handful of reds that are all one fact about the # platform. # # THE DAY THE BOUNDARY MOVES, THIS BECOMES THE REAL TEST BY ITSELF -- no edit to # this suite is needed, which is the whole point of the probe. if [ "$XC_OK" -eq 1 ]; then # A configuration that INCLUDES the bundled tree rather than replacing it: # the criterion is about the shipped raddb, so this overrides only the two # ports, one client and one user. mkdir -p "$D/raddb" cp -r /etc/raddb/. "$D/raddb/" 2>/dev/null cat > "$D/raddb/clients.conf" < "$D/raddb/mods-config/files/authorize" </dev/null sed -i 's/^user = radiusd/user = root/; s/^group = radiusd/group = root/' \ "$D/raddb/radiusd.conf" 2>/dev/null check freeradius/test-config-check sh -c "$RADIUSD -XC -d '$D/raddb' -l stdout > '$D/xc2.log' 2>&1" # Started and LEFT RUNNING, per the header. The readiness probe is what # decides it came up, never a sleep alone. ( "$RADIUSD" -f -d "$D/raddb" -l stdout > "$D/radiusd.log" 2>&1 & echo $! > "$D/pid" ) i=0 while [ $i -lt 60 ]; do grep -q "Ready to process requests" "$D/radiusd.log" 2>/dev/null && break i=$((i+1)); sleep 1 done if grep -q "Ready to process requests" "$D/radiusd.log" 2>/dev/null; then bst_ok freeradius/daemon-ready else bst_notok freeradius/daemon-ready "radiusd did not announce readiness in 60s -- $(_bst_1line "$(tail -3 "$D/radiusd.log")")" fi # --- Access-Accept for a valid user ------------------------------------ printf 'User-Name = "%s"\nUser-Password = "%s"\nNAS-IP-Address = 127.0.0.1\n' "$USER" "$PASS" \ > "$D/accept.req" ( timeout 30 radclient -x -f "$D/accept.req" "127.0.0.1:$AUTHPORT" auth "$SECRET" \ > "$D/accept.out" 2>&1 ) acc=$(cat "$D/accept.out") if has "$acc" "Received Access-Accept"; then bst_ok freeradius/auth-accept else bst_notok freeradius/auth-accept "no Access-Accept -- $(_bst_1line "$(tail -3 "$D/accept.out")")"; fi # The reply is not empty: a server answering Access-Accept carrying nothing # would pass the case above and still not be doing the job. for a in "Service-Type = Framed-User" "Framed-Protocol = PPP"; do id=$(printf '%s' "$a" | sed 's/[^A-Za-z0-9]/-/g' | cut -c1-24) if has "$acc" "$a"; then bst_ok "freeradius/accept-attr-$id" else bst_notok "freeradius/accept-attr-$id" "reply does not carry '$a'"; fi done # --- the controls that give the Accept its meaning ---------------------- printf 'User-Name = "%s"\nUser-Password = "wrong-%s"\nNAS-IP-Address = 127.0.0.1\n' "$USER" "$PASS" \ > "$D/reject.req" ( timeout 30 radclient -x -f "$D/reject.req" "127.0.0.1:$AUTHPORT" auth "$SECRET" \ > "$D/reject.out" 2>&1 ) if has "$(cat "$D/reject.out")" "Received Access-Reject"; then bst_ok freeradius/auth-reject-wrong-password else bst_notok freeradius/auth-reject-wrong-password "no Access-Reject -- $(_bst_1line "$(tail -3 "$D/reject.out")")" fi printf 'User-Name = "no-such-%s"\nUser-Password = "%s"\nNAS-IP-Address = 127.0.0.1\n' "$USER" "$PASS" \ > "$D/unknown.req" ( timeout 30 radclient -x -f "$D/unknown.req" "127.0.0.1:$AUTHPORT" auth "$SECRET" \ > "$D/unknown.out" 2>&1 ) if has "$(cat "$D/unknown.out")" "Received Access-Reject"; then bst_ok freeradius/auth-reject-unknown-user else bst_notok freeradius/auth-reject-unknown-user "unknown user drew no Access-Reject -- $(_bst_1line "$(tail -3 "$D/unknown.out")")" fi # radtest: the shipped wrapper over radclient, a second consumer of the same # exchange reached through a different caller. ( timeout 30 radtest "$USER" "$PASS" "127.0.0.1:$AUTHPORT" 0 "$SECRET" \ > "$D/radtest.out" 2>&1 ) if has "$(cat "$D/radtest.out")" "Received Access-Accept"; then bst_ok freeradius/radtest-accept else bst_notok freeradius/radtest-accept "radtest drew no Access-Accept -- $(_bst_1line "$(tail -3 "$D/radtest.out")")" fi rm -rf "$D/raddb" else # ONE skip, naming what is not being asserted and why, rather than silence. # A skip is honest here in a way a red is not: the exchange was not attempted, # so nothing about freeradius's authentication path was measured either way. bst_skip freeradius/auth-exchange \ "not attempted: radiusd -XC does not reach a serving configuration on this image -- see freeradius/config-check-ok, which names which platform gap stopped it (the rlimit syscalls, or the absent module loader behind them)" fi rm -rf "$D"