mirror of
https://github.com/DragonOS-Community/DragonOS.git
synced 2026-09-08 23:57:59 +08:00
feat(user): add dormant userspace DHCP service (#2245)
Introduce an opt-in dragon-network control plane for DHCP, static, and externally managed interfaces without changing the default kernel DHCP owner. The service validates root-owned data-only configuration, serializes per-interface control and lease mutations, records crash-recoverable network transactions, preserves external address and route ownership, and derives managed DNS atomically under /run. Add bounded and identity-checked udhcpc lifecycle handling, including DragonOS-compatible zombie detection, and mount /run as tmpfs during early userspace initialization. No interface configuration or startup hook is installed by default. Validated with make fmt, make kernel, make user, isolated Linux network namespaces using BusyBox, fault-injected transaction and route-ownership scenarios, and DragonOS QEMU boot plus DHCP lifecycle smoke tests. Signed-off-by: longjin <longjin@dragonos.org>
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
dragon-network interface configuration
|
||||
======================================
|
||||
|
||||
This directory is intentionally empty by default. Files named IFACE.conf are
|
||||
only read after an administrator explicitly runs:
|
||||
|
||||
/usr/sbin/dragon-network start IFACE
|
||||
|
||||
PR-07 does not start this service from rcS. Until the default ownership switch
|
||||
is made in PR-08, do not enable DHCP on the physical eth* interface already
|
||||
managed by the kernel DHCP worker. Use an isolated interface such as veth1.
|
||||
|
||||
The format is data, not shell syntax. Each non-comment line is a key followed
|
||||
by one value. Unknown keys, duplicate scalar keys, unsafe ownership/modes, and
|
||||
invalid values are rejected. dns and search are repeatable list keys.
|
||||
|
||||
DHCP:
|
||||
|
||||
mode dhcp
|
||||
|
||||
Static IPv4:
|
||||
|
||||
mode static
|
||||
address 192.0.2.10/24
|
||||
gateway 192.0.2.1
|
||||
dns 192.0.2.53
|
||||
dns 192.0.2.54
|
||||
search example.test
|
||||
|
||||
External ownership (unmanaged is an alias):
|
||||
|
||||
mode external
|
||||
|
||||
Commands:
|
||||
|
||||
dragon-network start IFACE
|
||||
dragon-network stop IFACE
|
||||
dragon-network renew IFACE
|
||||
dragon-network status IFACE
|
||||
|
||||
The service uses only /bin/busybox ip and stores private per-interface state in
|
||||
/run/dragon-network/interfaces. Its atomically generated, multi-interface DNS
|
||||
view is /run/dragon-network/resolv.conf. PR-07 does not replace or rewrite
|
||||
/etc/resolv.conf.
|
||||
@@ -11,6 +11,19 @@ if [ ! -d /tmp ]; then
|
||||
fi
|
||||
mount -t tmpfs tmpfs /tmp || /bin/busybox mount -t tmpfs tmpfs /tmp
|
||||
|
||||
# Runtime state must not survive reboot. Network services are not started here;
|
||||
# PR-08 owns the later switch from kernel DHCP to userspace policy.
|
||||
if [ ! -d /run ]; then
|
||||
mkdir -p /run
|
||||
fi
|
||||
if ! /bin/busybox mountpoint -q /run; then
|
||||
if mount -t tmpfs tmpfs /run || /bin/busybox mount -t tmpfs tmpfs /run; then
|
||||
/bin/busybox chmod 0755 /run
|
||||
else
|
||||
echo "[rcS] Warning: failed to mount /run tmpfs"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 如果 rootfs 中带有 apt,则按 Ubuntu 24.04 的 deb822 sources 格式切换到 USTC HTTP 源。
|
||||
if command -v apt >/dev/null 2>&1 || command -v apt-get >/dev/null 2>&1; then
|
||||
echo "[rcS] apt detected, switching Ubuntu 24.04 apt sources to USTC..."
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+93
@@ -0,0 +1,93 @@
|
||||
#!/bin/busybox sh
|
||||
|
||||
set -f
|
||||
|
||||
. /usr/lib/dragon-network/common.sh
|
||||
|
||||
dn_first_router() {
|
||||
local value first
|
||||
first=
|
||||
for value in ${router:-}; do
|
||||
dn_valid_host_ipv4 "$value" || return 1
|
||||
[ -n "$first" ] || first=$value
|
||||
done
|
||||
DN_DHCP_ROUTER=$first
|
||||
}
|
||||
|
||||
dn_dhcp_prefix() {
|
||||
if [ -z "${subnet:-}" ]; then
|
||||
[ -z "${mask:-}" ] || return 1
|
||||
DN_PREFIX=32
|
||||
return 0
|
||||
fi
|
||||
dn_prefix_from_netmask "$subnet" || return 1
|
||||
if [ -n "${mask:-}" ]; then
|
||||
dn_valid_prefix "$mask" && [ "$mask" -eq "$DN_PREFIX" ] || return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
dn_validate_dhcp_event() {
|
||||
local value search_values
|
||||
dn_valid_host_ipv4 "${ip:-}" || return 1
|
||||
dn_dhcp_prefix || return 1
|
||||
dn_first_router || return 1
|
||||
dn_valid_ipv4_list "${dns:-}" || return 1
|
||||
|
||||
if [ -n "${search:-}" ]; then
|
||||
search_values=$search
|
||||
else
|
||||
search_values=${domain:-}
|
||||
fi
|
||||
dn_valid_domain_list "$search_values" || return 1
|
||||
if [ -n "${lease:-}" ]; then
|
||||
dn_valid_uint "$lease" || return 1
|
||||
fi
|
||||
|
||||
DN_DHCP_ADDRESS="$ip/$DN_PREFIX"
|
||||
DN_DHCP_GATEWAY_ROUTE=
|
||||
if [ "$DN_PREFIX" -eq 32 ] && [ -n "$DN_DHCP_ROUTER" ]; then
|
||||
DN_DHCP_GATEWAY_ROUTE="$DN_DHCP_ROUTER/32"
|
||||
fi
|
||||
DN_DHCP_DNS=${dns:-}
|
||||
DN_DHCP_SEARCH=$search_values
|
||||
return 0
|
||||
}
|
||||
|
||||
[ "$#" -eq 1 ] || {
|
||||
dn_log udhcpc "missing DHCP event"
|
||||
exit 2
|
||||
}
|
||||
event=$1
|
||||
iface=${interface:-}
|
||||
dn_valid_ifname "$iface" || {
|
||||
dn_log udhcpc "invalid interface name"
|
||||
exit 1
|
||||
}
|
||||
dn_init_runtime && dn_init_interface_runtime "$iface" || exit 1
|
||||
|
||||
case "$event" in
|
||||
deconfig)
|
||||
dn_deconfigure "$iface"
|
||||
;;
|
||||
bound | renew)
|
||||
dn_load_config "$iface" && [ "$DN_CONFIG_MODE" = dhcp ] || {
|
||||
dn_write_error "$iface" "DHCP event rejected because mode is not dhcp"
|
||||
exit 1
|
||||
}
|
||||
dn_validate_dhcp_event || {
|
||||
dn_write_error "$iface" "invalid DHCP lease data for $event"
|
||||
exit 1
|
||||
}
|
||||
dn_apply "$iface" dhcp "$DN_DHCP_ADDRESS" "$DN_DHCP_ROUTER" \
|
||||
"$DN_DHCP_GATEWAY_ROUTE" "$DN_DHCP_DNS" "$DN_DHCP_SEARCH"
|
||||
;;
|
||||
nak | leasefail)
|
||||
# BusyBox drives the following state transition and deconfig event.
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
dn_write_error "$iface" "unsupported DHCP event: $event"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Executable
+287
@@ -0,0 +1,287 @@
|
||||
#!/bin/busybox sh
|
||||
|
||||
set -f
|
||||
|
||||
. /usr/lib/dragon-network/common.sh
|
||||
|
||||
dn_usage() {
|
||||
echo "usage: dragon-network {start|stop|renew|status} IFACE" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
dn_pid_value() {
|
||||
local pid
|
||||
[ -f "$DN_PID_FILE" ] && [ ! -L "$DN_PID_FILE" ] || return 1
|
||||
IFS= read -r pid < "$DN_PID_FILE" || return 1
|
||||
dn_valid_uint "$pid" && [ "$pid" -gt 1 ] || return 1
|
||||
DN_CLIENT_PID=$pid
|
||||
return 0
|
||||
}
|
||||
|
||||
dn_pid_matches_client() {
|
||||
local iface pid cmdline old_ifs arg previous saw_udhcpc iface_count pid_count script_count saw_foreground saw_release
|
||||
iface=$1
|
||||
dn_pid_value || return 1
|
||||
pid=$DN_CLIENT_PID
|
||||
[ -r "/proc/$pid/cmdline" ] || return 1
|
||||
cmdline=$($DN_BUSYBOX tr '\000' '\n' < "/proc/$pid/cmdline" 2>/dev/null) || return 1
|
||||
old_ifs=$IFS
|
||||
IFS='
|
||||
'
|
||||
set -- $cmdline
|
||||
IFS=$old_ifs
|
||||
saw_udhcpc=0
|
||||
iface_count=0
|
||||
pid_count=0
|
||||
script_count=0
|
||||
saw_foreground=0
|
||||
saw_release=0
|
||||
previous=
|
||||
for arg in "$@"; do
|
||||
[ "$arg" = udhcpc ] && saw_udhcpc=1
|
||||
if [ "$previous" = -i ]; then
|
||||
[ "$arg" = "$iface" ] || return 1
|
||||
iface_count=$((iface_count + 1))
|
||||
fi
|
||||
if [ "$previous" = -p ]; then
|
||||
[ "$arg" = "$DN_PID_FILE" ] || return 1
|
||||
pid_count=$((pid_count + 1))
|
||||
fi
|
||||
if [ "$previous" = -s ]; then
|
||||
[ "$arg" = "$DN_UDHCPC_SCRIPT" ] || return 1
|
||||
script_count=$((script_count + 1))
|
||||
fi
|
||||
[ "$arg" = -f ] && saw_foreground=1
|
||||
[ "$arg" = -R ] && saw_release=1
|
||||
previous=$arg
|
||||
done
|
||||
[ "$saw_udhcpc" -eq 1 ] && [ "$iface_count" -eq 1 ] && [ "$pid_count" -eq 1 ] &&
|
||||
[ "$script_count" -eq 1 ] && [ "$saw_foreground" -eq 1 ] &&
|
||||
[ "$saw_release" -eq 1 ] && $DN_BUSYBOX kill -0 "$pid" 2>/dev/null
|
||||
}
|
||||
|
||||
dn_remove_stale_pid() {
|
||||
[ ! -e "$DN_PID_FILE" ] || $DN_BUSYBOX rm -f "$DN_PID_FILE"
|
||||
}
|
||||
|
||||
dn_process_alive() {
|
||||
local pid stat rest state
|
||||
pid=$1
|
||||
$DN_BUSYBOX kill -0 "$pid" 2>/dev/null || return 1
|
||||
[ -r "/proc/$pid/stat" ] || return 0
|
||||
IFS= read -r stat < "/proc/$pid/stat" || return 0
|
||||
# Field 2 is parenthesized and may contain spaces. Stripping the longest
|
||||
# `*) ` prefix leaves the Linux-compatible state character as field 3.
|
||||
rest=${stat##*) }
|
||||
[ "$rest" != "$stat" ] || return 0
|
||||
state=${rest%% *}
|
||||
[ "$state" != Z ] && [ "$state" != X ]
|
||||
}
|
||||
|
||||
dn_wait_for_exit() {
|
||||
local pid limit attempt
|
||||
pid=$1
|
||||
limit=$2
|
||||
attempt=0
|
||||
while [ "$attempt" -lt "$limit" ]; do
|
||||
dn_process_alive "$pid" || return 0
|
||||
$DN_BUSYBOX sleep 1
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
! dn_process_alive "$pid"
|
||||
}
|
||||
|
||||
dn_finish_failed_start() {
|
||||
local iface child_pid message
|
||||
iface=$1
|
||||
child_pid=$2
|
||||
message=$3
|
||||
wait "$child_pid" 2>/dev/null || :
|
||||
dn_remove_stale_pid || {
|
||||
dn_write_error "$iface" "failed to remove invalid udhcpc pidfile"
|
||||
return 1
|
||||
}
|
||||
if dn_deconfigure "$iface"; then
|
||||
dn_write_error "$iface" "$message"
|
||||
else
|
||||
dn_write_error "$iface" "failed to clean network after invalid udhcpc startup"
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
dn_stop_client_locked() {
|
||||
local iface pid
|
||||
iface=$1
|
||||
if ! dn_pid_matches_client "$iface"; then
|
||||
dn_remove_stale_pid || return 1
|
||||
return 0
|
||||
fi
|
||||
pid=$DN_CLIENT_PID
|
||||
if ! $DN_BUSYBOX kill -TERM "$pid" 2>/dev/null; then
|
||||
# The validated client may exit between the identity check and kill(2).
|
||||
# Treat that as an idempotent stop, but never hide a real signalling
|
||||
# failure while the same client is still alive.
|
||||
if ! dn_pid_matches_client "$iface"; then
|
||||
dn_remove_stale_pid || return 1
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
if ! dn_wait_for_exit "$pid" 5; then
|
||||
dn_write_error "$iface" "udhcpc did not stop within 5 seconds"
|
||||
return 1
|
||||
fi
|
||||
dn_remove_stale_pid || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
dn_start_dhcp_locked() {
|
||||
local iface attempt child_pid pid_from_file
|
||||
iface=$1
|
||||
if dn_pid_matches_client "$iface"; then
|
||||
return 0
|
||||
fi
|
||||
dn_remove_stale_pid || return 1
|
||||
dn_deconfigure "$iface" || return 1
|
||||
$DN_BUSYBOX ip link set dev "$iface" up || return 1
|
||||
|
||||
(
|
||||
exec 9>&-
|
||||
exec $DN_BUSYBOX env -i $DN_BUSYBOX udhcpc -f -R -i "$iface" \
|
||||
-s "$DN_UDHCPC_SCRIPT" -p "$DN_PID_FILE"
|
||||
) </dev/null >/dev/null 2>&1 &
|
||||
child_pid=$!
|
||||
|
||||
attempt=0
|
||||
while [ "$attempt" -lt 3 ]; do
|
||||
if ! dn_process_alive "$child_pid"; then
|
||||
dn_finish_failed_start "$iface" "$child_pid" "udhcpc exited during startup"
|
||||
return 1
|
||||
fi
|
||||
if dn_pid_value; then
|
||||
pid_from_file=$DN_CLIENT_PID
|
||||
if [ "$pid_from_file" = "$child_pid" ] && dn_pid_matches_client "$iface"; then
|
||||
dn_clear_error
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
$DN_BUSYBOX sleep 1
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
$DN_BUSYBOX kill -TERM "$child_pid" 2>/dev/null || :
|
||||
if ! dn_wait_for_exit "$child_pid" 5; then
|
||||
# No valid pidfile exists on this path, so leaving the known child
|
||||
# alive would let a later start create a competing DHCP client.
|
||||
$DN_BUSYBOX kill -KILL "$child_pid" 2>/dev/null || :
|
||||
if ! dn_wait_for_exit "$child_pid" 5; then
|
||||
dn_write_error "$iface" "invalid udhcpc startup could not be terminated"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
dn_finish_failed_start "$iface" "$child_pid" "udhcpc did not publish a valid pidfile"
|
||||
return 1
|
||||
}
|
||||
|
||||
dn_start_static_locked() {
|
||||
local iface prefix gateway_route
|
||||
iface=$1
|
||||
dn_stop_client_locked "$iface" || return 1
|
||||
$DN_BUSYBOX ip link set dev "$iface" up || return 1
|
||||
prefix=${DN_CONFIG_ADDRESS#*/}
|
||||
gateway_route=
|
||||
if [ "$prefix" = 32 ] && [ -n "$DN_CONFIG_GATEWAY" ]; then
|
||||
gateway_route="$DN_CONFIG_GATEWAY/32"
|
||||
fi
|
||||
dn_apply "$iface" static "$DN_CONFIG_ADDRESS" "$DN_CONFIG_GATEWAY" "$gateway_route" \
|
||||
"$DN_CONFIG_DNS" "$DN_CONFIG_SEARCH"
|
||||
}
|
||||
|
||||
dn_start_unmanaged_locked() {
|
||||
local iface
|
||||
iface=$1
|
||||
dn_stop_client_locked "$iface" || return 1
|
||||
dn_deconfigure "$iface"
|
||||
}
|
||||
|
||||
dn_status_locked() {
|
||||
local iface running result
|
||||
iface=$1
|
||||
running=0
|
||||
result=0
|
||||
if dn_pid_matches_client "$iface"; then
|
||||
echo "$iface: dhcp client running (pid $DN_CLIENT_PID)"
|
||||
running=1
|
||||
else
|
||||
dn_remove_stale_pid || return 1
|
||||
fi
|
||||
dn_acquire_state_lock || return 1
|
||||
if ! dn_load_pending; then
|
||||
echo "$iface: invalid pending transaction state" >&2
|
||||
result=1
|
||||
elif [ "$DN_PENDING_PRESENT" -eq 1 ]; then
|
||||
echo "$iface: dragon-network transaction recovery pending" >&2
|
||||
result=1
|
||||
elif ! dn_load_lease "$DN_LEASE_FILE"; then
|
||||
echo "$iface: invalid committed lease state" >&2
|
||||
result=1
|
||||
elif [ "$DN_LEASE_PRESENT" -eq 1 ]; then
|
||||
echo "$iface: $DN_LEASE_OWNER configuration committed"
|
||||
elif [ "$running" -eq 1 ]; then
|
||||
echo "$iface: awaiting DHCP lease"
|
||||
else
|
||||
echo "$iface: unmanaged"
|
||||
fi
|
||||
[ ! -f "$DN_ERROR_FILE" ] || {
|
||||
printf '%s: last error: ' "$iface"
|
||||
$DN_BUSYBOX cat "$DN_ERROR_FILE"
|
||||
}
|
||||
exec 7>&-
|
||||
return "$result"
|
||||
}
|
||||
|
||||
[ "$#" -eq 2 ] || dn_usage
|
||||
command=$1
|
||||
iface=$2
|
||||
dn_valid_ifname "$iface" || {
|
||||
dn_log "$iface" "invalid interface name"
|
||||
exit 2
|
||||
}
|
||||
dn_init_runtime && dn_init_interface_runtime "$iface" || exit 1
|
||||
|
||||
control_lock="$DN_IFACE_RUN/control.lock"
|
||||
[ ! -L "$control_lock" ] || exit 1
|
||||
: > "$control_lock" || exit 1
|
||||
$DN_BUSYBOX chmod 0600 "$control_lock" || exit 1
|
||||
exec 9>"$control_lock" || exit 1
|
||||
$DN_BUSYBOX flock -x 9 || exit 1
|
||||
|
||||
case "$command" in
|
||||
start)
|
||||
dn_load_config "$iface" || {
|
||||
dn_write_error "$iface" "invalid interface configuration"
|
||||
exit 1
|
||||
}
|
||||
case "$DN_CONFIG_MODE" in
|
||||
dhcp) dn_start_dhcp_locked "$iface" ;;
|
||||
static) dn_start_static_locked "$iface" ;;
|
||||
unmanaged) dn_start_unmanaged_locked "$iface" ;;
|
||||
esac
|
||||
;;
|
||||
stop)
|
||||
dn_stop_client_locked "$iface" && dn_deconfigure "$iface"
|
||||
;;
|
||||
renew)
|
||||
if dn_pid_matches_client "$iface"; then
|
||||
$DN_BUSYBOX kill -USR1 "$DN_CLIENT_PID"
|
||||
else
|
||||
dn_remove_stale_pid
|
||||
dn_write_error "$iface" "no running DHCP client to renew"
|
||||
false
|
||||
fi
|
||||
;;
|
||||
status)
|
||||
dn_status_locked "$iface"
|
||||
;;
|
||||
*) dn_usage ;;
|
||||
esac
|
||||
Reference in New Issue
Block a user