Skip to content

fix: browser ping timeout — gVisor MTU below IPv6 minimum - #8

Merged
knowlet merged 2 commits into
mainfrom
fix/browser-masque-ping-timeout
Oct 1, 2026
Merged

knowlet merged 2 commits into
mainfrom
fix/browser-masque-ping-timeout

Conversation

@knowlet

@knowlet knowlet commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

Symptom

Browser demo at https://knowlet.github.io/MasqueCat/ failed every dial with ping: context deadline exceeded, while CLI (native) through the same relay worked (pong via relay-masque).

Root cause

The browser (js/wasm) gVisor stack used MTU 1000, below the IPv6 minimum link MTU of 1280 (RFC 8200). gVisor calculateNetworkMTU rejects every outbound IPv6 packet with ErrInvalidEndpointState in that case.

Evidence from an instrumented wasm build:

  • tcpActiveOpen=1 tcpSegsSent=0 tcpSegSendErr=3 ipSent=0 ipOutErr=3 — SYNs created, all dropped at IP write
  • relay datagram counter stayed 0 — nothing ever left the browser, even though WebTransport registration succeeded
  • reproduced natively with identical stack setup: MTU 1000 fails, MTU 1280 emits

Fix

  • browserMasqueMTU 1000 -> 1280 (moved to untagged masquecat_mtu.go so native tests reference the exact value). Small WebTransport datagrams still ensured by browserFragmentChunkSize=1000.
  • Verified E2E in headless Chrome against live relay: tailcatDial OK in ~8s, bidirectional relay forwarding confirmed.

Also included

  • Relay logs drops for unknown destinations (was silent).
  • Browser pingUntil logs each failed attempt.
  • TestBrowserStackEmitsIPv6 native regression test.

Verification

  • go build ./..., js/wasm build of ./web, go test ./... all pass.

Summary by Sourcery

Restore browser connectivity by using an IPv6-compliant MTU while preserving small WebTransport datagram fragmentation.

Bug Fixes:

  • Fix browser connection timeouts by raising the gVisor browser stack MTU to the IPv6 minimum of 1280 bytes.
  • Add relay diagnostics for unknown destinations and log failed browser ping attempts.

Tests:

  • Add a native regression test verifying that the browser-equivalent stack emits IPv6 TCP packets without IP errors.

Summary by cubic

Fixes the browser (js/wasm) demo dial failures where ping always timed out, even though the native CLI worked through the same relay. The browser gVisor stack used an MTU of 1000, below the IPv6 minimum link MTU of 1280 (RFC 8200), causing gVisor to reject every outbound IPv6 packet and never send a datagram.

Bug Fixes

  • Raised browser MTU to 1280; small WebTransport datagrams are still ensured by browserFragmentChunkSize.
  • Added logging for relay drops of unknown destinations and for each failed browser ping attempt.
  • Added a native regression test rebuilding the browser-equivalent stack to verify IPv6 TCP SYNs are emitted.

Written for commit 81e1539. Summary will update on new commits.

Review in cubic

The browser stack used MTU 1000, below the IPv6 minimum link MTU.
gVisor calculateNetworkMTU rejects every IPv6 packet with
ErrInvalidEndpointState in that case, so zero datagrams ever left the
browser while WebTransport registration succeeded, surfacing as
'ping: context deadline exceeded'. Small WT datagrams are still
ensured by browserFragmentChunkSize.

Also log relay drops for unknown destinations (was silent) and each
failed browser ping attempt, and add a native regression test that
rebuilds the browser-equivalent stack and asserts SYNs are emitted.
@sourcery-ai

sourcery-ai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Fixes browser IPv6 packet transmission by increasing the gVisor MTU from 1000 to 1280, while preserving small WebTransport datagrams through fragmentation. The change centralizes the browser stack configuration, adds a native regression test confirming SYNs leave the stack, and improves relay and browser ping diagnostics.

Sequence diagram for browser IPv6 ping through the MASQUE relay

sequenceDiagram
    participant Browser as Browser gVisor stack
    participant WT as WebTransport
    participant Relay as MASQUE relay
    participant Dest as Destination

    Browser->>Browser: calculateNetworkMTU(browserMasqueMTU)
    Browser->>Browser: TCP SYN
    Browser->>WT: Fragment into browserFragmentChunkSize datagrams
    WT->>Relay: WebTransport datagrams
    Relay->>Relay: lookup(pkt.dst)
    Relay->>Dest: ForwardPacket(pkt.src, pkt.dst, pkt.payload)
    Dest-->>Relay: Response packets
    Relay-->>WT: WebTransport datagrams
    WT-->>Browser: Reassembled response
Loading

Flow diagram for the browser MTU fix

flowchart LR
    A[browserMasqueMTU = 1280] --> B[IPv6 packet accepted by gVisor]
    B --> C[browserFragmentChunkSize = 1000]
    C --> D[Small WebTransport datagrams]
    D --> E[Relay forwarding]
    F[browserMasqueMTU = 1000] --> G[calculateNetworkMTU rejects IPv6 packet]
    G --> H[No datagram leaves browser]
    H --> I[Ping context deadline exceeded]
Loading

File-Level Changes

Change Details Files
Raise the browser gVisor link MTU to the IPv6 minimum and centralize browser stack constants for cross-platform regression coverage.
  • Move browser NIC and queue constants into an untagged shared file.
  • Set the browser MTU to 1280 while retaining 1000-byte WebTransport fragmentation.
  • Add a native browser-stack regression test that verifies IPv6 TCP SYN emission without IP errors.
masquecat_browser_js.go
masquecat_mtu.go
masquecat_mtu_test.go
Improve relay diagnostics for packets that cannot be forwarded.
  • Log unknown-destination drops in the HTTP/3 relay path.
  • Log unknown-destination drops and malformed datagram lengths in the browser WebTransport path.
masquecat_h3_server.go
masquecat_webtransport.go
Add visibility into browser ping retry failures.
  • Log each failed ping attempt before retrying or returning the final error.
web/main_js.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-01T10:19:24.919038Z af75006 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!

Sourcery assessment

Approved.


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 6 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="masquecat_mtu_test.go">

<violation number="1" location="masquecat_mtu_test.go:69">
P3: The test hardcodes the ping target (`fd00:7461:696c:6361:7400:7069:6e67:1`, port 65535) instead of referencing `browserMasquePingAddr`/`browserMasquePingPort`, duplicating values that already exist in masquecat_browser_js.go. That contradicts the pattern this PR itself established in masquecat_mtu.go (whose comment says the constants live in the untagged file "so native regression tests can reference the exact value the browser build uses") — the ping address/port are just as bound into the browser stack. If the browser ping target ever changes, this test silently keeps testing a stale destination and can pass while the browser fails. Move `browserMasquePingAddr`/`browserMasquePingPort` to masquecat_mtu.go (or another untagged file) next to `browserMasqueMTU` and reference them here.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread masquecat_mtu_test.go
t.Fatal(err)
}

pingAddr := netip.MustParseAddr("fd00:7461:696c:6361:7400:7069:6e67:1")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The test hardcodes the ping target (fd00:7461:696c:6361:7400:7069:6e67:1, port 65535) instead of referencing browserMasquePingAddr/browserMasquePingPort, duplicating values that already exist in masquecat_browser_js.go. That contradicts the pattern this PR itself established in masquecat_mtu.go (whose comment says the constants live in the untagged file "so native regression tests can reference the exact value the browser build uses") — the ping address/port are just as bound into the browser stack. If the browser ping target ever changes, this test silently keeps testing a stale destination and can pass while the browser fails. Move browserMasquePingAddr/browserMasquePingPort to masquecat_mtu.go (or another untagged file) next to browserMasqueMTU and reference them here.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At masquecat_mtu_test.go, line 69:

<comment>The test hardcodes the ping target (`fd00:7461:696c:6361:7400:7069:6e67:1`, port 65535) instead of referencing `browserMasquePingAddr`/`browserMasquePingPort`, duplicating values that already exist in masquecat_browser_js.go. That contradicts the pattern this PR itself established in masquecat_mtu.go (whose comment says the constants live in the untagged file "so native regression tests can reference the exact value the browser build uses") — the ping address/port are just as bound into the browser stack. If the browser ping target ever changes, this test silently keeps testing a stale destination and can pass while the browser fails. Move `browserMasquePingAddr`/`browserMasquePingPort` to masquecat_mtu.go (or another untagged file) next to `browserMasqueMTU` and reference them here.</comment>

<file context>
@@ -0,0 +1,90 @@
+		t.Fatal(err)
+	}
+
+	pingAddr := netip.MustParseAddr("fd00:7461:696c:6361:7400:7069:6e67:1")
+	ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
+	defer cancel()
</file context>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Requires human review: Auto-approval blocked by 1 unresolved issue from previous reviews.

Re-trigger cubic

@knowlet
knowlet merged commit 0178c54 into main Oct 1, 2026
24 of 33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant