v1.0.0
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
# est
|
||||
|
||||
`est` is a small command-line tool that manages SSH port-forwarding tunnels
|
||||
from a TOML configuration file. It starts one `ssh` process per tunnel and
|
||||
shuts every process down when the application receives `SIGINT` (`Ctrl+C`) or
|
||||
`SIGTERM`.
|
||||
|
||||
## Features
|
||||
|
||||
- Local forwarding (`rtl`): expose a local port and forward it through SSH to
|
||||
an address reachable from the remote server.
|
||||
- Remote forwarding (`ltr`): expose a port on the remote SSH server and
|
||||
forward it to a local address.
|
||||
- Run multiple tunnels from one configuration file.
|
||||
- Use SSH host aliases, a shared SSH config file, and per-tunnel identity keys.
|
||||
- Fail early when mandatory tunnel fields are missing.
|
||||
- Start SSH with `ExitOnForwardFailure=yes`, so a refused port forward fails
|
||||
the application instead of silently leaving a connected but unusable tunnel.
|
||||
|
||||
`ssh` must be installed and available on `PATH`.
|
||||
|
||||
## Install the CLI
|
||||
|
||||
Install the latest released version with Go:
|
||||
|
||||
```sh
|
||||
go install git.scuroneko.dev/ScuroNeko/est/cmd/est@latest
|
||||
```
|
||||
|
||||
The command is installed to `$GOBIN`, or to `$(go env GOPATH)/bin` when
|
||||
`GOBIN` is not set. Ensure that directory is on your `PATH`:
|
||||
|
||||
```sh
|
||||
export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
est --config /etc/est/tunnels.toml
|
||||
```
|
||||
|
||||
For a private Git server, configure the Go tool to fetch the module directly:
|
||||
|
||||
```sh
|
||||
go env -w GOPRIVATE=git.scuroneko.dev
|
||||
go env -w GONOSUMDB=git.scuroneko.dev
|
||||
go env -w GOPROXY=direct
|
||||
```
|
||||
|
||||
Your Git SSH key or access token must already grant read access to the
|
||||
repository. Pin a version for repeatable deployments:
|
||||
|
||||
```sh
|
||||
go install git.scuroneko.dev/ScuroNeko/est/cmd/est@v1.0.0
|
||||
```
|
||||
|
||||
## Run from source
|
||||
|
||||
The required Go version is declared in [go.mod](go.mod).
|
||||
|
||||
```sh
|
||||
cp config.example.toml config.toml
|
||||
# Edit config.toml before starting est.
|
||||
go run ./cmd/est --config ./config.toml
|
||||
```
|
||||
|
||||
`config.toml` is ignored by Git so private hosts and key paths are not
|
||||
committed accidentally. The default configuration path is `./config.toml`;
|
||||
use `--config` or `-c` to select another file.
|
||||
|
||||
## Docker
|
||||
|
||||
Pull the published image:
|
||||
|
||||
```sh
|
||||
docker login git.scuroneko.dev
|
||||
docker pull git.scuroneko.dev/scuroneko/est:latest
|
||||
```
|
||||
|
||||
Or build it locally:
|
||||
|
||||
```sh
|
||||
docker build -t git.scuroneko.dev/scuroneko/est:local .
|
||||
```
|
||||
|
||||
Run the local Docker smoke test to build the final image and verify that its
|
||||
SSH client is available:
|
||||
|
||||
```sh
|
||||
./scripts/docker-smoke-test.sh
|
||||
```
|
||||
|
||||
The release build publishes both `linux/amd64` and `linux/arm64` images. It
|
||||
requires a Buildx builder with ARM64 emulation installed on an AMD64 host:
|
||||
|
||||
```sh
|
||||
docker run --privileged --rm tonistiigi/binfmt --install arm64
|
||||
docker buildx create --name est-builder --driver docker-container --use
|
||||
docker buildx inspect --bootstrap
|
||||
make docker-build
|
||||
```
|
||||
|
||||
`make docker-build` pushes
|
||||
`git.scuroneko.dev/scuroneko/est:latest` and
|
||||
`git.scuroneko.dev/scuroneko/est:1.0.0`. Adjust the Makefile tags before a
|
||||
different release.
|
||||
|
||||
Do not bake your private key or tunnel configuration into the image. Mount
|
||||
them at runtime instead. The command below reuses the host SSH configuration,
|
||||
keys, and `known_hosts` file without granting the container write access:
|
||||
|
||||
```sh
|
||||
docker run --rm --init \
|
||||
--user "$(id -u):$(id -g)" \
|
||||
--env HOME=/ssh \
|
||||
--volume "$PWD/config.toml:/app/config.toml:ro" \
|
||||
--volume "$HOME/.ssh:/ssh:ro" \
|
||||
git.scuroneko.dev/scuroneko/est:latest --config /app/config.toml
|
||||
```
|
||||
|
||||
With `HOME=/ssh`, `est` and OpenSSH can discover `/ssh/config` and
|
||||
`/ssh/known_hosts`. The mounted configuration can use paths such as
|
||||
`identityFile = "/ssh/id_ed25519"`. If you use another mount location, set
|
||||
`sshConfig` and `identityFile` to paths inside the container.
|
||||
|
||||
The image intentionally does not publish ports. For `rtl` tunnels, publish the
|
||||
local listening port explicitly when it must be reachable outside Docker, for
|
||||
example `-p 127.0.0.1:5433:5433`.
|
||||
|
||||
## Configuration
|
||||
|
||||
The configuration file is TOML. Every `[[entry]]` section describes one
|
||||
tunnel.
|
||||
|
||||
| Field | Required | Description |
|
||||
|----------------|---------------|-----------------------------------------------------------------------------------------------------|
|
||||
| `direction` | yes | Tunnel direction: `ltr` or `rtl`. |
|
||||
| `host` | yes | SSH destination or host alias, for example `user@example.com` or `vps`. |
|
||||
| `localIP` | no | Address on the machine that runs `est`. Defaults to `127.0.0.1`. |
|
||||
| `localPort` | yes | Port on the machine that runs `est`. Must be non-zero. |
|
||||
| `remoteIP` | no | Remote bind/destination address, depending on the direction. Defaults to `127.0.0.1`. |
|
||||
| `remotePort` | yes | Remote bind/destination port. Must be non-zero. |
|
||||
| `identityFile` | no | Private SSH key path for this tunnel. |
|
||||
| `retry` | no | A top-level or per-entry retry table described below. |
|
||||
| `sshConfig` | no, top level | SSH config path applied to every tunnel. |
|
||||
|
||||
`host` must be non-empty and ports must be non-zero. An empty `localIP` or
|
||||
`remoteIP` is converted to `127.0.0.1`. When `sshConfig` is omitted, `est`
|
||||
uses `~/.ssh/config` if the file exists.
|
||||
|
||||
The optional top-level retry table applies to every entry:
|
||||
|
||||
```toml
|
||||
[retry]
|
||||
count = 5
|
||||
delay = "5s"
|
||||
```
|
||||
|
||||
`count` is the number of reconnect attempts after the initial SSH process
|
||||
exits. Its default is `5`; use `0` to disable reconnects and `-1` to retry
|
||||
forever. `delay` is a positive Go duration such as `"5s"`, `"500ms"`, or
|
||||
`"1m"`; its default is `"5s"`.
|
||||
|
||||
Add `[entry.retry]` immediately after a tunnel entry to override the top-level
|
||||
retry settings for that tunnel. This is a full override: if an entry retry
|
||||
block omits `count` or `delay`, the omitted value uses the standard default,
|
||||
not the top-level value.
|
||||
|
||||
### Remote forwarding (`ltr`)
|
||||
|
||||
`ltr` means local-to-remote. It opens a port on the remote SSH server and
|
||||
forwards connections to an address reachable from the local machine:
|
||||
|
||||
```toml
|
||||
[[entry]]
|
||||
direction = "ltr"
|
||||
host = "vps"
|
||||
|
||||
localIP = "127.0.0.1"
|
||||
localPort = 22
|
||||
|
||||
remoteIP = "127.0.0.1"
|
||||
remotePort = 2222
|
||||
|
||||
[entry.retry]
|
||||
count = -1
|
||||
delay = "2s"
|
||||
```
|
||||
|
||||
This produces the equivalent SSH command:
|
||||
|
||||
```sh
|
||||
ssh -N -T -o ExitOnForwardFailure=yes \
|
||||
-R 127.0.0.1:2222:127.0.0.1:22 vps
|
||||
```
|
||||
|
||||
Connecting to `127.0.0.1:2222` on `vps` reaches `127.0.0.1:22` on the machine
|
||||
running `est`. Binding a remote address other than loopback may require the SSH
|
||||
server's `GatewayPorts` setting.
|
||||
|
||||
### Local forwarding (`rtl`)
|
||||
|
||||
`rtl` means remote-to-local. It opens a port on the machine that runs `est`
|
||||
and forwards connections to an address reachable from the remote SSH server:
|
||||
|
||||
```toml
|
||||
[[entry]]
|
||||
direction = "rtl"
|
||||
host = "vps"
|
||||
|
||||
localIP = "127.0.0.1"
|
||||
localPort = 5433
|
||||
|
||||
remoteIP = "127.0.0.1"
|
||||
remotePort = 5432
|
||||
identityFile = "/home/user/.ssh/vps_key"
|
||||
```
|
||||
|
||||
Equivalent SSH command:
|
||||
|
||||
```sh
|
||||
ssh -N -T -o ExitOnForwardFailure=yes \
|
||||
-i /home/user/.ssh/vps_key \
|
||||
-L 127.0.0.1:5433:127.0.0.1:5432 vps
|
||||
```
|
||||
|
||||
After the connection is established, the service reachable by `vps` at
|
||||
`127.0.0.1:5432` is available locally at `127.0.0.1:5433`.
|
||||
|
||||
### Shared SSH config
|
||||
|
||||
Set `sshConfig` at the top level to use a non-default SSH configuration file:
|
||||
|
||||
```toml
|
||||
sshConfig = "/etc/est/ssh_config"
|
||||
|
||||
[[entry]]
|
||||
direction = "ltr"
|
||||
host = "vps"
|
||||
localIP = "127.0.0.1"
|
||||
localPort = 22
|
||||
remoteIP = "127.0.0.1"
|
||||
remotePort = 2222
|
||||
```
|
||||
|
||||
## Lifecycle and errors
|
||||
|
||||
Press `Ctrl+C` or send `SIGTERM` to stop `est`. The shared application context
|
||||
is cancelled and the `ssh` subprocesses started by it are terminated.
|
||||
|
||||
If any tunnel fails to start or exhausts its reconnect attempts, `est` logs the
|
||||
SSH exit code and standard error, cancels the shared context, and stops the
|
||||
remaining tunnels. Add `-v`, `-vv`, or `-vvv` to the SSH arguments temporarily
|
||||
when a connection needs deeper diagnosis.
|
||||
|
||||
## Development checks
|
||||
|
||||
Run the Go checks locally:
|
||||
|
||||
```sh
|
||||
go test ./...
|
||||
go vet ./...
|
||||
```
|
||||
|
||||
The Gitea workflow verifies Go formatting, runs `golangci-lint`, and builds
|
||||
the Docker image with the SSH-client smoke test on pushes and pull requests.
|
||||
|
||||
## Release status
|
||||
|
||||
The CLI and Docker workflows are documented, but the current repository is
|
||||
not ready for a `v1.0.0` release yet. See the release checklist below.
|
||||
|
||||
### v1.0.0 release checklist
|
||||
|
||||
- Confirm that the Go and Docker CI jobs pass for the release commit.
|
||||
- Publish the versioned Go module tag and Docker image, then verify the
|
||||
installation instructions from a clean environment.
|
||||
- Publish a tag such as `v1.0.0` only after the checklist is complete and the
|
||||
public CLI/configuration contract is considered stable.
|
||||
Reference in New Issue
Block a user