Files
Clara 6d9af54e7f [posix] set O_NONBLOCK with F_SETFL on Apple platforms (#13727)
`SocketWithCloseExec()` has no SOCK_NONBLOCK on Apple platforms and passed
O_NONBLOCK to fcntl(F_SETFD), which takes descriptor flags only and drops
it. Every kSocketNonBlock socket that did not set the flag again by itself
was therefore blocking on macOS: the utun control socket, the platform UDP
and TCP sockets, the MLD monitor, the infra-if route socket, the BPF
forwarder's membership socket and the daemon's listening socket. The
Thread routing socket and TREL set it with F_SETFL on their own. Set the
file status flag with F_SETFL in the helper, and drop the routing socket's
and TREL's own fcntl, which this makes redundant; the routing socket's
filter setup is written like infra_if.cpp's CreateRouteSocket().

BSD sockets inherit the listening socket's file status flags on accept(),
Linux sockets do not. The daemon's session socket is therefore
non-blocking today on FreeBSD and NetBSD, and would become so on macOS,
while it is blocking on Linux; output of more than the socket buffer
(8 KB on macOS) ahead of ot-ctl then fails with EAGAIN and closes the
session. Clear the flag on the accepted socket, as accept(2) asks of
portable programs.

check-posix-pty verifies with lsof that the daemon's listening socket is
non-blocking and its session socket is not.
2026-11-06 14:05:13 -08:00

499 lines
19 KiB
Bash
Executable File

#!/bin/bash
#
# Copyright (c) 2018, The OpenThread Authors.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. Neither the name of the copyright holder nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
set -e
set -x
die()
{
echo " *** ERROR: " "$*"
exit 1
}
at_exit()
{
EXIT_CODE=$?
sudo killall expect || true
sudo killall ot-ctl || true
sudo killall ot-daemon || true
sudo killall ot-cli || true
sudo killall ot-rcp || true
sudo killall socat || true
# The daemon's output no longer streams through tee; show it when the check failed.
if [[ ${EXIT_CODE} != 0 && -f ${OT_DAEMON_OUTPUT:-} ]]; then
cat "${OT_DAEMON_OUTPUT}"
fi
exit $EXIT_CODE
}
wait_for_leader()
{
if grep -q leader "$OT_OUTPUT"; then
return 0
else
echo 'Still waiting for leader'
fi
return 1
}
timeout_run()
{
local count="$1"
local exit_code
shift 1
while [[ $count != 0 && $exit_code != 0 ]]; do
count=$((count - 1))
"$@" && return 0 || exit_code=$?
sleep 1
done
return $exit_code
}
do_build()
{
./script/cmake-build simulation
# OMR prefixes are installed as kernel routes (which needs border routing for otNetDataContainsOmrPrefix), so that
# the daemon's route handling is exercised.
./script/cmake-build posix -DOT_PLATFORM_NETIF=1 -DOT_PLATFORM_UDP=1 -DOT_UDP_FORWARD=0 -DOT_POSIX_MAX_POWER_TABLE=1 -DOT_POSIX_INSTALL_OMR_ROUTES=ON -DOT_BORDER_ROUTING=ON -DOT_DAEMON="${OT_DAEMON}" -DOT_READLINE="${OT_READLINE}"
}
# The netif joins ff02::1 on the Thread interface when it comes up and leaves it when it
# goes down; both must reach the kernel. On macOS the join used to fail with EINVAL on a
# never-used utun (IPv6 is attached to a tun by its first address, which the stack adds
# after this join) and the failure was tolerated with a warning. The ot-daemon log is checked
# rather than a kernel-side group listing, which could not tell the daemon's own socket
# membership apart from one the kernel holds on its own.
check_daemon_multicast_group()
{
local action="$1"
[[ ${OT_DAEMON} == 'on' ]] || return 0
grep -q -E "${action} multicast address ff02::1([^0-9a-f:]|$)" "${OT_DAEMON_OUTPUT}" || die "ff02::1 not ${action}"
if grep -q -E 'IPV6_(JOIN|LEAVE)_GROUP failure' "${OT_DAEMON_OUTPUT}"; then
die 'multicast group failure in the ot-daemon log'
fi
}
# Taking the Thread interface down makes OpenThread leave its multicast groups, which
# ot-daemon treats as fatal when it fails. Verify that ot-daemon survives it.
check_daemon_netif_down()
{
local status
[[ ${OT_DAEMON} == 'on' ]] || return 0
"${OT_CTL[@]}" -I "${NETIF_NAME}" ifconfig down || true
sleep 1
# OT_DAEMON_JOB is sudo (or the sudo function of script/_maybe_isolate), which
# lives as long as ot-daemon and exits with its status.
if ! ps -p "${OT_DAEMON_JOB}" >/dev/null; then
wait "${OT_DAEMON_JOB}" && status=0 || status=$?
die "ot-daemon exited with status ${status} on ifconfig down"
fi
"${OT_CTL[@]}" -I "${NETIF_NAME}" ifconfig | grep 'down' || die 'ot-daemon does not respond after ifconfig down'
check_daemon_multicast_group Removed
}
# Lists a route to the prefix through the Thread interface. On Linux only the daemon's (proto boot) counts, since the
# kernel adds one of its own (proto kernel) for the interface's address once the address is deprecated.
kernel_route_known()
{
if [[ ${OSTYPE} == "linux-gnu"* ]]; then
ip -6 route show "$1" dev "${NETIF_NAME}" proto boot | grep .
else
netstat -rn -f inet6 | grep -F "$1" | grep -w "${NETIF_NAME}"
fi
}
# Runs an ot-ctl command on the daemon and succeeds when it answers Done. The output is collected first, so that a
# grep -q cannot end ot-ctl before it has read the daemon's whole reply.
ot_ctl_done()
{
local output
output=$("${OT_CTL[@]}" -I "${NETIF_NAME}" "$@") || return 1
grep -q Done <<<"${output}"
}
# Passes when the route listing fails too, so it must follow a kernel_route_known that passed.
kernel_route_gone()
{
! kernel_route_known "$1"
}
check_daemon_omr_route()
{
local omr_prefix=fd00:dead:beef::/64
# OpenThread prints every group within the prefix length, zero groups included.
local logged_prefix=fd00:dead:beef:0::/64
[[ ${OT_DAEMON} == 'on' ]] || return 0
# An OMR prefix in the network data becomes a kernel route through the Thread interface, and goes with the prefix.
# The kernel installs a prefix route of its own for the address the daemon configures from the prefix (XNU cannot
# be told not to), so the table alone does not prove the daemon's work: its log must show the route. The daemon
# logs critical lines only until told otherwise, and the route is reported at the info level.
ot_ctl_done log level 4 || die 'log level failed'
ot_ctl_done prefix add "${omr_prefix}" paos || die 'prefix add failed'
ot_ctl_done netdata register || die 'netdata register failed'
timeout_run 10 kernel_route_known "${omr_prefix}" || die 'the OMR prefix was not installed as a route through the Thread interface'
timeout_run 10 grep -qE "(Successfully added|Failed to add) an OMR route ${logged_prefix} in kernel" "${OT_DAEMON_OUTPUT}" \
|| die 'the daemon did not report the route of the OMR prefix'
grep -qF "Successfully added an OMR route ${logged_prefix} in kernel" "${OT_DAEMON_OUTPUT}" \
|| die 'the daemon failed to install the route of the OMR prefix'
ot_ctl_done prefix remove "${omr_prefix}" || die 'prefix remove failed'
ot_ctl_done netdata register || die 'netdata register failed'
timeout_run 10 kernel_route_gone "${omr_prefix}" || die 'the route of the removed OMR prefix is still installed'
timeout_run 10 grep -qE "(Successfully deleted|Failed to delete) an OMR route ${logged_prefix} in kernel" "${OT_DAEMON_OUTPUT}" \
|| die 'the daemon did not report the removal of the route of the OMR prefix'
grep -qF "Successfully deleted an OMR route ${logged_prefix} in kernel" "${OT_DAEMON_OUTPUT}" \
|| die 'the daemon failed to delete the route of the OMR prefix'
}
set_host_address()
{
if [[ ${OSTYPE} == darwin* ]]; then
sudo ifconfig "${NETIF_NAME}" inet6 "$2" prefixlen 64 "$([[ $1 == add ]] && echo alias || echo -alias)"
else
sudo ip -6 addr "$1" "$2/64" dev "${NETIF_NAME}"
fi
}
check_daemon_address_events()
{
local address='fd00:1234:5678::1'
# ot-ctl prints every address as eight groups, without "::"; `ipaddr -v` adds the origin, the prefix
# length taken from the trimmed netmask and the flags the address was added with.
local listed='fd00:1234:5678:0:0:0:0:1'
local listed_verbose="${listed} origin:manual plen:64 preferred:1 valid:1"
[[ ${OT_DAEMON} == 'on' ]] || return 0
# The routing socket (netlink on Linux) reports every address and multicast membership of the
# interface to the daemon, its own included, and the daemon logs each report at info level.
grep -q 'ADD \[U\] ' "${OT_DAEMON_OUTPUT}" || die "the reports of OpenThread's own addresses did not reach ot-daemon"
grep -q 'ADD \[M\] ' "${OT_DAEMON_OUTPUT}" || die "the reports of the interface's multicast memberships did not reach ot-daemon"
# An address the host adds becomes an address of OpenThread and goes with its removal.
set_host_address add "${address}"
timeout_run 10 host_address_known "${listed_verbose}" || die 'an address added on the host did not reach OpenThread'
set_host_address del "${address}"
timeout_run 10 host_address_gone "${listed}" || die 'an address removed on the host is still known to OpenThread'
}
# The CLI ends its lines with CR LF; the CR goes before the whole-line match.
host_address_known()
{
local addresses
addresses=$("${OT_CTL[@]}" -I "${NETIF_NAME}" ipaddr -v) || return 1
grep -Fx "$1" <<<"${addresses//$'\r'/}" >/dev/null
}
host_address_gone()
{
local addresses
addresses=$("${OT_CTL[@]}" -I "${NETIF_NAME}" ipaddr) || return 1
! grep -Fx "$1" <<<"${addresses//$'\r'/}" >/dev/null
}
check_daemon_socket_flags()
{
local fifo="$PWD/tmp/ot-ctl-input"
local sockets
# ot-ctl connects before it reads its input; the session stays open until it reads "exit".
mkfifo "${fifo}"
"${OT_CTL[@]}" -I "${NETIF_NAME}" <"${fifo}" &
exec 3>"${fifo}"
# lsof prints the file status flags of each descriptor with +fg: NB for O_NONBLOCK, or ND
# where that is the same flag as O_NDELAY (Linux). Two descriptors of ot-daemon carry the
# socket path: the listening socket, which is non-blocking, and the session it accepted,
# which is served blocking on every platform (on BSD it inherits the flag from the listening
# socket until the daemon clears it).
for _ in $(seq 1 10); do
sockets=$(sudo lsof +fg -U -a -c ot-daemon | grep "openthread-${NETIF_NAME}.sock" || true)
[[ $(echo "${sockets}" | grep -c .) == 2 ]] && break
sleep 1
done
echo "${sockets}"
echo exit >&3 || true
exec 3>&-
wait $! || die 'ot-ctl did not exit cleanly'
rm -f "${fifo}"
# lsof lists the descriptors in order; the listening socket was opened first.
[[ $(echo "${sockets}" | grep -c .) == 2 ]] || die 'ot-daemon does not hold a listening and a session socket'
echo "${sockets}" | sed -n '1p' | grep -qwE 'NB|ND' || die 'the listening socket must be non-blocking'
! echo "${sockets}" | sed -n '2p' | grep -qwE 'NB|ND' || die 'the session socket must be blocking'
}
do_check()
{
trap at_exit INT TERM EXIT
sudo rm -rf tmp
mkdir tmp
SOCAT_PTY="$PWD/tmp/ot-rcp-pty"
OT_OUTPUT="$PWD/tmp/ot-output"
OT_DAEMON_OUTPUT="$PWD/tmp/ot-daemon-output"
RADIO_NCP_PATH="$PWD/build/simulation/examples/apps/ncp/ot-rcp"
socat pty,raw,echo=0,link="${SOCAT_PTY}" exec:"${RADIO_NCP_PATH} 1" &
timeout_run 10 test -c "${SOCAT_PTY}"
echo 'SOCAT_PTY' "${SOCAT_PTY}"
# Cover setting a valid network interface name. On macOS only a utun number can be chosen.
if [[ ${OSTYPE} == darwin* ]]; then
VALID_NETIF_NAME="utun$((200 + 10#$(date +%S)))"
else
VALID_NETIF_NAME="wan$(date +%H%M%S)"
fi
readonly VALID_NETIF_NAME
RADIO_URL="spinel+hdlc+uart://${SOCAT_PTY}?region=US&max-power-table-mbm=1100,1200,1300,1400,1500,1600,1700,1800,1900,2000,2100,2200,2300,2400,2500,2600"
if [[ ${OT_DAEMON} == 'on' ]]; then
# The log file is in our own tmp directory, so the redirect does not need sudo.
# shellcheck disable=SC2024
sudo -E "$PWD/build/posix/src/posix/ot-daemon" -d5 -v -I "${VALID_NETIF_NAME}" "${RADIO_URL}" >"${OT_DAEMON_OUTPUT}" 2>&1 &
OT_DAEMON_JOB=$!
sleep 3
NETIF_NAME=$(grep -o 'Thread interface: .\+' "${OT_DAEMON_OUTPUT}" | cut -d: -f2 | tr -d ' \r\n')
[[ ${NETIF_NAME} == "${VALID_NETIF_NAME}" ]] || die "ot-daemon created ${NETIF_NAME}, not the requested ${VALID_NETIF_NAME}"
OT_CTL_PATH="$PWD/build/posix/src/posix/ot-ctl"
if [[ ${OT_DAEMON_ALLOW_ALL} == 1 ]] || [[ -n ${OT_ISOLATED_REEXEC:-} ]]; then
OT_CTL=("${OT_CTL_PATH}")
else
OT_CTL=(sudo "${OT_CTL_PATH}")
fi
# -d5 asks for debug logging; the daemon's output must carry OpenThread's own log lines, not only the
# syslog() lines of its startup.
timeout_run 10 grep -qE '\[(D|I)\] ' "${OT_DAEMON_OUTPUT}" || die 'the daemon does not log at the level -d asked for'
"${OT_CTL[@]}" -I "${NETIF_NAME}" panid 0xface | grep 'Done' || die 'failed to set panid with ot-ctl'
# verify supports options in OpenThread commands without separator --
"${OT_CTL[@]}" -I "${NETIF_NAME}" pskc -p 123456 | grep 'Done' || die 'unable to set pskc'
# verify this reset and factoryreset end immediately
"${OT_CTL[@]}" -I "${NETIF_NAME}" reset
# sleep a while for daemon ready
sleep 2
"${OT_CTL[@]}" -I "${NETIF_NAME}" factoryreset
# sleep a while for daemon ready
sleep 2
OPENTHREAD_CONFIG_CLI_MAX_LINE_LENGTH=640
readonly OPENTHREAD_CONFIG_CLI_MAX_LINE_LENGTH
local -r kMaxStringLength="$((OPENTHREAD_CONFIG_CLI_MAX_LINE_LENGTH - 1))"
# verify success if command length doesn't exceed the limit
for len in $(seq 1 ${kMaxStringLength}); do
"${OT_CTL[@]}" -I "${NETIF_NAME}" "$(printf '1%.0s' $(seq 1 "${len}"))"
done
# verify failure if command length exceeds the limit
len=${OPENTHREAD_CONFIG_CLI_MAX_LINE_LENGTH}
if "${OT_CTL[@]}" -I "${NETIF_NAME}" "$(printf '1%.0s' $(seq 1 "${len}"))"; then
die
fi
# check_daemon_multicast_group reads INFO lines of the netif; -d5 above gives them along
# with everything at DEBG, so settle on INFO for the rest of the run.
"${OT_CTL[@]}" -I "${NETIF_NAME}" log level 4 | grep 'Done' || die 'failed to set log level'
check_daemon_socket_flags
OT_CLI_CMD="${OT_CTL[*]} -I ${NETIF_NAME}"
else
OT_CLI="$PWD/build/posix/src/posix/ot-cli"
sudo "${OT_CLI}" -I "${VALID_NETIF_NAME}" -n "${RADIO_URL}"
# Cover setting a too long(max is 15 characters) network interface name.
# Expect exit code to be 2(OT_EXIT_INVALID_ARGUMENTS).
INVALID_NETIF_NAME="wan0123456789123"
readonly INVALID_NETIF_NAME
sudo "${OT_CLI}" -I "${INVALID_NETIF_NAME}" -n "${RADIO_URL}" || test $? = 2
if [[ ${OSTYPE} == darwin* ]]; then
# A utun name needs a number. The check runs when the interface is created, after a dry run ends,
# so no -n here; the CLI exits with 2 (OT_EXIT_INVALID_ARGUMENTS) before it would block.
sudo "${OT_CLI}" -I utunabc "${RADIO_URL}" || test $? = 2
fi
OT_CLI_CMD="$PWD/build/posix/src/posix/ot-cli ${RADIO_URL}"
fi
sudo expect <<EOF | tee "${OT_OUTPUT}" &
spawn ${OT_CLI_CMD}
expect_after {
timeout { error }
}
send "region\r\n"
expect "US"
expect "Done"
send "dataset init new\r\n"
expect "Done"
send "dataset commit active\r\n"
expect "Done"
send "routerselectionjitter 1\r\n"
expect "Done"
send "ifconfig up\r\n"
expect "Done"
send "thread start\r\n"
expect "Done"
sleep 10
send "state\r\n"
expect "leader"
expect "Done"
send "extaddr\r\n"
expect "Done"
send "dataset active\r\n"
expect "Done"
send "ipaddr\r\n"
expect "Done"
send "coex\r\n"
expect "Done"
send "coap start\r\n"
expect "Done"
send "coap resource TestResource\r\n"
expect "Done"
send "coap set TestContent\r\n"
expect "Done"
set timeout -1
expect eof
EOF
sleep 5
# wait until the node becomes leader
timeout_run 10 wait_for_leader
# wait coap service start
sleep 5
sudo lsof -iUDP:5683 -nP || die 'Application CoAP port is not available!'
extaddr=$(grep -ao -A +1 'extaddr' "$OT_OUTPUT" | tail -n1 | tr -d '\r\n\0')
echo "Extended address is: ${extaddr}"
prefix=$(grep -ao 'Mesh Local Prefix: [0-9a-f:]\+' "$OT_OUTPUT" | cut -d: -f2- | tr -d ' \r\n')
LEADER_ALOC="${prefix}ff:fe00:fc00"
if [[ ${OT_DAEMON} == 'on' ]]; then
# ot-daemon serves one session at a time and the interactive ot-ctl of the expect
# session reconnects whenever it is displaced, so end it before using ot-ctl below.
sudo killall -9 expect || true
sudo killall -9 ot-ctl || true
fi
check_daemon_multicast_group Added
check_daemon_address_events
# A group an application joins on the host becomes a membership of OpenThread and goes with the leave
# (netlink on Linux, RTM_NEWMADDR/RTM_DELMADDR on BSD).
if [[ ${OT_DAEMON} == 'on' ]]; then
NETIF_INDEX=$(python3 -c 'import socket, sys; print(socket.if_nametoindex(sys.argv[1]))' "${NETIF_NAME}")
sudo env PATH="$(dirname "${OT_CTL_PATH}"):${PATH}" \
python3 "$PWD/tests/scripts/misc/test_multicast_join.py" "${NETIF_INDEX}" "${NETIF_NAME}" \
|| die 'multicast group join failed'
fi
# skip testing CoAP for https://github.com/openthread/openthread/issues/6363
if [[ $OSTYPE != "linux-gnu"* ]]; then
check_daemon_omr_route
check_daemon_netif_down
return 0
fi
# Retrievie test resource through application CoAP
coap_response=$(coap-client -B 5 -m GET "coap://[${LEADER_ALOC}]:5683/TestResource")
echo "CoAP response is: ${coap_response}"
# Verify CoAP response contains the test content
if [[ ${coap_response} == *TestContent* ]]; then
echo 'Success'
else
die 'Failed to access application CoAP'
fi
# Retrievie extended address through network diagnostic get
coap_response=$(echo -n '120100' | xxd -r -p | coap-client -B 5 -m POST "coap://[${LEADER_ALOC}]:61631/d/dg" -f-)
# Verify Tmf CoAP is blocked
if [[ -z ${coap_response} ]]; then
die 'Tmf is not blocked'
fi
check_daemon_omr_route
check_daemon_netif_down
}
main()
{
if [[ $# == 0 ]]; then
do_build
do_check
return 0
fi
while [[ $# != 0 ]]; do
case $1 in
build)
do_build
;;
check)
# shellcheck source=script/_maybe_isolate
source script/_maybe_isolate
do_check
;;
*)
echo "Unknown action: $1"
return 1
;;
esac
shift
done
}
main "$@"