diff --git a/README.md b/README.md index 625dcbb..f5a355d 100644 --- a/README.md +++ b/README.md @@ -66,8 +66,22 @@ Web UI # Clone and configure git clone https://github.com/staack/the-other-dude.git && cd the-other-dude cp .env.example .env -# Edit .env -- set CREDENTIAL_ENCRYPTION_KEY and JWT_SECRET_KEY at minimum +``` +**Edit `.env` before starting** -- at minimum, generate unique values for: + +```bash +# Generate a JWT signing key +JWT_SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_urlsafe(64))") + +# Generate a Fernet encryption key (used to encrypt device credentials at rest) +CREDENTIAL_ENCRYPTION_KEY=$(python3 -c "import secrets, base64; print(base64.b64encode(secrets.token_bytes(32)).decode())") +``` + +> **Warning** +> The `.env.example` ships with **hard-coded dev defaults** for both keys. These are fine for local development but **must be replaced before exposing the instance to any network**. Anyone with the default `JWT_SECRET_KEY` can forge authentication tokens, and the default `CREDENTIAL_ENCRYPTION_KEY` leaves all stored device credentials readable. + +```bash # Build images sequentially (avoids OOM on low-RAM machines) docker compose --profile full build api docker compose --profile full build poller diff --git a/backend/app/routers/remote_access.py b/backend/app/routers/remote_access.py index b89e195..180ee1f 100644 --- a/backend/app/routers/remote_access.py +++ b/backend/app/routers/remote_access.py @@ -159,11 +159,17 @@ async def open_winbox_session( if not isinstance(port, int) or not (49000 <= port <= 49100): raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Invalid port allocation from tunnel service") + # Derive the tunnel host from the request so remote clients get the server's + # address rather than 127.0.0.1 (which would point to the user's own machine). + tunnel_host = (request.headers.get("x-forwarded-host") or request.headers.get("host") or "127.0.0.1") + # Strip port from host header (e.g. "10.101.0.175:8001" → "10.101.0.175") + tunnel_host = tunnel_host.split(":")[0] + return WinboxSessionResponse( tunnel_id=tunnel_id, - host="127.0.0.1", + host=tunnel_host, port=port, - winbox_uri=f"winbox://127.0.0.1:{port}", + winbox_uri=f"winbox://{tunnel_host}:{port}", ) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 290eb2b..0e7c421 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -82,7 +82,7 @@ services: POLL_INTERVAL_SECONDS: 60 WIREGUARD_GATEWAY: wireguard TUNNEL_PORT_MIN: 49000 - TUNNEL_PORT_MAX: 49100 + TUNNEL_PORT_MAX: 49004 TUNNEL_IDLE_TIMEOUT: 300 SSH_RELAY_PORT: 8080 SSH_IDLE_TIMEOUT: 900 @@ -90,7 +90,7 @@ services: SSH_MAX_PER_USER: 10 SSH_MAX_PER_DEVICE: 20 ports: - - "127.0.0.1:49000-49100:49000-49100" + - "49000-49004:49000-49004" ulimits: nofile: soft: 8192 diff --git a/poller/internal/tunnel/manager.go b/poller/internal/tunnel/manager.go index ec28e8b..5f88467 100644 --- a/poller/internal/tunnel/manager.go +++ b/poller/internal/tunnel/manager.go @@ -65,7 +65,7 @@ func (m *Manager) OpenTunnel(deviceID, tenantID, userID, remoteAddr string) (*Op return nil, err } - ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port)) + ln, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", port)) if err != nil { m.portPool.Release(port) return nil, fmt.Errorf("failed to listen on port %d: %w", port, err) diff --git a/poller/internal/tunnel/portpool.go b/poller/internal/tunnel/portpool.go index a4fc36b..b7feeb4 100644 --- a/poller/internal/tunnel/portpool.go +++ b/poller/internal/tunnel/portpool.go @@ -54,7 +54,7 @@ func (pp *PortPool) Release(port int) { } func canBind(port int) bool { - ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port)) + ln, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", port)) if err != nil { return false } diff --git a/poller/internal/tunnel/portpool_test.go b/poller/internal/tunnel/portpool_test.go index eeee6b8..08a7b3d 100644 --- a/poller/internal/tunnel/portpool_test.go +++ b/poller/internal/tunnel/portpool_test.go @@ -71,7 +71,7 @@ func TestPortPool_ConcurrentAccess(t *testing.T) { func TestPortPool_BindVerification(t *testing.T) { // Occupy a port, then verify Allocate skips it - ln, err := net.Listen("tcp", "127.0.0.1:49050") + ln, err := net.Listen("tcp", "0.0.0.0:49050") require.NoError(t, err) defer ln.Close()