手里有台常年放在家里的安卓手机,想在外面用笔记本直接连上去——投屏看两眼、装个包、跑几条 adb 命令。摆在面前的是两个障碍:家庭宽带基本拿不到公网 IPv4,而 ADB 自己又完全没有认证,把 adbd 直接暴露到公网等于把手机交出去。
frp 的 stcp 和 xtcp 两种代理类型刚好能对付这对矛盾:不在服务器上开任何公网端口,靠共享密钥鉴权,打洞成功时流量还是两台机器直连。下面是这套配置的完整记录。
为什么不直接用 tcp 代理
frp 最常见的 tcp 类型会在 frps 上监听一个公网端口,配置最简单,但对 ADB 来说这个默认选项并不合适。三种类型放在一起看会比较清楚:
- tcp:公网端口一旦被扫到,任何人都能连上你的 adbd。ADB 协议没有密码这一层,能连上就等于拿到了手机的调试权限。
- stcp:不监听公网端口。frps 只在中间牵线,只有
secretKey相同的 visitor 才能连接到对应的 proxy,流量仍然经过服务器中转。 - xtcp:在 stcp 的基础上再进一步。先通过 frps 交换地址信息做 UDP 打洞,成功后两端直接通信,流量不再经过服务器,也就不受 VPS 带宽限制。代价是成功率取决于双方的 NAT 类型,官方也明确说了可用性无法保证。
单用 xtcp 的问题是打洞失败就连不上。所以实际部署时把两者都挂上,用 fallbackTo 串起来:平时尽力直连,打不通自动回退到 stcp 中转,可用性和速度各让一步。
三种角色与拓扑
两条数据路径:
- 打洞成功:
电脑:6000→手机:5555,直连,不经过服务器 - 打洞失败:
电脑:6000→frps→手机:5555,走 stcp 中转
对使用者来说这两条路径没有区别,连的都是 127.0.0.1:6000。
服务端 frps
bindPort = 7000
auth.token = "your-strong-token"
userConnTimeout = 30
[transport]
tcpMux = true
tcpMuxKeepaliveInterval = 30bindPort是 frpc 的接入端口,不是业务端口。stcp 和 xtcp 都不会在这里额外开监听。auth.token用于校验 frpc 身份,别用弱口令。一旦泄露,别人可以拿你的 frps 当中转节点。tcpMux让多条隧道复用同一条 TCP 连接,减少连接数;30 秒的 keepalive 负责保活。
手机上的 frpc
这一端以常驻服务的方式跑在手机上(Termux 里配开机自启,或者用带后台守护的 frpc 客户端),localIP / localPort 指向的就是手机自己的 adbd。
serverAddr = "frp.example.com"
serverPort = 7000
auth.token = "your-strong-token"
loginFailExit = false
[transport]
protocol = "tcp"
tcpMux = true
tcpMuxKeepaliveInterval = 30
poolCount = 4
heartbeatTimeout = 90
[[proxies]]
name = "phone_adb_stcp"
type = "stcp"
secretKey = "your-shared-secret"
localIP = "127.0.0.1"
localPort = 5555
[[proxies]]
name = "phone_adb_xtcp"
type = "xtcp"
secretKey = "your-shared-secret"
localIP = "127.0.0.1"
localPort = 5555- 两个 proxy 指向同一个本地端口,只是类型不同:xtcp 是主力,stcp 负责兜底。stcp 也可以被 visitor 单独使用,不依赖 xtcp。
secretKey在 proxy 和对应的 visitor 两端必须完全一致,这是唯一的安全边界。loginFailExit = false让网络抖动时 frpc 持续重连而不是直接退出。手机端网络切换频繁,这个必须留着。poolCount = 4预建 4 条连接备用,heartbeatTimeout = 90容忍一定的网络延迟。
前提是手机上的 adbd 已经切到 TCP 模式、在 5555 端口监听,也就是执行过一次 adb tcpip 5555。
电脑上的 frpc
访问端同时定义两个 visitor:一个 stcp 只做兜底,一个 xtcp 对外提供服务端口。
serverAddr = "frp.example.com"
serverPort = 7000
auth.token = "your-strong-token"
loginFailExit = false
[transport]
protocol = "tcp"
tcpMux = true
tcpMuxKeepaliveInterval = 30
poolCount = 4
heartbeatInterval = 30
heartbeatTimeout = 90
[[visitors]]
name = "phone_adb_stcp_visitor"
type = "stcp"
serverName = "phone_adb_stcp"
secretKey = "your-shared-secret"
bindPort = -1
[[visitors]]
name = "phone_adb_xtcp_visitor"
type = "xtcp"
serverName = "phone_adb_xtcp"
secretKey = "your-shared-secret"
bindAddr = "127.0.0.1"
bindPort = 6000
fallbackTo = "phone_adb_stcp_visitor"
fallbackTimeoutMs = 3000
keepTunnelOpen = true
maxRetriesAnHour = 60xtcp visitor 关键参数
这几个参数是整套配置里最容易配错的地方,值得单独说明:
连上去用起来
两端 frpc 起来之后,访问端本地就有了一个普通端口,剩下的和插着数据线没有区别:
adb connect 127.0.0.1:6000
adb devices
scrcpy -s 127.0.0.1:6000第一次连接会在后台触发打洞。如果 NAT 允许,几秒后隧道建立,之后的流量就走直连了;不允许的话会自动落到 stcp,连接照样能用。
容易踩的坑
打洞不一定成功
xtcp 依赖 STUN 判断 NAT 类型并做 UDP 打洞。双方都是锥形 NAT 时成功率不错;碰上对称型 NAT 或运营商的 CGNAT,基本打不通,会一直走 stcp。这不影响可用性,只是流量要过服务器、受 VPS 带宽限制。默认 STUN 服务器不通时,可以在 frpc 里换一个:natHoleStunServer = "..."。
手机重启后 adbd 会掉回 USB 模式
adb tcpip 5555 是运行时设置,重启即失效。想让手机随时可连,得在每次开机后重新触发一次;有条件的话做成开机脚本,或者改走 Android 11+ 的无线调试配对。这是整套方案里最容易忽略、也最容易在关键时刻掉链子的一环。
fallbackTimeoutMs 不要设太小
官方文档特意提醒过:超时时间要结合两端的延迟来定。设得太短,即使打洞本来能成功,也会因为来不及建立而一直触发回退,直连等于白打。默认值 1000ms 偏保守,跨运营商或跨境场景可以放宽到 2000–3000ms。
保活与耗电的取舍
keepTunnelOpen = true 让 frpc 定期检测并重建隧道,好处是随时连都很快;代价是手机侧要持续维持连接和重试,对续航有实际影响。如果只是偶尔连一次,可以关掉它,接受第一次连接时的打洞等待。
安全边界
auth.token和secretKey一旦泄露,等于把手机的调试通道交出去。别提交到公开仓库,写博客、发截图之前也要先替换掉——本篇里的域名、token 和密钥就都是占位符。- ADB 协议本身是明文的,frp 隧道解决的是「怎么连上」,并没有改变 adbd 的能力边界:连上来的人可以装应用、读写文件。这台手机最好不要同时放敏感数据。
- frps 的
bindPort只需要对 frpc 开放,建议配合防火墙限制来源地址。
小结
整套配置的核心其实就三件事:stcp 提供一条不暴露公网端口、靠密钥鉴权的兜底通道;xtcp 在这条通道之外尝试 P2P 直连,把大流量从服务器上卸下来;fallbackTo 把两者串起来,让「打洞失败」从可用性问题降级成性能问题。
对手机 ADB 这种平时不用、要用就想立刻连上的场景,把 keepTunnelOpen 开着比较省心;如果更在意续航,关掉它、再把 fallbackTimeoutMs 放宽一些,也是个合理的取舍。