From 2e9daab3eaa4bae4b7f145121b3c2aba49f5892b Mon Sep 17 00:00:00 2001 From: snek Date: Thu, 18 Sep 2025 17:05:05 +0200 Subject: [PATCH] feat(unstable): fetch tunnel proxy --- Cargo.lock | 1 + cli/tsc/dts/lib.deno.ns.d.ts | 3 +++ ext/fetch/22_http_client.js | 3 ++- ext/fetch/Cargo.toml | 1 + ext/fetch/lib.rs | 15 ++++++------- ext/fetch/proxy.rs | 36 +++++++++++++++++++------------- ext/tls/lib.rs | 9 ++++++++ tests/specs/run/tunnel/client.ts | 7 +++++++ tests/specs/run/tunnel/test.out | 8 +++++++ tests/specs/run/tunnel/test.ts | 23 +++++++++++++++++--- 10 files changed, 81 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index be49a152cb..1e3a8caebd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2106,6 +2106,7 @@ dependencies = [ "deno_core", "deno_error", "deno_fs", + "deno_net", "deno_path_util", "deno_permissions", "deno_tls", diff --git a/cli/tsc/dts/lib.deno.ns.d.ts b/cli/tsc/dts/lib.deno.ns.d.ts index 4ac2ec9688..477e6720e0 100644 --- a/cli/tsc/dts/lib.deno.ns.d.ts +++ b/cli/tsc/dts/lib.deno.ns.d.ts @@ -6387,6 +6387,9 @@ declare namespace Deno { cid: number; /** The port of the vsock to connect to. */ port: number; + } | { + transport: "tunnel"; + kind: "agent"; }; /** diff --git a/ext/fetch/22_http_client.js b/ext/fetch/22_http_client.js index 2bc971e02f..f16cbdad98 100644 --- a/ext/fetch/22_http_client.js +++ b/ext/fetch/22_http_client.js @@ -77,7 +77,8 @@ function createHttpClient(options) { } case "tcp": case "unix": - case "vsock": { + case "vsock": + case "tunnel": { break; } default: { diff --git a/ext/fetch/Cargo.toml b/ext/fetch/Cargo.toml index 3ea6d47338..62f87975bc 100644 --- a/ext/fetch/Cargo.toml +++ b/ext/fetch/Cargo.toml @@ -20,6 +20,7 @@ data-url.workspace = true deno_core.workspace = true deno_error.workspace = true deno_fs.workspace = true +deno_net.workspace = true deno_path_util.workspace = true deno_permissions.workspace = true deno_tls.workspace = true diff --git a/ext/fetch/lib.rs b/ext/fetch/lib.rs index 23611d7f91..f7d5fdcd06 100644 --- a/ext/fetch/lib.rs +++ b/ext/fetch/lib.rs @@ -972,6 +972,7 @@ where let permissions = state.borrow_mut::(); permissions.check_net_vsock(*cid, *port, "Deno.createHttpClient()")?; } + Proxy::Tunnel { .. } => {} } } @@ -1144,16 +1145,15 @@ pub fn create_http_client( } intercept } - Proxy::Tcp { - hostname: host, - port, - } => { - let target = proxy::Target::new_tcp(host, port); + Proxy::Tcp { hostname, port } => { + let target = proxy::Target::Tcp { hostname, port }; proxy::Intercept::all(target) } #[cfg(not(windows))] Proxy::Unix { path } => { - let target = proxy::Target::new_unix(PathBuf::from(path)); + let target = proxy::Target::Unix { + path: PathBuf::from(path), + }; proxy::Intercept::all(target) } #[cfg(windows)] @@ -1166,7 +1166,7 @@ pub fn create_http_client( target_os = "macos" ))] Proxy::Vsock { cid, port } => { - let target = proxy::Target::new_vsock(cid, port); + let target = proxy::Target::Vsock { cid, port }; proxy::Intercept::all(target) } #[cfg(not(any( @@ -1177,6 +1177,7 @@ pub fn create_http_client( Proxy::Vsock { .. } => { return Err(HttpClientCreateError::VsockProxyNotSupported); } + Proxy::Tunnel { .. } => proxy::Intercept::all(proxy::Target::Tunnel), }; proxies.prepend(intercept); } diff --git a/ext/fetch/proxy.rs b/ext/fetch/proxy.rs index 2eed5665aa..4259b55277 100644 --- a/ext/fetch/proxy.rs +++ b/ext/fetch/proxy.rs @@ -128,6 +128,7 @@ pub(crate) enum Target { cid: u32, port: u32, }, + Tunnel, } #[derive(Debug, Clone, Copy)] @@ -242,6 +243,9 @@ impl Intercept { Target::Vsock { .. } => { // Auth not supported for Vsock sockets } + Target::Tunnel => { + // Auth not supported for Vsock sockets + } } } } @@ -339,20 +343,6 @@ impl Target { Some(target) } - - pub(crate) fn new_tcp(hostname: String, port: u16) -> Self { - Target::Tcp { hostname, port } - } - - #[cfg(not(windows))] - pub(crate) fn new_unix(path: PathBuf) -> Self { - Target::Unix { path } - } - - #[cfg(any(target_os = "android", target_os = "linux", target_os = "macos"))] - pub(crate) fn new_vsock(cid: u32, port: u32) -> Self { - Target::Vsock { cid, port } - } } #[derive(Debug)] @@ -560,6 +550,8 @@ pub enum Proxied { /// Forwarded via Vsock socket #[cfg(any(target_os = "android", target_os = "linux", target_os = "macos"))] Vsock(TokioIo), + /// Forwarded through tunnel + Tunnel(TokioIo), } impl Service for ProxyConnector @@ -691,6 +683,15 @@ where let io = VsockStream::connect(addr).await?; Ok(Proxied::Vsock(TokioIo::new(io))) }), + Target::Tunnel => Box::pin(async move { + let Some(tunnel) = deno_net::tunnel::get_tunnel() else { + return Err("tunnel is not connected".into()); + }; + + let stream = tunnel.create_agent_stream().await?; + + Ok(Proxied::Tunnel(TokioIo::new(stream))) + }), }; } @@ -808,6 +809,7 @@ where target_os = "macos" ))] Proxied::Vsock(ref mut p) => Pin::new(p).poll_read(cx, buf), + Proxied::Tunnel(ref mut p) => Pin::new(p).poll_read(cx, buf), } } } @@ -835,6 +837,7 @@ where target_os = "macos" ))] Proxied::Vsock(ref mut p) => Pin::new(p).poll_write(cx, buf), + Proxied::Tunnel(ref mut p) => Pin::new(p).poll_write(cx, buf), } } @@ -856,6 +859,7 @@ where target_os = "macos" ))] Proxied::Vsock(ref mut p) => Pin::new(p).poll_flush(cx), + Proxied::Tunnel(ref mut p) => Pin::new(p).poll_flush(cx), } } @@ -877,6 +881,7 @@ where target_os = "macos" ))] Proxied::Vsock(ref mut p) => Pin::new(p).poll_shutdown(cx), + Proxied::Tunnel(ref mut p) => Pin::new(p).poll_shutdown(cx), } } @@ -895,6 +900,7 @@ where target_os = "macos" ))] Proxied::Vsock(ref p) => p.is_write_vectored(), + Proxied::Tunnel(ref p) => p.is_write_vectored(), } } @@ -921,6 +927,7 @@ where target_os = "macos" ))] Proxied::Vsock(ref mut p) => Pin::new(p).poll_write_vectored(cx, bufs), + Proxied::Tunnel(ref mut p) => Pin::new(p).poll_write_vectored(cx, bufs), } } } @@ -958,6 +965,7 @@ where target_os = "macos" ))] Proxied::Vsock(_) => Connected::new().proxy(true), + Proxied::Tunnel(_) => Connected::new().proxy(true), } } } diff --git a/ext/tls/lib.rs b/ext/tls/lib.rs index 4feb34559c..1bc5ddaef7 100644 --- a/ext/tls/lib.rs +++ b/ext/tls/lib.rs @@ -224,6 +224,12 @@ impl ServerCertVerifier for NoServerNameVerification { } } +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub enum TunnelKind { + Agent, +} + #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase", tag = "transport")] pub enum Proxy { @@ -243,6 +249,9 @@ pub enum Proxy { cid: u32, port: u32, }, + Tunnel { + kind: TunnelKind, + }, } #[derive(Deserialize, Default, Debug, Clone)] diff --git a/tests/specs/run/tunnel/client.ts b/tests/specs/run/tunnel/client.ts index dc86ca7c16..1f55a221e4 100644 --- a/tests/specs/run/tunnel/client.ts +++ b/tests/specs/run/tunnel/client.ts @@ -1,11 +1,18 @@ let serveAddr; +const client = Deno.createHttpClient({ + proxy: { transport: "tunnel", kind: "agent" }, +}); + Deno.serve({ onListen(addr) { serveAddr = addr; }, }, async (req, info) => { const headers = Object.fromEntries(req.headers); + + await fetch("http://meow.com", { client }); + return Response.json({ method: req.method, url: req.url, diff --git a/tests/specs/run/tunnel/test.out b/tests/specs/run/tunnel/test.out index a462569bf3..93076c553b 100644 --- a/tests/specs/run/tunnel/test.out +++ b/tests/specs/run/tunnel/test.out @@ -1,4 +1,12 @@ You are connected to https://localhost:[WILDLINE] +GET http://meow.com/ HTTP/1.1 +accept: */* +accept-language: * +user-agent: Deno/[WILDCARD] +accept-encoding: gzip,br +host: meow.com + + HTTP/1.1 200 OK content-type: application/json vary: Accept-Encoding diff --git a/tests/specs/run/tunnel/test.ts b/tests/specs/run/tunnel/test.ts index cd44e67768..c6398b5947 100644 --- a/tests/specs/run/tunnel/test.ts +++ b/tests/specs/run/tunnel/test.ts @@ -43,10 +43,10 @@ for await (const incoming of listener) { async function handleConnection(incoming: Deno.QuicIncoming) { const conn = await incoming.accept(); + const incomingBidi = conn.incomingBidirectionalStreams.getReader(); + { - const { value: bi } = await conn.incomingBidirectionalStreams - .getReader() - .read(); + const { value: bi } = await incomingBidi.read(); const reader = bi.readable.getReader({ mode: "byob" }); const version = await readUint32LE(reader); @@ -105,6 +105,23 @@ async function handleConnection(incoming: Deno.QuicIncoming) { new TextEncoder().encode(`GET / HTTP/1.1\r\nHost: localhost\r\n\r\n`), ); + { + const { value: agentStream } = await incomingBidi.read(); + const reader = agentStream.readable.getReader({ mode: "byob" }); + const agentHeader = await readStreamHeader(reader); + if (agentHeader.headerType !== "Agent") { + conn.close({ closeCode: 1, reason: "expected Agent" }); + return; + } + const { value } = await reader.read(new Uint8Array(1024)); + console.log(new TextDecoder().decode(value)); + await agentStream.writable.getWriter().write( + new TextEncoder().encode( + `HTTP/1.1 201 No Content\r\nConnection: close\r\n\r\n`, + ), + ); + } + const reader = stream.readable.getReader({ mode: "byob" }); const { value } = await reader.read(new Uint8Array(1024)); console.log(new TextDecoder().decode(value));