#!/usr/bin/env bash # Configure Raspberry Pi OS: Wi-Fi client on IF_WAN (default wlan0), Ethernet IF_LAN # (default eth0) toward an external AP. Static LAN IP, DHCP via dnsmasq, NAT masquerade. # # Usage: # sudo ./pi-eth-lan-router.sh install # sudo ./pi-eth-lan-router.sh remove # # Environment overrides (optional): # IF_WAN=wlan0 IF_LAN=eth0 LAN_IP=192.168.4.1 LAN_PREFIX=24 \ # DHCP_START=192.168.4.100 DHCP_END=192.168.4.200 \ # DNSMASQ_DNS=1.1.1.1,8.8.8.8 \ # sudo ./pi-eth-lan-router.sh install set -euo pipefail IF_WAN="${IF_WAN:-wlan0}" IF_LAN="${IF_LAN:-eth0}" LAN_IP="${LAN_IP:-192.168.4.1}" LAN_PREFIX="${LAN_PREFIX:-24}" DHCP_START="${DHCP_START:-192.168.4.100}" DHCP_END="${DHCP_END:-192.168.4.200}" # Comma-separated DNS for DHCP clients (Pi does not need to run a resolver). DNSMASQ_DNS="${DNSMASQ_DNS:-1.1.1.1,8.8.8.8}" NM_CON_NAME="pi-eth-lan-router" MARK_BEGIN="# BEGIN pi-eth-lan-router (scripts/pi-eth-lan-router.sh)" MARK_END="# END pi-eth-lan-router" SYSCTL_FILE="/etc/sysctl.d/99-pi-eth-lan-router.conf" DNSMASQ_SNIPPET="/etc/dnsmasq.d/pi-eth-lan-router.conf" NFT_SNIPPET="/etc/nftables.d/50-pi-eth-lan-router.nft" NFT_INCLUDE='include "/etc/nftables.d/50-pi-eth-lan-router.nft"' NFTABLES_CONF="/etc/nftables.conf" DHCPCD_CONF="/etc/dhcpcd.conf" die() { echo "error: $*" >&2; exit 1; } log() { echo "$*"; } need_root() { [[ "${EUID:-0}" -eq 0 ]] || die "run as root (sudo)" } have_cmd() { command -v "$1" >/dev/null 2>&1; } apt_install() { export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y -qq dnsmasq nftables } write_sysctl() { cat >"$SYSCTL_FILE" </dev/null || sysctl -p "$SYSCTL_FILE" || true } remove_sysctl() { rm -f "$SYSCTL_FILE" sysctl --system -q 2>/dev/null || true } write_dnsmasq() { local mask="255.255.255.0" if [[ "$LAN_PREFIX" != "24" ]]; then die "only LAN_PREFIX=24 is supported by this script (extend dnsmasq netmask manually)" fi cat >"$DNSMASQ_SNIPPET" <"$NFT_SNIPPET" </dev/null; then printf '\n# pi-eth-lan-router\n%s\n' "$NFT_INCLUDE" >>"$NFTABLES_CONF" elif [[ ! -f "$NFTABLES_CONF" ]]; then log "warning: $NFTABLES_CONF missing; NAT was not added for boot persistence. Install/configure nftables, or add: $NFT_INCLUDE" fi } remove_nft() { rm -f "$NFT_SNIPPET" if [[ -f "$NFTABLES_CONF" ]]; then sed -i '/# pi-eth-lan-router/d;/50-pi-eth-lan-router\.nft/d' "$NFTABLES_CONF" || true fi nft delete table ip pi_eth_wlan_nat 2>/dev/null || true } apply_nft() { if have_cmd nft; then nft delete table ip pi_eth_wlan_nat 2>/dev/null || true nft -f "$NFT_SNIPPET" fi } configure_nm_eth() { have_cmd nmcli || return 1 systemctl is-active --quiet NetworkManager 2>/dev/null || return 1 if nmcli -t -f NAME con show --active 2>/dev/null | grep -qxF "$NM_CON_NAME"; then nmcli con down "$NM_CON_NAME" || true fi if nmcli -t -f NAME con show 2>/dev/null | grep -qxF "$NM_CON_NAME"; then nmcli con mod "$NM_CON_NAME" \ connection.interface-name "$IF_LAN" \ ipv4.method manual \ ipv4.addresses "${LAN_IP}/${LAN_PREFIX}" \ ipv4.gateway "" \ ipv4.dns "" \ ipv4.never-default yes \ ipv6.method ignore else nmcli con add type ethernet con-name "$NM_CON_NAME" ifname "$IF_LAN" \ ipv4.method manual \ ipv4.addresses "${LAN_IP}/${LAN_PREFIX}" \ ipv4.gateway "" \ ipv4.dns "" \ ipv4.never-default yes \ ipv6.method ignore fi if ! nmcli con up "$NM_CON_NAME"; then log "warning: could not activate '$NM_CON_NAME' (is $IF_LAN connected?); profile saved for next boot." fi return 0 } remove_nm_eth() { have_cmd nmcli || return 0 if nmcli -t -f NAME con show 2>/dev/null | grep -qxF "$NM_CON_NAME"; then nmcli con delete "$NM_CON_NAME" || true fi } configure_dhcpcd_eth() { [[ -f "$DHCPCD_CONF" ]] || return 1 if grep -qF "$MARK_BEGIN" "$DHCPCD_CONF" 2>/dev/null; then sed -i "/$MARK_BEGIN/,/$MARK_END/d" "$DHCPCD_CONF" || true fi { echo "$MARK_BEGIN" echo "interface $IF_LAN" echo "static ip_address=${LAN_IP}/${LAN_PREFIX}" echo "nohook wpa_supplicant" echo "$MARK_END" } >>"$DHCPCD_CONF" systemctl restart dhcpcd 2>/dev/null || true return 0 } remove_dhcpcd_block() { [[ -f "$DHCPCD_CONF" ]] || return 0 if grep -qF "$MARK_BEGIN" "$DHCPCD_CONF" 2>/dev/null; then sed -i "/$MARK_BEGIN/,/$MARK_END/d" "$DHCPCD_CONF" || true systemctl restart dhcpcd 2>/dev/null || true fi } configure_eth_static() { if configure_nm_eth; then log "configured $IF_LAN via NetworkManager profile '$NM_CON_NAME'" return 0 fi if configure_dhcpcd_eth; then log "configured $IF_LAN via dhcpcd ($DHCPCD_CONF)" return 0 fi die "neither NetworkManager (active) nor $DHCPCD_CONF found; set $IF_LAN to ${LAN_IP}/${LAN_PREFIX} manually" } remove_eth_static() { remove_nm_eth remove_dhcpcd_block } do_install() { need_root log "installing packages (dnsmasq, nftables)…" apt_install log "writing sysctl, dnsmasq, nftables snippets…" write_sysctl write_dnsmasq write_nft log "setting static IP on $IF_LAN…" configure_eth_static log "restarting dnsmasq…" systemctl enable dnsmasq systemctl restart dnsmasq log "loading NAT rules and enabling nftables…" apply_nft systemctl enable nftables 2>/dev/null || true systemctl restart nftables 2>/dev/null || true log "done. Connect $IF_LAN to the external AP (DHCP off on the AP)." log "Join Wi-Fi on $IF_WAN to the uplink network and complete any captive portal on the Pi." } do_remove() { need_root remove_eth_static remove_dnsmasq systemctl restart dnsmasq 2>/dev/null || true remove_nft systemctl restart nftables 2>/dev/null || true remove_sysctl sysctl -w net.ipv4.ip_forward=0 2>/dev/null || true log "removed pi-eth-lan-router configuration snippets and NM profile '$NM_CON_NAME' (if present)." } usage() { cat <