8000 feat: snail007/goproxy front door; relicense to GPL-3.0 by adeelahmad · Pull Request #8 · adeelahmad/Tether · GitHub
[go: up one dir, main page]

Skip to content

feat: snail007/goproxy front door; relicense to GPL-3.0 - #8

Merged
adeelahmad merged 23 commits into
mainfrom
claude/tether-v1-cnb97e
Oct 11, 2026
Merged

adeelahmad merged 23 commits into
mainfrom
claude/tether-v1-cnb97e

Conversation

@adeelahmad
Copy link
Copy Markdown
Owner

Before: connections went straight from a net.Listener into net/http, and Tether was MIT-licensed.

After: every connection passes through a new internal/frontdoor. It uses snail007/goproxy's HTTP request decoder to classify the connection as a CONNECT tunnel, an absolute-form proxy request, or a request for Tether's own endpoints, then hands it to net/http. Auth, the loopback guard and routing are unchanged. Because goproxy is GPLv3, Tether is relicensed to GPL-3.0-or-later. That covers LICENSE, the README, CONTRIBUTING, the GoReleaser image label and the Homebrew cask license.

How
frontdoor contains four goproxy problems found while integrating it:

  • Its decoder reads the request head in a single 4 KB Read. The front door reads the whole head first (up to 1 MiB, with a 30s deadline) and passes it over in one piece, so large cookies and slow clients still work.
  • It logs every request line, query tokens included, to the standard library's global logger. That logger is discarded; Tether only logs through slog.
  • It panics on short malformed heads (string(buf)[:50]). The panic is recovered and the connection r 10BC8 ejected.
  • ServerChannel.ListenTCP has a data race on its error result (fails under -race). Tether runs its own accept loop instead.

Verified

  • Unit tests pass with -race, including new frontdoor tests: head over 4 KB, HTTP/1.0 without Host, malformed input, IPv6 and Close.
  • The real-Chrome integration test passes.
  • golangci-lint reports 0 issues.

🤖 Generated with Claude Code

https://claude.ai/code/session_01X9CEjRcHsJmgWdtz6ZTKsk


Generated by Claude Code

claude added 23 commits October 11, 2026 03:18
…ense to GPL-3.0

New internal/frontdoor accepts every connection and classifies it with
goproxy's HTTP request decoder (CONNECT, absolute-form proxy request, or
a local request) before net/http serves it. Auth, the loopback guard and
routing are unchanged.

goproxy issues contained in frontdoor:
- its decoder reads the head in one fixed-size Read: we read the whole
  head (up to 1 MiB) first and hand it over in one piece;
- it logs request lines, query tokens included, to the global logger:
  the global logger is discarded (Tether logs only through slog);
- it panics on short malformed heads: recovered into an error;
- ServerChannel.ListenTCP races on its error result: we run our own
  accept loop.

goproxy is GPLv3, so Tether is relicensed from MIT to GPL-3.0-or-later.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X9CEjRcHsJmgWdtz6ZTKsk
Extend the single global --allow-domains list into layered policies:

- internal/policy: domain rules (allow/deny, *.sub, *), principals and a
  JSON --policy-file defining named tokens with their own rules, an admin
  flag and a scripts permission. --deny-domains adds a global denylist.
- Auth attaches the token's principal to the request context; --token is
  the admin "default" principal.
- Tab ownership: non-admin tokens only see their own tabs, get their own
  per-site default tabs, and cannot adopt existing tabs.
- core.RulesPolicy checks global ∩ requester ∩ owner ∩ session rules,
  re-checks the final URL, and guards restricted tabs in the browser via
  the DevTools Fetch domain so redirects, clicks and page script obey the
  same rules.
- API: session rules on tab create, GET/PUT /tabs/{id}/policy, /whoami.
  WebDriver: tether:options.policy, ownership on every command, and
  Execute Script gated by the scripts permission.
- Docs: docs/POLICIES.md, SECURITY.md, ARCHITECTURE.md, README, example
  policy file. Integration test drives a real Chrome as a restricted token.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfsJJgC4Jp8VqckdcChH4W
Replace the /mcp stub with a real Model Context Protocol server on the
shared session core:

- Streamable HTTP transport (MCP 2025-06-18, 2025-03-26, 2024-11-05):
  Mcp-Session-Id sessions, protocol-version checks, batches, 202 for
  notifications, DELETE to end a session. Inherits token auth, the
  loopback guard and --allow-domains from the server.
- 16 browser tools: tabs/new/select/close, navigate, history, snapshot
  (interactive elements with stable backend-node refs plus page text),
  click, type, press_key, select_option, scroll, wait_for, eval,
  screenshot and in-tab fetch with the user's cookies. Tool annotations,
  isError results with stable codes, policy checked against the page's
  current URL, password values and Set-Cookie never returned, tool
  arguments never logged.
- Each MCP session tracks a current tab and the tabs it opened; ending
  the session (or idling past --idle-timeout) closes only those.
- `tether mcp`: stdio bridge for Claude Desktop and other stdio clients,
  local or across a tailnet, with ordered forwarding.
- --mcp flag (default on) to turn the face off.

Also fix screenshots of idle background tabs hanging forever (Chrome
does not composite hidden tabs): probe briefly, then bring the tab to
the front and retry, as ChromeDriver does. Affects WebDriver too.

Docs: docs/MCP.md, README, ARCHITECTURE, SECURITY threat model,
CHANGELOG. Tests: unit tests with the cdp fakes and a real headless
Chrome integration test driving every major tool.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HpbHKDrhpTtF3U8jAhD61N
Replace the /remote stub with a real interactive face, embedded in the
binary and built on the shared session core:

- Live video of any Chrome tab via CDP Page.startScreencast, with
  screenshot keyframes for quiet and background tabs (faster for a few
  seconds after input). One screencast per tab is shared by every viewer;
  each viewer keeps only the newest frame.
- Mouse, wheel, touch, keyboard (IME and mobile keyboards), paste, copy
  and JavaScript dialog relay over a small documented websocket protocol.
  Input bypasses the session lock so a person can always intervene.
- Address bar (through Core.Navigate and the domain policy),
  back/forward/reload, bring to front, open/close/adopt tabs, watch-only
  mode, phone layout, light and dark themes. Plain HTML/CSS/JS, no build.
- Token-protected servers get a sign-in page that trades the token for an
  HttpOnly, SameSite=Strict, path-scoped HMAC cookie accepted only under
  /remote/. Strict CSP, no framing, Origin-checked websocket.
- New cdp.Screen capability (screencast, mouse, insertText, dialogs,
  bringToFront), cdptest fakes, --no-remote and --remote-quality flags.
- Unit tests, a real-Chrome integration test, and docs/REMOTE.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzmmijqffKE848kSpCqq72
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HpbHKDrhpTtF3U8jAhD61N
…rint

Close the remaining WebDriver gaps on top of new cdp.Tab primitives:

- Switch To Frame / Parent Frame for same- and cross-site (out-of-process)
  frames at any depth, via cdp.Tab.InFrame and iframe auto-attach.
- Get Element Shadow Root and Find From Shadow Root, including closed roots;
  shadow roots round-trip through scripts.
- Alert commands and unhandledPromptBehavior; calls blocked by a dialog
  fail fast instead of hanging.
- Multiple windows: New Window, popups joined automatically, switching,
  closing; the session ends with its last window.
- Window rect, maximise, minimise and fullscreen, applied only to windows
  that hold nothing but Tether tabs.
- File inputs via Element Send Keys plus Selenium's /se/file upload.
- Print Page.
- Focus emulation on attached tabs so input reaches background tabs with
  cross-site iframes promptly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AhaxD4mv92X8w5zV8j34MD
Apply the same HTTPS detection to the logout cookie as to the login one,
and test both. Plain-HTTP loopback/tailnet use keeps non-Secure cookies,
which browsers would otherwise refuse to store.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzmmijqffKE848kSpCqq72
POST/DELETE /session/{id}/actions dispatch key, mouse, pen, touch and
wheel input as real CDP events, tick by tick, with element/pointer/viewport
origins, animated moves and scrolls, click counting, US-layout Shift, and
input state that persists until Release Actions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnJnokVdV3oFBMpLc45zcJ
…t click

Chrome routes input into frames asynchronously, and on some versions a
real click into a cross-site frame of a background tab never arrives.
Arm a one-shot listener before clicking inside a frame, wait briefly for
it, and click from script if the real click did not land.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AhaxD4mv92X8w5zV8j34MD
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AhaxD4mv92X8w5zV8j34MD
Page.startScreencast fails with "Not attached to an active page" while a
navigation commits, which ended the stream for a viewer that connected to
a freshly opened tab (seen in CI). Retry the start on every keyframe tick
instead of giving up, and re-issue it after each main-frame navigation,
which can swap the page's renderer and silently end the stream.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzmmijqffKE848kSpCqq72
Attaching to a tab that is closing can fail after Target.getTargetInfo
succeeds (Page.enable: session not found). Map that to ErrNoSuchTab so
faces answer 410/no such window instead of 500.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AhaxD4mv92X8w5zV8j34MD
Resolves conflicts with #7 (console logs): Tab gets both Guard and Logs,
the API keeps the policy routes and adds /tabs/{id}/logs, and docs list
both features. Also makes TestConsoleLogs tolerate a late favicon 404
between the two WebDriver log reads (it asserted the second read was
empty; it should assert no entry repeats).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X9CEjRcHsJmgWdtz6ZTKsk
Tab gains Mouse/Touch alongside Guard and Logs; the WebDriver session
keeps the log cursor and the Actions input state; docs list everything.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X9CEjRcHsJmgWdtz6ZTKsk
# Conflicts:
#	README.md
#	internal/cdp/cdptest/cdptest.go
Conflict resolution plus one fix the combination needs: popups and new
windows now belong to the session that opened them. A popup is attached
as the session's own tab (same owner, same session rules, guarded), so a
non-admin token can switch to its popups, and a restricted session can
no longer leave its allowlist through window.open or New Window. A popup
that loaded a forbidden page before Tether attached is closed and the
switch fails.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X9CEjRcHsJmgWdtz6ZTKsk
# Conflicts:
#	CHANGELOG.md
#	docs/WEBDRIVER.md
#	internal/cdp/cdptest/cdptest.go
#	internal/webdriver/errors.go
#	internal/webdriver/webdriver.go
#	test/integration/integration_test.go
Conflict resolution: the fake tab serves MCP's OnCall/Props and the
WebDriver wrappers; app wiring keeps per-token credentials and the
optional MCP face; docs describe MCP alongside per-token policies.
TestNavigateHonoursPolicy now simulates drift with SetURL, since the
browser-side guard (from policies) refuses the navigation it used.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X9CEjRcHsJmgWdtz6ZTKsk
# Conflicts:
#	README.md
#	internal/cdp/cdptest/cdptest.go
#	test/integration/integration_test.go
The UI login cookie is now per token and carries that token's principal,
so the remote UI is held to tab ownership and rules like every face.
Non-admin tokens list only their own tabs, and viewed tabs get the
browser-side navigation guard. Screen drops Mouse and HandleDialog in
favour of the Tab methods.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X9CEjRcHsJmgWdtz6ZTKsk
# Conflicts:
#	CHANGELOG.md
#	CONTRIBUTING.md
#	README.md
#	SECURITY.md
#	cmd/tether/main.go
#	docs/ARCHITECTURE.md
#	internal/app/app.go
#	internal/cdp/cdptest/cdptest.go
#	internal/config/config.go
#	internal/core/core.go
#	internal/server/server.go
#	internal/session/session.go
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X9CEjRcHsJmgWdtz6ZTKsk
# Conflicts:
#	docs/ARCHITECTURE.md
#	go.sum
@adeelahmad
adeelahmad marked this pull request as ready for review October 11, 2026 04:35
@adeelahmad
adeelahmad merged commit 968e212 into main Oct 11, 2026
13 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.

2 participants

0