diff --git a/muxplex/main.py b/muxplex/main.py index 7c2b51e..8b5cecf 100644 --- a/muxplex/main.py +++ b/muxplex/main.py @@ -17,6 +17,7 @@ import os import pathlib import pwd import socket +import ssl import subprocess import sys import time @@ -754,13 +755,27 @@ async def federation_terminal_ws_proxy(websocket: WebSocket, remote_id: int) -> else: ws_url = remote_url + "/terminal/ws" # assume already ws:// or wss:// + # Build an SSL context that skips verification for self-signed certs on + # remote instances. Same rationale as httpx verify=False: federation + # peers may use self-signed or Tailscale-issued certs that don't pass the + # system CA store. None tells websockets to use default behaviour (no + # TLS) for plain ws:// URLs. + ssl_context: ssl.SSLContext | None = None + if ws_url.startswith("wss://"): + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + await websocket.accept(subprotocol="tty") try: async with websockets.connect( ws_url, subprotocols=[Subprotocol("tty")], - additional_headers={"Authorization": f"Bearer {remote_key}"} if remote_key else {}, + additional_headers={"Authorization": f"Bearer {remote_key}"} + if remote_key + else {}, + ssl=ssl_context, ) as remote_ws: async def client_to_remote() -> None: diff --git a/muxplex/tests/test_ws_proxy.py b/muxplex/tests/test_ws_proxy.py index 4fb814b..5749a1e 100644 --- a/muxplex/tests/test_ws_proxy.py +++ b/muxplex/tests/test_ws_proxy.py @@ -442,3 +442,20 @@ def test_federation_ws_proxy_route_exists(): assert "/federation/{remote_id}/terminal/ws" in paths, ( "App must have a WebSocket route at /federation/{remote_id}/terminal/ws" ) + + +def test_federation_ws_proxy_uses_ssl_context_for_wss(): + """Federation WS proxy must pass an SSL context when connecting via wss://. + + Self-signed certs on remote instances (cortex, spark-2, etc.) fail the + default SSL verification in websockets.connect(). The proxy must build an + SSLContext with CERT_NONE for wss:// URLs — the same fix already applied + to the httpx client (verify=False) but for the websockets library. + """ + from muxplex.main import federation_terminal_ws_proxy + + source = inspect.getsource(federation_terminal_ws_proxy) + assert "ssl" in source and ("CERT_NONE" in source or "ssl_context" in source), ( + "Federation WS proxy must configure an SSL context (CERT_NONE / ssl_context) " + "for self-signed cert support on wss:// connections" + )