Skip to content

Instantly share code, notes, and snippets.

@jvhaarst
Last active February 7, 2026 16:05
Show Gist options
  • Select an option

  • Save jvhaarst/366f4f83bf73cda17b43c892c9651f6a to your computer and use it in GitHub Desktop.

Select an option

Save jvhaarst/366f4f83bf73cda17b43c892c9651f6a to your computer and use it in GitHub Desktop.
#!/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
@jvhaarst
Copy link
Author

jvhaarst commented Feb 7, 2026

To grab from AP:

opkg update && opkg install curl
curl --remote-name https://gist.githubusercontent.com/jvhaarst/366f4f83bf73cda17b43c892c9651f6a/raw/6051873faf41819797869c69137d53ab8ef4804e/m3000_openwrt_setup.sh
chmod +x m3000_openwrt_setup.sh
./m3000_openwrt_setup.sh -h

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment