Skip to content

Instantly share code, notes, and snippets.

@ihipop
Last active August 12, 2026 08:08
Show Gist options
  • Select an option

  • Save ihipop/c95673a362d1084cbffd297cb42aaa5d to your computer and use it in GitHub Desktop.

Select an option

Save ihipop/c95673a362d1084cbffd297cb42aaa5d to your computer and use it in GitHub Desktop.
Transactional br0 network stack for LibreELEC, ConnMan, DHCP and systemd-nspawn
#!/bin/sh
# Transactional br0 network stack for LibreELEC + ConnMan + DHCP client.
#
# Public commands:
# install - provision DHCP support and install/enable units and drop-ins
# enable - create br0.enabled and start bridge mode now
# disable - remove br0.enabled and return to the stock ConnMan
# status - show switch, service and network state
# fallback - force cleanup and return to the stock ConnMan
#
# Internal commands used by systemd:
# topology-up/topology-down - create and remove the bridge topology
# dhcp-run/dhcp-cleanup - run and clean up the DHCP client unit
# reconcile - apply a direct br0.enabled file change
# deconfig/bound/renew - DHCP event hooks
set -eu
resolve_self_path() {
case "$0" in
/*) script_candidate="$0" ;;
*/*) script_candidate="$PWD/$0" ;;
*) script_candidate="$(command -v "$0" 2>/dev/null || printf '%s/%s' "$PWD" "$0")" ;;
esac
case "$script_candidate" in
/*) ;;
*) script_candidate="$PWD/$script_candidate" ;;
esac
readlink -f "$script_candidate" 2>/dev/null || printf '%s\n' "$script_candidate"
}
SCRIPT_SOURCE_PATH="$(resolve_self_path)"
# Values may be overridden with Environment= in the generated systemd units.
PARENT_IF="${PARENT_IF:-eth0}"
BRIDGE_IF="${BRIDGE_IF:-br0}"
SYS_CLASS_NET="${SYS_CLASS_NET:-/sys/class/net}"
ENABLE_FILE="${ENABLE_FILE:-/storage/.config/br0.enabled}"
FALLBACK_FILE="${FALLBACK_FILE:-/run/br0-stack.fallback}"
RUN_DIR="${RUN_DIR:-/run/br0-stack}"
OWNED_FILE="$RUN_DIR/owned"
BRIDGE_PORTS_FILE="$RUN_DIR/detached-bridge-ports"
DHCP_PID_FILE="$RUN_DIR/dhcp-client.pid"
DHCP_KIND_FILE="$RUN_DIR/dhcp-client.kind"
LEASE_ADDR_FILE="$RUN_DIR/lease-address"
DHCLIENT_LEASE_FILE="${DHCLIENT_LEASE_FILE:-/storage/.cache/dhclient-br0.leases}"
CONNMAN_UNIT="${CONNMAN_UNIT:-connman.service}"
CONNMAND_BIN="${CONNMAND_BIN:-/usr/sbin/connmand}"
CONNMAN_DEBUG_FILE="${CONNMAN_DEBUG_FILE:-/run/libreelec/debug/connman.conf}"
CONNMAN_EXTRA_ENV_FILE="${CONNMAN_EXTRA_ENV_FILE:-$RUN_DIR/connman-extra.conf}"
SYSTEM_BUSYBOX_BIN="${SYSTEM_BUSYBOX_BIN:-}"
# The installer uses Alpine's official static BusyBox package only when neither
# the system nor an existing local installation provides udhcpc. The APK and
# extracted binary are both pinned. Runtime commands never download software.
ALPINE_BUSYBOX_BASE_URL="${ALPINE_BUSYBOX_BASE_URL:-https://dl-cdn.alpinelinux.org/alpine/v3.23/main}"
ALPINE_BUSYBOX_VERSION='1.37.0-r30'
LOCAL_BUSYBOX_BIN="${LOCAL_BUSYBOX_BIN:-/storage/.local/bin/busybox}"
LOCAL_UDHCPC_BIN="${LOCAL_UDHCPC_BIN:-/storage/.local/bin/udhcpc}"
ENTWARE_DHCLIENT_BIN="${ENTWARE_DHCLIENT_BIN:-/opt/sbin/dhclient}"
RESOLV_CONF="${RESOLV_CONF:-/etc/resolv.conf}"
SYSTEMD_DIR="${SYSTEMD_DIR:-/storage/.config/system.d}"
if [ "${SCRIPT_INSTALL_PATH+x}" = x ]; then
SCRIPT_INSTALL_PATH_EXPLICIT=1
else
SCRIPT_INSTALL_PATH_EXPLICIT=0
SCRIPT_INSTALL_PATH="$SCRIPT_SOURCE_PATH"
fi
DHCP_CLIENT_KIND=''
DHCP_CLIENT_MODE=''
DHCP_CLIENT_BIN=''
RUN_REASON='unknown'
say() { printf '%s\n' "$*"; }
warn() { say "Warning: $*" >&2; }
die() { RUN_REASON="$*"; say "Error: $*" >&2; exit 1; }
link_exists() { ip link show dev "$1" >/dev/null 2>&1; }
unit_active() { systemctl is-active --quiet "$1" 2>/dev/null; }
wait_until_inactive() {
wait_unit="$1"
wait_limit="${2:-15}"
wait_elapsed=0
while unit_active "$wait_unit" && [ "$wait_elapsed" -lt "$wait_limit" ]; do
sleep 1
wait_elapsed=$((wait_elapsed + 1))
done
! unit_active "$wait_unit"
}
valid_ifname() {
case "$1" in
''|*[!A-Za-z0-9_.:@-]*) return 1 ;;
*) return 0 ;;
esac
}
valid_script_path() {
# Keep the generated systemd ExecStart and udhcpc -s argument unambiguous.
# Normal absolute Unix paths are accepted; whitespace/shell metacharacters
# require relocating or renaming the script before installation.
case "$1" in
/*) ;;
*) return 1 ;;
esac
case "$1" in
*[!A-Za-z0-9_./:@+-]*) return 1 ;;
*) return 0 ;;
esac
}
choose_install_path() {
if [ "$SCRIPT_INSTALL_PATH_EXPLICIT" = 1 ]; then
printf '%s\n' "$SCRIPT_INSTALL_PATH"
return
fi
install_home="${HOME:-/storage}"
install_dir="$install_home/.local/bin"
if [ -t 0 ]; then
printf 'Install directory [~/.local/bin]: ' >&2
IFS= read -r install_answer || install_answer=''
case "$install_answer" in
'') ;;
'~') install_dir="$install_home" ;;
'~/'*) install_dir="$install_home/${install_answer#\~/}" ;;
/*) install_dir="$install_answer" ;;
*) die "The install directory must be an absolute path or start with ~/"
esac
else
say "No interactive terminal; using install directory ~/.local/bin" >&2
fi
printf '%s/br0-stack.sh\n' "${install_dir%/}"
}
system_is_stopping() {
system_state="$(systemctl is-system-running 2>/dev/null || true)"
[ "$system_state" = stopping ]
}
busybox_has_udhcpc() {
candidate_busybox="$1"
[ -x "$candidate_busybox" ] || return 1
"$candidate_busybox" --list 2>/dev/null | grep -qx udhcpc
}
select_system_udhcpc() {
candidate_busybox="$SYSTEM_BUSYBOX_BIN"
if [ -z "$candidate_busybox" ]; then
# PATH may begin with /storage/.local/bin, so it cannot distinguish the
# system BusyBox from a locally installed one.
candidate_busybox=/usr/bin/busybox
fi
if busybox_has_udhcpc "$candidate_busybox"; then
DHCP_CLIENT_KIND='udhcpc-system'
DHCP_CLIENT_MODE='busybox-applet'
DHCP_CLIENT_BIN="$candidate_busybox"
return 0
fi
return 1
}
alpine_busybox_package() {
machine_arch="$(uname -m 2>/dev/null || true)"
case "$machine_arch" in
aarch64|arm64)
alpine_arch=aarch64
alpine_apk_sha256=44c9abdfb970f398fa72c8382fe2d8808eea16beaf82daf6ac708b92f1b8659e
alpine_bin_sha256=198f6f675a49ac734082e054b823d3e2e947180ca2f632852ae1c09e950fd676
;;
armv6l|armhf)
alpine_arch=armhf
alpine_apk_sha256=9715aaed8836523efe8ed511ce6ebae3aba98515092e717b632a3557fa518f4a
alpine_bin_sha256=0a732282c4d7076549c051744b19925ec05b31e323a9e3161cba70a8e363a818
;;
armv7l|armv7|armv8l)
alpine_arch=armv7
alpine_apk_sha256=fb023d6a1e2dce9069b11b302e674c273b9e17d04d17b03ee16526475b0971e6
alpine_bin_sha256=1c0ed2db00c0bbdd796e4bdb4de58c564a3df8e61afc716652004b3ce0ee50f2
;;
loongarch64|loong64)
alpine_arch=loongarch64
alpine_apk_sha256=91e817d5598cdb0cd51706995119fa4058fd1cdd372d8c57db1e2c4a487e7920
alpine_bin_sha256=21b49635e4cf3a0ab54815ec36cce5fffe194fefc8209aada10dc1e4df44de45
;;
ppc64le)
alpine_arch=ppc64le
alpine_apk_sha256=0b7c72091efc25e7583bc1fdd8b14a8e2dfe1b0b34c3bec8e92dcb86735552a5
alpine_bin_sha256=38b1ee144dcd11e0dd5e0f8eb6347d9342d8f375e272b1934eb0ba7e4ca1fe13
;;
riscv64)
alpine_arch=riscv64
alpine_apk_sha256=538f48780e21ef78cd171137cecb4197da2cc3c15d3f1895a23eeea99a4a3614
alpine_bin_sha256=f822bc9975d9d727b802bdeabe4b7ce96691b231345d0cd68fb9e5a47e12f08d
;;
s390x)
alpine_arch=s390x
alpine_apk_sha256=80799f38dad256c47052fe0594ffe82da775d2452e906ac7d3dd555b9ea90f4b
alpine_bin_sha256=668c14a74db856f18d6b787722100926ff19be88cbc7b8b2041e3868bc7ea01f
;;
i386|i486|i586|i686|x86)
alpine_arch=x86
alpine_apk_sha256=e07d58e411bcf23f3515cd11cc6d16d2309c43abdf7ac77ded6e78deb1a7ae9f
alpine_bin_sha256=645f090c12b8f1b74e86a004d44a7af618b3f1e7eb0dfb94e62da7655bca818f
;;
x86_64|amd64)
alpine_arch=x86_64
alpine_apk_sha256=bb30365b954f531938a33de18173342185844828863121d7e821115d2f7f29c9
alpine_bin_sha256=82bbbabec12a985ae58810cfe975c3399264dc888aa592d8e460732bdd30a8dd
;;
*) return 1 ;;
esac
alpine_apk_url="${ALPINE_BUSYBOX_BASE_URL%/}/$alpine_arch/busybox-static-$ALPINE_BUSYBOX_VERSION.apk"
}
install_alpine_busybox_udhcpc() (
if [ -e "$LOCAL_BUSYBOX_BIN" ] || [ -L "$LOCAL_BUSYBOX_BIN" ] || \
[ -e "$LOCAL_UDHCPC_BIN" ] || [ -L "$LOCAL_UDHCPC_BIN" ]
then
warn "Local BusyBox/udhcpc paths already exist but are unusable; refusing to overwrite them"
return 1
fi
local_bin_dir="$(dirname "$LOCAL_BUSYBOX_BIN")"
install_tmp_dir="$local_bin_dir/.br0-stack-busybox.$$"
apk_tmp="$install_tmp_dir/busybox-static.apk"
extract_dir="$install_tmp_dir/root"
binary_tmp="$extract_dir/bin/busybox.static"
mkdir -p "$local_bin_dir" || return 1
mkdir "$install_tmp_dir" || return 1
cleanup_alpine_busybox_install() {
trap - EXIT HUP INT TERM
rm -rf "$install_tmp_dir"
}
trap cleanup_alpine_busybox_install EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
say "Downloading $alpine_apk_url ..."
if ! wget -O "$apk_tmp" "$alpine_apk_url"; then
warn "wget failed to download $alpine_apk_url"
return 1
fi
set -- $(sha256sum "$apk_tmp" 2>/dev/null || true)
if [ "${1:-}" != "$alpine_apk_sha256" ]; then
warn "Downloaded Alpine busybox-static APK failed SHA-256 verification"
return 1
fi
mkdir "$extract_dir" || return 1
if ! tar -xzf "$apk_tmp" -C "$extract_dir" bin/busybox.static; then
warn "Unable to extract bin/busybox.static from the Alpine APK"
return 1
fi
set -- $(sha256sum "$binary_tmp" 2>/dev/null || true)
if [ "${1:-}" != "$alpine_bin_sha256" ]; then
warn "Extracted Alpine BusyBox failed SHA-256 verification"
return 1
fi
if ! chmod 0755 "$binary_tmp" || ! busybox_has_udhcpc "$binary_tmp"; then
warn "Verified Alpine BusyBox cannot run here or does not provide udhcpc"
return 1
fi
if ! mv "$binary_tmp" "$LOCAL_BUSYBOX_BIN"; then
return 1
fi
if ! ln -s "$(basename "$LOCAL_BUSYBOX_BIN")" "$LOCAL_UDHCPC_BIN"; then
warn "Installed $LOCAL_BUSYBOX_BIN but could not create $LOCAL_UDHCPC_BIN"
rm -f "$LOCAL_BUSYBOX_BIN"
return 1
fi
say "Installed Alpine busybox-static for $alpine_arch as $LOCAL_BUSYBOX_BIN"
return 0
)
select_local_udhcpc() {
if [ -x "$LOCAL_UDHCPC_BIN" ]; then
DHCP_CLIENT_KIND='udhcpc-local'
DHCP_CLIENT_MODE='direct-udhcpc'
DHCP_CLIENT_BIN="$LOCAL_UDHCPC_BIN"
return 0
fi
if busybox_has_udhcpc "$LOCAL_BUSYBOX_BIN"; then
DHCP_CLIENT_KIND='udhcpc-local'
DHCP_CLIENT_MODE='busybox-applet'
DHCP_CLIENT_BIN="$LOCAL_BUSYBOX_BIN"
return 0
fi
return 1
}
select_dhcp_client() {
# Priority is intentional and must not follow Entware-first PATH lookup:
# 1. system BusyBox udhcpc applet
# 2. existing local udhcpc/BusyBox
# 3. Entware ISC dhclient
# Downloads are deliberately excluded from every runtime command.
if select_system_udhcpc; then
return 0
fi
if select_local_udhcpc; then
return 0
fi
if [ -x "$ENTWARE_DHCLIENT_BIN" ]; then
DHCP_CLIENT_KIND='dhclient-entware'
DHCP_CLIENT_MODE='isc-dhclient'
DHCP_CLIENT_BIN="$ENTWARE_DHCLIENT_BIN"
return 0
fi
return 1
}
require_dhcp_client() {
select_dhcp_client || \
die "No DHCP client found after checking system/local udhcpc and Entware dhclient; run install while networking is available"
}
provision_dhcp_client() {
if select_system_udhcpc || select_local_udhcpc; then
return 0
fi
if alpine_busybox_package; then
install_alpine_busybox_udhcpc || \
die "Unable to install the verified Alpine BusyBox for $(uname -m 2>/dev/null || printf unknown)"
select_local_udhcpc || \
die "The installed Alpine BusyBox does not provide a usable udhcpc"
return 0
fi
# Entware is an architecture fallback only. If a mapped Alpine package is
# downloaded but fails verification or execution, install must report it.
if [ -x "$ENTWARE_DHCLIENT_BIN" ]; then
DHCP_CLIENT_KIND='dhclient-entware'
DHCP_CLIENT_MODE='isc-dhclient'
DHCP_CLIENT_BIN="$ENTWARE_DHCLIENT_BIN"
warn "Using Entware dhclient because this architecture has no verified Alpine BusyBox mapping"
return 0
fi
die "No DHCP client available: this architecture has no verified Alpine BusyBox mapping, and Entware dhclient is absent"
}
netmask_to_prefix() {
input_mask="$1"
old_ifs="$IFS"
IFS=.
set -- $input_mask
IFS="$old_ifs"
[ "$#" = 4 ] || return 1
prefix=0
partial_seen=0
for octet in "$@"; do
case "$octet" in
255) bits=8 ;;
254) bits=7 ;;
252) bits=6 ;;
248) bits=5 ;;
240) bits=4 ;;
224) bits=3 ;;
192) bits=2 ;;
128) bits=1 ;;
0) bits=0 ;;
*) return 1 ;;
esac
[ "$partial_seen" = 0 ] || [ "$bits" = 0 ] || return 1
[ "$bits" = 8 ] || partial_seen=1
prefix=$((prefix + bits))
done
printf '%s\n' "$prefix"
}
update_resolver() {
[ -n "${dns:-}" ] || return 0
# Redirection follows LibreELEC's /etc/resolv.conf symlink without trying
# to replace the read-only symlink itself.
if { : > "$RESOLV_CONF"; } 2>/dev/null; then
if [ -n "${search:-}" ]; then
printf 'search %s\n' "$search" >> "$RESOLV_CONF" 2>/dev/null || :
elif [ -n "${domain:-}" ]; then
printf 'search %s\n' "$domain" >> "$RESOLV_CONF" 2>/dev/null || :
fi
for dns_server in $dns; do
printf 'nameserver %s\n' "$dns_server" >> "$RESOLV_CONF" 2>/dev/null || :
done
else
warn "Unable to update $RESOLV_CONF; addresses and routes remain configured"
fi
}
udhcpc_event() {
event="$1"
event_if="${interface:-$BRIDGE_IF}"
valid_ifname "$event_if" || exit 1
mkdir -p "$RUN_DIR" || exit 1
case "$event" in
deconfig|nak|leasefail)
if [ -f "$LEASE_ADDR_FILE" ]; then
old_lease_cidr="$(sed -n '1p' "$LEASE_ADDR_FILE" 2>/dev/null || true)"
[ -n "$old_lease_cidr" ] && ip addr del "$old_lease_cidr" dev "$event_if" 2>/dev/null || :
fi
ip route del default dev "$event_if" 2>/dev/null || :
rm -f "$LEASE_ADDR_FILE"
;;
bound|renew)
[ -n "${ip:-}" ] || exit 1
[ -n "${subnet:-}" ] || exit 1
prefix="$(netmask_to_prefix "$subnet")" || exit 1
lease_cidr="$ip/$prefix"
if [ -f "$LEASE_ADDR_FILE" ]; then
old_lease_cidr="$(sed -n '1p' "$LEASE_ADDR_FILE" 2>/dev/null || true)"
if [ -n "$old_lease_cidr" ] && [ "$old_lease_cidr" != "$lease_cidr" ]; then
ip addr del "$old_lease_cidr" dev "$event_if" 2>/dev/null || :
fi
fi
ip link set "$event_if" up
if ! ip -f inet addr show dev "$event_if" 2>/dev/null | grep -F "inet $lease_cidr " >/dev/null 2>&1; then
ip addr add "$lease_cidr" dev "$event_if"
fi
printf '%s\n' "$lease_cidr" > "$LEASE_ADDR_FILE"
if [ -n "${router:-}" ]; then
set -- $router
gateway="$1"
ip route replace default via "$gateway" dev "$event_if"
fi
update_resolver
;;
*)
warn "Ignoring unknown udhcpc event: $event"
;;
esac
}
dhclient_event() {
dhclient_reason="${reason:-}"
case "$dhclient_reason" in
PREINIT)
event_if="${interface:-$BRIDGE_IF}"
valid_ifname "$event_if" || exit 1
ip link set "$event_if" up
;;
BOUND|RENEW|REBIND|REBOOT|TIMEOUT)
# Normalize ISC dhclient-script variables to the same event data
# consumed by the udhcpc hook. This keeps address deletion scoped
# to our recorded DHCP lease and preserves a Keepalived VRRP VIP.
ip="${new_ip_address:-}"
subnet="${new_subnet_mask:-}"
router="${new_routers:-}"
dns="${new_domain_name_servers:-}"
search="${new_domain_search:-}"
domain="${new_domain_name:-}"
udhcpc_event bound
;;
EXPIRE|FAIL|RELEASE|STOP)
udhcpc_event deconfig
;;
MEDIUM)
;;
*)
warn "Ignoring unknown dhclient event: ${dhclient_reason:-unset}"
;;
esac
}
remember_bridge_ports() {
bridge_ports_dir="$SYS_CLASS_NET/$BRIDGE_IF/brif"
[ -d "$bridge_ports_dir" ] || return 0
ports_tmp="$BRIDGE_PORTS_FILE.tmp.$$"
umask 077
: > "$ports_tmp" || return 1
# Keep ports remembered by an earlier failed rebuild, then merge in every
# current bridge member except the physical parent managed explicitly.
if [ -f "$BRIDGE_PORTS_FILE" ]; then
while IFS= read -r remembered_port; do
valid_ifname "$remembered_port" || continue
[ "$remembered_port" != "$PARENT_IF" ] || continue
grep -Fqx -- "$remembered_port" "$ports_tmp" 2>/dev/null || \
printf '%s\n' "$remembered_port" >> "$ports_tmp"
done < "$BRIDGE_PORTS_FILE"
fi
for port_path in "$bridge_ports_dir"/*; do
[ -e "$port_path" ] || continue
bridge_port="${port_path##*/}"
valid_ifname "$bridge_port" || continue
[ "$bridge_port" != "$PARENT_IF" ] || continue
grep -Fqx -- "$bridge_port" "$ports_tmp" 2>/dev/null || \
printf '%s\n' "$bridge_port" >> "$ports_tmp"
done
if [ -s "$ports_tmp" ]; then
mv "$ports_tmp" "$BRIDGE_PORTS_FILE"
else
rm -f "$ports_tmp" "$BRIDGE_PORTS_FILE"
fi
}
restore_bridge_ports() {
[ -f "$BRIDGE_PORTS_FILE" ] || return 0
ports_retry="$BRIDGE_PORTS_FILE.retry.$$"
umask 077
: > "$ports_retry" || return 1
while IFS= read -r bridge_port; do
if ! valid_ifname "$bridge_port"; then
warn "Ignoring invalid remembered bridge port: $bridge_port"
continue
fi
[ "$bridge_port" != "$PARENT_IF" ] || continue
# A stopped container removes its veth. If it starts again later,
# systemd-nspawn will attach the new host-side port itself.
link_exists "$bridge_port" || continue
if ip link set "$bridge_port" master "$BRIDGE_IF"; then
say "Reattached $bridge_port to $BRIDGE_IF."
else
warn "Unable to reattach $bridge_port to $BRIDGE_IF"
printf '%s\n' "$bridge_port" >> "$ports_retry"
fi
done < "$BRIDGE_PORTS_FILE"
if [ -s "$ports_retry" ]; then
mv "$ports_retry" "$BRIDGE_PORTS_FILE"
else
rm -f "$ports_retry" "$BRIDGE_PORTS_FILE"
fi
}
cleanup_topology() {
set +e
# Only delete topology that this service marked as its own. An unrelated
# pre-existing br0 must never be removed by a failed start attempt.
if [ -e "$OWNED_FILE" ]; then
remember_bridge_ports || warn "Unable to remember ports attached to $BRIDGE_IF"
if link_exists "$PARENT_IF"; then
ip link set "$PARENT_IF" nomaster 2>/dev/null || :
ip link set "$PARENT_IF" up 2>/dev/null || :
fi
if link_exists "$BRIDGE_IF"; then
ip link set "$BRIDGE_IF" down 2>/dev/null || :
ip link del "$BRIDGE_IF" type bridge 2>/dev/null || ip link del "$BRIDGE_IF" 2>/dev/null || :
fi
fi
rm -f "$DHCP_PID_FILE" "$DHCP_KIND_FILE" "$LEASE_ADDR_FILE"
}
start_stock_connman() {
system_is_stopping && return 0
mark_fallback="${1:-no}"
had_extra_env=0
[ -e "$CONNMAN_EXTRA_ENV_FILE" ] && had_extra_env=1
[ "$mark_fallback" = fallback ] && : > "$FALLBACK_FILE"
rm -f "$CONNMAN_EXTRA_ENV_FILE"
if unit_active "$CONNMAN_UNIT"; then
# Restart only when the temporary --nodevice environment was present.
# This keeps repeated reconciliation idempotent.
if [ "$had_extra_env" = 1 ]; then
systemctl --no-block restart "$CONNMAN_UNIT" 2>/dev/null || :
fi
else
systemctl --no-block start "$CONNMAN_UNIT" 2>/dev/null || :
fi
}
topology_up_exit() {
exit_code=$?
trap - EXIT HUP INT TERM
set +e
if [ "$exit_code" -ne 0 ] && ! system_is_stopping && unit_active "$CONNMAN_UNIT"; then
systemctl stop "$CONNMAN_UNIT" 2>/dev/null || :
fi
cleanup_topology
if [ "$exit_code" -ne 0 ] && ! system_is_stopping; then
warn "Bridge topology setup failed ($RUN_REASON); handing control back to $CONNMAN_UNIT"
start_stock_connman fallback
fi
rm -f "$OWNED_FILE"
rmdir "$RUN_DIR" 2>/dev/null || :
exit "$exit_code"
}
topology_down() {
set +e
# Stop ConnMan while it still has the temporary --nodevice arguments, then
# restore its stock command line after the bridge is gone. Shutdown skips
# both operations. A normal stop is not a boot-wide fallback condition.
if ! system_is_stopping && unit_active "$CONNMAN_UNIT"; then
systemctl stop "$CONNMAN_UNIT" 2>/dev/null || :
fi
cleanup_topology
rm -f "$OWNED_FILE"
rmdir "$RUN_DIR" 2>/dev/null || :
if ! system_is_stopping; then
start_stock_connman
fi
}
write_connman_nodevice_env() {
# LibreELEC's original ExecStart already expands $CONNMAN_DEBUG. Preserve
# its optional debug flags and append only the bridge exclusion argument.
CONNMAN_DEBUG=''
if [ -r "$CONNMAN_DEBUG_FILE" ]; then
. "$CONNMAN_DEBUG_FILE" || return 1
fi
merged_debug="${CONNMAN_DEBUG:+$CONNMAN_DEBUG }--nodevice=$PARENT_IF,$BRIDGE_IF"
# Quote for systemd EnvironmentFile syntax. Dollar signs are not expanded
# there; backslashes and double quotes must be escaped inside double quotes.
escaped_debug="$(printf '%s' "$merged_debug" | sed 's/[\\"]/\\&/g')"
umask 077
printf 'CONNMAN_DEBUG="%s"\n' "$escaped_debug" > "$CONNMAN_EXTRA_ENV_FILE"
}
dhcp_run() {
[ -e "$ENABLE_FILE" ] || exit 0
[ ! -e "$FALLBACK_FILE" ] || die "Fallback is already active for this boot"
valid_ifname "$BRIDGE_IF" || die "Invalid bridge interface name: $BRIDGE_IF"
mkdir -p "$RUN_DIR" "$(dirname "$DHCLIENT_LEASE_FILE")" || die "Unable to create runtime or lease directory"
link_exists "$BRIDGE_IF" || die "$BRIDGE_IF does not exist; refusing to start DHCP"
require_dhcp_client
printf '%s\n' "$DHCP_CLIENT_KIND ($DHCP_CLIENT_BIN)" > "$DHCP_KIND_FILE"
say "Starting DHCP client on $BRIDGE_IF: $DHCP_CLIENT_KIND ($DHCP_CLIENT_BIN) ..."
case "$DHCP_CLIENT_MODE" in
busybox-applet)
exec "$DHCP_CLIENT_BIN" udhcpc -f -i "$BRIDGE_IF" \
-p "$DHCP_PID_FILE" -s "$SCRIPT_INSTALL_PATH"
;;
direct-udhcpc)
exec "$DHCP_CLIENT_BIN" -f -i "$BRIDGE_IF" \
-p "$DHCP_PID_FILE" -s "$SCRIPT_INSTALL_PATH"
;;
isc-dhclient)
# Entware's isc-dhcp-client-ipv4 is already IPv4-only and its
# command-line parser rejects the usual upstream -4 option.
exec "$DHCP_CLIENT_BIN" -d -v \
-pf "$DHCP_PID_FILE" \
-lf "$DHCLIENT_LEASE_FILE" \
-sf "$SCRIPT_INSTALL_PATH" \
"$BRIDGE_IF"
;;
*)
die "Unknown DHCP client mode: $DHCP_CLIENT_MODE"
;;
esac
}
dhcp_cleanup() {
set +e
if link_exists "$BRIDGE_IF"; then
interface="$BRIDGE_IF"
udhcpc_event deconfig || :
fi
rm -f "$DHCP_PID_FILE" "$DHCP_KIND_FILE"
}
topology_up() {
[ -e "$ENABLE_FILE" ] || exit 0
[ ! -e "$FALLBACK_FILE" ] || die "Fallback is already active for this boot; run '$0 enable' before retrying"
# Setup failures are transactional: restore stock ConnMan and remove only
# the topology created by this service. Once setup returns successfully,
# br0-stack.service owns the topology and its ExecStop performs teardown.
trap topology_up_exit EXIT HUP INT TERM
valid_ifname "$PARENT_IF" || die "Invalid physical interface name: $PARENT_IF"
valid_ifname "$BRIDGE_IF" || die "Invalid bridge interface name: $BRIDGE_IF"
mkdir -p "$RUN_DIR" "$(dirname "$DHCLIENT_LEASE_FILE")" || die "Unable to create runtime or lease directory"
link_exists "$PARENT_IF" || die "Physical interface $PARENT_IF does not exist"
if [ ! -x "$CONNMAND_BIN" ]; then
detected_connmand="$(command -v connmand 2>/dev/null || true)"
[ -n "$detected_connmand" ] && CONNMAND_BIN="$detected_connmand"
fi
[ -x "$CONNMAND_BIN" ] || die "ConnMan executable not found: $CONNMAND_BIN"
"$CONNMAND_BIN" --help 2>&1 | grep -q 'nodevice' || \
die "$CONNMAND_BIN does not support --nodevice; refusing to continue"
if link_exists "$BRIDGE_IF"; then
die "$BRIDGE_IF already exists; refusing to replace an unknown bridge. Run '$0 fallback' first"
fi
write_connman_nodevice_env || die "Unable to generate the ConnMan --nodevice runtime environment"
old_connman_active=0
unit_active "$CONNMAN_UNIT" && old_connman_active=1
if [ "$old_connman_active" = 1 ]; then
systemctl stop "$CONNMAN_UNIT" || die "Unable to stop $CONNMAN_UNIT"
fi
# The stock ConnMan may have left addresses/routes briefly behind.
ip -f inet addr flush dev "$PARENT_IF" 2>/dev/null || :
parent_mac="$(cat "$SYS_CLASS_NET/$PARENT_IF/address")"
say "Creating $BRIDGE_IF (MAC $parent_mac) ..."
ip link add "$BRIDGE_IF" type bridge || die "Failed to create $BRIDGE_IF"
link_exists "$BRIDGE_IF" || die "$BRIDGE_IF does not exist after creation"
: > "$OWNED_FILE"
ip link set "$BRIDGE_IF" address "$parent_mac" || die "Failed to set the MAC address on $BRIDGE_IF"
ip link set "$PARENT_IF" master "$BRIDGE_IF" || die "Unable to attach $PARENT_IF to $BRIDGE_IF"
ip link set "$PARENT_IF" up || die "Unable to bring up $PARENT_IF"
ip link set "$BRIDGE_IF" up || die "Unable to bring up $BRIDGE_IF"
restore_bridge_ports || warn "Unable to restore remembered ports to $BRIDGE_IF"
if [ "$old_connman_active" = 1 ]; then
systemctl --no-block start "$CONNMAN_UNIT" 2>/dev/null || :
fi
trap - EXIT HUP INT TERM
say "$BRIDGE_IF topology is ready; br0-dhcp.service owns the DHCP client."
}
fallback_now() {
set +e
rm -f "$ENABLE_FILE"
: > "$FALLBACK_FILE"
systemctl stop br0-stack.target 2>/dev/null || {
warn "Unable to stop the bridge stack; refusing to start stock ConnMan in parallel"
return 1
}
wait_until_inactive br0-stack.service 15 || {
warn "br0-stack.service did not stop within 15 seconds; refusing to start stock ConnMan in parallel"
return 1
}
start_stock_connman fallback
say "Fell back to $CONNMAN_UNIT."
}
reconcile_switch() {
[ "$(id -u)" = 0 ] || die "The reconcile command must be run as root"
if [ -e "$ENABLE_FILE" ] && [ ! -e "$FALLBACK_FILE" ]; then
systemctl start br0-stack.target
return
fi
# Removing br0.enabled is an immediate mode switch. A fallback marker also
# keeps automatic retries disabled until an explicit `enable` clears it.
systemctl stop br0-stack.target 2>/dev/null || \
die "Unable to stop the bridge stack"
wait_until_inactive br0-stack.service 15 || \
die "br0-stack.service did not stop within 15 seconds"
start_stock_connman
}
install_units() {
[ "$(id -u)" = 0 ] || die "The install command must be run as root"
# Copy the script to its persistent destination before changing any
# systemd enablement or unit files. Generated units use this absolute path.
source_path="$SCRIPT_SOURCE_PATH"
selected_path="$(choose_install_path)"
valid_script_path "$selected_path" || \
die "The script installation path must be absolute and contain no spaces or shell metacharacters: $selected_path"
target_path="$(readlink -f "$selected_path" 2>/dev/null || printf '%s' "$selected_path")"
valid_script_path "$target_path" || die "Unable to resolve the script installation path: $selected_path"
mkdir -p "$(dirname "$target_path")"
if [ "$source_path" != "$target_path" ]; then
cp "$source_path" "$target_path"
fi
chmod 0755 "$target_path"
SCRIPT_INSTALL_PATH="$target_path"
# Provision while ConnMan still owns eth0. Runtime commands never download,
# so a missing client fails before the bridge topology is changed.
provision_dhcp_client
say "DHCP client ready: $DHCP_CLIENT_KIND ($DHCP_CLIENT_BIN)"
mkdir -p "$SYSTEMD_DIR/connman.service.d"
mkdir -p "$SYSTEMD_DIR/systemd-nspawn@debian.service.d"
mkdir -p "$SYSTEMD_DIR/keepalived.service.d"
cat > "$SYSTEMD_DIR/br0-stack.target" <<EOF
[Unit]
Description=br0 bridge network stack
Documentation=file:$SCRIPT_INSTALL_PATH
Requires=br0-stack.service
Wants=br0-dhcp.service
After=br0-stack.service br0-dhcp.service
ConditionPathExists=/storage/.config/br0.enabled
ConditionPathExists=!/run/br0-stack.fallback
[Install]
WantedBy=multi-user.target
EOF
cat > "$SYSTEMD_DIR/br0-stack.service" <<EOF
[Unit]
Description=br0 bridge topology
Documentation=file:$SCRIPT_INSTALL_PATH
After=local-fs.target dbus.service
Wants=dbus.service
Before=br0-dhcp.service connman.service keepalived.service systemd-nspawn@debian.service
PartOf=br0-stack.target
RefuseManualStart=yes
RefuseManualStop=yes
ConditionPathExists=/storage/.config/br0.enabled
ConditionPathExists=!/run/br0-stack.fallback
RequiresMountsFor=/storage $SCRIPT_INSTALL_PATH
[Service]
Type=oneshot
RemainAfterExit=yes
Environment=PATH=/usr/sbin:/usr/bin:/sbin:/bin:/storage/.local/bin:/opt/sbin:/opt/bin
Environment=PARENT_IF=eth0
Environment=BRIDGE_IF=br0
Environment=CONNMAN_UNIT=connman.service
Environment=CONNMAND_BIN=/usr/sbin/connmand
Environment=CONNMAN_DEBUG_FILE=/run/libreelec/debug/connman.conf
Environment=CONNMAN_EXTRA_ENV_FILE=/run/br0-stack/connman-extra.conf
Environment=SYSTEM_BUSYBOX_BIN=
Environment=LOCAL_BUSYBOX_BIN=/storage/.local/bin/busybox
Environment=LOCAL_UDHCPC_BIN=/storage/.local/bin/udhcpc
Environment=ENTWARE_DHCLIENT_BIN=/opt/sbin/dhclient
ExecStart=$SCRIPT_INSTALL_PATH topology-up
ExecStop=$SCRIPT_INSTALL_PATH topology-down
TimeoutStartSec=60
TimeoutStopSec=30
Restart=no
EOF
cat > "$SYSTEMD_DIR/br0-dhcp.service" <<EOF
[Unit]
Description=DHCP client for br0
Documentation=file:$SCRIPT_INSTALL_PATH
Requires=br0-stack.service
BindsTo=br0-stack.service
After=br0-stack.service
PartOf=br0-stack.target
ConditionPathExists=/storage/.config/br0.enabled
ConditionPathExists=!/run/br0-stack.fallback
RequiresMountsFor=/storage $SCRIPT_INSTALL_PATH
[Service]
Type=simple
Environment=PATH=/usr/sbin:/usr/bin:/sbin:/bin:/storage/.local/bin:/opt/sbin:/opt/bin
Environment=PARENT_IF=eth0
Environment=BRIDGE_IF=br0
Environment=CONNMAN_UNIT=connman.service
Environment=CONNMAND_BIN=/usr/sbin/connmand
Environment=CONNMAN_DEBUG_FILE=/run/libreelec/debug/connman.conf
Environment=CONNMAN_EXTRA_ENV_FILE=/run/br0-stack/connman-extra.conf
Environment=SYSTEM_BUSYBOX_BIN=
Environment=LOCAL_BUSYBOX_BIN=/storage/.local/bin/busybox
Environment=LOCAL_UDHCPC_BIN=/storage/.local/bin/udhcpc
Environment=ENTWARE_DHCLIENT_BIN=/opt/sbin/dhclient
ExecStart=$SCRIPT_INSTALL_PATH dhcp-run
ExecStopPost=$SCRIPT_INSTALL_PATH dhcp-cleanup
KillMode=control-group
TimeoutStopSec=15
Restart=on-failure
RestartSec=2
EOF
cat > "$SYSTEMD_DIR/br0-stack-switch.service" <<EOF
[Unit]
Description=Apply the br0.enabled network mode switch
Documentation=file:$SCRIPT_INSTALL_PATH
After=local-fs.target
RequiresMountsFor=/storage $SCRIPT_INSTALL_PATH
[Service]
Type=oneshot
ExecStart=$SCRIPT_INSTALL_PATH reconcile
EOF
cat > "$SYSTEMD_DIR/br0-stack-switch.path" <<'EOF'
[Unit]
Description=Watch the br0.enabled network mode switch
[Path]
PathChanged=/storage/.config/br0.enabled
Unit=br0-stack-switch.service
MakeDirectory=yes
[Install]
WantedBy=multi-user.target
EOF
# Do not replace LibreELEC's ExecStart. It already expands CONNMAN_DEBUG,
# so an optional runtime EnvironmentFile can append --nodevice.
cat > "$SYSTEMD_DIR/connman.service.d/20-br0-stack.conf" <<'EOF'
[Service]
EnvironmentFile=-/run/br0-stack/connman-extra.conf
EOF
# The container starts after the layer-2 bridge attempt, but a later
# topology failure or teardown must not stop the container itself. Its
# host0 interface obtains an address independently from the host DHCP.
cat > "$SYSTEMD_DIR/systemd-nspawn@debian.service.d/20-br0-stack.conf" <<'EOF'
[Unit]
Wants=br0-stack.service
After=br0-stack.service
ConditionPathExists=/storage/.config/br0.enabled
ConditionPathExists=!/run/br0-stack.fallback
EOF
# Keepalived starts after the bridge attempt. Both topology and DHCP may
# disappear temporarily without systemd stopping Keepalived.
cat > "$SYSTEMD_DIR/keepalived.service.d/20-br0-stack.conf" <<'EOF'
[Unit]
Wants=br0-stack.service
After=br0-stack.service br0-dhcp.service
ConditionPathExists=/storage/.config/br0.enabled
ConditionPathExists=!/run/br0-stack.fallback
EOF
systemctl daemon-reload
systemctl enable br0-stack.target br0-stack-switch.path
systemctl start br0-stack-switch.path
systemctl start br0-stack-switch.service
say "Installation complete."
say "Enable bridge mode: $SCRIPT_INSTALL_PATH enable"
say "Disable bridge mode (ConnMan automatically drops --nodevice): $SCRIPT_INSTALL_PATH disable"
}
enable_bridge() {
[ "$(id -u)" = 0 ] || die "The enable command must be run as root"
# Runtime commands only select an already installed DHCP client.
require_dhcp_client
# A prior failure blocks automatic retry for the rest of this boot. Clear
# it only on this explicit operator action, before the path watcher sees
# br0.enabled appear.
rm -f "$FALLBACK_FILE"
: > "$ENABLE_FILE"
systemctl reset-failed br0-stack.target br0-stack.service br0-dhcp.service 2>/dev/null || :
systemctl start br0-stack.target
}
disable_bridge() {
[ "$(id -u)" = 0 ] || die "The disable command must be run as root"
rm -f "$ENABLE_FILE"
reconcile_switch
}
show_status() {
if [ -e "$ENABLE_FILE" ]; then
say "Switch: enabled ($ENABLE_FILE exists)"
else
say "Switch: disabled ($ENABLE_FILE does not exist)"
fi
if [ -e "$FALLBACK_FILE" ]; then
say "This boot: stock ConnMan fallback is active"
else
say "This boot: fallback has not been triggered"
fi
if [ -f "$DHCP_KIND_FILE" ]; then
say "DHCP client: $(sed -n '1p' "$DHCP_KIND_FILE" 2>/dev/null || true)"
fi
say ""
systemctl --no-pager --full status \
br0-stack.target br0-stack.service br0-dhcp.service \
br0-stack-switch.path br0-stack-switch.service 2>/dev/null || :
say ""
ip link show dev "$PARENT_IF" 2>/dev/null || :
ip link show dev "$BRIDGE_IF" 2>/dev/null || :
ip -f inet addr show dev "$PARENT_IF" 2>/dev/null || :
ip -f inet addr show dev "$BRIDGE_IF" 2>/dev/null || :
ip route 2>/dev/null || :
say ""
connmanctl services 2>/dev/null || :
}
usage() {
say "Usage: $0 {install|enable|disable|status|fallback}"
}
case "${1:-}" in
install) install_units ;;
enable) enable_bridge ;;
disable) disable_bridge ;;
status) show_status ;;
fallback) fallback_now ;;
topology-up) topology_up ;;
topology-down) topology_down ;;
dhcp-run) dhcp_run ;;
dhcp-cleanup) dhcp_cleanup ;;
reconcile) reconcile_switch ;;
deconfig|bound|renew|nak|leasefail) udhcpc_event "$1" ;;
'')
if [ -n "${reason:-}" ]; then
dhclient_event
else
usage
exit 2
fi
;;
*) usage; exit 2 ;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment