- README.md 英文,顶部有简体中文链接 - README.zh-CN.md 中文,顶部有 English 链接 - 补上安装/发布下载、macOS quarantine、CI 只跑 GitHub 的说明
235 lines
8.9 KiB
Markdown
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
|