I'm using this on a raspberry pi 3 running Ubuntu server to bridge an Ethernet-only OpenSprinkler to my WiFi network.
This is a proxy-ARP pseudo-bridge (parprouted + dhcp-helper), not an L2 bridge. The pi routes between eth0 and wlan0.
sudo apt install parprouted dhcp-helper avahi-daemon net-tools networkd-dispatcher arp-scan iw/etc/netplan/60-wifi-bridge.yaml:
network:
version: 2
ethernets:
renderer: networkd
eth0:
optional: true
dhcp4: false/etc/sysctl.d/local.conf:
net.ipv4.ip_forward=1
# best-effort: let eth0 learn the OpenSprinkler from its gratuitous ARP after DHCP.
# not sufficient on its own — see the priming timer below.
net.ipv4.conf.eth0.arp_accept=1
/usr/local/sbin/wifi-bridge-setup.sh:
#!/bin/sh
# Mirror wlan0's current address onto eth0 as a /32 for parprouted.
set -e
addr=$(/sbin/ip -o -f inet addr show wlan0 | awk '{print $4}' | cut -d/ -f1 | head -n1)
[ -n "$addr" ] || exit 1
/usr/sbin/iw dev wlan0 set power_save off || true # Eat errors instead of failing the entire setup
/sbin/ip link set wlan0 promisc on
/sbin/ip link set eth0 up
# flush first: a changed wlan0 lease must not leave a stale /32 behind
/sbin/ip -4 addr flush dev eth0
/sbin/ip addr add "$addr/32" dev eth0/etc/systemd/system/wifi-bridge.service:
[Unit]
Description=parprouted proxy-ARP bridge (eth0 <-> wlan0)
After=network.target
[Service]
Type=forking
ExecStartPre=/usr/local/sbin/wifi-bridge-setup.sh
ExecStart=/usr/sbin/parprouted eth0 wlan0
ExecStopPost=-/sbin/ip -4 addr flush dev eth0
Restart=always
RestartSec=5/etc/networkd-dispatcher/routable.d/50-wifi-bridge:
#!/bin/sh
[ "$IFACE" = "wlan0" ] || exit 0
/usr/bin/systemctl restart wifi-bridge.service/etc/networkd-dispatcher/off.d/50-wifi-bridge:
#!/bin/sh
[ "$IFACE" = "wlan0" ] || exit 0
/usr/bin/systemctl stop wifi-bridge.service/usr/local/sbin/wifi-bridge-prime.sh:
#!/bin/sh
# Discover whatever the OpenSprinkler leased and keep its eth0 neighbor entry fresh.
# Also clears bogus wlan0 neighbor entries for eth0-side hosts, which make
# parprouted thrash (create arptab entry -> want_route 0 -> delete, forever).
# Logs only on change so the journal reads as events, not a 15s heartbeat.
state=/run/wifi-bridge-prime.state
subnet=$(/sbin/ip -o -f inet route show dev wlan0 scope link | awk '{print $1; exit}')
if [ -z "$subnet" ]; then
echo "wlan0 has no connected subnet yet; skipping this cycle"
exit 0
fi
self=$(cat /sys/class/net/eth0/address)
hosts=$(/usr/sbin/arp-scan --interface=eth0 --retry=2 "$subnet" 2>&1 \
| awk -v self="$self" \
'tolower($2) != self && /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[ \t]/ {print $1, $2}')
# The real work: refresh every run, silently. Clearing a stray wlan0 entry is
# rare enough to log as an event.
echo "$hosts" | while read ip mac; do
[ -n "$ip" ] || continue
/sbin/ip neigh replace "$ip" lladdr "$mac" dev eth0 nud reachable
if /sbin/ip neigh show to "$ip" dev wlan0 2>/dev/null | grep -q .; then
echo "clearing bogus wlan0 neighbor entry for $ip"
/sbin/ip neigh del "$ip" dev wlan0 2>/dev/null
fi
done
# The logging: only when what we found differs from last run.
if [ "$hosts" != "$(cat "$state" 2>/dev/null)" ]; then
if [ -z "$hosts" ]; then
echo "no hosts found on eth0 (subnet $subnet)"
else
echo "discovered on eth0: $hosts"
fi
printf '%s\n' "$hosts" > "$state"
fiMake the hook scripts executable.
sudo chmod +x /etc/networkd-dispatcher/routable.d/50-wifi-bridge
sudo chmod +x /etc/networkd-dispatcher/off.d/50-wifi-bridge
sudo chmod +x /usr/local/sbin/wifi-bridge-prime.sh
sudo chmod +x /usr/local/sbin/wifi-bridge-setup.shAdd Symlink:
sudo mkdir -p /etc/networkd-dispatcher/no-carrier.d
sudo ln -s /etc/networkd-dispatcher/off.d/50-wifi-bridge /etc/networkd-dispatcher/no-carrier.d/50-wifi-bridge/etc/systemd/system/wifi-bridge-prime.service:
[Unit]
Description=Prime parprouted neighbor table for OpenSprinkler
After=network-online.target
[Service]
Type=oneshot
ExecStart=-/usr/local/sbin/wifi-bridge-prime.sh/etc/systemd/system/wifi-bridge-prime.timer:
[Unit]
Description=Periodically prime parprouted for OpenSprinkler
[Timer]
OnBootSec=30
OnUnitActiveSec=15
[Install]
WantedBy=timers.targetEdit /etc/default/dhcp-helper:
# relay dhcp requests as broadcast to wlan0
DHCPHELPER_OPTS="-b wlan0"Edit /etc/avahi/avahi-daemon.conf:
[reflector]
enable-reflector=yes
Edit /etc/systemd/journald.conf:
SystemMaxUse=50MEnable services:
sudo systemctl enable dhcp-helper
sudo systemctl enable wifi-bridge-prime.timerNote: wifi-bridge.service is not enabled as it's managed by the routable hooks.
Reboot!
ip neigh show dev eth0
ip route | grep eth0
systemctl status wifi-bridge
journalctl -u wifi-bridge-prime.service
journalctl -u wifi-bridge.service- Main documentation: https://wiki.debian.org/BridgeNetworkConnectionsProxyArp
- Netplan yaml reference: https://netplan.readthedocs.io/en/stable/netplan-yaml/
- Netplan FAQ for post-up/post-down scripts: https://netplan.io/faq
- networkd-dispatcher docs: https://gitlab.com/craftyguy/networkd-dispatcher/-/tree/master
- WiFi promiscuous mode: PiSCSI/piscsi#1387
Inspired by: https://gist.github.com/Jiab77/76000284f8200da5019a232854421564