用 frp 的 stcp + xtcp 打通手机 ADB:一套 P2P 远程控制的完整配置
IT 内网穿透 智能手机 自建服务 5

手里有台常年放在家里的安卓手机,想在外面用笔记本直接连上去——投屏看两眼、装个包、跑几条 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 中转,可用性和速度各让一步。

三种角色与拓扑

角色位置职责
frps有公网 IP 的 VPS鉴权、牵线,以及 stcp 的中转
frpc(listener)被控的手机把本机 adbd 的 5555 端口挂到 frps 上
frpc(visitor)你面前的电脑本地开一个 6000 端口,adb 和 scrcpy 连它

两条数据路径:

  • 打洞成功:电脑: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 = 30
  • bindPort 是 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 = 60

xtcp visitor 关键参数

这几个参数是整套配置里最容易配错的地方,值得单独说明:

参数本文取值含义
bindPort(stcp visitor)-1不监听本地物理端口,只用来接受 xtcp 回退过来的连接。fallback 机制要求这样写
bindAddr / bindPort(xtcp visitor)127.0.0.1:6000真正对外提供服务的本地地址,adb 连的就是它
fallbackTophone_adb_stcp_visitor打洞失败时回退到哪个 visitor,填 stcp visitor 的 name
fallbackTimeoutMs3000连接进来后等待打洞多久,超时就回退(默认 1000)
keepTunnelOpentrue主动保活隧道,定期检测并重新打洞,避免每次连接都要等
maxRetriesAnHour60每小时最大重试次数(默认 8),放宽后恢复更快
minRetryInterval默认 90两次重试之间的最小间隔,单位秒,本篇沿用默认值

连上去用起来

两端 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 放宽一些,也是个合理的取舍。

用 frp 的 stcp + xtcp 打通手机 ADB:一套 P2P 远程控制的完整配置
https://blog.yaorelax.com/archives/frp-p2p-android-adb-remote-control
作者
DeepSeek
发布于
更新于
许可