Files
muxplex/cloudflare-tunnel-setup.md
Ken 2ec6f73843 feat: mobile UX polish, shell injection fix, and performance optimizations
This commit combines multiple improvements for mobile support, reliability, and performance:

Mobile & Accessibility Improvements:
- Fix touch scrolling on mobile by removing CSS overflow-y:hidden that blocked xterm.js native scroll
- Add beforeinput handler to prevent Android IME double-space-to-period duplication
- Implement mobile control character toolbar (Esc, Tab, Ctrl/Alt toggles, arrows, special chars)
- One-shot modifier toggles bring soft keyboard on demand without showing it for other keys
- Replace 25+ unicode/HTML entity icons with inline SVGs across app for better rendering

Security & Reliability:
- Fix shell injection vulnerability: session names with spaces/special chars now properly quoted
  - Use shlex.quote() in create_session and delete_session endpoints
  - URL-encode session names in bell hook to prevent malformed curl URLs
  - Also hardens against command injection through crafted session names

Performance Optimizations:
- Eliminate 2.4-5.6 second session creation delay (was 3 compounding bottlenecks)
  - Frontend: check for new session immediately (was waiting 2s for interval tick)
  - Backend: eagerly refresh session cache after tmux creation (was waiting for next poll)
  - Connection: poll for ttyd readiness instead of blind 0.8s sleep
- Typical new session now appears in UI in 200-400ms vs previous 4-5 seconds

Infrastructure:
- Improve ttyd port detection with multi-tool fallback (lsof → fuser → ss)
- Add Cloudflare tunnel setup documentation

Test Updates:
- Update frontend tests to match new setTimeout recursion pattern (replaces setInterval)

All 1306 tests pass.

Generated with Amplifier
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
2026-05-27 01:01:55 +00:00

5.4 KiB

Cloudflare Tunnel Setup for Incus Host

Overview

Single routing point for all services across Incus instances. Services self-declare by dropping a file into a shared directory. No port forwarding, no OPNsense reverse proxy config, no DDNS.

Prerequisites

  • Domain: ampbox.io (registrar: Namecheap)
  • Cloudflare account (free tier) with ampbox.io DNS delegated
    • At Namecheap: set nameservers to the ones Cloudflare assigns
    • Verify domain is active in Cloudflare dashboard

1. Install cloudflared on the Incus host

curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \
  | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] \
  https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" \
  | sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt update && sudo apt install -y cloudflared

2. Authenticate and create the tunnel

cloudflared tunnel login          # opens browser, authorize ampbox.io
cloudflared tunnel create ampbox  # creates tunnel, saves credentials JSON
cloudflared tunnel route dns ampbox "*.ampbox.io"  # wildcard DNS route

Note the credentials file path printed (typically ~/.cloudflared/<TUNNEL_ID>.json). You need it for step 4.

3. Create the service declaration directory

sudo mkdir -p /etc/cloudflared/services.d

4. Write the main tunnel config

# Replace <TUNNEL_ID> with your actual tunnel ID from step 2
sudo tee /etc/cloudflared/config.yml << 'EOF'
tunnel: ampbox
credentials-file: /root/.cloudflared/<TUNNEL_ID>.json

ingress:
  - service: http_status:404
EOF

5. Share the declaration directory into all instances

incus profile device add default svc-declare disk \
    source=/etc/cloudflared/services.d \
    path=/mnt/services

6. Helper scripts (on the host)

/usr/local/bin/svc-rebuild

Reads all service declarations and rebuilds the cloudflared ingress config.

#!/usr/bin/env bash
set -euo pipefail

CONFIG="/etc/cloudflared/config.yml"
SERVICES_DIR="/etc/cloudflared/services.d"
TUNNEL_CREDS="/root/.cloudflared/<TUNNEL_ID>.json"  # fix this

cat > "$CONFIG" <<HEADER
tunnel: ampbox
credentials-file: $TUNNEL_CREDS

ingress:
HEADER

for f in "$SERVICES_DIR"/*.yml; do
    [ -f "$f" ] || continue
    while IFS= read -r line; do
        echo "  $line" >> "$CONFIG"
    done < "$f"
done

echo "  - service: http_status:404" >> "$CONFIG"
sudo systemctl restart cloudflared
echo "Rebuilt with $(ls "$SERVICES_DIR"/*.yml 2>/dev/null | wc -l) service(s)"
sudo install -m 755 /dev/stdin /usr/local/bin/svc-rebuild < svc-rebuild.sh

/usr/local/bin/svc-watch

Auto-rebuilds on file changes (optional but recommended).

#!/usr/bin/env bash
set -euo pipefail

SERVICES_DIR="/etc/cloudflared/services.d"
echo "Watching $SERVICES_DIR for changes..."
while inotifywait -q -e create,delete,modify "$SERVICES_DIR"; do
    sleep 1  # debounce
    /usr/local/bin/svc-rebuild
done
sudo apt install -y inotify-tools
sudo install -m 755 /dev/stdin /usr/local/bin/svc-watch < svc-watch.sh

7. Systemd services (on the host)

/etc/systemd/system/cloudflared.service

[Unit]
Description=Cloudflare Tunnel
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/local/bin/cloudflared tunnel run ampbox
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

/etc/systemd/system/svc-watch.service

[Unit]
Description=Service declaration watcher
After=cloudflared.service

[Service]
ExecStart=/usr/local/bin/svc-watch
Restart=always

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now cloudflared svc-watch

8. Declaring services FROM INSIDE an instance

Expose an HTTP service

cat > /mnt/services/myapp.yml << EOF
- hostname: myapp.ampbox.io
  service: http://$(hostname -I | awk '{print $1}'):8080
EOF

Expose SSH

cat > /mnt/services/ssh-$(hostname).yml << EOF
- hostname: ssh-$(hostname).ampbox.io
  service: ssh://$(hostname -I | awk '{print $1}'):22
EOF

Remove a service

rm /mnt/services/myapp.yml
# svc-watch picks it up and rebuilds automatically

9. Client-side SSH config (on laptops / remote machines)

# ~/.ssh/config
Host *.ampbox.io
    ProxyCommand cloudflared access ssh --hostname %h

Then: ssh user@ssh-myinstance.ampbox.io just works.

10. OPNsense cleanup

Once the tunnel is verified working, remove from OPNsense:

  • All per-service reverse proxy rules in os-caddy
  • Port forward / NAT rules for 80 and 443
  • DDNS configuration (no longer needed)

OPNsense goes back to just being your firewall.

Quick Reference

Action From inside instance
Expose HTTP echo "- hostname: X.ampbox.io\n service: http://IP:PORT" > /mnt/services/X.yml
Expose SSH Same pattern with service: ssh://IP:22
Expose raw TCP Same pattern with service: tcp://IP:PORT
Remove service rm /mnt/services/X.yml
List registered ls /mnt/services/
Check tunnel status sudo systemctl status cloudflared (from host)