Resolving Routing Conflicts Between Tailscale and ZeroTier on OpenWrt/Linux (IPProto-44)

因为有时候tailscale和zerotier都不太稳定,所以我在OpenWrt上同时使用了这两个VPN。平时主要用tailscale,zerotier作为应急有时候有奇效。

问题的浮现:神秘的丢包日志

一切始于一个看似无关的现象。在检查 OpenWrt 的系统日志时,我反复看到由 tailscaled(Tailscale 的守护进程)打印出的错误信息:

1
2
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)

日志显示 Tailscale 正在丢弃一种它不认识的协议(IPProto-44)的数据包,来源和目的地都是我网络内的设备。更奇怪的是,这些流量似乎与 Tailscale 本身无关。经过排查,元凶很快就锁定了——这些“异常流量”正是我的 ZeroTier 客户端为了连接其 Moon 中继服务器而发出的。

深入分析,路由表?

为什么 ZeroTier 的流量会跑到 Tailscale 的地盘上呢?答案藏在路由规则里。

通过 SSH 登录到 OpenWrt 并执行 ip rule show,我们看到了问题的关键:

1
2
3
4
5
6
root@ImmortalWrt:~# ip rule show
0: from all lookup local
...
5270: from all lookup 52
32766: from all lookup main
...

Linux 的路由规则是按优先级(prio,数字越小优先级越高)执行的。这段输出告诉我们:
系统会先检查是不是发给路由器自己的流量 (lookup local)。如果不是,它会去检查一个优先级为 5270 的规则,这条规则说:“所有流量,都先去查阅路由表 52”。路由表 52 是 Tailscale 安装时创建的专属路由表,里面只有通往其他 Tailscale 节点的路由。只有在路由表 52 里也找不到路时,流量才会继续往下,去查询优先级更低的 main 主路由表(我们正常的互联网出口就在这里)。

结论显而易见:Tailscale 设置了一条过于“霸道”的高优先级规则,把所有从路由器发出的流量(包括 ZeroTier 的)都“劫持”了。当 ZeroTier 的特殊数据包被导向 Tailscale 的网络时,不认识它的 Tailscale 自然就选择丢弃。

解决方案

要解决这个问题,我们不能粗暴地修改 Tailscale 的规则,而是应该为 ZeroTier 建立一条优先级更高的“专属通道”。

我们的策略是: 创建一条新的路由规则,明确告诉系统:“凡是从 ZeroTier 虚拟网卡发出的流量,请直接走 main 主路由表”,并确保这条规则的优先级高于 Tailscale 的 5270

Openwrt脚本与使用

第一步:(推荐)安装完整版 ip 工具

OpenWrt 自带的 ip 命令功能可能不全。安装 ip-full 可以避免很多疑难杂症。

1
2
opkg update
opkg install ip-full
第二步:创建服务脚本

使用 vinano 创建一个新的服务文件。

1
nano /etc/init.d/zerotier-policy-route

将下面的完整脚本代码粘贴到你打开的文件中。

请根据你的实际情况,修改 ZT_IFACE 变量的值。你可以通过 ifconfig | grep zt 命令找到你的 ZeroTier 网卡名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#!/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
}
第三步:授权并启用服务

保存文件后,在终端执行以下命令:

1
2
3
4
5
6
7
8
# 赋予脚本执行权限
chmod +x /etc/init.d/zerotier-policy-route

# 启用服务,让它开机自启
/etc/init.d/zerotier-policy-route enable

# 立即启动服务以应用规则
/etc/init.d/zerotier-policy-route start
第四步:验证成果

执行 ip rule show,你将看到类似下面的输出,我们为 ZeroTier 的每个 IP 添加的 5260 规则已经稳稳地待在了 Tailscale 的规则之上。

1
2
3
4
5
6
7
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
...

Linux服务器脚本与使用

由于Openwrt出现这个提示,必然tailscale虚拟局域网中有另一台Linux服务器也安装了Tailscale和ZeroTier。我们可以将上面的脚本稍作修改,适用于其他Linux发行版。

策略路由脚本
1
sudo nano /usr/local/sbin/zerotier-policy-route.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#!/bin/bash

#
# This script creates policy routing rules for all ZeroTier interfaces
# to coexist with Tailscale on a Debian-based 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').
# We use `ip -o link show` for a stable, one-line-per-interface output.
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
# Only add the rule if it doesn't already exist.
if ! ip rule | grep -q "from $ip lookup main"; then
logger -t "$LOG_TAG" "Adding rule for $ip (on $iface) with priority $RULE_PRIO."
# Use the '-6' flag for IPv6 addresses.
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 for all found interfaces
remove_rules() {
logger -t "$LOG_TAG" "Removing policy routing rules..."
# We need to find all possible IPs that could have had rules.
# It's safer to just try deleting any rule with our priority.
# This is simpler and more robust than tracking IPs.
while ip rule | grep -q "prio $RULE_PRIO"; do
# Find the first rule with our priority and delete it. Loop until none are left.
RULE_TO_DELETE=$(ip rule | grep "prio $RULE_PRIO" | head -n 1)
logger -t "$LOG_TAG" "Deleting rule: $RULE_TO_DELETE"
# Re-construct the delete command from the rule string.
ip rule del ${RULE_TO_DELETE}
done
}

# Main command logic
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

最后记得加上权限

1
sudo chmod +x /usr/local/sbin/zerotier-policy-route.sh

自启动服务脚本

1
sudo nano /etc/systemd/system/zerotier-policy-route.service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[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

最后,要启动服务。

1
2
sudo systemctl enable zerotier-policy-route.service
sudo systemctl start zerotier-policy-route.service

可以验证一下ip rule show,应该可以看到下面这样

1
2
3
4
5
6
7
8
9
10
0:      from all lookup local
5210: from all fwmark 0x80000/0xff0000 lookup main
5230: from all fwmark 0x80000/0xff0000 lookup default
5250: from all fwmark 0x80000/0xff0000 unreachable
5260: from 172.27.72.101 lookup main <--
5260: from 172.27.62.101 lookup main <--
5260: from 172.27.71.101 lookup main <--
5270: from all lookup 52
32766: from all lookup main
32767: from all lookup default

同时,logread 中烦人的丢包日志也从此消失了。

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:

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.

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

Create a new init script using vi or nano.

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

Paste the following script into the file.

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:

# 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
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:

sudo chmod +x /usr/local/sbin/zerotier-policy-route.sh
Systemd Service Unit
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:

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://tokisaki.top/blog/resolve-route-conflict-zerotier-tailscale/
作者
Tokisaki Galaxy
发布于
2025年7月11日
许可协议