适合 Linux 桌面的 sing-box 客户端配置

sing-box 作为代理工具的集大成者,支持多协议多功能,且覆盖多平台,但是配置起来比较复杂,这里给出一个适合 Linux 桌面使用的配置样本,供参考。

本配置适合 Linux 桌面,不适配 Windows 和 Android,Mac OS 也许可以参考部分。

配置策略

本配置主要实现的目的:

  1. 提供本地 HTTP/HTTPS/SOCKS5 代理,供给浏览器(Firefox)和命令行工具等使用,自动分流国内和国外流量(基于 GeoIP 和 GeoSite);
  2. 通过代理的 DNS 同样提供国内和国外分流;
  3. 提供本地 DNS 服务器,通过该服务器可以实现 DNS 国内和国外分流;
  4. 避免 DNS 泄漏(DNS 泄漏可以认为是:访问的境外域名泄漏给国内 DNS 服务商,使之可以获悉你访问的境外网站);
  5. 过滤广告;

配置内容

本配置主要内容如下,适合 sing-box 1.10 beta 以上版本,如果使用版本为 1.9.x,则可以考虑删除广告过滤的配置即可:

  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
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
{
  "log": {
    "disabled": false,
    "level": "info",
    "timestamp": false
  },
  "dns": {
    "servers": [
      {
        "tag": "dns-remote",
        "address": "https://1.1.1.1/dns-query",
        "detour": "proxy",
        "client_subnet": "1.0.1.0"
      },
      {
        "tag": "dns-local",
        "address": "local",
        "detour": "direct"
      },
      {
        "tag": "dns-default",
        "address": "https://223.5.5.5/dns-query",
        "detour": "direct"
      },
      {
        "tag": "dns-block",
        "address": "rcode://refused"
      }
    ],
    "rules": [
      {
        "rule_set": [
          "anti-ad"
        ],
        "server": "dns-block",
        "disable_cache": true
      },
      {
        "inbound": [
          "global"
        ],
        "server": "dns-remote"
      },
      {
        "rule_set": [
          "geosite-cn"
        ],
        "server": "dns-default"
      },
      {
        "outbound": "any",
        "server": "dns-default"
      }
    ],
    "final": "dns-remote",
    "independent_cache": true
  },
  "inbounds": [
    {
      "tag": "dns-in",
      "type": "direct",
      "listen": "127.0.0.1",
      "listen_port": 5335,
      "network": "udp",
      "sniff": true,
    },
    {
      "tag": "global",
      "type": "mixed",
      "listen": "127.0.0.1",
      "listen_port": 1091,
      "sniff": true,
      "set_system_proxy": false
    },
    {
      "tag": "auto",
      "type": "mixed",
      "listen": "127.0.0.1",
      "listen_port": 1090,
      "sniff": true,
      "set_system_proxy": false
    }
  ],
  "outbounds": [
    {
      "tag": "proxy",
      "type": "selector",
      "outbounds": [
        "trojan",
        "hysteria"
      ],
      "default": "hysteria"
    },
    {
      "tag": "trojan",
      "type": "trojan",
      ...
    },
    {
      "tag": "hysteria",
      "type": "hysteria",
      ...
    },
    {
      "tag": "direct",
      "type": "direct"
    },
    {
      "tag": "block",
      "type": "block"
    },
    {
      "tag": "dns-out",
      "type": "dns"
    }
  ],
  "route": {
    "rules": [
      {
        "inbound": "dns-in",
        "outbound": "dns-out"
      },
      {
        "rule_set": [
          "anti-ad"
        ],
        "outbound": "block"
      },
      {
        "inbound": [
          "global"
        ],
        "outbound": "proxy"
      },
      {
        "rule_set": [
          "geoip-cn",
          "geosite-cn"
        ],
        "ip_is_private": true,
        "outbound": "direct"
      },
      {
        "protocol": "dns",
        "outbound": "dns-out"
      }
    ],
    "final": "proxy",
    "rule_set": [
      {
        "tag": "anti-ad",
        "type": "remote",
        "format": "binary",
        "url": "https://anti-ad.net/anti-ad-sing-box.srs",
        "download_detour": "proxy"
      },
      {
        "tag": "geosite-cn",
        "type": "remote",
        "format": "binary",
        "url": "https://github.com/SagerNet/sing-geosite/raw/rule-set/geosite-cn.srs",
        "download_detour": "proxy"
      },
      {
        "tag": "geoip-cn",
        "type": "remote",
        "format": "binary",
        "url": "https://github.com/SagerNet/sing-geoip/raw/rule-set/geoip-cn.srs",
        "download_detour": "proxy"
      }
    ]
  },
  "experimental": {
    "cache_file": {
      "enabled": true
    }
  }
}

备注

主要配置思路:

  1. dns 服务器部分,国内使用 alidns 作为境内 dns 服务器,有普通和 DOH 两个,国外使用 Cloudflare 的 DOH dns 服务器,同时创建了一个用于屏蔽的 dns-block 服务器;

  2. dns 规则部分较为简单,基于 anti-ad 规则进行 dns 屏蔽;对于全局代理入口 global 则都转发到 dns-remote 解析;对于域名匹配了 GeoIP-CN 和 GeoSite-CN 规则集,则使用境内 dns 服务器; final 使用 dns-remote。这里实现了 dns 分流和广告屏蔽。outbound=any 规则保证了代理服务器域名的解析,否则代理无法正常启动。

  3. inbounds 提供了 3 个服务入口:

    • dns-in 提供本地 dns 服务,后续可以将系统 dns 请求转发到这里(配合 systemd-resolved);
    • global 提供本地全局代理,支持 HTTP/HTTPS/SOCKS5,代理流量全部走境外;
    • auto 提供本地分流代理,支持 HTTP/HTTPS/SOCKS5,代理流量会自动分流;
  4. outbounds 除了常规的 direct、block 外,创建了一个 dns-out 服务,以及用于代理的 selector 和真正的 trojan/hyseria,这里具体配置则根据实际的来。

  5. route 规则部分,主要包括:

    • dns-in 转发给 dns-out,用于本地 dns 服务器;
    • 基于 anti-ad 的广告屏蔽;
    • global 的全局代理路由;
    • 基于境内网站、IP 匹配或私有网络 IP 匹配,则流量通过本地;
    • final 设置为 proxy 走境外来保证境外流量规则;
    • protocol=dns 部分走 dns-out 是为了 dns 分流;
  6. 规则集部分,除了 anti-ad 需要 1.10 beta 以上版本支持,其他都兼容 1.9.x。这里都是用远程规则集,并使用了 experimental.cache_file 来保证本地缓存。

  7. 需要额外注意的是,不要在 DNS 规则中使用 IP 规则集(比如 GeoIP 等),这会导致在 DNS 路由处理前将域名解析为 IP 进行匹配,以至于在这个阶段发生 DNS 泄漏。

自启动管理

为了实现 sing-box 自启动和权限,则可以使用如下 systemd service 文件,需要注意可执行文件路径及配置文件路径:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# path: /usr/local/lib/systemd/system/sing-box.service
[Unit]
Description=Sing-Box
After=network.target 
Wants=network-online.target

[Service]
Type=simple
# 限制用户权限
DynamicUser=yes
ExecStart=/usr/local/bin/sing-box run -D ${CACHE_DIRECTORY} -c /usr/local/etc/sing-box/config.json
# 提供特权端口监听权限
AmbientCapabilities=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
CacheDirectory=sing-box
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

通过 systemctl enable sing-box –now 后,通过 systemctl status sing-box 查看状态,通过 journalctl -u sing-box 查看日志。

DNS 转发

另外,如果想将系统 DNS 全部转发给 sing-box,我们则可以通过 systemd-resolved dnsmasq 来转发请求(dnsmasq 可以利用 strict-order 来实现 fallback 机制)。

安装完 dnsmasq 后,新增配置文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# path: /etc/dnsmasq.d/dns-servers.conf
# only bind to lo
interface=lo
bind-interfaces
# not use or poll /etc/resolv.conf
no-resolv
no-poll
# cache
# max live in cache
max-cache-ttl=3600
# min live in cache
min-cache-ttl=300
# cache size
cache-size=1000
# keep strict order on servers
strict-order
# sing-box server
server=127.0.0.1#5335
# fallback server if sing-box failed
server=223.5.5.5

然后 systemctl restart dnsmasq 来重启服务。

同时,需要手动修改 /etc/resolv.conf 文件来指定使用 dnsmasq 服务器:

1
2
3
4
# path: /etc/resolv.conf
nameserver ::1
nameserver 127.0.0.1
options edns0 trust-ad

为了避免 /etc/resolv.conf 被以外修改,可以使用 sudo chattr +i /etc/resolv.conf 来标记不可变属性。

对于 Firefox,需要将配置中的代理配置为 127.0.0.1:1090,所有协议共用这个端口。对于普通命令行工具,则可以在 ~/.bashrc 文件中追加两行:

1
2
export http_proxy=http://127.0.0.1:1090
export https_proxy=$http_proxy

DNS 泄漏检测

所有正常后,想检查是否存在 DNS 泄漏,则可以用 Firefox 访问以下两个网站任意一个即可:

  1. https://ipleak.net/
  2. https://browserleaks.com/dns

如果访问的 ip 显示境外,且 dns 服务器也在境外,则表明不存在泄漏。另外,还可以使用 wireshark 来抓 dns 包检查。

关闭 popen 打开的子进程

常见功能开发中,需要获取其他命令的输出数据时,通常可以采用 popen 打开子进程进行读操作,但是 popen 并没有提供终止子进程的接口,pclose 会阻塞等待子进程自己结束然后才返回。导致在一些会持续输出的命令调用中,无法方便的结束子进程。

一种解决方法是使用 fork 来自行实现类似 popen 的接口,比如 Kill a process started with popen,但是在一些简单场景,可以尝试更为简单的方法。

popen 在 Linux 上通常是调用 shell 去创建会话执行命令,可以利用 shell 的方法来获取会话进程的 pid,然后在输出中输出 pid ,这样在调用 pclose 之前可以使用 pkill 调用去终止会话及子进程以避免 pclose 阻塞。

shell(常见 bash)中,$$ 变量表示当前 shell 的进程 pid, 我们可以利用该变量来控制子进程。以 ping 指令为例,使用如下命令创建子进程:

1
2
3
# 需要注意 \$\$ 而非 $$,否则当前 shell 会直接解译该变量导致输出为当前 shell pid,
# 而非会话子进程
sh -c "echo pid=\$\$; ping 223.5.5.5"

其输出:

1
2
3
4
5
6
7
pid=62853
PING 223.5.5.5 (223.5.5.5) 56(84) bytes of data.
64 bytes from 223.5.5.5: icmp_seq=1 ttl=116 time=24.1 ms
64 bytes from 223.5.5.5: icmp_seq=2 ttl=116 time=23.3 ms
64 bytes from 223.5.5.5: icmp_seq=3 ttl=116 time=24.4 ms
64 bytes from 223.5.5.5: icmp_seq=4 ttl=116 time=23.4 ms
...

这时,在另外 shell 中执行 pkill -P 62883 来终止会话子进程(该命令无需 root 权限, pkill -P 只终止父进程 pid 匹配的进程, shell 进程在子进程命令结束后自然退出),子会话及其子进程会终止退出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
pid=62853
PING 223.5.5.5 (223.5.5.5) 56(84) bytes of data.
64 bytes from 223.5.5.5: icmp_seq=1 ttl=116 time=24.6 ms
64 bytes from 223.5.5.5: icmp_seq=2 ttl=116 time=24.7 ms
64 bytes from 223.5.5.5: icmp_seq=3 ttl=116 time=24.5 ms
64 bytes from 223.5.5.5: icmp_seq=4 ttl=116 time=24.2 ms
64 bytes from 223.5.5.5: icmp_seq=5 ttl=116 time=22.6 ms
64 bytes from 223.5.5.5: icmp_seq=6 ttl=116 time=21.2 ms
64 bytes from 223.5.5.5: icmp_seq=7 ttl=116 time=26.3 ms
64 bytes from 223.5.5.5: icmp_seq=8 ttl=116 time=24.7 ms
Terminated

利用该特性,在实践中可以在 popen 后,利用 fgets 从输出中获取会话 pid,然后在 pclose 前,调用 system("pkill -P xxxx") 来终止会话子进程,然后再调用 pclose 去释放资源,此时 pclose 调用不会阻塞, 该方法虽不优雅,但足够 quick-and-dirty.

解决 LXC 客户机中登陆缓慢的问题

如果在 LXC 容器中运行 Ubuntu/Debian,有时候会碰到登陆缓慢卡住的情况,尤其是跨架构的容器,需要很久才登陆成功:

  1. 使用 lxc-attach xxxx 后,使用 su user 切换用户时,卡住;
  2. 直接使用 ssh user@10.0.3.xxx,输入完用户名密码后,卡住;

卡住之后,系统负载上升,检查会发现容器的 systemd 进程 CPU 占用很高,需要很久才正常,然后登陆成功。同样,关闭 LXC 容器时,命令也会等待很久。

解决办法可以参考 Fix Debian 11 bullseye slow SSH login and sudo on LXC Proxmox:

  1. 在容器中运行 systemctl mask systemd-logind;
  2. 在容器中运行 pam-auth-update 然后取消选择 Register user sessions in the systemd control group hierarchy,即可。或者直接运行 pam-auth-update --remove systemd.

具体原因倒不是特别清晰,这种现象特别影响效率。

LXC 容器无法 DHCP 获取到 IP

如果 LXC 容器已经使用 DHCP 获取 IP, 但是启动后无法正常获取 IP,则可以通过这种方法解决:

  • 临时方案:在宿主系统运行 ethtool -K vethXXXX tx off 和 ethtool -K lxcbrXXX tx off,命令需要 root 权限,该命令会关闭 LXC 虚拟网卡的校验功能。
  • 永久方案:使用 udev 规则来自动处理校验关闭,创建文件 /etc/udev/rules.d/90-lxc-net.rules:
1
2
ACTION=="add",SUBSYSTEM=="net",KERNEL=="veth*", RUN+="/usr/sbin/ethtool -K $env{INTERFACE} tx off"
ACTION=="add",SUBSYSTEM=="net",KERNEL=="lxcbr*",RUN+="/usr/sbin/ethtool -K $env{INTERFACE} tx off"

这种现象原因是,虚拟网卡一般不需要在 IP 包中附加校验,但是 DHCP 工具目前会主动校验数据包,造成检验失败,DHCP 握手失败。