FILE / ScuroNeko/est

README.md

Исходный файл и его история в репозитории.
FILE main
Files
ScuroNeko 80278ee801
Golang lint / docker-smoke (push) Successful in 13s
Golang lint / lint (push) Successful in 15s
readme cleanup
2026-08-06 12:53:05 +03:00

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.