feat: support linux vsock (#28725)

impl support for vsock
https://man7.org/linux/man-pages/man7/vsock.7.html
This commit is contained in:
snek 2025-04-11 07:35:05 +02:00 committed by GitHub
parent 7218113d24
commit 9da231dc7a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 680 additions and 28 deletions

25
Cargo.lock generated
View file

@ -2240,6 +2240,7 @@ dependencies = [
"socket2", "socket2",
"thiserror 2.0.12", "thiserror 2.0.12",
"tokio", "tokio",
"tokio-vsock",
"url", "url",
"web-transport-proto", "web-transport-proto",
] ]
@ -5588,6 +5589,7 @@ dependencies = [
"cfg-if", "cfg-if",
"cfg_aliases", "cfg_aliases",
"libc", "libc",
"memoffset",
] ]
[[package]] [[package]]
@ -8696,6 +8698,19 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "tokio-vsock"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "074885a713a0e1e8f2cc6855a004c7c882572d980d4f8262523dc2b094c96da8"
dependencies = [
"bytes",
"futures",
"libc",
"tokio",
"vsock",
]
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.5.11" version = "0.5.11"
@ -9199,6 +9214,16 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
[[package]]
name = "vsock"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e8b4d00e672f147fc86a09738fadb1445bd1c0a40542378dfb82909deeee688"
dependencies = [
"libc",
"nix 0.29.0",
]
[[package]] [[package]]
name = "vte" name = "vte"
version = "0.11.1" version = "0.11.1"

View file

@ -369,6 +369,7 @@ syn = { version = "2", features = ["full", "extra-traits"] }
# unix deps # unix deps
nix = "=0.27.1" nix = "=0.27.1"
tokio-vsock = "0.7"
# windows deps # windows deps
junction = "=1.2.0" junction = "=1.2.0"

View file

@ -32,6 +32,7 @@ pub fn validator(host_and_port: &str) -> Result<String, String> {
if Url::parse(&format!("internal://{host_and_port}")).is_ok() if Url::parse(&format!("internal://{host_and_port}")).is_ok()
|| host_and_port.parse::<IpAddr>().is_ok() || host_and_port.parse::<IpAddr>().is_ok()
|| host_and_port.parse::<BarePort>().is_ok() || host_and_port.parse::<BarePort>().is_ok()
|| NetDescriptor::parse(host_and_port).is_ok()
{ {
Ok(host_and_port.to_string()) Ok(host_and_port.to_string())
} else { } else {

View file

@ -5160,6 +5160,23 @@ declare namespace Deno {
path: string; path: string;
} }
/**
* Options that can be passed to `Deno.serve` to create a server listening on
* a vsock socket.
*
* @category HTTP Server
*/
export interface ServeVsockOptions extends ServeOptions<Deno.VsockAddr> {
/** The transport to use. */
transport?: "vsock";
/** The context identifier to use. */
cid: number;
/** The port to use. */
port: number;
}
/** /**
* @category HTTP Server * @category HTTP Server
*/ */
@ -5261,6 +5278,56 @@ declare namespace Deno {
options: ServeUnixOptions, options: ServeUnixOptions,
handler: ServeHandler<Deno.UnixAddr>, handler: ServeHandler<Deno.UnixAddr>,
): HttpServer<Deno.UnixAddr>; ): HttpServer<Deno.UnixAddr>;
/** Serves HTTP requests with the given option bag and handler.
*
* You can specify the socket path with `path` option.
*
* ```ts
* Deno.serve(
* { cid: -1, port: 3000 },
* (_req) => new Response("Hello, world")
* );
* ```
*
* You can stop the server with an {@linkcode AbortSignal}. The abort signal
* needs to be passed as the `signal` option in the options bag. The server
* aborts when the abort signal is aborted. To wait for the server to close,
* await the promise returned from the `Deno.serve` API.
*
* ```ts
* const ac = new AbortController();
*
* const server = Deno.serve(
* { signal: ac.signal, cid: -1, port: 3000 },
* (_req) => new Response("Hello, world")
* );
* server.finished.then(() => console.log("Server closed"));
*
* console.log("Closing server...");
* ac.abort();
* ```
*
* By default `Deno.serve` prints the message
* `Listening on path/to/socket` on listening. If you like to
* change this behavior, you can specify a custom `onListen` callback.
*
* ```ts
* Deno.serve({
* onListen({ cid, port }) {
* console.log(`Server started at ${cid}:${port}`);
* // ... more info specific to your server ..
* },
* cid: -1,
* port: 3000,
* }, (_req) => new Response("Hello, world"));
* ```
*
* @category HTTP Server
*/
export function serve(
options: ServeVsockOptions,
handler: ServeHandler<Deno.VsockAddr>,
): HttpServer<Deno.VsockAddr>;
/** Serves HTTP requests with the given option bag and handler. /** Serves HTTP requests with the given option bag and handler.
* *
* You can specify an object with a port and hostname option, which is the * You can specify an object with a port and hostname option, which is the
@ -5348,6 +5415,34 @@ declare namespace Deno {
export function serve( export function serve(
options: ServeUnixOptions & ServeInit<Deno.UnixAddr>, options: ServeUnixOptions & ServeInit<Deno.UnixAddr>,
): HttpServer<Deno.UnixAddr>; ): HttpServer<Deno.UnixAddr>;
/** Serves HTTP requests with the given option bag.
*
* You can specify an object with the path option, which is the
* vsock socket to listen on.
*
* ```ts
* const ac = new AbortController();
*
* const server = Deno.serve({
* cid: -1,
* port: 3000,
* handler: (_req) => new Response("Hello, world"),
* signal: ac.signal,
* onListen({ cid, port }) {
* console.log(`Server started at ${cid}:${port}`);
* },
* });
* server.finished.then(() => console.log("Server closed"));
*
* console.log("Closing server...");
* ac.abort();
* ```
*
* @category HTTP Server
*/
export function serve(
options: ServeVsockOptions & ServeInit<Deno.VsockAddr>,
): HttpServer<Deno.VsockAddr>;
/** Serves HTTP requests with the given option bag. /** Serves HTTP requests with the given option bag.
* *
* You can specify an object with a port and hostname option, which is the * You can specify an object with a port and hostname option, which is the

View file

@ -19,7 +19,14 @@ declare namespace Deno {
} }
/** @category Network */ /** @category Network */
export type Addr = NetAddr | UnixAddr; export interface VsockAddr {
transport: "vsock";
cid: number;
port: number;
}
/** @category Network */
export type Addr = NetAddr | UnixAddr | VsockAddr;
/** A generic network listener for stream-oriented protocols. /** A generic network listener for stream-oriented protocols.
* *
@ -67,6 +74,12 @@ declare namespace Deno {
*/ */
export type UnixListener = Listener<UnixConn, UnixAddr>; export type UnixListener = Listener<UnixConn, UnixAddr>;
/** Specialized listener that accepts Vsock connections.
*
* @category Network
*/
export type VsockListener = Listener<VsockConn, VsockAddr>;
/** @category Network */ /** @category Network */
export interface Conn<A extends Addr = Addr> extends Disposable { export interface Conn<A extends Addr = Addr> extends Disposable {
/** Read the incoming data from the connection into an array buffer (`p`). /** Read the incoming data from the connection into an array buffer (`p`).
@ -223,6 +236,32 @@ declare namespace Deno {
options: UnixListenOptions & { transport: "unix" }, options: UnixListenOptions & { transport: "unix" },
): UnixListener; ): UnixListener;
/** Options which can be set when opening a vsock listener via
* {@linkcode Deno.listen}.
*
* @category Network
*/
export interface VsockListenOptions {
cid: number;
port: number;
}
/** Listen announces on the local transport address.
*
* ```ts
* const listener = Deno.listen({ cid: -1, port: 80, transport: "vsock" })
* ```
*
* Requires `allow-net` permission.
*
* @tags allow-net
* @category Network
*/
// deno-lint-ignore adjacent-overload-signatures
export function listen(
options: VsockListenOptions & { transport: "vsock" },
): VsockListener;
/** /**
* Provides certified key material from strings. The key material is provided in * Provides certified key material from strings. The key material is provided in
* `PEM`-format (Privacy Enhanced Mail, https://www.rfc-editor.org/rfc/rfc1422) which can be identified by having * `PEM`-format (Privacy Enhanced Mail, https://www.rfc-editor.org/rfc/rfc1422) which can be identified by having
@ -350,6 +389,36 @@ declare namespace Deno {
// deno-lint-ignore adjacent-overload-signatures // deno-lint-ignore adjacent-overload-signatures
export function connect(options: UnixConnectOptions): Promise<UnixConn>; export function connect(options: UnixConnectOptions): Promise<UnixConn>;
/** @category Network */
export interface VsockConnectOptions {
transport: "vsock";
cid: number;
port: number;
}
/** @category Network */
export interface VsockConn extends Conn<VsockAddr> {}
/** Connects to the hostname (default is "127.0.0.1") and port on the named
* transport (default is "tcp"), and resolves to the connection (`Conn`).
*
* ```ts
* const conn1 = await Deno.connect({ port: 80 });
* const conn2 = await Deno.connect({ hostname: "192.0.2.1", port: 80 });
* const conn3 = await Deno.connect({ hostname: "[2001:db8::1]", port: 80 });
* const conn4 = await Deno.connect({ hostname: "golang.org", port: 80, transport: "tcp" });
* const conn5 = await Deno.connect({ path: "/foo/bar.sock", transport: "unix" });
* const conn6 = await Deno.connect({ cid: -1, port: 80, transport: "vsock" });
* ```
*
* Requires `allow-net` permission for "tcp" and "vsock", and `allow-read` for "unix".
*
* @tags allow-net, allow-read
* @category Network
*/
// deno-lint-ignore adjacent-overload-signatures
export function connect(options: VsockConnectOptions): Promise<VsockConn>;
/** @category Network */ /** @category Network */
export interface ConnectTlsOptions { export interface ConnectTlsOptions {
/** The port to connect to. */ /** The port to connect to. */

View file

@ -46,6 +46,7 @@ const {
TypedArrayPrototypeGetSymbolToStringTag, TypedArrayPrototypeGetSymbolToStringTag,
Uint8Array, Uint8Array,
Promise, Promise,
Number,
} = primordials; } = primordials;
import { InnerBody } from "ext:deno_fetch/22_body.js"; import { InnerBody } from "ext:deno_fetch/22_body.js";
@ -348,6 +349,13 @@ class InnerRequest {
} }
this.#methodAndUri = op_http_get_request_method_and_url(this.#external); this.#methodAndUri = op_http_get_request_method_and_url(this.#external);
} }
if (transport === "vsock") {
return {
transport,
cid: Number(this.#methodAndUri[3]),
port: this.#methodAndUri[4],
};
}
return { return {
transport: "tcp", transport: "tcp",
hostname: this.#methodAndUri[3], hostname: this.#methodAndUri[3],
@ -769,6 +777,7 @@ function serve(arg1, arg2) {
const wantsHttps = hasTlsKeyPairOptions(options); const wantsHttps = hasTlsKeyPairOptions(options);
const wantsUnix = ObjectHasOwn(options, "path"); const wantsUnix = ObjectHasOwn(options, "path");
const wantsVsock = ObjectHasOwn(options, "cid");
const signal = options.signal; const signal = options.signal;
const onError = options.onError ?? const onError = options.onError ??
function (error) { function (error) {
@ -792,6 +801,23 @@ function serve(arg1, arg2) {
}); });
} }
if (wantsVsock) {
const listener = listen({
transport: "vsock",
cid: options.cid,
port: options.port,
[listenOptionApiName]: "Deno.serve",
});
const { cid, port } = listener.addr;
return serveHttpOnListener(listener, signal, handler, onError, () => {
if (options.onListen) {
options.onListen(listener.addr);
} else {
import.meta.log("info", `Listening on vsock:${cid}:${port}`);
}
});
}
const listenOpts = { const listenOpts = {
hostname: options.hostname ?? "0.0.0.0", hostname: options.hostname ?? "0.0.0.0",
port: options.port ?? 8000, port: options.port ?? 8000,

View file

@ -385,7 +385,7 @@ where
.into(); .into();
let port: v8::Local<v8::Value> = match request_info.peer_port { let port: v8::Local<v8::Value> = match request_info.peer_port {
Some(port) => v8::Integer::new(scope, port.into()).into(), Some(port) => v8::Number::new(scope, port.into()).into(),
None => v8::undefined(scope).into(), None => v8::undefined(scope).into(),
}; };
@ -1025,6 +1025,10 @@ where
NetworkStream::Unix(conn) => { NetworkStream::Unix(conn) => {
serve_http(conn, connection_properties, lifetime, tx, options) serve_http(conn, connection_properties, lifetime, tx, options)
} }
#[cfg(unix)]
NetworkStream::Vsock(conn) => {
serve_http(conn, connection_properties, lifetime, tx, options)
}
} }
} }

View file

@ -23,15 +23,15 @@ use hyper::Uri;
pub struct HttpListenProperties { pub struct HttpListenProperties {
pub scheme: &'static str, pub scheme: &'static str,
pub fallback_host: String, pub fallback_host: String,
pub local_port: Option<u16>, pub local_port: Option<u32>,
pub stream_type: NetworkStreamType, pub stream_type: NetworkStreamType,
} }
#[derive(Clone)] #[derive(Clone)]
pub struct HttpConnectionProperties { pub struct HttpConnectionProperties {
pub peer_address: Rc<str>, pub peer_address: Rc<str>,
pub peer_port: Option<u16>, pub peer_port: Option<u32>,
pub local_port: Option<u16>, pub local_port: Option<u32>,
pub stream_type: NetworkStreamType, pub stream_type: NetworkStreamType,
} }
@ -161,15 +161,19 @@ impl HttpPropertyExtractor for DefaultHttpPropertyExtractor {
0, 0,
))) )))
}); });
let peer_port: Option<u16> = match peer_address { let peer_port: Option<u32> = match peer_address {
NetworkStreamAddress::Ip(ip) => Some(ip.port()), NetworkStreamAddress::Ip(ip) => Some(ip.port() as _),
#[cfg(unix)] #[cfg(unix)]
NetworkStreamAddress::Unix(_) => None, NetworkStreamAddress::Unix(_) => None,
#[cfg(unix)]
NetworkStreamAddress::Vsock(vsock) => Some(vsock.port()),
}; };
let peer_address = match peer_address { let peer_address = match peer_address {
NetworkStreamAddress::Ip(addr) => Rc::from(addr.ip().to_string()), NetworkStreamAddress::Ip(addr) => Rc::from(addr.ip().to_string()),
#[cfg(unix)] #[cfg(unix)]
NetworkStreamAddress::Unix(_) => Rc::from("unix"), NetworkStreamAddress::Unix(_) => Rc::from("unix"),
#[cfg(unix)]
NetworkStreamAddress::Vsock(addr) => Rc::from(addr.cid().to_string()),
}; };
let local_port = listen_properties.local_port; let local_port = listen_properties.local_port;
let stream_type = listen_properties.stream_type; let stream_type = listen_properties.stream_type;
@ -204,10 +208,12 @@ fn listener_properties(
) -> Result<HttpListenProperties, std::io::Error> { ) -> Result<HttpListenProperties, std::io::Error> {
let scheme = req_scheme_from_stream_type(stream_type); let scheme = req_scheme_from_stream_type(stream_type);
let fallback_host = req_host_from_addr(stream_type, &local_address); let fallback_host = req_host_from_addr(stream_type, &local_address);
let local_port: Option<u16> = match local_address { let local_port: Option<u32> = match local_address {
NetworkStreamAddress::Ip(ip) => Some(ip.port()), NetworkStreamAddress::Ip(ip) => Some(ip.port() as _),
#[cfg(unix)] #[cfg(unix)]
NetworkStreamAddress::Unix(_) => None, NetworkStreamAddress::Unix(_) => None,
#[cfg(unix)]
NetworkStreamAddress::Vsock(vsock) => Some(vsock.port()),
}; };
Ok(HttpListenProperties { Ok(HttpListenProperties {
scheme, scheme,
@ -252,6 +258,10 @@ fn req_host_from_addr(
percent_encoding::NON_ALPHANUMERIC, percent_encoding::NON_ALPHANUMERIC,
) )
.to_string(), .to_string(),
#[cfg(unix)]
NetworkStreamAddress::Vsock(vsock) => {
format!("{}:{}", vsock.cid(), vsock.port())
}
} }
} }
@ -261,6 +271,8 @@ fn req_scheme_from_stream_type(stream_type: NetworkStreamType) -> &'static str {
NetworkStreamType::Tls => "https://", NetworkStreamType::Tls => "https://",
#[cfg(unix)] #[cfg(unix)]
NetworkStreamType::Unix => "http+unix://", NetworkStreamType::Unix => "http+unix://",
#[cfg(unix)]
NetworkStreamType::Vsock => "http+vsock://",
} }
} }
@ -268,11 +280,13 @@ fn req_host<'a>(
uri: &'a Uri, uri: &'a Uri,
headers: &'a HeaderMap, headers: &'a HeaderMap,
addr_type: NetworkStreamType, addr_type: NetworkStreamType,
port: u16, port: u32,
) -> Option<Cow<'a, str>> { ) -> Option<Cow<'a, str>> {
// Unix sockets always use the socket address // Unix sockets always use the socket address
#[cfg(unix)] #[cfg(unix)]
if addr_type == NetworkStreamType::Unix { if addr_type == NetworkStreamType::Unix
|| addr_type == NetworkStreamType::Vsock
{
return None; return None;
} }
@ -291,6 +305,8 @@ fn req_host<'a>(
} }
#[cfg(unix)] #[cfg(unix)]
NetworkStreamType::Unix => {} NetworkStreamType::Unix => {}
#[cfg(unix)]
NetworkStreamType::Vsock => {}
} }
return Some(Cow::Borrowed(auth.as_str())); return Some(Cow::Borrowed(auth.as_str()));
} }

View file

@ -12,14 +12,17 @@ import {
op_dns_resolve, op_dns_resolve,
op_net_accept_tcp, op_net_accept_tcp,
op_net_accept_unix, op_net_accept_unix,
op_net_accept_vsock,
op_net_connect_tcp, op_net_connect_tcp,
op_net_connect_unix, op_net_connect_unix,
op_net_connect_vsock,
op_net_join_multi_v4_udp, op_net_join_multi_v4_udp,
op_net_join_multi_v6_udp, op_net_join_multi_v6_udp,
op_net_leave_multi_v4_udp, op_net_leave_multi_v4_udp,
op_net_leave_multi_v6_udp, op_net_leave_multi_v6_udp,
op_net_listen_tcp, op_net_listen_tcp,
op_net_listen_unix, op_net_listen_unix,
op_net_listen_vsock,
op_net_recv_udp, op_net_recv_udp,
op_net_recv_unixpacket, op_net_recv_unixpacket,
op_net_send_udp, op_net_send_udp,
@ -249,6 +252,20 @@ class UnixConn extends Conn {
} }
} }
class VsockConn extends Conn {
#rid = 0;
constructor(rid, remoteAddr, localAddr) {
super(rid, remoteAddr, localAddr);
ObjectDefineProperty(this, internalRidSymbol, {
__proto__: null,
enumerable: false,
value: rid,
});
this.#rid = rid;
}
}
class Listener { class Listener {
#rid = 0; #rid = 0;
#addr = null; #addr = null;
@ -278,6 +295,9 @@ class Listener {
case "unix": case "unix":
promise = op_net_accept_unix(this.#rid); promise = op_net_accept_unix(this.#rid);
break; break;
case "vsock":
promise = op_net_accept_vsock(this.#rid);
break;
default: default:
throw new Error(`Unsupported transport: ${this.addr.transport}`); throw new Error(`Unsupported transport: ${this.addr.transport}`);
} }
@ -285,17 +305,24 @@ class Listener {
if (this.#unref) core.unrefOpPromise(promise); if (this.#unref) core.unrefOpPromise(promise);
const { 0: rid, 1: localAddr, 2: remoteAddr, 3: fd } = await promise; const { 0: rid, 1: localAddr, 2: remoteAddr, 3: fd } = await promise;
this.#promise = null; this.#promise = null;
if (this.addr.transport == "tcp") { switch (this.addr.transport) {
case "tcp":
localAddr.transport = "tcp"; localAddr.transport = "tcp";
remoteAddr.transport = "tcp"; remoteAddr.transport = "tcp";
return new TcpConn(rid, remoteAddr, localAddr, fd); return new TcpConn(rid, remoteAddr, localAddr, fd);
} else if (this.addr.transport == "unix") { case "unix":
return new UnixConn( return new UnixConn(
rid, rid,
{ transport: "unix", path: remoteAddr }, { transport: "unix", path: remoteAddr },
{ transport: "unix", path: localAddr }, { transport: "unix", path: localAddr },
); );
} else { case "vsock":
return new VsockConn(
rid,
{ transport: "vsock", cid: remoteAddr[0], port: remoteAddr[1] },
{ transport: "vsock", cid: localAddr[0], port: localAddr[1] },
);
default:
throw new Error("unreachable"); throw new Error("unreachable");
} }
} }
@ -528,6 +555,18 @@ function listen(args) {
}; };
return new Listener(rid, addr); return new Listener(rid, addr);
} }
case "vsock": {
const { 0: rid, 1: cid, 2: port } = op_net_listen_vsock(
args.cid,
args.port,
);
const addr = {
transport: "vsock",
cid,
port,
};
return new Listener(rid, addr);
}
default: default:
throw new TypeError(`Unsupported transport: '${transport}'`); throw new TypeError(`Unsupported transport: '${transport}'`);
} }
@ -605,6 +644,15 @@ async function connect(args) {
{ transport: "unix", path: localAddr }, { transport: "unix", path: localAddr },
); );
} }
case "vsock": {
const { 0: rid, 1: localAddr, 2: remoteAddr } =
await op_net_connect_vsock(args.cid, args.port);
return new VsockConn(
rid,
{ transport: "vsock", cid: remoteAddr[0], port: remoteAddr[1] },
{ transport: "vsock", cid: localAddr[0], port: localAddr[1] },
);
}
default: default:
throw new TypeError(`Unsupported transport: '${transport}'`); throw new TypeError(`Unsupported transport: '${transport}'`);
} }
@ -622,4 +670,5 @@ export {
UnixConn, UnixConn,
UpgradedConn, UpgradedConn,
validatePort, validatePort,
VsockConn,
}; };

View file

@ -30,3 +30,6 @@ thiserror.workspace = true
tokio.workspace = true tokio.workspace = true
url.workspace = true url.workspace = true
web-transport-proto.workspace = true web-transport-proto.workspace = true
[target.'cfg(unix)'.dependencies]
tokio-vsock.workspace = true

View file

@ -189,3 +189,44 @@ impl Resource for UnixStreamResource {
self.cancel_read_ops(); self.cancel_read_ops();
} }
} }
#[cfg(unix)]
pub type VsockStreamResource =
FullDuplexResource<tokio_vsock::OwnedReadHalf, tokio_vsock::OwnedWriteHalf>;
#[cfg(not(unix))]
pub struct VsockStreamResource;
#[cfg(not(unix))]
impl VsockStreamResource {
fn read(self: Rc<Self>, _data: &mut [u8]) -> AsyncResult<usize> {
unreachable!()
}
fn write(self: Rc<Self>, _data: &[u8]) -> AsyncResult<usize> {
unreachable!()
}
#[allow(clippy::unused_async)]
pub async fn shutdown(self: Rc<Self>) -> Result<(), JsErrorBox> {
unreachable!()
}
pub fn cancel_read_ops(&self) {
unreachable!()
}
}
impl Resource for VsockStreamResource {
deno_core::impl_readable_byob!();
deno_core::impl_writable!();
fn name(&self) -> Cow<str> {
"vsockStream".into()
}
fn shutdown(self: Rc<Self>) -> AsyncResult<()> {
Box::pin(self.shutdown().map_err(JsErrorBox::from_err))
}
fn close(self: Rc<Self>) {
self.cancel_read_ops();
}
}

View file

@ -47,6 +47,13 @@ pub trait NetPermissions {
p: &'a Path, p: &'a Path,
api_name: &str, api_name: &str,
) -> Result<Cow<'a, Path>, PermissionCheckError>; ) -> Result<Cow<'a, Path>, PermissionCheckError>;
#[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"]
fn check_vsock(
&mut self,
cid: u32,
port: u32,
api_name: &str,
) -> Result<(), PermissionCheckError>;
} }
impl NetPermissions for deno_permissions::PermissionsContainer { impl NetPermissions for deno_permissions::PermissionsContainer {
@ -87,6 +94,18 @@ impl NetPermissions for deno_permissions::PermissionsContainer {
self, path, api_name, self, path, api_name,
) )
} }
#[inline(always)]
fn check_vsock(
&mut self,
cid: u32,
port: u32,
api_name: &str,
) -> Result<(), PermissionCheckError> {
deno_permissions::PermissionsContainer::check_net_vsock(
self, cid, port, api_name,
)
}
} }
/// Helper for checking unstable features. Used for sync ops. /// Helper for checking unstable features. Used for sync ops.
@ -139,6 +158,9 @@ deno_core::extension!(deno_net,
ops::op_dns_resolve<P>, ops::op_dns_resolve<P>,
ops::op_set_nodelay, ops::op_set_nodelay,
ops::op_set_keepalive, ops::op_set_keepalive,
ops::op_net_listen_vsock<P>,
ops::op_net_accept_vsock,
ops::op_net_connect_vsock<P>,
ops_tls::op_tls_key_null, ops_tls::op_tls_key_null,
ops_tls::op_tls_key_static, ops_tls::op_tls_key_static,

View file

@ -152,6 +152,9 @@ pub enum NetError {
#[class(generic)] #[class(generic)]
#[error("{0}")] #[error("{0}")]
Reunite(tokio::net::tcp::ReuniteError), Reunite(tokio::net::tcp::ReuniteError),
#[class(generic)]
#[error("VSOCK is not supported on this platform")]
VsockUnsupported,
} }
pub(crate) fn accept_err(e: std::io::Error) -> NetError { pub(crate) fn accept_err(e: std::io::Error) -> NetError {
@ -608,6 +611,145 @@ where
net_listen_udp::<NP>(state, addr, reuse_address, loopback) net_listen_udp::<NP>(state, addr, reuse_address, loopback)
} }
#[cfg(unix)]
#[op2(async, stack_trace)]
#[serde]
pub async fn op_net_connect_vsock<NP>(
state: Rc<RefCell<OpState>>,
#[smi] cid: u32,
#[smi] port: u32,
) -> Result<(ResourceId, (u32, u32), (u32, u32)), NetError>
where
NP: NetPermissions + 'static,
{
use tokio_vsock::VsockAddr;
use tokio_vsock::VsockStream;
super::check_unstable(
&state.borrow(),
r#"Deno.connect({ transport: "vsock" })"#,
);
state.borrow_mut().borrow_mut::<NP>().check_vsock(
cid,
port,
"Deno.connect()",
)?;
let addr = VsockAddr::new(cid, port);
let vsock_stream = VsockStream::connect(addr).await?;
let local_addr = vsock_stream.local_addr()?;
let remote_addr = vsock_stream.peer_addr()?;
let rid =
state
.borrow_mut()
.resource_table
.add(crate::io::VsockStreamResource::new(
vsock_stream.into_split(),
));
Ok((
rid,
(local_addr.cid(), local_addr.port()),
(remote_addr.cid(), remote_addr.port()),
))
}
#[cfg(not(unix))]
#[op2]
#[serde]
pub fn op_net_connect_vsock<NP>() -> Result<(), NetError>
where
NP: NetPermissions + 'static,
{
Err(NetError::VsockUnsupported)
}
#[cfg(unix)]
#[op2(stack_trace)]
#[serde]
pub fn op_net_listen_vsock<NP>(
state: &mut OpState,
#[smi] cid: u32,
#[smi] port: u32,
) -> Result<(ResourceId, u32, u32), NetError>
where
NP: NetPermissions + 'static,
{
use tokio_vsock::VsockAddr;
use tokio_vsock::VsockListener;
super::check_unstable(state, r#"Deno.listen({ transport: "vsock" })"#);
state
.borrow_mut::<NP>()
.check_vsock(cid, port, "Deno.listen()")?;
let addr = VsockAddr::new(cid, port);
let listener = VsockListener::bind(addr)?;
let local_addr = listener.local_addr()?;
let listener_resource = NetworkListenerResource::new(listener);
let rid = state.resource_table.add(listener_resource);
Ok((rid, local_addr.cid(), local_addr.port()))
}
#[cfg(not(unix))]
#[op2]
#[serde]
pub fn op_net_listen_vsock<NP>() -> Result<(), NetError>
where
NP: NetPermissions + 'static,
{
Err(NetError::VsockUnsupported)
}
#[cfg(unix)]
#[op2(async)]
#[serde]
pub async fn op_net_accept_vsock(
state: Rc<RefCell<OpState>>,
#[smi] rid: ResourceId,
) -> Result<(ResourceId, (u32, u32), (u32, u32)), NetError> {
use tokio_vsock::VsockListener;
let resource = state
.borrow()
.resource_table
.get::<NetworkListenerResource<VsockListener>>(rid)
.map_err(|_| NetError::ListenerClosed)?;
let listener = RcRef::map(&resource, |r| &r.listener)
.try_borrow_mut()
.ok_or_else(|| NetError::AcceptTaskOngoing)?;
let cancel = RcRef::map(resource, |r| &r.cancel);
let (vsock_stream, _socket_addr) = listener
.accept()
.try_or_cancel(cancel)
.await
.map_err(accept_err)?;
let local_addr = vsock_stream.local_addr()?;
let remote_addr = vsock_stream.peer_addr()?;
let mut state = state.borrow_mut();
let rid = state
.resource_table
.add(crate::io::VsockStreamResource::new(
vsock_stream.into_split(),
));
Ok((
rid,
(local_addr.cid(), local_addr.port()),
(remote_addr.cid(), remote_addr.port()),
))
}
#[cfg(not(unix))]
#[op2]
#[serde]
pub fn op_net_accept_vsock() -> Result<(), NetError> {
Err(NetError::VsockUnsupported)
}
#[derive(Serialize, Eq, PartialEq, Debug)] #[derive(Serialize, Eq, PartialEq, Debug)]
#[serde(untagged)] #[serde(untagged)]
pub enum DnsReturnRecord { pub enum DnsReturnRecord {
@ -1148,6 +1290,15 @@ mod tests {
) -> Result<Cow<'a, Path>, PermissionCheckError> { ) -> Result<Cow<'a, Path>, PermissionCheckError> {
Ok(Cow::Borrowed(p)) Ok(Cow::Borrowed(p))
} }
fn check_vsock(
&mut self,
_cid: u32,
_port: u32,
_api_name: &str,
) -> Result<(), PermissionCheckError> {
Ok(())
}
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[tokio::test(flavor = "multi_thread", worker_threads = 1)]

View file

@ -280,6 +280,14 @@ network_stream!(
tokio::net::UnixListener, tokio::net::UnixListener,
tokio::net::unix::SocketAddr, tokio::net::unix::SocketAddr,
crate::io::UnixStreamResource crate::io::UnixStreamResource
],
[
Vsock,
vsock,
tokio_vsock::VsockStream,
tokio_vsock::VsockListener,
tokio_vsock::VsockAddr,
crate::io::VsockStreamResource
] ]
); );
@ -307,6 +315,8 @@ pub enum NetworkStreamAddress {
Ip(std::net::SocketAddr), Ip(std::net::SocketAddr),
#[cfg(unix)] #[cfg(unix)]
Unix(tokio::net::unix::SocketAddr), Unix(tokio::net::unix::SocketAddr),
#[cfg(unix)]
Vsock(tokio_vsock::VsockAddr),
} }
impl From<std::net::SocketAddr> for NetworkStreamAddress { impl From<std::net::SocketAddr> for NetworkStreamAddress {
@ -322,6 +332,13 @@ impl From<tokio::net::unix::SocketAddr> for NetworkStreamAddress {
} }
} }
#[cfg(unix)]
impl From<tokio_vsock::VsockAddr> for NetworkStreamAddress {
fn from(value: tokio_vsock::VsockAddr) -> Self {
NetworkStreamAddress::Vsock(value)
}
}
#[derive(Debug, thiserror::Error, deno_error::JsError)] #[derive(Debug, thiserror::Error, deno_error::JsError)]
pub enum TakeNetworkStreamError { pub enum TakeNetworkStreamError {
#[class("Busy")] #[class("Busy")]
@ -334,6 +351,10 @@ pub enum TakeNetworkStreamError {
#[class("Busy")] #[class("Busy")]
#[error("Unix socket is currently in use")] #[error("Unix socket is currently in use")]
UnixBusy, UnixBusy,
#[cfg(unix)]
#[class("Busy")]
#[error("Vsock socket is currently in use")]
VsockBusy,
#[class(generic)] #[class(generic)]
#[error(transparent)] #[error(transparent)]
ReuniteTcp(#[from] tokio::net::tcp::ReuniteError), ReuniteTcp(#[from] tokio::net::tcp::ReuniteError),
@ -341,6 +362,10 @@ pub enum TakeNetworkStreamError {
#[class(generic)] #[class(generic)]
#[error(transparent)] #[error(transparent)]
ReuniteUnix(#[from] tokio::net::unix::ReuniteError), ReuniteUnix(#[from] tokio::net::unix::ReuniteError),
#[cfg(unix)]
#[class(generic)]
#[error("Cannot reunite halves from different streams")]
ReuniteVsock,
#[class(inherit)] #[class(inherit)]
#[error(transparent)] #[error(transparent)]
Resource(deno_core::error::ResourceError), Resource(deno_core::error::ResourceError),
@ -388,6 +413,21 @@ pub fn take_network_stream_resource(
return Ok(NetworkStream::Unix(unix_stream)); return Ok(NetworkStream::Unix(unix_stream));
} }
#[cfg(unix)]
if let Ok(resource_rc) =
resource_table.take::<crate::io::VsockStreamResource>(stream_rid)
{
// This Vsock socket might be used somewhere else.
let resource = Rc::try_unwrap(resource_rc)
.map_err(|_| TakeNetworkStreamError::VsockBusy)?;
let (read_half, write_half) = resource.into_inner();
if !read_half.is_pair_of(&write_half) {
return Err(TakeNetworkStreamError::ReuniteVsock);
}
let vsock_stream = read_half.unsplit(write_half);
return Ok(NetworkStream::Vsock(vsock_stream));
}
Err(TakeNetworkStreamError::Resource( Err(TakeNetworkStreamError::Resource(
ResourceError::BadResourceId, ResourceError::BadResourceId,
)) ))

View file

@ -817,6 +817,7 @@ pub struct WriteDescriptor(pub PathBuf);
pub enum Host { pub enum Host {
Fqdn(FQDN), Fqdn(FQDN),
Ip(IpAddr), Ip(IpAddr),
Vsock(u32),
} }
#[derive(Debug, thiserror::Error, deno_error::JsError)] #[derive(Debug, thiserror::Error, deno_error::JsError)]
@ -881,7 +882,7 @@ impl Host {
} }
#[derive(Clone, Eq, PartialEq, Hash, Debug)] #[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub struct NetDescriptor(pub Host, pub Option<u16>); pub struct NetDescriptor(pub Host, pub Option<u32>);
impl QueryDescriptor for NetDescriptor { impl QueryDescriptor for NetDescriptor {
type AllowDesc = NetDescriptor; type AllowDesc = NetDescriptor;
@ -953,6 +954,8 @@ pub enum NetDescriptorParseError {
Ipv6MissingSquareBrackets(String), Ipv6MissingSquareBrackets(String),
#[error("{0}")] #[error("{0}")]
Host(#[from] HostParseError), Host(#[from] HostParseError),
#[error("invalid vsock: '{0}'")]
InvalidVsock(String),
} }
#[derive(Debug, thiserror::Error, deno_error::JsError)] #[derive(Debug, thiserror::Error, deno_error::JsError)]
@ -967,6 +970,24 @@ pub enum NetDescriptorFromUrlParseError {
impl NetDescriptor { impl NetDescriptor {
pub fn parse(hostname: &str) -> Result<Self, NetDescriptorParseError> { pub fn parse(hostname: &str) -> Result<Self, NetDescriptorParseError> {
#[cfg(unix)]
if let Some(vsock) = hostname.strip_prefix("vsock:") {
let mut split = vsock.split(':');
let Some(cid) = split.next().and_then(|c| {
if c == "-1" {
Some(u32::MAX)
} else {
c.parse().ok()
}
}) else {
return Err(NetDescriptorParseError::InvalidVsock(hostname.into()));
};
let Some(port) = split.next().and_then(|p| p.parse().ok()) else {
return Err(NetDescriptorParseError::InvalidVsock(hostname.into()));
};
return Ok(NetDescriptor(Host::Vsock(cid), Some(port)));
}
if hostname.starts_with("http://") || hostname.starts_with("https://") { if hostname.starts_with("http://") || hostname.starts_with("https://") {
return Err(NetDescriptorParseError::Url(hostname.to_string())); return Err(NetDescriptorParseError::Url(hostname.to_string()));
} }
@ -995,7 +1016,10 @@ impl NetDescriptor {
hostname.to_string(), hostname.to_string(),
)); ));
}; };
return Ok(NetDescriptor(Host::Ip(IpAddr::V6(ip)), port)); return Ok(NetDescriptor(
Host::Ip(IpAddr::V6(ip)),
port.map(Into::into),
));
} else { } else {
return Err(NetDescriptorParseError::InvalidHost(hostname.to_string())); return Err(NetDescriptorParseError::InvalidHost(hostname.to_string()));
} }
@ -1032,7 +1056,7 @@ impl NetDescriptor {
Some(port) Some(port)
}; };
Ok(NetDescriptor(host, port)) Ok(NetDescriptor(host, port.map(Into::into)))
} }
pub fn from_url(url: &Url) -> Result<Self, NetDescriptorFromUrlParseError> { pub fn from_url(url: &Url) -> Result<Self, NetDescriptorFromUrlParseError> {
@ -1041,7 +1065,14 @@ impl NetDescriptor {
})?; })?;
let host = Host::parse(host)?; let host = Host::parse(host)?;
let port = url.port_or_known_default(); let port = url.port_or_known_default();
Ok(NetDescriptor(host, port)) Ok(NetDescriptor(host, port.map(Into::into)))
}
pub fn from_vsock(
cid: u32,
port: u32,
) -> Result<Self, NetDescriptorParseError> {
Ok(NetDescriptor(Host::Vsock(cid), Some(port)))
} }
} }
@ -1051,6 +1082,7 @@ impl fmt::Display for NetDescriptor {
Host::Fqdn(fqdn) => write!(f, "{fqdn}"), Host::Fqdn(fqdn) => write!(f, "{fqdn}"),
Host::Ip(IpAddr::V4(ip)) => write!(f, "{ip}"), Host::Ip(IpAddr::V4(ip)) => write!(f, "{ip}"),
Host::Ip(IpAddr::V6(ip)) => write!(f, "[{ip}]"), Host::Ip(IpAddr::V6(ip)) => write!(f, "[{ip}]"),
Host::Vsock(cid) => write!(f, "vsock:{cid}"),
}?; }?;
if let Some(port) = self.1 { if let Some(port) = self.1 {
write!(f, ":{}", port)?; write!(f, ":{}", port)?;
@ -2933,11 +2965,27 @@ impl PermissionsContainer {
let inner = &mut inner.net; let inner = &mut inner.net;
skip_check_if_is_permission_fully_granted!(inner); skip_check_if_is_permission_fully_granted!(inner);
let hostname = Host::parse(host.0.as_ref())?; let hostname = Host::parse(host.0.as_ref())?;
let descriptor = NetDescriptor(hostname, host.1); let descriptor = NetDescriptor(hostname, host.1.map(Into::into));
inner.check(&descriptor, Some(api_name))?; inner.check(&descriptor, Some(api_name))?;
Ok(()) Ok(())
} }
#[inline(always)]
pub fn check_net_vsock(
&mut self,
cid: u32,
port: u32,
api_name: &str,
) -> Result<(), PermissionCheckError> {
let mut inner = self.inner.lock();
if inner.net.is_allow_all() {
return Ok(());
}
let desc = NetDescriptor(Host::Vsock(cid), Some(port));
inner.net.check(&desc, Some(api_name))?;
Ok(())
}
#[inline(always)] #[inline(always)]
pub fn check_ffi( pub fn check_ffi(
&mut self, &mut self,

View file

@ -150,6 +150,15 @@ impl deno_net::NetPermissions for Permissions {
) -> Result<Cow<'a, Path>, PermissionCheckError> { ) -> Result<Cow<'a, Path>, PermissionCheckError> {
unreachable!("snapshotting!") unreachable!("snapshotting!")
} }
fn check_vsock(
&mut self,
_cid: u32,
_port: u32,
_api_name: &str,
) -> Result<(), PermissionCheckError> {
unreachable!("snapshotting!")
}
} }
impl deno_fs::FsPermissions for Permissions { impl deno_fs::FsPermissions for Permissions {

View file

@ -4045,6 +4045,58 @@ Deno.test(
}, },
); );
Deno.test(
{
ignore: Deno.build.os !== "linux",
permissions: { run: true, net: true },
},
async function httpServerVsockSocket() {
const { promise, resolve } = Promise.withResolvers<Deno.VsockAddr>();
const ac = new AbortController();
await using server = Deno.serve(
{
signal: ac.signal,
cid: -1,
port: 8000,
onListen(info) {
resolve(info);
},
onError: createOnErrorCb(ac),
},
(_req, { remoteAddr }) => {
assertEquals(remoteAddr.transport, "vsock");
assertEquals(remoteAddr.cid, 1);
assertEquals(remoteAddr.port, conn.localAddr.port);
return new Response("hello world!");
},
);
assertEquals((await promise).cid, 4294967295);
assertEquals((await promise).port, 8000);
const conn = await Deno.connect({
transport: "vsock",
cid: 1,
port: 8000,
});
await conn.write(
new TextEncoder().encode("GET / HTTP/1.1\r\nhost: example.com\r\n\r\n"),
);
const data = new Uint8Array(512);
const n = await conn.read(data);
const body = new TextDecoder().decode(data.subarray(0, n!));
assertEquals(
"hello world!",
body.split("\r\n").at(-1),
);
await conn.close();
ac.abort();
await server.finished;
},
);
// serve Handler must return Response class or promise that resolves Response class // serve Handler must return Response class or promise that resolves Response class
Deno.test( Deno.test(
{ permissions: { net: true, run: true } }, { permissions: { net: true, run: true } },