Replit Agent commited on
Commit
5d42acf
·
1 Parent(s): 901f8e8

fix: replace serveo with bore transparent TCP tunnel — fixes Termius connection

Browse files

ROOT CAUSE:
serveo.net is an SSH-level proxy. It terminates the SSH connection
at its end and re-originates a new one. PTY allocation requests are
intercepted in this double-SSH hop and silently dropped, causing
Termius to authenticate but never receive a shell or prompt.
Cloudflare free tunnels have the same fundamental problem for raw
TCP SSH — they wrap traffic in HTTPS/WebSocket, so plain SSH
clients cannot connect to them at all (Termius timed out on port 22).

FIX — bore transparent TCP tunnel:
bore local 22 --to bore.pub
bore forwards raw bytes — zero protocol interpretation. Termius
connects directly: bore.pub:PORT → raw TCP → sshd:22. The full SSH
handshake, PTY allocation, and all channel types flow directly
between Termius and sshd with no middleman.

CHANGES:
Dockerfile:
- Add bore v0.6.0 binary (x86_64-unknown-linux-musl, single binary)
- Remove openssh-client (was only needed to ssh into serveo)

entrypoint.sh:
- Remove serveo tunnel block (two-attempt named/random logic)
- Add bore tunnel block:
waits up to 20s for assigned port
logs Termius host/port/user/pass prominently
background auto-restart loop with port detection
saves bore.pub:PORT to /data/logs/tunnel-url.txt

Files changed (2) hide show
  1. Dockerfile +7 -3
  2. entrypoint.sh +55 -80
Dockerfile CHANGED
@@ -3,8 +3,8 @@
3
  ## /terminal → ttyd on :7681 (real PTY bash)
4
  ## / → opencode on :8080 (chat UI + API)
5
  ##
6
- ## SSH access: sshd on :22 (internal) reached via reverse tunnel
7
- ## to a Fly.io jump server. No inbound tunnel services used.
8
  FROM debian:bookworm-slim
9
 
10
  ENV DEBIAN_FRONTEND=noninteractive
@@ -20,16 +20,20 @@ ENV PORT=7860
20
 
21
  ARG OPENCODE_VERSION=1.18.3
22
  ARG TTYD_VERSION=1.7.7
 
23
 
24
  RUN apt-get update && apt-get install -y --no-install-recommends \
25
  ca-certificates curl git gnupg python3 python3-pip nginx \
26
- openssh-server openssh-client \
27
  && curl -fsSL "https://github.com/anomalyco/opencode/releases/download/v${OPENCODE_VERSION}/opencode-linux-x64.tar.gz" \
28
  | tar -xz -C /usr/local/bin opencode \
29
  && chmod +x /usr/local/bin/opencode \
30
  && curl -fsSL "https://github.com/tsl0922/ttyd/releases/download/${TTYD_VERSION}/ttyd.x86_64" \
31
  -o /usr/local/bin/ttyd \
32
  && chmod +x /usr/local/bin/ttyd \
 
 
 
33
  && mkdir -p /var/run/sshd \
34
  && rm -rf /var/lib/apt/lists/*
35
 
 
3
  ## /terminal → ttyd on :7681 (real PTY bash)
4
  ## / → opencode on :8080 (chat UI + API)
5
  ##
6
+ ## SSH access: sshd on :22 (internal) reached via bore transparent TCP tunnel.
7
+ ## bore.pub:PORT → raw TCP → sshd:22 (no SSH interception → full PTY support)
8
  FROM debian:bookworm-slim
9
 
10
  ENV DEBIAN_FRONTEND=noninteractive
 
20
 
21
  ARG OPENCODE_VERSION=1.18.3
22
  ARG TTYD_VERSION=1.7.7
23
+ ARG BORE_VERSION=0.6.0
24
 
25
  RUN apt-get update && apt-get install -y --no-install-recommends \
26
  ca-certificates curl git gnupg python3 python3-pip nginx \
27
+ openssh-server \
28
  && curl -fsSL "https://github.com/anomalyco/opencode/releases/download/v${OPENCODE_VERSION}/opencode-linux-x64.tar.gz" \
29
  | tar -xz -C /usr/local/bin opencode \
30
  && chmod +x /usr/local/bin/opencode \
31
  && curl -fsSL "https://github.com/tsl0922/ttyd/releases/download/${TTYD_VERSION}/ttyd.x86_64" \
32
  -o /usr/local/bin/ttyd \
33
  && chmod +x /usr/local/bin/ttyd \
34
+ && curl -fsSL "https://github.com/ekzhang/bore/releases/download/v${BORE_VERSION}/bore-v${BORE_VERSION}-x86_64-unknown-linux-musl.tar.gz" \
35
+ | tar -xz -C /usr/local/bin bore \
36
+ && chmod +x /usr/local/bin/bore \
37
  && mkdir -p /var/run/sshd \
38
  && rm -rf /var/lib/apt/lists/*
39
 
entrypoint.sh CHANGED
@@ -5,8 +5,9 @@
5
  # /terminal → ttyd :7681 — real PTY bash, mobile-optimised
6
  # / → opencode :8080 — chat UI + REST API + SSE
7
  # sshd :22 (internal only)
8
- # ↑ exposed via reverse SSH tunnel through serveo.net
9
- # Termius → opencode-hf.serveo.net:22 → here
 
10
  #
11
  set -u
12
 
@@ -156,106 +157,80 @@ ssh-keygen -A 2>/dev/null || true
156
  /usr/sbin/sshd
157
  echo "[SSH] sshd running on port 22."
158
 
159
- # ─── Reverse SSH tunnel via serveo.net ───────────────────────────────
160
- # serveo.net is a free relay — no account, no binary, no tokens.
161
- # We open an outbound SSH connection (HF allows this) and serveo
162
- # exposes the tunnel at a public hostname:port.
 
163
  #
164
- # Two attempts:
165
- # 1. Named subdomain: opencode-hf.serveo.net:22 (fixed, easy to remember)
166
- # 2. Random port: serveo.net:NNNNN (fallback)
167
- #
168
- SERVEO_LOG=/data/logs/serveo.log
169
  TUNNEL_HOST=""
170
  TUNNEL_PORT=""
 
171
 
172
- echo "[TUNNEL] Starting SSH tunnel via serveo.net..."
173
-
174
- # Attempt 1 — named subdomain (opencode-hf.serveo.net:22)
175
- ssh -o StrictHostKeyChecking=no \
176
- -o UserKnownHostsFile=/dev/null \
177
- -o ServerAliveInterval=30 \
178
- -o ServerAliveCountMax=6 \
179
- -o LogLevel=ERROR \
180
- -R opencode-hf:22:localhost:22 \
181
- serveo.net > "${SERVEO_LOG}" 2>&1 &
182
- SERVEO_PID=$!
183
 
184
- sleep 8
 
185
 
186
- # Check if named subdomain was accepted
187
- if kill -0 "${SERVEO_PID}" 2>/dev/null; then
188
- # Process still running — tunnel likely up
189
- TUNNEL_HOST="opencode-hf.serveo.net"
190
- TUNNEL_PORT="22"
191
- else
192
- # Named subdomain taken or serveo refused — try random port
193
- echo "[TUNNEL] Named subdomain unavailable, trying random port..."
194
- ssh -o StrictHostKeyChecking=no \
195
- -o UserKnownHostsFile=/dev/null \
196
- -o ServerAliveInterval=30 \
197
- -o ServerAliveCountMax=6 \
198
- -o LogLevel=INFO \
199
- -R 0:localhost:22 \
200
- serveo.net > "${SERVEO_LOG}" 2>&1 &
201
- SERVEO_PID=$!
202
- sleep 8
203
 
204
- # Parse assigned port from log
205
- TUNNEL_PORT=$(grep -oE 'Allocated port [0-9]+' "${SERVEO_LOG}" 2>/dev/null \
206
- | grep -oE '[0-9]+' | head -1)
207
- if [ -n "${TUNNEL_PORT}" ]; then
208
- TUNNEL_HOST="serveo.net"
209
- fi
210
  fi
211
 
212
- # Report result
213
  echo "[TUNNEL] ============================================"
214
  if [ -n "${TUNNEL_HOST}" ]; then
215
- echo "[TUNNEL] SSH tunnel is UP via serveo.net"
 
216
  echo "[TUNNEL] ----"
217
- echo "[TUNNEL] Termius host : ${TUNNEL_HOST}"
218
- echo "[TUNNEL] Termius port : ${TUNNEL_PORT}"
219
- echo "[TUNNEL] Termius username : root"
220
- echo "[TUNNEL] Termius password : your SSH_PASSWORD secret"
221
  echo "[TUNNEL] ----"
222
- echo "[TUNNEL] Tunnel log : ${SERVEO_LOG}"
 
 
 
223
 
224
- # Auto-restart loop in background if tunnel drops
225
  (
 
226
  while true; do
227
- sleep 15
228
- if ! kill -0 "${SERVEO_PID}" 2>/dev/null; then
229
- echo "[TUNNEL] Tunnel dropped — reconnecting..." >> "${SERVEO_LOG}"
230
- if [ "${TUNNEL_HOST}" = "opencode-hf.serveo.net" ]; then
231
- ssh -o StrictHostKeyChecking=no \
232
- -o UserKnownHostsFile=/dev/null \
233
- -o ServerAliveInterval=30 \
234
- -o ServerAliveCountMax=6 \
235
- -o LogLevel=ERROR \
236
- -R opencode-hf:22:localhost:22 \
237
- serveo.net >> "${SERVEO_LOG}" 2>&1 &
238
- SERVEO_PID=$!
239
- else
240
- ssh -o StrictHostKeyChecking=no \
241
- -o UserKnownHostsFile=/dev/null \
242
- -o ServerAliveInterval=30 \
243
- -o ServerAliveCountMax=6 \
244
- -o LogLevel=ERROR \
245
- -R 0:localhost:22 \
246
- serveo.net >> "${SERVEO_LOG}" 2>&1 &
247
- SERVEO_PID=$!
248
  fi
249
  fi
250
  done
251
  ) &
252
  else
253
- echo "[TUNNEL] WARNING: Could not establish tunnel via serveo.net."
254
- echo "[TUNNEL] serveo.net may be temporarily down."
255
- echo "[TUNNEL] Check ${SERVEO_LOG} for details."
256
- echo "[TUNNEL] Terminal at /terminal still works without it."
 
257
  fi
258
- echo "[TUNNEL] ============================================"
259
 
260
  # ─── Start DB self-healing daemon ────────────────────────────────────
261
  echo "[CLEANER] Starting self-healing daemon..."
 
5
  # /terminal → ttyd :7681 — real PTY bash, mobile-optimised
6
  # / → opencode :8080 — chat UI + REST API + SSE
7
  # sshd :22 (internal only)
8
+ # ↑ exposed via bore transparent TCP tunnel
9
+ # Termius → bore.pub:PORT → raw TCP → sshd:22
10
+ # (no SSH interception = full PTY, arrow keys, Ctrl+C, vim)
11
  #
12
  set -u
13
 
 
157
  /usr/sbin/sshd
158
  echo "[SSH] sshd running on port 22."
159
 
160
+ # ─── Transparent TCP tunnel via bore ─────────────────────────────────
161
+ # bore forwards raw TCP bytes without touching the SSH protocol.
162
+ # Termius → bore.pub:PORT → raw TCP → sshd:22
163
+ # The SSH handshake happens directly between Termius and sshd — no
164
+ # middleman, so PTY, arrow keys, Ctrl+C, vim all work perfectly.
165
  #
166
+ BORE_LOG=/data/logs/bore.log
 
 
 
 
167
  TUNNEL_HOST=""
168
  TUNNEL_PORT=""
169
+ BORE_PID=""
170
 
171
+ echo "[TUNNEL] Starting transparent TCP tunnel via bore..."
172
+ > "${BORE_LOG}"
 
 
 
 
 
 
 
 
 
173
 
174
+ bore local 22 --to bore.pub > "${BORE_LOG}" 2>&1 &
175
+ BORE_PID=$!
176
 
177
+ # Wait up to 20s for bore to print the assigned port
178
+ for _i in $(seq 1 20); do
179
+ sleep 1
180
+ TUNNEL_PORT=$(grep -oE 'bore\.pub:[0-9]+' "${BORE_LOG}" 2>/dev/null \
181
+ | grep -oE '[0-9]+$' | head -1)
182
+ [ -n "${TUNNEL_PORT}" ] && break
183
+ done
 
 
 
 
 
 
 
 
 
 
184
 
185
+ if [ -n "${TUNNEL_PORT}" ] && kill -0 "${BORE_PID}" 2>/dev/null; then
186
+ TUNNEL_HOST="bore.pub"
 
 
 
 
187
  fi
188
 
 
189
  echo "[TUNNEL] ============================================"
190
  if [ -n "${TUNNEL_HOST}" ]; then
191
+ echo "[TUNNEL] Tunnel type : bore (transparent TCP — no SSH interception)"
192
+ echo "[TUNNEL] Status : UP"
193
  echo "[TUNNEL] ----"
194
+ echo "[TUNNEL] Termius host : ${TUNNEL_HOST}"
195
+ echo "[TUNNEL] Termius port : ${TUNNEL_PORT}"
196
+ echo "[TUNNEL] Termius user : root"
197
+ echo "[TUNNEL] Termius pass : (your SSH_PASSWORD secret)"
198
  echo "[TUNNEL] ----"
199
+ echo "[TUNNEL] PTY support : FULL (arrow keys, Ctrl+C, vim, tab completion)"
200
+ echo "[TUNNEL] Tunnel log : ${BORE_LOG}"
201
+ echo "[TUNNEL] ============================================"
202
+ echo "bore.pub:${TUNNEL_PORT}" > /data/logs/tunnel-url.txt
203
 
204
+ # Auto-restart: if bore dies, reconnect and log the new port
205
  (
206
+ _bore_pid="${BORE_PID}"
207
  while true; do
208
+ sleep 10
209
+ if ! kill -0 "${_bore_pid}" 2>/dev/null; then
210
+ _ts="$(date -u '+%H:%M:%S')"
211
+ echo "[TUNNEL][${_ts}] bore dropped — reconnecting..." >> "${BORE_LOG}"
212
+ bore local 22 --to bore.pub >> "${BORE_LOG}" 2>&1 &
213
+ _bore_pid=$!
214
+ for _w in $(seq 1 15); do
215
+ sleep 1
216
+ _new_port=$(grep -oE 'bore\.pub:[0-9]+' "${BORE_LOG}" 2>/dev/null \
217
+ | grep -oE '[0-9]+$' | tail -1)
218
+ [ -n "${_new_port}" ] && break
219
+ done
220
+ if [ -n "${_new_port}" ]; then
221
+ echo "[TUNNEL][$(date -u '+%H:%M:%S')] Reconnected: bore.pub:${_new_port}" >> "${BORE_LOG}"
222
+ echo "bore.pub:${_new_port}" > /data/logs/tunnel-url.txt
 
 
 
 
 
 
223
  fi
224
  fi
225
  done
226
  ) &
227
  else
228
+ echo "[TUNNEL] FAILED — bore could not connect to bore.pub"
229
+ echo "[TUNNEL] Bore output:"
230
+ cat "${BORE_LOG}" 2>/dev/null | sed 's/^/[TUNNEL] /'
231
+ echo "[TUNNEL] Terminal at /terminal still works."
232
+ echo "[TUNNEL] ============================================"
233
  fi
 
234
 
235
  # ─── Start DB self-healing daemon ────────────────────────────────────
236
  echo "[CLEANER] Starting self-healing daemon..."