The goal is to connect a non-WiFi computer to a WiFI network via a Raspberry Pi. We will use a Raspberry Pi 4 B as a bridge between the non-WiFi computer and the WiFi network. The Raspberry Pi connects to WiFi and shares its connection with other computers using Ethernet.

diagram of the network topology

The Raspberry Pi 4 B has 802.11ac WiFi, and so seems well suited to this task. This Stack Overflow answer and accompanying script as well as this proxy arp approach and Debian’s Bridging Network Connections with Proxy ARP are the primary sources for how I got this working and are the inspiration for this guide.

I have two separate guides below. Follow either the Same Subnet steps or the Separate Subnet steps below based on the configuration you would prefer. My process has evolved over time and I prefer the Same Subnet approach as it is meant to be a seamless bridge for clients.

Note: These instructions were only tested and verified on a fresh install of 2019-09-26-raspbian-buster-lite.img

Option 1 - Same Subnet

This option is a bit involved, but provides the most seamless experience. Bridged clients connected to the Pi should behave as if they were connected directly to the upstream network

The following script configures everything in one go for a standard Raspbian-based Raspberry Pi. This script is based off a very helpful Stack Overflow answer. The only changes you should need to make are for the ssid and psk variables used for connecting the Pi to your WiFi network.

Note: This script drastically changes the networking configuration and your Pi may end up in an unreliable networking state if anything goes wrong

#!/usr/bin/env bash

set -e

[ $EUID -ne 0 ] && echo "run as root" >&2 && exit 1

####################################
# Update these variables as needed #
####################################

ssid="the ssid"
psk="the password"
country="US"

##########################################################
# You should not need to update anything below this line #
##########################################################

# parprouted  - Proxy ARP IP bridging daemon
# dhcp-helper - A DHCP/BOOTP relay agent
# dnsmasq     - A lightweight DHCP and caching DNS server

apt update && apt install -y parprouted dhcp-helper dnsmasq

systemctl stop dhcp-helper
systemctl enable dhcp-helper

# Prevent the networking and dhcpcd service from running. networking.service
# is a debian-specific package and not the same as systemd-network. Disable the
# dhcpcd service as well.
systemctl mask networking.service dhcpcd.service

# This tells resolvconf to ignore whenever some daemon tries to modify the
# resolv.conf file - https://wiki.debian.org/resolv.conf
sed -i '1i resolvconf=NO' /etc/resolvconf.conf

systemctl enable systemd-networkd.service systemd-resolved.service

# Use systemd-resolved to handle the /etc/resolf.conf config
ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf

# Create the WiFi config to connect to the upstream network like normal
cat > /etc/wpa_supplicant/wpa_supplicant-wlan0.conf < /etc/systemd/network/08-wlan0.network < /etc/default/dhcp-helper </etc/avahi/avahi-daemon.conf
[server]
use-ipv4=yes
use-ipv6=yes
ratelimit-interval-usec=1000000
ratelimit-burst=1000

[wide-area]
enable-wide-area=yes

[publish]
publish-hinfo=no
publish-workstation=no

[reflector]
enable-reflector=yes

[rlimits]
EOF

# Create a helper script to get an adapter's IP address
cat <<'EOF' >/usr/bin/get-adapter-ip
#!/usr/bin/env bash

/sbin/ip -4 -br addr show ${1} | /bin/grep -Po "\\d+\\.\\d+\\.\\d+\\.\\d+"
EOF
chmod +x /usr/bin/get-adapter-ip

# I have to admit, I do not understand ARP and IP forwarding enough to explain
# exactly what is happening here. I am building off the work of others. In short
# this is a service to forward traffic from wlan0 to eth0
cat <<'EOF' >/etc/systemd/system/parprouted.service
[Unit]
Description=proxy arp routing service
Documentation=https://raspberrypi.stackexchange.com/q/88954/79866

[Service]
Type=forking
# Restart until wlan0 gained carrier
Restart=on-failure
RestartSec=5
TimeoutStartSec=30
ExecStartPre=/lib/systemd/systemd-networkd-wait-online --interface=wlan0 --timeout=6 --quiet
ExecStartPre=/bin/echo 'systemd-networkd-wait-online: wlan0 is online'
# clone the dhcp-allocated IP to eth0 so dhcp-helper will relay for the correct subnet
ExecStartPre=/bin/bash -c '/sbin/ip addr add $(/usr/bin/get-adapter-ip wlan0)/32 dev eth0'
ExecStartPre=/sbin/ip link set dev eth0 up
ExecStartPre=/sbin/ip link set wlan0 promisc on
ExecStart=-/usr/sbin/parprouted eth0 wlan0
ExecStopPost=/sbin/ip link set wlan0 promisc off
ExecStopPost=/sbin/ip link set dev eth0 down
ExecStopPost=/bin/bash -c '/sbin/ip addr del $(/usr/bin/get-adapter-ip eth0)/32 dev eth0'

[Install]
WantedBy=wpa_supplicant@wlan0.service
EOF

systemctl daemon-reload
systemctl enable parprouted.service

systemctl start wpa_supplicant@wlan0 dhcp-helper systemd-networkd systemd-resolved

It may take a moment for your Pi to connect to WiFi, but once it does (and on subsequent reboots) it should be able to start forwarding traffic over the ethernet port.

Option 2 - Separate Subnet

This option is simpler and does not modify the system networking stack as dramatically as the first option. However, this option results in a more limited setup. Bridged clients will be on a separate subnet so the network configuration may not work like you expect. This option is fine if all you can about is connecting a bridged client to the Internet. Note that this script is a bit opinionated and chooses DNS servers and the subnet IP range for you. Update the script as needed

Connect to WiFi on the Raspberry Pi like normal, then run this script below.

#!/usr/bin/env bash

set -e

[ $EUID -ne 0 ] && echo "run as root" >&2 && exit 1

apt-get update && apt install -y dnsmasq

# Create a directory where we will store our `iptables` forwarding rules.
mkdir -p /etc/iptables

# Create `iptables` rules by running this command to generate a `rules.v4` file
cat <<'EOF' >/etc/iptables/rules.v4
# Generated by iptables-save v1.6.0 on Sat Apr 14 22:29:00 2018
*nat
:PREROUTING ACCEPT [98:9304]
:INPUT ACCEPT [98:9304]
:OUTPUT ACCEPT [2:152]
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -o wlan0 -j MASQUERADE
COMMIT
# Completed on Sat Apr 14 22:29:00 2018
# Generated by iptables-save v1.6.0 on Sat Apr 14 22:29:00 2018
*filter
:INPUT ACCEPT [791:83389]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [333:34644]
-A FORWARD -i wlan0 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -i eth0 -o wlan0 -j ACCEPT
COMMIT
# Completed on Sat Apr 14 22:29:00 2018
EOF

# Load our `iptables` forwarding rules at each boot
# https://major.io/2009/11/16/automatically-loading-iptables-on-debianubuntu/
cat <<'EOF' >/etc/network/if-up.d/iptables
#!/bin/sh
iptables-restore < /etc/iptables/rules.v4
EOF

chmod +x /etc/network/if-up.d/iptables

# Enable persistent `ipv4` forwarding for each system boot
# http://www.ducea.com/2006/08/01/how-to-enable-ip-forwarding-in-linux/
sed -i'' \
  s/#net.ipv4.ip_forward=1/net.ipv4.ip_forward=1/ \
  /etc/sysctl.conf

# Create a static IP address configuration. The `eth0` adapter will use a
# static IP of `10.1.1.1` on this new subnet.
cat <<'EOF' >/etc/network/interfaces.d/eth0
auto eth0
allow-hotplug eth0
iface eth0 inet static
  address 10.1.1.1
  netmask 255.255.255.0
  gateway 10.1.1.1
EOF

# Create a `dnsmasq` DHCP config at `/etc/dnsmasq.d/bridge.conf`.
# The Raspberry Pi will act as a DHCP server to the client connected over
# ethernet. The DNS server will be `8.8.8.8` (Google's DNS) and the
# range will start at `10.1.1.2`.
cat <<'EOF' >/etc/dnsmasq.d/bridge.conf
interface=eth0
bind-interfaces
server=8.8.8.8
domain-needed
bogus-priv
dhcp-range=10.1.1.2,10.1.1.254,12h
EOF

Reboot.

reboot

View our current iptables to verify that they are loading at boot as needed.

iptables -L

Conclusion

You should now be able to connect a device to the ethernet port on the Raspberry Pi and receive an IP address.

I ran some very basic speed tests for my desktop connected through the Raspberry Pi bridge and was pleasantly surprised by the results.

I cannot guarantee how reliable this is for longterm use, but it seems promising.

$ iperf3 --reverse --format m --version4 --client iperf.he.net
Connecting to host iperf.he.net, port 5201
Reverse mode, remote host iperf.he.net is sending
[  6] local 10.1.1.187 port 52264 connected to 216.218.227.10 port 5201
[ ID] Interval           Transfer     Bitrate
[  6]   0.00-1.00   sec  2.06 MBytes  17.3 Mbits/sec
[  6]   1.00-2.00   sec  7.57 MBytes  63.5 Mbits/sec
[  6]   2.00-3.00   sec  8.46 MBytes  71.0 Mbits/sec
[  6]   3.00-4.00   sec  8.48 MBytes  71.2 Mbits/sec
[  6]   4.00-5.00   sec  8.49 MBytes  71.2 Mbits/sec
[  6]   5.00-6.00   sec  6.32 MBytes  53.0 Mbits/sec
[  6]   6.00-7.00   sec  8.54 MBytes  71.6 Mbits/sec
[  6]   7.00-8.00   sec  8.06 MBytes  67.6 Mbits/sec
[  6]   8.00-9.00   sec  6.92 MBytes  58.0 Mbits/sec
[  6]   9.00-10.00  sec  7.73 MBytes  64.9 Mbits/sec
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate         Retr
[  6]   0.00-10.00  sec  77.6 MBytes  65.1 Mbits/sec  809             sender
[  6]   0.00-10.00  sec  72.6 MBytes  60.9 Mbits/sec                  receiver

iperf Done.

speed results