Files
VoiletSshTools/README.md
T
liyp 4365e80075 sshtools: 非交互 SSH 工具集(golink / goscp)
golink: 一条命令在远端执行,退出码原样透传,从不提示、不分配终端。
goscp:  用 SCP 协议上传,-files 一个参数同时接受文件和目录。

- 纯 Go,唯一依赖 golang.org/x/crypto,可离线交叉编译到六个桌面平台
- internal/sshx 统一建连、认证(password + keyboard-interactive)与主机密钥策略
- smoketest/ 自带迷你 sshd,57 项端到端测试(golink 38 + goscp 19)
2026-10-19 10:22:53 +08:00

216 lines
8.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SSH_TOOLS
Go 实现的 SSH 命令行工具集,对标 PuTTY 的 plink / pscp:单个静态二进制,纯 Go,不依赖本机
OpenSSH,也不需要安装任何运行时。
当前包含 **golink**(对应 plink)和 **goscp**(对应 pscp 的上传方向)。
---
## golink
非交互 SSH 命令执行器:连上去、跑一条命令、把远端退出码原样带回来。
**从不提示密码、从不询问主机密钥、从不分配终端**,所以可以直接放进脚本和定时任务。
### 你要的那行,原样可用
```
golink user@host -p "password" -port 22 -command "命令"
```
Go 标准库的 flag 遇到第一个位置参数就停止解析,所以 golink 在解析前把参数重排了一遍:
位置参数在前、flag 在后、或者混着写,都能正确识别。
### 示例
```bash
# 最普通的一条
golink root@10.0.0.5 -p 'secret' -port 22 -command "uptime && df -h"
# 密码走环境变量,不出现在 ps 和 shell history 里
GOLINK_PASSWORD='secret' golink deploy@web1 -command "systemctl restart nginx"
# 远端退出码原样返回,脚本里可以直接判断
if golink ci@build1 -p "$PASS" -command "make test"; then echo 通过; else echo "失败: $?"; fi
# 本地 stdin 直接喂给远端命令
tar czf - ./src | golink root@backup -p "$PASS" -command "cat > /srv/src.tgz"
# 把远端输出接进本地管道
golink db@10.0.0.9 -p "$PASS" -command "cat /var/log/app.log" | grep ERROR
# 密钥登录(OpenSSH 格式私钥)
golink ops@bastion -i ~/.ssh/id_ed25519 -command hostname
# 校验主机指纹,避免中间人
golink ops@10.0.0.7 -p "$PASS" -hostkey "$(ssh-keyscan -p 22 10.0.0.7 2>/dev/null | ssh-keygen -lf - | awk '{print $2}')" -command true
# 命令也可以当位置参数跟在后面(plink 风格)
golink ops@web1 -p "$PASS" systemctl status nginx
```
### 参数
| 参数 | 默认 | 说明 |
| --- | --- | --- |
| `-p` / `-password` / `-pw` | 空 | SSH 密码;也可以用环境变量 `GOLINK_PASSWORD` |
| `-port` / `-P` | 22 | SSH 端口;也可以写在目标里 `host:2222` |
| `-l` | 空 | 登录用户,等价于 `user@host` 里的 user |
| `-command` | 空 | 远端执行的命令;后面的位置参数会自动拼上去 |
| `-i` | 空 | OpenSSH 格式私钥文件(不支持带口令的私钥,见下) |
| `-knownhosts` | 空 | known_hosts 文件;未知或变更的主机密钥直接失败,不会提示 |
| `-hostkey` | 空 | 要求的主机指纹,`SHA256:...` 或 MD5 均可 |
| `-timeout` | 15s | 只覆盖「建连 + 握手 + 认证」,命令跑多久都不受它限制 |
| `-v` | 关 | 把连接、主机指纹、认证过程打到 stderr,不污染 stdout |
| `-version` / `-h` | | 版本 / 帮助 |
目标写法支持 `user@host`、`user@host:2222`、`[::1]:2222`。
### 退出码
| 退出码 | 含义 |
| --- | --- |
| 0-255 | 远端命令的退出码原样返回(被信号杀死时按 shell 惯例返回 128+信号,如 SIGTERM → 143) |
| 2 | 用法错误:参数写错、没写用户、没写命令、没给任何凭据 |
| 255 | 连接、主机密钥或认证失败(和 OpenSSH 一致) |
stdout 只放远端命令的标准输出,远端 stderr 走本地 stderr,`-v` 的诊断信息也走 stderr,
所以 `golink ... | ...` 和 `golink ... > out.txt` 都是安全的。
### 多平台二进制
| 文件 | 平台 |
| --- | --- |
| `dist/golink_linux_amd64` | Linux x86-64 |
| `dist/golink_linux_arm64` | Linux ARM64 |
| `dist/golink_darwin_amd64` | macOS Intel |
| `dist/golink_darwin_arm64` | macOS Apple Silicon |
| `dist/golink_windows_amd64.exe` | Windows x86-64 |
| `dist/golink_windows_arm64.exe` | Windows ARM64 |
都是 `CGO_ENABLED=0` 的静态二进制,`chmod +x` 就能用;`dist/SHA256SUMS` 里有校验和。
### 重新编译
```bash
./build.sh # 六个平台全部编译到 dist/
./build.sh local # 只编当前平台,产出 dist/golink
VERSION=1.2.3 ./build.sh
```
唯一的依赖是 `golang.org/x/crypto`(本地 module cache 里已经有 v0.41.0),所以**离线也能编译**。
注意 build.sh 默认把 `GOCACHE` 指到仓库内的 `.gocache/`:某些受限环境里 `~/.cache/go-build`
是只读的,不换目录 go 会以非零码退出。
### 自测
```bash
./smoketest/run.sh
```
38 项端到端检查,全部在本地跑,不需要外网。它会编译一个自带的迷你 sshd
(`smoketest/sshd`,支持 password / keyboard-interactive / publickey 三种认证),然后逐个
验证:参数顺序、stdin 管道、20 万字节回传、退出码透传(含 143/137)、stdout/stderr 分离、
主机指纹校验、known_hosts、错误密码快速失败、加密私钥报错、键盘交互登录等。
每个用例都关掉 stdin 并加了超时——只要哪里会等待交互输入,测试就会挂掉。
### Windows 提示
- cmd 里中文乱码:先 `chcp 65001`;PowerShell 7 一般没问题。
- 退出码在 cmd 里看 `%ERRORLEVEL%`,PowerShell 里看 `$LASTEXITCODE`。
- 首次运行可能被 SmartScreen / 杀软拦,需要放行。
### 主机密钥策略(重要)
默认**不校验**主机密钥(`InsecureIgnoreHostKey`),因为脚本无法回答「是否信任这台主机」的
交互提示——这跟 plink `-batch` 的取舍是一样的,代价是理论上可被中间人攻击。要防这一手:
- `-hostkey SHA256:xxxx`:写死指纹,不匹配就拒绝;
- `-knownhosts ~/.ssh/known_hosts`:未知或变更直接失败,绝不提示。
### 安全提示
密码写在命令行会出现在 `ps` 和 shell history 里(plink 的 `-pw` 同样如此)。写脚本时更推荐
`GOLINK_PASSWORD` 环境变量或 `-i` 密钥。带口令的私钥不支持:golink 不会交互式提问,遇到会
直接报错并提示先用 `ssh-keygen -p` 去掉口令。
### 目录结构
```
cmd/golink/ 命令行入口:参数、退出码映射
internal/cli/ 参数重排(让 flag 可以出现在任意位置)
internal/sshx/ 建连、认证、主机密钥策略(后续工具复用)
smoketest/sshd/ 测试用迷你 SSH 服务端
smoketest/run.sh 端到端测试
build.sh 交叉编译脚本
dist/ 产物
```
---
## goscp
把本地文件或目录传到远端,走 SCP 协议(远端跑 `scp -t`),不依赖 SFTP 子系统。
```bash
goscp [flags] [user@]host[:port] -files "dir or file" [-files ...] [-dest /remote/dir]
```
### 示例
```bash
# 传一个文件到远端家目录
goscp jarue@172.19.4.240 -p jarue -port 22 -files "build/output.bin"
# 传整个目录到指定位置(远端目录不存在会自动建)
goscp jarue@172.19.4.240 -p jarue -files "build" -dest /home/jarue/app
# 多个文件
goscp user@host -p "$PASS" -files a.txt -files b.txt -dest /srv/in
# 密码走环境变量
GOSCP_PASSWORD='secret' goscp user@host -files ./dist/
```
### 参数
| 参数 | 默认 | 说明 |
| --- | --- | --- |
| `-files` | 空 | 要上传的本地文件**或目录**,同一套参数自动识别;可重复,尾随位置参数也算 |
| `-dest` | 远端家目录 | 远端目标目录,不存在会自动 `mkdir -p` |
| `-p` / `-password` / `-pw` | 空 | SSH 密码;也可用 `GOSCP_PASSWORD` / `GOLINK_PASSWORD` |
| `-port` / `-P` | 22 | SSH 端口 |
| `-l` | 空 | 登录用户 |
| `-i` | 空 | OpenSSH 格式私钥 |
| `-knownhosts` | 空 | known_hosts 文件;未知或变更直接失败 |
| `-hostkey` | 空 | 要求的主机指纹 |
| `-timeout` | 15s | 建连 + 认证超时,不含传输 |
| `-v` | 关 | 逐条列出上传的文件和大小 |
| `-q` | 关 | 不打印最后那行汇总 |
### 退出码
| 退出码 | 含义 |
| --- | --- |
| 0 | 全部成功 |
| 1 | 传输失败(本地路径不存在、远端 scp 报错、权限不足) |
| 2 | 用法错误(参数、目标、没给文件、没给凭据) |
| 255 | 连接、主机密钥或认证失败 |
### 行为说明
- **文件和目录同一条命令**:`-files` 给什么就传什么,目录自动递归,不需要 `-r` 之类的开关
- 目录保留相对结构:`-files build -dest /srv` → `/srv/build/...`
- 权限:普通文件 0644,本地本身有执行位的保持 0755,目录 0755
- 符号链接:命令行上直接点名的会跟随;目录内部遇到的跳过并在 stderr 提示
- 文件名含空格没问题(SCP 头按行解析);含换行的不支持
- 传输是流式的,1 MiB 文件实测逐字节一致
### 还没做
- 下载方向(远端 → 本地)
- 断点续传、并行传输、进速显示
- SFTP 后端(需要额外依赖,目前零依赖)
- 跳板机(`-J`)、端口转发、交互式 shell / pty