FILE / ScuroNeko/est
README.md
Исходный файл и его история в репозитории.
276 lines
8.8 KiB
Markdown
276 lines
8.8 KiB
Markdown
# 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.
|