+3
-16
@@ -1,20 +1,7 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM docker.io/library/alpine:3.24.2
|
||||
|
||||
FROM docker.io/library/alpine:3
|
||||
RUN apk add --no-cache openssh-client tini
|
||||
|
||||
COPY --chmod=0755 entrypoint.sh /usr/local/bin/sish-client
|
||||
|
||||
# nobody; HOME and state live in /tmp so the root filesystem can be read-only
|
||||
ENV HOME=/tmp \
|
||||
STATE_DIR=/tmp/sish-client
|
||||
# nobody: ssh needs a uid that exists in /etc/passwd, so keep runAsUser at 65534
|
||||
USER 65534:65534
|
||||
|
||||
# -g: forward signals to the whole process group, so ssh stops cleanly too
|
||||
# tini reaps zombies and forwards SIGTERM to the whole process group (-g), incl. ssh
|
||||
ENTRYPOINT ["/sbin/tini", "-g", "--", "/usr/local/bin/sish-client"]
|
||||
|
||||
HEALTHCHECK --interval=15s --timeout=3s --start-period=30s --retries=2 \
|
||||
CMD ["/bin/sh", "-c", "test -f \"$STATE_DIR/ready\""]
|
||||
|
||||
LABEL org.opencontainers.image.title="sish-client" \
|
||||
org.opencontainers.image.description="Environment-driven sish connector (SSH reverse tunnels with SNI passthrough)"
|
||||
|
||||
@@ -1,188 +1,47 @@
|
||||
# sish-client
|
||||
|
||||
A small, environment-driven connector for a [sish](https://github.com/antoniomika/sish) edge.
|
||||
It opens SSH reverse tunnels to the edge, claims hostnames there, and relays their traffic to
|
||||
local targets. In the default mode sish routes by **SNI without terminating TLS**, and each
|
||||
connection starts with a **PROXY v2 header** carrying the real client address.
|
||||
|
||||
```
|
||||
client ──TLS──▶ edge :443 (sish, SNI routing) ══ssh══▶ sish-client ──▶ target (terminates TLS)
|
||||
```
|
||||
|
||||
Image: `registry.traberph.de/public/sish-client` (Alpine + OpenSSH client + tini, ~20 MB,
|
||||
runs as `nobody`, works with a read-only root filesystem).
|
||||
|
||||
## Quick start
|
||||
Opens SSH reverse tunnels to a [sish](https://github.com/antoniomika/sish) edge and reconnects
|
||||
every 5s when the session drops. Image: `registry.traberph.de/public/sish-client`.
|
||||
|
||||
```sh
|
||||
docker run -d --name sish-client --read-only --tmpfs /tmp \
|
||||
docker run -d --read-only --tmpfs /tmp \
|
||||
-e SISH_HOST=edge.example.com \
|
||||
-e SISH_HOST_KEY="ssh-ed25519 AAAA..." \
|
||||
-e ROUTES="app.example.com=app:8443" \
|
||||
-e SISH_ROUTES="app.example.com:443=app:8443" \
|
||||
-v ./id_ed25519:/secrets/id_ed25519:ro \
|
||||
registry.traberph.de/public/sish-client:latest
|
||||
```
|
||||
|
||||
The connector's **public** key must be authorized on the edge (sish `--authentication-keys-directory`),
|
||||
and each hostname must be allowed there (sish `--bind-hosts` or `_sish` TXT records).
|
||||
|
||||
## Configuration
|
||||
|
||||
| Variable | Default | Description |
|
||||
| Variable | Default | |
|
||||
|---|---|---|
|
||||
| `SISH_HOST` | *required* | Edge hostname or IP |
|
||||
| `SISH_HOST` | *required* | Edge host |
|
||||
| `SISH_HOST_KEY` | *required* | Edge host key, `"<type> <base64>"` (first two fields of its `.pub` file) |
|
||||
| `SISH_ROUTES` | *required* | Comma-separated `host:bind-port=target:port` |
|
||||
| `SISH_PORT` | `2222` | sish SSH port |
|
||||
| `SISH_USER` | `connector` | SSH user (sish accepts any; shows up in edge logs) |
|
||||
| `SISH_KEY` | | Private key contents (e.g. from a Kubernetes `secretKeyRef`). Takes precedence over `SISH_KEY_FILE` |
|
||||
| `SISH_KEY_FILE` | `/secrets/id_ed25519` | Path to a mounted private key |
|
||||
| `SISH_HOST_KEY` | | Edge host public key, `"<type> <base64>"` (the first two fields of the `.pub` file) |
|
||||
| `SISH_KNOWN_HOSTS_FILE` | | Alternative to `SISH_HOST_KEY`: a known_hosts file matching `[SISH_HOST]:SISH_PORT` |
|
||||
| `SISH_STRICT_HOST_KEY` | `true` | `false` disables host key verification (testing only) |
|
||||
| `ROUTES` | | Routes, separated by commas, spaces or newlines (see below) |
|
||||
| `ROUTES_FILE` | | File with routes, one per line, `#` comments allowed. Combined with `ROUTES` |
|
||||
| `TARGET` | | Default `host:port` for routes without an explicit target |
|
||||
| `DEFAULT_BIND_PORT` | `443` | Edge port for routes without an explicit bind port |
|
||||
| `SNI_PROXY` | `true` | Ask sish to route by SNI instead of terminating TLS |
|
||||
| `PROXY_PROTOCOL` | `2` | PROXY header version sent to the target: `1`, `2` or `off` |
|
||||
| `KEEPALIVE_INTERVAL` | `15` | Seconds between SSH keepalives (3 missed = reconnect) |
|
||||
| `CONNECT_TIMEOUT` | `10` | SSH connect timeout in seconds |
|
||||
| `BACKOFF_MIN` / `BACKOFF_MAX` | `2` / `60` | Reconnect backoff in seconds (doubles per failure, resets after a successful session) |
|
||||
| `SSH_EXTRA_OPTS` | | Extra `ssh` arguments, e.g. `-o Ciphers=aes128-gcm@openssh.com` |
|
||||
| `STATE_DIR` | `/tmp/sish-client` | Writable directory for the key copy, known_hosts and the ready marker |
|
||||
| `SISH_KEY_FILE` | `/secrets/id_ed25519` | Private key |
|
||||
| `SISH_SNI_PROXY` | `true` | `true`: sish routes by SNI and passes TLS through. `false`: HTTP routing by `Host` header |
|
||||
| `SISH_PROXY_PROTOCOL` | `2` | PROXY header sent to the target: `1`, `2` or `off` |
|
||||
|
||||
### Routes
|
||||
HTTP (:80) routes need `SISH_SNI_PROXY=false` and a separate instance from SNI (:443) routes.
|
||||
If sish rejects any route, the session fails and is retried, so a bad route shows up in the logs.
|
||||
|
||||
```
|
||||
<host>[:<bind-port>][=<target-host>:<target-port>]
|
||||
```
|
||||
## Requirements
|
||||
|
||||
| Example | Meaning |
|
||||
|---|---|
|
||||
| `app.example.com=traefik:8443` | SNI `app.example.com` on edge :443 → `traefik:8443` |
|
||||
| `app.example.com` | Same, using `TARGET` |
|
||||
| `*.apps.example.com=traefik:8443` | Wildcard (sish needs `--bind-wildcards`) |
|
||||
| `app.example.com:80=traefik:8080` | Plain HTTP on edge :80, routed by `Host` header (**needs `SNI_PROXY=false`**, see below) |
|
||||
- `/tmp` must be writable (e.g. a memory `emptyDir`), the root filesystem can be read-only.
|
||||
- Run as uid `65534` (the image default). ssh refuses to start for uids missing from `/etc/passwd`.
|
||||
- The key file must be readable by uid 65534, e.g. a Secret volume with `defaultMode: 0440`
|
||||
and `fsGroup: 65534`. ssh rejects keys owned by 65534 that others can read.
|
||||
|
||||
**HTTP (:80) and SNI (:443) routes cannot share one connector.** With `SNI_PROXY=true` sish
|
||||
treats every forward as an SNI listener, which collides with its HTTP listener on :80. Run a
|
||||
second instance for HTTP routes (e.g. for HTTP→HTTPS redirects):
|
||||
## Security
|
||||
|
||||
```sh
|
||||
SNI_PROXY=false PROXY_PROTOCOL=off ROUTES="app.example.com:80=traefik:8080"
|
||||
```
|
||||
|
||||
If the edge rejects any route, the whole session fails (`ExitOnForwardFailure`) and is retried
|
||||
with backoff. A misconfigured route is therefore loud (visible in the logs and health status)
|
||||
instead of silently missing.
|
||||
|
||||
Several connectors may claim the same hostname. sish then load-balances between them, which is
|
||||
how you get redundancy (e.g. one connector per ingress replica).
|
||||
|
||||
## Health
|
||||
|
||||
The container writes `$STATE_DIR/ready` once sish has confirmed **every** route, and removes it
|
||||
when the tunnel drops. The image's `HEALTHCHECK` tests this file; in Kubernetes use it as a
|
||||
readiness probe:
|
||||
|
||||
```yaml
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["test", "-f", "/tmp/sish-client/ready"]
|
||||
periodSeconds: 5
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
image: nginx # must terminate TLS and accept PROXY protocol on 8443
|
||||
connector:
|
||||
image: registry.traberph.de/public/sish-client:latest
|
||||
read_only: true
|
||||
tmpfs: [/tmp]
|
||||
cap_drop: [ALL]
|
||||
security_opt: [no-new-privileges:true]
|
||||
environment:
|
||||
SISH_HOST: edge.example.com
|
||||
SISH_HOST_KEY: ssh-ed25519 AAAA...
|
||||
ROUTES: app.example.com=app:8443
|
||||
volumes:
|
||||
- ./id_ed25519:/secrets/id_ed25519:ro
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
The key file must be readable by uid `65534` inside the container. Compose ignores `uid`/`mode`
|
||||
on file secrets, so either:
|
||||
- make the key readable by a group and run with `user: "65534:<gid>"`. With **rootless** Docker
|
||||
your host files appear as owned by root inside the container, so the gid is `0`.
|
||||
- or pass the key via `SISH_KEY` instead of a file.
|
||||
|
||||
### Kubernetes sidecar (next to an ingress controller)
|
||||
|
||||
```yaml
|
||||
- name: sish-client
|
||||
image: registry.traberph.de/public/sish-client:20260926154233-1a2b3c4 # {"$imagepolicy": "flux-system:sish-client"}
|
||||
env:
|
||||
- { name: SISH_HOST, value: edge.example.com }
|
||||
- { name: SISH_HOST_KEY, value: "ssh-ed25519 AAAA..." }
|
||||
- { name: ROUTES_FILE, value: /etc/sish-client/routes.txt }
|
||||
- { name: TARGET, value: "127.0.0.1:9443" } # ingress entrypoint with PROXY protocol
|
||||
- name: SISH_KEY
|
||||
valueFrom: { secretKeyRef: { name: sish-client, key: id_ed25519 } }
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
readOnlyRootFilesystem: true
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities: { drop: [ALL] }
|
||||
volumeMounts:
|
||||
- { name: tmp, mountPath: /tmp }
|
||||
- { name: routes, mountPath: /etc/sish-client, readOnly: true }
|
||||
readinessProbe:
|
||||
exec: { command: ["test", "-f", "/tmp/sish-client/ready"] }
|
||||
```
|
||||
|
||||
Routes are only read at connect time. Roll the pods after changing them (e.g. with a config
|
||||
hash annotation).
|
||||
|
||||
## Image tags
|
||||
|
||||
The Gitea workflow (`.gitea/workflows/build.yaml`) builds `linux/amd64` and `linux/arm64` and pushes:
|
||||
|
||||
| Trigger | Tags |
|
||||
|---|---|
|
||||
| Push to `main` | `<yyyymmddHHMMSS>-<sha7>` (UTC) and `latest` |
|
||||
| Git tag `v*` | `<yyyymmddHHMMSS>-<sha7>` and the git tag, e.g. `v1.0.0` |
|
||||
|
||||
Repository secrets required: `REGISTRY_USERNAME`, `REGISTRY_TOKEN` (a token with push access
|
||||
to `registry.traberph.de/public`). The runner needs Docker (Buildx + QEMU for arm64).
|
||||
|
||||
### Flux image automation
|
||||
|
||||
```yaml
|
||||
apiVersion: image.toolkit.fluxcd.io/v1
|
||||
kind: ImageRepository
|
||||
metadata: { name: sish-client, namespace: flux-system }
|
||||
spec:
|
||||
image: registry.traberph.de/public/sish-client
|
||||
interval: 10m
|
||||
---
|
||||
apiVersion: image.toolkit.fluxcd.io/v1
|
||||
kind: ImagePolicy
|
||||
metadata: { name: sish-client, namespace: flux-system }
|
||||
spec:
|
||||
imageRepositoryRef: { name: sish-client }
|
||||
filterTags:
|
||||
pattern: '^(?P<ts>[0-9]{14})-[0-9a-f]{7}$'
|
||||
extract: '$ts'
|
||||
policy:
|
||||
numerical: { order: asc }
|
||||
```
|
||||
|
||||
## Build locally
|
||||
|
||||
```sh
|
||||
docker build -t registry.traberph.de/public/sish-client:dev .
|
||||
```
|
||||
- The edge is authenticated only by `SISH_HOST_KEY`. There is no trust-on-first-use and no
|
||||
fallback: a wrong or missing key fails the connection.
|
||||
- All ssh_config files are ignored (`-F /dev/null`). Agent and X11 forwarding are off, so the
|
||||
edge can only open connections to the configured route targets.
|
||||
- **PROXY protocol:** the target trusts the PROXY header for the client IP. It must only accept
|
||||
PROXY connections from this client (e.g. listen on `127.0.0.1` in the sidecar and set trusted
|
||||
IPs), otherwise anything that can reach that port can spoof client IPs. If the target does not
|
||||
expect a PROXY header, set `SISH_PROXY_PROTOCOL=off`.
|
||||
- Whoever controls the edge sees HTTP traffic, and with `SISH_SNI_PROXY=true` still sees the
|
||||
SNI hostnames and client IPs, but not TLS contents.
|
||||
- The base image follows `alpine:3`. Rebuild regularly to pick up OpenSSH fixes.
|
||||
|
||||
+38
-172
@@ -1,185 +1,51 @@
|
||||
#!/bin/sh
|
||||
# sish-client: environment-driven connector for a sish edge.
|
||||
# Claims hostnames on the edge via SSH reverse forwards and relays them to local
|
||||
# targets. Reconnects forever with exponential backoff. See README.md.
|
||||
# Opens SSH reverse tunnels to a sish edge and reconnects forever. See README.md.
|
||||
set -eu
|
||||
set -f # routes may contain wildcards (*.example.com); never glob-expand them
|
||||
set -f # no globbing: routes may contain wildcards (*.example.com)
|
||||
|
||||
ESC=$(printf '\033')
|
||||
die() { echo "sish-client: $*" >&2; exit 64; }
|
||||
|
||||
log() { printf '%s sish-client: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*"; }
|
||||
die() { log "error: $*"; exit 64; }
|
||||
is_true() {
|
||||
case "$(printf '%s' "$1" | tr 'A-Z' 'a-z')" in
|
||||
1 | true | yes | on) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
is_uint() { case "$1" in '' | *[!0-9]*) return 1 ;; *) return 0 ;; esac; }
|
||||
: "${SISH_HOST:?is required}" "${SISH_HOST_KEY:?is required}" "${SISH_ROUTES:?is required}"
|
||||
SNI_PROXY=${SISH_SNI_PROXY:-true}
|
||||
PROXY_PROTOCOL=${SISH_PROXY_PROTOCOL:-2}
|
||||
KEY=${SISH_KEY_FILE:-/secrets/id_ed25519}
|
||||
|
||||
# --- configuration -----------------------------------------------------------
|
||||
case $SNI_PROXY in true | false) ;; *) die "SISH_SNI_PROXY must be true or false" ;; esac
|
||||
case $PROXY_PROTOCOL in 1 | 2 | off) ;; *) die "SISH_PROXY_PROTOCOL must be 1, 2 or off" ;; esac
|
||||
[ -r "$KEY" ] || die "cannot read private key $KEY"
|
||||
|
||||
SISH_HOST=${SISH_HOST:-}
|
||||
SISH_PORT=${SISH_PORT:-2222}
|
||||
SISH_USER=${SISH_USER:-connector}
|
||||
SISH_KEY=${SISH_KEY:-}
|
||||
SISH_KEY_FILE=${SISH_KEY_FILE:-/secrets/id_ed25519}
|
||||
SISH_HOST_KEY=${SISH_HOST_KEY:-}
|
||||
SISH_KNOWN_HOSTS_FILE=${SISH_KNOWN_HOSTS_FILE:-}
|
||||
SISH_STRICT_HOST_KEY=${SISH_STRICT_HOST_KEY:-true}
|
||||
|
||||
ROUTES=${ROUTES:-}
|
||||
ROUTES_FILE=${ROUTES_FILE:-}
|
||||
TARGET=${TARGET:-}
|
||||
DEFAULT_BIND_PORT=${DEFAULT_BIND_PORT:-443}
|
||||
|
||||
SNI_PROXY=${SNI_PROXY:-true}
|
||||
PROXY_PROTOCOL=${PROXY_PROTOCOL:-2}
|
||||
|
||||
KEEPALIVE_INTERVAL=${KEEPALIVE_INTERVAL:-15}
|
||||
CONNECT_TIMEOUT=${CONNECT_TIMEOUT:-10}
|
||||
BACKOFF_MIN=${BACKOFF_MIN:-2}
|
||||
BACKOFF_MAX=${BACKOFF_MAX:-60}
|
||||
SSH_EXTRA_OPTS=${SSH_EXTRA_OPTS:-}
|
||||
STATE_DIR=${STATE_DIR:-/tmp/sish-client}
|
||||
|
||||
[ -n "$SISH_HOST" ] || die "SISH_HOST is required"
|
||||
for v in SISH_PORT DEFAULT_BIND_PORT KEEPALIVE_INTERVAL CONNECT_TIMEOUT BACKOFF_MIN BACKOFF_MAX; do
|
||||
eval "val=\$$v"
|
||||
is_uint "$val" || die "$v must be a non-negative integer (got '$val')"
|
||||
done
|
||||
case "$PROXY_PROTOCOL" in 1 | 2 | off | none | false | 0) ;; *) die "PROXY_PROTOCOL must be 1, 2 or off" ;; esac
|
||||
|
||||
umask 077
|
||||
mkdir -p "$STATE_DIR" || die "STATE_DIR $STATE_DIR is not writable (mount a tmpfs on /tmp with a read-only root fs)"
|
||||
READY_FILE=$STATE_DIR/ready
|
||||
FIFO=$STATE_DIR/ssh.out
|
||||
KEY=$STATE_DIR/id_key
|
||||
KNOWN_HOSTS=$STATE_DIR/known_hosts
|
||||
rm -f "$READY_FILE"
|
||||
|
||||
# --- private key: always copied so ssh sees a private 0600 file we own -------
|
||||
|
||||
if [ -n "$SISH_KEY" ]; then
|
||||
printf '%s\n' "$SISH_KEY" > "$KEY"
|
||||
elif [ -r "$SISH_KEY_FILE" ]; then
|
||||
cat "$SISH_KEY_FILE" > "$KEY"
|
||||
else
|
||||
die "no private key: set SISH_KEY or mount a readable key at SISH_KEY_FILE ($SISH_KEY_FILE)"
|
||||
fi
|
||||
chmod 600 "$KEY"
|
||||
|
||||
# --- host key verification ---------------------------------------------------
|
||||
|
||||
HOST_KEY_OPTS=""
|
||||
if [ -n "$SISH_HOST_KEY" ]; then
|
||||
# Pin the edge host key: the only entry ssh will trust. A newline would allow
|
||||
# extra known_hosts lines (e.g. a wildcard entry), so the key must be one line.
|
||||
case $SISH_HOST_KEY in *'
|
||||
'*) die "SISH_HOST_KEY must be a single line" ;; esac
|
||||
KNOWN_HOSTS=$(mktemp) # private 0600 file with a random name, safe in a shared /tmp
|
||||
printf 'sish-edge %s\n' "$SISH_HOST_KEY" > "$KNOWN_HOSTS"
|
||||
HOST_KEY_OPTS="-o HostKeyAlias=sish-edge -o UserKnownHostsFile=$KNOWN_HOSTS -o StrictHostKeyChecking=yes"
|
||||
elif [ -n "$SISH_KNOWN_HOSTS_FILE" ]; then
|
||||
[ -r "$SISH_KNOWN_HOSTS_FILE" ] || die "SISH_KNOWN_HOSTS_FILE $SISH_KNOWN_HOSTS_FILE is not readable"
|
||||
HOST_KEY_OPTS="-o UserKnownHostsFile=$SISH_KNOWN_HOSTS_FILE -o StrictHostKeyChecking=yes"
|
||||
elif is_true "$SISH_STRICT_HOST_KEY"; then
|
||||
die "no host key: set SISH_HOST_KEY (e.g. 'ssh-ed25519 AAAA...') or SISH_KNOWN_HOSTS_FILE"
|
||||
else
|
||||
log "WARNING: host key verification disabled (SISH_STRICT_HOST_KEY=false)"
|
||||
HOST_KEY_OPTS="-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
|
||||
fi
|
||||
|
||||
# --- routes ------------------------------------------------------------------
|
||||
# Entry: <host>[:<bind-port>][=<target-host>:<target-port>]
|
||||
# Without "=target", TARGET is used. Bind port defaults to DEFAULT_BIND_PORT.
|
||||
|
||||
route_entries() {
|
||||
{
|
||||
printf '%s\n' "$ROUTES" | tr ', ' '\n\n'
|
||||
if [ -n "$ROUTES_FILE" ]; then
|
||||
[ -r "$ROUTES_FILE" ] || die "ROUTES_FILE $ROUTES_FILE is not readable"
|
||||
sed 's/#.*//' "$ROUTES_FILE" | tr ', \t' '\n\n\n'
|
||||
fi
|
||||
} | sed '/^[[:space:]]*$/d'
|
||||
}
|
||||
|
||||
FORWARDS=""
|
||||
ROUTE_COUNT=0
|
||||
for entry in $(route_entries); do
|
||||
case "$entry" in
|
||||
*=*) bind=${entry%%=*} target=${entry#*=} ;;
|
||||
*) bind=$entry target=$TARGET ;;
|
||||
esac
|
||||
[ -n "$target" ] || die "route '$entry' has no target and TARGET is not set"
|
||||
case "$target" in *:*) ;; *) die "target '$target' of route '$entry' must be host:port" ;; esac
|
||||
case "$bind" in *:*) ;; *) bind="$bind:$DEFAULT_BIND_PORT" ;; esac
|
||||
FORWARDS="$FORWARDS -R $bind:$target"
|
||||
ROUTE_COUNT=$((ROUTE_COUNT + 1))
|
||||
log "route: $bind -> $target"
|
||||
# host:bind-port=target:port -> -R host:bind-port:target:port
|
||||
# Each route stays a single argument to -R, so it cannot inject ssh options.
|
||||
forwards=""
|
||||
for r in $(printf '%s' "$SISH_ROUTES" | tr , ' '); do
|
||||
case $r in *=*) ;; *) die "bad route '$r', expected host:port=target:port" ;; esac
|
||||
forwards="$forwards -R ${r%%=*}:${r#*=}"
|
||||
done
|
||||
[ "$ROUTE_COUNT" -gt 0 ] || die "no routes: set ROUTES and/or ROUTES_FILE"
|
||||
[ -n "$forwards" ] || die "SISH_ROUTES contains no routes"
|
||||
|
||||
# --- sish session options (sent as the remote "command") ---------------------
|
||||
# sish session options, sent as the remote command
|
||||
opts=""
|
||||
if [ "$SNI_PROXY" = true ]; then opts="sni-proxy=true"; fi
|
||||
if [ "$PROXY_PROTOCOL" != off ]; then opts="$opts proxy-protocol=$PROXY_PROTOCOL"; fi
|
||||
|
||||
REMOTE_CMD=""
|
||||
is_true "$SNI_PROXY" && REMOTE_CMD="$REMOTE_CMD sni-proxy=true"
|
||||
case "$PROXY_PROTOCOL" in 1 | 2) REMOTE_CMD="$REMOTE_CMD proxy-protocol=$PROXY_PROTOCOL" ;; esac
|
||||
|
||||
SSH_OPTS="-T -p $SISH_PORT -i $KEY
|
||||
-o IdentitiesOnly=yes -o BatchMode=yes -o LogLevel=ERROR
|
||||
-o ExitOnForwardFailure=yes
|
||||
-o ConnectTimeout=$CONNECT_TIMEOUT
|
||||
-o ServerAliveInterval=$KEEPALIVE_INTERVAL -o ServerAliveCountMax=3
|
||||
$HOST_KEY_OPTS $SSH_EXTRA_OPTS"
|
||||
|
||||
# --- run loop ----------------------------------------------------------------
|
||||
|
||||
SSH_PID=""
|
||||
shutdown() {
|
||||
log "received signal, shutting down"
|
||||
rm -f "$READY_FILE"
|
||||
[ -n "$SSH_PID" ] && kill "$SSH_PID" 2>/dev/null
|
||||
exit 0
|
||||
}
|
||||
trap shutdown TERM INT
|
||||
|
||||
# Runs one ssh session. Marks ready once sish confirmed every forward.
|
||||
WAS_READY=0
|
||||
run_once() {
|
||||
WAS_READY=0
|
||||
rm -f "$READY_FILE" "$FIFO"
|
||||
mkfifo "$FIFO"
|
||||
# shellcheck disable=SC2086 # word splitting of option strings is intended
|
||||
ssh $SSH_OPTS $FORWARDS "$SISH_USER@$SISH_HOST" $REMOTE_CMD > "$FIFO" 2>&1 &
|
||||
SSH_PID=$!
|
||||
confirmed=0
|
||||
while IFS= read -r line; do
|
||||
line=$(printf '%s' "$line" | sed "s/${ESC}\[[0-9;]*m//g; s/\r\$//")
|
||||
[ -n "$line" ] || continue
|
||||
log "edge: $line"
|
||||
case "$line" in
|
||||
*"Starting SSH Forwarding service"*)
|
||||
confirmed=$((confirmed + 1))
|
||||
if [ "$confirmed" -ge "$ROUTE_COUNT" ] && [ "$WAS_READY" -eq 0 ]; then
|
||||
WAS_READY=1
|
||||
: > "$READY_FILE"
|
||||
log "ready: $confirmed/$ROUTE_COUNT forward(s) active"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done < "$FIFO"
|
||||
rc=0
|
||||
wait "$SSH_PID" || rc=$?
|
||||
SSH_PID=""
|
||||
rm -f "$READY_FILE" "$FIFO"
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
delay=$BACKOFF_MIN
|
||||
# -F /dev/null: ignore all ssh_config files, every option is set here.
|
||||
# The server can only open channels for the forwards requested above; agent and
|
||||
# X11 forwarding are off by default.
|
||||
while :; do
|
||||
log "connecting to $SISH_USER@$SISH_HOST:$SISH_PORT ($ROUTE_COUNT route(s))"
|
||||
rc=0
|
||||
run_once || rc=$?
|
||||
[ "$WAS_READY" -eq 1 ] && delay=$BACKOFF_MIN
|
||||
log "tunnel closed (exit $rc), reconnecting in ${delay}s"
|
||||
sleep "$delay" &
|
||||
wait $! || true
|
||||
delay=$((delay * 2))
|
||||
[ "$delay" -le "$BACKOFF_MAX" ] || delay=$BACKOFF_MAX
|
||||
# shellcheck disable=SC2086 # $forwards and $opts are split on purpose
|
||||
ssh -F /dev/null -T -p "${SISH_PORT:-2222}" -i "$KEY" \
|
||||
-o BatchMode=yes -o IdentitiesOnly=yes -o ExitOnForwardFailure=yes \
|
||||
-o ServerAliveInterval=15 -o ServerAliveCountMax=3 \
|
||||
-o HostKeyAlias=sish-edge -o StrictHostKeyChecking=yes \
|
||||
-o UserKnownHostsFile="$KNOWN_HOSTS" -o GlobalKnownHostsFile=/dev/null \
|
||||
$forwards "connector@$SISH_HOST" $opts \
|
||||
|| echo "sish-client: ssh exited ($?), reconnecting in 5s" >&2
|
||||
sleep 5
|
||||
done
|
||||
|
||||
Reference in New Issue
Block a user