Skip to content

Commit bd35fc1

Browse files
authored
Merge pull request #126 from kernel/fix/telemetry-events-vm-routing
fix: don't misroute telemetry/events to the browser VM
2 parents c4246fb + 161369f commit bd35fc1

4 files changed

Lines changed: 83 additions & 9 deletions

File tree

browser_routing.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,10 @@ func browserRouteFromRef(ref browserrouting.Ref) (browserrouting.Route, bool) {
8383
func browserRoutingSubresourcesFromEnv() []string {
8484
raw, ok := os.LookupEnv(browserRoutingSubresourcesEnv)
8585
if !ok {
86-
return []string{"curl", "telemetry"}
86+
// Path prefixes eligible for direct-to-VM routing. "telemetry/stream" is
87+
// the live SSE endpoint (served by the VM); "telemetry/events" is a
88+
// historical read served by the control plane (S2) and must NOT be here.
89+
return []string{"curl", "telemetry/stream"}
8790
}
8891
if strings.TrimSpace(raw) == "" {
8992
return []string{}

browser_routing_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func TestBrowserRoutingWarmsCacheAndRoutesAllowlistedSubresources(t *testing.T)
7272
}
7373

7474
func TestBrowserRoutingRewritesTelemetryStreamToVM(t *testing.T) {
75-
t.Setenv(browserRoutingSubresourcesEnv, "telemetry")
75+
t.Setenv(browserRoutingSubresourcesEnv, "telemetry/stream")
7676

7777
var calls []struct {
7878
Path string
@@ -189,8 +189,8 @@ func TestBrowserRoutingSubresourcesFromEnvDefaultsToCurl(t *testing.T) {
189189
}
190190
_ = os.Setenv(browserRoutingSubresourcesEnv, original)
191191
})
192-
if got := browserRoutingSubresourcesFromEnv(); len(got) != 2 || got[0] != "curl" || got[1] != "telemetry" {
193-
t.Fatalf("expected default subresources [curl telemetry], got %#v", got)
192+
if got := browserRoutingSubresourcesFromEnv(); len(got) != 2 || got[0] != "curl" || got[1] != "telemetry/stream" {
193+
t.Fatalf("expected default subresources [curl telemetry/stream], got %#v", got)
194194
}
195195

196196
t.Setenv(browserRoutingSubresourcesEnv, "")

lib/browserrouting/route_cache.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,15 @@ func (c *RouteCache) Delete(sessionID string) {
7474
// DirectVMRoutingMiddleware rewrites allowlisted browser subresource requests to
7575
// the browser VM using cached base_url and jwt data.
7676
func DirectVMRoutingMiddleware(cache *RouteCache, subresources []string) option.Middleware {
77-
allowed := map[string]struct{}{}
78-
for _, subresource := range subresources {
79-
if trimmed := strings.TrimSpace(subresource); trimmed != "" {
80-
allowed[trimmed] = struct{}{}
77+
// allowPrefixes are path prefixes (relative to browsers/{id}/) eligible for
78+
// direct-to-VM routing, e.g. "curl" or "telemetry/stream". Matching is
79+
// segment-boundary aware (see matchesDirectVMPrefix), so "telemetry/stream"
80+
// covers "telemetry/stream[/...]" but NOT "telemetry/events" (a control-plane
81+
// historical read served from S2) or "telemetry/streamfoo".
82+
allowPrefixes := make([]string, 0, len(subresources))
83+
for _, s := range subresources {
84+
if trimmed := strings.Trim(strings.TrimSpace(s), "/"); trimmed != "" {
85+
allowPrefixes = append(allowPrefixes, trimmed)
8186
}
8287
}
8388

@@ -88,7 +93,7 @@ func DirectVMRoutingMiddleware(cache *RouteCache, subresources []string) option.
8893
}
8994
sessionID, subresource, suffix, ok := parseDirectVMPath(req.URL.Path)
9095
if ok {
91-
if _, ok := allowed[subresource]; ok {
96+
if matchesDirectVMPrefix(subresource+suffix, allowPrefixes) {
9297
route, ok := cache.Load(sessionID)
9398
if ok {
9499
base, err := url.Parse(route.BaseURL)
@@ -313,6 +318,21 @@ func parseDirectVMPath(path string) (sessionID, subresource, suffix string, ok b
313318
return "", "", "", false
314319
}
315320

321+
// matchesDirectVMPrefix reports whether tail (the request path after
322+
// browsers/{id}/) is covered by an allow prefix, matching on segment boundaries:
323+
// "telemetry/stream" matches "telemetry/stream" and "telemetry/stream/...", but
324+
// not "telemetry/events" or "telemetry/streamfoo". This keeps historical
325+
// control-plane reads (e.g. telemetry/events, served from S2) off the VM.
326+
func matchesDirectVMPrefix(tail string, prefixes []string) bool {
327+
tail = strings.Trim(tail, "/")
328+
for _, p := range prefixes {
329+
if tail == p || strings.HasPrefix(tail, p+"/") {
330+
return true
331+
}
332+
}
333+
return false
334+
}
335+
316336
func joinURLPath(basePath, subresource, suffix string) string {
317337
base := "/" + strings.Trim(strings.TrimSpace(basePath), "/")
318338
if base == "/" {

lib/browserrouting/route_cache_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,57 @@ func TestDirectVMRoutingMiddlewareClearsStaleRawPath(t *testing.T) {
5353
}
5454
}
5555

56+
func TestDirectVMRoutingMiddlewareAllowlistMatching(t *testing.T) {
57+
// Pins the segment-boundary allowlist: telemetry/stream (live SSE) routes to
58+
// the VM, telemetry/events (historical, served by the control plane from S2)
59+
// does NOT, and a stream-prefixed-but-different path is not matched.
60+
cases := []struct {
61+
name string
62+
path string
63+
routedToVM bool
64+
}{
65+
{"telemetry stream -> VM", "/browsers/sess-1/telemetry/stream", true},
66+
{"telemetry events -> control plane", "/browsers/sess-1/telemetry/events", false},
67+
{"telemetry streaming-config not a stream prefix", "/browsers/sess-1/telemetry/streaming-config", false},
68+
{"bare telemetry -> control plane", "/browsers/sess-1/telemetry", false},
69+
{"curl proxy -> VM", "/browsers/sess-1/curl/raw", true},
70+
{"non-allowlisted subresource -> control plane", "/browsers/sess-1/fs/read", false},
71+
}
72+
73+
for _, tc := range cases {
74+
t.Run(tc.name, func(t *testing.T) {
75+
cache := NewRouteCache()
76+
cache.Store(Route{
77+
SessionID: "sess-1",
78+
BaseURL: "https://browser.example/browser/kernel",
79+
JWT: "jwt-123",
80+
})
81+
middleware := DirectVMRoutingMiddleware(cache, []string{"curl", "telemetry/stream"})
82+
83+
reqURL, err := url.Parse("https://api.example" + tc.path)
84+
if err != nil {
85+
t.Fatal(err)
86+
}
87+
req := &http.Request{URL: reqURL, Header: http.Header{}}
88+
89+
var got *http.Request
90+
if _, err := middleware(req, func(next *http.Request) (*http.Response, error) {
91+
got = next
92+
return nil, nil
93+
}); err != nil {
94+
t.Fatal(err)
95+
}
96+
if got == nil {
97+
t.Fatal("expected middleware to invoke next handler")
98+
}
99+
routedToVM := got.URL.Host == "browser.example"
100+
if routedToVM != tc.routedToVM {
101+
t.Fatalf("path %q routedToVM=%v, want %v (host=%q path=%q)", tc.path, routedToVM, tc.routedToVM, got.URL.Host, got.URL.Path)
102+
}
103+
})
104+
}
105+
}
106+
56107
func TestParseCacheLifecycleRejectsBrowserSubresourcePaths(t *testing.T) {
57108
cases := []string{
58109
"/browsers/sess-1/process/exec",

0 commit comments

Comments
 (0)