Spaces:
Paused
Paused
Replit Agent commited on
Commit Β·
bc3314b
1
Parent(s): 8a804bd
feat: migrate to Cloudflare Named Tunnel (remove Quick Tunnel)
Browse files- Replace 'cloudflared tunnel --url ssh://localhost:22' (Quick Tunnel)
with 'cloudflared tunnel run --token $CF_TUNNEL_TOKEN' (Named Tunnel)
- Add sshd health check via ss -tlnp before starting tunnel
- Add iproute2 to Dockerfile for ss(8) command
- Gracefully skip tunnel if CF_TUNNEL_TOKEN is not set (warn + continue)
- Log connector ID, tunnel name, edge location, protocol on connect
- Detect and diagnose: auth failures, reconnect loops, network errors,
invalid tokens, duplicate connectors
- Never print or log the tunnel token
- Remove all Quick Tunnel code: trycloudflare.com URL parsing,
tunnel-url.txt generation, 60s URL wait loop
- Update .env.example with CF_TUNNEL_TOKEN and SSH_PASSWORD docs
- .env.example +19 -2
- Dockerfile +1 -1
- entrypoint.sh +158 -33
.env.example
CHANGED
|
@@ -1,7 +1,24 @@
|
|
| 1 |
-
# API Keys (at least one required)
|
| 2 |
ANTHROPIC_API_KEY=
|
| 3 |
OPENAI_API_KEY=
|
| 4 |
|
| 5 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
OPENCODE_SERVER_USERNAME=
|
| 7 |
OPENCODE_SERVER_PASSWORD=
|
|
|
|
| 1 |
+
# API Keys (at least one required for AI features)
|
| 2 |
ANTHROPIC_API_KEY=
|
| 3 |
OPENAI_API_KEY=
|
| 4 |
|
| 5 |
+
# SSH access via Cloudflare Named Tunnel
|
| 6 |
+
# Required for Termius connections.
|
| 7 |
+
# Steps:
|
| 8 |
+
# 1. Log in to https://one.dash.cloudflare.com
|
| 9 |
+
# 2. Go to Zero Trust β Networks β Tunnels
|
| 10 |
+
# 3. Create a tunnel (or select an existing one)
|
| 11 |
+
# 4. Under "Public Hostname", add a hostname routed to:
|
| 12 |
+
# Service type: SSH
|
| 13 |
+
# URL: localhost:22
|
| 14 |
+
# 5. On the tunnel's Overview tab, copy the tunnel token
|
| 15 |
+
# 6. Paste it here (or add as a Hugging Face Space Secret)
|
| 16 |
+
CF_TUNNEL_TOKEN=
|
| 17 |
+
|
| 18 |
+
# SSH password for the root user inside the container.
|
| 19 |
+
# If omitted, a random password is generated and printed to Space logs on startup.
|
| 20 |
+
SSH_PASSWORD=
|
| 21 |
+
|
| 22 |
+
# Optional: HTTP Basic Auth for the OpenCode server
|
| 23 |
OPENCODE_SERVER_USERNAME=
|
| 24 |
OPENCODE_SERVER_PASSWORD=
|
Dockerfile
CHANGED
|
@@ -20,7 +20,7 @@ ARG TTYD_VERSION=1.7.7
|
|
| 20 |
|
| 21 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 22 |
ca-certificates curl git gnupg python3 python3-pip nginx \
|
| 23 |
-
openssh-server \
|
| 24 |
&& curl -fsSL "https://github.com/anomalyco/opencode/releases/download/v${OPENCODE_VERSION}/opencode-linux-x64.tar.gz" \
|
| 25 |
| tar -xz -C /usr/local/bin opencode \
|
| 26 |
&& chmod +x /usr/local/bin/opencode \
|
|
|
|
| 20 |
|
| 21 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 22 |
ca-certificates curl git gnupg python3 python3-pip nginx \
|
| 23 |
+
openssh-server iproute2 \
|
| 24 |
&& curl -fsSL "https://github.com/anomalyco/opencode/releases/download/v${OPENCODE_VERSION}/opencode-linux-x64.tar.gz" \
|
| 25 |
| tar -xz -C /usr/local/bin opencode \
|
| 26 |
&& chmod +x /usr/local/bin/opencode \
|
entrypoint.sh
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
# nginx :7860 (HF exposed)
|
| 5 |
# /terminal β ttyd :7681 β real PTY bash, mobile-optimised
|
| 6 |
# / β opencode :8080 β chat UI + REST API + SSE
|
| 7 |
-
# sshd :22 (Cloudflare
|
| 8 |
#
|
| 9 |
set -u
|
| 10 |
|
|
@@ -131,7 +131,7 @@ nginx
|
|
| 131 |
echo "[NGINX] Started."
|
| 132 |
|
| 133 |
# βββ SSH server ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 134 |
-
# Listens on port 22 (internal only β Cloudflare
|
| 135 |
# Password is set via SSH_PASSWORD Space Secret.
|
| 136 |
# Falls back to a generated password logged to Space logs if not set.
|
| 137 |
echo "[SSH] Configuring SSH server..."
|
|
@@ -172,42 +172,167 @@ ssh-keygen -A 2>/dev/null || true
|
|
| 172 |
/usr/sbin/sshd
|
| 173 |
echo "[SSH] sshd started on port 22."
|
| 174 |
|
| 175 |
-
# βββ
|
| 176 |
-
#
|
| 177 |
-
#
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
--no-autoupdate \
|
| 184 |
-
--logfile "${CF_LOG}" \
|
| 185 |
-
--loglevel info \
|
| 186 |
-
> /dev/null 2>&1 &
|
| 187 |
-
|
| 188 |
-
# Wait up to 60 s for the tunnel URL to appear in the log
|
| 189 |
-
# cloudflared logs the URL at INFO level β must use --loglevel info (not warn)
|
| 190 |
-
TUNNEL_URL=""
|
| 191 |
-
for i in $(seq 1 60); do
|
| 192 |
sleep 1
|
| 193 |
-
TUNNEL_URL=$(grep -o 'https://[^ |]*\.trycloudflare\.com' "${CF_LOG}" 2>/dev/null | tr -d ' ' | head -1)
|
| 194 |
-
[ -n "${TUNNEL_URL}" ] && break
|
| 195 |
done
|
| 196 |
|
| 197 |
-
if [ -n "$
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
echo "[
|
| 202 |
-
echo "[
|
| 203 |
-
echo "[
|
| 204 |
-
|
| 205 |
-
echo "[
|
| 206 |
-
echo "[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
echo "[TUNNEL] ============================================"
|
| 208 |
else
|
| 209 |
-
echo "[TUNNEL]
|
| 210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
fi
|
| 212 |
|
| 213 |
# βββ Start DB self-healing daemon ββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 4 |
# nginx :7860 (HF exposed)
|
| 5 |
# /terminal β ttyd :7681 β real PTY bash, mobile-optimised
|
| 6 |
# / β opencode :8080 β chat UI + REST API + SSE
|
| 7 |
+
# sshd :22 (Cloudflare Named Tunnel β Termius)
|
| 8 |
#
|
| 9 |
set -u
|
| 10 |
|
|
|
|
| 131 |
echo "[NGINX] Started."
|
| 132 |
|
| 133 |
# βββ SSH server ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 134 |
+
# Listens on port 22 (internal only β Cloudflare Named Tunnel exposes it).
|
| 135 |
# Password is set via SSH_PASSWORD Space Secret.
|
| 136 |
# Falls back to a generated password logged to Space logs if not set.
|
| 137 |
echo "[SSH] Configuring SSH server..."
|
|
|
|
| 172 |
/usr/sbin/sshd
|
| 173 |
echo "[SSH] sshd started on port 22."
|
| 174 |
|
| 175 |
+
# βββ Verify sshd is actually listening on port 22 ββββββββββββββββββββ
|
| 176 |
+
# Uses ss(8) from iproute2. Fails hard if sshd did not bind β a tunnel
|
| 177 |
+
# to a port nobody is listening on is worse than no tunnel at all.
|
| 178 |
+
echo "[SSH] Verifying sshd is listening on port 22..."
|
| 179 |
+
SSHD_LISTEN=""
|
| 180 |
+
for _i in $(seq 1 10); do
|
| 181 |
+
SSHD_LISTEN=$(ss -tlnp 2>/dev/null | grep ' 0\.0\.0\.0:22 \|:22 ' || true)
|
| 182 |
+
[ -n "$SSHD_LISTEN" ] && break
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
sleep 1
|
|
|
|
|
|
|
| 184 |
done
|
| 185 |
|
| 186 |
+
if [ -n "$SSHD_LISTEN" ]; then
|
| 187 |
+
echo "[SSH] Confirmed: sshd is listening on port 22."
|
| 188 |
+
echo "[SSH] $SSHD_LISTEN"
|
| 189 |
+
else
|
| 190 |
+
echo "[SSH] ============================================"
|
| 191 |
+
echo "[SSH] FATAL: sshd is NOT listening on port 22."
|
| 192 |
+
echo "[SSH] Active TCP listeners:"
|
| 193 |
+
ss -tlnp 2>/dev/null || true
|
| 194 |
+
echo "[SSH] ============================================"
|
| 195 |
+
echo "[SSH] Cannot start Cloudflare tunnel β no SSH service to tunnel to."
|
| 196 |
+
# Do not exit β OpenCode and the terminal should still work.
|
| 197 |
+
# The operator needs to investigate why sshd failed to bind.
|
| 198 |
+
fi
|
| 199 |
+
|
| 200 |
+
# βββ Cloudflare Named Tunnel ββββββββββββββββββββββββββββββββββββββββββ
|
| 201 |
+
# Requires CF_TUNNEL_TOKEN set in Space Secrets.
|
| 202 |
+
# The token is obtained from the Cloudflare Zero Trust dashboard:
|
| 203 |
+
# Zero Trust β Networks β Tunnels β select/create tunnel β Overview tab β copy token
|
| 204 |
+
#
|
| 205 |
+
# The named tunnel must be configured in Cloudflare Zero Trust to route
|
| 206 |
+
# the public hostname to ssh://localhost:22 (service: SSH, URL: localhost:22).
|
| 207 |
+
#
|
| 208 |
+
# NOTE: The token is NEVER printed or logged.
|
| 209 |
+
echo "[TUNNEL] ============================================"
|
| 210 |
+
CF_LOG=/data/logs/cloudflared.log
|
| 211 |
+
|
| 212 |
+
if [ -z "${CF_TUNNEL_TOKEN:-}" ]; then
|
| 213 |
+
echo "[TUNNEL] WARNING: CF_TUNNEL_TOKEN secret is not set."
|
| 214 |
+
echo "[TUNNEL] SSH access via Cloudflare Named Tunnel is DISABLED."
|
| 215 |
+
echo "[TUNNEL] To enable:"
|
| 216 |
+
echo "[TUNNEL] 1. Create a Named Tunnel in Cloudflare Zero Trust"
|
| 217 |
+
echo "[TUNNEL] (Zero Trust β Networks β Tunnels β Add a tunnel)"
|
| 218 |
+
echo "[TUNNEL] 2. Configure the public hostname to route to ssh://localhost:22"
|
| 219 |
+
echo "[TUNNEL] 3. Copy the tunnel token from the Cloudflare dashboard"
|
| 220 |
+
echo "[TUNNEL] 4. Add CF_TUNNEL_TOKEN to Space Secrets"
|
| 221 |
+
echo "[TUNNEL] OpenCode and terminal will start normally without it."
|
| 222 |
echo "[TUNNEL] ============================================"
|
| 223 |
else
|
| 224 |
+
echo "[TUNNEL] Named tunnel starting..."
|
| 225 |
+
# Use 'run --token' β the only correct form for Named Tunnels.
|
| 226 |
+
# Do NOT use 'tunnel --url' (Quick Tunnel) β it creates ephemeral
|
| 227 |
+
# trycloudflare.com addresses that SSH clients cannot reliably use.
|
| 228 |
+
cloudflared tunnel run \
|
| 229 |
+
--token "${CF_TUNNEL_TOKEN}" \
|
| 230 |
+
--no-autoupdate \
|
| 231 |
+
--logfile "${CF_LOG}" \
|
| 232 |
+
--loglevel info \
|
| 233 |
+
> /dev/null 2>&1 &
|
| 234 |
+
CF_PID=$!
|
| 235 |
+
|
| 236 |
+
# ββ Wait up to 40 s for the tunnel to register βββββββββββββββββββ
|
| 237 |
+
TUNNEL_CONNECTED=0
|
| 238 |
+
for _i in $(seq 1 40); do
|
| 239 |
+
sleep 1
|
| 240 |
+
# cloudflared Named Tunnel logs "Registered tunnel connection" on success
|
| 241 |
+
if grep -qi \
|
| 242 |
+
"Registered tunnel connection\|registered tunnel connection\|Connection registered" \
|
| 243 |
+
"${CF_LOG}" 2>/dev/null; then
|
| 244 |
+
TUNNEL_CONNECTED=1
|
| 245 |
+
break
|
| 246 |
+
fi
|
| 247 |
+
# Detect fatal auth errors early β no point waiting the full 40 s
|
| 248 |
+
if grep -qi \
|
| 249 |
+
"Invalid.*token\|token.*invalid\|failed to authenticate\|authentication failed\|ERR_FAILED_TO_AUTHENTICATE\|Invalid tunnel credentials\|failed to validate token" \
|
| 250 |
+
"${CF_LOG}" 2>/dev/null; then
|
| 251 |
+
echo "[TUNNEL] ERROR: Cloudflare authentication failed β aborting wait."
|
| 252 |
+
break
|
| 253 |
+
fi
|
| 254 |
+
done
|
| 255 |
+
|
| 256 |
+
if [ "$TUNNEL_CONNECTED" = "1" ]; then
|
| 257 |
+
# ββ Extract metadata from JSON log lines ββββββββββββββββββββββ
|
| 258 |
+
# cloudflared Named Tunnel writes structured JSON; field values
|
| 259 |
+
# are extracted with portable sed without exposing the token.
|
| 260 |
+
CONNECTOR_ID=$(grep -o '"connectorID":"[^"]*"' "${CF_LOG}" 2>/dev/null \
|
| 261 |
+
| head -1 | sed 's/"connectorID":"//;s/"//')
|
| 262 |
+
TUNNEL_NAME=$(grep -o '"tunnelName":"[^"]*"' "${CF_LOG}" 2>/dev/null \
|
| 263 |
+
| head -1 | sed 's/"tunnelName":"//;s/"//')
|
| 264 |
+
TUNNEL_ID=$(grep -o '"tunnelID":"[^"]*"' "${CF_LOG}" 2>/dev/null \
|
| 265 |
+
| head -1 | sed 's/"tunnelID":"//;s/"//')
|
| 266 |
+
CONN_LOCATION=$(grep -o '"location":"[^"]*"' "${CF_LOG}" 2>/dev/null \
|
| 267 |
+
| head -1 | sed 's/"location":"//;s/"//')
|
| 268 |
+
CONN_PROTOCOL=$(grep -o '"protocol":"[^"]*"' "${CF_LOG}" 2>/dev/null \
|
| 269 |
+
| head -1 | sed 's/"protocol":"//;s/"//')
|
| 270 |
+
|
| 271 |
+
echo "[TUNNEL] Named tunnel connected"
|
| 272 |
+
echo "[TUNNEL] ============================================"
|
| 273 |
+
[ -n "$CONNECTOR_ID" ] && echo "[TUNNEL] Connector ID : ${CONNECTOR_ID}"
|
| 274 |
+
[ -n "$TUNNEL_NAME" ] && echo "[TUNNEL] Tunnel name : ${TUNNEL_NAME}"
|
| 275 |
+
[ -n "$TUNNEL_ID" ] && echo "[TUNNEL] Tunnel ID : ${TUNNEL_ID}"
|
| 276 |
+
[ -n "$CONN_LOCATION" ] && echo "[TUNNEL] Edge location : ${CONN_LOCATION}"
|
| 277 |
+
[ -n "$CONN_PROTOCOL" ] && echo "[TUNNEL] Protocol : ${CONN_PROTOCOL}"
|
| 278 |
+
echo "[TUNNEL] SSH port : 22"
|
| 279 |
+
echo "[TUNNEL] SSH user : root"
|
| 280 |
+
echo "[TUNNEL] Connect via : the public hostname configured in"
|
| 281 |
+
echo "[TUNNEL] Cloudflare Zero Trust for this tunnel."
|
| 282 |
+
echo "[TUNNEL] Logs : ${CF_LOG}"
|
| 283 |
+
echo "[TUNNEL] ============================================"
|
| 284 |
+
else
|
| 285 |
+
# ββ Diagnose the failure ββββββββββββββββββββββββββββββββββββββ
|
| 286 |
+
echo "[TUNNEL] ============================================"
|
| 287 |
+
echo "[TUNNEL] WARNING: Named tunnel did NOT confirm connection within 40 s."
|
| 288 |
+
echo "[TUNNEL] Connection status: FAILED"
|
| 289 |
+
echo "[TUNNEL] ----"
|
| 290 |
+
|
| 291 |
+
# Show recent log lines β strip any line containing the token pattern
|
| 292 |
+
# (tokens are long base64 strings; we never echo the CF_TUNNEL_TOKEN var).
|
| 293 |
+
echo "[TUNNEL] Recent cloudflared output:"
|
| 294 |
+
tail -25 "${CF_LOG}" 2>/dev/null \
|
| 295 |
+
| grep -v -i "token\|credential\|secret\|password" \
|
| 296 |
+
|| echo "[TUNNEL] (log not yet written)"
|
| 297 |
+
echo "[TUNNEL] ----"
|
| 298 |
+
|
| 299 |
+
# Specific diagnoses
|
| 300 |
+
if grep -qi \
|
| 301 |
+
"Invalid.*token\|token.*invalid\|failed to validate token\|Invalid tunnel credentials\|ERR_FAILED_TO_AUTHENTICATE\|authentication failed\|failed to authenticate" \
|
| 302 |
+
"${CF_LOG}" 2>/dev/null; then
|
| 303 |
+
echo "[TUNNEL] DIAGNOSIS : Invalid or expired tunnel token."
|
| 304 |
+
echo "[TUNNEL] ACTION : Regenerate the token in Cloudflare Zero Trust:"
|
| 305 |
+
echo "[TUNNEL] Zero Trust β Networks β Tunnels β select tunnel"
|
| 306 |
+
echo "[TUNNEL] β Overview tab β copy new token"
|
| 307 |
+
echo "[TUNNEL] Update CF_TUNNEL_TOKEN in Space Secrets."
|
| 308 |
+
|
| 309 |
+
elif grep -qi "reconnect\|retrying connection\|retry connection" \
|
| 310 |
+
"${CF_LOG}" 2>/dev/null; then
|
| 311 |
+
echo "[TUNNEL] DIAGNOSIS : Tunnel is in a reconnect loop."
|
| 312 |
+
echo "[TUNNEL] ACTION : Check Cloudflare service status and Space outbound"
|
| 313 |
+
echo "[TUNNEL] network connectivity."
|
| 314 |
+
|
| 315 |
+
elif grep -qi \
|
| 316 |
+
"dial tcp\|connection refused\|no such host\|network unreachable\|i/o timeout\|TLS handshake" \
|
| 317 |
+
"${CF_LOG}" 2>/dev/null; then
|
| 318 |
+
echo "[TUNNEL] DIAGNOSIS : Network error β cannot reach the Cloudflare edge."
|
| 319 |
+
echo "[TUNNEL] ACTION : Verify outbound HTTPS/QUIC is not blocked from the Space."
|
| 320 |
+
|
| 321 |
+
elif grep -qi "already running\|tunnel already" \
|
| 322 |
+
"${CF_LOG}" 2>/dev/null; then
|
| 323 |
+
echo "[TUNNEL] DIAGNOSIS : A duplicate tunnel connector may already be running."
|
| 324 |
+
echo "[TUNNEL] ACTION : Check your Cloudflare Zero Trust Tunnels list for"
|
| 325 |
+
echo "[TUNNEL] stale connectors and remove them."
|
| 326 |
+
|
| 327 |
+
else
|
| 328 |
+
echo "[TUNNEL] DIAGNOSIS : Unknown β inspect full log for details."
|
| 329 |
+
echo "[TUNNEL] Log path : ${CF_LOG}"
|
| 330 |
+
fi
|
| 331 |
+
|
| 332 |
+
echo "[TUNNEL] ============================================"
|
| 333 |
+
echo "[TUNNEL] OpenCode and terminal continue normally."
|
| 334 |
+
echo "[TUNNEL] SSH access via Cloudflare will be unavailable until resolved."
|
| 335 |
+
fi
|
| 336 |
fi
|
| 337 |
|
| 338 |
# βββ Start DB self-healing daemon ββββββββββββββββββββββββββββββββββββ
|