在 Termux 里跑华为云 KooCLI:一次跨越 glibc、DNS、CA 和 seccomp 的适配

举报
Syphurith 发表于 2026/10/27 20:38:53 2026/10/27
【摘要】 华为云 KooCLI(`hcloud`)官方只提供 glibc 动态链接的 Linux 二进制,Termux 用的是 Android Bionic,直接执行报 `cannot execute: required file not found`。我在红米 K40(Android 13,内核 4.19.157)上把它跑通了,代价是踩了四个坑。

在 Termux 里跑华为云 KooCLI:一次跨越 glibc、DNS、CA 和 seccomp 的适配

摘要(三句话版)

华为云 KooCLI(hcloud)官方只提供 glibc 动态链接的 Linux 二进制,Termux 用的是 Android Bionic,直接执行报 cannot execute: required file not found。我在红米 K40(Android 13,内核 4.19.157)上把它跑通了,代价是踩了四个坑:glibc loader、$USER、/etc/resolv.conf、CA 证书。本文把每一步的命令、报错原文和根因都摊开讲,并额外给出一个更硬的结论——Android seccomp 白名单里没有 faccessat2,连 hcloud 自己在 obsutil 子命令路径上都会崩溃,而这个缺陷 Go 官方只在 GOOS=android 编译时规避。

本文适合谁:想在 Android 手机上做云资源运维、或者需要理解"为什么同一个 Go 二进制,一半命令能跑一半崩溃"的开发者。
数据截止:2026 年 10 月 23 日。所有命令均在本机实测,报错为原文复制;数字对应文末来源。

关键数字一览

数字 含义 来源
4.19.157 测试机内核版本(faccessat2 需要 5.8+) 本机 uname -a
40.2 MB hcloud-real 二进制体积 本机 ls -l
7.3.12 实测 KooCLI 版本 hcloud version
1.26.1 hcloud-real 的 Go 编译器版本 strings 提取
68 / 0 hcloud version 的 faccessat / faccessat2 调用次数 strace
30 / 2 hcloud OBS 的 faccessat / faccessat2 调用次数 strace
4 需要修掉的适配坑数量 本文第二至五节
0.25 元 一次完整的 ECS 建-验-删闭环实际花费(账单分两笔:0.22 + 0.03) 费用中心

四个坑一览

# 坑 报错原文 解法
1 glibc 动态加载器缺失 cannot execute: required file not found grun(glibc-runner)
2 $USER 未设置 user: Current requires cgo or $USER set in environment export USER=root
3 DNS 解析失败 lookup ... on [::1]:53: connection refused proot bind mount resolv.conf
4 CA 证书缺失 x509: certificate signed by unknown authority SSL_CERT_FILE

一、为什么会有这个问题:Bionic 与 glibc 的根本冲突

Termux 不是"Android 上的 Linux 发行版",它是跑在 Android 内核上的一个普通应用进程。它的 libc 是 Android 的 Bionic,不是主流 Linux 的 glibc。两者在动态加载器上就不兼容:

标准 Linux(glibc) Termux(Bionic)
动态加载器路径 /lib/ld-linux-aarch64.so.1 /system/bin/linker64
C 库 libc.so.6 libc.so(Bionic)
LD_PRELOAD 常规机制 被 libtermux-exec.so 占用做可执行文件路径重写

hcloud 官方二进制的 readelf 输出是明确的:

[Requesting program interpreter: /lib/ld-linux-aarch64.so.1]

这个路径在 Termux 里不存在,所以内核在 execve 阶段就直接拒绝,报的错是 cannot execute: required file not found——注意这个措辞,它容易让人误以为二进制文件本身丢了,其实是加载器找不到。

一句话结论:这不是"缺少依赖库",是内核层面找不到 ELF interpreter,装多少个 .so 都没用。

二、坑 1 与坑 2:先补加载器,再补 $USER

glibc-runner 是 termux-pacman 项目提供的脚本层[9],核心动作是用 patchelf 改写二进制的 interpreter 指向和 rpath。官方 wiki 的原话是[1]:

Glibc-runner allows you to run such binaries with $PREFIX/glibc/bin/ld.so or set the dynamic linker path and Glibc-based libraries directory path with patchelf.

它提供两种用法:grun ./binary(临时包装)和 grun --set ./binary(永久改写)。我用的是前者,不污染原文件。

前置检查(本机已装,直接可用):

pkg list-installed | grep -iE "glibc|patchelf"
# glibc-repo/stable,now 1.0 all [installed]
# glibc-runner/glibc,now 2.0-3 all [installed]
# glibc/glibc,now 2.44 aarch64 [installed]
# patchelf/now 0.19.1 aarch64 [installed]

下载官方二进制(ARM64 版本,来源为华为云官方 OBS[2]):

curl -LO "https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest/huaweicloud-cli-linux-arm64.tar.gz"
tar -zxvf huaweicloud-cli-linux-arm64.tar.gz
# 解出 hcloud / README.md / OpenSourceSoftwareNotice.md

第一次运行,撞上坑 2:

grun ./hcloud version
# [CLI_ERROR]Failed to obtain home directory of current user.
#   Cause: user: Current requires cgo or $USER set in environment

坑 2 是 Go 运行时的特性:os.UserHomeDir() 在无 cgo 构建下优先读 $USER 环境变量,然后去 /etc/passwd 里按用户名查家目录。而 Termux 的默认 shell 里 USER 确实是空的(本机实测 env | grep ^USER= 无输出),HOME 倒是有值——但这条路径先看 USER,空就直接报错说 “requires cgo or $USER set”。

即便 USER 设了,还得能在 /etc/passwd 里查到。Android 的 /etc/passwd 是个 0 字节空文件(实测 wc -l /system/etc/passwd = 0),所以 HOME 最好显式给出来:

env | grep -E "^USER=|^HOME="
# HOME=/data/data/com.termux/files/home      ← 只有 HOME,没有 USER

# 两个都不设:直接报错
grun ./hcloud version
# [CLI_ERROR]Failed to obtain home directory of current user.
#   Cause: user: Current requires cgo or $USER set in environment

# 只设 USER:能跑起来(实测通过)
USER=root grun ./hcloud version
# Current KooCLI version: 7.3.12

# 两个都给:本文采用的方式
USER=root HOME=$HOME grun ./hcloud version
# KooCLI will connect to the internet through HTTPS to manage your cloud resources...
# Agree and continue (y)/Disagree and exit (N):

到这一步二进制已经跑起来了,只差首次运行的隐私声明确认:

echo "y" | USER=root HOME=$HOME grun ./hcloud version
# Current KooCLI version: 7.3.12

一句话结论:坑 1 和坑 2 都是"能跑起来"级别的,一个 grun 加一个 export 就够。

三、坑 3:DNS 为什么在 glibc 环境里能用、在 hcloud 里不能

这是整篇文章里最反直觉的一处。现象是:

# glibc 环境里的 getent 能正常解析
USER=root HOME=$HOME grun /data/data/com.termux/files/usr/glibc/bin/getent hosts huaweicloud.com
# 119.3.120.194   huaweicloud.com

# 但 hcloud 报 DNS 失败(首次运行、无元数据缓存时)
hcloud ECS --help
# [APIE_ERROR]Failed to obtain service list.
#   Cause: dial tcp: lookup apiexplorer.cn-north-4.myhuaweicloud.com on [::1]:53:
#          read udp [::1]:49867->[::1]:53: read: connection refused

同一个 glibc 环境,为什么一个能解析一个不能?

我一开始的猜测是"LD_PRELOAD 重写路径,只对 libc 程序有效,对 Go 静态二进制无效"。这个猜测是错的,用 strace 一查就露馅了。

先看 getent 实际打开的是哪个文件:

strace -f -e trace=openat -o g.log \
  grun $PREFIX/glibc/bin/getent hosts huaweicloud.com

grep resolv g.log
# openat(AT_FDCWD, "/data/data/com.termux/files/usr/glibc/etc/resolv.conf", O_RDONLY) = 3

它读的是 $PREFIX/glibc/etc/resolv.conf,不是 /etc/resolv.conf。 这不是运行时重写的结果——glibc 在编译时就把 sysconfdir 定死成了 $PREFIX/glibc/etc,字符串直接烧在 libc.so.6 里:

strings $PREFIX/glibc/lib/libc.so.6 | grep "glibc/etc/"
# /data/data/com.termux/files/usr/glibc/etc/resolv.conf
# /data/data/com.termux/files/usr/glibc/etc/hosts
# /data/data/com.termux/files/usr/glibc/etc/nsswitch.conf

而 $PREFIX/glibc/etc/resolv.conf 本身是个软链,指向 Termux 的配置:

ls -l $PREFIX/glibc/etc/resolv.conf
# ... -> /data/data/com.termux/files/usr/etc/resolv.conf

LD_PRELOAD 在这里完全没参与。 顺带也排除了这个假设:libtermux-exec 的 strings 里只有 /bin/、/usr/bin 这类可执行文件路径的重写规则,压根没有 /etc/ 的映射表。

再看 hcloud 打开的是哪个:

strace -f -e trace=openat -o h.log \
  grun $HOME/.local/bin/hcloud-real ECS ListServersDetails --cli-region=cn-north-4

grep resolv h.log
# openat(AT_FDCWD, "/etc/resolv.conf", O_RDONLY) = -1 ENOENT (No such file or directory)

硬编码 /etc/resolv.conf,直接 ENOENT。 而 Termux 里 /etc 是个指向 /system/etc 的只读软链,那里面没有 resolv.conf:

ls -ld /etc
# lrw-r--r--. 1 root root 11 Jan  1  2009 /etc -> /system/etc

ls /system/etc/resolv.conf
# ls: cannot access '/system/etc/resolv.conf': No such file or directory

touch /system/etc/.probe
# touch: cannot touch '/system/etc/.probe': Read-only file system

所以真正的根因不是"LD_PRELOAD 救不了 Go",而是两条路径压根不同:

读哪个文件 怎么决定的
glibc 的 getent $PREFIX/glibc/etc/resolv.conf 编译期 sysconfdir 常量
Go 的 hcloud /etc/resolv.conf Go resolver 硬编码的绝对路径

Go 的 net 包在 Linux 上把 /etc/resolv.conf 写成了绝对路径,不看 sysconfdir,也不读任何环境变量。读到 ENOENT 之后它不报错,而是静默使用一份内置默认配置——实测报错里的 [::1]:53 就是这份默认配置的产物,本机 IPv6 回环上没有 DNS 服务在听:

dial tcp: lookup ... on [::1]:53: read udp [::1]:49867->[::1]:53: read: connection refused

(关于 Go 具体用哪几个默认 nameserver、代码在哪一行,本文不引源码——这里只陈述 strace 和报错原文能证明的部分:它确实尝试了 [::1]:53。)

注意一个无效尝试:GODEBUG=netdns=cgo 不管用,因为二进制里根本没编译进 cgo resolver:

GODEBUG=netdns=cgo hcloud ECS --help
# 同样的 [::1]:53 报错

有效解法是 proot 的 bind mount:

proot \
  -b /data/data/com.termux/files/usr/etc/resolv.conf:/etc/resolv.conf \
  -b /data/data/com.termux/files/usr/etc/hosts:/etc/hosts \
  grun /path/to/hcloud-real ECS --help

这一幕是真实历史:--help 报 DNS 错只发生在首次运行(服务元数据还没缓存进 ~/.hcloud/metaRepo/)。之后 --help 会命中缓存不再走网络——所以判断 DNS 是否修好,必须用真实 API 调用,别用 --help。这一步之所以能证明"DNS 通了",是因为报错从 DNS 变成了证书,说明它确实联网了:

# tls: failed to verify certificate: x509: certificate signed by unknown authority

一句话结论:不是 LD_PRELOAD 失效,是两条路径从一开始就不同——glibc 的 sysconfdir 被编译成 $PREFIX/glibc/etc,而 Go 的 resolver 把 /etc/resolv.conf 写死在代码里。proot 的 bind mount 是我实测有效的解法(理论上也可以改 /etc 软链或重编译走 cgo,但那两条我没验证,且 /system/etc 是只读的,改不了)。

四、坑 4:CA 证书与 SSL_CERT_FILE

Go 的 crypto/x509 会在一组固定的系统路径下找 CA 证书(例如 /etc/ssl/certs/ca-certificates.crt 这类发行版标准位置),而 Termux 把证书放在 $PREFIX/etc/tls/cert.pem——不在那份清单里。所以即使 DNS 通了,TLS 握手仍然失败:

# 加上 proot bind mount 后,报错从 DNS 变成了证书
tls: failed to verify certificate: x509: certificate signed by unknown authority

Go 支持用 SSL_CERT_FILE 环境变量覆盖这个查找路径:

export SSL_CERT_FILE="$PREFIX/etc/tls/cert.pem"

(本机实测该文件存在;glibc 包另有一份在 $PREFIX/glibc/etc/ssl/certs/ca-certificates.crt,两者都可作来源。)

四个坑全通之后,真实 API 调用成功:

hcloud ECS ListServersDetails --cli-region=cn-north-4
# {
#   "count": 0,
#   "servers": []
# }

一句话结论:四个坑里三个是环境变量/路径问题,只有坑 3 是真正的架构性冲突,必须靠 proot 解决。

五、最终形态:一个包装器脚本

把四个修复固化进 ~/.local/bin/hcloud,原二进制改名 hcloud-real:

#!/data/data/com.termux/files/usr/bin/bash
PREFIX_TERMUX="${PREFIX:-/data/data/com.termux/files/usr}"
HCLOUD_REAL="$HOME/.local/bin/hcloud-real"

export USER="${USER:-root}"
export HOME="${HOME:-/data/data/com.termux/files/home}"
export SSL_CERT_FILE="${SSL_CERT_FILE:-$PREFIX_TERMUX/etc/tls/cert.pem}"

exec proot \
    -b "$PREFIX_TERMUX/etc/resolv.conf:/etc/resolv.conf" \
    -b "$PREFIX_TERMUX/etc/hosts:/etc/hosts" \
    grun "$HCLOUD_REAL" "$@"

然后正常用:

hcloud configure set \
  --cli-access-key="$AK" --cli-secret-key="$SK" \
  --cli-region="cn-north-4" --cli-mode=AKSK
hcloud configure show
hcloud ECS ListServersDetails --cli-region=cn-north-4

六、seccomp 盲区:hcloud 的 OBS 子命令自己就会崩

先说一个我原先没料到的事实:hcloud 自己也不是全绿的。 包装器搭好之后我以为万事大吉,直到拿一个没缓存过的服务名去试:

grun ~/.local/bin/hcloud-real OBS
# SIGSYS: bad system call
# PC=0x1b6c0 m=6 sigcode=1

hcloud OBS 走的是内置 obsutil 子命令,它在 spawn 子进程前后会对自己的可执行文件做一次可执行性检查,参数是 X_OK | AT_EACCESS——这个检查路径正好落在 faccessat2 上。strace 抓到的原文:

faccessat2(AT_FDCWD, "/data/data/com.termux/files/home/.local/bin/hcloud-real", X_OK, AT_EACCESS) = -1 ENETDOWN
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_syscall=__NR_faccessat2} ---

包装器里那个 proot 顺手把它兜住了——hcloud OBS 经包装器调用就正常输出 obsutil 的帮助。这一点很关键:proot 在本方案里不只是"修 DNS",它同时是 seccomp 的绕行层。

(这不是 hcloud 特有的毛病。同样的现象在别的 glibc Go 二进制上也存在,比如华为云的 devbridge 在 auth status 路径同样踩 faccessat2 崩溃——这是这类二进制的通病,hcloud 只是我们手里的这一个。)

SIGSYS 不是段错误,是 seccomp 主动杀进程。 根因链条如下。

第一步:确认故障面。 用 strace 对比 hcloud 两条命令:

命令 faccessat faccessat2 结果
hcloud version 68 次 0 次 ✅ 正常
hcloud OBS 30 次 2 次 ❌ 第 2 次即 SIGSYS

结论:不是二进制整体不可用,而是"只要代码路径走到 faccessat2 就死"。 hcloud 常用的 ECS/VPC/EIP 路径恰好不碰它,但 OBS 子命令要拉起 obsutil,恰好踩中。

第二步:为什么 Android 不允许 faccessat2。 AOSP 的 seccomp 策略是白名单制,算法写在 SECCOMP_BLOCKLIST_APP.TXT 的注释里[3]:

The final seccomp allowlist is SYSCALLS.TXT - SECCOMP_BLOCKLIST.TXT + SECCOMP_ALLOWLIST.TXT

而 SYSCALLS.TXT 里关于 faccessat 只有一行[3]:

__faccessat:faccessat(int, const char*, int)  all

只有 faccessat,没有 faccessat2。 所以 faccessat2 不在白名单内,触发 seccomp 动作。Go 官方 issue #57393 里,Go 团队成员 prattmic 的判断与此一致[4]:

In this case I believe faccess2 is not allowed simply because it isn’t defined in the base SYSCALLS.TXT yet (it is a fairly new syscall).

第三步:faccessat2 是什么时候的 syscall。 它是 Linux 5.8 引入的(2020 年)。测试机内核是 4.19.157,内核本身就没有这个 syscall。正常情况下应该返回 ENOSYS,Go 就会自动回退到旧的 faccessat。但 Android 的 seccomp 策略不是返回错误码,而是直接发信号终止进程——strace 里看到的 si_code=SYS_SECCOMP 就是这个含义。Go 的回退逻辑因此没机会执行。

Go 团队在 issue 里明确说过这一点[4]:

If faccessat2 is not supported then we expect that system call to return ENOSYS or EPERM. We certainly don’t expect the program to be killed by a signal.

第四步,也是最关键的一步:Go 1.20 修了,但没修到我们头上。

Go 确实在 1.20 修了这个问题(CL 458495,syscall: don't use faccessat2 on android[10])。但看修复后的源码[5]:

// Don't attempt on Android, which does not allow faccessat2 through
// its seccomp policy [1] on any version of Android as of 2022-12-20.
if runtime.GOOS != "android" {
    if err := faccessat2(dirfd, path, mode, flags); err != ENOSYS && err != EPERM {
        return err
    }
}

这是编译期条件。 只有 GOOS=android 编译出来的二进制才跳过 faccessat2。

而 hcloud-real 的 strings 输出明确显示它是 linux/arm64 的 go1.26.1 二进制:

strings hcloud-real | grep -oE "go1\.[0-9]+(\.[0-9]+)?" | sort -u
# go1.26
# go1.26.1

Go 版本远超 1.20,但因为编译目标是 linux 而不是 android,那段规避代码根本不存在于二进制里。

一句话结论:Go 官方的修复对"跑在 Android 上的 linux/arm64 二进制"完全无效。这是 GOOS 与"实际运行环境"错配导致的系统性盲区。

第五步:绕过它只需要 proot,不需要完整的 proot-distro。 proot 用 ptrace 在用户态接管子进程的 syscall,自己模拟执行,syscall 不再原样落到内核的 seccomp 过滤器上。这一点有实测支撑——同一个 hcloud OBS,直连裸二进制崩,套上包装器就正常:

# 直连(无 proot):崩
USER=root HOME=$HOME SSL_CERT_FILE=$PREFIX/etc/tls/cert.pem \
  grun ~/.local/bin/hcloud-real OBS
# SIGSYS: bad system call

# 经包装器(带 proot):正常输出 obsutil 帮助
hcloud OBS
# Usage: obsutil [command] [args...] [options...]

结论:hcloud 包装器里为了修 DNS 而加的 proot,同时就是 seccomp 的绕行层。 一个 proot 同时解决 DNS 与 seccomp,不必额外装 proot-distro 或任何完整发行版。

一句话结论:grun 解决 glibc 加载,proot 解决 seccomp(也顺带解决 DNS);两者各管一段,不能互相替代。同一个 hcloud 包装器脚本把两件事一次做完了。

七、一个完整的实战闭环:建 ECS、验站、提交、删除

跑通工具链之后,我用它完整走了一遍华为云的"2048 闯关"活动:从创建资源到公网验证通过约 6 分钟。命令序列如下(凭据从文件读取,不出现在命令行历史里)。

账单实测(费用中心,2026-10-23):一次完整的建-验-删闭环,现金支出累计 ¥0.25(按量付费整点结算,跨结算点被拆成 0.22 + 0.03 两笔)。判断"到底花了多少"要看「现金支出」累计值,不是「本月应付」——后者只反映已结算部分。

补子网 DNS(云主机 cloud-init 装软件的前提):

hcloud VPC UpdateSubnet --cli-region=cn-north-4 \
  --subnet_id=2ecc42f7-1513-44a2-91c8-a09c3bb58ab5 \
  --vpc_id=aaa40c74-864a-4381-9bec-0ca31d5caeeb \
  --subnet.name=subnet-default-smb \
  --subnet.dnsList.1=100.125.1.250 \
  --subnet.dnsList.2=100.125.129.250

建安全组——这里踩到一个官方教程的坑:文档写的是 CreateSecurityGroup --security_group.vpc_id=...,但 KooCLI 默认走 v3 版本,v3 的 CreateSecurityGroup 不接受 vpc_id:

# 官方教程的写法,报错
hcloud VPC CreateSecurityGroup --security_group.vpc_id=... --security_group.name=game2048-sg
# [USE_ERROR]Invalid parameter: security_group.vpc_id

# 正确写法:显式指定 v2
hcloud VPC CreateSecurityGroup/v2 --cli-region=cn-north-4 \
  --security_group.vpc_id=aaa40c74-864a-4381-9bec-0ca31d5caeeb \
  --security_group.name=game2048-sg
# → security_group.id = 712403da-...

同理,加规则时 v3 用的是 multiport 而不是 port_range_min/max:

hcloud VPC CreateSecurityGroupRule --cli-region=cn-north-4 \
  --security_group_rule.security_group_id=712403da-... \
  --security_group_rule.direction=ingress \
  --security_group_rule.protocol=tcp \
  --security_group_rule.ethertype=IPv4 \
  --security_group_rule.multiport=80 \
  --security_group_rule.remote_ip_prefix=0.0.0.0/0

建 EIP(按流量计费,而不是按带宽——本例页面只有 4 KB,出云流量费实测趋近于零[6]:按量流量费 0.64 元/GB、IP 保有费 0.02 元/小时):

hcloud EIP CreatePublicip --cli-region=cn-north-4 \
  --publicip.type=5_bgp --publicip.ip_version=4 \
  --bandwidth.share_type=PER --bandwidth.name=game2048-bw \
  --bandwidth.size=1 --bandwidth.charge_mode=traffic
# → public_ip_address = 120.46.80.79

ECS dry-run 先验参数(零成本):

hcloud ECS CreateServers --cli-region=cn-north-4 --dry_run=true \
  --server.name=game2048-web \
  --server.imageRef=dd7fba37-0ce2-465b-bae3-73319fcb64f0 \
  --server.flavorRef=s6.medium.2 \
  --server.vpcid=aaa40c74-864a-4381-9bec-0ca31d5caeeb \
  --server.nics.1.subnet_id=2ecc42f7-1513-44a2-91c8-a09c3bb58ab5 \
  --server.security_groups.1.id=712403da-5511-4160-97a7-47e664dbdc7c \
  --server.availability_zone=cn-north-4a \
  --server.root_volume.volumetype=SSD \
  --server.root_volume.size=40
# 返回 202 = 参数校验通过,未创建

绑 EIP 时又踩一个坑:UpdatePublicip 要的是网卡 port_id,不是 server_id。传 server_id 会报 PortNotFound:

# 从 ECS 反查 port_id(v2 版本才有 device_id 参数)
hcloud VPC ListPorts/v2 --cli-region=cn-north-4 --device_id=<SERVER_ID>
# → ports[0].id = b314da8a-e561-4064-9b32-b85b882e55c0

hcloud EIP UpdatePublicip --cli-region=cn-north-4 \
  --publicip_id=bffff683-17de-4047-8de5-14823415d797 \
  --publicip.port_id=b314da8a-e561-4064-9b32-b85b882e55c0

验证与删除:

curl -sI http://120.46.80.79/hc-activity/index.html
# HTTP/1.1 200 OK
# Server: nginx/1.20.1
# Content-Length: 3988

hcloud ECS DeleteServers --cli-region=cn-north-4 \
  --servers.1.id=<SERVER_ID> \
  --delete_publicip=true --delete_volume=true

两个删除开关必须显式给。API 文档明确:--delete_volume 默认 false(只卸载不删盘,盘继续计费),--delete_publicip 不传时取决于 EIP 的 delete_on_termination 属性。实测即使传了 --delete_publicip=true,EIP 仍残留为 DOWN 状态,需要单独再删一次:

hcloud EIP DeletePublicip --cli-region=cn-north-4 --publicip_id=<EIP_ID>

清理后必须逐项核验,不能只看删除命令返回成功。实测的核验结果:

hcloud ECS ListServersDetails --cli-region=cn-north-4 | grep '"count"'
#   "count": 0,

hcloud EVS ListVolumes --cli-region=cn-north-4 | grep '"count"'
#   "count": 0,

hcloud EIP ListPublicips --cli-region=cn-north-4 | grep -E '"total_count"'
#   "total_count": 0,

hcloud VPC ListSecurityGroups/v2 --cli-region=cn-north-4 | grep '"name"'
#   "name": "sg-default-smb",     ← 系统默认,非本次创建
#   "name": "Sys-default",        ← 系统默认,非本次创建

注意一个易混淆点:费用中心总览页的「近 7 天扣费资源」会显示"云硬盘 1、弹性云服务器 1",但这只是过去 7 天的账单统计,不代表资源还在。判断资源是否存活必须查 API 的实时列表,不能看这个统计块。

计费粒度值得记住:ECS 与 EIP 的按量付费都是秒级计费、整点结算[6][11]。关机不等于停止计费——vCPU 和内存不计费了,但云硬盘和带宽照扣[7]。想真正止损只有删除。

一句话结论:这个闭环的实操成本 0.25 元,但如果不删 EIP 和磁盘,会持续产生费用,且账单要等下一个整点才出现。

八、这套方案的能力边界

诚实地说清楚哪些行、哪些不行。

能力 状态 原因
hcloud 常用服务(ECS/VPC/EIP/EVS/IMS/CES…) ✅ 可用 实测这些路径不触发 faccessat2
hcloud OBS(直连,不经包装器) ❌ 崩溃 实测 spawn 子进程前后 faccessat2(X_OK, AT_EACCESS)
hcloud 经包装器(带 proot) ✅ 实测可用 proot 让 syscall 不原样落到 seccomp(含会崩的 OBS 路径)
手机端跑 Docker ❌ 不推荐 Termux 无内核容器支持,需 proot 套娃

这不是"潜伏风险",而是已经复现的故障。 hcloud 直连跑 OBS 服务名实测就崩。strace 能直接证明的只有两点:调用确实发生在 spawn 子进程前后、参数确实是 X_OK | AT_EACCESS、作用对象正是 hcloud-real 自己。至于 Go 在 os/exec 的哪一行发起这个检查,本文不引源码(本机当前网络下源码站不可达)。对读者的实际含义:只要某个功能要去找/拉起外部程序,就可能中招——obsutil 是最典型的例子。

实操建议:所有 hcloud 调用都走包装器,别直连 hcloud-real。包装器里的 proot 同时解决 DNS 和 seccomp 两件事,成本是每次调用多一点启动开销。

九、这件事真正的启示

一、GOOS 与运行环境的错配是系统性盲区。 Go 官方在 1.20 修了 faccessat2,但只对 GOOS=android 生效。所有"为 Linux 编译、在 Android 上跑"的二进制——包括 Termux 里装的绝大多数预编译工具——都继承这个缺陷。这不是华为云的问题,是整个生态的盲区。

二、不要把"能跑"归因于一个没验证的机制。 坑 3 我一开始认定是 LD_PRELOAD 的问题,写了一段很顺的因果解释,结果 strace 一查完全不成立:getent 读的是 $PREFIX/glibc/etc/resolv.conf(编译期常量),hcloud 读的是 /etc/resolv.conf(硬编码),两条路径本来就不同,跟 LD_PRELOAD 一点关系都没有。一个自洽的解释不等于正确的解释,strace 一条命令就能证伪。

三、官方文档的 CLI 示例可能已经过期。 这次撞了两次:CreateSecurityGroup 的 v3/v2 差异、CreateSecurityGroupRule 的 multiport vs port_range_*。动手前先 hcloud <Service> <Operation> --help 看当前参数,比照抄文档可靠。

四、闭源工具没有"重编译绕过"这条退路。 我确认过 huaweicloud/huaweicloud-cli 仓库[8]——只有 README.md(322 字节)、LICENSE、CONTRIBUTING.md 和 .github/ 模板,没有一行 Go 源码。所以"用 CGO 重新编译"这个思路(理论上 CGO 版会走 libc 的 access() 而非直接 syscall)虽然方向对,实际上无从下手。

十、如果你也要在 Termux 上跑云 CLI:五步清单

  1. 先判类型:readelf -l <binary> | grep INTERP。看到 /lib/ld-linux-* 就是 glibc 动态链接,需要 grun;看到 /system/bin/linker64 才是 Bionic 原生。
  2. 备好 glibc-runner:pkg install glibc-repo glibc-runner,然后 grun ./binary 先试最简单的 --version。
  3. 环境变量三件套:USER、HOME、SSL_CERT_FILE。少任何一个都会在某个点报错。
  4. DNS 单独处理:如果程序是 Go 静态编译(strings | grep "go1\." 有输出且 ldd 报 “not a dynamic executable”),直接上 proot -b resolv.conf:/etc/resolv.conf。
  5. 遇 SIGSYS 就上 strace:strace -f -e trace=faccessat,faccessat2 能一眼看出是哪个 syscall。确认是 faccessat2 后,把命令套进 proot -b resolv.conf:/etc/resolv.conf -b hosts:/etc/hosts grun <binary> 即可——不用装 proot-distro。另外,凡是功能内部要拉起子进程的(如 hcloud OBS 的 obsutil),直连裸二进制当场就崩,所以统一走包装器最省心。

附录 A:本文实测环境的完整命令记录

按执行顺序,全部为原文复制(凭据已脱敏)。

A-1 环境确认

uname -a
# Linux localhost 4.19.157-perf-g92c089fc2d37 #1 SMP PREEMPT ... aarch64 Android

grep -i seccomp /proc/self/status
# Seccomp:	2

readelf -l hcloud | grep -A1 INTERP
# [Requesting program interpreter: /lib/ld-linux-aarch64.so.1]

strings hcloud | grep -oE "go1\.[0-9]+(\.[0-9]+)?" | sort -u
# go1.26
# go1.26.1

A-2 坑 1 与坑 2

./hcloud version
# bash: ./hcloud: cannot execute: required file not found

grun ./hcloud version
# [CLI_ERROR]Failed to obtain home directory of current user.
#   Cause: user: Current requires cgo or $USER set in environment

echo "y" | USER=root HOME=$HOME grun ./hcloud version
# Current KooCLI version: 7.3.12

A-3 坑 3 与坑 4(注意:下面前两条报错发生在首次运行时;KooCLI 会把服务元数据缓存在 ~/.hcloud/metaRepo/,之后 --help 走缓存就不再需要 DNS。判断 DNS 是否真的修好,要看真实 API 调用,不要用 --help。)

# glibc 环境 DNS 正常
USER=root HOME=$HOME grun $PREFIX/glibc/bin/getent hosts huaweicloud.com
# 119.3.120.194   huaweicloud.com

# 但 hcloud 首次运行(无缓存)失败
hcloud ECS --help
# [APIE_ERROR]Failed to obtain service list. Cause: dial tcp:
#   lookup apiexplorer.cn-north-4.myhuaweicloud.com on [::1]:53:
#   read udp [::1]:49867->[::1]:53: read: connection refused

# GODEBUG 无效
GODEBUG=netdns=cgo hcloud ECS --help
# 同样报错

# 真实 API 调用(不走缓存的试金石):无 proot 时仍失败
USER=root HOME=$HOME grun hcloud-real ECS ListServersDetails --cli-region=cn-north-4
# Failed to obtain project ID. Cause: dial tcp: lookup iam.cn-north-4... on [::1]:53: connection refused

# proot bind mount 之后 DNS 通,报错变成证书问题
proot -b $PREFIX/etc/resolv.conf:/etc/resolv.conf \
      -b $PREFIX/etc/hosts:/etc/hosts \
      grun hcloud-real ECS ListServersDetails --cli-region=cn-north-4
# tls: failed to verify certificate: x509: certificate signed by unknown authority

# 加 SSL_CERT_FILE 后完全正常
SSL_CERT_FILE=$PREFIX/etc/tls/cert.pem proot ... grun hcloud-real ECS ListServersDetails --cli-region=cn-north-4
# { "count": 0, "servers": [] }

A-4 seccomp 对比实验(只针对 hcloud)

# hcloud version:68 次 faccessat,0 次 faccessat2
strace -f -e trace=faccessat,faccessat2 -o hcloud.log grun hcloud-real version
grep -c "faccessat2" hcloud.log   # 0
grep -c "faccessat(" hcloud.log   # 68

# hcloud OBS:30 次 faccessat、2 次 faccessat2,第 2 次 SIGSYS
strace -f -e trace=faccessat,faccessat2 -o obs.log grun hcloud-real OBS
# SIGSYS: bad system call
grep -c "faccessat2" obs.log      # 2
grep "faccessat2" obs.log         # faccessat2(AT_FDCWD, ".../hcloud-real", X_OK, AT_EACCESS) = -1 ENETDOWN

A-5 proot 绕过验证(与包装器完全相同的结构,无需 proot-distro)

# hcloud OBS 直连(无 proot):崩
USER=root HOME=$HOME SSL_CERT_FILE=$PREFIX/etc/tls/cert.pem \
  grun ~/.local/bin/hcloud-real OBS
# SIGSYS: bad system call

# 经包装器(带 proot):正常
hcloud OBS
# Usage: obsutil [command] [args...] [options...]

附录 B:尾注与来源

[1] termux-pacman/glibc-packages wiki, "About glibc runner (grun)". https://github-wiki-see.page/m/termux-pacman/glibc-packages/wiki/About-glibc-runner-(grun)

[2] 华为云 KooCLI 官方安装文档(Linux ARM64 下载地址), "在Linux系统上安装KooCLI". https://support.huaweicloud.com/qs-hcli/hcli_02_003_02.html

[3] AOSP Bionic 源码(`SYSCALLS.TXT` / `SECCOMP_BLOCKLIST_APP.TXT`). https://github.com/aosp-mirror/platform_bionic/tree/main/libc

[4] Go issue #57393, "syscall: Faccessat crashes because faccessat2 is blocked on Android", 2022-12-19. https://github.com/golang/go/issues/57393

[5] Go 源码 `src/syscall/syscall_linux.go`(`Faccessat` 实现,含 `runtime.GOOS != "android"` 条件). https://github.com/golang/go/blob/master/src/syscall/syscall_linux.go

[6] 华为云 EIP 计费文档, "按需计费"(秒级计费、整点结算;按量流量费 0.64 元/GB、IP 保有费 0.02 元/小时). https://support.huaweicloud.com/price-eip/eip_billing_0007.html

[7] 华为云 ECS FAQ, "弹性云服务器关机后还会计费吗?"(关机后云硬盘与带宽仍计费). https://support.huaweicloud.com/ecs_faq/zh-cn_topic_0018124776.html

[8] GitHub `huaweicloud/huaweicloud-cli` 仓库(仅模板文件,无 Go 源码). https://github.com/huaweicloud/huaweicloud-cli

[9] termux-pacman/glibc-packages 仓库(`glibc-runner` 脚本与 glibc 包构建定义). https://github.com/termux/glibc-packages

[10] Go CL 458495, "syscall: don't use faccessat2 on android". https://go-review.googlesource.com/c/go/+/458495

[11] 华为云 ECS 计费文档, "按需计费"(秒级计费,每小时出账单). https://support.huaweicloud.com/price-ecs/ecs_billing_2003.html

附录 C:版本与授权说明

  1. 本文所有命令均在本机实测,报错文本为终端原文复制,未做改写。
  2. hcloud(KooCLI)为闭源软件,官方只分发二进制。经核实,huaweicloud/huaweicloud-cli GitHub 仓库不含源码[8],因此本文不涉及任何源码修改,也不提供重新编译方案。
  3. 文中引用的 Go 源码片段(附录 B [5])为 Go 项目 BSD 风格许可,引用用于说明技术机制。
  4. AOSP SYSCALLS.TXT 引用([3])为 Apache-2.0 许可。
  5. 本文不含任何第三方图片。

本文所有数字均可在附录 B 的来源页中核对。文中的"坑"按实际踩坑顺序编号,不是按严重程度排序——真正卡住我的是坑 3(DNS),因为它牵涉 glibc 的编译期 sysconfdir 与 Go resolver 的硬编码路径这两套完全不同的机制。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。