1407 字
7 分钟
Resolving Routing Conflicts Between Tailscale and ZeroTier on OpenWrt/Linux (IPProto-44)

To ensure network reliability, I run both Tailscale and ZeroTier on my OpenWrt router. I primarily use Tailscale, but ZeroTier serves as an excellent failover that works wonders when Tailscale struggles with connectivity.

The Symptom: Mysterious Packet Drop Logs#

It all started with a seemingly minor anomaly. While checking the OpenWrt system logs, I noticed recurring error messages printed by tailscaled (the Tailscale daemon):

Fri Jul 11 21:10:43 2025 daemon.err tailscaled[3274]: Drop: IPProto-44{[fd7a:...] > [fd7a:...]} ... unknown-protocol-44
Fri Jul 11 21:10:53 2025 daemon.err tailscaled[3274]: [RATELIMIT] format("%s: %s %d %s\n%s") (8 dropped)

The logs indicated that Tailscale was dropping packets of a protocol it didn’t recognize (IPProto-44). Both the source and destination IPs belonged to devices within my internal network. Even stranger, this traffic appeared unrelated to Tailscale itself. After some troubleshooting, I identified the culprit: these “abnormal” packets were actually generated by the ZeroTier client attempting to communicate with its Moon relay servers.

Deep Dive: It’s the Routing Table#

Why was ZeroTier traffic leaking into Tailscale’s domain? The answer lies in the Linux routing rules.

By SSHing into the OpenWrt instance and running ip rule show, the root cause became clear:

Terminal window
root@ImmortalWrt:~# ip rule show
0: from all lookup local
...
5270: from all lookup 52
32766: from all lookup main
...

Linux routing rules are executed based on priority (prio), where a lower number indicates higher priority. This output reveals the following logic: The system first checks if the traffic is destined for the router itself (lookup local). If not, it moves to the rule with priority 5270. This rule states: “For all traffic, first check routing table 52.” Table 52 is a dedicated routing table created by Tailscale upon installation, which only contains routes to other Tailscale nodes. Only if no match is found in table 52 does the traffic proceed to the main routing table (where our standard internet gateway resides).

The conclusion is obvious: Tailscale implements an “aggressive” high-priority rule that intercepts all outgoing traffic from the router, including ZeroTier’s. When ZeroTier’s specific signaling packets are forced into the Tailscale network, Tailscale—unfamiliar with the protocol—simply drops them.

The Solution#

To resolve this, we shouldn’t modify Tailscale’s rules directly, as they are managed by the daemon. Instead, we should establish a “dedicated bypass” for ZeroTier with an even higher priority.

Our strategy: Create a new routing rule that explicitly tells the system: “Any traffic originating from a ZeroTier virtual interface must look up the main routing table directly,” ensuring this rule has a higher priority (a lower number) than Tailscale’s 5270.

Implementation on OpenWrt#

The default ip command in OpenWrt (provided by busybox) may lack certain features. Installing ip-full prevents various compatibility issues.

Terminal window
opkg update
opkg install ip-full
Step 2: Create the Service Script#

Create a new init script using vi or nano.

Terminal window
nano /etc/init.d/zerotier-policy-route

Paste the following script into the file.

WARNING

Please modify the ZT_IFACE variable to match your environment. You can find your ZeroTier interface name by running ifconfig | grep zt.

#!/bin/sh /etc/rc.common
#
# This script creates policy routing rules for ZeroTier to coexist with Tailscale.
# It prevents ZeroTier's traffic from being intercepted by Tailscale's high-priority rule.
# Version 3: Intelligently handles IPv4 and IPv6 addresses.
#
START=99
STOP=15
# --- CONFIGURATION ---
# The name of your ZeroTier virtual network interface. Find it with `ifconfig | grep zt`.
ZT_IFACE="ztypia4gba"
# The priority for the new routing rule. Must be lower (higher priority) than Tailscale's (usually 5270).
RULE_PRIO="5260"
# Tag for system log entries.
LOG_TAG="zerotier-policy-route"
# --- END CONFIGURATION ---
add_rules() {
# Get all global/unique IP addresses (v4 and v6), excluding link-local (fe80::) addresses.
IP_LIST=$(ip addr show dev "${ZT_IFACE}" | grep -E 'inet|inet6' | grep -v ' fe80::' | awk '{print $2}' | cut -d'/' -f1)
if [ -z "${IP_LIST}" ]; then
logger -t "${LOG_TAG}" "Warning: No usable IP addresses found on interface ${ZT_IFACE}."
return
fi
for ip in ${IP_LIST}; do
# Check if a rule for this IP already exists to avoid duplication.
if ! ip rule | grep -q "from ${ip} lookup main"; then
logger -t "${LOG_TAG}" "Adding rule for ${ip} with priority ${RULE_PRIO}."
# CRITICAL FIX: Use the '-6' flag for IPv6 addresses.
if echo "${ip}" | grep -q ":"; then
ip -6 rule add from "${ip}" lookup main prio "${RULE_PRIO}"
else
ip rule add from "${ip}" lookup main prio "${RULE_PRIO}"
fi
else
logger -t "${LOG_TAG}" "Rule for ${ip} already exists. Skipping."
fi
done
}
remove_rules() {
IP_LIST=$(ip addr show dev "${ZT_IFACE}" | grep -E 'inet|inet6' | grep -v ' fe80::' | awk '{print $2}' | cut -d'/' -f1)
if [ -z "${IP_LIST}" ]; then return; fi
for ip in ${IP_LIST}; do
if ip rule | grep -q "from ${ip} lookup main"; then
logger -t "${LOG_TAG}" "Removing rule for ${ip}."
# Use the '-6' flag for deletion of IPv6 rules as well.
if echo "${ip}" | grep -q ":"; then
ip -6 rule del from "${ip}" lookup main prio "${RULE_PRIO}"
else
ip rule del from "${ip}" lookup main prio "${RULE_PRIO}"
fi
fi
done
}
start() {
logger -t "${LOG_TAG}" "Service starting, applying rules..."
add_rules
}
stop() {
logger -t "${LOG_TAG}" "Service stopping, removing rules..."
remove_rules
}
reload() {
logger -t "${LOG_TAG}" "Service reloading..."
stop
start
}
Step 3: Authorize and Enable the Service#

After saving the file, execute the following commands in the terminal:

Terminal window
# Grant execution permissions
chmod +x /etc/init.d/zerotier-policy-route
# Enable the service to run on boot
/etc/init.d/zerotier-policy-route enable
# Start the service immediately
/etc/init.d/zerotier-policy-route start
Step 4: Verification#

Run ip rule show. You should see output similar to the following, where the 5260 rules for each ZeroTier IP are correctly positioned above Tailscale’s rules.

0: from all lookup local
...
5260: from 172.27.72.251 lookup main
5260: from fddd:ec77:... lookup main
5260: from fce1:9571:... lookup main
5270: from all lookup 52
...

Implementation on Linux Servers#

Since these errors appeared on OpenWrt, it’s highly likely that another Linux server in your Tailscale network is also running both services. We can adapt the script for standard Linux distributions.

Policy Routing Script#
Terminal window
sudo nano /usr/local/sbin/zerotier-policy-route.sh
#!/bin/bash
#
# This script creates policy routing rules for all ZeroTier interfaces
# to coexist with Tailscale on a standard Linux system.
#
set -e # Exit immediately if a command exits with a non-zero status.
# --- CONFIGURATION ---
# The priority for the new routing rule.
# This MUST be a lower number (higher priority) than Tailscale's rule (usually 5270).
RULE_PRIO="5260"
LOG_TAG="zerotier-policy-route"
# --- END CONFIGURATION ---
# Find all ZeroTier network interfaces (names starting with 'zt').
ZT_INTERFACES=$(ip -o link show | awk -F': ' '{print $2}' | grep '^zt')
if [ -z "$ZT_INTERFACES" ]; then
logger -t "$LOG_TAG" "No ZeroTier interfaces found. Exiting."
exit 0
fi
# Function to add rules for all found interfaces
add_rules() {
logger -t "$LOG_TAG" "Applying policy routing rules..."
for iface in $ZT_INTERFACES; do
# Get all global/unique IP addresses (v4 and v6), excluding link-local (fe80::)
IP_LIST=$(ip addr show dev "$iface" | grep -E 'inet|inet6' | grep -v 'fe80::' | awk '{print $2}' | cut -d'/' -f1)
if [ -z "$IP_LIST" ]; then
logger -t "$LOG_TAG" "No usable IP addresses on interface $iface. Skipping."
continue
fi
for ip in $IP_LIST; do
if ! ip rule | grep -q "from $ip lookup main"; then
logger -t "$LOG_TAG" "Adding rule for $ip (on $iface) with priority $RULE_PRIO."
if [[ "$ip" == *":"* ]]; then
ip -6 rule add from "$ip" lookup main prio "$RULE_PRIO"
else
ip rule add from "$ip" lookup main prio "$RULE_PRIO"
fi
fi
done
done
}
# Function to remove rules
remove_rules() {
logger -t "$LOG_TAG" "Removing policy routing rules..."
while ip rule | grep -q "prio $RULE_PRIO"; do
RULE_TO_DELETE=$(ip rule | grep "prio $RULE_PRIO" | head -n 1)
logger -t "$LOG_TAG" "Deleting rule: $RULE_TO_DELETE"
# We need to parse the rule string to delete it correctly
ip rule del $(echo ${RULE_TO_DELETE} | sed 's/[0-9]*:\s*//')
done
}
case "$1" in
start)
add_rules
;;
stop)
remove_rules
;;
restart)
remove_rules
add_rules
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
;;
esac
exit 0

Set the permissions:

Terminal window
sudo chmod +x /usr/local/sbin/zerotier-policy-route.sh
Systemd Service Unit#
Terminal window
sudo nano /etc/systemd/system/zerotier-policy-route.service
[Unit]
Description=ZeroTier Policy Routing Fix for Tailscale Coexistence
After=network-online.target
Wants=network-online.target
After=zerotier-one.service tailscaled.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/zerotier-policy-route.sh start
ExecStop=/usr/local/sbin/zerotier-policy-route.sh stop
[Install]
WantedBy=multi-user.target

Finally, enable and start the service:

Terminal window
sudo systemctl enable zerotier-policy-route.service
sudo systemctl start zerotier-policy-route.service

Verify with ip rule show; you should see the entries mapped:

0: from all lookup local
5210: from all fwmark 0x80000/0xff0000 lookup main
...
5260: from 172.27.72.101 lookup main <-- Fixed
5260: from 172.27.62.101 lookup main <-- Fixed
5270: from all lookup 52
...

With this setup, the annoying packet drop logs in logread will finally vanish.

Resolving Routing Conflicts Between Tailscale and ZeroTier on OpenWrt/Linux (IPProto-44)
https://tski.uk/blog/en/resolve-route-conflict-zerotier-tailscale/
作者
Tokisaki Galaxy
发布于
2025-07-11
许可协议
CC BY