Files
VoiletSshTools/README.md
T
liyp c3af8b4ab9 docs: README 改为英文默认,中文版移到 README.zh-CN.md
- README.md 英文,顶部有简体中文链接
- README.zh-CN.md 中文,顶部有 English 链接
- 补上安装/发布下载、macOS quarantine、CI 只跑 GitHub 的说明
2026-10-23 13:19:06 +08:00

235 lines
8.9 KiB
Markdown

# sshtools
Non-interactive SSH command-line tools for scripts: **golink** replaces PuTTY's
`plink`, **goscp** replaces the upload half of `pscp`.
Plain Go, `CGO_ENABLED=0`, one static binary per platform. The SSH stack and all
cryptography (RSA, ECDSA, Ed25519, AES-GCM, ChaCha20-Poly1305, SHA-2) are Go's
own implementation compiled into the binary -- nothing links against the system
OpenSSL and nothing has to be installed alongside it.
**English** | [简体中文](README.zh-CN.md)
## Install
Download the binary for your platform from the
[latest release](../../releases/latest), then `chmod +x` it.
| Platform | golink | goscp |
| --- | --- | --- |
| Windows x86-64 | `golink_windows_amd64.exe` | `goscp_windows_amd64.exe` |
| Windows ARM64 | `golink_windows_arm64.exe` | `goscp_windows_arm64.exe` |
| Linux x86-64 | `golink_linux_amd64` | `goscp_linux_amd64` |
| Linux ARM64 | `golink_linux_arm64` | `goscp_linux_arm64` |
| macOS Intel | `golink_darwin_amd64` | `goscp_darwin_amd64` |
| macOS Apple Silicon | `golink_darwin_arm64` | `goscp_darwin_arm64` |
SHA256SUMS is attached to every release.
On macOS a downloaded binary carries the quarantine attribute; clear it once with
`xattr -d com.apple.quarantine ./golink`.
## The contract both tools share
- they never prompt: not for a password, not for a host key, not for anything;
- they never allocate a terminal or open an interactive shell;
- stdout carries only the remote program's stdout, diagnostics go to stderr;
- exit codes are meaningful (see each tool below);
- every credential is supplied up front, so they are safe inside cron jobs and CI.
## golink
Connect, run one command, and return that command's exit status.
```bash
golink user@host -p "password" -port 22 -command "command"
```
Go's `flag` package stops parsing at the first positional argument, so golink
reorders `argv` before parsing: the target may come first, last, or in between,
and flags may be written in any order.
### Examples
```bash
# the common case
golink root@10.0.0.5 -p 'secret' -port 22 -command "uptime && df -h"
# password from the environment, so it stays out of ps and shell history
GOLINK_PASSWORD='secret' golink deploy@web1 -command "systemctl restart nginx"
# the remote exit status is passed through, so this works
if golink ci@build1 -p "$PASS" -command "make test"; then echo passed; else echo "failed: $?"; fi
# feed local stdin to the remote command
tar czf - ./src | golink root@backup -p "$PASS" -command "cat > /srv/src.tgz"
# pipe remote output into a local filter
golink db@10.0.0.9 -p "$PASS" -command "cat /var/log/app.log" | grep ERROR
# key auth (OpenSSH private key, no passphrase)
golink ops@bastion -i ~/.ssh/id_ed25519 -command hostname
# pin the host key fingerprint
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
# the command may also be trailing positional arguments, plink style
golink ops@web1 -p "$PASS" systemctl status nginx
```
### Flags
| Flag | Default | Meaning |
| --- | --- | --- |
| `-p` / `-password` / `-pw` | empty | SSH password; `GOLINK_PASSWORD` works too |
| `-port` / `-P` | 22 | SSH port; `host:2222` in the target also works |
| `-l` | empty | login user, alternative to `user@host` |
| `-command` | empty | command to run; trailing positional arguments are appended |
| `-i` | empty | OpenSSH private key file (no passphrase, see below) |
| `-knownhosts` | empty | known_hosts file; unknown or changed keys fail, never prompt |
| `-hostkey` | empty | required host key fingerprint, `SHA256:...` or MD5 |
| `-timeout` | 15s | covers connect, handshake and authentication only, never the command |
| `-v` | off | connection diagnostics on stderr |
| `-version` / `-h` | | version / help |
Targets accept `user@host`, `user@host:2222` and `[::1]:2222`.
### Exit status
| Code | Meaning |
| --- | --- |
| 0-255 | the remote command's status, passed through (a killed command reports 128+signal, so SIGTERM gives 143) |
| 2 | usage error: bad flag, missing user, missing command, no credentials |
| 255 | connection, host key or authentication failure (same convention as OpenSSH) |
## goscp
Copy local files and directories to a remote host with the SCP protocol. The
remote side runs `scp -t`, so no SFTP subsystem is required.
```bash
goscp [flags] [user@]host[:port] -files "dir or file" [-files ...] [-dest /remote/dir]
```
One flag handles both files and directories: `-files` inspects the path and
either sends a single entry or walks the tree. There is no `-r` switch and no
separate mode.
### Examples
```bash
# one file, into the remote user's home directory
goscp user@host -p "$PASS" -port 22 -files "build/output.bin"
# a whole directory, into a specific place (created when missing)
goscp user@host -p "$PASS" -files "build" -dest /home/user/app
# several files, either repeated or trailing
goscp user@host -p "$PASS" -files a.txt -files b.txt -dest /srv/in
goscp user@host -p "$PASS" -files a.txt b.txt -dest /srv/in
# password from the environment
GOSCP_PASSWORD='secret' goscp user@host -files ./dist/
```
### Flags
| Flag | Default | Meaning |
| --- | --- | --- |
| `-files` | empty | local file **or** directory, detected automatically; repeatable, trailing arguments count too |
| `-dest` | remote home | remote target directory, created with `mkdir -p` when missing |
| `-p` / `-password` / `-pw` | empty | SSH password; `GOSCP_PASSWORD` or `GOLINK_PASSWORD` |
| `-port` / `-P` | 22 | SSH port |
| `-l` | empty | login user |
| `-i` | empty | OpenSSH private key |
| `-knownhosts` / `-hostkey` | empty | host key policy |
| `-timeout` | 15s | connect and authentication, not the transfer |
| `-v` | off | list every entry as it is sent |
| `-q` | off | no summary line on stderr |
### Exit status
| Code | Meaning |
| --- | --- |
| 0 | everything was copied |
| 1 | transfer failed: missing local path, remote scp error, permission denied |
| 2 | usage error |
| 255 | connection, host key or authentication failure |
### Behaviour
- directories keep their name and relative structure: `-files build -dest /srv` gives `/srv/build/...`
- permissions: regular files 0644 (0755 when the local file is executable), directories 0755
- symlinks named on the command line are followed; symlinks met inside a tree are skipped with a warning
- file names may contain spaces; newlines are rejected
- the transfer is streamed and verified byte for byte in the test suite
## Host key policy
Host keys are **not** verified by default. A script cannot answer "do you trust
this host", so the tools behave like `plink -batch`: they accept the key and
continue. The trade-off is a theoretical man-in-the-middle.
Pin it when it matters:
- `-hostkey SHA256:...` -- fail unless the fingerprint matches;
- `-knownhosts ~/.ssh/known_hosts` -- unknown or changed keys fail, never prompt.
## Security notes
A password on the command line is visible in `ps` and the shell history, exactly
like `plink -pw`. Prefer `GOLINK_PASSWORD` / `GOSCP_PASSWORD` or `-i`.
Passphrase-protected keys are rejected with an explanatory error: these tools
never prompt, so decrypt the key once with `ssh-keygen -p` instead.
## Building
```bash
./build.sh # every program for every platform, into dist/
./build.sh local # only the host platform
VERSION=1.2.3 ./build.sh
```
The only dependency is `golang.org/x/crypto`, so an offline build works as long
as the module cache is populated. `build.sh` points `GOCACHE` at the in-repo
`.gocache/` directory, because some sandboxes mount `~/.cache/go-build` read-only.
Pushing a `v*` tag makes GitHub Actions run both test suites, cross-compile
everything and attach the binaries to the GitHub release. Gitea also discovers
`.github/workflows`, so the jobs carry an explicit
`if: github.server_url == 'https://github.com'` guard and stay idle there.
## Testing
```bash
./smoketest/run.sh # golink, 38 checks
./smoketest/run-goscp.sh # goscp, 19 checks
```
Both suites are fully local: they build a miniature sshd (`smoketest/sshd`,
supporting password, keyboard-interactive and public-key auth) and drive it over
loopback. Every case runs with stdin closed and a watchdog, so any attempt to
wait for interactive input shows up as a failure rather than a hang.
## Layout
```
cmd/golink/ golink entry point: flags, exit status mapping
cmd/goscp/ goscp entry point
internal/cli/ argv reordering so flags work anywhere
internal/sshx/ dial, authentication, host key policy
internal/scp/ SCP wire protocol, client side
smoketest/sshd/ miniature SSH server used by the tests
smoketest/run.sh golink end-to-end suite
smoketest/run-goscp.sh
build.sh cross-compilation for six targets
```
## Not implemented
- download direction for goscp (remote to local)
- resume, parallel transfers, progress
- an SFTP backend (would add a dependency; SCP keeps the binary self-contained)
- jump hosts (`-J`), port forwarding, interactive shells / pty