Raspberry Pi 5 VPN Router — via Tailscale Exit Node
Build a home VPN router from a Raspberry Pi 5, using OpenWrt and Tailscale. All LAN client traffic exits through a remote Tailscale exit node — no VPN client needed on the clients.
Table of Contents
- Parts List
- Network Topology
- Installing OpenWrt
- Network Configuration
- Installing Tailscale
- Connecting to a Remote Exit Node
- Auto-Start and Optimization
- Performance
- Troubleshooting
Parts List
| Part | Recommendation | Notes |
|---|---|---|
| Raspberry Pi 5 (4GB or 8GB) | Official resellers | 4GB is more than enough for a router |
| USB 3.0 Gigabit Ethernet adapter | RTL8153 chipset adapter | See below |
| microSD card (16GB+) | Samsung EVO Plus, SanDisk Extreme | Class 10 / A1 minimum |
| Power supply | Official Pi 5 USB-C 27W (5.1V/5A) | The USB GbE adapter draws extra power, don’t skimp on the PSU |
| Cooler | Official Active Cooler or metal case with fan | Router runs continuously, cooling is essential |
| Ethernet cable (x2) | Cat5e or Cat6 | WAN + LAN |
USB Ethernet Adapter — Which One?
Look for the RTL8153 chipset, not AX88179. The Realtek driver has better Linux/OpenWrt support — more stable and faster on the Pi.
Specific recommendations:
- Ugreen USB 3.0 Gigabit Ethernet Adapter — RTL8153, widely available, cheap
- Cable Matters USB 3.0 to Gigabit Ethernet — RTL8153, reliable
- TP-Link UE300 — RTL8153, though sometimes has boot-compatibility issues on Pi
Tip: Plug in the adapter before powering on the Pi — some adapters can cause a reboot if hot-plugged.
Network Topology
Internet ←→ ISP Router ←→ [eth0 WAN] RPi5 [eth1 LAN] ←→ Switch/AP ←→ Clients
↕
Tailscale tunnel
↕
Remote Exit Node
- eth0 (built-in port) → upstream internet/WAN side. Connect this port to the ISP router, modem-router, or another network that already has internet access.
- eth1 (USB adapter) → local network/LAN side. Connect this port to a switch, access point, or a single client device that should use the Pi as its router.
- The Pi NATs LAN traffic and routes it through the Tailscale tunnel to a remote exit node
In short: built-in Ethernet = upstream internet, USB Ethernet adapter = local network.
1. Installing OpenWrt
Download the Image
- Go to the OpenWrt Firmware Selector
- Select the 24.10.1 stable release (or newer if available)
- Download the Factory (SQUASHFS) image (
.img.gz)
Why SquashFS and not EXT4? SquashFS uses a read-only root + writable overlay. Advantage: you can restore factory state anytime with
firstboot(useful if you lock yourself out with the firewall), and it means fewer SD card writes → longer lifespan. EXT4 images don’t have a factory reset option.
Write to SD Card
Linux/macOS:
# Extract and write (replace /dev/sdX with your card's actual device)
gunzip openwrt-*.img.gz
sudo dd if=openwrt-*.img of=/dev/sdX bs=4M status=progress
sync
Or use Raspberry Pi Imager: “Use custom” → select the .img file.
First Boot
- Insert the microSD into the Pi
- Plug in the USB Ethernet adapter first
- For initial setup only, connect eth0 (built-in) to your laptop or a simple switch so you can reach OpenWrt
- Power on
- Wait ~1 minute for boot
- In your browser: http://192.168.1.1 → LuCI web interface
- First step: set a root password: System → Administration → Router Password
Temporary setup note: During first boot,
eth0still carries the default OpenWrt LAN at192.168.1.1. After the router cutover below,eth0becomes the WAN port and the USB adapter (eth1) becomes the LAN port.
Direct laptop-to-Pi connection note: On some systems, a direct Ethernet connection to the Pi on first boot may not get a DHCP lease automatically. If that happens, set a temporary static IP on your computer’s Ethernet interface:
- IP:
192.168.1.2- Netmask:
255.255.255.0- Gateway:
192.168.1.1Then browse to
http://192.168.1.1.
Reproducible Direct eth0 Setup From a Linux Laptop
This is a tested direct-connection workflow for a fresh OpenWrt boot when the USB Ethernet adapter is not working yet.
- Disconnect the USB Ethernet adapter from the Pi.
- Connect the laptop directly to the Pi’s built-in
eth0port. - Power on the Pi and wait 60-90 seconds.
- On the laptop, check the interface and routing state:
ip -brief link
ip -brief addr
ip route
nmcli device status
- If the Ethernet interface does not already have
192.168.1.2/24, set a temporary static IPv4 address with NetworkManager:
nmcli connection modify "Wired connection 1" ipv4.method manual ipv4.addresses 192.168.1.2/24 ipv4.gateway 192.168.1.1
nmcli connection down "Wired connection 1"
nmcli connection up "Wired connection 1"
- Verify that the Pi answers on the default OpenWrt address:
ping -c 3 -W 2 192.168.1.1
curl -I --max-time 5 http://192.168.1.1
ip neigh show dev enp0s31f6
Expected results:
pingreturns replies from192.168.1.1curlreturnsHTTP/1.1 200 OKip neighshows a MAC address for192.168.1.1
- Open LuCI in a browser:
http://192.168.1.1
- After setting the root password, verify SSH access:
ssh root@192.168.1.1
If you want to automate the SSH login for testing, use sshpass with your own password:
sshpass -p '<PASSWORD>' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@192.168.1.1
- Once logged into the Pi, verify the fresh-boot network state:
uname -a
uci get system.@system[0].hostname
ip link show
ip addr show
On a fresh OpenWrt boot, eth0 is typically attached to br-lan, and br-lan holds 192.168.1.1/24.
2. Network Configuration
Install the USB Ethernet Driver
Before configuring eth1, install the driver for the USB adapter chipset.
On older OpenWrt releases, the package manager is usually opkg. On OpenWrt 25.12.2, the package manager is apk.
For the recommended RTL8153 adapters:
opkg update
opkg install kmod-usb-net-rtl8152
reboot
After reboot, verify that the adapter appears as a network interface:
ip link show
lsusb
If you are using an AX88179-based adapter instead, install the matching ASIX driver package rather than kmod-usb-net-rtl8152.
Tested AX88179 Install On OpenWrt 25.12.2
The USB adapter used during testing identified as:
dmesg | grep -i -E "usb|ax88179|asix"
Relevant output:
Product: AX88179
Manufacturer: ASIX Elec. Corp.
Because the Pi was connected directly to a laptop on eth0 and had no upstream route yet, the driver packages were downloaded on the laptop and copied to the Pi manually.
On the Pi, verify the OS release and package manager first:
cat /etc/openwrt_release
which apk
On the laptop, download the required packages for OpenWrt 25.12.2 / kernel 6.12.74:
mkdir -p downloads
curl -L -o downloads/kmod-net-selftests-6.12.74-r1.apk "https://downloads.openwrt.org/releases/25.12.2/targets/bcm27xx/bcm2712/kmods/6.12.74-1-c48576c95291bf2086b1569f64c9c7f0/kmod-net-selftests-6.12.74-r1.apk"
curl -L -o downloads/kmod-phylink-6.12.74-r1.apk "https://downloads.openwrt.org/releases/25.12.2/targets/bcm27xx/bcm2712/kmods/6.12.74-1-c48576c95291bf2086b1569f64c9c7f0/kmod-phylink-6.12.74-r1.apk"
curl -L -o downloads/kmod-phy-ax88796b-6.12.74-r1.apk "https://downloads.openwrt.org/releases/25.12.2/targets/bcm27xx/bcm2712/kmods/6.12.74-1-c48576c95291bf2086b1569f64c9c7f0/kmod-phy-ax88796b-6.12.74-r1.apk"
curl -L -o downloads/kmod-usb-net-asix-6.12.74-r1.apk "https://downloads.openwrt.org/releases/25.12.2/targets/bcm27xx/bcm2712/kmods/6.12.74-1-c48576c95291bf2086b1569f64c9c7f0/kmod-usb-net-asix-6.12.74-r1.apk"
curl -L -o downloads/kmod-usb-net-asix-ax88179-6.12.74-r1.apk "https://downloads.openwrt.org/releases/25.12.2/targets/bcm27xx/bcm2712/kmods/6.12.74-1-c48576c95291bf2086b1569f64c9c7f0/kmod-usb-net-asix-ax88179-6.12.74-r1.apk"
Copy them to the Pi over SSH without scp:
dd if=downloads/kmod-net-selftests-6.12.74-r1.apk | ssh root@192.168.1.1 'dd of=/tmp/kmod-net-selftests-6.12.74-r1.apk'
dd if=downloads/kmod-phylink-6.12.74-r1.apk | ssh root@192.168.1.1 'dd of=/tmp/kmod-phylink-6.12.74-r1.apk'
dd if=downloads/kmod-phy-ax88796b-6.12.74-r1.apk | ssh root@192.168.1.1 'dd of=/tmp/kmod-phy-ax88796b-6.12.74-r1.apk'
dd if=downloads/kmod-usb-net-asix-6.12.74-r1.apk | ssh root@192.168.1.1 'dd of=/tmp/kmod-usb-net-asix-6.12.74-r1.apk'
dd if=downloads/kmod-usb-net-asix-ax88179-6.12.74-r1.apk | ssh root@192.168.1.1 'dd of=/tmp/kmod-usb-net-asix-ax88179-6.12.74-r1.apk'
Install them locally on the Pi:
apk add --allow-untrusted \
/tmp/kmod-net-selftests-6.12.74-r1.apk \
/tmp/kmod-phylink-6.12.74-r1.apk \
/tmp/kmod-phy-ax88796b-6.12.74-r1.apk \
/tmp/kmod-usb-net-asix-6.12.74-r1.apk \
/tmp/kmod-usb-net-asix-ax88179-6.12.74-r1.apk
Verify that the adapter appears as eth1:
ip link show
dmesg | grep -i -E "asix|ax88179|eth1|usb"
Expected kernel message:
ax88179_178a ... eth1: register 'ax88179_178a' ... ASIX AX88179 USB 3.0 Gigabit Ethernet
If you factory-reset the Pi or reflash OpenWrt, these packages do not persist and must be installed again.
Identify Interfaces
Via SSH (ssh root@192.168.1.1):
# List interfaces
ip link show
# Usually: eth0 = built-in, eth1 = USB adapter
# Verify:
dmesg | grep -i eth
WAN Interface (eth0 → facing ISP)
This is the upstream internet port. After setup is finished, plug the Pi’s built-in Ethernet port into the existing internet-connected router or modem-router.
Network → Interfaces → Add new interface:
| Field | Value |
|---|---|
| Name | wan |
| Protocol | DHCP client |
| Device | eth0 |
| Firewall zone | wan |
If upstream DHCP does not respond, configure a static WAN address instead. Tested fallback configuration:
uci set network.wan.proto='static'
uci set network.wan.ipaddr='192.168.2.250'
uci set network.wan.netmask='255.255.255.0'
uci set network.wan.gateway='192.168.2.1'
uci del network.wan.dns 2>/dev/null || true
uci add_list network.wan.dns='1.1.1.1'
uci add_list network.wan.dns='8.8.8.8'
uci commit network
/etc/init.d/network restart
Verify WAN connectivity:
ip addr show eth0
ip route show
ping -c 3 192.168.2.1
ping -c 3 1.1.1.1
nslookup openwrt.org 127.0.0.1
Tested UCI Cutover (eth0 -> wan, eth1 -> lan)
The tested command sequence to move the built-in port to wan and the USB adapter to lan was:
uci batch <<'EOF'
set network.@device[0].ports='eth1'
set network.lan.device='br-lan'
set network.lan.ipaddr='192.168.3.1/24'
set network.lan.proto='static'
set network.wan=interface
set network.wan.device='eth0'
set network.wan.proto='dhcp'
set firewall.@zone[1].network='wan'
commit network
commit firewall
EOF
/etc/init.d/network restart
/etc/init.d/firewall restart
Important: As soon as this is applied, management access moves from eth0 to the USB adapter on eth1. If your laptop is still plugged into eth0, you will immediately lose access to 192.168.3.1 until you move the cable to the USB adapter port.
LAN Interface (eth1 → internal network)
This is the local network port. Plug the USB Ethernet adapter into the switch, access point, or client side network that should be routed through Tailscale.
The default lan interface is on the br-lan bridge. Modify it:
If you need to move an already-running OpenWrt router from 192.168.1.1 to a non-conflicting LAN subnet before reassigning the LAN device, use a different private subnet such as 192.168.3.1:
uci set network.lan.ipaddr="192.168.3.1/24"
uci commit network
/etc/init.d/network reload
After the reload, reconnect to the router on 192.168.3.1.
Network → Interfaces → LAN → Edit:
| Field | Value |
|---|---|
| Protocol | Static address |
| IPv4 address | 192.168.3.1 |
| Netmask | 255.255.255.0 |
| Device | eth1 (USB adapter) |
| DHCP | Enabled (default range is fine) |
| Firewall zone | lan |
Important: If your upstream router already uses
192.168.2.0/24, assign a different subnet to the Pi LAN such as192.168.3.0/24, otherwise there will be a conflict.
Firewall
Network → Firewall: The default rules are fine:
lan → wan: ACCEPT (LAN clients can reach the internet)wan → lan: REJECT (nothing comes in from outside)- Masquerading (NAT): ON for the
wanzone
Save: Save & Apply.
Test
A device connected to the LAN port should receive a 192.168.3.x address via DHCP and be able to reach the internet through the upstream router.
# From the Pi:
ping -c 3 1.1.1.1
# From a LAN client:
ping -c 3 8.8.8.8
3. Installing Tailscale
Package Installation
On OpenWrt releases that still use opkg:
opkg update
opkg install tailscale
On OpenWrt 25.12.2, the tested installation used apk and required kmod-tun.
If the Pi does not yet have working internet access, download the packages on another machine:
curl -L -o downloads/kmod-tun-6.12.74-r1.apk "https://downloads.openwrt.org/releases/25.12.2/targets/bcm27xx/bcm2712/kmods/6.12.74-1-c48576c95291bf2086b1569f64c9c7f0/kmod-tun-6.12.74-r1.apk"
curl -L -o downloads/tailscale-1.94.1-r1.apk "https://downloads.openwrt.org/releases/25.12.2/packages/aarch64_cortex-a76/packages/tailscale-1.94.1-r1.apk"
dd if=downloads/kmod-tun-6.12.74-r1.apk | ssh root@192.168.1.1 'dd of=/tmp/kmod-tun-6.12.74-r1.apk'
dd if=downloads/tailscale-1.94.1-r1.apk | ssh root@192.168.1.1 'dd of=/tmp/tailscale-1.94.1-r1.apk'
Then install locally on the Pi:
apk add --allow-untrusted /tmp/kmod-tun-6.12.74-r1.apk /tmp/tailscale-1.94.1-r1.apk
Note: The Tailscale package takes ~22 MB of space. No issue with a 16GB SD card.
Start and Log In
# Enable and start the service
/etc/init.d/tailscale enable
/etc/init.d/tailscale start
# Log in
tailscale up --accept-routes
Tested interactive install/login flow on the Pi:
apk add tailscale
tailscale login
After login, verify the assigned Tailscale IPv4 address:
tailscale status
tailscale ip -4
ip addr show tailscale0
Verify the package install and daemon state:
tailscale version
/etc/init.d/tailscale status
ip link show tailscale0
During tested offline installation, the service started successfully, but tailscale up could not complete until the Pi had a working WAN route and DNS resolution.
If you factory-reset the Pi, tailscale and kmod-tun are removed and must be reinstalled.
This gives you a link — open it in your browser and log in with your Tailscale account. The Pi will appear in the admin console: https://login.tailscale.com/admin/machines
Firewall Zone for Tailscale
Network → Firewall → Add zone:
| Field | Value |
|---|---|
| Name | tailscale |
| Input | ACCEPT |
| Output | ACCEPT |
| Forward | ACCEPT |
| Covered devices | tailscale0 |
| Allow forward from | lan |
| Allow forward to | lan, wan |
Save & Apply.
Tested UCI firewall setup for router access over Tailscale:
uci batch <<'EOF'
add firewall zone
set firewall.@zone[-1].name='tailscale'
set firewall.@zone[-1].device='tailscale0'
set firewall.@zone[-1].input='ACCEPT'
set firewall.@zone[-1].output='ACCEPT'
set firewall.@zone[-1].forward='ACCEPT'
add firewall forwarding
set firewall.@forwarding[-1].src='tailscale'
set firewall.@forwarding[-1].dest='lan'
add firewall forwarding
set firewall.@forwarding[-1].src='lan'
set firewall.@forwarding[-1].dest='tailscale'
commit firewall
EOF
/etc/init.d/firewall restart
tailscale0 does not need to be added as a normal OpenWrt network interface. Tailscale creates and manages that device itself. In OpenWrt, the required integration point is the firewall zone for the tailscale0 device.
If LAN clients should egress through a Tailscale exit node, enable masquerading on the tailscale zone as well:
uci set firewall.@zone[-1].masq='1'
uci commit firewall
/etc/init.d/firewall restart
After this, the router itself should be reachable over its Tailscale IP for both SSH and LuCI.
Example tested access pattern:
ssh root@<TAILSCALE_IP>
LuCI over Tailscale:
http://<TAILSCALE_IP>
4. Connecting to a Remote Exit Node
Prerequisite: An Exit Node in Your Tailscale Network
You need another machine in your tailnet running as an exit node. This can be:
- A VPS (e.g., Hetzner, DigitalOcean) somewhere in the world
- Another home machine in a different country
- Any device running Tailscale
On that machine:
tailscale up --advertise-exit-node
Then in the Tailscale Admin Console, approve the exit node: Machine → “…” menu → Edit route settings → Use as exit node ✓
Point the Pi to the Exit Node
# List available exit nodes
tailscale exit-node list
# Connect (replace <HOSTNAME> with the exit node's name)
tailscale set --exit-node=<HOSTNAME> --exit-node-allow-lan-access=true
# Verify public egress from the Pi
wget -qO- https://ifconfig.me/ip
Manual checks that the exit-node routing is active:
# Show Tailscale status and current exit-node state
tailscale status
# Show detailed exit-node status
tailscale status --json | grep -E 'ExitNode|ExitNodeOption'
# Confirm that LAN access remains enabled while using the exit node
tailscale debug prefs | grep ExitNodeAllowLANAccess
# Show the Tailscale policy-routing table
ip route show table all | grep tailscale0
# Show the router's public IP as seen from the internet
wget -qO- https://ifconfig.me/ip
What to look for:
tailscale statusshould show the chosen exit node as activetailscale debug prefsshould showExitNodeAllowLANAccess: trueip route show table allshould includedefault dev tailscale0 table 52- the public IP returned by
ifconfig.meshould match the exit node’s egress IP, not the upstream ISP IP
Why Does This Work for LAN Clients Too?
LAN clients are NATed through the Pi. When the Pi’s traffic goes through the exit node, all LAN client traffic automatically goes there too — no need to install Tailscale on the clients.
Disable Exit Node
# Revert to normal routing (exit via ISP)
tailscale set --exit-node=
5. Auto-Start and Optimization
CPU Performance Governor
A router is under constant load; the powersave governor slows down crypto operations:
# Set immediately
echo performance > /sys/devices/system/cpu/cpufreq/policy0/scaling_governor
# Set at boot — add to /etc/rc.local (before exit 0):
echo performance > /sys/devices/system/cpu/cpufreq/policy0/scaling_governor
Tailscale Exit Node Setting After Reboot
tailscale set --exit-node=<HOSTNAME> persists across reboots — Tailscale stores the setting in its state file (/var/lib/tailscale/), and the daemon applies it automatically on restart. No extra boot script needed.
Watchdog
If the active exit node stops working, a watchdog script can switch to a fallback exit node.
Create /etc/tailscale-watchdog.sh:
cat << 'EOF' > /etc/tailscale-watchdog.sh
#!/bin/sh
PRIMARY_EXIT_NODE="<PRIMARY_HOSTNAME>"
FALLBACK_EXIT_NODE="<FALLBACK_HOSTNAME>"
TEST_URL="https://ifconfig.me/ip"
set_exit_node() {
tailscale set --exit-node="$1" --exit-node-allow-lan-access=true
}
if wget -qO- "$TEST_URL" >/dev/null 2>&1; then
exit 0
fi
logger -t tailscale-watchdog "Primary path failed, switching to fallback exit node"
set_exit_node "$FALLBACK_EXIT_NODE"
sleep 10
if wget -qO- "$TEST_URL" >/dev/null 2>&1; then
exit 0
fi
logger -t tailscale-watchdog "Fallback exit node failed, retrying primary exit node"
set_exit_node "$PRIMARY_EXIT_NODE"
EOF
chmod +x /etc/tailscale-watchdog.sh
Test it manually:
/etc/tailscale-watchdog.sh
logread | grep tailscale-watchdog
tailscale status
tailscale debug prefs | grep ExitNodeAllowLANAccess
Add it to cron (every 5 minutes):
echo '*/5 * * * * /etc/tailscale-watchdog.sh' >> /etc/crontabs/root
/etc/init.d/cron restart
Notes:
- The script checks whether the router can still reach the internet before switching exit nodes.
--exit-node-allow-lan-access=trueis required so LAN clients keep working after the failover.- Replace
<PRIMARY_HOSTNAME>and<FALLBACK_HOSTNAME>with the hostnames shown bytailscale exit-node list.
Performance
| Measurement | Expected Value |
|---|---|
| WireGuard (Tailscale) throughput | ~800–900 Mbps |
| One-direction max (CPU-limited) | ~1 Gbps |
| Latency overhead | +1–3 ms (within LAN) |
The Pi 5’s Cortex-A76 cores have ARMv8 crypto extensions, which provide hardware acceleration for WireGuard. On a 1 Gbps connection, the CPU is the bottleneck, not the NIC.
Troubleshooting
USB Adapter Not Showing Up
dmesg | grep -i eth
lsusb
ip link show
If the adapter uses the recommended RTL8153 chipset, install the driver and reboot:
opkg update
opkg install kmod-usb-net-rtl8152
reboot
If eth1 is still not visible, try plugging in the adapter with the Pi powered off, then boot.
Tailscale Won’t Connect
tailscale status
# If "Logged out":
tailscale up --accept-routes
LAN Clients Not Getting IP Addresses
# Is the DHCP server running?
/etc/init.d/dnsmasq status
# Check logs:
logread | grep dhcp
No Internet via Exit Node
# Exit node status
tailscale exit-node list
# Ping test directly from the Pi
ping -c 3 1.1.1.1
# If it fails, temporarily disable:
tailscale set --exit-node=
# If it works without exit node, the issue is with the exit node