#!/bin/sh # shellcheck shell=sh # # zt-gateway-setup — configure this OpenWrt router as a ZeroTier exit # gateway for the WIBLAN subnet (10.11.13.0/24). # # Subcommands: # status Show current setup state # setup-bridge Create the br-zt bridge interface # setup-routing Configure policy routing (tables 100/101, ip rules) # setup-dhcp Configure DHCP for WIBLAN on br-zt # setup-hotplug Create hotplug script to re-apply routes on ifup # setup-persistence Write rc.local + UCI network routes # setup-all Run all setup-* commands in order # # Exit codes: # 0 success # 1 usage / argument error # 2 precondition failed # 3 runtime failure # # Environment overrides (for testing and non-default configs): # ZTG_BRIDGE bridge device (default: br-zt) # ZTG_BRIDGE_PORTS space-separated ports (default: auto-detect zt* interface) # ZTG_WIBLAN_CIDR WIBLAN subnet (default: 10.11.13.0/24) # ZTG_WIBLAN_GW WIBLAN gateway IP (default: 10.11.13.1) # ZTG_BRIDGE_NETMASK bridge subnet mask (default: 255.255.254.0 /23) # ZTG_WIBLAN_LEASE_FIRST first DHCP IP (default: 10.11.13.100) # ZTG_WIBLAN_LEASE_LAST last DHCP IP (default: 10.11.13.200) # ZTG_TABLE_MAIN main policy table (default: 100) # ZTG_TABLE_DRAIN drain policy table (default: 101) # ZTG_TABLE_MWAN mwan3 return table (default: 1) # ZTG_FWMARK drain fwmark (default: 0x100) # ZTG_DRAIN_PRIORITY drain rule priority (default: 99) # ZTG_HOTPLUG hotplug script path # ZTG_RCLOCAL rc.local path # ZTG_DHCPCONF DHCP UCI config file (default: /etc/config/dhcp) # ZTG_NETWORKCONF network UCI config file (default: /etc/config/network) # ZTG_WIFI_SSID WiFi AP SSID (default: WIBLAN) # ZTG_WIFI_KEY WiFi AP WPA2 key (default: zt-r0ute-2026) # ZTG_WIFI_RADIO WiFi radio device (default: auto-detect first radio) # ZTG_WIFI_ENCRYPTION WiFi encryption (default: psk2) # ZTG_SKIP_PERSIST skip UCI persistence (testing) set -eu # ---------------------------------------------------------------------------- # Config # ---------------------------------------------------------------------------- BRIDGE="${ZTG_BRIDGE:-br-zt}" # Auto-detect ZeroTier interface if not specified (zt + random suffix, not br-zt) if [ -n "${ZTG_BRIDGE_PORTS:-}" ]; then BRIDGE_PORTS="$ZTG_BRIDGE_PORTS" else BRIDGE_PORTS=$(ip -o link show 2>/dev/null \ | awk -F': ' '/^[0-9]+:/{gsub(/@.*/, "", $2); if ($2 ~ /^zt/ && $2 != "br-zt") print $2; exit}') if [ -z "$BRIDGE_PORTS" ]; then BRIDGE_PORTS="ztabc0" fi fi WIBLAN_CIDR="${ZTG_WIBLAN_CIDR:-10.11.13.0/24}" WIBLAN_GW="${ZTG_WIBLAN_GW:-10.11.13.1}" # Bridge netmask must be /23 to cover both ZeroTier (10.11.12.x) and WIBLAN (10.11.13.x) BRIDGE_NETMASK="${ZTG_BRIDGE_NETMASK:-255.255.254.0}" WIBLAN_LEASE_FIRST="${ZTG_WIBLAN_LEASE_FIRST:-10.11.13.100}" WIBLAN_LEASE_LAST="${ZTG_WIBLAN_LEASE_LAST:-10.11.13.200}" TABLE_MAIN="${ZTG_TABLE_MAIN:-100}" TABLE_DRAIN="${ZTG_TABLE_DRAIN:-101}" TABLE_MWAN="${ZTG_TABLE_MWAN:-1}" FWMARK="${ZTG_FWMARK:-0x100}" DRAIN_PRIORITY="${ZTG_DRAIN_PRIORITY:-99}" HOTPLUG="${ZTG_HOTPLUG:-/etc/hotplug.d/net/99-zerotier-bridge}" RCLOCAL="${ZTG_RCLOCAL:-/etc/rc.local}" DHCPCONF="${ZTG_DHCPCONF:-/etc/config/dhcp}" SKIP_PERSIST="${ZTG_SKIP_PERSIST:-0}" WiFi_SSID="${ZTG_WIFI_SSID:-WIBLAN}" WiFi_KEY="${ZTG_WIFI_KEY:-zt-r0ute-2026}" WiFi_ENCRYPTION="${ZTG_WIFI_ENCRYPTION:-psk2}" # Auto-detect first WiFi radio if not specified WiFi_RADIO="${ZTG_WIFI_RADIO:-}" if [ -z "$WiFi_RADIO" ]; then WiFi_RADIO=$(uci -q get wireless.@wifi-device[0].name 2>/dev/null || echo "radio0") fi # Derived: extract prefix bits from CIDR WIBLAN_BITS="${WIBLAN_CIDR##*/}" # UCI-safe section name (replace hyphens with underscores) BRIDGE_UCI=$(printf '%s' "$BRIDGE" | tr '-' '_') # Interface name (used by DHCP and WiFi AP, must reference interface not device) WIBLAN_IFACE="zt_wiblan" # ---------------------------------------------------------------------------- # Logging # ---------------------------------------------------------------------------- log() { printf '[zt-gateway-setup] %s\n' "$*" >&2; } die() { rc=$1; shift; log "ERROR: $*"; exit "$rc"; } # Ensure a UCI config file exists (touch it if missing) ensure_uci_config() { _conf=/etc/config/"$1" if [ ! -f "$_conf" ]; then touch "$_conf" log "created empty UCI config: ${_conf}" fi } # CIDR to dotted mask (e.g. 24 -> 255.255.255.0) # CIDR to dotted mask (e.g. 24 -> 255.255.255.0) _cidr_to_mask() { bits=$1 # Build octets from the CIDR prefix octets="" while [ "$bits" -gt 0 ]; do if [ "$bits" -ge 8 ]; then oct=255 bits=$((bits - 8)) else oct=0 j=0 while [ $j -lt "$bits" ]; do oct=$(( oct | (1 << (7 - j)) )) j=$((j + 1)) done bits=0 fi if [ -n "$octets" ]; then octets="${octets}.${oct}" else octets="${oct}" fi done # Pad remaining octets with 0 while [ "$(printf '%s' "$octets" | tr -cd '.' | wc -c)" -lt 3 ]; do octets="${octets}.0" done printf '%s' "$octets" } # ---------------------------------------------------------------------------- # Status # ---------------------------------------------------------------------------- cmd_status() { log "checking setup status..." # Bridge if ip link show "$BRIDGE" >/dev/null 2>&1; then printf 'bridge: %s (up)\n' "$BRIDGE" # List ports ports=$(ip link show master "$BRIDGE" 2>/dev/null \ | awk -F': ' '/^[0-9]+:/{gsub(/@.*/, "", $2); print $2}' \ | tr '\n' ' ') if [ -n "$ports" ]; then printf ' ports: %s\n' "$ports" fi # IP on bridge br_addr=$(ip -4 addr show dev "$BRIDGE" 2>/dev/null \ | awk '/inet /{gsub(/\/.*/, "", $2); print $2; exit}') if [ -n "$br_addr" ]; then printf ' addr: %s\n' "$br_addr" fi else printf 'bridge: %s (missing)\n' "$BRIDGE" fi # Routing tables table_main_gw=$(ip route show table "$TABLE_MAIN" 2>/dev/null \ | awk '/^[[:space:]]*default/{ for (i=1; i<=NF; i++) if ($i=="via") { print $(i+1); exit } }') if [ -n "$table_main_gw" ]; then printf 'table %s: default via %s (configured)\n' "$TABLE_MAIN" "$table_main_gw" else printf 'table %s: (empty)\n' "$TABLE_MAIN" fi table_drain_gw=$(ip route show table "$TABLE_DRAIN" 2>/dev/null \ | awk '/^[[:space:]]*default/{ for (i=1; i<=NF; i++) if ($i=="via") { print $(i+1); exit } }') if [ -n "$table_drain_gw" ]; then printf 'table %s: default via %s (configured)\n' "$TABLE_DRAIN" "$table_drain_gw" else printf 'table %s: (empty)\n' "$TABLE_DRAIN" fi # ip rule for fwmark if ip rule show 2>/dev/null | grep -q "fwmark ${FWMARK}"; then printf 'ip rule: fwmark %s -> table %s (configured)\n' "$FWMARK" "$TABLE_DRAIN" else printf 'ip rule: fwmark %s (missing)\n' "$FWMARK" fi # DHCP if grep -q "interface '${BRIDGE}'" "$DHCPCONF" 2>/dev/null; then printf 'dhcp: %s configured in %s\n' "$BRIDGE" "$DHCPCONF" else printf 'dhcp: %s (not configured)\n' "$BRIDGE" fi # Hotplug if [ -x "$HOTPLUG" ] || [ -f "$HOTPLUG" ]; then printf 'hotplug: %s (present)\n' "$HOTPLUG" else printf 'hotplug: %s (missing)\n' "$HOTPLUG" fi # Persistence in rc.local if grep -q "zt-gateway" "$RCLOCAL" 2>/dev/null; then printf 'rc.local: entries present\n' else printf 'rc.local: no zt-gateway entries\n' fi } # ---------------------------------------------------------------------------- # setup-bridge # ---------------------------------------------------------------------------- cmd_setup_bridge() { log "setting up bridge ${BRIDGE}..." if ! command -v uci >/dev/null 2>&1; then die 3 "uci not found; cannot configure bridge" fi ensure_uci_config network # Create or update bridge device in network.uci # Note: UCI section names cannot contain hyphens, so we use BRIDGE_UCI if uci -q get "network.${BRIDGE_UCI}" >/dev/null 2>&1; then log "bridge device ${BRIDGE} already exists in UCI; updating" else uci -q set "network.${BRIDGE_UCI}=device" uci -q set "network.${BRIDGE_UCI}.type=bridge" uci -q set "network.${BRIDGE_UCI}.name=${BRIDGE}" fi # Set bridge ports (space-separated in UCI list) uci -q delete "network.${BRIDGE_UCI}.ports" 2>/dev/null || true for port in $BRIDGE_PORTS; do uci -q add_list "network.${BRIDGE_UCI}.ports=${port}" done # Create interface section bridging to br-zt for WIBLAN if ! uci -q get "network.zt_wiblan" >/dev/null 2>&1; then uci -q set "network.zt_wiblan=interface" uci -q set "network.zt_wiblan.proto=static" uci -q set "network.zt_wiblan.device=${BRIDGE}" uci -q set "network.zt_wiblan.ipaddr=${WIBLAN_GW}" uci -q set "network.zt_wiblan.netmask=${BRIDGE_NETMASK}" fi if [ "$SKIP_PERSIST" != "1" ]; then uci commit network log "bridge UCI config committed" fi # Bring up the bridge (best-effort; may need netifd restart) if command -v ifup >/dev/null 2>&1; then ifup "zt_wiblan" 2>/dev/null || \ log "warning: ifup zt_wiblan failed; may need 'service network restart'" fi # Ensure ZeroTier-assigned IP stays on the ZT interface (needed for ARP) # When ZT interface is a bridge port, the assigned IP can be lost zt_ip=$(zerotier-cli listnetworks 2>/dev/null \ | awk '/OK/{for(i=6;i<=NF;i++) if($i ~ /\//) {split($i,a,"/"); print a[1]; exit}}') zt_bits=$(zerotier-cli listnetworks 2>/dev/null \ | awk '/OK/{for(i=6;i<=NF;i++) if($i ~ /\//) {split($i,a,"/"); print a[2]; exit}}') if [ -n "$zt_ip" ] && [ -n "$zt_bits" ]; then if ! ip -4 addr show dev "$BRIDGE_PORTS" 2>/dev/null | grep -q "$zt_ip"; then ip addr add "${zt_ip}/${zt_bits}" dev "$BRIDGE_PORTS" 2>/dev/null || \ log "warning: could not add ZT IP ${zt_ip}/${zt_bits} to ${BRIDGE_PORTS}" fi fi # Add bridge interface to firewall LAN zone (needed for nftables fw4) if command -v uci >/dev/null 2>&1 && [ "$SKIP_PERSIST" != "1" ]; then lan_zone=$(uci -q get firewall.@zone[0].name 2>/dev/null) if [ "$lan_zone" = "lan" ]; then if ! uci -q get firewall.@zone[0].network 2>/dev/null | grep -q "zt_wiblan"; then uci -q add_list "firewall.@zone[0].network=zt_wiblan" uci -q commit firewall log "added zt_wiblan to firewall LAN zone" fi fi fi log "bridge ${BRIDGE} setup complete" } # ---------------------------------------------------------------------------- # setup-routing # ---------------------------------------------------------------------------- cmd_setup_routing() { log "setting up routing (tables ${TABLE_MAIN}/${TABLE_DRAIN})..." # Host route to WIBLAN gateway via bridge ip route replace "$WIBLAN_GW" dev "$BRIDGE" 2>/dev/null || \ log "warning: host route to ${WIBLAN_GW} failed" # Table 100 (main policy): default via exit gateway + direct WIBLAN subnet route # The exit gateway is the ZeroTier peer that has internet access (not WIBLAN_GW which is local) EXIT_GW=$(uci -q get zt-gateway.global.active_ip 2>/dev/null || \ uci -q get zt-gateway.@gateway[0].ip 2>/dev/null || \ ip route show table "$TABLE_MWAN" 2>/dev/null \ | awk '/default/{for(i=1;i<=NF;i++) if($i=="via") {print $(i+1); exit}}') if [ -z "$EXIT_GW" ]; then # Fallback: find ZeroTier peer IP on the same /23 EXIT_GW=$(ip route show table "$TABLE_MWAN" 2>/dev/null \ | awk '/via.*dev/{for(i=1;i<=NF;i++) if($i=="via") {print $(i+1); exit}}') fi if [ -n "$EXIT_GW" ]; then log "exit gateway: ${EXIT_GW}" ip route replace default via "$EXIT_GW" dev "$BRIDGE_PORTS" table "$TABLE_MAIN" ip route replace "$WIBLAN_CIDR" dev "$BRIDGE" table "$TABLE_MAIN" # Table 101 (drain): default via exit gateway ip route replace default via "$EXIT_GW" dev "$BRIDGE_PORTS" table "$TABLE_DRAIN" else log "warning: could not determine exit gateway; using WIBLAN_GW" ip route replace default via "$WIBLAN_GW" dev "$BRIDGE" table "$TABLE_MAIN" ip route replace "$WIBLAN_CIDR" dev "$BRIDGE" table "$TABLE_MAIN" ip route replace default via "$WIBLAN_GW" dev "$BRIDGE" table "$TABLE_DRAIN" fi # mwan3 return-traffic table: route WIBLAN back through bridge ip route replace "$WIBLAN_CIDR" dev "$BRIDGE" table "$TABLE_MWAN" # ip rule: WIBLAN subnet -> main policy table ip rule add from "$WIBLAN_CIDR" table "$TABLE_MAIN" priority 100 2>/dev/null || \ ip rule replace from "$WIBLAN_CIDR" table "$TABLE_MAIN" priority 100 # ip rule: fwmark 0x100 -> drain table ip rule add fwmark "$FWMARK" table "$TABLE_DRAIN" priority "$DRAIN_PRIORITY" 2>/dev/null || \ ip rule replace fwmark "$FWMARK" table "$TABLE_DRAIN" priority "$DRAIN_PRIORITY" log "routing setup complete" # Persist to UCI if [ "$SKIP_PERSIST" != "1" ] && command -v uci >/dev/null 2>&1; then ensure_uci_config network # Host route if ! uci -q get "network.zt_wiblan_host" >/dev/null 2>&1; then uci -q set "network.zt_wiblan_host=route" uci -q set "network.zt_wiblan_host.target=${WIBLAN_GW}" uci -q set "network.zt_wiblan_host.interface=${BRIDGE}" fi # Main policy table default route if ! uci -q get "network.zt_wiblan_default" >/dev/null 2>&1; then uci -q set "network.zt_wiblan_default=route" uci -q set "network.zt_wiblan_default.target=0.0.0.0" uci -q set "network.zt_wiblan_default.netmask=0.0.0.0" uci -q set "network.zt_wiblan_default.gateway=${WIBLAN_GW}" uci -q set "network.zt_wiblan_default.interface=${BRIDGE}" uci -q set "network.zt_wiblan_default.table=${TABLE_MAIN}" fi # WIBLAN subnet route in main policy table if ! uci -q get "network.zt_wiblan_subnet" >/dev/null 2>&1; then uci -q set "network.zt_wiblan_subnet=route" uci -q set "network.zt_wiblan_subnet.target=${WIBLAN_CIDR}" uci -q set "network.zt_wiblan_subnet.netmask=$(_cidr_to_mask "$WIBLAN_BITS")" uci -q set "network.zt_wiblan_subnet.interface=${BRIDGE}" uci -q set "network.zt_wiblan_subnet.table=${TABLE_MAIN}" fi uci commit network log "routing UCI config committed" fi } # ---------------------------------------------------------------------------- # setup-dhcp # ---------------------------------------------------------------------------- cmd_setup_dhcp() { log "setting up DHCP for ${BRIDGE} (${WIBLAN_CIDR})..." if ! command -v uci >/dev/null 2>&1; then die 3 "uci not found; cannot configure DHCP" fi ensure_uci_config dhcp # Create DHCP subnet entry for br-zt # Note: UCI section names cannot contain hyphens, so we use BRIDGE_UCI if uci -q get "dhcp.${BRIDGE_UCI}" >/dev/null 2>&1; then log "DHCP entry for ${BRIDGE} already exists; updating" else uci -q set "dhcp.${BRIDGE_UCI}=dhcp" fi uci -q set "dhcp.${BRIDGE_UCI}.interface=${WIBLAN_IFACE}" uci -q set "dhcp.${BRIDGE_UCI}.start=${WIBLAN_LEASE_FIRST##*.}" uci -q set "dhcp.${BRIDGE_UCI}.limit=$(( ${WIBLAN_LEASE_LAST##*.} - ${WIBLAN_LEASE_FIRST##*.} + 1 ))" uci -q set "dhcp.${BRIDGE_UCI}.leasetime=12h" # Provide gateway and DNS to DHCP clients uci -q set "dhcp.${BRIDGE_UCI}.dhcp_option=3,${WIBLAN_GW}" uci -q add_list "dhcp.${BRIDGE_UCI}.dhcp_option=6,8.8.8.8,1.1.1.1" # Disable DHCP on LAN to prevent conflicting leases if ! uci -q test dhcp.lan.ignore >/dev/null 2>&1; then uci -q set "dhcp.lan.ignore=1" fi if [ "$SKIP_PERSIST" != "1" ]; then uci commit dhcp log "DHCP UCI config committed" fi # Restart dnsmasq to pick up changes if command -v service >/dev/null 2>&1; then service dnsmasq restart 2>/dev/null || \ log "warning: dnsmasq restart failed; do it manually" fi log "DHCP setup complete" } # ---------------------------------------------------------------------------- # setup-wifi-ap # ---------------------------------------------------------------------------- cmd_setup_wifi_ap() { log "setting up WiFi AP (${WiFi_SSID}) on ${WiFi_RADIO}..." if ! command -v uci >/dev/null 2>&1; then die 3 "uci not found; cannot configure WiFi" fi ensure_uci_config wireless # Find existing wifinet section for our SSID, or create new one existing="" for idx in 0 1 2 3 4 5 6 7 8 9; do if uci -q get "wireless.wifinet${idx}.ssid" 2>/dev/null | grep -q "^${WiFi_SSID}$"; then existing="wifinet${idx}" break fi done if [ -n "$existing" ]; then log "WiFi AP '${WiFi_SSID}' already exists (${existing}); updating" section="$existing" else section=$(uci -q add wireless wifi-iface) log "created new wireless section: ${section}" fi uci -q set "wireless.${section}.device=${WiFi_RADIO}" uci -q set "wireless.${section}.mode=ap" uci -q set "wireless.${section}.ssid=${WiFi_SSID}" uci -q set "wireless.${section}.encryption=${WiFi_ENCRYPTION}" uci -q set "wireless.${section}.key=${WiFi_KEY}" uci -q set "wireless.${section}.network=${WIBLAN_IFACE}" uci -q set "wireless.${section}.wpa_disable_eapol_key_retries=1" if [ "$SKIP_PERSIST" != "1" ]; then uci commit wireless log "wireless UCI config committed" fi log "WiFi AP setup complete: ssid=${WiFi_SSID} radio=${WiFi_RADIO} bridge=${BRIDGE}" } # ---------------------------------------------------------------------------- # setup-hotplug # ---------------------------------------------------------------------------- cmd_setup_hotplug() { log "creating hotplug script ${HOTPLUG}..." mkdir -p "$(dirname "$HOTPLUG")" cat >"$HOTPLUG" <<'HOTPLUG_SCRIPT' #!/bin/sh # shellcheck shell=sh # ZeroTier gateway hotplug — re-apply routing when the bridge interface # comes up (e.g. after boot, after ZeroTier restart). # # Environment: INTERFACE, ACTION (set by netifd hotplug) # Config: /etc/config/zt-gateway (read at runtime for active gateway IP) ZTG_BRIDGE="${ZTG_BRIDGE:-br-zt}" ZTG_TABLE_MAIN="${ZTG_TABLE_MAIN:-100}" ZTG_TABLE_DRAIN="${ZTG_TABLE_DRAIN:-101}" ZTG_TABLE_MWAN="${ZTG_TABLE_MWAN:-1}" ZTG_FWMARK="${ZTG_FWMARK:-0x100}" ZTG_DRAIN_PRIORITY="${ZTG_DRAIN_PRIORITY:-99}" ZTG_WIBLAN_CIDR="${ZTG_WIBLAN_CIDR:-10.11.13.0/24}" # Only act on our bridge interface [ "$INTERFACE" = "$ZTG_BRIDGE" ] || exit 0 case "$ACTION" in ifup) # Ensure ZeroTier-assigned IP stays on the ZT interface (needed for ARP) zt_if=$(ip -o link show 2>/dev/null | awk -F': ' '/^[0-9]+:/{gsub(/@.*/, "", $2); if ($2 ~ /^zt/ && $2 != "'"$ZTG_BRIDGE"'") print $2; exit}') if [ -n "$zt_if" ]; then zt_ip=$(zerotier-cli listnetworks 2>/dev/null \ | awk '/OK/{for(i=6;i<=NF;i++) if($i ~ /\//) {split($i,a,"/"); print a[1]; exit}}') zt_bits=$(zerotier-cli listnetworks 2>/dev/null \ | awk '/OK/{for(i=6;i<=NF;i++) if($i ~ /\//) {split($i,a,"/"); print a[2]; exit}}') if [ -n "$zt_ip" ] && [ -n "$zt_bits" ] && \ ! ip -4 addr show dev "$zt_if" 2>/dev/null | grep -q "$zt_ip"; then ip addr add "${zt_ip}/${zt_bits}" dev "$zt_if" 2>/dev/null logger -t zt-gw-hotplug "added ZT IP ${zt_ip}/${zt_bits} to ${zt_if}" fi fi # Read active gateway from UCI active_ip=$(uci -q get zt-gateway.global.active_ip 2>/dev/null || \ uci -q get zt-gateway.global.active_gateway 2>/dev/null) # If active_ip is a region name, resolve to IP via gateway section if [ -n "$active_ip" ] && ! echo "$active_ip" | grep -q '^[0-9]'; then active_ip=$(uci -q get "zt-gateway.@gateway[0].ip" 2>/dev/null || \ for sec in $(uci -q show zt-gateway 2>/dev/null | \ awk -F'=' '/\.region=/{print $1}' | \ sed 's/\.region//'); do r=$(uci -q get "${sec}.region" 2>/dev/null) if [ "$r" = "$active_ip" ]; then uci -q get "${sec}.ip" 2>/dev/null break fi done) fi if [ -z "$active_ip" ]; then # No active gateway configured; try reading from table active_ip=$(ip route show table "$ZTG_TABLE_MAIN" 2>/dev/null \ | awk '/^[[:space:]]*default/{ for (i=1; i<=NF; i++) if ($i=="via") { print $(i+1); exit } }') fi if [ -z "$active_ip" ]; then logger -t zt-gw-hotplug "No active gateway IP; skipping route setup" exit 0 fi logger -t zt-gw-hotplug "ifup ${ZTG_BRIDGE}: applying routes via ${active_ip}" # Host route ip route replace "$active_ip" dev "$ZTG_BRIDGE" # Policy routes ip route replace default via "$active_ip" dev "$ZTG_BRIDGE" table "$ZTG_TABLE_MAIN" ip route replace default via "$active_ip" dev "$ZTG_BRIDGE" table "$ZTG_TABLE_DRAIN" # mwan3 return ip route replace "$ZTG_WIBLAN_CIDR" dev "$ZTG_BRIDGE" table "$ZTG_TABLE_MWAN" # ip rule for drain fwmark ip rule add fwmark "$ZTG_FWMARK" table "$ZTG_TABLE_DRAIN" \ priority "$ZTG_DRAIN_PRIORITY" 2>/dev/null || \ ip rule replace fwmark "$ZTG_FWMARK" table "$ZTG_TABLE_DRAIN" \ priority "$ZTG_DRAIN_PRIORITY" ;; ifdown) logger -t zt-gw-hotplug "ifdown ${ZTG_BRIDGE}: cleaning up" ip rule del fwmark "$ZTG_FWMARK" table "$ZTG_TABLE_DRAIN" \ priority "$ZTG_DRAIN_PRIORITY" 2>/dev/null || true ;; esac HOTPLUG_SCRIPT chmod +x "$HOTPLUG" log "hotplug script created at ${HOTPLUG}" } # ---------------------------------------------------------------------------- # setup-persistence # ---------------------------------------------------------------------------- cmd_setup_persistence() { log "setting up persistence..." # Ensure rc.local has the gateway restoration logic if ! grep -q "zt-gateway" "$RCLOCAL" 2>/dev/null; then log "adding zt-gateway entry to ${RCLOCAL}" # Read current rc.local content rc_content="" if [ -f "$RCLOCAL" ]; then rc_content=$(cat "$RCLOCAL") fi # Remove trailing 'exit 0' if present, add our block, re-add exit 0 rc_stripped=$(printf '%s\n' "$rc_content" | sed '/^exit 0$/d') cat >"$RCLOCAL" </dev/null 2>&1; then ensure_uci_config zt-gateway # Store the WIBLAN config for the hotplug to read if ! uci -q get "zt-gateway.global.wiblan_subnet" >/dev/null 2>&1; then uci -q set "zt-gateway.global.wiblan_subnet='${WIBLAN_CIDR}'" fi if ! uci -q get "zt-gateway.global.bridge_device" >/dev/null 2>&1; then uci -q set "zt-gateway.global.bridge_device='${BRIDGE}'" fi if ! uci -q get "zt-gateway.global.bridge_ports" >/dev/null 2>&1; then uci -q set "zt-gateway.global.bridge_ports='${BRIDGE_PORTS}'" fi if ! uci -q get "zt-gateway.global.wiblan_gw" >/dev/null 2>&1; then uci -q set "zt-gateway.global.wiblan_gw='${WIBLAN_GW}'" fi if ! uci -q get "zt-gateway.global.table_main" >/dev/null 2>&1; then uci -q set "zt-gateway.global.table_main='${TABLE_MAIN}'" fi if ! uci -q get "zt-gateway.global.table_drain" >/dev/null 2>&1; then uci -q set "zt-gateway.global.table_drain='${TABLE_DRAIN}'" fi if ! uci -q get "zt-gateway.global.dhcp_lease_first" >/dev/null 2>&1; then uci -q set "zt-gateway.global.dhcp_lease_first='${WIBLAN_LEASE_FIRST}'" fi if ! uci -q get "zt-gateway.global.dhcp_lease_last" >/dev/null 2>&1; then uci -q set "zt-gateway.global.dhcp_lease_last='${WIBLAN_LEASE_LAST}'" fi uci commit zt-gateway log "UCI global config updated with setup parameters" fi log "persistence setup complete" } # ---------------------------------------------------------------------------- # setup-all # ---------------------------------------------------------------------------- cmd_setup_all() { log "running full gateway setup..." cmd_setup_bridge cmd_setup_routing cmd_setup_dhcp cmd_setup_wifi_ap cmd_setup_hotplug cmd_setup_persistence log "=========================================" log "Full gateway setup complete!" log " Bridge: ${BRIDGE}" log " Ports: ${BRIDGE_PORTS}" log " Subnet: ${WIBLAN_CIDR}" log " Gateway IP: ${WIBLAN_GW}" log " Tables: ${TABLE_MAIN} (main), ${TABLE_DRAIN} (drain)" log " DHCP: ${WIBLAN_LEASE_FIRST} - ${WIBLAN_LEASE_LAST}" log " WiFi AP: ${WiFi_SSID} on ${WiFi_RADIO}" log "=========================================" } # ---------------------------------------------------------------------------- # Usage # ---------------------------------------------------------------------------- usage() { cat >&2 <<'USAGE' Usage: zt-gateway-setup Commands: status Show current setup state setup-bridge Create the br-zt bridge interface setup-routing Configure policy routing (tables 100/101, ip rules) setup-dhcp Configure DHCP for WIBLAN on br-zt setup-wifi-ap Create WIBLAN WiFi AP bridged to br-zt setup-hotplug Create hotplug script to re-apply routes on ifup setup-persistence Write rc.local + UCI network routes setup-all Run all setup-* commands in order USAGE } # ---------------------------------------------------------------------------- # Entry point # ---------------------------------------------------------------------------- main() { if [ $# -lt 1 ]; then usage die 1 "missing command" fi cmd=$1 shift case "$cmd" in status) cmd_status "$@" ;; setup-bridge) cmd_setup_bridge "$@" ;; setup-routing) cmd_setup_routing "$@" ;; setup-dhcp) cmd_setup_dhcp "$@" ;; setup-wifi-ap) cmd_setup_wifi_ap "$@" ;; setup-hotplug) cmd_setup_hotplug "$@" ;; setup-persistence) cmd_setup_persistence "$@" ;; setup-all) cmd_setup_all "$@" ;; -h|--help) usage; exit 0 ;; *) usage; die 1 "unknown command: $cmd" ;; esac } main "$@"