Last active
August 12, 2026 08:08
-
-
Save ihipop/c95673a362d1084cbffd297cb42aaa5d to your computer and use it in GitHub Desktop.
Transactional br0 network stack for LibreELEC, ConnMan, DHCP and systemd-nspawn
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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