Skip to content

Commit c8f856a

Browse files
committed
feat: introduce buffered random reader to optimize entropy access and implement asynchronous share URL generation for navigation attributes
1 parent 04f2e24 commit c8f856a

7 files changed

Lines changed: 162 additions & 16 deletions

File tree

cmd/wasm-kernel/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ package main
44

55
import (
66
"context"
7-
"crypto/rand"
87
"encoding/base64"
98
"encoding/json"
109
"fmt"
@@ -20,6 +19,7 @@ import (
2019
"github.com/gosuda/zeroproxy/internal/cookiejar"
2120
"github.com/gosuda/zeroproxy/internal/headers"
2221
"github.com/gosuda/zeroproxy/internal/htmltx"
22+
"github.com/gosuda/zeroproxy/internal/randbuf"
2323
"github.com/gosuda/zeroproxy/internal/smuxconn"
2424
"github.com/gosuda/zeroproxy/internal/swhttp"
2525
"github.com/gosuda/zeroproxy/internal/wsconn"
@@ -527,7 +527,7 @@ func (k *Kernel) tabFromValues(tabID, keyB64 string) *zphttp.TabState {
527527
key, _ := base64.RawURLEncoding.DecodeString(keyB64)
528528
if len(key) == 0 {
529529
key = make([]byte, 32)
530-
_, _ = rand.Read(key)
530+
_ = randbuf.ReadFull(key)
531531
}
532532
t := &zphttp.TabState{TabID: tabID, CookieJar: cookiejar.New(), StreamIsolationKey: key}
533533
k.tabs[tabID] = t

internal/randbuf/randbuf.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package randbuf
2+
3+
import (
4+
"bufio"
5+
"crypto/rand"
6+
"io"
7+
"sync"
8+
)
9+
10+
const defaultBufferSize = 32 * 1024
11+
12+
// Reader amortizes small crypto/rand reads, such as WebSocket frame masks, while
13+
// preserving an io.Reader seam for deterministic tests that inject their own RNG.
14+
var Reader io.Reader = &lockedReader{r: bufio.NewReaderSize(rand.Reader, defaultBufferSize)}
15+
16+
func ReadFull(p []byte) error {
17+
_, err := io.ReadFull(Reader, p)
18+
return err
19+
}
20+
21+
type lockedReader struct {
22+
mu sync.Mutex
23+
r *bufio.Reader
24+
}
25+
26+
func (r *lockedReader) Read(p []byte) (int, error) {
27+
r.mu.Lock()
28+
defer r.mu.Unlock()
29+
return r.r.Read(p)
30+
}

internal/shareurl/shareurl.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"crypto/aes"
55
"crypto/cipher"
66
"crypto/hmac"
7-
"crypto/rand"
87
"crypto/sha256"
98
"encoding/base64"
109
"fmt"
@@ -15,6 +14,8 @@ import (
1514
"strings"
1615

1716
"golang.org/x/crypto/hkdf"
17+
18+
"github.com/gosuda/zeroproxy/internal/randbuf"
1819
)
1920

2021
var (
@@ -34,7 +35,7 @@ const (
3435
// same AES-256-CBC + HMAC-SHA256 envelope as web/zp-core.js, carrying explicit
3536
// relay server parameters.
3637
func NewWithServers(target string, servers []string) (string, error) {
37-
return NewWithRandAndServers(rand.Reader, target, servers)
38+
return NewWithRandAndServers(randbuf.Reader, target, servers)
3839
}
3940

4041
func NewWithRand(random io.Reader, target string) (string, error) {

internal/wsproto/client.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package wsproto
33
import (
44
"bufio"
55
"context"
6-
"crypto/rand"
76
"crypto/sha1"
87
"encoding/base64"
98
"encoding/binary"
@@ -15,6 +14,7 @@ import (
1514
"strings"
1615
"sync"
1716

17+
"github.com/gosuda/zeroproxy/internal/randbuf"
1818
"github.com/gosuda/zeroproxy/internal/zphttp"
1919
)
2020

@@ -49,7 +49,7 @@ func Dial(ctx context.Context, engine *zphttp.Engine, target *url.URL, protocols
4949
return nil, nil, err
5050
}
5151
keyBytes := make([]byte, 16)
52-
if _, err := rand.Read(keyBytes); err != nil {
52+
if err := randbuf.ReadFull(keyBytes); err != nil {
5353
_ = c.Close()
5454
return nil, nil, err
5555
}
@@ -155,7 +155,7 @@ func (c *Conn) WriteFrame(op byte, payload []byte) error {
155155
n = 10
156156
}
157157
var mask [4]byte
158-
if _, err := rand.Read(mask[:]); err != nil {
158+
if err := randbuf.ReadFull(mask[:]); err != nil {
159159
return err
160160
}
161161
copy(hdr[n:], mask[:])

rewriter-rs/src/share_url.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ use base64::Engine;
44
use cbc::cipher::{block_padding::Pkcs7, BlockEncryptMut, KeyIvInit};
55
use hmac::{Hmac, Mac};
66
use sha2::Sha256;
7+
use std::cell::RefCell;
78
use std::collections::HashSet;
9+
use std::io::{self, BufReader, Read};
810
use std::net::IpAddr;
911
use url::Url;
1012

@@ -19,13 +21,37 @@ const MAX_RELAY_SERVERS: usize = 8;
1921
const MAX_RELAY_SERVER_BYTES: usize = 2048;
2022
const SEED_LEN: usize = 64;
2123
const IV_LEN: usize = 16;
24+
const RANDOM_BUFFER_SIZE: usize = 32 * 1024;
25+
26+
thread_local! {
27+
static RANDOM_READER: RefCell<BufReader<GetRandomReader>> =
28+
RefCell::new(BufReader::with_capacity(RANDOM_BUFFER_SIZE, GetRandomReader));
29+
}
30+
31+
struct GetRandomReader;
32+
33+
impl Read for GetRandomReader {
34+
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
35+
getrandom::getrandom(buf).map_err(|err| io::Error::other(err.to_string()))?;
36+
Ok(buf.len())
37+
}
38+
}
2239

2340
pub(crate) fn new_with_servers(target: &str, servers: &[String]) -> Result<String, String> {
2441
let mut random = [0u8; SEED_LEN + IV_LEN];
25-
getrandom::getrandom(&mut random).map_err(|err| err.to_string())?;
42+
fill_random(&mut random)?;
2643
new_with_seed_iv_and_servers(target, servers, &random[..SEED_LEN], &random[SEED_LEN..])
2744
}
2845

46+
fn fill_random(buf: &mut [u8]) -> Result<(), String> {
47+
RANDOM_READER.with(|reader| {
48+
reader
49+
.borrow_mut()
50+
.read_exact(buf)
51+
.map_err(|err| err.to_string())
52+
})
53+
}
54+
2955
pub(crate) fn new_with_seed_iv_and_servers(
3056
target: &str,
3157
servers: &[String],

test/e2e/proxy.test.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ function createTargetServer(requests) {
105105
dynamicImage.id = 'dynamic-image-probe';
106106
dynamicImage.src = '/image-probe.png?dynamic=1';
107107
document.body.appendChild(dynamicImage);
108+
const dynamicRelativeLink = document.createElement('a');
109+
dynamicRelativeLink.id = 'dynamic-relative-next';
110+
dynamicRelativeLink.setAttribute('href', '/next');
111+
dynamicRelativeLink.textContent = 'Dynamic next page';
112+
document.body.appendChild(dynamicRelativeLink);
108113
const dynamicCSP = document.createElement('meta');
109114
dynamicCSP.setAttribute('http-equiv', 'Content-Security-Policy');
110115
dynamicCSP.setAttribute('content', "default-src 'none'");
@@ -1623,6 +1628,16 @@ test('browser traffic uses internal SOCKS5 mode and covers proxied runtime integ
16231628
platform: navigator.platform,
16241629
userAgentData,
16251630
templateLink: window.__templateLinkFixture,
1631+
dynamicRelativeLink: (() => {
1632+
const el = document.getElementById('dynamic-relative-next');
1633+
return (
1634+
el && {
1635+
href: el.getAttribute('href'),
1636+
hrefProp: el.href,
1637+
outerHTML: el.outerHTML,
1638+
}
1639+
);
1640+
})(),
16261641
phase2Location: window.__phase2Location,
16271642
phase2DynamicFunction: window.__phase2DynamicFunction,
16281643
phase2EvalLocation: window.__phase2EvalLocation,
@@ -1824,6 +1839,45 @@ test('browser traffic uses internal SOCKS5 mode and covers proxied runtime integ
18241839
assert.match(addressBarShare, relayServerParam);
18251840
const staticNextHref = await page.$eval('#next', (el) => el.getAttribute('href') || '');
18261841
assert.equal(staticNextHref, `http://${targetHost}:${targetPort}/next`);
1842+
assert.deepEqual(home.dynamicRelativeLink, {
1843+
href: `http://${targetHost}:${targetPort}/next`,
1844+
hrefProp: `http://${targetHost}:${targetPort}/next`,
1845+
outerHTML: `<a id="dynamic-relative-next" href="http://${targetHost}:${targetPort}/next">Dynamic next page</a>`,
1846+
});
1847+
const rawDynamicRelativeLink = await (async () => {
1848+
const client = await page.target().createCDPSession();
1849+
const deadline = Date.now() + 5000;
1850+
let last = null;
1851+
while (Date.now() < deadline) {
1852+
const snap = await client.send('DOMSnapshot.captureSnapshot', {
1853+
computedStyles: [],
1854+
includeDOMRects: false,
1855+
includePaintOrder: false,
1856+
});
1857+
const strings = snap.strings;
1858+
for (const doc of snap.documents) {
1859+
const attrs = doc.nodes.attributes || [];
1860+
for (const nodeAttrs of attrs) {
1861+
const pairs = {};
1862+
for (let i = 0; i < (nodeAttrs || []).length; i += 2) {
1863+
pairs[strings[nodeAttrs[i]]] = strings[nodeAttrs[i + 1]];
1864+
}
1865+
if (pairs.id === 'dynamic-relative-next') {
1866+
last = pairs;
1867+
if (/^\/zp\/p\//.test(pairs.href || '')) return pairs;
1868+
}
1869+
}
1870+
}
1871+
await new Promise((resolve) => setTimeout(resolve, 50));
1872+
}
1873+
return last;
1874+
})();
1875+
assert.match(rawDynamicRelativeLink && rawDynamicRelativeLink.href || '', /^\/zp\/p\//);
1876+
assert.match(rawDynamicRelativeLink && rawDynamicRelativeLink.href || '', /#k=/);
1877+
assert.equal(
1878+
rawDynamicRelativeLink && rawDynamicRelativeLink['data-zp-target-url'],
1879+
`http://${targetHost}:${targetPort}/next`,
1880+
);
18271881
const externalContext = await (browser.createBrowserContext
18281882
? browser.createBrowserContext()
18291883
: browser.createIncognitoBrowserContext());

web/runtime-prelude.mjs

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import { createWorkerFacades } from './runtime/workers/facades.mjs';
6363
const documentCharset = String(boot.documentCharset || '');
6464
const dynamicCompileAllowed = boot.dynamicCompileAllowed === true;
6565
const urlMeta = new WeakMap();
66+
const navShareVersions = new WeakMap();
6667
const messageListenerWrappers = new WeakMap();
6768
const frameWindowOrigins = new WeakMap();
6869
const frameSandboxMeta = new WeakMap();
@@ -318,6 +319,10 @@ import { createWorkerFacades } from './runtime/workers/facades.mjs';
318319
}
319320
function shareFragmentForKey(key) { return ZP.makeShareFragment(String(key), activeServers); }
320321
function isHTTPURL(raw) { try { const u = new URL(String(raw), baseURL); return u.protocol === 'http:' || u.protocol === 'https:'; } catch { return false; } }
322+
function isControlURL(raw) {
323+
const value = String(raw || '').trim();
324+
return value.startsWith(ZP.CONTROL_PREFIX) || value.startsWith(proxyOrigin + ZP.CONTROL_PREFIX);
325+
}
321326
function hasExecutableURLScheme(raw) { return /^(?:javascript|data|vbscript):/i.test(String(raw).trim()); }
322327
function hasDangerousURLScheme(raw) { return /^(?:javascript|vbscript):/i.test(String(raw).trim()); }
323328
function shouldBlockURLAttribute(el, key, raw) {
@@ -1749,6 +1754,29 @@ import { createWorkerFacades } from './runtime/workers/facades.mjs';
17491754
function visibleNavigationURL(el, attrName) {
17501755
return Native.getAttribute.call(el, 'data-zp-target-url') || urlMeta.get(el) || Native.getAttribute.call(el, attrName) || '';
17511756
}
1757+
function isShareNavigationAttribute(el, attrName) {
1758+
const tag = el && el.localName;
1759+
return attrLocalName(attrName) === 'href' && (tag === 'a' || tag === 'area');
1760+
}
1761+
function bumpNavigationShareVersion(el) {
1762+
const version = (navShareVersions.get(el) || 0) + 1;
1763+
navShareVersions.set(el, version);
1764+
return version;
1765+
}
1766+
function scheduleNavigationShareURL(el, attrName, target) {
1767+
if (!isShareNavigationAttribute(el, attrName)) return;
1768+
const version = bumpNavigationShareVersion(el);
1769+
ZP.encryptShareURL(target).then(share => {
1770+
if (navShareVersions.get(el) !== version) return;
1771+
if ((Native.getAttribute.call(el, 'data-zp-target-url') || urlMeta.get(el) || '') !== target) return;
1772+
const href = `${ZP.makeSharePath(share.encrypted)}${shareFragmentForKey(share.key)}`;
1773+
if (Native.getAttribute.call(el, attrName) !== href) Native.setAttribute.call(el, attrName, href);
1774+
}).catch(() => {});
1775+
}
1776+
function rememberNavigationTarget(el, target) {
1777+
urlMeta.set(el, target);
1778+
if (Native.getAttribute.call(el, 'data-zp-target-url') !== target) Native.setAttribute.call(el, 'data-zp-target-url', target);
1779+
}
17521780
function visibleSrcset(el) {
17531781
const stored = Native.getAttribute.call(el, 'data-zp-target-srcset');
17541782
if (stored) return stored;
@@ -2392,10 +2420,11 @@ import { createWorkerFacades } from './runtime/workers/facades.mjs';
23922420
if (isResourceURLAttribute(this, key)) return setResourceURLAttribute(this, k, v);
23932421
if (isURLBearing(this, key)) {
23942422
if (shouldBlockURLAttribute(this, localKey, v)) return blockExecutableURL(this, localKey, v);
2423+
if (isControlURL(v)) return Native.setAttribute.call(this, k, v);
23952424
if (isHTTPURL(v)) {
23962425
const t = targetURL(v);
2397-
urlMeta.set(this, t);
2398-
if (!usesRawURLAttribute(this, key)) Native.setAttribute.call(this, 'data-zp-target-url', t);
2426+
rememberNavigationTarget(this, t);
2427+
scheduleNavigationShareURL(this, k, t);
23992428
if ((this.localName === 'iframe' || this.localName === 'frame') && localKey === 'src') {
24002429
setFrameSourceAttribute(this, k, t);
24012430
return;
@@ -2425,10 +2454,11 @@ import { createWorkerFacades } from './runtime/workers/facades.mjs';
24252454
if (isResourceURLAttribute(this, key)) return setResourceURLAttribute(this, k, v, ns);
24262455
if (isURLBearing(this, key)) {
24272456
if (shouldBlockURLAttribute(this, localKey, v)) return blockExecutableURL(this, localKey, v);
2457+
if (isControlURL(v)) return Native.setAttributeNS.call(this, ns, k, v);
24282458
if (isHTTPURL(v)) {
24292459
const t = targetURL(v);
2430-
urlMeta.set(this, t);
2431-
if (!usesRawURLAttribute(this, key)) Native.setAttribute.call(this, 'data-zp-target-url', t);
2460+
rememberNavigationTarget(this, t);
2461+
scheduleNavigationShareURL(this, k, t);
24322462
if ((this.localName === 'iframe' || this.localName === 'frame') && localKey === 'src') {
24332463
setFrameSourceAttribute(this, k, t, ns);
24342464
return;
@@ -2532,6 +2562,11 @@ import { createWorkerFacades } from './runtime/workers/facades.mjs';
25322562
urlMeta.delete(this);
25332563
Native.removeAttribute.call(this, 'data-zp-target-url');
25342564
}
2565+
if (isURLBearing(this, key) && usesRawURLAttribute(this, key)) {
2566+
bumpNavigationShareVersion(this);
2567+
urlMeta.delete(this);
2568+
Native.removeAttribute.call(this, 'data-zp-target-url');
2569+
}
25352570
if (isSrcsetAttribute(this, key)) Native.removeAttribute.call(this, 'data-zp-target-srcset');
25362571
return Native.removeAttribute.call(this, k);
25372572
}
@@ -3081,16 +3116,16 @@ import { createWorkerFacades } from './runtime/workers/facades.mjs';
30813116
if (!isURLBearing(el, key)) return;
30823117
const raw = Native.getAttribute.call(el, key);
30833118
if (shouldBlockURLAttribute(el, localKey, raw)) { blockExecutableURL(el, localKey, raw); return; }
3084-
if (!raw || !isHTTPURL(raw) || String(raw).startsWith(proxyOrigin)) return;
3119+
if (!raw || !isHTTPURL(raw) || isControlURL(raw) || String(raw).startsWith(proxyOrigin)) return;
30853120
if (isResourceURLAttribute(el, key)) {
30863121
if (!Native.getAttribute.call(el, 'data-zp-target-url')) setResourceURLAttribute(el, key, raw);
30873122
return;
30883123
}
30893124
let target;
30903125
try { target = targetURL(raw); } catch { return; }
3091-
const alreadyMapped = urlMeta.get(el) === target && (!usesRawURLAttribute(el, key) ? Native.getAttribute.call(el, 'data-zp-target-url') === target : true);
3092-
urlMeta.set(el, target);
3093-
if (!usesRawURLAttribute(el, key)) Native.setAttribute.call(el, 'data-zp-target-url', target);
3126+
const alreadyMapped = urlMeta.get(el) === target && Native.getAttribute.call(el, 'data-zp-target-url') === target;
3127+
rememberNavigationTarget(el, target);
3128+
scheduleNavigationShareURL(el, key, target);
30943129
if ((tag === 'iframe' || tag === 'frame') && localKey === 'src') {
30953130
sanitizeFrameSandbox(el);
30963131
setFrameSourceAttribute(el, key, target);

0 commit comments

Comments
 (0)