Last active
February 7, 2026 16:05
-
-
Save jvhaarst/366f4f83bf73cda17b43c892c9651f6a to your computer and use it in GitHub Desktop.
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 | |
| # | |
| # setup-transparent-bridge.sh | |
| # Configure an OpenWrt device as a transparent bridge / AP. | |
| # | |
| # What this does: | |
| # - Installs required packages (curl, ca-certificates, wpad-mbedtls) | |
| # - Bridges all physical ethernet ports (WAN + LAN) into br-lan | |
| # - Disables routing, NAT, firewall, and DHCP server | |
| # - Makes the device a DHCP *client* so it gets a management IP from upstream | |
| # - Configures dual-band WiFi (2.4GHz + 5GHz) as AP with 802.11r/k/v roaming | |
| # - Optionally enables STP to prevent loops | |
| # - Sets timezone to Europe/Amsterdam and configures NTP | |
| # | |
| # WARNING: After applying, the device will lose its current IP address and | |
| # obtain a new one via DHCP from your upstream router/network. | |
| # You WILL lose your SSH session. Find the device afterwards with: | |
| # nmap -sn <your_network>/24 | grep -i openwrt | |
| # | |
| # Usage: | |
| # sh setup-transparent-bridge.sh --ssid <SSID> --key <password> \ | |
| # [--country <CC>] [--channel-2g <ch>] [--channel-5g <ch>] \ | |
| # [--stp] [--dry-run] | |
| # | |
| # Required arguments: | |
| # --ssid <SSID> WiFi network name | |
| # --key <password> WiFi password (min 8 characters) | |
| # | |
| # Optional arguments: | |
| # --country <CC> Regulatory country code (default: NL) | |
| # --channel-2g <ch> 2.4GHz channel or 'auto' (default: auto) | |
| # --channel-5g <ch> 5GHz channel or 'auto' (default: auto) | |
| # --stp Enable Spanning Tree Protocol on the bridge | |
| # --dry-run Show what would be changed without applying anything | |
| # | |
| set -e | |
| # ── Options ────────────────────────────────────────────────────────────────── | |
| ENABLE_STP=0 | |
| DRY_RUN=0 | |
| WIFI_SSID="" | |
| WIFI_KEY="" | |
| WIFI_COUNTRY="NL" | |
| WIFI_CHANNEL_2G="auto" | |
| WIFI_CHANNEL_5G="auto" | |
| usage() { | |
| sed -n '2,/^$/{ s/^# \?//; p }' "$0" | |
| exit 1 | |
| } | |
| while [ $# -gt 0 ]; do | |
| case "$1" in | |
| --stp) ENABLE_STP=1 ;; | |
| --dry-run) DRY_RUN=1 ;; | |
| --ssid) WIFI_SSID="$2"; shift ;; | |
| --key) WIFI_KEY="$2"; shift ;; | |
| --country) WIFI_COUNTRY="$2"; shift ;; | |
| --channel-2g) WIFI_CHANNEL_2G="$2"; shift ;; | |
| --channel-5g) WIFI_CHANNEL_5G="$2"; shift ;; | |
| -h|--help) usage ;; | |
| *) echo "Unknown option: $1"; usage ;; | |
| esac | |
| shift | |
| done | |
| # Validate required arguments | |
| if [ -z "$WIFI_SSID" ]; then | |
| echo "Error: --ssid is required" | |
| usage | |
| fi | |
| if [ -z "$WIFI_KEY" ]; then | |
| echo "Error: --key is required" | |
| usage | |
| fi | |
| if [ "${#WIFI_KEY}" -lt 8 ]; then | |
| echo "Error: WiFi key must be at least 8 characters" | |
| exit 1 | |
| fi | |
| # ── Helpers ────────────────────────────────────────────────────────────────── | |
| info() { echo ">>> $*"; } | |
| warn() { echo "!!! $*" >&2; } | |
| # Wrapper: in dry-run mode, print the uci command instead of running it. | |
| _uci() { | |
| if [ "$DRY_RUN" -eq 1 ]; then | |
| echo " [dry-run] uci $*" | |
| else | |
| uci "$@" | |
| fi | |
| } | |
| # ── Detect the bridge device config section ────────────────────────────────── | |
| # Find the UCI section that defines br-lan (don't assume @device[0]). | |
| BRIDGE_SECTION="" | |
| idx=0 | |
| while uci -q get "network.@device[$idx]" >/dev/null 2>&1; do | |
| name=$(uci -q get "network.@device[$idx].name") | |
| type=$(uci -q get "network.@device[$idx].type") | |
| if [ "$name" = "br-lan" ] && [ "$type" = "bridge" ]; then | |
| BRIDGE_SECTION="network.@device[$idx]" | |
| break | |
| fi | |
| idx=$((idx + 1)) | |
| done | |
| if [ -z "$BRIDGE_SECTION" ]; then | |
| warn "Could not find a bridge device 'br-lan' in UCI network config." | |
| warn "Current network devices:" | |
| uci show network | grep '=device' | |
| exit 1 | |
| fi | |
| info "Found bridge config at: $BRIDGE_SECTION" | |
| # ── Detect physical ethernet interfaces ────────────────────────────────────── | |
| # Enumerate all physical ethernets (excluding lo, br-*, veth*, wlan*) from | |
| # /sys/class/net so we don't depend on hardcoded names. | |
| ALL_PORTS="" | |
| for iface in /sys/class/net/*; do | |
| name=$(basename "$iface") | |
| case "$name" in | |
| lo|br-*|veth*|wlan*|docker*) continue ;; | |
| esac | |
| # Only include interfaces that look like real hardware (have a device link | |
| # or are not virtual). On OpenWrt, DSA ports and plain ethX both qualify. | |
| if [ -d "$iface/device" ] || [ -e "$iface/phy80211" ] 2>/dev/null; then | |
| : | |
| elif [ ! -d "$iface/upper_br-lan" ] && [ ! -L "$iface/master" ] 2>/dev/null; then | |
| # Might already be in a bridge — still include it | |
| : | |
| fi | |
| # Filter: must be type 1 (ethernet) | |
| if [ -f "$iface/type" ] && [ "$(cat "$iface/type")" = "1" ]; then | |
| ALL_PORTS="$ALL_PORTS $name" | |
| fi | |
| done | |
| ALL_PORTS=$(echo "$ALL_PORTS" | xargs) # trim whitespace | |
| if [ -z "$ALL_PORTS" ]; then | |
| warn "Could not detect any physical ethernet interfaces!" | |
| exit 1 | |
| fi | |
| info "Detected physical ethernet ports: $ALL_PORTS" | |
| # Sanity check: we need at least 2 ports for a meaningful bridge | |
| PORT_COUNT=$(echo "$ALL_PORTS" | wc -w) | |
| if [ "$PORT_COUNT" -lt 2 ]; then | |
| warn "Only $PORT_COUNT ethernet port(s) detected ($ALL_PORTS)." | |
| warn "A transparent bridge typically needs at least 2 ports." | |
| printf "Continue anyway? [y/N] " | |
| read -r ans | |
| case "$ans" in y|Y) ;; *) echo "Aborted."; exit 1 ;; esac | |
| fi | |
| # ── Derive hostname from br-lan MAC ───────────────────────────────────────── | |
| MAC=$(cat /sys/class/net/br-lan/address 2>/dev/null || ip link show br-lan 2>/dev/null | awk '/link\/ether/{print $2}') | |
| ID=$(echo "$MAC" | tr -d ':' | cut -c 9-) | |
| HOSTNAME="OPENWRT-${ID:-UNKNOWN}" | |
| # ── Summary ────────────────────────────────────────────────────────────────── | |
| echo "" | |
| info "Configuration summary:" | |
| echo " Hostname: $HOSTNAME" | |
| echo " Bridge: $ALL_PORTS" | |
| echo " WiFi SSID: $WIFI_SSID" | |
| echo " Country: $WIFI_COUNTRY" | |
| echo " 2.4GHz ch: $WIFI_CHANNEL_2G" | |
| echo " 5GHz ch: $WIFI_CHANNEL_5G" | |
| echo " STP: $([ "$ENABLE_STP" -eq 1 ] && echo 'yes' || echo 'no')" | |
| echo "" | |
| if [ "$DRY_RUN" -eq 1 ]; then | |
| info "=== DRY RUN — no changes will be made ===" | |
| fi | |
| # ── 1. Install packages ───────────────────────────────────────────────────── | |
| info "Installing required packages ..." | |
| if [ "$DRY_RUN" -eq 0 ]; then | |
| opkg update | |
| opkg install curl ca-certificates | |
| # Replace basic wpad with full wpad for 802.11r/k/v support | |
| opkg remove wpad-basic-mbedtls 2>/dev/null || true | |
| opkg install wpad-mbedtls | |
| else | |
| echo " [dry-run] opkg update" | |
| echo " [dry-run] opkg install curl ca-certificates" | |
| echo " [dry-run] opkg remove wpad-basic-mbedtls && opkg install wpad-mbedtls" | |
| fi | |
| # ── 2. Hostname ────────────────────────────────────────────────────────────── | |
| info "Setting hostname to $HOSTNAME ..." | |
| _uci set system.@system[0].hostname="$HOSTNAME" | |
| # ── 3. Timezone & NTP ──────────────────────────────────────────────────────── | |
| info "Setting timezone to Europe/Amsterdam ..." | |
| _uci set system.@system[0].timezone='CET-1CEST,M3.5.0,M10.5.0/3' | |
| _uci set system.@system[0].zonename='Europe/Amsterdam' | |
| info "Configuring NTP ..." | |
| _uci set system.ntp.enabled='1' | |
| _uci -q delete system.ntp.server 2>/dev/null || true | |
| _uci add_list system.ntp.server='0.openwrt.pool.ntp.org' | |
| _uci add_list system.ntp.server='1.openwrt.pool.ntp.org' | |
| _uci commit system | |
| # ── 4. Disable DHCP server / RA on LAN ────────────────────────────────────── | |
| info "Disabling DHCP server and RA on LAN ..." | |
| _uci set dhcp.lan.ignore='1' | |
| _uci set dhcp.lan.dhcpv6='disabled' | |
| _uci set dhcp.lan.ra='disabled' | |
| _uci commit dhcp | |
| # ── 5. Remove WAN interfaces (no routing / NAT) ───────────────────────────── | |
| info "Removing WAN interfaces ..." | |
| _uci -q delete network.wan || true | |
| _uci -q delete network.wan6 || true | |
| # ── 6. Bridge all ports ───────────────────────────────────────────────────── | |
| info "Setting bridge ports to: $ALL_PORTS ..." | |
| _uci set "${BRIDGE_SECTION}.ports=$ALL_PORTS" | |
| if [ "$ENABLE_STP" -eq 1 ]; then | |
| info "Enabling STP on bridge ..." | |
| _uci set "${BRIDGE_SECTION}.stp=1" | |
| else | |
| info "STP not enabled (use --stp to enable)" | |
| fi | |
| # ── 7. LAN as DHCP client ─────────────────────────────────────────────────── | |
| info "Switching LAN to DHCP client ..." | |
| _uci set network.lan.proto='dhcp' | |
| _uci -q delete network.lan.ipaddr 2>/dev/null || true | |
| _uci -q delete network.lan.netmask 2>/dev/null || true | |
| _uci -q delete network.lan.ip6assign 2>/dev/null || true | |
| _uci set network.lan.ipv6='auto' | |
| _uci set network.lan.hostname="$HOSTNAME" | |
| _uci commit network | |
| # ── 8. Configure WiFi ─────────────────────────────────────────────────────── | |
| info "Configuring WiFi radios ..." | |
| # --- radio0: 2.4 GHz (HT40, psk-mixed for legacy client compat) --- | |
| _uci set wireless.radio0.channel="$WIFI_CHANNEL_2G" | |
| _uci set wireless.radio0.country="$WIFI_COUNTRY" | |
| _uci set wireless.radio0.band='2g' | |
| _uci set wireless.radio0.htmode='HT40' | |
| _uci set wireless.radio0.legacy_rates='1' | |
| _uci set wireless.radio0.cell_density='1' | |
| _uci -q delete wireless.radio0.disabled 2>/dev/null || true | |
| _uci set wireless.default_radio0.device='radio0' | |
| _uci set wireless.default_radio0.network='lan' | |
| _uci set wireless.default_radio0.mode='ap' | |
| _uci set wireless.default_radio0.ssid="$WIFI_SSID" | |
| _uci set wireless.default_radio0.encryption='psk-mixed' | |
| _uci set wireless.default_radio0.key="$WIFI_KEY" | |
| # 802.11r (Fast BSS Transition) | |
| _uci set wireless.default_radio0.ieee80211r='1' | |
| _uci set wireless.default_radio0.mobility_domain='dead' | |
| _uci set wireless.default_radio0.ft_over_ds='0' | |
| _uci set wireless.default_radio0.ft_psk_generate_local='1' | |
| # 802.11k/v (radio resource mgmt / BSS transition mgmt) | |
| _uci set wireless.default_radio0.ieee80211k='1' | |
| _uci set wireless.default_radio0.wnm_sleep_mode='1' | |
| _uci set wireless.default_radio0.wnm_sleep_mode_no_keys='1' | |
| _uci set wireless.default_radio0.bss_transition='1' | |
| _uci set wireless.default_radio0.proxy_arp='1' | |
| # --- radio1: 5 GHz (HE160 / WiFi 6, SAE-only) --- | |
| _uci set wireless.radio1.channel="$WIFI_CHANNEL_5G" | |
| _uci set wireless.radio1.country="$WIFI_COUNTRY" | |
| _uci set wireless.radio1.band='5g' | |
| _uci set wireless.radio1.htmode='HE160' | |
| _uci set wireless.radio1.cell_density='1' | |
| _uci -q delete wireless.radio1.disabled 2>/dev/null || true | |
| _uci set wireless.default_radio1.device='radio1' | |
| _uci set wireless.default_radio1.network='lan' | |
| _uci set wireless.default_radio1.mode='ap' | |
| _uci set wireless.default_radio1.ssid="$WIFI_SSID" | |
| _uci set wireless.default_radio1.encryption='sae' | |
| _uci set wireless.default_radio1.key="$WIFI_KEY" | |
| _uci set wireless.default_radio1.ocv='1' | |
| # 802.11r (Fast BSS Transition) | |
| _uci set wireless.default_radio1.ieee80211r='1' | |
| _uci set wireless.default_radio1.mobility_domain='dead' | |
| _uci set wireless.default_radio1.ft_over_ds='0' | |
| # 802.11k/v (radio resource mgmt / BSS transition mgmt) | |
| _uci set wireless.default_radio1.dtim_period='3' | |
| _uci set wireless.default_radio1.ieee80211k='1' | |
| _uci set wireless.default_radio1.wnm_sleep_mode='1' | |
| _uci set wireless.default_radio1.wnm_sleep_mode_no_keys='1' | |
| _uci set wireless.default_radio1.bss_transition='1' | |
| _uci set wireless.default_radio1.proxy_arp='1' | |
| _uci commit wireless | |
| # ── 9. Disable firewall ───────────────────────────────────────────────────── | |
| info "Disabling firewall ..." | |
| if [ "$DRY_RUN" -eq 0 ]; then | |
| /etc/init.d/firewall stop 2>/dev/null || true | |
| /etc/init.d/firewall disable 2>/dev/null || true | |
| fi | |
| # ── 10. Disable dnsmasq (no longer serving DHCP/DNS) ──────────────────────── | |
| info "Disabling dnsmasq (not needed in bridge mode) ..." | |
| if [ "$DRY_RUN" -eq 0 ]; then | |
| /etc/init.d/dnsmasq stop 2>/dev/null || true | |
| /etc/init.d/dnsmasq disable 2>/dev/null || true | |
| fi | |
| # ── 11. Disable odhcpd (no longer serving DHCPv6/RA) ──────────────────────── | |
| info "Disabling odhcpd (not needed in bridge mode) ..." | |
| if [ "$DRY_RUN" -eq 0 ]; then | |
| /etc/init.d/odhcpd stop 2>/dev/null || true | |
| /etc/init.d/odhcpd disable 2>/dev/null || true | |
| fi | |
| # ── Apply ──────────────────────────────────────────────────────────────────── | |
| if [ "$DRY_RUN" -eq 1 ]; then | |
| echo "" | |
| info "Dry run complete. No changes were made." | |
| info "Run without --dry-run to apply." | |
| exit 0 | |
| fi | |
| echo "" | |
| warn "About to restart networking and WiFi. You WILL lose your SSH session." | |
| warn "The device will obtain a new IP via DHCP." | |
| warn "Find it afterwards with: nmap -sn <your_network>/24 | grep -i openwrt" | |
| echo "" | |
| printf "Apply and restart now? [y/N] " | |
| read -r confirm | |
| case "$confirm" in | |
| y|Y) | |
| info "Restarting services ..." | |
| /etc/init.d/sysntpd restart | |
| /etc/init.d/network restart | |
| wifi reload | |
| info "Done! Hostname: $HOSTNAME | SSID: $WIFI_SSID" | |
| ;; | |
| *) | |
| warn "Changes are staged in UCI but NOT applied yet." | |
| warn "Run '/etc/init.d/network restart && wifi reload' manually to apply." | |
| ;; | |
| esac |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
To grab from AP: