Files
infix/board/common/rootfs/usr/share/udhcpc/default.script
T
Joachim Wiberg 221fea13a2 netd: watch conf.d with inotify, retry on backend failure
Two races could prevent DHCP-learned default routes from being installed
at boot:

1. The signal from the DHCP client script could be lost leaving conf.d
   updated but frr.conf stale.

2. Even when the signal was received, 'vtysh -b' could fail because the
   FRR daemon chain (mgmtd→zebra→staticd) writes PID files before being
   fully operational, causing netd to give up with no retry.

Fix both by refactoring netd to use libev:

- Use an inotify watch of CONF_DIR, so netd reacts directly to file
  changes without depending on signal delivery.

- On backend_apply() failure, schedule a retry in 5s and call pidfile()
  unconditionally so dependent services are not blocked waiting for the
  Finit condition 'pid/netd' to be satisfied.

We also take this opportunity to rename /etc/netd/conf.d/ to /etc/net.d/
The extra /etc/netd/ directory level served no purpose — nothing else
lives there.  Flatten to /etc/net.d/ which reads more naturally as the
drop-in directory for network configuration snippets.

Also, reduce logging a bit since each netd backend already logs success
or fail which is sufficient to know that a configuration change has been
applied or not.

Finally, with the new inotify processing in netd it's redundant to call
'initctl reload netd' from the udhcpc script, in fact it will only cause
unnecessary overhead, so we drop it.

Fixes #1438

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-03-12 12:07:49 +01:00

251 lines
5.8 KiB
Bash
Executable File

#!/bin/sh
# This script expect a system with resolvconf (openresolv) and iproute2
[ -z "$1" ] && echo "Error: should be called from udhcpc" && exit 1
ACTION="$1"
IP_CACHE="/var/lib/misc/${interface}.cache"
RESOLV_CONF="/run/resolvconf/interfaces/${interface}.conf"
NTPFILE="/run/chrony/dhcp-sources.d/${interface}.sources"
NAME="/etc/net.d/${interface}-dhcp.conf"
NEXT="${NAME}+"
[ -n "$broadcast" ] && BROADCAST="broadcast $broadcast"
[ -n "$subnet" ] || subnet=32
[ -n "$metric" ] || metric=5
# Handle stateful DHCPv6 like DHCPv4
[ -n "$ipv6" ] && ip="$ipv6/128"
if [ -z "${IF_WAIT_DELAY}" ]; then
IF_WAIT_DELAY=10
fi
log()
{
logger -I $$ -t udhcpc -p user.notice "${interface}: $*"
}
dbg()
{
logger -I $$ -t udhcpc -p user.debug "${interface}: $*"
}
err()
{
logger -I $$ -t udhcpc -p user.err "${interface}: $*"
}
wait_for_ipv6_default_route()
{
dbg "waiting for IPv6 default route to be installed."
while [ $IF_WAIT_DELAY -gt 0 ]; do
if ip -6 route list proto dhcp dev $interface | grep -q default; then
return
fi
sleep 1
printf "."
: $((IF_WAIT_DELAY -= 1))
done
err "Timed out waiting for IPv6 default route!"
}
# Check if a DHCP option was requested in the parameter request list
# Returns: 0 if requested, 1 if not requested or config unavailable
was_option_requested()
{
local opt_num="$1"
local config="/etc/finit.d/available/dhcp-client-${interface}.conf"
if [ ! -f "$config" ]; then
dbg "config file not found: $config"
return 1
fi
# Extract udhcpc command line and check for -O <option_num>
if grep -q -- "-O ${opt_num}\b" "$config"; then
return 0
fi
return 1
}
# RFC3442: If the DHCP server returns both a Classless
# Static Routes option and a Router option, the DHCP
# client MUST ignore the Router option.
set_dhcp_routes()
{
echo "# Generated by udhcpc" > "$NEXT"
echo "" >> "$NEXT"
if [ -n "$staticroutes" ]; then
if was_option_requested 121; then
# format: dest1/mask gw1 ... destn/mask gwn
set -- $staticroutes
while [ -n "$1" -a -n "$2" ]; do
dbg "adding route $1 via $2 metric $metric tag 100"
cat >> "$NEXT" <<-EOF
route {
prefix = "$1"
nexthop = "$2"
distance = $metric
tag = 100
}
EOF
shift 2
done
else
log "ignoring unrequested staticroutes (option 121)"
fi
elif [ -n "$router" ] ; then
if was_option_requested 3; then
for i in $router ; do
cat >> "$NEXT" <<-EOF
route {
prefix = "0.0.0.0/0"
nexthop = "$i"
distance = $metric
tag = 100
}
EOF
done
else
log "ignoring unrequested router (option 3)"
fi
fi
# Reduce changes needed by comparing with previous route(s)
cmp -s "$NAME" "$NEXT" && return
mv "$NEXT" "$NAME"
}
clr_dhcp_routes()
{
log "deleting DHCP routes"
[ -f "$NAME" ] || return
rm "$NAME"
}
clr_dhcp_addresses()
{
addrs=$(ip -j addr show dev $interface \
| jq -c '.[0].addr_info[] | select(.family == "inet") | select(.protocol == "dhcp")')
for addr in $addrs; do
ip="$(echo "$addr" | jq -r '."local"')"
prefix="$(echo "$addr" | jq -r '."prefixlen"')"
log "removing $ip/$prefix"
ip addr del "$ip/$prefix" dev "$interface"
done
}
log "action $ACTION"
case "$ACTION" in
deconfig)
clr_dhcp_addresses
clr_dhcp_routes
# drop info from this interface
rm -f "$RESOLV_CONF"
rm -f "$NTPFILE"
if [ -f "/etc/hostname.d/90-dhcp-${interface}" ]; then
log "removing /etc/hostname.d/90-dhcp-${interface}"
rm -f "/etc/hostname.d/90-dhcp-${interface}"
/usr/libexec/infix/hostname dhcp
fi
if [ -x /usr/sbin/avahi-autoipd ]; then
/usr/sbin/avahi-autoipd -c $interface && /usr/sbin/avahi-autoipd -k $interface
fi
;;
leasefail|nak)
if [ -x /usr/sbin/avahi-autoipd ]; then
/usr/sbin/avahi-autoipd -c $interface || /usr/sbin/avahi-autoipd -wD $interface --no-chroot
fi
;;
renew|bound)
if [ -x /usr/sbin/avahi-autoipd ]; then
/usr/sbin/avahi-autoipd -c $interface && /usr/sbin/avahi-autoipd -k $interface
fi
if /bin/ip addr add dev $interface $ip/$subnet $BROADCAST proto dhcp; then
echo "$ip" > "$IP_CACHE"
fi
if [ -n "$ipv6" ] ; then
wait_for_ipv6_default_route
fi
set_dhcp_routes
# set hostname if given and requested
if [ -n "$hostname" ]; then
if was_option_requested 12; then
log "received DHCP hostname: $hostname"
mkdir -p /etc/hostname.d
echo "$hostname" > "/etc/hostname.d/90-dhcp-${interface}"
/usr/libexec/infix/hostname dhcp
else
log "ignoring unrequested hostname (option 12): $hostname"
fi
fi
# drop info from this interface
truncate -s 0 "$RESOLV_CONF"
# prefer rfc3397 domain search list (option 119) if available
search_list=""
if [ -n "$search" ]; then
if was_option_requested 119; then
search_list=$search
else
log "ignoring unrequested search (option 119): $search"
fi
elif [ -n "$domain" ]; then
if was_option_requested 15; then
search_list=$domain
else
log "ignoring unrequested domain (option 15): $domain"
fi
fi
if [ -n "$search_list" ]; then
dbg "adding search $search_list"
echo "search $search_list # $interface" >> $RESOLV_CONF
fi
if [ -n "$dns" ]; then
if was_option_requested 6; then
for i in $dns ; do
dbg "adding dns $i"
echo "nameserver $i # $interface" >> $RESOLV_CONF
resolvconf -u
done
else
log "ignoring unrequested dns (option 6): $dns"
fi
fi
if [ -n "$ntpsrv" ]; then
if was_option_requested 42; then
truncate -s 0 "$NTPFILE"
for srv in $ntpsrv; do
dbg "got NTP server $srv"
echo "server $srv iburst" >> "$NTPFILE"
done
chronyc reload sources >/dev/null
else
log "ignoring unrequested ntpsrv (option 42): $ntpsrv"
fi
fi
esac
HOOK_DIR="$0.d"
for hook in "${HOOK_DIR}/"*; do
[ -f "${hook}" -a -x "${hook}" ] || continue
"${hook}" "$ACTION"
done
exit 0