Termux 自动化运维实战:定时任务防杀与 Tailscale 跨网络访问

在 Android 手机上跑自动化任务,像在流沙上盖房子。你以为地基打好了,一觉醒来,发现系统已经把你的进程连根拔起,不留字条,不给解释。《庄子》里说「适来,夫子时也;适去,夫子顺也」,Android 的后台管理大概深谙此道,只是被杀的进程未必那么豁达。

这篇文章记录一次 Termux 定时任务失踪事件的完整排查,以及由此引出的一个教训:你以为你要的是一个 IP 地址,其实你要的是一条回家的路。

问题现象

某天早晨,发现 Termux 上的定时采集任务没有执行。手机 IP 能 ping 通,但 SSH(端口 8022)连接被拒绝。就像敲一扇有灯光的门,里面有人,但没人来开。

排查过程

第一步:确认网络与服务状态

1
2
3
4
5
6
7
8
# IP 能 ping 通,说明手机在线
ping 192.168.x.x  # ✅ 通

# SSH 连不上,说明 sshd 没跑
ssh -p 8022 [email protected]  # ❌ Connection refused

# ADB 也没连接
adb devices  # 空

Termux 进程还在,但 sshd 没有运行。门在,锁在,钥匙孔被人用水泥封了。

第二步:排查 sshd 未自启的原因

手动启动 sshd 后 SSH 进去检查,发现 Termux 使用 termux-services(基于 runit)管理服务。sshd 的服务目录下存在一个 down 文件——runit 的设计哲学是:有这个文件就不自动启动,沉默而忠实。

1
2
ls /data/data/com.termux/files/usr/var/service/sshd/
# 发现 down 文件存在

删除 down 文件,让 runit 接管:

1
2
3
export SVDIR=/data/data/com.termux/files/usr/var/service
rm -f $SVDIR/sshd/down
sv up sshd

注意:SSH 会话中 $SVDIR 环境变量为空,必须手动 export 后才能使用 sv 命令。这是一个容易反复踩的坑。

第三步:排查定时任务未执行的原因

查看 cron 日志,最后一条记录停在前一天下午。之后晚间和次日早晨的任务都没有执行:

1
2
tail -5 ~/task-cron.log
# 最后记录停在前一天下午

而 crond 进程是当天早上才启动的(通过 PID 文件时间戳判断),早晨的任务在 crond 启动之前就已经错过了。cron 的脾气众所周知:错过就是错过,不补,不道歉,不解释。

根因:Android 系统在夜间杀掉了 Termux 进程,crond 随之停止。Termux 重启后 crond 恢复,但错过的任务石沉大海。

解决方案

方案一:配置服务自启动

确保 sshd 和 crond 都由 termux-services 管理且没有 down 文件:

1
2
3
4
5
6
7
8
9
export SVDIR=/data/data/com.termux/files/usr/var/service

# 删除 down 文件,允许自启动
rm -f $SVDIR/sshd/down
rm -f $SVDIR/crond/down

# 确认状态
sv status sshd   # run: sshd: (pid xxx) xxs
sv status crond  # run: crond: (pid xxx) xxs

方案二:@reboot 补救机制

既然 Android 杀进程不可避免,那就换个思路:不阻止死亡,而是在复活后检查遗产。

利用 cronie 的 @reboot 指令(crond 启动时执行一次),加一个检查脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#!/data/data/com.termux/files/usr/bin/bash
# ~/bin/check-missed.sh

TODAY=$(date +%Y-%m-%d)
LOG=~/task-cron.log

# 检查今天是否已有成功记录
if grep -q "^${TODAY}.*任务完成" "$LOG" 2>/dev/null; then
    echo "$(date '+%Y-%m-%d %H:%M:%S') [补救] 今日任务已完成,跳过"
    exit 0
fi

# 7点前不补跑
HOUR=$(date +%H)
if [ "$HOUR" -lt 7 ]; then
    echo "$(date '+%Y-%m-%d %H:%M:%S') [补救] 当前 ${HOUR} 点,早于7点,跳过"
    exit 0
fi

echo "$(date '+%Y-%m-%d %H:%M:%S') [补救] 检测到今日任务缺失,开始补跑..."
~/bin/run-task.sh

在 crontab 中添加:

1
@reboot sleep 60 && ~/bin/check-missed.sh >> ~/task-cron.log 2>&1

sleep 60 是给依赖服务恢复留出时间。这样即使 Termux 被杀后重启,crond 恢复时会自动检查并补跑漏掉的任务。逻辑很简单:醒来第一件事,看看今天的活干了没有。

方案三:手机端防杀措施

在小米手机上做以下设置,降低 Termux 被杀的概率:

  • 设置 Termux 为自启动应用
  • 在最近任务中锁定 Termux(下拉锁定)
  • 电池设置中将 Termux 设为「无限制」
  • Termux 内执行 termux-wake-lock 获取唤醒锁

这些措施降低概率,但不能杜绝。Android 的后台管理是个黑箱,你做了所有正确的事,它仍然可能在某个深夜动手。所以方案二的补救机制才是真正的兜底。

需求溯源:从获取 IP 到 Tailscale 组网

原始需求

写一个脚本获取手机 IP 地址。

追问根本问题

手机局域网 IP(如 192.168.x.x)会随 WiFi 网络变化。但更关键的问题是:如果手机和电脑不在同一个网络,即使知道局域网 IP 也连不上。192.168.x.x 是局域网地址,出了这个路由器的门,就是一串没有意义的数字。

真正要解决的不是「获取 IP」,而是「跨网络远程访问手机」。写一个 IP 查询脚本,就像给一个找不到路的人发一张详细的室内平面图——精确,但无用。

最短路径:Tailscale

Tailscale 为每台设备分配固定的虚拟 IP(100.x.x.x),不管设备在哪个物理网络,都能直接互联。

工作原理

  1. 每台设备运行 Tailscale 客户端,生成 WireGuard 密钥对
  2. 设备向 Tailscale 协调服务器注册公钥和网络位置
  3. 协调服务器告诉每台设备其他设备的地址和公钥
  4. 设备之间直接建立 WireGuard 点对点加密隧道(NAT 穿透)
  5. 只有极少数穿不透的情况才走 DERP 中继服务器

核心特点:协调服务器只交换地址信息,实际数据点对点直连,不经过 Tailscale 服务器。这个设计很克制——中心只做介绍人,不做中间商。

免费版(Personal)限制

项目限制
用户数3 人
设备数100 台
子网路由无限制
商业用途不允许

个人使用完全够用,官方承诺个人免费版永久免费。

部署效果

1
2
# 通过 Tailscale 固定 IP 连接,不受物理网络变化影响
ssh -p 8022 [email protected]

手机在家里、在办公室、在旅途中、用移动数据,都是同一个地址。地址不再是物理位置的附庸,而是身份的锚点。

与其他网络工具的兼容性

Tailscale 只接管 100.x.x.x 地址段的流量,其他网络工具工作在应用层,两者互不干扰。

写在最后

这次排查从一个没跑的 cron 任务开始,经过 sshd 自启修复、补救机制设计、需求溯源,最终落在 Tailscale 组网。回头看,最有价值的不是任何一个技术方案,而是那个追问:「你要解决的根本问题是什么?」

在技术工作中,人们常常带着一个具体方案来,方案本身没有错,但它回答的可能是一个错误的问题。写一个获取 IP 的脚本,逻辑上无可挑剔,但它解决的是「不知道 IP 是多少」,而真正的困境是「知道了也连不上」。《韩非子》说「郑人买履」,量好了尺码却忘了试鞋。技术世界里这样的事,每天都在发生。