1
0
mirror of https://github.com/sredevopsorg/deepseek-kubernetes-sandbox.git synced 2026-11-03 01:13:24 +00:00

Dial the sandbox pod IP for WebSocket upgrades (#10)

DSH's Host fence (isTrustedApiRequest) only trusts loopback, an allow-listed
host, or its own bind address, and requires any Origin to match the Host
authority. The gateway forwarded the browser Host as an additional header,
but websockets derives Host from the URI and cannot be overridden, so the
request carried a duplicate/mismatched Host plus the browser Origin and DSH
answered 403. The gateway then closed with 1011, which the DSH client retries
forever:

  [connection] connection lost, retry #1 ... #55

Dial the pod IP instead - it is the bind address, so the derived Host passes
the fence - and forward no Origin; CSWSH is already rejected by same_origin()
before the upstream connection. Fall back to the service FQDN when
status.podIPs is empty and bracket IPv6 literals.

Reproduced against dsh-v0.2.1-rc.2: pod-IP upgrade with no Origin opens;
the same upgrade carrying the browser Origin gets HTTP 403.

Co-authored-by: SREDevOpsAgents <agents@sredevops.org>
This commit is contained in:
SREDevOps.org Agent
2026-10-21 15:09:51 -03:00
committed by GitHub
parent 163abaeac0
commit abe5b4d5bb
3 changed files with 34 additions and 5 deletions

View File

@@ -669,6 +669,15 @@ class Gateway:
or sandbox["metadata"]["name"]
)
@staticmethod
def _pod_ip(sandbox: dict) -> str | None:
"""The pod IP is the authority DSH accepts in its Host fence."""
for entry in (sandbox.get("status") or {}).get("podIPs") or []:
ip = entry.get("ip") if isinstance(entry, dict) else entry
if ip:
return ip
return None
def _clear_handshake(self, user: str) -> None:
for key in [k for k in self.state.handshake if k[0] == user]:
self.state.handshake.pop(key, None)
@@ -1141,7 +1150,15 @@ against the JSON API. The browser session is an HttpOnly cookie.</p>
host = websocket.headers.get("host", "")
cookie, _, _ = await self.dsh_cookie(user, sandbox, fqdn, host)
path = websocket.path_params.get("path", "")
url = f"ws://{fqdn}:{self.settings.dsh_port}/{path}"
# DSH's Host fence accepts its own bind address and requires
# any Origin to have the same authority. websockets derives the
# Host header from the URI and cannot be overridden, so dial
# the pod IP instead of the service FQDN and forward no Origin;
# CSWSH is already rejected by same_origin() above.
upstream_host = self._pod_ip(sandbox) or fqdn
if ":" in upstream_host and not upstream_host.startswith("["):
upstream_host = f"[{upstream_host}]" # IPv6 literal
url = f"ws://{upstream_host}:{self.settings.dsh_port}/{path}"
query = filtered_query(websocket.url.query)
if query:
url = f"{url}?{query}"
@@ -1149,9 +1166,9 @@ against the JSON API. The browser session is an HttpOnly cookie.</p>
key: value
for key, value in websocket.headers.items()
if key.lower() not in OWNED_REQUEST_HEADERS
and key.lower() != "origin"
and not key.lower().startswith("sec-websocket-")
}
headers["host"] = host
headers["cookie"] = cookie
headers["x-forwarded-host"] = host
headers["x-forwarded-proto"] = websocket.url.scheme