Skip to content

Commit 429a9cb

Browse files
committed
feat: enhance share URL handling to include relay server parameters and update related documentation
1 parent 5d0667c commit 429a9cb

13 files changed

Lines changed: 255 additions & 46 deletions

File tree

ARCHITECTURE.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ The relay server terminates only the browser WebSocket and yamux session. It use
3939
## Core invariants
4040

4141
- Target document navigations use encrypted `/zp/p/<encrypted>#k=<key>&server=...` routes on the proxy origin.
42-
- The `#k` fragment is decrypted in the browser shell, removed with `history.replaceState`, and not sent to the server.
42+
- The `#k` fragment is decrypted in the browser shell, kept visible with canonical `server=` relay parameters, and never sent to the HTTP server.
4343
- Every Service Worker-controlled request is classified. Unknown requests are blocked; there is no native `fetch(event.request)` fallback.
4444
- Privileged runtime-to-Service-Worker control messages require a per-tab capability token injected into the runtime prelude and removed from target-visible DOM before target code runs.
4545
- Target TCP connections are opened through WebSocket -> yamux -> SOCKS5 DOMAINNAME. With a Tor `-socks` address the relay byte-bridges to Tor; with `-socks internal` the relay validates the SOCKS5 CONNECT bytes and direct-dials the target for Tor-free testing. The kernel does not call `http.Transport` for target egress.
@@ -52,7 +52,7 @@ The relay server terminates only the browser WebSocket and yamux session. It use
5252
| Area | Files | Responsibility |
5353
|---|---|---|
5454
| Static shell | `web/index.html`, `web/zp-core.js` | Service Worker registration, target URL canonicalization, share URL encryption/decryption, initial target open. |
55-
| Share URL envelope | `web/zp-core.js`, `internal/shareurl/*` | Compatible JavaScript and Go implementations of `/zp/p/<encrypted>#k=<key>` using AES-256-CBC, HMAC-SHA256, HKDF, raw base64url, and inherited relay-server fragments. |
55+
| Share URL envelope | `web/zp-core.js`, `internal/shareurl/*` | Compatible JavaScript and Go implementations of `/zp/p/<encrypted>#k=<key>&server=...` using AES-256-CBC, HMAC-SHA256, HKDF, raw base64url, and inherited relay-server fragments. |
5656
| Service Worker | `web/sw.js` | Classifies every controlled request under `/zp/`, blocks unknowns, manages in-memory tab/entry state and inherited relay servers, requires per-tab capability tokens on privileged runtime bridge messages, calls the WASM kernel, exposes runtime bridge APIs. |
5757
| Runtime prelude | `web/runtime-prelude.js`, `web/worker-prelude.js` | Installs target-realm containment hooks before target scripts run. Main-window fetch/XHR/EventSource/WebSocket/sendBeacon, navigation/form/history/location/storage/worker/iframe/device APIs are hooked; main-window and worker `fetch` bridge through `/zp/api/fetch`. Runtime membrane helpers (`__zp_get`, `__zp_set`, `__zp_assign`, `__zp_update`, `__zp_call`, `__zp_construct`, `__zp_getOwnPropertyDescriptor`, `__zp_ownKeys`) and dynamic compilation wrappers execute `Function`/`eval`/string timer bodies under the virtual global scope. |
5858
| WASM kernel | `cmd/wasm-kernel/main.go`, `internal/swhttp/*` | Converts JS `Request`/`Response`, initializes transport, owns target HTTP and WebSocket execution. |
@@ -62,16 +62,16 @@ The relay server terminates only the browser WebSocket and yamux session. It use
6262

6363
## Request flow
6464

65-
1. The shell registers `/zp/sw.js` with `scope: '/zp/'`, waits for a controller, canonicalizes an `http:` or `https:` target, encrypts it, and navigates to `/zp/p/<encrypted>#k=<key>` on the proxy origin.
66-
2. The shell loaded on `/zp/p/<encrypted>#k=<key>` decrypts the fragment key in window context, validates the HMAC before decryption, normalizes repeated `server=` relay fragments, removes the fragment from the visible URL when policy allows, and sends `ZP_OPEN_SHARE` to the Service Worker.
65+
1. The shell registers `/zp/sw.js` with `scope: '/zp/'`, waits for a controller, canonicalizes an `http:` or `https:` target, encrypts it, and navigates to `/zp/p/<encrypted>#k=<key>&server=...` on the proxy origin.
66+
2. The shell loaded on `/zp/p/<encrypted>#k=<key>&server=...` decrypts the fragment key in window context, validates the HMAC before decryption, normalizes repeated or missing `server=` relay fragments, keeps the canonical fragment visible, and sends `ZP_OPEN_SHARE` to the Service Worker.
6767
3. The Service Worker stores the decrypted target plus relay-server list in in-memory tab/entry maps and activates `/zp/p/<encrypted>` as a proxy document route.
6868
4. A `/zp/p/<encrypted>` document request is resolved back to the target URL. The Service Worker calls `__go_jshttp` with `X-ZP-*` internal metadata plus inherited relay-server headers.
6969
5. The WASM kernel ensures one long-lived WebSocket connection to the selected relay server or `/zp/ws-pipe`, wraps it in a yamux client, and opens one yamux stream per target TCP connection.
7070
6. Each target connection performs SOCKS5 `CONNECT` with DOMAINNAME ATYP and a Tor `IsolateSOCKSAuth` username derived from the tab stream-isolation key and target site. In `-socks internal` mode the relay accepts that same binary SOCKS5 handshake locally and direct-dials the requested host:port; no external Tor process is used.
7171
7. HTTPS fetch targets advertise `h2` and `http/1.1` through uTLS ALPN; target WebSocket connections advertise only `http/1.1`.
7272
8. `internal/zphttp` dispatches negotiated `h2` connections through `golang.org/x/net/http2.ClientConn`; HTTP/1.1 fallback writes a direct request and reads the response with `http.ReadResponse`.
7373
9. Redirects are followed inside the kernel so raw `Location` headers are not exposed to browser code.
74-
10. HTML document responses are transformed: Rust rewrite asset plus runtime prelude are injected, document navigation URLs are rewritten to encrypted `/zp/p/<encrypted>#k=<key>` routes, risky tags and headers are removed, and the browser receives a same-origin `Response` with ZeroProxy CSP.
74+
10. HTML document responses are transformed: Rust rewrite asset plus runtime prelude are injected, document navigation URLs are rewritten to encrypted `/zp/p/<encrypted>#k=<key>&server=...` routes, risky tags and headers are removed, and the browser receives a same-origin `Response` with ZeroProxy CSP.
7575

7676
## Shared URL flow
7777

@@ -83,7 +83,7 @@ Shared links use this envelope:
8383

8484
`web/zp-core.js` and `internal/shareurl` derive separate HKDF-SHA256 AES-CBC and HMAC keys from the 64-byte seed. The MAC covers a fixed version prefix, IV, and ciphertext. Decryption verifies HMAC first, then decrypts and canonicalizes the target URL. Only `http:` and `https:` targets are accepted for document/fetch traffic; WebSocket wrappers accept only `ws:` and `wss:`.
8585

86-
The Go HTML transformer uses `internal/shareurl.New` when laundering document-navigation attributes, so transformed links/forms/frames keep using encrypted `/zp/p` routes instead of legacy virtual URL paths.
86+
The Go HTML transformer uses `internal/shareurl.NewWithServers` when laundering document-navigation attributes, so transformed links/forms/frames keep using encrypted `/zp/p` routes with inherited relay-server fragments instead of legacy virtual URL paths.
8787

8888
## Service Worker classification
8989

PHASE3_PLAN.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,11 @@ Relay server selection and inheritance:
8383
- no fragment;
8484
- normalized host, port, path, and query;
8585
- bounded count and total serialized length.
86-
- The fragment is parsed by the shell before target code runs, then removed with `history.replaceState`. The fragment is never sent to the origin server or target server.
86+
- The fragment is parsed by the shell before target code runs, then kept visible in canonical `#k=...&server=...` form. The fragment is never sent to the origin server or target server.
8787
- The shell sends `{ targetUrl, routeKey, servers }` to the Service Worker in `ZP_OPEN_SHARE`. The Service Worker stores `servers` in tab/entry context and passes them to the WASM kernel transport initialization and WebSocket stream path.
8888
- Runtime-generated document navigations, forms, iframe navigations, worker bootstrap, module/script API calls, runtime fetch APIs, WebSocket, and `WebSocketStream` inherit `servers` from the active context. Target-authored URLs cannot override the relay server list.
8989
- New share URLs created by ZeroProxy UI or runtime helpers include the current inherited `server` parameters unless the user explicitly chooses a different relay set through ZeroProxy-controlled UI.
90-
- Cold restore without Service Worker memory must fail safely unless the URL fragment supplies both a valid `k` and a valid server list, or the deployment has an explicit default relay policy.
90+
- Cold restore without Service Worker memory must activate from the URL fragment when it supplies `k` and relay `server` values; missing server values are canonicalized to the deployment's current `/zp/ws-pipe` relay.
9191

9292
### 1. Static document-navigation attributes are not rewritten
9393

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ Status: **Prototype / partial implementation**.
1414

1515
Implemented core spine:
1616

17-
- Encrypted active/share route format: `/p/<encrypted>#k=<key>`.
17+
- Encrypted active/share route format: `/zp/p/<encrypted>#k=<key>&server=...`.
1818
- AES-256-CBC + HMAC-SHA256 URL envelope with HKDF-separated encryption/MAC keys and HMAC verification before decryption.
1919
- Service Worker request classifier that handles every controlled request, blocks unknown requests instead of falling back to native `fetch(event.request)`, and requires a per-tab runtime capability token on privileged runtime bridge messages.
2020
- Go WASM exports: `__go_jshttp`, `__zp_stream`, `__zp_kernel_init`, and `__zp_cookie_set`.
2121
- A single browser WebSocket pipe carrying yamux streams to the relay server, then SOCKS5 DOMAINNAME CONNECT, uTLS for HTTPS, HTTP/2 when ALPN selects `h2`, and HTTP/1.1 fallback/direct handling. `-socks 127.0.0.1:9050` preserves the Tor bridge; `-socks internal` is a Tor-free development/test mode that parses SOCKS5 on the relay and dials targets directly from the relay process.
22-
- Tokenizer-based HTML transform that injects the runtime prelude, launders executable external scripts through `/__zp/api/script?u=...`, rewrites iframe/frame document URLs to encrypted `/p` routes, preserves author-visible anchor/form attributes for runtime navigation interception, removes or neutralizes preload/preconnect/manifest hints, drops dangerous tags and headers, routes executable event attributes through the Rust WASM rewriter, and handles `srcdoc`.
23-
- Runtime containment for main-window `fetch`, XHR, EventSource, WebSocket, `sendBeacon`, navigation, forms, history/location masking, storage facades, workers, iframes, and high-risk device/network APIs. Main-window and worker `fetch` paths are bridged through `/__zp/api/fetch` so strict `connect-src 'self'` does not block target API calls before the Service Worker can route them. Runtime-to-Service-Worker control messages carry a closure-held per-tab capability token. The runtime also applies basic self-fingerprint masking for patched function source strings, Canvas/Audio extraction jitter, and speech voice lists; broad anti-bot spoofing is not a project goal.
22+
- Tokenizer-based HTML transform that injects the runtime prelude, launders executable external scripts through `/zp/api/script?u=...`, rewrites iframe/frame document URLs to encrypted `/zp/p` routes with inherited `server=` relay fragments, preserves author-visible anchor/form attributes for runtime navigation interception, removes or neutralizes preload/preconnect/manifest hints, drops dangerous tags and headers, routes executable event attributes through the Rust WASM rewriter, and handles `srcdoc`.
23+
- Runtime containment for main-window `fetch`, XHR, EventSource, WebSocket, `sendBeacon`, navigation, forms, history/location masking, storage facades, workers, iframes, and high-risk device/network APIs. Main-window and worker `fetch` paths are bridged through `/zp/api/fetch` so strict `connect-src 'self'` does not block target API calls before the Service Worker can route them. Runtime-to-Service-Worker control messages carry a closure-held per-tab capability token. The runtime also applies basic self-fingerprint masking for patched function source strings, Canvas/Audio extraction jitter, and speech voice lists; broad anti-bot spoofing is not a project goal.
2424
- Rust WASM JavaScript rewriting is the only script rewrite engine: target-response CSP no longer permits `connect-src *`, external, module, worker, imported, inline, event-handler, and synchronous dynamic-function bodies are parsed before execution, dangerous global/window/location access is rewritten to runtime membrane helpers, parse/transform failures fail closed, constructor-constructor escapes are routed through runtime helpers instead of blocked, and blob/data worker scripts remain blocked when they cannot be rewritten synchronously.
25-
- Relay server static asset service and `/__zp/ws-pipe` WebSocket endpoint.
25+
- Relay server static asset service and `/zp/ws-pipe` WebSocket endpoint.
2626
- Go and JavaScript share URL implementations that use the same envelope format.
2727

2828
Not complete enough for production or high-assurance acceptance:
@@ -110,7 +110,7 @@ Open the browser shell on the proxy origin:
110110
http://proxy.localhost:8080/
111111
```
112112

113-
Use `proxy.localhost` from the start so the shell, Service Worker, and encrypted `/p/<encrypted>#k=<key>` routes share one origin. The server starts even if Tor is not reachable. Target browsing needs either a configured Tor SOCKS5 listener or the explicit non-anonymous `-socks internal` test mode.
113+
Use `proxy.localhost` from the start so the shell, Service Worker, and encrypted `/zp/p/<encrypted>#k=<key>&server=...` routes share one origin. The server starts even if Tor is not reachable. Target browsing needs either a configured Tor SOCKS5 listener or the explicit non-anonymous `-socks internal` test mode.
114114

115115
## Verification commands
116116

internal/htmltx/transform.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ func wrapAttrURL(raw string, opt Options, nav bool) (wrapped, target string, ok
378378
if abs.Scheme != "http" && abs.Scheme != "https" {
379379
return "#", "", false
380380
}
381-
sharePath, err := shareurl.New(abs.String())
381+
sharePath, err := shareurl.NewWithServers(abs.String(), opt.Servers)
382382
if err != nil {
383383
return "#", "", false
384384
}

internal/htmltx/transform_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ import (
99

1010
func TestTransformInjectsAndLaundersDocumentNavigation(t *testing.T) {
1111
target, _ := url.Parse("https://example.com/dir/page.html")
12-
out, err := Transform(strings.NewReader(`<!doctype html><html><head><base href="https://evil.test/"><script src="/early.js"></script><link rel="preconnect" href="https://evil.test"><meta http-equiv="refresh" content="0;url=https://evil.test/"></head><body><a href="/next" ping="https://ping.test">n</a><form action="submit"><button formaction="/alt">go</button></form><iframe src="/child" srcdoc="<p>x</p>"></iframe><object data="x"></object></body></html>`), Options{TabID: "tab", EntryID: "entry", TargetURL: target})
12+
out, err := Transform(strings.NewReader(`<!doctype html><html><head><base href="https://evil.test/"><script src="/early.js"></script><link rel="preconnect" href="https://evil.test"><meta http-equiv="refresh" content="0;url=https://evil.test/"></head><body><a href="/next" ping="https://ping.test">n</a><form action="submit"><button formaction="/alt">go</button></form><iframe src="/child" srcdoc="<p>x</p>"></iframe><object data="x"></object></body></html>`), Options{TabID: "tab", EntryID: "entry", TargetURL: target, Servers: []string{"wss://relay.example/ws"}})
1313
if err != nil {
1414
t.Fatal(err)
1515
}
1616
s := string(out)
17-
for _, want := range []string{"/zp/assets/zp-core.js", "/zp/assets/runtime-prelude.js", "/zp/p/", "#k=", "__ZP_SET_BASE", "https://evil.test/", `data-zp-target-url="https://example.com/next"`, `data-zp-target-url="https://example.com/dir/submit"`, `data-zp-target-url="https://example.com/alt"`, `data-zp-target-url="https://example.com/child"`, "ZeroProxy blocked object"} {
17+
for _, want := range []string{"/zp/assets/zp-core.js", "/zp/assets/runtime-prelude.js", "/zp/p/", "#k=", "server=wss%3A%2F%2Frelay.example%2Fws", "__ZP_SET_BASE", "https://evil.test/", `data-zp-target-url="https://example.com/next"`, `data-zp-target-url="https://example.com/dir/submit"`, `data-zp-target-url="https://example.com/alt"`, `data-zp-target-url="https://example.com/child"`, "ZeroProxy blocked object"} {
1818
if !strings.Contains(s, want) {
1919
t.Fatalf("missing %q in %s", want, s)
2020
}

0 commit comments

Comments
 (0)