fix: WinBox tunnel bind address, port range, and proxy support
- Bind tunnel listeners to 0.0.0.0 instead of 127.0.0.1 so tunnels are reachable through reverse proxies and container networks - Reduce port range to 49000-49004 (5 concurrent tunnels) - Derive WinBox URI host from request Host header instead of hardcoding 127.0.0.1, enabling use behind reverse proxies - Add README security warning about default encryption keys Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
16
README.md
16
README.md
@@ -66,8 +66,22 @@ Web UI
|
|||||||
# Clone and configure
|
# Clone and configure
|
||||||
git clone https://github.com/staack/the-other-dude.git && cd the-other-dude
|
git clone https://github.com/staack/the-other-dude.git && cd the-other-dude
|
||||||
cp .env.example .env
|
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)
|
# Build images sequentially (avoids OOM on low-RAM machines)
|
||||||
docker compose --profile full build api
|
docker compose --profile full build api
|
||||||
docker compose --profile full build poller
|
docker compose --profile full build poller
|
||||||
|
|||||||
@@ -159,11 +159,17 @@ async def open_winbox_session(
|
|||||||
if not isinstance(port, int) or not (49000 <= port <= 49100):
|
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")
|
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(
|
return WinboxSessionResponse(
|
||||||
tunnel_id=tunnel_id,
|
tunnel_id=tunnel_id,
|
||||||
host="127.0.0.1",
|
host=tunnel_host,
|
||||||
port=port,
|
port=port,
|
||||||
winbox_uri=f"winbox://127.0.0.1:{port}",
|
winbox_uri=f"winbox://{tunnel_host}:{port}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ services:
|
|||||||
POLL_INTERVAL_SECONDS: 60
|
POLL_INTERVAL_SECONDS: 60
|
||||||
WIREGUARD_GATEWAY: wireguard
|
WIREGUARD_GATEWAY: wireguard
|
||||||
TUNNEL_PORT_MIN: 49000
|
TUNNEL_PORT_MIN: 49000
|
||||||
TUNNEL_PORT_MAX: 49100
|
TUNNEL_PORT_MAX: 49004
|
||||||
TUNNEL_IDLE_TIMEOUT: 300
|
TUNNEL_IDLE_TIMEOUT: 300
|
||||||
SSH_RELAY_PORT: 8080
|
SSH_RELAY_PORT: 8080
|
||||||
SSH_IDLE_TIMEOUT: 900
|
SSH_IDLE_TIMEOUT: 900
|
||||||
@@ -90,7 +90,7 @@ services:
|
|||||||
SSH_MAX_PER_USER: 10
|
SSH_MAX_PER_USER: 10
|
||||||
SSH_MAX_PER_DEVICE: 20
|
SSH_MAX_PER_DEVICE: 20
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:49000-49100:49000-49100"
|
- "49000-49004:49000-49004"
|
||||||
ulimits:
|
ulimits:
|
||||||
nofile:
|
nofile:
|
||||||
soft: 8192
|
soft: 8192
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ func (m *Manager) OpenTunnel(deviceID, tenantID, userID, remoteAddr string) (*Op
|
|||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
m.portPool.Release(port)
|
m.portPool.Release(port)
|
||||||
return nil, fmt.Errorf("failed to listen on port %d: %w", port, err)
|
return nil, fmt.Errorf("failed to listen on port %d: %w", port, err)
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func (pp *PortPool) Release(port int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func canBind(port int) bool {
|
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 {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ func TestPortPool_ConcurrentAccess(t *testing.T) {
|
|||||||
|
|
||||||
func TestPortPool_BindVerification(t *testing.T) {
|
func TestPortPool_BindVerification(t *testing.T) {
|
||||||
// Occupy a port, then verify Allocate skips it
|
// 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)
|
require.NoError(t, err)
|
||||||
defer ln.Close()
|
defer ln.Close()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user