# 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.