手机当服务器用,最难受的从来不是性能,是地址。我用 Termux 在一台安卓机上跑常驻服务快两年了,真正把大多数人劝退的环节只有一个:手机重启一次、切一次 Wi-Fi、基站换一个,SSH 就直接连不上。这篇内容要解决的就是这条链路——怎么在 Termux 里把 IPv6 公网地址挖出来、怎么确认它真的从外部可达、怎么把 sshd 挂上去,最后用一套自动解析把变来变去的地址固定到一个域名上,让你在任意网络环境下都能连回自己的手机。
需要先说清楚一个前提:这套方案成立的唯一条件是你的手机本身能拿到全球单播 IPv6 地址。如果所在网络只有 IPv4,或者运营商没有下发 IPv6 前缀,那这里讲的所有东西都跑不起来,这是物理层面的硬边界,不是配置技巧能绕过去的。适合读这篇的人有三类:手里有闲置安卓机想当低功耗小主机的人、想随时 SSH 回手机跑脚本的人、以及已经知道 IPv6 但被"地址老变、连不上、连上卡"折磨过的人。下面按"确认地址、验证可达、挂服务、做解析、划安全线、排故障"这条真实顺序往下走,中间所有命令都是我实测能跑的。
1. 公网 IPv6 落到手机上,究竟改变了什么
1.1 从"地址不够用"到"每台设备一个全球地址"
IPv4 时代家用宽带大多只有一个公网地址,剩下的设备全躲在路由器后面,外部想主动连进来必须靠端口映射,而端口映射的前提是你在路由器上手动配置并保证路由器本身有公网入口。到了移动网络这里情况更糟——大量终端共享同一批地址,外部主动连接基本无从谈起。
IPv6 的设计目标之一就是恢复端到端可达:地址空间足够大,运营商可以直接给每台在线设备下发一个全球单播地址,中间不需要地址转换。对 Termux 用户来说,这意味着一个很直接的结果:手机上的服务可以直接监听在自己的全球地址上,外部设备只要也有 IPv6 就能直连,中间没有 NAT 这一层。
这个变化的价值不在"能省几块钱服务器钱",而在于链路的确定性和延迟。直连没有中转节点,路径长度由路由决定,实测同一台手机用直连和走中转,往返延迟差别经常是几十毫秒级别的。当然代价也很明确:没有了 NAT 这层天然遮挡,你的设备就是全网可扫描的一台主机,这件事后面第 5 节会专门讲。
1.2 蜂窝数据与 Wi-Fi:两条完全不同的取址路径
很多人第一次在 Termux 里看到一长串 IPv6 地址时是懵的,原因在于不同接入方式拿到的地址来源完全不同。搞不清这一点,后面所有的排查都会变成瞎猜。
蜂窝数据路径:运营商侧通过标准流程把前缀下发到手机,手机在这个前缀里生成自己的接口标识。这种情况下手机通常能直接拿到一个全球单播地址,前缀落在2400::/12这个大段里(具体是哪个子段由运营商决定,不同地区不同运营商都不一样)。这是最容易成功的一条路,因为中间没有你自己的设备参与配置。
Wi-Fi 路径:家庭宽带光猫从运营商拿到前缀后,需要路由器继续往下分发,手机才能拿到全球地址。这里有两个常见的断点:一是路由器只给自己分配了地址而没有把前缀继续分下去,二是路由器在 IPv6 防火墙里默认丢弃所有入站连接。第一条表现为手机根本没有全球地址,第二条表现为有地址但外部死活连不上——这两种症状看起来一样,但处理方式完全不同。
我在不同运营商和不同路由器上反复试过,蜂窝数据的成功率明显高于 Wi-Fi。所以如果你只是想尽快跑通验证一遍,建议先用蜂窝数据把整条链路走通,再去折腾家里的路由器。
1.3 动手之前必须先确认的三件事
在敲第一行命令之前,请先在心里过一遍这三个问题,任何一个答案是否定的,后面就都是白费功夫:
- 手机当前接入的网络是否真的下发了 IPv6 前缀?这个不看你手机的设置界面,而是要看实际地址,第 2 节会给具体方法。
- 运营商或路由器是否对入站连接做了过滤?这个从手机侧看不出来,只能从外部设备实测。
- 你要暴露的服务是什么?如果只是 SSH,风险相对可控;如果打算开放 Web 服务或数据库端口,务必先看第 5 节。
把这三件事排好序的好处是,你遇到"连不上"时不会到处乱改配置。我见过太多人一上来就改 sshd 配置、换端口、关认证方式,最后发现问题根本不在服务端,而在于客户端压根没有 IPv6 出口。
2. 在 Termux 里把地址和可达性摸清楚
2.1 三条命令交叉验证本机 IPv6 状态
Termux 是运行在安卓应用沙箱里的,很多标准 Linux 网络命令在这里会受到系统权限限制,这一点必须先接受,否则你会怀疑自己的操作。下面三条命令按可靠性从高到低排列,建议三条都跑一遍交叉验证。
第一条,直接读内核暴露的接口表,这是最稳的,基本不受 SELinux 影响:
cat /proc/net/if_inet6输出每行的结构是:32 位十六进制地址、接口编号、前缀长度、作用域、标志位、接口名。看到fe80开头的那些直接忽略,它们是链路本地地址,跨网段没有任何意义。
第二条,用 iproute2:
pkg install iproute2 -y ip -6 addr show scope global部分安卓版本(尤其是定制 ROM)会因为权限问题让这条命令输出为空或者报 netlink 相关的错误。如果遇到这种情况不要慌,回到第一条命令就行。
第三条,net-tools 里的老命令:
pkg install net-tools -y ifconfig这条命令在新版安卓上时好时坏,能出结果就看一眼,出不来就别纠结。我的实际经验是:把/proc/net/if_inet6当作唯一真相来源,其他两条只作为辅助确认。
2.2 地址前缀怎么读:全球单播、临时地址与链路本地
拿到一串地址之后,关键动作是判断它属于哪一类。下面这张表是我自己整理出来贴在手边的对照表,规律很简单:看开头几位。
| 地址开头 | 类型 | 能否被外部直连 |
|---|---|---|
fe80:: | 链路本地地址 | 不能,仅同链路有效 |
fc00::/fd00:: | 唯一本地地址 | 不能,等同内网地址 |
2400::至240f:: | 全球单播,常见于移动网络 | 可以,前提是入站未被过滤 |
2001::至2003:: | 全球单播,其他规划段 | 可以,视运营商而定 |
::ffff:开头 | IPv4 映射地址 | 不适用,本质是 IPv4 |
除了类型,还要注意一个容易踩的点:安卓在默认配置下会启用隐私扩展,也就是除了那个稳定的接口标识之外,还会额外生成若干个临时地址,生存周期较短。你在/proc/net/if_inet6里看到的可能有一堆地址,最终哪个能被用来连入并不确定。
我的做法是:先拿所有全球单播地址逐个做外部测试,确认哪一个稳定可达,然后优先使用基于稳定接口标识的那个(通常对应stable-privacy或固定的eui64标识)。如果实在分不清,就在路由器或 DNS 侧记录所有候选地址——AAAA 记录本身支持多条,客户端会自动挑能通的。
2.3 出口连通性自测:从 ping 到 curl -6
确认了本机有全球地址,下一步要确认的是这条路径真的能把包发出去并且回来。这里有两层:本机的出站能力,以及外部对你入站的可见性。
先测出站:
pkg install iputils curl -y ping -6 -c 3 2400:3200::1 curl -6 -s --max-time 8 https://api6.ipify.org第一条用 IPv6 目标做 ICMP 探测,第二条强制走 IPv6 访问一个仅回显 IPv6 地址的服务。第二条返回的那个地址,就是你在公网上被看到的样子。正常情况下它应该和你本机if_inet6里的某个全球地址完全一致;如果不一致,说明中间存在地址转换,这种情况下从外部直连基本不可能成功,需要回头确认网络环境。
再测入站,这一步必须换一台设备,用另一个网络(比如手机开热点给电脑、或者找一台别的网络的机器):
ping -6 -c 3 2400:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx nc -6 -vz 2400:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx 8022注意 IPv6 地址在使用时必须用方括号包起来,这是在 ssh、curl、scp 里通用的写法。如果 ICMP 不通但 TCP 通了,说明对方只是屏蔽了 ICMP,不影响使用;如果两个都不通,那大概率是入站被过滤了,直接跳到第 6 节按顺序排查。
3. 把 Termux 变成可被外部登录的 SSH 服务端
3.1 装 openssh 与账号初始化
这一步本身不难,但有几个细节如果搞错,后面会出现"服务起来了但登不上"。先装基础包:
pkg update && pkg upgrade -y pkg install openssh -y whoamiwhoami输出的不是root,而是形如u0_a123这样的应用用户名,这是正常的——Termux 运行在安卓的应用沙箱里,本来就没有系统 root。这个用户名就是你后面登录时要用的账号名,务必记下来。
接着设置登录密码:
passwd这里有个坑我踩过:密码输入时屏幕完全没有任何回显,连星号都没有,这不是卡死了,正常输入回车即可。如果密码设得太短,某些版本会提示但依然接受,为了安全建议直接用密钥登录,密码只作为应急手段。
如果要定期从固定设备登录,把客户端的公钥写进来:
mkdir -p ~/.ssh && chmod 700 ~/.ssh cat >> ~/.ssh/authorized_keys # 粘贴客户端公钥后按 Ctrl+D chmod 600 ~/.ssh/authorized_keys权限位必须严格,authorized_keys是 600、.ssh目录是 700。sshd 对这两个权限非常敏感,文件权限过松会直接拒绝使用密钥,而且报错信息里不会明说原因,只会在日志里留下一行 "Authentication refused: bad ownership or modes",这就是典型的"服务没毛病但就是登不上"。
3.2 sshd_config 里真正要改的几行
Termux 里 sshd 的配置文件在$PREFIX/etc/ssh/sshd_config,也就是:
$PREFIX/etc/ssh/sshd_config改之前先备份一份原文件,这一点很重要,因为改错了会导致 sshd 起不来而你又回不到出厂状态。需要动的字段不多,我把自己实际用的最小改动列出来:
| 配置项 | 建议值 | 作用 |
|---|---|---|
Port | 8022 | 默认值,非 root 环境下能绑定的端口 |
AddressFamily | any | 同时接受 IPv4 和 IPv6 连接 |
PasswordAuthentication | no | 关闭密码登录,只留密钥 |
PubkeyAuthentication | yes | 启用密钥登录 |
PermitRootLogin | no | 即使有 root 也禁止直接登录 |
MaxAuthTries | 3 | 减少暴力尝试空间 |
LoginGraceTime | 20 | 缩短认证窗口 |
ClientAliveInterval | 60 | 保活探测间隔 |
ClientAliveCountMax | 3 | 连续 3 次无响应断开 |
AddressFamily这一项不要随手设成inet6。设成inet6之后 sshd 只会监听 IPv6,万一你的 IPv6 路径临时不可用,就连本地 IPv4 的救急登录都做不到了。留成any是更务实的取舍。
改完启动:
sshd ss -tlnp | grep 8022第二条命令用来确认端口真的在监听。如果ss不可见,用ss -tln也能看,只是没有进程名。看到监听记录之后再去做第 3.4 节的完整验证。
3.3 非 root 环境下的端口现实:为什么是 8022
这是很多人第一个卡住的地方:为什么不能直接用 22 端口?原因很直接,Linux 内核规定 1024 以下的端口属于特权端口,只有具备相应权限的进程才能绑定,而 Termux 里的 sshd 是以应用用户身份运行的,绑不了 22。所以 8022 不是"推荐配置",而是当前权限模型下的结果。
有人会想通过改端口来隐藏服务,认为用非标准端口更安全。这里必须说清楚:在公网环境下,靠换端口带来的安全性提升非常有限。全端口扫描是常规操作,从常见的 22、2222、8022 一直到高位端口,扫一遍的时间成本很低。端口号唯一的实际作用是减少日志噪音,让你在翻 auth 日志时少看几千行无效尝试。
真正有效的防护是密钥认证加严格权限,而不是端口号。这一点想明白了,你就不会把精力浪费在"换个没人知道的端口"这种思路上。
3.4 第一次从外部连入的完整验证链路
从客户端发起连接:
ssh -p 8022 -i ~/.ssh/id_ed25519 u0_a123@[2400:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx]如果嫌每次输地址太长,可以在客户端的~/.ssh/config里固化下来:
Host phone HostName 2400:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx Port 8022 User u0_a123 IdentityFile ~/.ssh/id_ed25519 ServerAliveInterval 30 ServerAliveCountMax 3之后直接ssh phone就行。ServerAliveInterval这几行在移动网络下很重要,后面 6.2 节会解释原因。
验证的顺序建议是:先在手机本地ssh -p 8022 u0_a123@localhost确认服务本身没问题,再从同一 Wi-Fi 下的另一台设备用地址连,最后才从完全不同的网络连。这个顺序能把"服务端问题"和"网络路径问题"彻底分开,省下大量瞎试的时间。
4. 地址会变,所以解析必须自动化
4.1 手机上的 IPv6 为什么"活不长"
这是整个方案里最需要接受的一点:手机上拿到的全球地址,稳定性远不如一台固定服务器。变化来源主要有三个:
一是接入网络切换。从 Wi-Fi 切到蜂窝,前缀完全换了一套,地址必然改变。二是前缀重新下发。即使一直在同一个网络,运营商侧的前缀也可能在重启、重新拨号、租期到期后变化。三是隐私扩展带来的临时地址轮换,接口上的可用地址集合本身就在变。
所以"记住一个地址然后手填"这种做法,本质上只在几分钟到几小时的时间尺度内有效。想让它可用,就必须把"当前地址"这件事自动化成一条持续更新的记录,而这条记录最自然的载体就是域名解析——也就是往 AAAA 记录里写当前的 IPv6 地址。
4.2 拿到出口地址:几个可用的探测方式
写 DDNS 脚本的第一步是拿到"别人连我时该用的那个地址"。有两种获取方式,各有前提:
方式一,从本机接口读:
ip -6 addr show scope global | grep inet6 | awk '{print $2}' | cut -d/ -f1优点是快、无外部依赖;缺点是在接口不可用的 ROM 上会失败,而且如果本机有多个全球地址,你得知道该选哪个。
方式二,从外部服务回显:
curl -6 -s --max-time 8 https://api6.ipify.org优点是拿到的就是你在公网上的实际样子;缺点是需要外部网络可用。
我的做法是两者都取,然后做一次比对:一致就用它,不一致说明中间可能做了地址转换,此时直连方案本身就不可靠,应该停下来重新评估网络环境,而不是硬着头皮往下写脚本。这个比对逻辑只有几行代码,但帮我提前发现过好几次环境问题。
4.3 自建 DDNS:用脚本把 AAAA 记录改成当前地址
下面这个脚本是我实际在用的结构,把里面的接口地址和令牌换成你自己的就行。这里以通用 DNS 服务商的 API 为例,其他服务商的字段名不同但思路完全一样。
#!/data/data/com.termux/files/usr/bin/bash set -uo pipefail CF_TOKEN="your_api_token" ZONE_ID="your_zone_id" RECORD_ID="your_record_id" RECORD_NAME="termux.example.com" STATE_FILE="$HOME/.termux_ipv6_last" # 取当前出口 IPv6 NEW_IP=$(curl -6 -s --max-time 10 https://api6.ipify.org) if ! echo "$NEW_IP" | grep -qE '^[0-9a-fA-F:]+$'; then echo "$(date '+%F %T') 获取地址失败,跳过" >> "$HOME/.termux_ipv6.log" exit 1 fi # 和上次比较,没变就不动 DNS OLD_IP=$(cat "$STATE_FILE" 2>/dev/null || echo "") if [ "$NEW_IP" = "$OLD_IP" ]; then exit 0 fi # 更新 AAAA 记录 RESP=$(curl -s --max-time 15 -X PUT \ "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${RECORD_ID}" \ -H "Authorization: Bearer ${CF_TOKEN}" \ -H "Content-Type: application/json" \ --data "{\"type\":\"AAAA\",\"name\":\"${RECORD_NAME}\",\"content\":\"${NEW_IP}\",\"ttl\":60,\"proxied\":false}") if echo "$RESP" | grep -q '"success":true'; then echo "$NEW_IP" > "$STATE_FILE" echo "$(date '+%F %T') 更新成功 $OLD_IP -> $NEW_IP" >> "$HOME/.termux_ipv6.log" else echo "$(date '+%F %T') 更新失败 $RESP" >> "$HOME/.termux_ipv6.log" fi有几个设计上的取舍值得说明。第一,先比对再更新。很多免费 DNS 接口对写入频率有限制,如果每分钟都盲写一次,很容易被限流,加上状态文件比对可以把请求量降到极低。第二,proxied必须设为 false。开了 CDN 代理之后,解析出来的地址会变成代理节点地址,而我们要的是直连,开了反而连不上。第三,TTL 设成 60 秒。手机地址变化频繁,TTL 太长会让客户端长时间拿旧地址,60 秒是可用性和查询量之间的平衡点。第四,记录名建议用独立子域。比如termux.example.com,不要占主域名,避免以后想给主站做其他配置时互相干扰。
4.4 定时、重试与日志:让它自己跑起来
脚本写完不会自己跑,需要一个调度。Termux 里有两个方向:
轻量方案是用 crontab:
pkg install cronie -y crond crontab -e内容是每分钟跑一次:
* * * * * /data/data/com.termux/files/home/ddns.sh配合脚本里的"先比对再更新",一分钟一次并不会造成压力,因为绝大多数执行会在比对那一步就退出。
想要更省电的话,可以改成每 5 分钟一次,代价是地址变化后有最多 5 分钟的连不上窗口。这个取舍取决于你的使用习惯,我自己的配置是 2 分钟一次,兼顾了响应速度和唤醒频率。
另一个必须处理的问题是开机自启。安卓会自动清理后台进程,cron 也不例外,手机重启之后一切归零。需要装 Termux:Boot 这个配套应用,然后在~/.termux/boot/目录下放一个启动脚本:
mkdir -p ~/.termux/boot cat > ~/.termux/boot/start-services.sh <<'EOF' #!/data/data/com.termux/files/usr/bin/sh termux-wake-lock sshd crond EOF chmod +x ~/.termux/boot/start-services.shtermux-wake-lock这一行千万别省,它的作用是让系统不要因为省电策略把 Termux 的进程挂起。这是我踩过最深的坑之一:服务配置全对,脚本也正常,但手机息屏十分钟后 SSH 就再也连不上,日志显示 sshd 还在跑,实际进程已经被系统冻结了。加上这行之后问题消失。
5. 公网可达的另一面:先把安全边界划出来
5.1 全端口扫描是常态,不是意外
有全球地址意味着你的设备直接暴露在公网上,没有任何中间层帮你过滤。这不代表立刻会被攻击,但意味着你必须假设有人正在扫描。实际观察下来的情况是,一个全新的 IPv6 地址挂上 sshd 之后,通常在几小时到一天之内就会在认证日志里看到来自不同来源的尝试记录。
理解这一点之后,安全策略的思路就很清楚了:不追求"没人发现我",而是追求"发现了也进不来"。这个思路下,密钥认证、最小权限、日志可查就是三个必须做到的基础项,而端口号、隐藏服务这类做法优先级很低。
还有一个容易被忽略的暴露面:sshd 不是唯一在监听的进程。Termux 里跑的其他脚本、临时启的 Web 服务、甚至某个库自带的调试端口,都会一起暴露。定期用ss -tln检查一遍监听列表,是我现在的固定习惯,每次跑完总能看到一两个自己都忘了的进程。
5.2 密钥登录 + 关闭密码:最小必要改动
这套改动只需要三步,但效果最明显:
- 客户端生成密钥,推荐 ed25519:
ssh-keygen -t ed25519 -a 100 -C "termux-phone"把公钥写进手机的
~/.ssh/authorized_keys,权限按 3.1 节设置。在
sshd_config里设置PasswordAuthentication no并重启 sshd。
之所以要关密码登录,不是因为密码一定弱,而是因为密码登录允许对方无限次尝试,密钥登录几乎不给机会。开了密钥之后,即使有人找到你的地址和端口,也基本没有可行的进入路径。
这里有一个务实的提醒:关掉密码登录之后,如果密钥丢了就彻底进不去了。所以要么在另一台设备上留一份私钥备份,要么在手机本地保留一个可以通过物理接触登录的方式。我自己是把手上的私钥加密备份了一份放在完全离线的介质里,这是最后一道保险。
5.3 监听地址、端口与时间窗口的收窄技巧
如果希望进一步收紧,有几个不太费力但有效的做法。
限制来源网段。sshd_config支持按来源地址做匹配,把认证方式进一步绑定到特定范围:
Match Address 2001:db8:1234::/48,203.0.113.0/24 PasswordAuthentication no PubkeyAuthentication yes这样即使将来临时打开了别的入口,也只有指定来源的可信设备能走完整的认证流程。注意Match块要写在文件末尾,且块内不能出现某些全局指令,写之前先确认语法,改错了 sshd 会直接拒绝启动。
收窄监听地址。如果确定只用某一个全球地址连入,可以在配置里显式指定,减少接口上其他地址的暴露。不过由于手机地址会变,这条对动态环境不太友好,一般我用AddressFamily any就够了。
控制服务运行时间。如果你的使用场景是"每天固定几小时需要连",那完全可以用 cron 定时开关 sshd,其余时间不开监听。这是最彻底的收窄方式,代价是失去随时接入的便利。我在早期用过这个方案,配合脚本在需要的时间段前后启动和关闭服务,实测下来很安心。
5.4 手机侧的存活问题:省电策略与 wakelock
安卓的省电机制对长驻进程非常不友好,这里必须做三步处理,缺一步都会出现"凌晨能连,白天连不上"这类诡异现象。
第一步,关闭系统对 Termux 的电池优化。在系统设置的电池管理里把 Termux 标记为不受限制,不同系统路径不一样,一般叫"电池优化"或"后台限制"。
第二步,保持 wakelock。前面提到的termux-wake-lock就是做这件事的,它会在系统层面申请一个持有锁。执行后可以在通知栏看到对应提示。需要注意的是这个锁在设备重启后失效,所以必须放到开机启动脚本里。
第三步,接受"系统可能随时回收"这个事实,把服务做成可恢复的。我的做法是写一个巡检脚本,每几分钟检查一次 sshd 和 crond 是否在运行,不在就拉起来。这类脚本只要几行,但对稳定性提升非常明显:
#!/data/data/com.termux/files/usr/bin/bash if ! pgrep -x sshd > /dev/null; then sshd echo "$(date '+%F %T') sshd 已重启" >> "$HOME/.termux_supervisor.log" fi巡检日志我保留了一个月,回头看能很清晰地看出系统在什么时间点回收过进程,对判断设备是否需要换一台很有帮助。
6. 连不上和连上但很卡:两类故障的排查顺序
6.1 分层排查:从地址到端口到应用
遇到连不上,最忌讳的操作是随便挑一个环节去改。正确的做法是按"客户端出口、网络路径、服务端监听、认证"这四层依次确认,任何一层不通过就停在那里解决,不要往下走。
第一层,客户端有没有 IPv6 出口。在客户端跑curl -6 -s --max-time 8 https://api6.ipify.org,如果这里就报错或者超时,说明你所在的网络完全没有 IPv6 能力,后面的问题根本不用查。这一层的失败率远超其他层,尤其是很多人用的办公网络、公共 Wi-Fi。
第二层,地址是否解析正确。用dig AAAA termux.example.com查一下,和手机上当前的实际地址比。不一致说明 DDNS 脚本没跑成功,去看脚本日志。这里有个细节:本地可能有 DNS 缓存,明明记录已经更新了但查出来还是旧的,换个公共解析服务再查一次就能确认。
第三层,端口是否可达。用nc -6 -vz <地址> 8022从外部测,配合手机上ss -tln确认监听状态。如果监听正常但外部测不通,问题在网络路径或入站过滤,不在服务端。
第四层,认证是否通过。如果前面三层都通,那问题就落在密钥或权限上,直接看客户端的ssh -vvv输出,错误信息会明确告诉你卡在哪一步。
按这个顺序走,绝大多数问题五分钟内就能定位。我早期最大的时间浪费就是不走这个顺序,看哪条配置不顺眼就改一下,结果把原本正常的部分也改坏了。
6.2 MTU 与路径问题导致的"能连上但卡死"
这是一类非常典型且容易被误诊的故障:连接能建立,密钥认证也能过,但登录进去之后命令执行极慢,或者执行到一半直接卡住不动。很多人第一反应是手机性能不行,其实往往是路径上的 MTU 不匹配导致的。
IPv6 规范里不允许中间设备做分片,只能由发送方根据路径 MTU 来分片,一旦链路上某个节点丢弃了"需要分片"的包却不回报 ICMPv6 消息,就会出现这种连得上但传不动数据的现象。移动网络下的 MTU 值经常和标准值不同,跨网络访问时更容易触发。
排查方法是逐步探测可用包长:
ping -6 -c 3 -M do -s 1400 <目标地址> ping -6 -c 3 -M do -s 1200 <目标地址>从较大的值往下试,找到能稳定通的最大值,然后用这个结果去推断可用 MTU。如果发现 1400 不通而 1200 通,说明链路上有节点把 MTU 压得比较低。
客户端侧的缓解办法是尽量让传输走已有的连接而不是频繁重连,ServerAliveInterval和ClientAliveInterval就是干这个的。另外,交互式使用场景下如果对卡顿特别敏感,可以考虑用更容忍高延迟的交互方式,纯 SSH 在高丢包环境下体验确实一般。需要说明的是,在非 root 环境下没办法直接调整接口的 MTU 参数,所以这一层主要是靠探测定位问题,然后通过改变路径或使用方式来规避。
6.3 只有 IPv4 出口的场景:这是硬边界
必须坦白讲清楚一个问题:如果你常用的网络环境只能提供 IPv4,那这套基于 IPv6 直连的方案就是用不了。这不是配置问题,而是协议栈层面的现实。我见过太多人在这上面耗掉一整个周末,反复怀疑是端口、防火墙、密钥出了问题,实际上客户端根本没有 IPv6。
判断方法很简单,客户端执行前面那条curl -6命令,失败就是没有。这里有个容易混淆的点:域名解析出 AAAA 记录不代表你能连上,解析成功只是拿到了地址,能不能通取决于客户端的网络能力。域名同时配了 A 记录和 AAAA 记录时,客户端会根据自己的能力自动选择,所以从某些网络能连、从另一些不能,是完全正常的现象,不是服务端故障。
如果你确实需要在 IPv4 环境下也能接入,那需要换一套完全不同的思路,和本文讲的直连方案不是一回事。我的建议还是先确认自己的主要使用场景里有没有 IPv6,如果有,就按本文的路径做;如果没有,趁早换方案,不要在这里死磕。
6.4 错误信息对照表与快速处置
下面这张表是我从实际日志里一条条积累出来的对应关系,遇到报错直接查表,比盲目搜索快得多。
| 客户端报错 | 最可能的原因 | 处置方向 |
|---|---|---|
Network is unreachable | 客户端没有 IPv6 出口 | 换网络,或确认接入方式 |
No route to host | 地址格式不对或缺省路由缺失 | 检查方括号写法、地址是否完整 |
Connection timed out | 路径不通或入站被过滤 | 用 nc 测端口,确认路由器防火墙 |
Connection refused | 能到主机但端口无监听 | 手机上检查 sshd 是否运行 |
Permission denied (publickey) | 认证失败 | 查 authorized_keys 权限和内容 |
Host key verification failed | 服务端密钥变了 | 清理本地 known_hosts 对应条目 |
| 连上后命令无响应 | MTU 或路径质量差 | 按 6.2 节做包长探测 |
| 半夜断连、白天正常 | 系统省电回收了进程 | 加 wakelock 和巡检脚本 |
这张表里我特别想强调最后两行。它们的特点是"看起来不像网络问题",所以最容易被误判成硬件或系统故障。而实际上一个是传输层参数,一个是系统电源策略,都属于只要知道了原因就能很快解决的类型。
我自己现在的做法是把这张表直接写成一个文本文件放在手机里,每次出问题先对一遍,对不上的再从头排查。这个习惯让我把平均排障时间从半小时压到了几分钟——比起研究各种高级配置,把已知问题的处置路径固化下来,实际收益要大得多。
最后再补一个小经验:把域名和验证命令一起存在手机的备忘录里,包括curl -6的探测命令和dig AAAA的查询命令。因为最容易出问题的时刻,往往是你人在外面、手上只有手机、没有任何参考资料的时候,能一键复制粘贴几条命令直接确认状态,比什么都管用。