日期:2026-06-19
公开版说明:本文保留配置结构、排查路径和关键判断,内网地址、客户端标识、Xray 出站 tag、Wi-Fi 密码等环境信息已替换为示例值。
这份文档记录的是一次很具体的树莓派网络改造:把一台树莓派当成 Wi-Fi 热点,让手机、平板或其他设备连上这个热点后,普通网站按原来的国内外规则分流,需要代理的网站走树莓派上的 Xray。
这不是“把热点开起来就算完”的教程。真正的问题有三个:
```text
NetworkManager 能开热点,但无线协商速率很低,5GHz 也只跑在 20 MHz
热点客户端的 TCP/UDP 流量要进 Xray 透明代理,而不是从 eth0 直连泄漏
客户端 DNS 不能被本地网络污染,也不能直接吃 Xray DNS 入口的 REFUSED
```
最后能稳定工作的方案是:
```text
eth0 继续作为树莓派上联网口和管理口
wlan0 用 hostapd 开 5GHz 802.11ac 80 MHz 热点
热点网段使用 10.42.0.0/24
dnsmasq 给热点客户端发 DHCP,并监听 10.42.0.1:53
dnsmasq 的上游不是公网直连 DNS,而是本机 127.0.0.1:5353
127.0.0.1:5353 通过 Xray SOCKS 出口访问 Cloudflare DoH
客户端普通 TCP/UDP 流量用 nftables tproxy 送进 Xray
UDP/443 在进入 Xray 前快速 reject,让客户端尽快回退 TCP
```
这里说的“接近 500M”指 Wi-Fi 协商速率,不是保证实际下载吞吐。树莓派板载 Wi-Fi 是单流 802.11ac,80 MHz 下常见最高协商值是 `433.3 MBit/s`。手机或系统界面有时会把它显示成接近 500M 的连接档位,但真实 TCP 下载速度还会受信号、客户端天线、CPU、代理出口和上游带宽影响。
## 相关折腾记录
- [树莓派 Tailscale Exit 与 Xray 透明代理配置整理](https://aside0.me/raspberry-pi-tailscale-xray-2026-06-15/)
- [Cloudflare Tunnel 通过 Xray 出口:cloudflared、nftables 和 sniffing 排查](https://aside0.me/cloudflared-tunnel-xray-sniff-nft-2026-06-17/)
- [树莓派 Xray DNS 防泄露和游戏下载分流](https://aside0.me/raspberry-pi-xray-dns-leak-game-split-2026-06-20/)
## 系统和环境
这台机器是树莓派,不是 OpenWrt。
```text
硬件:Raspberry Pi 5
系统:Debian GNU/Linux 13 (trixie)
网络管理:NetworkManager
无线热点:hostapd
DHCP/DNS:dnsmasq
防火墙:nftables
代理:Xray
上联网口:eth0
热点网口:wlan0
树莓派 LAN 地址:192.168.10.20
热点网关地址:10.42.0.1
热点网段:10.42.0.0/24
```
Xray 这边已有透明代理入站:
```text
Xray SOCKS:0.0.0.0:10808
Xray TCP TProxy:127.0.0.1:1041
Xray UDP TProxy:127.0.0.1:1051
Xray DNS 入站:192.168.10.20:53
```
本文只写 Wi-Fi 热点和热点客户端这条链路。树莓派上已有的 Tailscale exit、cloudflared、普通旁路由规则不重复展开。
## 最终拓扑
最终路径可以拆成两条:一条是 DNS,一条是普通流量。
DNS 走这条链路:
```mermaid
flowchart LR
phone["热点客户端<br/>手机 / 平板 / 电脑"]
ap["wlan0<br/>RasPi-Hotspot"]
dnsmasq["dnsmasq<br/>10.42.0.1:53"]
doh_local["本机 DoH 转发器<br/>127.0.0.1:5353"]
socks["Xray SOCKS<br/>127.0.0.1:10808"]
proxy["Xray 默认代理出站"]
cf["Cloudflare DoH<br/>cloudflare-dns.com"]
phone --> ap
ap --> dnsmasq
dnsmasq --> doh_local
doh_local --> socks
socks --> proxy
proxy --> cf
```
普通 TCP/UDP 走这条链路:
```mermaid
flowchart LR
phone["热点客户端<br/>10.42.0.0/24"]
wlan["wlan0"]
nft["nftables<br/>xray_tproxy"]
tcp_in["Xray TCP TProxy<br/>127.0.0.1:1041"]
udp_in["Xray UDP TProxy<br/>127.0.0.1:1051"]
route["Xray routing<br/>direct / proxy / blackhole"]
eth["eth0"]
internet["Internet"]
phone --> wlan
wlan --> nft
nft --> tcp_in
nft --> udp_in
tcp_in --> route
udp_in --> route
route --> eth
eth --> internet
```
UDP/443 单独提前处理:
```mermaid
flowchart TD
client["热点客户端发 UDP/443<br/>QUIC / HTTP3"]
reject["nftables 快速 reject"]
fallback["客户端回退 TCP/443"]
tproxy["TCP/443 进入 Xray TProxy"]
client --> reject
reject --> fallback
fallback --> tproxy
```
这里不是让 QUIC 通过代理,而是明确禁用 UDP/443,并且不要用黑洞拖到超时。对手机 App 来说,快速收到 unreachable 往往比静默丢包更快回退。
## 为什么不用 NetworkManager 热点
NetworkManager 的热点模式确实能把 `wlan0` 开起来,也能发 DHCP。但这次实测它有一个问题:即使切到 5GHz,最终无线宽度仍然停在 20 MHz,协商速率很低。
当时能看到类似状态:
```text
channel 149 (5745 MHz), width: 20 MHz
tx bitrate: 78.0 MBit/s
rx bitrate: 43.3 MBit/s
```
在 2.4GHz 时更差,甚至出现过个位数 Mbit/s 的协商:
```text
tx bitrate: 6.5 MBit/s
rx bitrate: 5.5 MBit/s
```
这种状态下,即使 Xray 配得再好,用户体感也会像网络坏了。最后改成直接用 `hostapd` 控制 AP 参数,明确开启 802.11ac 和 80 MHz。
## hostapd 开 5GHz 80 MHz 热点
热点配置示例:
```ini
country_code=CN
ieee80211d=1
interface=wlan0
driver=nl80211
ssid=RasPi-Hotspot
hw_mode=a
channel=149
ieee80211n=1
ieee80211ac=1
wmm_enabled=1
ht_capab=[HT40+][SHORT-GI-20][SHORT-GI-40]
vht_capab=[SHORT-GI-80]
vht_oper_chwidth=1
vht_oper_centr_freq_seg0_idx=155
beacon_int=100
dtim_period=2
auth_algs=1
wpa=2
wpa_key_mgmt=WPA-PSK
rsn_pairwise=CCMP
wpa_passphrase=<wifi-psk>
```
几个关键点:
- `hw_mode=a` 表示使用 5GHz。
- `channel=149` 是这次实测可用的 5GHz 信道。
- `ieee80211ac=1` 开启 802.11ac。
- `vht_oper_chwidth=1` 表示 80 MHz。
- `vht_oper_centr_freq_seg0_idx=155` 对应 channel 149 的 80 MHz 中心频段。
- `wmm_enabled=1` 不要省,很多 802.11n/ac 高速率依赖 WMM。
启动后验证:
```sh
iw dev wlan0 info
iw dev wlan0 station dump
```
期望看到:
```text
ssid RasPi-Hotspot
type AP
channel 149 (5745 MHz), width: 80 MHz, center1: 5775 MHz
```
客户端连接后,协商速率应该接近:
```text
tx bitrate: 433.3 MBit/s
rx bitrate: 260.0 MBit/s
```
`tx bitrate` 是 AP 发给客户端的方向,通常更接近客户端下载速度的上限。`rx bitrate` 是客户端发给 AP 的方向,和上传或客户端信号状态更相关。它们都是瞬时协商值,不是测速结果。
## dnsmasq 只做热点侧入口
热点侧仍然需要一个本地 DNS 和 DHCP。这里用 `dnsmasq`:
```ini
interface=wlan0
bind-interfaces
listen-address=10.42.0.1
dhcp-range=10.42.0.10,10.42.0.254,255.255.255.0,1h
dhcp-option=3,10.42.0.1
dhcp-option=6,10.42.0.1
no-resolv
server=127.0.0.1#5353
filter-rr=65
log-dhcp
```
`dhcp-option=3` 告诉客户端默认网关是 `10.42.0.1`。`dhcp-option=6` 告诉客户端 DNS 也是 `10.42.0.1`。
真正重要的是这两行:
```ini
server=127.0.0.1#5353
filter-rr=65
```
`server=127.0.0.1#5353` 表示 dnsmasq 不直接问公网 DNS,而是转给本机的 DoH 转发器。`filter-rr=65` 用来过滤 HTTPS 记录的具体 RR,减少客户端在 ECH、HTTP/3 和特殊 HTTPS 记录上走出不可控路径;即使有 CNAME,客户端仍然能继续拿 A 记录。
## 为什么不直接把 Xray DNS 给手机用
树莓派上本来就有 Xray DNS 入站,例如:
```text
192.168.10.20:53
```
第一反应很自然:既然 Xray 有 DNS 入口,为什么热点客户端不直接用它?
实测结果是不适合。Xray DNS 入口对普通 A 记录能返回,但对手机系统会问的一些记录类型不完整。例如 iOS 会反复问:
```text
SOA? .
HTTPS? example.com
```
当热点 DNS 直接沿用 Xray DNS 入口时,能看到类似结果:
```text
192.168.10.20 www.youtube.com type=1 rcode=0 answers=1
192.168.10.20 www.youtube.com type=65 rcode=5 answers=0
192.168.10.20 www.youtube.com type=6 rcode=5 answers=0
192.168.10.20 . type=6 timed out
```
`rcode=5` 是 `REFUSED`。这对 Xray 自己做路由解析未必是问题,但给手机当完整递归 DNS 就很容易出事。手机看到 DNS 层不断 REFUSED 或超时,会误判当前 Wi-Fi 网络不稳定,应用也可能卡在请求发出之前。
所以这里没有让客户端直接使用 Xray DNS 入口,而是:
```text
手机只认 dnsmasq
dnsmasq 负责热点客户端兼容性
dnsmasq 的上游再通过 Xray SOCKS 去问 DoH
```
Xray 继续负责代理出口,不强行把它的 DNS 入站当全功能 resolver。
## 直连公网 DNS 为什么也不行
还有一个看起来更简单的方案:
```text
dnsmasq -> 1.1.1.1 / 8.8.8.8
```
这个方案可以让 `SOA` 和 `HTTPS/type65` 不再 REFUSED,但它在当前网络里会遇到 DNS 污染。实测热点 DNS 直连公网 DNS 时,结果明显不对:
```text
www.google.com -> 104.244.42.197
www.youtube.com -> 69.171.235.22
youtubei.googleapis.com -> 162.125.32.10
telegram.org -> 162.125.32.2
```
这些地址不是正常的 Google、YouTube 或 Telegram 解析结果。客户端拿到这些脏 IP 后,就算后面的 TCP 流量被 nftables 送进 Xray,也是在代理一个错误目标,自然打不开。
把 DNS 查询本身也放进 Xray 出口后,解析恢复正常:
```text
www.google.com -> 142.251.x.x
www.youtube.com -> youtube-ui.l.google.com -> 142.250.x.x
youtubei.googleapis.com -> 216.239.32.223 / 216.239.34.223 / ...
telegram.org -> 149.154.167.99
chatgpt.com -> 104.18.32.47 / 172.64.155.209
```
这也是这次故障最关键的判断:不是 Xray 节点坏了,也不是热点流量完全没有进 Xray,而是客户端在进 Xray 之前已经拿到了污染的 DNS 结果。
## 本机 DoH 转发器
这次使用一个本机小转发器监听 `127.0.0.1:5353`,它收到标准 DNS UDP 查询后,通过 Xray SOCKS 连接 Cloudflare DoH:
```text
127.0.0.1:5353
-> SOCKS 127.0.0.1:10808
-> cloudflare-dns.com:443
```
systemd 服务结构如下:
```ini
[Unit]
Description=DNS-over-HTTPS forwarder through local Xray SOCKS
After=network-online.target xray.service
Wants=network-online.target
Requires=xray.service
[Service]
Type=simple
ExecStart=/usr/local/lib/raspi-hotspot/socks-doh-proxy.py --listen 127.0.0.1 --port 5353
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
[Install]
WantedBy=multi-user.target
```
这里不强依赖这个脚本本身。也可以换成 `dnscrypt-proxy`、`mosdns`、`smartdns` 或其他 DNS 组件,只要它满足两个条件:
```text
对下游表现为普通 DNS resolver
对上游访问 DoH / DoT / DNS 时能走本机 Xray SOCKS 或代理出口
```
最重要的是不要再让热点客户端的海外域名解析从本地网络直连出去。
## nftables 把热点客户端送进 Xray
热点客户端普通流量由 `nftables` 在 `prerouting` 里接住:
```nft
table inet xray_tproxy {
chain prerouting {
type filter hook prerouting priority mangle; policy accept;
iifname "wlan0" udp dport 443 reject comment "fast-reject-udp443"
iifname "wlan0" ip saddr 10.42.0.0/24 jump proxy4
}
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
}
}
```
对应的策略路由:
```sh
ip rule add fwmark 0x100/0x100 table 100
ip route add local default dev lo table 100
```
这两条缺一不可。`tproxy` 给包打上 mark 后,策略路由要把这些包送回本机 `lo`,Xray 才能在 `127.0.0.1:1041` 和 `127.0.0.1:1051` 收到。
UDP/443 的 reject 放在跳转 `proxy4` 之前:
```nft
iifname "wlan0" udp dport 443 reject comment "fast-reject-udp443"
```
这样 YouTube、Google、Apple、Bilibili 这类客户端尝试 QUIC 时,会很快收到失败,然后回退 TCP/443。之前如果把 UDP/443 交给 Xray 后再 blackhole,客户端可能会等超时,体感就是“很慢”或“打不开”。
## Xray 入站和路由要点
Xray 入站至少需要这几个:
```json
[
{
"tag": "socks-in",
"listen": "0.0.0.0",
"port": 10808,
"protocol": "socks",
"sniffing": {
"enabled": true,
"destOverride": ["http", "tls", "quic"],
"routeOnly": true
}
},
{
"tag": "tproxy-in",
"listen": "127.0.0.1",
"port": 1041,
"protocol": "tunnel",
"sniffing": {
"enabled": true,
"destOverride": ["http", "tls", "quic"],
"routeOnly": true
}
},
{
"tag": "udp-redir",
"listen": "127.0.0.1",
"port": 1051,
"protocol": "dokodemo-door",
"sniffing": {
"enabled": true,
"destOverride": ["quic"],
"routeOnly": true
}
}
]
```
`routeOnly: true` 是一个重要细节。它允许 Xray 从 TLS、HTTP 或 QUIC 里 sniff 出域名,用来做 `geosite` 路由判断,但不把原始目标强行改写掉。透明代理场景下,这能减少很多“sniff 到域名以后反而拨错目标”的问题。
路由策略按实际环境可以很复杂,但基本顺序是:
```text
DNS 流量先走 DNS 出站
UDP/443 禁用或提前 reject
私有地址直连
国内 IP / 国内域名直连
广告规则可 blackhole
剩余默认走代理出站
```
示意:
```json
{
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"type": "field",
"network": "tcp,udp",
"port": 53,
"outboundTag": "dns-out"
},
{
"type": "field",
"ip": ["geoip:private"],
"outboundTag": "direct"
},
{
"type": "field",
"ip": ["geoip:cn"],
"outboundTag": "direct"
},
{
"type": "field",
"domain": ["geosite:cn"],
"outboundTag": "direct"
}
]
}
}
```
默认出站的 tag、节点参数、UUID、Reality key、VLESS 配置都不应该写进公开文档。
## 验证无线速度
先看 AP 是否真在 80 MHz:
```sh
iw dev wlan0 info
```
期望:
```text
channel 149 (5745 MHz), width: 80 MHz, center1: 5775 MHz
```
再看客户端协商:
```sh
iw dev wlan0 station dump
```
期望:
```text
Station <client-mac> (on wlan0)
tx bitrate: 433.3 MBit/s
rx bitrate: 260.0 MBit/s
```
如果仍然只有几十 Mbit/s,先不要看 Xray。优先检查:
- 客户端是不是连到了 5GHz。
- hostapd 是否真的使用 80 MHz。
- 信道是否被国家码或驱动限制。
- 手机和树莓派距离是否太远。
- `wlan0` 是否被 NetworkManager 同时接管。
## 验证 DNS 没有污染
热点 DNS 应该从 `10.42.0.1` 查询:
```sh
dig @10.42.0.1 www.youtube.com A
dig @10.42.0.1 youtubei.googleapis.com A
dig @10.42.0.1 www.google.com A
dig @10.42.0.1 . SOA
```
期望看到正常结果:
```text
www.youtube.com CNAME youtube-ui.l.google.com -> 142.250.x.x
youtubei.googleapis.com 216.239.32.223 / 216.239.34.223 / ...
www.google.com 142.251.x.x
. SOA a.root-servers.net
```
不应该再看到这种明显污染结果:
```text
www.google.com -> 104.244.42.197
www.youtube.com -> 69.171.235.22
youtubei.googleapis.com -> 162.125.32.10
```
如果手机刚才已经拿过污染结果,需要关开 Wi-Fi 或飞行模式几秒,让系统清掉旧 DNS 缓存。
## 验证流量进 Xray
先从树莓派本机确认代理出口可用:
```sh
curl --socks5-hostname 127.0.0.1:10808 \
-4 -L -m 12 -o /dev/null -sS \
-w '%{http_code} %{time_starttransfer}\n' \
https://www.youtube.com/generate_204
```
正常应该返回:
```text
204 0.xxx
```
再看 Xray access log。热点客户端访问代理网站时,应该出现类似:
```text
from 10.42.0.20:53500 accepted tcp:142.250.197.238:443 [tproxy-in >> <proxy-outbound>]
from 10.42.0.20:53501 accepted tcp:216.239.34.223:443 [tproxy-in >> <proxy-outbound>]
```
如果看到的是:
```text
from 10.42.0.20 accepted tcp:<国内 IP>:443 [tproxy-in -> direct]
```
那说明流量进了 Xray,只是按国内规则直连。不要把它误判成“没有进代理”。
抓包可以看得更直接:
```sh
tcpdump -i wlan0 -nn 'net 10.42.0.0/24 and (udp port 53 or port 443)'
```
DNS 正常后,打开 YouTube 或其他 Google 服务时,应该能看到 `googlevideo.com`、`youtube-ui.l.google.com` 或对应的 Google IP,Xray access log 里也应该出现 `tproxy-in >> <proxy-outbound>`。
## 这次踩到的坑
第一个坑是把“热点能开”当成“热点速度正常”。NetworkManager 开热点很方便,但这次没有把 `wlan0` 推到 80 MHz,导致协商速率一直很低。对 5GHz 802.11ac 热点来说,要看 `iw dev wlan0 info` 里的 `width: 80 MHz`,不能只看 SSID 和信道。
第二个坑是把“DNS 能返回”当成“DNS 没问题”。直连 `1.1.1.1` 或 `8.8.8.8` 可以让 `SOA` 不再 REFUSED,但在当前网络里会被污染。客户端拿到错误 IP 后,后面再透明代理也没用。
第三个坑是把 Xray DNS 入口直接发给手机。Xray DNS 对 Xray 自己做路由解析没问题,但它不一定适合作为手机的完整递归 resolver。iOS 会问 `SOA .`、`HTTPS/type65`,这些查询如果被 REFUSED 或超时,手机网络栈会变得很奇怪。
第四个坑是 UDP/443 用黑洞。禁用 QUIC 没问题,但黑洞会让客户端等超时。用 nft 在入口快速 reject,客户端更容易回退到 TCP/443。
第五个坑是热点里有多个客户端。排查时要先确认当前手机到底是哪个 `10.42.0.x`,否则会把另一个设备的 Xray 日志当成自己的访问结果。
## 回滚
如果要临时关闭热点:
```sh
sudo kill "$(cat /run/raspi-hostapd-test.pid)"
sudo kill "$(cat /run/raspi-dnsmasq-test.pid)"
sudo nmcli dev set wlan0 managed yes
sudo systemctl restart NetworkManager
```
如果要停掉本机 DoH 转发器:
```sh
sudo systemctl disable --now socks-doh-dns.service
```
如果要删除临时 UDP/443 快速拒绝规则,先看 handle:
```sh
sudo nft -a list chain inet xray_tproxy prerouting
```
再按 handle 删除:
```sh
sudo nft delete rule inet xray_tproxy prerouting handle <handle>
```
如果要让热点 DNS 回到直连公网 DNS,可以把 dnsmasq 改回:
```ini
server=1.1.1.1
server=8.8.8.8
```
但在这次环境里不推荐这么做,因为已经实测存在 DNS 污染。更稳的回滚方式是停掉热点,而不是把客户端重新丢回污染 DNS。
## 最终判断
这次最后的问题不是 Xray 节点本身不可用。树莓派本机通过 Xray SOCKS 访问 Google、YouTube、X、Facebook、Telegram 都能返回,说明代理出口是通的。
真正的故障点在热点客户端侧:
```text
无线层:NetworkManager 热点没有跑到 80 MHz,速率太低
DNS 层:直连公网 DNS 被污染,Xray DNS 入口又不适合直接给手机当 resolver
传输层:UDP/443 黑洞会拖慢客户端回退
```
把这三层分开处理后,链路就清楚了:
```text
hostapd 负责把 wlan0 推到 5GHz 80 MHz
dnsmasq 负责热点客户端 DHCP/DNS 兼容性
本机 DoH 转发器负责让 DNS 查询走 Xray 出口
nftables 负责把热点客户端普通流量送进 Xray
Xray 负责国内外分流和默认代理出站
```
这样树莓派就不只是“开了一个 Wi-Fi”,而是变成了一个带 DNS 防污染、Xray 透明代理和接近 500M 协商速率的 Wi-Fi 热点。
文章评论