日期:2026-06-15
这份文档记录今天在树莓派上做的网络调整。它不是一份通用教程,里面的路径、端口和策略都按这台机器当前的用法写。
机器角色很简单:树莓派作为单臂旁路由和 Tailscale exit node。外部设备通过 Tailscale 或局域网把流量交给树莓派,树莓派再用 Xray 做透明代理和分流。
真正要解决的问题有几个:
- 树莓派的 Tailscale 到手机有时先走 DERP relay,直连建立慢。
- 树莓派原来没有拿到公网 IPv6,Tailscale 只能靠 IPv4 打洞。
- Xray 上游代理不支持 IPv6,不能把 IPv6 目标直接丢给代理。
- 作为 exit node 时,客户端的 IPv6 流量不能泄漏成树莓派家宽直连。
- DNS 不能继续给客户端大量返回 AAAA,否则客户端会优先尝试 IPv6,然后失败或卡住。
- geosite 和 geoip 需要自动更新,不想以后手动替换。
最后的目标不是把所有东西都做成 IPv6。这个上游代理不支持 IPv6,所以更实际的目标是:
```text
Tailscale 打洞可以用 IPv6
树莓派自己不能随便用公网 IPv6 出口
Tailscale exit 上网尽量走 IPv4
IPv4 上网继续进 Xray
公网 IPv6 exit 流量直接拒绝,避免泄漏
```
## 相关折腾记录
- [Cloudflare Tunnel 通过 Xray 出口:cloudflared、nftables 和 sniffing 排查](https://aside0.me/cloudflared-tunnel-xray-sniff-nft-2026-06-17/)
- [树莓派 Wi-Fi 热点接入 Xray:DNS 防污染和透明代理排查](https://aside0.me/raspberry-pi-wifi-hotspot-xray-dns-2026-06-19/)
- [树莓派 Xray DNS 防泄露和游戏下载分流](https://aside0.me/raspberry-pi-xray-dns-leak-game-split-2026-06-20/)
## 系统和环境
这台机器是树莓派,系统不是 OpenWrt,而是 Debian。今天的配置都按这个环境做:
```text
硬件:Raspberry Pi 5 Model B Rev 1.0
系统:Debian GNU/Linux 13 (trixie)
内核:6.12.62+rpt-rpi-v8
架构:linux/arm64
网络管理:NetworkManager
防火墙:nftables
服务管理:systemd 257
Tailscale:1.98.4
Xray:26.3.27
```
网络上它是单臂旁路由,只接一根网线,所有进出都在 `eth0` 上:
```text
LAN 网段:192.168.51.0/24
树莓派 IPv4:192.168.51.72/24
默认 IPv4 网关:192.168.51.1
公网 IPv6:通过 RA 自动获取
Tailscale IPv4:100.76.137.29
Tailscale IPv6:fd7a:115c:a1e0::6039:891d
```
几个前提要先说清楚,不然后面的规则会看起来有点绕:
- 树莓派自己有公网 IPv6,但它不应该直接拿这个 IPv6 上网。
- Tailscale 需要 IPv6 来做 direct 打洞,所以不能把 IPv6 整个关掉。
- Xray 的上游代理不支持 IPv6 目标,所以公网 IPv6 exit 流量不能交给 Xray。
- 最终策略是让客户端尽量拿 IPv4,IPv4 再走 Xray;IPv6 只保留给 Tailscale 打洞和局域网必要协议。
## 当前拓扑
```mermaid
flowchart LR
phone["Tailscale 客户端<br/>手机或电脑"]
ts["tailscale0<br/>树莓派"]
nft["nftables<br/>xray_tproxy"]
xray["Xray<br/>透明代理和分流"]
proxy["Xray 上游代理<br/>只支持 IPv4 目标"]
wan4["IPv4 Internet"]
wan6["IPv6 Internet"]
phone -->|IPv4 exit 流量| ts
ts --> nft
nft -->|IPv4 TCP/UDP tproxy| xray
xray --> proxy
proxy --> wan4
phone -->|公网 IPv6 exit 流量| ts
ts --> nft
nft -->|reject| wan6
```
树莓派本机自己的路径和 exit 流量不一样。这个点容易搞混。
```mermaid
flowchart TD
app["树莓派本机程序"]
output["Linux OUTPUT 链"]
block6["公网 IPv6 output reject"]
ipv4["IPv4 默认路由"]
xray_socks["显式使用 Xray socks"]
proxy["Xray 上游代理"]
app --> output
output -->|普通公网 IPv6| block6
output -->|普通 IPv4| ipv4
app -->|socks5 127.0.0.1:10808| xray_socks
xray_socks --> proxy
```
Tailscale 自己的打洞流量不能一起封掉。它是本机 `tailscaled` 进程发出的 UDP,如果全局 drop IPv6 output,刚修好的 IPv6 direct 会被打回去。
```mermaid
flowchart LR
tailscaled["tailscaled<br/>本机进程"]
output["nft output"]
stun["STUN<br/>UDP 3478"]
wg["Tailscale UDP<br/>41642"]
peer["Tailscale peer<br/>手机"]
tailscaled --> output
output -->|允许 UDP 3478| stun
output -->|允许 UDP 41642| wg
wg --> peer
```
## 做完之后的状态
树莓派当前状态:
```text
IPv4 地址:192.168.51.72/24
IPv4 网关:192.168.51.1
Tailscale IP:100.76.137.29
Tailscale UDP 端口:41642
Xray socks:0.0.0.0:10808
Xray TProxy TCP:127.0.0.1:1041
Xray TProxy UDP:127.0.0.1:1051
Xray DNS 入站:192.168.51.72:53
```
服务状态应该是:
```sh
systemctl is-active xray xray-tproxy tailscaled xray-geo-update.timer
systemctl is-enabled xray xray-tproxy tailscaled xray-geo-update.timer
```
期望结果:
```text
active
active
active
active
enabled
enabled
enabled
enabled
```
## 1. 让树莓派拿到 IPv6,但不要把 IPv6 当出口能力
一开始树莓派的 `eth0` 是这样的:
```text
ipv6.method: disabled
```
这会导致 `tailscale netcheck` 只能看到:
```text
IPv6: no, but OS has support
```
改成自动获取 IPv6:
```sh
sudo nmcli connection modify "Wired connection 1" \
ipv6.method auto \
ipv6.may-fail yes \
ipv6.never-default no \
ipv6.ignore-auto-routes no \
ipv6.ignore-auto-dns no
sudo nmcli device reapply eth0
```
因为树莓派同时开了转发,Linux 默认会影响 RA 接收。这里要让 `eth0` 在 forwarding 打开时仍然接收路由通告:
```sh
sudo tee /etc/sysctl.d/98-raspi-eth0-ipv6.conf >/dev/null <<'EOF'
# Keep accepting IPv6 router advertisements on eth0 even when forwarding is enabled.
net.ipv6.conf.eth0.accept_ra=2
net.ipv6.conf.eth0.autoconf=1
net.ipv6.conf.eth0.disable_ipv6=0
net.ipv6.conf.default.disable_ipv6=0
net.ipv6.conf.all.disable_ipv6=0
EOF
sudo sysctl -p /etc/sysctl.d/98-raspi-eth0-ipv6.conf
```
验证:
```sh
ip -6 addr show dev eth0
ip -6 route show default
tailscale netcheck
```
做完之后,树莓派能拿到公网 IPv6,`tailscale netcheck` 会显示:
```text
IPv6: yes
```
这个 IPv6 的作用主要是改善 Tailscale direct,不是让客户端通过树莓派访问 IPv6 Internet。
## 2. 避免 Tailscale 端口冲突
树莓派和 Kwrt 在同一个上游 NAT 后面。之前两台机器都用默认 UDP `41641`,树莓派到手机有时会长时间停在 DERP。
树莓派改成 `41642`:
```sh
sudo sed -i 's/^PORT=.*/PORT=41642/' /etc/default/tailscaled
sudo systemctl restart tailscaled
```
nft input 同时允许两个端口,避免以后互相测的时候误伤:
```nft
udp dport { 41641, 41642 } accept
```
验证:
```sh
ss -lunp | grep 41642
tailscale netcheck
tailscale ping 100.89.253.91
```
`tailscale ping` 一开始可能会先走 DERP,随后切到 direct。这是正常的。Tailscale 自己也说明,第一次 ping 很可能先走 DERP,等 NAT traversal 找到路径后再切直连。
## 3. Xray 在这里负责什么
Xray 在这台树莓派上只负责一件事:把应该代理的 IPv4 流量接住,然后按规则决定直连、代理、丢弃还是走 DNS 出站。
它不负责 Tailscale 自己的打洞。`tailscaled` 是本机进程,直接从 Linux OUTPUT 发 UDP 包出去。这个路径必须保留,否则 Tailscale 的 IPv6 direct 会变差。
它也不负责公网 IPv6 exit。原因很现实:上游 Xray 代理不支持 IPv6 目标。把 IPv6 目标硬塞给它,结果通常不是变成 IPv4,而是连接失败。所以这里的策略是让客户端尽量只拿 IPv4;如果还有公网 IPv6 流量进来,就直接拒绝。
Xray 内部大概是这条路径:
```mermaid
flowchart LR
socks["socks-in<br/>10808"]
tproxy["tproxy-in<br/>1041"]
udp["udp-redir<br/>1051"]
dnsin["dns-in<br/>53"]
route["routing rules"]
direct["direct"]
proxy["default proxy"]
block["blackhole"]
dnsout["dns-out"]
socks --> route
tproxy --> route
udp --> route
dnsin --> dnsout
route --> direct
route --> proxy
route --> block
route --> dnsout
```
当前 Xray 服务和资源位置:
```text
配置文件:/etc/xray/config.json
二进制:/usr/local/bin/xray
资源目录:/usr/local/share/xray
服务:xray.service
运行用户:root
运行组:xray_tproxy
```
运行前要保证 systemd 环境里有:
```text
XRAY_LOCATION_ASSET=/usr/local/share/xray
```
否则 `geosite.dat` 和 `geoip.dat` 可能找不到。
### Xray 入站
当前有四个入站。
```text
socks-in 0.0.0.0:10808 socks
tproxy-in 127.0.0.1:1041 tunnel
udp-redir 127.0.0.1:1051 dokodemo-door
dns-in 192.168.51.72:53 dokodemo-door
```
`socks-in` 是给手动代理用的。局域网里如果某个程序支持 socks,可以直接填:
```text
192.168.51.72:10808
```
它开启了 UDP,也开启了 sniffing。sniffing 会从 HTTP、TLS、QUIC 里尽量还原域名,这样路由规则可以按 `geosite` 分流,而不是只看到 IP。当前 sniffing 使用 `routeOnly`,域名只参与路由判断,不改写真实目标地址;Cloudflare Tunnel、API 和 Zero Trust Access 这类控制面域名用 `domainsExcluded` 单独排除,避免用过宽的 `geoip:cloudflare` 影响普通 CDN 站点分流。
`tproxy-in` 是透明代理的 TCP 主入口。nft 把命中的 TCP 流量送到 `127.0.0.1:1041`,Xray 通过 TProxy 读取原始目标地址。这里用的是 `tunnel`,并且允许 `tcp,udp`,不过当前 nft 的 UDP 被单独送到 `udp-redir`。
`udp-redir` 是透明代理的 UDP 入口。它监听 `127.0.0.1:1051`,协议是 `dokodemo-door`,开启 `followRedirect`。这条主要是为了让 DNS、QUIC、STUN 之外的 UDP 流量能被明确接住和记录。
`dns-in` 是给客户端用的 DNS 入口。它监听树莓派的 LAN 地址:
```text
192.168.51.72:53
```
这个入站收到的 DNS 请求会交给 `dns-out`,而不是直接当普通流量走代理。
### Xray 出站
当前出站可以按用途理解:
```text
default:vless-enc-openwrt 默认代理出站
direct 直连
blackhole 丢弃
dns-out Xray DNS 出站
```
还有一个备用的 VLESS 出站。它在配置里存在,但当前路由默认走的是 `default:vless-enc-openwrt`。
`direct` 出站的 `domainStrategy` 是 `UseIP`。这表示直连时会解析目标再连接。配合 `geoip:cn` 和 `geosite:cn`,大陆 IP 和大陆域名会尽量不走代理。
`blackhole` 用来处理广告域名。它不是防火墙 drop,而是 Xray 收到流量后主动丢弃,所以日志里能看到命中了哪个规则。
### Xray 路由顺序
Xray 的路由是从上往下匹配的。当前顺序是:
```text
dns-in -> dns-out
port 53 -> dns-out
dns-local -> direct
geosite:category-games@cn -> direct
geosite:category-ads-all -> blackhole
geoip:private -> direct
geoip:cn -> direct
geosite:cn -> direct
UDP 123 -> direct
其他 -> default:vless-enc-openwrt
```
顺序不能随便换。几个点比较容易踩:
- `dns-in` 和 `port 53` 要在前面,不然 DNS 可能被当成普通代理流量。
- `dns-local -> direct` 是为了避免 Xray 自己的 DNS 查询绕回 Xray。
- `category-games@cn` 放在 `category-ads-all` 前面,是为了减少国内游戏域名被广告规则误伤。
- `geoip:private` 要直连,否则内网地址、Tailscale 地址和保留地址容易被错误代理。
- `geosite:cn` 和 `geoip:cn` 都保留,因为透明代理有时只能看到 IP,有时能 sniff 到域名。
当前路由的 `domainStrategy` 是:
```text
IPIfNonMatch
```
意思是域名规则没有命中时,Xray 会再尝试解析 IP,然后继续用 IP 规则判断。这个配置适合这里的分流方式,因为很多透明代理流量不一定一开始就有完整域名。
### DNS 只返回 IPv4
因为上游 Xray 代理不支持 IPv6,DNS 不能继续主动给客户端 AAAA。否则客户端拿到 IPv6 目标后,要么失败,要么绕过代理。
Xray DNS 当前是:
```json
{
"servers": ["localhost"],
"queryStrategy": "UseIPv4",
"useSystemHosts": true,
"tag": "dns-local"
}
```
这个配置的意思不是做 IPv6 转 IPv4。它只是尽量不把 AAAA 返回给客户端,让客户端优先走 IPv4。对双栈网站够用,对 IPv6-only 网站不支持。
验证:
```sh
dig @192.168.51.72 www.google.com A +short
dig @192.168.51.72 www.google.com AAAA +short
```
期望是 A 有结果,AAAA 为空。
### Xray 验证
配置测试要用 `sudo` 跑。普通用户直接跑会因为无法打开 `/var/log/xray/access.log` 报权限错误,这不代表配置坏了。
```sh
sudo env XRAY_LOCATION_ASSET=/usr/local/share/xray \
/usr/local/bin/xray run -test -config /etc/xray/config.json
```
检查监听:
```sh
sudo ss -lntup | grep -E 'xray|:1041|:1051|:10808|:53'
```
检查分流:
```sh
curl -x socks5h://127.0.0.1:10808 http://www.baidu.com/ -I
curl -x socks5h://127.0.0.1:10808 https://www.google.com/generate_204 -I
curl -x socks5h://127.0.0.1:10808 http://googleads.g.doubleclick.net/ -I
```
日志期望:
```text
www.baidu.com -> direct
www.google.com -> default:vless-enc-openwrt
googleads.g.doubleclick.net -> blackhole
```
## 4. 规则库来源不是核心条件
`geosite.dat` 和 `geoip.dat` 是 Xray 的规则数据。这里需要的是这些规则能被解析:
```text
geoip:private
geoip:cn
geosite:cn
geosite:category-ads-all
geosite:category-games@cn
```
换成 Loyalsoldier 不是旁路由成立的必要条件。它只是今天选择的规则库来源。用 Xray 官方规则库、v2fly 规则库,或者其他兼容 dat,也可以跑这个方案。前提是你用到的规则名在那份 dat 里存在,或者你把配置里的规则名改成对应数据源支持的名字。
今天换成 Loyalsoldier 的原因很普通:它的 `geosite.dat` 和 `geoip.dat` 发布稳定,包含常用分类,直接下载同名文件就能覆盖 Xray 的资源目录。
当前安装位置:
```text
/usr/local/share/xray/geosite.dat
/usr/local/share/xray/geoip.dat
```
如果只是重建这套网络,不一定要先换规则库。更实用的顺序是:
```text
先让 Xray 跑通
再确认 geosite/geoip 规则能命中
最后再决定要不要换规则库来源和自动更新
```
手动替换流程可以这样做:
```sh
tmpdir=$(mktemp -d)
cd "$tmpdir"
curl -fsSL -o geosite.dat \
https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
curl -fsSL -o geoip.dat \
https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
sudo cp -a /usr/local/share/xray/geosite.dat /usr/local/share/xray/geosite.dat.bak.$(date +%Y%m%d-%H%M%S)
sudo cp -a /usr/local/share/xray/geoip.dat /usr/local/share/xray/geoip.dat.bak.$(date +%Y%m%d-%H%M%S)
sudo install -o root -g root -m 0644 geosite.dat /usr/local/share/xray/geosite.dat
sudo install -o root -g root -m 0644 geoip.dat /usr/local/share/xray/geoip.dat
sudo env XRAY_LOCATION_ASSET=/usr/local/share/xray \
/usr/local/bin/xray run -test -config /etc/xray/config.json
sudo systemctl restart xray
```
## 5. nft 透明代理和 IPv6 策略
核心 nft 表是:
```text
inet xray_tproxy
```
IPv4 入口:
```nft
iifname "tailscale0" jump proxy4
iifname "eth0" ip saddr 192.168.51.0/24 jump proxy4
```
这表示:
- Tailscale 进来的 IPv4 exit 流量进 Xray。
- 局域网其他设备把树莓派当网关时,IPv4 流量进 Xray。
IPv4 透明代理规则:
```nft
ip daddr {
0.0.0.0/8,
10.0.0.0/8,
100.64.0.0/10,
127.0.0.0/8,
169.254.0.0/16,
172.16.0.0/12,
192.168.0.0/16,
224.0.0.0/3
} return
meta l4proto tcp tproxy ip to 127.0.0.1:1041 meta mark set 0x100 accept
meta l4proto udp tproxy ip to 127.0.0.1:1051 meta mark set 0x100 accept
```
公网 IPv6 exit 流量不代理,直接拒绝:
```nft
chain proxy6 {
ip6 daddr { ::/127, fc00::/7, fe80::/10, ff00::/8 } return
reject with icmpv6 type admin-prohibited
}
```
这里保留了 `fc00::/7`,因为 Tailscale overlay IPv6 是 `fd7a:...`,不能把内网 Tailscale IPv6 一起打掉。
## 6. 本机公网 IPv6 output 也阻断
后来又补了一条策略:树莓派本机也不能直接走公网 IPv6。
但不能全封。Tailscale 的 IPv6 direct 和 netcheck 需要本机 UDP 流量,所以 output 链保留这些:
```nft
chain output {
type filter hook output priority filter; policy accept;
meta nfproto ipv4 accept
oifname "lo" accept
ct state established,related accept
ip6 daddr { ::/127, fc00::/7, fe80::/10, ff00::/8 } accept
udp sport 41642 accept
udp dport 41642 accept
udp dport 3478 accept
icmpv6 type {
destination-unreachable,
packet-too-big,
time-exceeded,
parameter-problem,
nd-router-solicit,
nd-router-advert,
nd-neighbor-solicit,
nd-neighbor-advert
} accept
meta nfproto ipv6 reject with icmpv6 type admin-prohibited
}
```
这个策略的意思是:
- 本机普通 IPv6 访问会失败。
- 本机 IPv4 不受影响。
- 显式走 Xray socks 仍然能代理。
- Tailscale IPv6 打洞还在。
验证:
```sh
curl -6 https://ifconfig.co/ip
curl -4 https://ifconfig.co/ip
curl -x socks5h://127.0.0.1:10808 https://ifconfig.co/ip
tailscale netcheck
```
期望:
```text
curl -6 失败
curl -4 返回家宽 IPv4
socks 返回代理出口 IP
tailscale netcheck 仍然 IPv6: yes
```
这一步一开始只放行了 `41642`,`tailscale netcheck` 会变成 `IPv6: no`。原因是 netcheck 还要访问 STUN 的 UDP `3478`。补上 `udp dport 3478 accept` 后恢复正常。
## 7. 规则库自动更新,可选但已启用
这一步不是透明代理必须做的事。没有自动更新,Xray 也能正常工作。它解决的是另一个问题:`geoip.dat` 和 `geosite.dat` 会变旧,国内直连、广告屏蔽和分类规则会慢慢不准。
当前脚本下载的是 Loyalsoldier 的 release 文件。如果以后想换回别的规则库来源,改脚本里的下载地址就行,不需要重做 TProxy、DNS 或 Tailscale 配置。
新增脚本:
```text
/usr/local/sbin/xray-update-geo-assets
```
新增 systemd unit:
```text
/etc/systemd/system/xray-geo-update.service
/etc/systemd/system/xray-geo-update.timer
```
定时:
```text
每周日 04:00 CST
```
查看:
```sh
systemctl list-timers xray-geo-update.timer --no-pager
```
脚本逻辑:
```mermaid
flowchart TD
start["timer 触发"]
dl["下载 geosite.dat 和 geoip.dat<br/>到临时目录"]
same{"和当前文件一致吗"}
backup["备份当前 dat"]
install["安装新 dat"]
test["xray run -test"]
restart["restart xray"]
rollback["失败则回滚"]
done["完成"]
start --> dl
dl --> same
same -->|是| done
same -->|否| backup
backup --> install
install --> test
test -->|失败| rollback
test -->|通过| restart
restart -->|失败| rollback
restart -->|成功| done
```
手动验证:
```sh
sudo systemctl start xray-geo-update.service
systemctl status xray-geo-update.service --no-pager -l
```
## 8. 为什么不做 IPv6 转 IPv4
这个地方容易想歪。
NAT64 和 DNS64 是 IPv6-only 网络访问 IPv4 服务的方案。它不能把一个 IPv6-only 目标凭空变成 IPv4 目标。如果客户端直接访问 `[2606:4700:4700::1111]` 这种 IPv6 literal,DNS 层也没法帮你改。
我们这里的问题是 Xray 上游代理不支持 IPv6 目标。所以更稳的方案是:
```text
不要给客户端 AAAA
不要让 exit IPv6 直连泄漏
让客户端回落 IPv4
IPv4 再走 Xray
```
这不完美。IPv6-only 网站会打不开。但它比偷偷直连树莓派家宽 IPv6 更可控。
## 9. 验证清单
每次改完可以按这个顺序看。
服务:
```sh
systemctl is-active xray xray-tproxy tailscaled xray-geo-update.timer
systemctl is-enabled xray xray-tproxy tailscaled xray-geo-update.timer
```
Xray 配置:
```sh
sudo env XRAY_LOCATION_ASSET=/usr/local/share/xray \
/usr/local/bin/xray run -test -config /etc/xray/config.json
```
监听端口:
```sh
sudo ss -lntup | grep -E 'xray|tailscaled|:1041|:1051|:10808|:53|:41642'
```
nft 规则:
```sh
sudo nft list table inet xray_tproxy
```
DNS:
```sh
dig @192.168.51.72 www.google.com A +short
dig @192.168.51.72 www.google.com AAAA +short
```
Tailscale:
```sh
tailscale netcheck
tailscale ping 100.89.253.91
tailscale status
```
出口:
```sh
curl -6 https://ifconfig.co/ip
curl -4 https://ifconfig.co/ip
curl -x socks5h://127.0.0.1:10808 https://ifconfig.co/ip
```
当前期望:
```text
curl -6 失败
curl -4 返回树莓派家宽 IPv4
socks 返回 Xray 代理出口
tailscale netcheck IPv6: yes
```
## 10. 回滚
今天所有关键文件都留了时间戳备份。
Xray 配置:
```text
/etc/xray/config.json.bak.*
```
透明代理脚本:
```text
/usr/local/sbin/xray-tproxy-setup.bak.*
```
Tailscale 端口配置:
```text
/etc/default/tailscaled.bak.*
```
规则库:
```text
/usr/local/share/xray/geosite.dat.bak.*
/usr/local/share/xray/geoip.dat.bak.*
```
回滚示例:
```sh
sudo cp -a /etc/xray/config.json.bak.YYYYMMDD-HHMMSS /etc/xray/config.json
sudo cp -a /usr/local/sbin/xray-tproxy-setup.bak.YYYYMMDD-HHMMSS /usr/local/sbin/xray-tproxy-setup
sudo env XRAY_LOCATION_ASSET=/usr/local/share/xray \
/usr/local/bin/xray run -test -config /etc/xray/config.json
sudo systemctl restart xray
sudo systemctl restart xray-tproxy
```
## 11. 当前设计的边界
这套配置不是为了让树莓派成为完整双栈代理出口。它更像一个折中:
- IPv6 用来改善 Tailscale 打洞。
- 上网出口仍以 IPv4 为主。
- Xray 上游不支持 IPv6,所以公网 IPv6 exit 直接拒绝。
- DNS 尽量只给 IPv4,减少客户端先走 IPv6 的机会。
- 如果客户端自己开 DoH 或硬编码 IPv6 地址,可能还是会先失败。
这就是今天最后落下来的状态。它不算优雅,但方向是清楚的:能代理的走代理,不能代理的不要偷偷直连。
## 附录:脚本和配置快照
下面是 2026-06-15 从树莓派上导出的脱敏快照。它的用途是让以后排查时能快速找到当时到底改了哪些文件、每个脚本做了什么、服务是怎么启动的。
注意两点:
- Xray 的出口代理细节已经脱敏,包括服务器地址、端口、UUID、加密、flow、streamSettings 等。
- Tailscale 的私钥、节点 ID、用户资料、主机名等已经脱敏。
这些脱敏块适合阅读和复查,不适合直接覆盖到机器上运行。
关键文件路径:
```text
/usr/local/sbin/xray-tproxy-setup
/usr/local/sbin/xray-tproxy-cleanup
/usr/local/sbin/xray-update-geo-assets
/etc/systemd/system/xray-tproxy.service
/etc/systemd/system/xray.service
/etc/systemd/system/xray-geo-update.service
/etc/systemd/system/xray-geo-update.timer
/etc/xray/config.json
/etc/default/tailscaled
/usr/lib/systemd/system/tailscaled.service
```
### `/usr/local/sbin/xray-tproxy-setup`
```sh
#!/bin/sh
set -eu
NFT=/usr/sbin/nft
TABLE=100
LAN_IFACE=eth0
LAN_CIDR=192.168.51.0/24
MARK_HEX=0x100
TPROXY_TCP_PORT=1041
TPROXY_UDP_PORT=1051
TPROXY_IP=127.0.0.1
NFT_FILE=/run/xray_tproxy.nft
modprobe nf_tables 2>/dev/null || true
modprobe nft_tproxy 2>/dev/null || true
modprobe nft_socket 2>/dev/null || true
modprobe nf_tproxy_ipv4 2>/dev/null || true
modprobe nf_tproxy_ipv6 2>/dev/null || true
modprobe nf_socket_ipv4 2>/dev/null || true
modprobe nf_socket_ipv6 2>/dev/null || true
sysctl -w net.ipv4.ip_forward=1 >/dev/null
sysctl -w net.ipv6.conf.all.forwarding=1 >/dev/null
sysctl -w net.ipv4.ip_nonlocal_bind=1 >/dev/null
sysctl -w net.ipv4.conf.all.rp_filter=0 net.ipv4.conf.default.rp_filter=0 >/dev/null
sysctl -w net.ipv4.conf.all.route_localnet=1 net.ipv4.conf.default.route_localnet=1 >/dev/null
sysctl -w net.ipv4.conf.all.accept_local=1 net.ipv4.conf.default.accept_local=1 >/dev/null
sysctl -w net.ipv4.conf.all.send_redirects=0 net.ipv4.conf.default.send_redirects=0 >/dev/null
for iface in "$LAN_IFACE" tailscale0; do
[ -d "/proc/sys/net/ipv4/conf/$iface" ] || continue
sysctl -w "net.ipv4.conf.$iface.rp_filter=0" >/dev/null || true
sysctl -w "net.ipv4.conf.$iface.route_localnet=1" >/dev/null || true
sysctl -w "net.ipv4.conf.$iface.accept_local=1" >/dev/null || true
sysctl -w "net.ipv4.conf.$iface.send_redirects=0" >/dev/null || true
done
for chain in XRAY_TPROXY XRAY_LAN_TPROXY_TEST TPROXY_PROBE TPROXY_NS_PROBE XRAY_LAN_REDIR_MARK; do
while iptables -t mangle -D PREROUTING -i tailscale0 -j "$chain" 2>/dev/null; do :; done
while iptables -t mangle -D PREROUTING -i "$LAN_IFACE" -s "$LAN_CIDR" -j "$chain" 2>/dev/null; do :; done
while iptables -t mangle -D PREROUTING -i "$LAN_IFACE" -s 192.168.51.170/32 -j "$chain" 2>/dev/null; do :; done
while iptables -t mangle -D PREROUTING -i veth-tpx-host -s 10.200.0.2/32 -j "$chain" 2>/dev/null; do :; done
iptables -t mangle -F "$chain" 2>/dev/null || true
iptables -t mangle -X "$chain" 2>/dev/null || true
done
for chain in XRAY_LAN_REDIR; do
while iptables -t nat -D PREROUTING -i "$LAN_IFACE" -s "$LAN_CIDR" -j "$chain" 2>/dev/null; do :; done
iptables -t nat -F "$chain" 2>/dev/null || true
iptables -t nat -X "$chain" 2>/dev/null || true
done
for chain in XRAY_TPROXY_INPUT_ALLOW XRAY_LAN_FORWARD_ALLOW XRAY_TPROXY_INPUT_GUARD XRAY_TPROXY_OUTPUT_GUARD XRAY_TPROXY_GUARD; do
while iptables -D INPUT -j "$chain" 2>/dev/null; do :; done
while iptables -D OUTPUT -j "$chain" 2>/dev/null; do :; done
while iptables -D FORWARD -j "$chain" 2>/dev/null; do :; done
iptables -F "$chain" 2>/dev/null || true
iptables -X "$chain" 2>/dev/null || true
done
if command -v ip6tables >/dev/null 2>&1; then
for chain in XRAY_TPROXY6; do
while ip6tables -t mangle -D PREROUTING -i tailscale0 -j "$chain" 2>/dev/null; do :; done
ip6tables -t mangle -F "$chain" 2>/dev/null || true
ip6tables -t mangle -X "$chain" 2>/dev/null || true
done
fi
$NFT delete table inet xray_tproxy 2>/dev/null || true
$NFT delete table inet lan_xray_tproxy_test 2>/dev/null || true
$NFT delete table inet tproxy_probe 2>/dev/null || true
ip netns del tpxns 2>/dev/null || true
rm -f /tmp/tproxy_probe.py /tmp/tproxy_probe.log /tmp/tproxy_probe.pid 2>/dev/null || true
while ip rule del fwmark "$MARK_HEX"/"$MARK_HEX" table "$TABLE" 2>/dev/null; do :; done
while ip rule del fwmark 0x1 table "$TABLE" 2>/dev/null; do :; done
while ip rule del fwmark 0x1/0x1 table "$TABLE" 2>/dev/null; do :; done
while ip -6 rule del fwmark "$MARK_HEX"/"$MARK_HEX" table "$TABLE" 2>/dev/null; do :; done
while ip -6 rule del fwmark 0x1 table "$TABLE" 2>/dev/null; do :; done
while ip -6 rule del fwmark 0x1/0x1 table "$TABLE" 2>/dev/null; do :; done
ip rule add pref 5206 fwmark "$MARK_HEX"/"$MARK_HEX" table "$TABLE"
ip -6 rule add pref 5206 fwmark "$MARK_HEX"/"$MARK_HEX" table "$TABLE"
ip route replace local 0.0.0.0/0 dev lo table "$TABLE"
ip -6 route replace local ::/0 dev lo table "$TABLE"
cat > "$NFT_FILE" <<NFT_RULES
table inet xray_tproxy {
chain prerouting {
type filter hook prerouting priority mangle; policy accept;
iifname "tailscale0" jump proxy4
iifname "$LAN_IFACE" ip saddr $LAN_CIDR jump proxy4
iifname "tailscale0" meta nfproto ipv6 jump proxy6
}
chain proxy4 {
ip daddr { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/3 } return
meta l4proto tcp tproxy ip to $TPROXY_IP:$TPROXY_TCP_PORT meta mark set $MARK_HEX accept
meta l4proto udp tproxy ip to $TPROXY_IP:$TPROXY_UDP_PORT meta mark set $MARK_HEX accept
}
chain proxy6 {
ip6 daddr { ::/128, ::1/128, fc00::/7, fe80::/10, ff00::/8 } return
reject with icmpv6 type admin-prohibited
}
chain input {
type filter hook input priority filter; policy drop;
iifname "lo" accept
ct state established,related accept
ct state invalid drop
meta mark $MARK_HEX accept
iifname "tailscale0" accept
udp dport { 41641, 41642 } accept
tcp dport 22 accept
ip protocol icmp accept
icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, echo-request, echo-reply, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept
udp sport 67 udp dport 68 accept
ip daddr 224.0.0.251 udp dport 5353 accept
ip daddr 239.255.255.250 udp dport 1900 accept
ip saddr 192.168.0.0/16 accept
ip saddr 100.64.0.0/10 accept
}
chain forward {
type filter hook forward priority filter; policy drop;
ct state established,related accept
ct state invalid drop
iifname "$LAN_IFACE" oifname "$LAN_IFACE" ip saddr $LAN_CIDR accept
iifname "tailscale0" oifname "$LAN_IFACE" accept
iifname "$LAN_IFACE" oifname "tailscale0" accept
iifname "docker0" accept
oifname "docker0" ct state established,related accept
}
chain output {
type filter hook output priority filter; policy accept;
meta nfproto ipv4 accept
oifname "lo" accept
ct state established,related accept
ip6 daddr { ::/127, fc00::/7, fe80::/10, ff00::/8 } accept
udp sport 41642 accept
udp dport 41642 accept
udp dport 3478 accept
icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept
meta nfproto ipv6 reject with icmpv6 type admin-prohibited
}
}
NFT_RULES
$NFT -c -f "$NFT_FILE"
$NFT -f "$NFT_FILE"
```
### `/usr/local/sbin/xray-tproxy-cleanup`
```sh
#!/bin/sh
set -u
NFT=/usr/sbin/nft
TABLE=100
$NFT delete table inet xray_tproxy 2>/dev/null || true
while ip rule del fwmark 0x100/0x100 table "$TABLE" 2>/dev/null; do :; done
while ip rule del fwmark 0x1/0x1 table "$TABLE" 2>/dev/null; do :; done
while ip rule del fwmark 0x1 table "$TABLE" 2>/dev/null; do :; done
while ip -6 rule del fwmark 0x100/0x100 table "$TABLE" 2>/dev/null; do :; done
while ip -6 rule del fwmark 0x1/0x1 table "$TABLE" 2>/dev/null; do :; done
while ip -6 rule del fwmark 0x1 table "$TABLE" 2>/dev/null; do :; done
ip route flush table "$TABLE" 2>/dev/null || true
ip -6 route flush table "$TABLE" 2>/dev/null || true
```
### `/usr/local/sbin/xray-update-geo-assets`
```sh
#!/bin/sh
set -eu
ASSET_DIR="${XRAY_LOCATION_ASSET:-/usr/local/share/xray}"
XRAY_BIN="${XRAY_BIN:-/usr/local/bin/xray}"
XRAY_CONFIG="${XRAY_CONFIG:-/etc/xray/config.json}"
BASE_URL="https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download"
LOCK_FILE="/run/xray-update-geo-assets.lock"
log() {
printf "%s %s\n" "$(date '+%Y-%m-%d %H:%M:%S%z')" "$*"
}
if [ "$(id -u)" -ne 0 ]; then
log "must run as root"
exit 1
fi
if command -v flock >/dev/null 2>&1; then
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
log "another update is already running"
exit 0
fi
fi
need_cmd() {
command -v "$1" >/dev/null 2>&1 || {
log "missing command: $1"
exit 1
}
}
need_cmd curl
need_cmd install
need_cmd sha256sum
need_cmd systemctl
[ -x "$XRAY_BIN" ] || { log "xray binary not executable: $XRAY_BIN"; exit 1; }
[ -f "$XRAY_CONFIG" ] || { log "xray config not found: $XRAY_CONFIG"; exit 1; }
install -d -m 0755 "$ASSET_DIR"
tmpdir=$(mktemp -d)
cleanup() {
rm -rf "$tmpdir"
}
trap cleanup EXIT INT TERM
fetch_asset() {
name="$1"
url="$BASE_URL/$name"
log "downloading $url"
curl -fsSL --connect-timeout 20 --retry 4 --retry-delay 5 --speed-limit 1024 --speed-time 60 -o "$tmpdir/$name" "$url"
[ -s "$tmpdir/$name" ] || { log "downloaded file is empty: $name"; exit 1; }
}
fetch_asset geosite.dat
fetch_asset geoip.dat
if [ -f "$ASSET_DIR/geosite.dat" ] && [ -f "$ASSET_DIR/geoip.dat" ] \
&& cmp -s "$tmpdir/geosite.dat" "$ASSET_DIR/geosite.dat" \
&& cmp -s "$tmpdir/geoip.dat" "$ASSET_DIR/geoip.dat"; then
log "geo assets already up to date"
sha256sum "$ASSET_DIR/geosite.dat" "$ASSET_DIR/geoip.dat"
exit 0
fi
ts=$(date +%Y%m%d-%H%M%S)
geosite_bak="$ASSET_DIR/geosite.dat.bak.$ts"
geoip_bak="$ASSET_DIR/geoip.dat.bak.$ts"
restore_assets() {
[ -f "$geosite_bak" ] && cp -a "$geosite_bak" "$ASSET_DIR/geosite.dat"
[ -f "$geoip_bak" ] && cp -a "$geoip_bak" "$ASSET_DIR/geoip.dat"
}
[ -f "$ASSET_DIR/geosite.dat" ] && cp -a "$ASSET_DIR/geosite.dat" "$geosite_bak"
[ -f "$ASSET_DIR/geoip.dat" ] && cp -a "$ASSET_DIR/geoip.dat" "$geoip_bak"
install -o root -g root -m 0644 "$tmpdir/geosite.dat" "$ASSET_DIR/geosite.dat"
install -o root -g root -m 0644 "$tmpdir/geoip.dat" "$ASSET_DIR/geoip.dat"
export XRAY_LOCATION_ASSET="$ASSET_DIR"
if ! "$XRAY_BIN" run -test -config "$XRAY_CONFIG"; then
log "xray config test failed after geo update; restoring backups"
restore_assets
exit 1
fi
if systemctl is-active --quiet xray; then
if ! systemctl restart xray; then
log "xray restart failed; restoring backups"
restore_assets
systemctl restart xray || true
exit 1
fi
fi
find "$ASSET_DIR" -name "geosite.dat.bak.*" -type f -mtime +90 -delete 2>/dev/null || true
find "$ASSET_DIR" -name "geoip.dat.bak.*" -type f -mtime +90 -delete 2>/dev/null || true
log "geo assets updated"
sha256sum "$ASSET_DIR/geosite.dat" "$ASSET_DIR/geoip.dat"
log "backups: $geosite_bak $geoip_bak"
```
### systemd 单元
`/etc/systemd/system/xray-tproxy.service`:
```ini
[Unit]
Description=Xray iptables TPROXY rules for Tailscale exit traffic
After=network-online.target tailscaled.service xray.service
Wants=network-online.target tailscaled.service
Requires=xray.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/xray-tproxy-setup
ExecStop=/usr/local/sbin/xray-tproxy-cleanup
[Install]
WantedBy=multi-user.target
```
`/etc/systemd/system/xray.service`:
```ini
[Unit]
Description=Xray proxy service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
Group=xray_tproxy
Environment=XRAY_LOCATION_ASSET=/usr/local/share/xray
ExecStart=/usr/local/bin/xray run -config /etc/xray/config.json
Restart=on-failure
RestartSec=3
LimitNOFILE=1048576
[Install]
WantedBy=multi-user.target
```
`/etc/systemd/system/xray-geo-update.service`:
```ini
[Unit]
Description=Update Xray geosite and geoip data from Loyalsoldier
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
Environment=XRAY_LOCATION_ASSET=/usr/local/share/xray
ExecStart=/usr/local/sbin/xray-update-geo-assets
```
`/etc/systemd/system/xray-geo-update.timer`:
```ini
[Unit]
Description=Weekly Xray geosite and geoip data update
[Timer]
OnCalendar=Sun *-*-* 04:00:00
AccuracySec=1m
Persistent=true
Unit=xray-geo-update.service
[Install]
WantedBy=timers.target
```
### `/etc/xray/config.json` 脱敏版
出口代理细节已经脱敏,所以这个 JSON 只适合看结构和规则顺序。
```json
{
"log": {
"loglevel": "warning",
"access": "/var/log/xray/access.log",
"error": "/var/log/xray/error.log"
},
"policy": {
"levels": {
"0": {
"statsUserUplink": false,
"statsUserDownlink": false
}
}
},
"inbounds": [
{
"port": 10808,
"protocol": "socks",
"sniffing": {
"enabled": true,
"routeOnly": true,
"domainsExcluded": [
"courier.push.apple.com",
"rbsxbxp-mim.vivox.com",
"rbsxbxp.www.vivox.com",
"rbsxbxp-ws.vivox.com",
"rbspsxp.www.vivox.com",
"rbspsxp-mim.vivox.com",
"rbspsxp-ws.vivox.com",
"rbswxp.www.vivox.com",
"rbswxp-mim.vivox.com",
"disp-rbspsp-5-1.vivox.com",
"disp-rbsxbp-5-1.vivox.com",
"proxy.rbsxbp.vivox.com",
"proxy.rbspsp.vivox.com",
"proxy.rbswp.vivox.com",
"rbswp.vivox.com",
"rbsxbp.vivox.com",
"rbspsp.vivox.com",
"rbspsp.www.vivox.com",
"rbswp.www.vivox.com",
"rbsxbp.www.vivox.com",
"rbsxbxp.vivox.com",
"rbspsxp.vivox.com",
"rbswxp.vivox.com",
"Mijia Cloud",
"dlg.io.mi.com",
"marscdn.c2c.wechat.com",
"domain:argotunnel.com",
"domain:cftunnel.com",
"domain:cloudflareaccess.com",
"domain:cloudflareresearch.com",
"full:api.cloudflare.com"
],
"destOverride": [
"http",
"tls",
"quic"
]
},
"settings": {
"udp": true,
"auth": "noauth"
},
"listen": "0.0.0.0",
"tag": "socks-in"
},
{
"tag": "tproxy-in",
"port": 1041,
"protocol": "tunnel",
"settings": {
"allowedNetwork": "tcp,udp",
"followRedirect": true
},
"sniffing": {
"enabled": true,
"routeOnly": true,
"domainsExcluded": [
"domain:argotunnel.com",
"domain:cftunnel.com",
"domain:cloudflareaccess.com",
"domain:cloudflareresearch.com",
"full:api.cloudflare.com"
],
"destOverride": [
"http",
"tls",
"quic"
]
},
"streamSettings": {
"sockopt": {
"tproxy": "tproxy"
}
},
"listen": "127.0.0.1"
},
{
"tag": "udp-redir",
"listen": "127.0.0.1",
"port": 1051,
"protocol": "dokodemo-door",
"settings": {
"network": "udp",
"followRedirect": true
},
"sniffing": {
"enabled": true,
"routeOnly": true,
"domainsExcluded": [
"domain:argotunnel.com",
"domain:cftunnel.com",
"domain:cloudflareaccess.com",
"domain:cloudflareresearch.com",
"full:api.cloudflare.com"
],
"destOverride": [
"quic"
]
},
"streamSettings": {
"sockopt": {
"tproxy": "tproxy"
}
}
},
{
"tag": "dns-in",
"listen": "192.168.51.72",
"port": 53,
"protocol": "dokodemo-door",
"settings": {
"address": "1.1.1.1",
"port": 53,
"network": "tcp,udp"
}
}
],
"outbounds": [
{
"mux": {
"enabled": false
},
"protocol": "vless",
"settings": {
"vnext": [
{
"port": "REDACTED",
"users": [
{
"encryption": "REDACTED",
"id": "REDACTED",
"level": 0,
"flow": "REDACTED"
}
],
"address": "REDACTED"
}
]
},
"streamSettings": "REDACTED",
"tag": "default:vless-enc-openwrt"
},
{
"streamSettings": {
"sockopt": {}
},
"settings": {
"domainStrategy": "UseIP"
},
"protocol": "freedom",
"tag": "direct"
},
{
"protocol": "blackhole",
"tag": "blackhole"
},
{
"mux": {
"enabled": false
},
"protocol": "vless",
"settings": {
"vnext": [
{
"port": "REDACTED",
"users": [
{
"encryption": "REDACTED",
"id": "REDACTED",
"level": 0,
"flow": "REDACTED"
}
],
"address": "REDACTED"
}
]
},
"streamSettings": "REDACTED",
"tag": "pNeL0qJ1:vless-enc-openwrt"
},
{
"tag": "dns-out",
"protocol": "dns",
"settings": {}
}
],
"routing": {
"rules": [
{
"type": "field",
"inboundTag": [
"dns-in"
],
"outboundTag": "dns-out"
},
{
"type": "field",
"port": 53,
"network": "tcp,udp",
"outboundTag": "dns-out"
},
{
"type": "field",
"inboundTag": [
"dns-local"
],
"outboundTag": "direct"
},
{
"type": "field",
"domain": [
"geosite:category-games@cn"
],
"outboundTag": "direct"
},
{
"type": "field",
"domain": [
"geosite:category-ads-all"
],
"outboundTag": "blackhole"
},
{
"type": "field",
"ip": [
"geoip:private"
],
"outboundTag": "direct"
},
{
"type": "field",
"ip": [
"geoip:cn"
],
"outboundTag": "direct"
},
{
"type": "field",
"domain": [
"geosite:cn"
],
"outboundTag": "direct"
},
{
"type": "field",
"port": 123,
"network": "udp",
"outboundTag": "direct"
}
],
"domainStrategy": "IPIfNonMatch",
"domainMatcher": "mph"
},
"dns": {
"servers": [
"localhost"
],
"queryStrategy": "UseIPv4",
"useSystemHosts": true,
"tag": "dns-local"
}
}
```
### nft 当前完整规则快照
导出命令是 `sudo nft list ruleset`。这里包含 Docker、Tailscale 自动生成的表,也包含我们手工维护的 `inet xray_tproxy` 表。
```nft
# Warning: table ip filter is managed by iptables-nft, do not touch!
# Warning: table ip nat is managed by iptables-nft, do not touch!
# Warning: table ip6 filter is managed by iptables-nft, do not touch!
# Warning: table ip6 nat is managed by iptables-nft, do not touch!
# Warning: XT target MASQUERADE not found
# Warning: table ip mangle is managed by iptables-nft, do not touch!
# Warning: table ip6 mangle is managed by iptables-nft, do not touch!
table ip filter {
chain DOCKER {
iifname != "docker0" oifname "docker0" counter packets 0 bytes 0 drop
}
chain DOCKER-FORWARD {
counter packets 9 bytes 528 jump DOCKER-CT
counter packets 9 bytes 528 jump DOCKER-INTERNAL
counter packets 9 bytes 528 jump DOCKER-BRIDGE
iifname "docker0" counter packets 0 bytes 0 accept
}
chain DOCKER-BRIDGE {
oifname "docker0" counter packets 0 bytes 0 jump DOCKER
}
chain DOCKER-CT {
oifname "docker0" ct state related,established counter packets 0 bytes 0 accept
}
chain DOCKER-INTERNAL {
}
chain FORWARD {
type filter hook forward priority filter; policy accept;
counter packets 7439 bytes 4847002 jump ts-forward
counter packets 9 bytes 528 jump DOCKER-USER
counter packets 9 bytes 528 jump DOCKER-FORWARD
}
chain DOCKER-USER {
}
chain INPUT {
type filter hook input priority filter; policy accept;
counter packets 105296 bytes 148263055 jump ts-input
}
chain OUTPUT {
type filter hook output priority filter; policy accept;
}
chain ts-input {
ip saddr 100.76.137.29 iifname "lo" counter packets 0 bytes 0 accept
iifname "tailscale0" counter packets 3121 bytes 441272 accept
udp dport 41642 counter packets 5252 bytes 1309884 accept
ip saddr 100.115.92.0/23 iifname != "tailscale0" counter packets 0 bytes 0 return
ip saddr 100.64.0.0/10 iifname != "tailscale0" counter packets 0 bytes 0 drop
}
chain ts-forward {
iifname "tailscale0" counter packets 4512 bytes 345211 meta mark set mark and 0xff00ffff xor 0x40000
meta mark & 0x00ff0000 == 0x00040000 counter packets 4512 bytes 345211 accept
ip saddr 100.64.0.0/10 oifname "tailscale0" counter packets 0 bytes 0 drop
oifname "tailscale0" counter packets 2927 bytes 4501791 accept
}
}
table ip nat {
chain DOCKER {
}
chain PREROUTING {
type nat hook prerouting priority dstnat; policy accept;
fib daddr type local counter packets 3263419 bytes 203092044 jump DOCKER
}
chain OUTPUT {
type nat hook output priority dstnat; policy accept;
ip daddr != 127.0.0.0/8 fib daddr type local counter packets 404 bytes 19672 jump DOCKER
}
chain POSTROUTING {
type nat hook postrouting priority srcnat; policy accept;
counter packets 6101 bytes 421744 jump ts-postrouting
ip saddr 172.17.0.0/16 oifname != "docker0" counter packets 0 bytes 0 masquerade
}
chain ts-postrouting {
meta mark & 0x00ff0000 == 0x00040000 counter packets 217 bytes 13020 masquerade
}
}
table ip6 filter {
chain DOCKER {
}
chain DOCKER-FORWARD {
counter packets 0 bytes 0 jump DOCKER-CT
counter packets 0 bytes 0 jump DOCKER-INTERNAL
counter packets 0 bytes 0 jump DOCKER-BRIDGE
}
chain DOCKER-BRIDGE {
}
chain DOCKER-CT {
}
chain DOCKER-INTERNAL {
}
chain FORWARD {
type filter hook forward priority filter; policy accept;
counter packets 0 bytes 0 jump ts-forward
counter packets 0 bytes 0 jump DOCKER-USER
counter packets 0 bytes 0 jump DOCKER-FORWARD
}
chain DOCKER-USER {
}
chain INPUT {
type filter hook input priority filter; policy accept;
counter packets 8448 bytes 1239723 jump ts-input
}
chain OUTPUT {
type filter hook output priority filter; policy accept;
}
chain ts-input {
ip6 saddr fd7a:115c:a1e0::6039:891d iifname "lo" counter packets 0 bytes 0 accept
iifname "tailscale0" counter packets 0 bytes 0 accept
udp dport 41642 counter packets 2707 bytes 485006 accept
}
chain ts-forward {
iifname "tailscale0" counter packets 0 bytes 0 meta mark set mark and 0xff00ffff xor 0x40000
meta mark & 0x00ff0000 == 0x00040000 counter packets 0 bytes 0 accept
oifname "tailscale0" counter packets 0 bytes 0 accept
}
}
table ip6 nat {
chain DOCKER {
}
chain PREROUTING {
type nat hook prerouting priority dstnat; policy accept;
fib daddr type local counter packets 140 bytes 21210 jump DOCKER
}
chain OUTPUT {
type nat hook output priority dstnat; policy accept;
ip6 daddr != ::1 fib daddr type local counter packets 18 bytes 7146 jump DOCKER
}
chain POSTROUTING {
type nat hook postrouting priority srcnat; policy accept;
counter packets 1781 bytes 167791 jump ts-postrouting
}
chain ts-postrouting {
meta mark & 0x00ff0000 == 0x00040000 counter packets 0 bytes 0 xt target "MASQUERADE"
}
}
table ip mangle {
chain PREROUTING {
type filter hook prerouting priority mangle; policy accept;
ct state related,established counter packets 80604 bytes 141020574 meta mark set ct mark and 0xff0000
}
chain OUTPUT {
type route hook output priority mangle; policy accept;
ct state new meta mark & 0x00ff0000 != 0x00000000 counter packets 19903 bytes 1275821 ct mark set mark and 0xff0000
}
}
table ip6 mangle {
chain PREROUTING {
type filter hook prerouting priority mangle; policy accept;
ct state related,established counter packets 7332 bytes 1120179 meta mark set ct mark and 0xff0000
}
chain OUTPUT {
type route hook output priority mangle; policy accept;
ct state new meta mark & 0x00ff0000 != 0x00000000 counter packets 5669 bytes 475008 ct mark set mark and 0xff0000
}
}
table ip raw {
chain PREROUTING {
type filter hook prerouting priority raw; policy accept;
}
}
table inet xray_tproxy {
chain prerouting {
type filter hook prerouting priority mangle; policy accept;
iifname "tailscale0" jump proxy4
iifname "eth0" ip saddr 192.168.51.0/24 jump proxy4
iifname "tailscale0" meta nfproto ipv6 jump proxy6
}
chain proxy4 {
ip daddr { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/3 } return
meta l4proto tcp tproxy ip to 127.0.0.1:1041 meta mark set 0x00000100 accept
meta l4proto udp tproxy ip to 127.0.0.1:1051 meta mark set 0x00000100 accept
}
chain proxy6 {
ip6 daddr { ::/127, fc00::/7, fe80::/10, ff00::/8 } return
reject with icmpv6 admin-prohibited
}
chain input {
type filter hook input priority filter; policy drop;
iifname "lo" accept
ct state established,related accept
ct state invalid drop
meta mark 0x00000100 accept
iifname "tailscale0" accept
udp dport { 41641, 41642 } accept
tcp dport 22 accept
ip protocol icmp accept
icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, echo-request, echo-reply, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept
udp sport 67 udp dport 68 accept
ip daddr 224.0.0.251 udp dport 5353 accept
ip daddr 239.255.255.250 udp dport 1900 accept
ip saddr 192.168.0.0/16 accept
ip saddr 100.64.0.0/10 accept
}
chain forward {
type filter hook forward priority filter; policy drop;
ct state established,related accept
ct state invalid drop
iifname "eth0" oifname "eth0" ip saddr 192.168.51.0/24 accept
iifname "tailscale0" oifname "eth0" accept
iifname "eth0" oifname "tailscale0" accept
iifname "docker0" accept
oifname "docker0" ct state established,related accept
}
chain output {
type filter hook output priority filter; policy accept;
meta nfproto ipv4 accept
oifname "lo" accept
ct state established,related accept
ip6 daddr { ::/127, fc00::/7, fe80::/10, ff00::/8 } accept
udp sport 41642 accept
udp dport 41642 accept
udp dport 3478 accept
icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept
reject with icmpv6 admin-prohibited
}
}
```
### Tailscale 配置
`/etc/default/tailscaled`:
```sh
# Set the port to listen on for incoming VPN packets.
# Remote nodes will automatically be informed about the new port number,
# but you might want to configure this in order to set external firewall
# settings.
PORT=41642
# Extra flags you might want to pass to tailscaled.
FLAGS=""
```
`/usr/lib/systemd/system/tailscaled.service`:
```ini
[Unit]
Description=Tailscale node agent
Documentation=https://tailscale.com/docs/
Wants=network-pre.target
After=network-pre.target NetworkManager.service systemd-resolved.service
[Service]
EnvironmentFile=/etc/default/tailscaled
ExecStart=/usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port=${PORT} $FLAGS
ExecStopPost=/usr/sbin/tailscaled --cleanup
Restart=on-failure
RuntimeDirectory=tailscale
RuntimeDirectoryMode=0755
StateDirectory=tailscale
StateDirectoryMode=0700
CacheDirectory=tailscale
CacheDirectoryMode=0750
Type=notify
[Install]
WantedBy=multi-user.target
```
`tailscale debug prefs` 脱敏版:
```json
{
"ControlURL": "https://controlplane.tailscale.com",
"RouteAll": false,
"ExitNodeID": "",
"ExitNodeIP": "",
"InternalExitNodePrior": "",
"ExitNodeAllowLANAccess": false,
"CorpDNS": true,
"RunSSH": false,
"RunWebClient": false,
"WantRunning": true,
"LoggedOut": false,
"ShieldsUp": false,
"AdvertiseTags": null,
"Hostname": "REDACTED",
"NotepadURLs": false,
"AdvertiseRoutes": [
"0.0.0.0/0",
"::/0",
"192.168.51.0/24"
],
"AdvertiseServices": null,
"Sync": null,
"NoSNAT": false,
"NoStatefulFiltering": true,
"NetfilterMode": 2,
"AutoUpdate": {
"Check": true,
"Apply": true
},
"AppConnector": {
"Advertise": false
},
"PostureChecking": false,
"NetfilterKind": "",
"DriveShares": null,
"AllowSingleHosts": true,
"Config": {
"PrivateNodeKey": "REDACTED",
"OldPrivateNodeKey": "REDACTED",
"UserProfile": "REDACTED",
"NetworkLockKey": "REDACTED",
"NodeID": "REDACTED"
}
}
```
文章评论