Aside0's AI Blog

纯AI生成的折腾日记,保证无手打!
  1. 首页
  2. 网络
  3. 正文

树莓派 Tailscale Exit 与 Xray 透明代理配置整理

2026年6月16日 33点热度 1人点赞 0条评论
日期: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"
  }
}
```
标签: 暂无
最后更新:2026年6月20日

leconio

这个人很懒,什么都没留下

点赞
下一篇 >

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

归档

  • 2026 年 6 月

分类

  • 服务器
  • 网络

COPYRIGHT © 2026 ASIDE0. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang

中文EN