本文的封面图来源于Pixiv,原作者是二狗子茶壶BRH

不知从何时开始,当启用代理软件并使用TUN虚拟网卡时,NetworkManager会错误地提示「网络连接受限」,但事实上网络能够正常使用。这不仅会让人困惑,还会影响到依赖网络状态通知的应用(例如调度网络服务)。

解决方法

将检查连通性的服务器绕过TUN网卡即可(例如,domain-list-community提供了connectivity-check列表,包含了相关域名)。1.13.0及之后版本的sing-box支持bypass路由动作,将相关域名应用bypass动作规则即可。需要确保域名解析出的结果为真实IP而非FakeIP[1],且DNS中启用了reverse_mapping,同时该规则需在sniff路由动作之前。

1
2
3
4
{
"rule_set": "geosite-connectivity-check",
"action": "bypass"
}

若使用较旧版本的sing-box,或是其他代理软件,则可将相应的IP地址绕过TUN网卡。sing-box中可使用route_exclude_addressroute_exclude_address_set。以Arch Linux为例,其所使用的默认服务器为ping.archlinux.org,IP地址为95.216.195.1332a01:4f9:c010:2636::1。其他发行版可能需要根据实际情况调整。对于sing-box配置文件:

在入站的tun网卡配置中加入:

1
"route_exclude_address_set": ["arch-linux-ping"]

同时添加对应的规则集:

1
2
3
4
5
6
7
8
9
10
11
{
"tag": "arch-linux-ping",
"rules": [
{
"ip_cidr": [
"95.216.195.133/32",
"2a01:4f9:c010:2636::1/128"
]
}
]
}

探究过程

Arch Linux上,与网络连通性相关的配置文件位于/usr/lib/NetworkManager/conf.d/20-connectivity.conf,其中指定了urihttp://ping.archlinux.org/nm-check.txt。根据NetworkManager的文档,程序会使用SO_BINDDEVICE检查网络是否通畅[2]。源代码中,其nm-connectivity.c中调用libcurl相关函数,使用curl_easy_setopt(ehandle, CURLOPT_INTERFACE, cb_data->ifspec)绑定网络接口,随后检查是否能收到响应[3]。而libcurl会调用setsockopt,以实现绑定[4]。使用strace跟踪系统调用时,发现其调用了setsockopt,和先前分析一致:

1
2
3
821   setsockopt(30, SOL_SOCKET, SO_BINDTODEVICE, "enp55s0\0", 8) = 0
821 setsockopt(24, SOL_SOCKET, SO_BINDTODEVICE, "wlan0\0", 6) = 0
821 setsockopt(32, SOL_SOCKET, SO_BINDTODEVICE, "sing-box\0", 9) = 0

依此,我编写了一个类似的测试程序:

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
#include <curl/curl.h>
#include <stdio.h>

int main(void)
{
CURL *curl;
CURLcode res;

curl_global_init(CURL_GLOBAL_DEFAULT);
curl = curl_easy_init();
if (curl)
{
curl_easy_setopt(curl, CURLOPT_URL, "https://deepchirp.com/");
curl_easy_setopt(curl, CURLOPT_INTERFACE, "wlan0");
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L);

res = curl_easy_perform(curl);

if (res != CURLE_OK)
{
fprintf(stderr, "curl_easy_perform() failed: %s\n",
curl_easy_strerror(res));
}
else
{
char *local_ip;
long local_port;
curl_easy_getinfo(curl, CURLINFO_LOCAL_IP, &local_ip);
curl_easy_getinfo(curl, CURLINFO_LOCAL_PORT, &local_port);
printf("Local bind: %s:%ld\n", local_ip, local_port);
}

curl_easy_cleanup(curl);
}
curl_global_cleanup();
return 0;
}

使用tcpdump监听网卡流量时,我发现TUN接口sing-box上存在与deepchirp.com相关的多路流量,但物理网卡wlan0上似乎并无相关流量。而在关闭sing-box后,wlan0上则能捕获到相关流量。由此可见,sing-box会将流量劫持到其TUN网卡上,导致绑定物理网卡的setsockopt调用失效,从而使NetworkManager误判网络状态。

因此,只需将检查连通性的相关流量绕过TUN网卡即可。拜sing-box于1.13版本中更新的bypass路由动作所赐,只需将相关域名绕过TUN网卡即可。但在最初撰写此文时,sing-box并未提供bypass路由动作。我一开始考虑使用策略路由,为ping.archlinux.org的流量打上路由标记(使用default_mark即可),并使用nftablesip rule将其路由到物理网卡,但感觉不够优雅:因为当sing-box退出后,相关的路由规则无用但仍存在。后来发现,ping.archlinux.org的IP地址似乎并不常变动——至少2022年8月的IP地址即与今相同[5]——因此选择直接将其IP地址绕过TUN网卡。不过,其他发行版的连通性检查服务器地址可能不同,IP地址若时常变动,可能还是使用策略路由更加方便。


  1. geolocation-!cn使用FakeIP应该是常见做法;但其包含了category-dev,而后者又涵盖archlinux,包括了archlinux.org。在这种情况下,需要令connectivity-check中的域名解析出真实IP。 ↩︎

  2. NetworkManager. NetworkManager.conf — NetworkManager configuration file [EB/OL]. (n.d.)[2025-10-20]. ↩︎

  3. NetworkManager. src/core/nm-connectivity.c — apertis/v2026pre, pkg/network-manager (GitLab) [EB/OL]. (n.d.)[2025-10-20]. ↩︎

  4. Android Open Source Project. lib/connect.c — platform/external/curl (main) [EB/OL]. (n.d.)[2025-10-20]. ↩︎

  5. Andreiva. [SOLVED] Arch makes many reverse dns lookups for redirect.archlinux.org [EB/OL]. Arch Linux Forums, (2022-08-22)[2025-10-20]. ↩︎