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.

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.

apk update
apk add tailscale

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: NetworkInterfacesAdd new interface

  • Name: tailscale
  • Protocol: Unmanaged
  • Device: tailscale0

Once the new tailscale interface has been created, set up the DHCP server for masquerading: NetworkInterfacestailscaleEditDHCP Settings

  • Press the Set up DHCP Server button
  • Ignore interface: on (disable DHCP for this interface (dnsmasq only))

Verify that the interface has had your Tailscale address assigned:

ip address show tailscale0

Create a new firewall zone via LuCI: NetworkFirewallZonesAdd

  • Name: tailscale
  • Input: ACCEPT
  • Output: ACCEPT
  • Forward: any (only matters if you have multiple interfaces in the tailscale firewall zone)
  • Masquerading: on (optionally restrict this to relevant source subnets in advanced options, e.g. corresponding to LAN)
  • MSS Clamping: on
  • Covered networks: tailscale
  • Allow forward to destination zones: Select your LAN (and/or other internal zones; and/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/exit nodes)

Click Save & Apply

Tailscale supports a name resolution service for devices on your tailnet - they call this MagicDNS. On Tailscale-connected machines the Tailscale daemon can optionally act as a DNS resolver for the tailnet (*.ts.net) via the --accept-dns option. The daemon normally properly integrates with a variety of Linux DNS managers, and failing that will overwrite /etc/resolv.conf. This means that when --accept-dns is enabled on an OpenWRT device, /etc/resolv.conf will use the Tailscale MagicDNS and not dnsmasq. That may or may not work well depending no how you have configured MagicDNS. (Aside: the Tailscale DNS resolver has been observed to occasionally misbehave and return SERVFAIL for all queries.)

It is simpler and more reliable to not use --accept-dns, and instead add the Tailscale resolver as a server in dnsmasq:

tailscale set --accept-dns=false
/etc/init.d/tailscale restart

uci add_list dhcp.@dnsmasq[0].server='/ts.net/100.100.100.100'
uci commit dhcp
/etc/init.d/dnsmasq restart

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. Since Masquarading is enabled in the firewall zone settings, tailscale SNAT should be disabled.

tailscale up --advertise-routes=10.0.0.0/24,10.0.1.0/24 --snat-subnet-routes=false

Adding an additional --accept-routes option will manage adding static routes for other subnet routes within your tailnet. 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 add on the additional option of --advertise-exit-node node here to offer also a WAN gateway to your tailscale network.

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.

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.

0. Verify that packet forwarding is disabled by default (this is the OpenWRT default): NetworkFirewallGeneral Settings → Forward: reject or drop

1. Add the interface and firewall zone as per the Initial Setup section

2. Start tailscale with tailscale up --exit-node=MY-EXIT-NODE --exit-node-allow-lan-access=true

3. Disable LAN-to-WAN forwarding: NetworkFirewallZoneslanEdit

  • Allow forward to destination zones: Ensure that your WAN zone is unselected.

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.

NB: To force most traffic through tailscale, but have some go elsewhere, you should be able to use tailscale together with pbr_app as long as you follow the tailscale-specific mentioned in its wiki page. You //should even be able set up pbr_app to achieve a similar outcome as the selective tunneling described below, but it's not necessarily intuitive. //

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 and configuring a new tailscale firewall zone. I also disabled “Use default gateway” in the interface Advanced Settings.

2. Start tailscale with tailscale up --exit-node=xxx --exit-node-allow-lan-access=true.

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.

3. In the SystemSoftware menu, install coreutils-sleep.

4. Using your preferred method, create a shell script, e.g. /root/ts-strip-default-route.sh, with the following content:

#!/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

5. Create a new service, e.g. /etc/init.d/ts-strip-default-route, with the following content:

#!/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
}

6. Make sure both of these are set as executable (-x) by/for root. Enable and Start the new service under SystemStartup.

7. Finally, configure your custom routing. Either use pbr_app (out of the box), or static routes under NetworkRouting, e.g.:

Interface: tailscale
Route type: unicast
Target: x.x.x.x/x
Gateway: 0.0.0.0

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.

You can view your current settings with: ethtool -k eth1

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

apk update
apk add ethtool

2. Apply the changes:

Substitute eth1 below for your WAN interface.

ethtool -K eth1 rx-gro-list off
ethtool -K eth1 rx-udp-gro-forwarding on

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 or netstat -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

If your OpenWrt device is storage constrained, the process below will produce a small package (around ~3MB) which can be used to install Tailscale on devices which would otherwise not be able to install Tailscale.

The official Tailscale small binary build guide doesn't build cleanly for the current v1.98.3 tag. The most OpenWrt-aligned guide is this one.

How does it work

Most of the space savings can be credited to the fact that Tailscale's build system is designed for customization using featuretags.

Click to display ⇲

Click to hide ⇱

                 ace: Alternate Connectivity Endpoints
                acme: ACME TLS certificate management
   advertiseexitnode: Run an exit node
     advertiseroutes: Advertise routes for other nodes to use
       appconnectors: App Connectors support
                 aws: AWS integration
          bakedroots: Embed CA (LetsEncrypt) x509 roots to use as fallback
                bird: Bird BGP integration
                 c2n: Control-to-node (C2N) support
         cachenetmap: Cache the netmap on disk between runs
       captiveportal: Captive portal detection
             capture: Packet capture
                 cli: embed the CLI into the tailscaled binary
         cliconndiag: CLI connection error diagnostics
       clientmetrics: Client metrics support
        clientupdate: Client auto-update support
               cloud: detect cloud environment to learn instances IPs and DNS servers
           colorable: Colorized terminal output
          completion: CLI shell completion
  completion_scripts: embed CLI shell completion scripts
              conn25: Route traffic for configured domains through connector devices
                dbus: Linux DBus support
               debug: various debug support, for things that don't have or need their own more specific feature
       debugeventbus: eventbus debug support
     debugportmapper: portmapper debug support
    desktop_sessions: Desktop sessions support
                 dns: MagicDNS and system DNS configuration support
              doctor: Diagnose possible issues with Tailscale and its host environment
               drive: Tailscale Drive (file server) support
                 gro: Generic Receive Offload support (performance)
              health: Health checking support
          hujsonconf: HuJSON config file support
  identityfederation: Auth key generation via identity federation support
              ipnbus: IPN notification bus (watch-ipn-bus) support, used by GUIs, debugging, and nicer 'tailscale up' support
            iptables: Linux iptables support
                kube: Kubernetes integration
           linkspeed: Set link speed on TUN device for better OS integration (Linux only)
       linuxdnsfight: Linux support for detecting DNS fights (inotify watching of /etc/resolv.conf)
      listenrawdisco: Use raw sockets for more robust disco (NAT traversal) message receiving (Linux only)
             logtail: upload logs to log.tailscale.com (debug logs for bug reports and also by network flow logs if enabled)
              netlog: Network flow logging support
            netstack: gVisor netstack (userspace networking) support
      networkmanager: Linux NetworkManager integration
            oauthkey: OAuth secret-to-authkey resolution support
            osrouter: Configure the operating system's network stack, IPs, and routing tables
       outboundproxy: Support running an outbound localhost HTTP/SOCK5 proxy support that sends traffic over Tailscale
       peerapiclient: PeerAPI client support
       peerapiserver: PeerAPI server support
            portlist: Optionally advertise listening service ports
          portmapper: NAT-PMP/PCP/UPnP port mapping support
             posture: Device posture checking support
             qrcodes: QR codes in tailscale CLI
         relayserver: Relay server
            resolved: Linux systemd-resolved integration
            sdnotify: systemd notification support
               serve: Serve and Funnel support
                 ssh: Tailscale SSH support
            synology: Synology NAS integration (applies to Linux builds only)
           syspolicy: System policy configuration (MDM) support
             systray: Linux system tray
            taildrop: Taildrop (file sending) support
         tailnetlock: Tailnet Lock support
                 tap: Experimental Layer 2 (ethernet) support
                 tpm: TPM support
         tundevstats: Poll TUN device statistics (Linux only)
  unixsocketidentity: differentiate between users accessing the LocalAPI over unix sockets (if omitted, all users have full access)
         useexitnode: Use exit nodes
            useproxy: Support using system proxies as specified by env vars or the system configuration to reach Tailscale servers.
         usermetrics: Usermetrics (documented, stable) metrics support
           useroutes: Use routes advertised by other nodes
           wakeonlan: Wake-on-LAN support
          webbrowser: Open URLs in the user's web browser
           webclient: Web client support

The featuretags specified in official guide build a binary that will not start on OpenWrt. The featuretags specified in OpenWrt's official package produce a 30MB binary. This is the trade off.

Through trial and error, I've settled on a minimum set of featuretags that produce a binary small enough to fit on my device but still support the features I use, namely: osrouter,cli,unixsocketidentity,netstack,ipnbus,health,gro,listenrawdisco,portlist,advertiseroutes,useroutes.

But your mileage WILL vary and when you encounter a panic() in your logs from tailscaled or some other unexpected runtime behavior, reference ./tool/go run ./cmd/featuretags --list in your tailscale checkout to see which tags you may need to compile-in.

The Tailscale build system also offers a “multicall binary” build target which reduces the storage footprint in half. Current versions of the OpenWrt tailscale package use this feature but older versions did not.

Additional savings may come from the fact that this process strips symbols at buildtime with -ldflags=“-s -w” (instead of relying on buildroot's) and external linking tends to produce a larger binary than Go's internal linker.

Keep in mind that stripping removes ELF symbols only. Stripped Go binaries still produce readable stack traces with function names and line numbers as these live in the PC-to-line table which cannot be removed.

Lastly, if you don't mind the additional startup time, you can further reduce the size of the package by running the resulting binary through a packer, like UPX. As of 1.98.3, this will result in an additional ~90% reduction in size (from ~15MB to ~3MB)!

All of that is to say, that until there is a separate “tailscale-tiny” package, your best bet is to compile Tailscale yourself if you're storage constrained.

Instructions

To keep your installation sane and consistent, the instructions below repack the upstream OpenWrt package with your patched version and we install from a local file-based repo in /tmp. We use a _p1-r1 suffix which wins over upstream at the same base version and install by specifying apk add version=... which pins the package at a specific version and can be verified in /etc/apk/world. This will ensure proper accounting of things like installed-size and ensures apk upgrade doesn't break the system by overwriting with an upstream version.

What follows is a brief outline of the process:

On dev machine:

  1. Build apk-tool
  2. Build and optimize tailscale binary
  3. Unpack the current official OpenWrt tailscale package
  4. Repack the package with the optimized binary instead of distributed binary
  5. Generate repo metadata

On your OpenWrt device:

  1. Add custom repo
  2. Install the package

The steps below were performed on Debian Linux 13 (Trixie) for a ''mips_24kc'' target.

# convenience variables
export release=25.12.5
export arch=mips_24kc
export version=1.98.3-r1
export package=tailscale-${version}.apk
 
export patched_version=${version%%-r*}_p1-r1 # patch level
export patched_package=tailscale-${patched_version}.apk
 
# dependencies
sudo apt install upx-ucl jq fakeroot meson ninja-build cmake lua5.3 pkg-config libssl-dev zlib1g-dev libzstd-dev scdoc
mkdir scripts root localrepo
 
# build apk-tools. No debian package unfortunately.
git clone https://github.com/alpinelinux/apk-tools.git
pushd apk-tools
meson setup -Dpython=disabled build
ninja -C build src/apk
export PATH="${PATH}:${PWD}/build/src"
popd
 
# build tailscaled
git clone https://github.com/tailscale/tailscale.git
pushd tailscale
git checkout tags/v1.98.3 -b v1.98.3
eval $(TS_USE_TOOLCHAIN=1 ./build_dist.sh shellvars)
env GOOS=linux GOARCH=mips GOMIPS=softfloat ./tool/go build -o tailscale.multicall -tags "$(./tool/go run ./cmd/featuretags --min --add=osrouter,cli,unixsocketidentity,netstack,ipnbus,health,gro,listenrawdisco,portlist,advertiseroutes,useroutes)" -trimpath -ldflags="-s -w -X tailscale.com/version.longStamp=${VERSION_LONG} -X tailscale.com/version.shortStamp=${VERSION_SHORT}" ./cmd/tailscaled
 
# pack tailscale.multicall
upx --lzma --best tailscale.multicall
popd
 
# download .apk
wget https://downloads.openwrt.org/releases/${release}/packages/${arch}/packages/${package}
 
# dump scripts into scriptdir
apk adbdump --format json tailscale-${version}.apk > metadata.json
for script in $(jq -r '.scripts | keys[]' metadata.json); do jq -re --arg s "${script}" '.scripts[$s]' metadata.json > "scripts/${script}"; done
 
# parse metadata
export description=$(jq -r '.info.description | sub("\n+$";"")' metadata.json)
export license=$(jq -r '.info.license' metadata.json)
export origin=$(jq -r '.info.origin' metadata.json)
export url=$(jq -r '.info.url' metadata.json)
export maintainer=$(jq -r '.info.maintainer' metadata.json)
export depends=$(jq -r '.info.depends | join(" ")' metadata.json)
export provides=$(jq -r '.info.provides | join(" ")' metadata.json | sed "s/${version}/${patched_version}/g")
 
# repack
fakeroot sh << EOF
apk extract --allow-untrusted --destination root "${package}"
install -m 0755 tailscale/tailscale.multicall root/usr/sbin/tailscaled
apk mkpkg \
  --info "name:tailscale" \
  --info "version:${patched_version}" \
  --info "arch:${arch}" \
  --info "description:${description}" \
  --info "license:${license}" \
  --info "origin:${origin}" \
  --info "url:${url}" \
  --info "maintainer:${maintainer}" \
  --info "depends:${depends}" \
  --info "provides:${provides}" \
  --script post-install:scripts/post-install \
  --script pre-deinstall:scripts/pre-deinstall \
  --script post-upgrade:scripts/post-upgrade \
  --files root \
  --output "localrepo/${patched_package}"
EOF
 
# verify
diff -u <(apk adbdump ${package}) <(apk adbdump localrepo/${patched_package})
 
# create apk index
apk mkndx --allow-untrusted --output localrepo/packages.adb "localrepo/${patched_package}"
 
# copy localrepo to your device
scp -O -r localrepo root@device:/tmp

On OpenWrt device:

# convenience variables
export version=1.98.3-r1
export patched_version=${version%%-r*}_p1-r1 # patch level
export patched_package=tailscale-${patched_version}.apk
 
# add local repo source, update index, and install package
echo 'file:///tmp/localrepo/packages.adb' > /etc/apk/repositories.d/00-local.list
apk update --allow-untrusted
apk add --allow-untrusted "tailscale=${patched_version}"
 
# update sysupgrade.conf
echo "/usr/sbin/tailscaled" >> /etc/sysupgrade.conf
echo "/etc/apk/repositories.d/00-local.list" >> /etc/sysupgrade.conf

You're now ready to continue to initial_setup.

Footnotes

  • You should reset your git checkout of Tailscale to a tagged stable release to ensure compatibility with the OpenWrt package you're repacking. This is best practice and makes troubleshooting bugs easier.
  • UPX 3.96 produces broken mips binaries, use the latest version (UPX 5.2.1 as of this writing). UPX can handle all executable formats, so you don't need to run it under the target architecture.
  • 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 using a different compression level with UPX
  • 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.
  • If you need a very specific feature that is excluded by the --min flag, you can get a full list with descriptions using: ./tool/go run ./cmd/featuretags --list.

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.

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

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.

  • Last modified: 2026/09/11 10:19
  • by benl