Spaces:
Paused
fix: replace serveo with bore transparent TCP tunnel — fixes Termius connection
Browse filesROOT 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
- Dockerfile +7 -3
- entrypoint.sh +55 -80
|
@@ -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
|
| 7 |
-
##
|
| 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
|
| 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 |
|
|
@@ -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
|
| 9 |
-
# Termius →
|
|
|
|
| 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 |
-
# ───
|
| 160 |
-
#
|
| 161 |
-
#
|
| 162 |
-
#
|
|
|
|
| 163 |
#
|
| 164 |
-
|
| 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
|
| 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 |
-
|
|
|
|
| 185 |
|
| 186 |
-
#
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 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 |
-
|
| 205 |
-
|
| 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]
|
|
|
|
| 216 |
echo "[TUNNEL] ----"
|
| 217 |
-
echo "[TUNNEL] Termius host
|
| 218 |
-
echo "[TUNNEL] Termius port
|
| 219 |
-
echo "[TUNNEL] Termius
|
| 220 |
-
echo "[TUNNEL] Termius
|
| 221 |
echo "[TUNNEL] ----"
|
| 222 |
-
echo "[TUNNEL]
|
|
|
|
|
|
|
|
|
|
| 223 |
|
| 224 |
-
# Auto-restart
|
| 225 |
(
|
|
|
|
| 226 |
while true; do
|
| 227 |
-
sleep
|
| 228 |
-
if ! kill -0 "${
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 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]
|
| 254 |
-
echo "[TUNNEL]
|
| 255 |
-
|
| 256 |
-
echo "[TUNNEL] Terminal at /terminal still works
|
|
|
|
| 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..."
|