Tailscale
Tailscale creates a virtual network between hosts. It can be used as a simple mechanism to allow remote administration without port forwarding or even be configured to allow peers in your virtual network to proxy traffic through connected devices as an ad-hoc vpn.
You can read more about how Tailscale works here.
Installation
Depending on your OpenWrt version the package included may be outdated and missing security updates. You can find instructions on how to update to the latest Tailscale package via your Tailscale Admin Console page.
opkg update opkg install tailscale
Initial Setup
After installing Tailscale, run the command below and finish device registration by pasting the given link into a web browser and authenticating via a supported method:
tailscale up
Once registered, device connectivity can be seen by using the “status” command:
tailscale status
Additional configuration may be necessary to communicate with other machines in your Tailnet depending on your default forwarding rules. The following instructions can be used to add a new unmanaged interface and firewall zone so that you can classify and apply forwarding rules to Tailscale traffic.
Create a new unmanaged interface via LuCI: Network → Interfaces → Add new interface
- Name: tailscale
- Protocol: Unmanaged
- Device: tailscale0
Verify that the interface has had your Tailscale address assigned:
ip address show tailscale0
Create a new firewall zone via LuCI: Network → Firewall → Zones → Add
- Name: tailscale
- Input: ACCEPT (default)
- Output: ACCEPT (default)
- Forward: ACCEPT
- Masquerading: on
- MSS Clamping: on
- Covered networks: tailscale
- Allow forward to destination zones: Select your LAN (and/or other internal zones or WAN if you plan on using this device as an exit node)
- Allow forward from source zones: Select your LAN (and/or other internal zones or leave it blank if you do not want to route LAN traffic to other tailscale hosts)
Click Save & Apply
How to setup a Subnet Router/Exit Node
In order to get tailscale to cooperate well with LuCI, you will need to create a new managed interface and firewall zone for tailscale.
1. Add the interface and firewall zone as per the Initial Setup section
2. Restart tailscale
and add the routes you want to advertise to peers using the --advertise-routes
option with a comma separated list of network addresses and CIDRs. The example below is advertising 10.0.0.0/24
and 10.0.1.0/24
yours are likely to be different. The --accept-routes
option will manage adding static routes for other subnet routes within your tailnet.
tailscale up --advertise-routes=10.0.0.0/24,10.0.1.0/24 --accept-routes
If configuring multiple subnet routers which advertise the same subnets for high-availability, do not include the --accept-routes
option as this can cause routing issues and/or loss of connectivity on peer subnet routers.
You can also also use --advertise-exit-node
node here to offer both subnet routes and a WAN gateway to your tailscale network:
tailscale up --advertise-routes=10.0.0.0/24,10.0.1.0/24 --accept-routes --advertise-exit-node
3. Open the Machines page in the Tailscale admin interface. Once you've found the machine from the ellipsis icon menu, open the
Edit route settings..
panel, and approve exported routes and or enable the Use as exit node
option.
4. Devices on either subnet should be able to route traffic over the VPN. If you've configured this device to be an exit node, it should now be selectable from your tailscale apps as an Exit Node
. You can test connectivity with tools like ping
or traceroute
.
Force LAN traffic to route through Exit Node
To use the device as a VPN gateway, configure Tailscale to use an exit node. This will route all LAN traffic to go through your exit node only.
tailscale up --exit-node=MY-EXIT-NODE --exit-node-allow-lan-access=true
1. Add the interface and firewall zone as per the Initial Setup section
2. Disable packet forwarding by default: Network → Firewall → General Settings
- Forward: reject
3. Disable LAN-to-WAN forwarding: Network → Firewall → Zones → lan → Edit
- Allow forward to destination zones: Ensure that your WAN zone is unselected.
4. Enable Tailscale NAT: Network → Firewall → Zones → tailscale → Edit
- Allow forward to destination zones: Unspecified (all unchecked)
- Ensure that Masquerading is checked (this allows LAN traffic to appear as the router's tailscale address (similar to how WAN works)
5. Enable LAN-to-Tailscale forwarding Network → Firewall → Zones → lan → Edit
- Allow forward to destination zones: Ensure that your Tailscale zone is selected.
You can verify that all traffic is being forced over your remote Tailscale exit node by running traceroute. You should see your Tailscale exit node in the second or so hop. If your Tailscale connected OpenWrt router is sending all traffic to the exit node but not LAN clients:
1. Double check that your LAN firewall zone does not include the WAN for destination forwarding.
2. Make sure to have a specified DNS server in your LAN interface otherwise the LAN clients would not be able to connect the internet through Tailscale. If insure what to use, Cloudflare or Google Public DNS are reasonable choices.
3. You may have unexpected iptables or nftables stale rules. Reboot your OpenWrt device so you get a clean boot and application of rules.
Force only SOME traffic through tailscale exit note (i.e., Split VPN)
NB: To force most traffic through tailscale, but have some go elsewhere, you should be able to use tailscale together with PBR and the tailscale-specific configuration mentioned here: pbr_app
The other scenario of selective tunneling is unfortunately not supported easily out of the box, but here's how I implemented it with tailscale 1.80.3 and openwrt 24.10. Note that my configuration is unsuitable for accessing devices actually on your tailnet (which I didn't care about) - please update the section if you manage a configuration that achieves both.
1. Follow the Initial Setup instructions for adding an unmanaged interface for tailscale. I also disabled “Use default gateway” in the Advanced Settings.
2. Configure Firewall zone. You can probably set up a new zone based on the various instructions above, but I just added the tailscale interface to my existing WAN zone (obvious limitation for other use cases)
3. Start tailscale with tailscale up --exit-node=xxx --exit-node-allow-lan-access=true
.
4. At this point, all traffic will route through the exit node, since tailscale configures an associated ip route. The next steps are needed to override this.
5. Create a shell script, e.g. /root/ts-strip-default-route.sh
with the following:
#!/bin/sh # # Remove "default" routes that get added by tailscale with --exit-node # Use ip monitor to pause execution until changes are detected # Rely on procd for looping set -e sleep 0.005 >&2 echo "Tailscale Route Stripping in Progress" ip route del default dev tailscale0 table 52 2>/dev/null || true ip -6 route del default dev tailscale0 table 52 2>/dev/null || true FIFO=/tmp/tsmon.$$ trap 'kill "$MON_PID" 2>/dev/null || true; rm -f "$FIFO"' EXIT INT TERM mkfifo "$FIFO" ip monitor route dev tailscale0 > "$FIFO" & MON_PID=$! read -r <"$FIFO" kill "$MON_PID" 2>/dev/null || true
6. Create a new service under /etc/init.d
#!/bin/sh /etc/rc.common START=90 USE_PROCD=1 start_service() { procd_open_instance procd_set_param command /root/ts-strip-default-route.sh procd_set_param stdout 1 procd_set_param stderr 1 procd_set_param respawn 0 0 0 #restart immediately on exit procd_close_instance }
7. Make sure both of these are executable. Enable and Start the new service under System → Startup.
8. Finally, configure your custom routing.
- Either static routes under Network → Routing, e.g.:
Interface: tailscale Route type: unicast Target: x.x.x.x/x Gateway: 0.0.0.0
- Alternatively, use PBR. However, there currently is a potential bug with PBR potentially picking up the wrong gateway.
Troubleshooting
OpenWrt 22.03
OpenWrt 22.03 and later, use nftables (superseding iptables) as a backend to firewall4. Tailscale is unable to configure nftables automatically on the package included for 22.03 and this prevents the tailscale daemon from initializing properly and forwarding traffic.
A workaround for this issue has been applied to the master branch. If you're unable or unwilling to run an image built from the master branch, the following steps can be used as a manual workaround on 22.03.x Credit: aricade, csrutil, youngt2:
This fix is not required for OpenWRT 23.0.5
or later as the package has been fixed. If using OpenWrt 23+ you do NOT need to apply --netfliter-mode=off
.
When starting Tailscale, you must prevent iptables rules from being created with the --netfilter-mode=off
flag. This setting will be preserved in /etc/tailscale/tailscaled.state
for future boots.
tailscale up --netfilter-mode=off
Restart the daemon
service tailscale restart
Verify no Kernel errors occur:
tailscale status
Continue with initial_setup but keep in mind that you will need to add --netfilter-mode=off
for each invocation of tailscale in the guide.
tailscale 1.58-2.1 issue
For Tailscale versions before 1.58.2-1
the init script may need to be modified to force tailscale to assign an IP to the tailscale0
interface.
- Edit
/etc/init.d/tailscale
- After the last
procd_append_param
add:procd_append_param command --tun tailscale0
IPv6 Routing Issue
When using IPv6 with the exit node to WAN, default routes may be set up such that they are only routed for the LAN prefix:
# ip addr default from 2607:dead:beef::1 via fe80::ff:fecb:f5c3 dev eth1 metric 512 default from 2607:dead:beef::/64 via fe80::ff:fecb:f5c3 dev eth1 metric 512
You can workaround this by disabling IPv6 Source Routing (note that this may not be ideal if you have multiple upstream IPv6 connections): Eg: Network→Interfaces→(WAN6) Edit → Advanced Settings → IPv6 source routing (Uncheck) → Apply/Save.
Throughput improvements via transport layer offloading in OpenWrt 24.10
Tailscale version 1.54 or later used with OpenWrt 24.10 or later (which uses kernel 6.6) enables UDP throughput improvements via transport layer offloading.
Namely, tuning two features may show improved throughput:
rx-udp-gro-forwarding
: Enables UDP Generic Receive Offload (GRO) forwarding, which aggregates incoming UDP packets to reduce CPU overhead on receive.rx-gro-list
: If disabled (off), it prevents multiple flows from being aggregated simultaneously which simplifies flow handling and performance on some workloads.
These changes should be applied to your physical WAN interfaces which will actually be performing the UDP encapsulation of tailscale traffic
1. Install ethtool
opkg update opkg install ethtool
2. Apply the changes:
Substitute eth1
below for your WAN interface.
ethtool -K rx-gro-list off eth1 ethtool -K rx-udp-gro-forwarding on eth1
3. Test the changes before and after before committing them permanently with something similar to the following commands.
You want to verify:
- Packet aggregation is working as measured by reduced packets/sec on the wire with GRO enabled (verify with tools like:
ethtool -S <interface> | grep udp
ornetstat -su
) - CPU usage is reduced. Lower CPU usage on the receiver compared to same test with
rx-udp-gro-forwarding
turned off - High throughput is achieved near line rate (e.g., 1 Gbps, 10Gbps, etc) without packetloss.
You will need the iperf3
package installed for this
Receiver
iperf3 -s
Sender
iperf3 -c <remote_addr> -u -b 1G -l 1400 -t 10
If you're satisfied with the results and want it to persist across reboots.
4. Create /etc/config/ethtool
ether using uci or by creating the file manually. The following example will use uci
:
Substitute eth1
below for your WAN interface.
touch /etc/config/ethtool uci set ethtool.eth1=device uci set ethtool.eth1.rx_gro_list='off' uci set ethtool.eth1.rx_udp_gro_forwarding='on' uci commit
5. Create the following file in /etc/hotplug.d/iface/90-ethtool
:
#!/bin/sh # shellcheck disable=SC3043 # # Author: Josh Enders <josh.enders@gmail.com> # License: CC BY-NC 4.0 # https://gist.github.com/joshenders/1baa9de07c1b7af489f14c30d4667e40 [ "${ACTION}" = "ifup" ] || exit 0 # shellcheck source=/dev/null . /lib/functions.sh config_load ethtool log_crit() { logger -t "$0" -p crit "$1"; } log_info() { logger -t "$0" -p info "$1"; } apply_settings() { local config feature ifname option value ifname="$1" config=$(uci show ethtool."${ifname}" | sed -n "s/^ethtool.${ifname}\.\([^=]*\)=.*/\1/p") for option in ${config}; do config_get value "${ifname}" "${option}" feature=$(echo "${option}" | tr '_' '-') if [ -n "${value}" ]; then { ethtool -K "${ifname}" "${feature}" "${value}" \ && log_info "${feature} set to ${value} on ${ifname}"; } || log_crit "Failed to set ${feature} to ${value} on ${ifname}" else log_crit "Failed to set ${feature} to ${value} on ${ifname}" fi done } config_foreach apply_settings device
6. Append /etc/hotplug.d/iface/90-ethtool
to /etc/sysupgrade.conf
to preserve this file during upgrades.
echo '/etc/hotplug.d/iface/90-ethtool' >> /etc/sysupgrade.conf
Installation on storage constrained devices
Tailscale cannot be installed on devices with 16MB or less of flash memory because the package and its dependencies consume too much space. Until the day that there is a separate “tailscale-lite” build, your best bet is to compile (or cross-compile) it yourself from upstream sources and use the multicall binary build target.
To reduce the filesize further, you can strip debugging symbols and run the resulting binary through a packer, like upx. As of 1.56.1
, this will result in ~98% reduction in size (from ~33MB to ~5.2MB).
Instead of running the optimized binaries directly, it is recommend that you repack the tailscale.ipk
, and tailscaled.ipk
packages with smaller, optimized binaries. This will let you benefit from OpenWrt conventions like init scripts, opkg installation receipts, etc, keeping your installation sane and consistent while still being able to use the smaller binaries.
If your device has 16MB of flash or less, the tailscale build system offers a “multicall” binary target that can be used to share a single binary between both of the official OpenWrt tailscale
and tailscaled
packages and reduces the storage footprint by nearly half. What follows is a brief summary but more detailed, step-by-step copy/paste style instructions are also provided below.
- Unpack the current official OpenWrt
tailscale
andtailscaled
packages and “stub” out the large/usr/sbin/tailscale{d}
binaries contained within them with a small shell script that always returnstrue
. - Repack both packages
- Install both packages
- Manually copy the multicall binary to the device
- Replace the stubs installed previously with symlinks to the shared multicall binary
The official Tailscale small binary build guide is here:
Other useful links:
Tips:
- You should reset your git checkout of Tailscale to a tagged stable release to ensure compatibility with the OpenWrt package you're repacking. This also makes troubleshooting easier on yourself in the future and is best practice.
- upx 3.96 produces broken mips binaries, use the latest version. Upx can handle all executable formats, so you don't need to run it under the target architecture.
- You can shave off an additional MB or so with
strip --strip-all
. This strips even more than when usinggo build
ldflags alone. Look for thebinutils
package of your target architecture. For my MIPS target on Linux, that wasbinutils-mips-linux-gnu
, on macOS the same MacPorts package is calledmips-elf-binutils
.
- Be very careful when repacking your
.ipk
not to include leading paths. An absolute path in the root of the package will produce an unusable.ipk
.
- Don't forget to symlink
/usr/sbin/talescale.multicall
to/usr/sbin/tailscale
and/usr/sbin/tailscaled
.
- If installing on >= 22.03, don't forget to apply the work arounds listed earlier on this page.
- On slow devices, upx packed executables may appear to hang at first when you run them but this is normal; higher startup time for lower storage costs. If having trouble try compressing without
--best
- It's a good idea to check that your
tailscale.multicall
actually runs on your target architecture with a simple./tailscaled --version
on the target device before going to the trouble of repacking.
Shell history on macOS where I built tailscale from source for a big endian mips target:
sudo port install go mips-elf-binutils upx git clone https://github.com/tailscale/tailscale.git cd tailscale git checkout tags/v1.56.1 -b v1.56.1 env GOOS=linux GOARCH=mips GOMIPS=softfloat go build -o tailscale.multicall -tags ts_include_cli,ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube -trimpath -ldflags="-s -w" ./cmd/tailscaled /opt/local/bin/mips-elf-strip --strip-all tailscale.multicall upx --lzma --best tailscale.multicall # (v3.96 has deadlock bug)
The steps below were performed on Debian Linux. I repacked the OpenWrt .ipk
for the tailscale
and tailscaled
packages and replaced the large binaries with small stub scripts that always return true. I did this so that the pre/post scripts will run successfully and the opkg database at /usr/lib/opkg
will be consistent. The packages were installed manually and stub files deleted post installation. The multicall binary was then uploaded and symlinks created.
# convenience variables export package=tailscaled_1.56.1-1_mips_24kc.ipk export release=23.05.2 export arch=mips_24kc # download .ipk wget https://downloads.openwrt.org/releases/${release}/packages/${arch}/packages/${package} mkdir ${package%%.ipk} pushd ${package%%.ipk} tar -xvf ../${package} # data mkdir data pushd data tar -xvf ../data.tar.gz echo -e '#!/bin/sh\ntrue\n' > usr/sbin/${package%%_*} tar --numeric-owner --group=0 --owner=0 -czf ../data.tar.gz * popd size=$(du -sb data | awk '{ print $1 }') rm -rf data # control mkdir control pushd control tar -xvf ../control.tar.gz sed -i "s/^Installed-Size.*/Installed-Size: ${size}/g" control tar --numeric-owner --group=0 --owner=0 -czf ../control.tar.gz * popd rm -rf control # repack .ipk tar --numeric-owner --group=0 --owner=0 -cvzf ../${package} debian-binary data.tar.gz control.tar.gz popd
You should now have a repacked package named tailscaled_1.56.1-1_mips_24kc.ipk
. You will need to repeat this process for the tailscale
package.
Set a new package variable:
export package=tailscale_1.56.1-1_mips_24kc.ipk
Repeat the repack process above to create the repacked tailscale
package.
You should now have two repacked packages similar to tailscale_1.56.1-1_mips_24kc.ipk
and tailscaled_1.56.1-1_mips_24kc.ipk
. Copy these files to /tmp
on your device. Since sftp-server is not included with dropbear/OpenWrt, if you're scp from a relatively recent version of OpenSSH you'll need to either use scp -O
to use the legacy scp fallback mode, or do this from the OpenWrt device.
# convenience variables export version=1.56.1 export arch=mips_24kc # copy files scp remote:/tmp/tailscale.multicall /tmp scp remote:/tmp/tailscaled_${version}-1_${arch}.ipk /tmp scp remote:/tmp/tailscale_${version}-1_${arch}.ipk /tmp # install dependencies opkg install kmod-tun # install repacked packages opkg install /tmp/tailscaled_${version}-1_${arch}.ipk opkg install /tmp/tailscale_${version}-1_${arch}.ipk # remove stubs and link multicall binary rm /usr/sbin/tailscaled rm /usr/sbin/tailscale cd /usr/sbin cp /tmp/tailscale.multicall . ln -s tailscale.multicall tailscaled ln -s tailscale.multicall tailscale # verify tailscale --version tailscaled --version
You're now ready to continue to initial_setup.