Skip to content

Commit 8de6c8b

Browse files
authored
Merge pull request #122 from kernel/fix/telemetry-events-vm-routing
fix: don't misroute telemetry/events to the browser VM
2 parents 4506e68 + e31a985 commit 8de6c8b

2 files changed

Lines changed: 67 additions & 4 deletions

File tree

src/kernel/lib/browser_routing/routing.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ class BrowserRoutingConfig:
4141
def browser_routing_config_from_env() -> BrowserRoutingConfig:
4242
raw = os.environ.get("KERNEL_BROWSER_ROUTING_SUBRESOURCES")
4343
if raw is None:
44-
return BrowserRoutingConfig(subresources=("curl", "telemetry"))
44+
# Path prefixes eligible for direct-to-VM routing. "telemetry/stream" is
45+
# the live SSE endpoint (VM); "telemetry/events" is a historical read
46+
# served by the control plane (S2) and must NOT be here.
47+
return BrowserRoutingConfig(subresources=("curl", "telemetry/stream"))
4548
if raw.strip() == "":
4649
return BrowserRoutingConfig()
4750

@@ -188,6 +191,21 @@ def _session_id_from_browser_pool_release_request(request: httpx.Request, path:
188191
return normalized or None
189192

190193

194+
def _matches_direct_vm_prefix(tail: str, prefixes: tuple[str, ...]) -> bool:
195+
"""Whether tail (the path after browsers/{id}/) is covered by an allow prefix,
196+
matching on segment boundaries: "telemetry/stream" matches "telemetry/stream"
197+
and "telemetry/stream/...", but not "telemetry/events" or "telemetry/streamfoo".
198+
Keeps historical control-plane reads (e.g. telemetry/events, served from S2)
199+
off the VM.
200+
"""
201+
tail = tail.strip("/")
202+
for prefix in prefixes:
203+
prefix = prefix.strip("/")
204+
if prefix and (tail == prefix or tail.startswith(prefix + "/")):
205+
return True
206+
return False
207+
208+
191209
def rewrite_direct_vm_options(
192210
options: FinalRequestOptions,
193211
*,
@@ -199,7 +217,7 @@ def rewrite_direct_vm_options(
199217
return options
200218

201219
session_id, subresource, suffix = match
202-
if subresource not in set(config.subresources):
220+
if not _matches_direct_vm_prefix(f"{subresource}{suffix}", config.subresources):
203221
return options
204222

205223
route = cache.get(session_id)

tests/test_browser_routing.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def test_browser_request_uses_curl_raw() -> None:
9999

100100
@respx.mock
101101
def test_telemetry_stream_routes_directly_to_vm(monkeypatch: pytest.MonkeyPatch) -> None:
102-
monkeypatch.setenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", "telemetry")
102+
monkeypatch.setenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", "telemetry/stream")
103103
route = respx.get("http://browser-session.test/browser/kernel/telemetry/stream").mock(
104104
return_value=httpx.Response(
105105
200,
@@ -337,7 +337,52 @@ def test_browser_route_from_browser_requires_base_url_and_jwt() -> None:
337337

338338
def test_browser_routing_config_from_env_defaults_to_curl(monkeypatch: pytest.MonkeyPatch) -> None:
339339
monkeypatch.delenv("KERNEL_BROWSER_ROUTING_SUBRESOURCES", raising=False)
340-
assert browser_routing_config_from_env().subresources == ("curl", "telemetry")
340+
assert browser_routing_config_from_env().subresources == ("curl", "telemetry/stream")
341+
342+
343+
def test_direct_vm_routing_allowlist_segment_boundary() -> None:
344+
# Pins the fix: telemetry/stream (live SSE) routes to the VM; telemetry/events
345+
# (historical, served by the control plane from S2) does NOT; and a
346+
# stream-prefixed-but-different path is not matched.
347+
from kernel.lib.browser_routing.routing import _matches_direct_vm_prefix
348+
349+
prefixes = ("curl", "telemetry/stream")
350+
assert _matches_direct_vm_prefix("telemetry/stream", prefixes) is True
351+
assert _matches_direct_vm_prefix("telemetry/stream/x", prefixes) is True
352+
assert _matches_direct_vm_prefix("telemetry/events", prefixes) is False
353+
assert _matches_direct_vm_prefix("telemetry/streaming-config", prefixes) is False
354+
assert _matches_direct_vm_prefix("telemetry", prefixes) is False
355+
assert _matches_direct_vm_prefix("curl/raw", prefixes) is True
356+
assert _matches_direct_vm_prefix("fs/read", prefixes) is False
357+
358+
359+
def test_rewrite_direct_vm_options_keeps_telemetry_events_on_control_plane() -> None:
360+
# Integration through the real routing hook: telemetry/events (historical,
361+
# control-plane/S2) must NOT be rewritten to the VM, while telemetry/stream
362+
# (live SSE) must be.
363+
from kernel._models import FinalRequestOptions
364+
from kernel.lib.browser_routing.routing import (
365+
BrowserRoute,
366+
BrowserRouteCache,
367+
BrowserRoutingConfig,
368+
rewrite_direct_vm_options,
369+
)
370+
371+
cache = BrowserRouteCache()
372+
cache.set(
373+
BrowserRoute(session_id="sess-1", base_url="http://browser-session.test/browser/kernel", jwt="token-abc")
374+
)
375+
config = BrowserRoutingConfig(subresources=("curl", "telemetry/stream"))
376+
377+
events = rewrite_direct_vm_options(
378+
FinalRequestOptions(method="get", url="/browsers/sess-1/telemetry/events"), cache=cache, config=config
379+
)
380+
assert events.url == "/browsers/sess-1/telemetry/events" # unchanged -> control plane
381+
382+
stream = rewrite_direct_vm_options(
383+
FinalRequestOptions(method="get", url="/browsers/sess-1/telemetry/stream"), cache=cache, config=config
384+
)
385+
assert str(stream.url).startswith("http://browser-session.test/browser/kernel/telemetry/stream")
341386

342387

343388
def test_browser_routing_config_from_env_empty_string_disables_routing(monkeypatch: pytest.MonkeyPatch) -> None:

0 commit comments

Comments
 (0)