README.md
8.3 KiB
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:
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:
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:
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:
go install git.scuroneko.dev/ScuroNeko/est/cmd/est@v1.0.0
Run from source
The required Go version is declared in go.mod.
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:
docker login git.scuroneko.dev
docker pull git.scuroneko.dev/scuroneko/est:latest
Or build it locally:
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:
./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:
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:
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:
[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:
[[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:
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:
[[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:
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:
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:
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.