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",
"thiserror 2.0.12",
"tokio",
"tokio-vsock",
"url",
"web-transport-proto",
]
@ -5588,6 +5589,7 @@ dependencies = [
"cfg-if",
"cfg_aliases",
"libc",
"memoffset",
]
[[package]]
@ -8696,6 +8698,19 @@ dependencies = [
"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]]
name = "toml"
version = "0.5.11"
@ -9199,6 +9214,16 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "vte"
version = "0.11.1"

View file

@ -369,6 +369,7 @@ syn = { version = "2", features = ["full", "extra-traits"] }
# unix deps
nix = "=0.27.1"
tokio-vsock = "0.7"
# windows deps
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()
|| host_and_port.parse::<IpAddr>().is_ok()
|| host_and_port.parse::<BarePort>().is_ok()
|| NetDescriptor::parse(host_and_port).is_ok()
{
Ok(host_and_port.to_string())
} else {

View file

@ -5160,6 +5160,23 @@ declare namespace Deno {
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
*/
@ -5261,6 +5278,56 @@ declare namespace Deno {
options: ServeUnixOptions,
handler: ServeHandler<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.
*
* You can specify an object with a port and hostname option, which is the
@ -5348,6 +5415,34 @@ declare namespace Deno {
export function serve(
options: ServeUnixOptions & ServeInit<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.
*
* 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 */
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.
*
@ -67,6 +74,12 @@ declare namespace Deno {
*/
export type UnixListener = Listener<UnixConn, UnixAddr>;
/** Specialized listener that accepts Vsock connections.
*
* @category Network
*/
export type VsockListener = Listener<VsockConn, VsockAddr>;
/** @category Network */
export interface Conn<A extends Addr = Addr> extends Disposable {
/** Read the incoming data from the connection into an array buffer (`p`).
@ -223,6 +236,32 @@ declare namespace Deno {
options: UnixListenOptions & { transport: "unix" },
): 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
* `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
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 */
export interface ConnectTlsOptions {
/** The port to connect to. */

View file

@ -46,6 +46,7 @@ const {
TypedArrayPrototypeGetSymbolToStringTag,
Uint8Array,
Promise,
Number,
} = primordials;
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);
}
if (transport === "vsock") {
return {
transport,
cid: Number(this.#methodAndUri[3]),
port: this.#methodAndUri[4],
};
}
return {
transport: "tcp",
hostname: this.#methodAndUri[3],
@ -769,6 +777,7 @@ function serve(arg1, arg2) {
const wantsHttps = hasTlsKeyPairOptions(options);
const wantsUnix = ObjectHasOwn(options, "path");
const wantsVsock = ObjectHasOwn(options, "cid");
const signal = options.signal;
const onError = options.onError ??
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 = {
hostname: options.hostname ?? "0.0.0.0",
port: options.port ?? 8000,

View file

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

View file

@ -12,14 +12,17 @@ import {
op_dns_resolve,
op_net_accept_tcp,
op_net_accept_unix,
op_net_accept_vsock,
op_net_connect_tcp,
op_net_connect_unix,
op_net_connect_vsock,
op_net_join_multi_v4_udp,
op_net_join_multi_v6_udp,
op_net_leave_multi_v4_udp,
op_net_leave_multi_v6_udp,
op_net_listen_tcp,
op_net_listen_unix,
op_net_listen_vsock,
op_net_recv_udp,
op_net_recv_unixpacket,
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 {
#rid = 0;
#addr = null;
@ -278,6 +295,9 @@ class Listener {
case "unix":
promise = op_net_accept_unix(this.#rid);
break;
case "vsock":
promise = op_net_accept_vsock(this.#rid);
break;
default:
throw new Error(`Unsupported transport: ${this.addr.transport}`);
}
@ -285,18 +305,25 @@ class Listener {
if (this.#unref) core.unrefOpPromise(promise);
const { 0: rid, 1: localAddr, 2: remoteAddr, 3: fd } = await promise;
this.#promise = null;
if (this.addr.transport == "tcp") {
localAddr.transport = "tcp";
remoteAddr.transport = "tcp";
return new TcpConn(rid, remoteAddr, localAddr, fd);
} else if (this.addr.transport == "unix") {
return new UnixConn(
rid,
{ transport: "unix", path: remoteAddr },
{ transport: "unix", path: localAddr },
);
} else {
throw new Error("unreachable");
switch (this.addr.transport) {
case "tcp":
localAddr.transport = "tcp";
remoteAddr.transport = "tcp";
return new TcpConn(rid, remoteAddr, localAddr, fd);
case "unix":
return new UnixConn(
rid,
{ transport: "unix", path: remoteAddr },
{ transport: "unix", path: localAddr },
);
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");
}
}
@ -528,6 +555,18 @@ function listen(args) {
};
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:
throw new TypeError(`Unsupported transport: '${transport}'`);
}
@ -605,6 +644,15 @@ async function connect(args) {
{ 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:
throw new TypeError(`Unsupported transport: '${transport}'`);
}
@ -622,4 +670,5 @@ export {
UnixConn,
UpgradedConn,
validatePort,
VsockConn,
};

View file

@ -30,3 +30,6 @@ thiserror.workspace = true
tokio.workspace = true
url.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();
}
}
#[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,
api_name: &str,
) -> 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 {
@ -87,6 +94,18 @@ impl NetPermissions for deno_permissions::PermissionsContainer {
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.
@ -139,6 +158,9 @@ deno_core::extension!(deno_net,
ops::op_dns_resolve<P>,
ops::op_set_nodelay,
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_static,

View file

@ -152,6 +152,9 @@ pub enum NetError {
#[class(generic)]
#[error("{0}")]
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 {
@ -608,6 +611,145 @@ where
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)]
#[serde(untagged)]
pub enum DnsReturnRecord {
@ -1148,6 +1290,15 @@ mod tests {
) -> Result<Cow<'a, Path>, PermissionCheckError> {
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)]

View file

@ -280,6 +280,14 @@ network_stream!(
tokio::net::UnixListener,
tokio::net::unix::SocketAddr,
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),
#[cfg(unix)]
Unix(tokio::net::unix::SocketAddr),
#[cfg(unix)]
Vsock(tokio_vsock::VsockAddr),
}
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)]
pub enum TakeNetworkStreamError {
#[class("Busy")]
@ -334,6 +351,10 @@ pub enum TakeNetworkStreamError {
#[class("Busy")]
#[error("Unix socket is currently in use")]
UnixBusy,
#[cfg(unix)]
#[class("Busy")]
#[error("Vsock socket is currently in use")]
VsockBusy,
#[class(generic)]
#[error(transparent)]
ReuniteTcp(#[from] tokio::net::tcp::ReuniteError),
@ -341,6 +362,10 @@ pub enum TakeNetworkStreamError {
#[class(generic)]
#[error(transparent)]
ReuniteUnix(#[from] tokio::net::unix::ReuniteError),
#[cfg(unix)]
#[class(generic)]
#[error("Cannot reunite halves from different streams")]
ReuniteVsock,
#[class(inherit)]
#[error(transparent)]
Resource(deno_core::error::ResourceError),
@ -388,6 +413,21 @@ pub fn take_network_stream_resource(
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(
ResourceError::BadResourceId,
))

View file

@ -817,6 +817,7 @@ pub struct WriteDescriptor(pub PathBuf);
pub enum Host {
Fqdn(FQDN),
Ip(IpAddr),
Vsock(u32),
}
#[derive(Debug, thiserror::Error, deno_error::JsError)]
@ -881,7 +882,7 @@ impl Host {
}
#[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 {
type AllowDesc = NetDescriptor;
@ -953,6 +954,8 @@ pub enum NetDescriptorParseError {
Ipv6MissingSquareBrackets(String),
#[error("{0}")]
Host(#[from] HostParseError),
#[error("invalid vsock: '{0}'")]
InvalidVsock(String),
}
#[derive(Debug, thiserror::Error, deno_error::JsError)]
@ -967,6 +970,24 @@ pub enum NetDescriptorFromUrlParseError {
impl NetDescriptor {
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://") {
return Err(NetDescriptorParseError::Url(hostname.to_string()));
}
@ -995,7 +1016,10 @@ impl NetDescriptor {
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 {
return Err(NetDescriptorParseError::InvalidHost(hostname.to_string()));
}
@ -1032,7 +1056,7 @@ impl NetDescriptor {
Some(port)
};
Ok(NetDescriptor(host, port))
Ok(NetDescriptor(host, port.map(Into::into)))
}
pub fn from_url(url: &Url) -> Result<Self, NetDescriptorFromUrlParseError> {
@ -1041,7 +1065,14 @@ impl NetDescriptor {
})?;
let host = Host::parse(host)?;
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::Ip(IpAddr::V4(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 {
write!(f, ":{}", port)?;
@ -2933,11 +2965,27 @@ impl PermissionsContainer {
let inner = &mut inner.net;
skip_check_if_is_permission_fully_granted!(inner);
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))?;
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)]
pub fn check_ffi(
&mut self,

View file

@ -150,6 +150,15 @@ impl deno_net::NetPermissions for Permissions {
) -> Result<Cow<'a, Path>, PermissionCheckError> {
unreachable!("snapshotting!")
}
fn check_vsock(
&mut self,
_cid: u32,
_port: u32,
_api_name: &str,
) -> Result<(), PermissionCheckError> {
unreachable!("snapshotting!")
}
}
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
Deno.test(
{ permissions: { net: true, run: true } },