1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-01-21 04:52:26 -05:00

refactor(quic): introduce endpoint, 0rtt, cleanup (#27444)

A QUIC endpoint is a UDP socket which multiplexes QUIC sessions, which
may be initiated in either direction. This PR exposes endpoints and
moves things around as needed.

Now that endpoints can be reused between client connections, we have a
way to share tls tickets between them and allow 0rtt. This interface
currently works by conditionally returning a promise.

Also cleaned up the rust op names, fixed some lingering problems in the
data transmission, and switched to explicit error types.
This commit is contained in:
snek 2025-01-06 15:24:59 +01:00 committed by GitHub
parent 4b35ba6b13
commit ccd375802a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1128 additions and 596 deletions

View file

@ -1,33 +1,42 @@
// Copyright 2018-2025 the Deno authors. MIT license.
import { core, primordials } from "ext:core/mod.js";
import {
op_quic_accept,
op_quic_accept_bi,
op_quic_accept_incoming,
op_quic_accept_uni,
op_quic_close_connection,
op_quic_close_endpoint,
op_quic_connect,
op_quic_connecting_0rtt,
op_quic_connecting_1rtt,
op_quic_connection_accept_bi,
op_quic_connection_accept_uni,
op_quic_connection_close,
op_quic_connection_closed,
op_quic_connection_get_max_datagram_size,
op_quic_connection_get_protocol,
op_quic_connection_get_remote_addr,
op_quic_connection_get_server_name,
op_quic_connection_handshake,
op_quic_connection_open_bi,
op_quic_connection_open_uni,
op_quic_connection_read_datagram,
op_quic_connection_send_datagram,
op_quic_endpoint_close,
op_quic_endpoint_connect,
op_quic_endpoint_create,
op_quic_endpoint_get_addr,
op_quic_get_send_stream_priority,
op_quic_endpoint_listen,
op_quic_incoming_accept,
op_quic_incoming_accept_0rtt,
op_quic_incoming_ignore,
op_quic_incoming_local_ip,
op_quic_incoming_refuse,
op_quic_incoming_remote_addr,
op_quic_incoming_remote_addr_validated,
op_quic_listen,
op_quic_max_datagram_size,
op_quic_open_bi,
op_quic_open_uni,
op_quic_read_datagram,
op_quic_send_datagram,
op_quic_set_send_stream_priority,
op_quic_listener_accept,
op_quic_listener_stop,
op_quic_recv_stream_get_id,
op_quic_send_stream_get_id,
op_quic_send_stream_get_priority,
op_quic_send_stream_set_priority,
} from "ext:core/ops";
import {
getReadableStreamResourceBacking,
getWritableStreamResourceBacking,
ReadableStream,
readableStreamForRid,
@ -39,29 +48,297 @@ const {
BadResourcePrototype,
} = core;
const {
Uint8Array,
TypedArrayPrototypeSubarray,
ObjectPrototypeIsPrototypeOf,
PromisePrototypeThen,
Symbol,
SymbolAsyncIterator,
SafePromisePrototypeFinally,
ObjectPrototypeIsPrototypeOf,
} = primordials;
let getEndpointResource;
function transportOptions({
keepAliveInterval,
maxIdleTimeout,
maxConcurrentBidirectionalStreams,
maxConcurrentUnidirectionalStreams,
preferredAddressV4,
preferredAddressV6,
congestionControl,
}) {
return {
keepAliveInterval,
maxIdleTimeout,
maxConcurrentBidirectionalStreams,
maxConcurrentUnidirectionalStreams,
preferredAddressV4,
preferredAddressV6,
congestionControl,
};
}
const kRid = Symbol("rid");
class QuicEndpoint {
#endpoint;
constructor(
{ hostname = "::", port = 0, [kRid]: rid } = { __proto__: null },
) {
this.#endpoint = rid ?? op_quic_endpoint_create({ hostname, port }, true);
}
get addr() {
return op_quic_endpoint_get_addr(this.#endpoint);
}
listen(options) {
const keyPair = loadTlsKeyPair("Deno.QuicEndpoint.listen", {
cert: options.cert,
key: options.key,
});
const listener = op_quic_endpoint_listen(
this.#endpoint,
{ alpnProtocols: options.alpnProtocols },
transportOptions(options),
keyPair,
);
return new QuicListener(listener, this);
}
close({ closeCode = 0, reason = "" } = { __proto__: null }) {
op_quic_endpoint_close(this.#endpoint, closeCode, reason);
}
static {
getEndpointResource = (e) => e.#endpoint;
}
}
class QuicListener {
#listener;
#endpoint;
constructor(listener, endpoint) {
this.#listener = listener;
this.#endpoint = endpoint;
}
get endpoint() {
return this.#endpoint;
}
async incoming() {
const incoming = await op_quic_listener_accept(this.#listener);
return new QuicIncoming(incoming, this.#endpoint);
}
async accept() {
const incoming = await this.incoming();
const connection = await incoming.accept();
return connection;
}
async next() {
try {
const connection = await this.accept();
return { value: connection, done: false };
} catch (error) {
if (ObjectPrototypeIsPrototypeOf(BadResourcePrototype, error)) {
return { value: undefined, done: true };
}
throw error;
}
}
[SymbolAsyncIterator]() {
return this;
}
stop() {
op_quic_listener_stop(this.#listener);
}
}
class QuicIncoming {
#incoming;
#endpoint;
constructor(incoming, endpoint) {
this.#incoming = incoming;
this.#endpoint = endpoint;
}
get localIp() {
return op_quic_incoming_local_ip(this.#incoming);
}
get remoteAddr() {
return op_quic_incoming_remote_addr(this.#incoming);
}
get remoteAddressValidated() {
return op_quic_incoming_remote_addr_validated(this.#incoming);
}
accept(options) {
const tOptions = options ? transportOptions(options) : null;
if (options?.zeroRtt) {
const conn = op_quic_incoming_accept_0rtt(
this.#incoming,
tOptions,
);
return new QuicConn(conn, this.#endpoint);
}
return PromisePrototypeThen(
op_quic_incoming_accept(this.#incoming, tOptions),
(conn) => new QuicConn(conn, this.#endpoint),
);
}
refuse() {
op_quic_incoming_refuse(this.#incoming);
}
ignore() {
op_quic_incoming_ignore(this.#incoming);
}
}
class QuicConn {
#resource;
#bidiStream = null;
#uniStream = null;
#closed;
#handshake;
#endpoint;
constructor(resource, endpoint) {
this.#resource = resource;
this.#endpoint = endpoint;
this.#closed = op_quic_connection_closed(this.#resource);
core.unrefOpPromise(this.#closed);
}
get endpoint() {
return this.#endpoint;
}
get protocol() {
return op_quic_connection_get_protocol(this.#resource);
}
get remoteAddr() {
return op_quic_connection_get_remote_addr(this.#resource);
}
get serverName() {
return op_quic_connection_get_server_name(this.#resource);
}
async createBidirectionalStream(
{ sendOrder, waitUntilAvailable } = { __proto__: null },
) {
const { 0: txRid, 1: rxRid } = await op_quic_connection_open_bi(
this.#resource,
waitUntilAvailable ?? false,
);
if (sendOrder !== null && sendOrder !== undefined) {
op_quic_send_stream_set_priority(txRid, sendOrder);
}
return new QuicBidirectionalStream(txRid, rxRid, this.#closed);
}
async createUnidirectionalStream(
{ sendOrder, waitUntilAvailable } = { __proto__: null },
) {
const rid = await op_quic_connection_open_uni(
this.#resource,
waitUntilAvailable ?? false,
);
if (sendOrder !== null && sendOrder !== undefined) {
op_quic_send_stream_set_priority(rid, sendOrder);
}
return writableStream(rid, this.#closed);
}
get incomingBidirectionalStreams() {
if (this.#bidiStream === null) {
this.#bidiStream = ReadableStream.from(
bidiStream(this.#resource, this.#closed),
);
}
return this.#bidiStream;
}
get incomingUnidirectionalStreams() {
if (this.#uniStream === null) {
this.#uniStream = ReadableStream.from(
uniStream(this.#resource, this.#closed),
);
}
return this.#uniStream;
}
get maxDatagramSize() {
return op_quic_connection_get_max_datagram_size(this.#resource);
}
async readDatagram() {
const buffer = await op_quic_connection_read_datagram(this.#resource);
return buffer;
}
async sendDatagram(data) {
await op_quic_connection_send_datagram(this.#resource, data);
}
get handshake() {
if (!this.#handshake) {
this.#handshake = op_quic_connection_handshake(this.#resource);
}
return this.#handshake;
}
get closed() {
core.refOpPromise(this.#closed);
return this.#closed;
}
close({ closeCode = 0, reason = "" } = { __proto__: null }) {
op_quic_connection_close(this.#resource, closeCode, reason);
}
}
class QuicSendStream extends WritableStream {
get sendOrder() {
return op_quic_get_send_stream_priority(
return op_quic_send_stream_get_priority(
getWritableStreamResourceBacking(this).rid,
);
}
set sendOrder(p) {
op_quic_set_send_stream_priority(
op_quic_send_stream_set_priority(
getWritableStreamResourceBacking(this).rid,
p,
);
}
get id() {
return op_quic_send_stream_get_id(
getWritableStreamResourceBacking(this).rid,
);
}
}
class QuicReceiveStream extends ReadableStream {}
class QuicReceiveStream extends ReadableStream {
get id() {
return op_quic_recv_stream_get_id(
getReadableStreamResourceBacking(this).rid,
);
}
}
function readableStream(rid, closed) {
// stream can be indirectly closed by closing connection.
@ -100,7 +377,7 @@ class QuicBidirectionalStream {
async function* bidiStream(conn, closed) {
try {
while (true) {
const r = await op_quic_accept_bi(conn);
const r = await op_quic_connection_accept_bi(conn);
yield new QuicBidirectionalStream(r[0], r[1], closed);
}
} catch (error) {
@ -114,7 +391,7 @@ async function* bidiStream(conn, closed) {
async function* uniStream(conn, closed) {
try {
while (true) {
const uniRid = await op_quic_accept_uni(conn);
const uniRid = await op_quic_connection_accept_uni(conn);
yield readableStream(uniRid, closed);
}
} catch (error) {
@ -125,241 +402,48 @@ async function* uniStream(conn, closed) {
}
}
class QuicConn {
#resource;
#bidiStream = null;
#uniStream = null;
#closed;
constructor(resource) {
this.#resource = resource;
this.#closed = op_quic_connection_closed(this.#resource);
core.unrefOpPromise(this.#closed);
}
get protocol() {
return op_quic_connection_get_protocol(this.#resource);
}
get remoteAddr() {
return op_quic_connection_get_remote_addr(this.#resource);
}
async createBidirectionalStream(
{ sendOrder, waitUntilAvailable } = { __proto__: null },
) {
const { 0: txRid, 1: rxRid } = await op_quic_open_bi(
this.#resource,
waitUntilAvailable ?? false,
);
if (sendOrder !== null && sendOrder !== undefined) {
op_quic_set_send_stream_priority(txRid, sendOrder);
}
return new QuicBidirectionalStream(txRid, rxRid, this.#closed);
}
async createUnidirectionalStream(
{ sendOrder, waitUntilAvailable } = { __proto__: null },
) {
const rid = await op_quic_open_uni(
this.#resource,
waitUntilAvailable ?? false,
);
if (sendOrder !== null && sendOrder !== undefined) {
op_quic_set_send_stream_priority(rid, sendOrder);
}
return writableStream(rid, this.#closed);
}
get incomingBidirectionalStreams() {
if (this.#bidiStream === null) {
this.#bidiStream = ReadableStream.from(
bidiStream(this.#resource, this.#closed),
);
}
return this.#bidiStream;
}
get incomingUnidirectionalStreams() {
if (this.#uniStream === null) {
this.#uniStream = ReadableStream.from(
uniStream(this.#resource, this.#closed),
);
}
return this.#uniStream;
}
get maxDatagramSize() {
return op_quic_max_datagram_size(this.#resource);
}
async readDatagram(p) {
const view = p || new Uint8Array(this.maxDatagramSize);
const nread = await op_quic_read_datagram(this.#resource, view);
return TypedArrayPrototypeSubarray(view, 0, nread);
}
async sendDatagram(data) {
await op_quic_send_datagram(this.#resource, data);
}
get closed() {
core.refOpPromise(this.#closed);
return this.#closed;
}
close({ closeCode, reason }) {
op_quic_close_connection(this.#resource, closeCode, reason);
}
}
class QuicIncoming {
#incoming;
constructor(incoming) {
this.#incoming = incoming;
}
get localIp() {
return op_quic_incoming_local_ip(this.#incoming);
}
get remoteAddr() {
return op_quic_incoming_remote_addr(this.#incoming);
}
get remoteAddressValidated() {
return op_quic_incoming_remote_addr_validated(this.#incoming);
}
async accept() {
const conn = await op_quic_incoming_accept(this.#incoming);
return new QuicConn(conn);
}
refuse() {
op_quic_incoming_refuse(this.#incoming);
}
ignore() {
op_quic_incoming_ignore(this.#incoming);
}
}
class QuicListener {
#endpoint;
constructor(endpoint) {
this.#endpoint = endpoint;
}
get addr() {
return op_quic_endpoint_get_addr(this.#endpoint);
}
async accept() {
const conn = await op_quic_accept(this.#endpoint);
return new QuicConn(conn);
}
async incoming() {
const incoming = await op_quic_accept_incoming(this.#endpoint);
return new QuicIncoming(incoming);
}
async next() {
let conn;
try {
conn = await this.accept();
} catch (error) {
if (ObjectPrototypeIsPrototypeOf(BadResourcePrototype, error)) {
return { value: undefined, done: true };
}
throw error;
}
return { value: conn, done: false };
}
[SymbolAsyncIterator]() {
return this;
}
close({ closeCode, reason }) {
op_quic_close_endpoint(this.#endpoint, closeCode, reason);
}
}
async function listenQuic(
function connectQuic(options) {
const endpoint = options.endpoint ??
new QuicEndpoint({
[kRid]: op_quic_endpoint_create({ hostname: "::", port: 0 }, 0, false),
});
const keyPair = loadTlsKeyPair("Deno.connectQuic", {
cert: options.cert,
key: options.key,
});
const connecting = op_quic_endpoint_connect(
getEndpointResource(endpoint),
{
hostname,
port,
cert,
key,
alpnProtocols,
keepAliveInterval,
maxIdleTimeout,
maxConcurrentBidirectionalStreams,
maxConcurrentUnidirectionalStreams,
addr: {
hostname: options.hostname,
port: options.port,
},
) {
hostname = hostname || "0.0.0.0";
const keyPair = loadTlsKeyPair("Deno.listenQuic", { cert, key });
const endpoint = await op_quic_listen(
{ hostname, port },
{ alpnProtocols },
{
keepAliveInterval,
maxIdleTimeout,
maxConcurrentBidirectionalStreams,
maxConcurrentUnidirectionalStreams,
caCerts: options.caCerts,
alpnProtocols: options.alpnProtocols,
serverName: options.serverName,
},
transportOptions(options),
keyPair,
);
return new QuicListener(endpoint);
}
async function connectQuic(
{
hostname,
port,
serverName,
caCerts,
cert,
key,
alpnProtocols,
keepAliveInterval,
maxIdleTimeout,
maxConcurrentBidirectionalStreams,
maxConcurrentUnidirectionalStreams,
congestionControl,
},
) {
const keyPair = loadTlsKeyPair("Deno.connectQuic", { cert, key });
const conn = await op_quic_connect(
{ hostname, port },
{
caCerts,
alpnProtocols,
serverName,
},
{
keepAliveInterval,
maxIdleTimeout,
maxConcurrentBidirectionalStreams,
maxConcurrentUnidirectionalStreams,
congestionControl,
},
keyPair,
if (options.zeroRtt) {
const conn = op_quic_connecting_0rtt(connecting);
if (conn) {
return new QuicConn(conn, endpoint);
}
}
return PromisePrototypeThen(
op_quic_connecting_1rtt(connecting),
(conn) => new QuicConn(conn, endpoint),
);
return new QuicConn(conn);
}
export {
connectQuic,
listenQuic,
QuicBidirectionalStream,
QuicConn,
QuicEndpoint,
QuicIncoming,
QuicListener,
QuicReceiveStream,

View file

@ -450,6 +450,24 @@ declare namespace Deno {
options?: StartTlsOptions,
): Promise<TlsConn>;
/**
* **UNSTABLE**: New API, yet to be vetted.
* @experimental
* @category Network
*/
export interface QuicEndpointOptions {
/**
* A literal IP address or host name that can be resolved to an IP address.
* @default {"::"}
*/
hostname?: string;
/**
* The port to bind to.
* @default {0}
*/
port?: number;
}
/**
* **UNSTABLE**: New API, yet to be vetted.
* @experimental
@ -479,6 +497,11 @@ declare namespace Deno {
* @default {100}
*/
maxConcurrentUnidirectionalStreams?: number;
/**
* The congestion control algorithm used when sending data over this connection.
* @default {"default"}
*/
congestionControl?: "throughput" | "low-latency" | "default";
}
/**
@ -486,46 +509,8 @@ declare namespace Deno {
* @experimental
* @category Network
*/
export interface ListenQuicOptions extends QuicTransportOptions {
/** The port to connect to. */
port: number;
/**
* A literal IP address or host name that can be resolved to an IP address.
* @default {"0.0.0.0"}
*/
hostname?: string;
/** Server private key in PEM format */
key: string;
/** Cert chain in PEM format */
cert: string;
/** Application-Layer Protocol Negotiation (ALPN) protocols to announce to
* the client. QUIC requires the use of ALPN.
*/
alpnProtocols: string[];
}
/**
* **UNSTABLE**: New API, yet to be vetted.
* Listen announces on the local transport address over QUIC.
*
* ```ts
* const lstnr = await Deno.listenQuic({ port: 443, cert: "...", key: "...", alpnProtocols: ["h3"] });
* ```
*
* Requires `allow-net` permission.
*
* @experimental
* @tags allow-net
* @category Network
*/
export function listenQuic(options: ListenQuicOptions): Promise<QuicListener>;
/**
* **UNSTABLE**: New API, yet to be vetted.
* @experimental
* @category Network
*/
export interface ConnectQuicOptions extends QuicTransportOptions {
export interface ConnectQuicOptions<ZRTT extends boolean>
extends QuicTransportOptions {
/** The port to connect to. */
port: number;
/** A literal IP address or host name that can be resolved to an IP address. */
@ -543,30 +528,73 @@ declare namespace Deno {
* Must be in PEM format. */
caCerts?: string[];
/**
* The congestion control algorithm used when sending data over this connection.
* If no endpoint is provided, a new one is bound on an ephemeral port.
*/
congestionControl?: "throughput" | "low-latency";
endpoint?: QuicEndpoint;
/**
* Attempt to convert the connection into 0-RTT. Any data sent before
* the TLS handshake completes is vulnerable to replay attacks.
* @default {false}
*/
zeroRtt?: ZRTT;
}
/**
* **UNSTABLE**: New API, yet to be vetted.
* Establishes a secure connection over QUIC using a hostname and port. The
* cert file is optional and if not included Mozilla's root certificates will
* be used. See also https://github.com/ctz/webpki-roots for specifics.
*
* ```ts
* const caCert = await Deno.readTextFile("./certs/my_custom_root_CA.pem");
* const conn1 = await Deno.connectQuic({ hostname: "example.com", port: 443, alpnProtocols: ["h3"] });
* const conn2 = await Deno.connectQuic({ caCerts: [caCert], hostname: "example.com", port: 443, alpnProtocols: ["h3"] });
* ```
*
* Requires `allow-net` permission.
*
* @experimental
* @tags allow-net
* @category Network
*/
export function connectQuic(options: ConnectQuicOptions): Promise<QuicConn>;
export interface QuicServerTransportOptions extends QuicTransportOptions {
/**
* Preferred IPv4 address to be communicated to the client during
* handshaking. If the client is able to reach this address it will switch
* to it.
* @default {undefined}
*/
preferredAddressV4?: string;
/**
* Preferred IPv6 address to be communicated to the client during
* handshaking. If the client is able to reach this address it will switch
* to it.
* @default {undefined}
*/
preferredAddressV6?: string;
}
/**
* **UNSTABLE**: New API, yet to be vetted.
* @experimental
* @category Network
*/
export interface QuicListenOptions extends QuicServerTransportOptions {
/** Application-Layer Protocol Negotiation (ALPN) protocols to announce to
* the client. QUIC requires the use of ALPN.
*/
alpnProtocols: string[];
/** Server private key in PEM format */
key: string;
/** Cert chain in PEM format */
cert: string;
}
/**
* **UNSTABLE**: New API, yet to be vetted.
* @experimental
* @category Network
*/
export interface QuicAcceptOptions<ZRTT extends boolean>
extends QuicServerTransportOptions {
/** Application-Layer Protocol Negotiation (ALPN) protocols to announce to
* the client. QUIC requires the use of ALPN.
*/
alpnProtocols?: string[];
/**
* Convert this connection into 0.5-RTT at the cost of weakened security, as
* 0.5-RTT data may be sent before TLS client authentication has occurred.
* @default {false}
*/
zeroRtt?: ZRTT;
}
/**
* **UNSTABLE**: New API, yet to be vetted.
@ -582,14 +610,93 @@ declare namespace Deno {
/**
* **UNSTABLE**: New API, yet to be vetted.
* An incoming connection for which the server has not yet begun its part of the handshake.
*
* @experimental
* @category Network
*/
export interface QuicSendStreamOptions {
/** Indicates the send priority of this stream relative to other streams for
* which the value has been set.
* @default {0}
*/
sendOrder?: number;
/** Wait until there is sufficient flow credit to create the stream.
* @default {false}
*/
waitUntilAvailable?: boolean;
}
/**
* **UNSTABLE**: New API, yet to be vetted.
* @experimental
* @category Network
*/
export class QuicEndpoint {
/**
* Create a QUIC endpoint which may be used for client or server connections.
*
* Requires `allow-net` permission.
*
* @experimental
* @tags allow-net
* @category Network
*/
constructor(options?: QuicEndpointOptions);
/** Return the address of the `QuicListener`. */
readonly addr: NetAddr;
/**
* **UNSTABLE**: New API, yet to be vetted.
* Listen announces on the local transport address over QUIC.
*
* @experimental
* @category Network
*/
listen(options: QuicListenOptions): QuicListener;
/**
* Closes the endpoint. All associated connections will be closed and incoming
* connections will be rejected.
*/
close(info?: QuicCloseInfo): void;
}
/**
* **UNSTABLE**: New API, yet to be vetted.
* Specialized listener that accepts QUIC connections.
*
* @experimental
* @category Network
*/
export interface QuicListener extends AsyncIterable<QuicConn> {
/** Waits for and resolves to the next incoming connection. */
incoming(): Promise<QuicIncoming>;
/** Wait for the next incoming connection and accepts it. */
accept(): Promise<QuicConn>;
/** Stops the listener. This does not close the endpoint. */
stop(): void;
[Symbol.asyncIterator](): AsyncIterableIterator<QuicConn>;
/** The endpoint for this listener. */
readonly endpoint: QuicEndpoint;
}
/**
* **UNSTABLE**: New API, yet to be vetted.
* An incoming connection for which the server has not yet begun its part of
* the handshake.
*
* @experimental
* @category Network
*/
export interface QuicIncoming {
/**
* The local IP address which was used when the peer established the connection.
* The local IP address which was used when the peer established the
* connection.
*/
readonly localIp: string;
@ -599,14 +706,17 @@ declare namespace Deno {
readonly remoteAddr: NetAddr;
/**
* Whether the socket address that is initiating this connection has proven that they can receive traffic.
* Whether the socket address that is initiating this connection has proven
* that they can receive traffic.
*/
readonly remoteAddressValidated: boolean;
/**
* Accept this incoming connection.
*/
accept(): Promise<QuicConn>;
accept<ZRTT extends boolean>(
options?: QuicAcceptOptions<ZRTT>,
): ZRTT extends true ? QuicConn : Promise<QuicConn>;
/**
* Refuse this incoming connection.
@ -619,48 +729,6 @@ declare namespace Deno {
ignore(): void;
}
/**
* **UNSTABLE**: New API, yet to be vetted.
* Specialized listener that accepts QUIC connections.
*
* @experimental
* @category Network
*/
export interface QuicListener extends AsyncIterable<QuicConn> {
/** Return the address of the `QuicListener`. */
readonly addr: NetAddr;
/** Waits for and resolves to the next connection to the `QuicListener`. */
accept(): Promise<QuicConn>;
/** Waits for and resolves to the next incoming request to the `QuicListener`. */
incoming(): Promise<QuicIncoming>;
/** Close closes the listener. Any pending accept promises will be rejected
* with errors. */
close(info: QuicCloseInfo): void;
[Symbol.asyncIterator](): AsyncIterableIterator<QuicConn>;
}
/**
* **UNSTABLE**: New API, yet to be vetted.
*
* @experimental
* @category Network
*/
export interface QuicSendStreamOptions {
/** Indicates the send priority of this stream relative to other streams for
* which the value has been set.
* @default {undefined}
*/
sendOrder?: number;
/** Wait until there is sufficient flow credit to create the stream.
* @default {false}
*/
waitUntilAvailable?: boolean;
}
/**
* **UNSTABLE**: New API, yet to be vetted.
*
@ -670,7 +738,7 @@ declare namespace Deno {
export interface QuicConn {
/** Close closes the listener. Any pending accept promises will be rejected
* with errors. */
close(info: QuicCloseInfo): void;
close(info?: QuicCloseInfo): void;
/** Opens and returns a bidirectional stream. */
createBidirectionalStream(
options?: QuicSendStreamOptions,
@ -682,17 +750,25 @@ declare namespace Deno {
/** Send a datagram. The provided data cannot be larger than
* `maxDatagramSize`. */
sendDatagram(data: Uint8Array): Promise<void>;
/** Receive a datagram. If no buffer is provider, one will be allocated.
* The size of the provided buffer should be at least `maxDatagramSize`. */
readDatagram(buffer?: Uint8Array): Promise<Uint8Array>;
/** Receive a datagram. */
readDatagram(): Promise<Uint8Array>;
/** The endpoint for this connection. */
readonly endpoint: QuicEndpoint;
/** Returns a promise that resolves when the TLS handshake is complete. */
readonly handshake: Promise<void>;
/** Return the remote address for the connection. Clients may change
* addresses at will, for example when switching to a cellular internet
* connection.
*/
readonly remoteAddr: NetAddr;
/** The negotiated ALPN protocol, if provided. */
/**
* The negotiated ALPN protocol, if provided. Only available after the
* handshake is complete. */
readonly protocol: string | undefined;
/** The negotiated server name. Only available on the server after the
* handshake is complete. */
readonly serverName: string | undefined;
/** Returns a promise that resolves when the connection is closed. */
readonly closed: Promise<QuicCloseInfo>;
/** A stream of bidirectional streams opened by the peer. */
@ -728,6 +804,11 @@ declare namespace Deno {
/** Indicates the send priority of this stream relative to other streams for
* which the value has been set. */
sendOrder: number;
/**
* 62-bit stream ID, unique within this connection.
*/
readonly id: bigint;
}
/**
@ -736,7 +817,39 @@ declare namespace Deno {
* @experimental
* @category Network
*/
export interface QuicReceiveStream extends ReadableStream<Uint8Array> {}
export interface QuicReceiveStream extends ReadableStream<Uint8Array> {
/**
* 62-bit stream ID, unique within this connection.
*/
readonly id: bigint;
}
/**
* **UNSTABLE**: New API, yet to be vetted.
* Establishes a secure connection over QUIC using a hostname and port. The
* cert file is optional and if not included Mozilla's root certificates will
* be used. See also https://github.com/ctz/webpki-roots for specifics.
*
* ```ts
* const caCert = await Deno.readTextFile("./certs/my_custom_root_CA.pem");
* const conn1 = await Deno.connectQuic({ hostname: "example.com", port: 443, alpnProtocols: ["h3"] });
* const conn2 = await Deno.connectQuic({ caCerts: [caCert], hostname: "example.com", port: 443, alpnProtocols: ["h3"] });
* ```
*
* If an endpoint is shared among many connections, 0-RTT can be enabled.
* When 0-RTT is successful, a QuicConn will be synchronously returned
* and data can be sent immediately with it. **Any data sent before the
* TLS handshake completes is vulnerable to replay attacks.**
*
* Requires `allow-net` permission.
*
* @experimental
* @tags allow-net
* @category Network
*/
export function connectQuic<ZRTT extends boolean>(
options: ConnectQuicOptions<ZRTT>,
): ZRTT extends true ? (QuicConn | Promise<QuicConn>) : Promise<QuicConn>;
export {}; // only export exports
}

View file

@ -20,6 +20,7 @@ use deno_core::OpState;
use deno_permissions::PermissionCheckError;
use deno_tls::rustls::RootCertStore;
use deno_tls::RootCertStoreProvider;
pub use quic::QuicError;
pub const UNSTABLE_FEATURE_NAME: &str = "net";
@ -161,33 +162,42 @@ deno_core::extension!(deno_net,
ops_unix::op_net_recv_unixpacket,
ops_unix::op_net_send_unixpacket<P>,
quic::op_quic_accept,
quic::op_quic_accept_bi,
quic::op_quic_accept_incoming,
quic::op_quic_accept_uni,
quic::op_quic_close_connection,
quic::op_quic_close_endpoint,
quic::op_quic_connecting_0rtt,
quic::op_quic_connecting_1rtt,
quic::op_quic_connection_accept_bi,
quic::op_quic_connection_accept_uni,
quic::op_quic_connection_close,
quic::op_quic_connection_closed,
quic::op_quic_connection_get_protocol,
quic::op_quic_connection_get_remote_addr,
quic::op_quic_connect<P>,
quic::op_quic_connection_get_server_name,
quic::op_quic_connection_handshake,
quic::op_quic_connection_open_bi,
quic::op_quic_connection_open_uni,
quic::op_quic_connection_get_max_datagram_size,
quic::op_quic_connection_read_datagram,
quic::op_quic_connection_send_datagram,
quic::op_quic_endpoint_close,
quic::op_quic_endpoint_connect<P>,
quic::op_quic_endpoint_create<P>,
quic::op_quic_endpoint_get_addr,
quic::op_quic_get_send_stream_priority,
quic::op_quic_endpoint_listen,
quic::op_quic_incoming_accept,
quic::op_quic_incoming_refuse,
quic::op_quic_incoming_accept_0rtt,
quic::op_quic_incoming_ignore,
quic::op_quic_incoming_local_ip,
quic::op_quic_incoming_refuse,
quic::op_quic_incoming_remote_addr,
quic::op_quic_incoming_remote_addr_validated,
quic::op_quic_listen<P>,
quic::op_quic_max_datagram_size,
quic::op_quic_open_bi,
quic::op_quic_open_uni,
quic::op_quic_read_datagram,
quic::op_quic_send_datagram,
quic::op_quic_set_send_stream_priority,
quic::op_quic_listener_accept,
quic::op_quic_listener_stop,
quic::op_quic_recv_stream_get_id,
quic::op_quic_send_stream_get_id,
quic::op_quic_send_stream_get_priority,
quic::op_quic_send_stream_set_priority,
],
esm = [ "01_net.js", "02_tls.js", "03_quic.js" ],
esm = [ "01_net.js", "02_tls.js" ],
lazy_loaded_esm = [ "03_quic.js" ],
options = {
root_cert_store_provider: Option<Arc<dyn RootCertStoreProvider>>,
unsafely_ignore_certificate_errors: Option<Vec<String>>,

View file

@ -4,24 +4,24 @@ use std::borrow::Cow;
use std::cell::RefCell;
use std::future::Future;
use std::net::IpAddr;
use std::net::Ipv4Addr;
use std::net::Ipv6Addr;
use std::net::SocketAddr;
use std::net::SocketAddrV4;
use std::net::SocketAddrV6;
use std::pin::pin;
use std::rc::Rc;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::task::Context;
use std::task::Poll;
use std::time::Duration;
use deno_core::error::bad_resource;
use deno_core::error::generic_error;
use deno_core::error::AnyError;
use deno_core::futures::task::noop_waker_ref;
use deno_core::op2;
use deno_core::AsyncRefCell;
use deno_core::AsyncResult;
use deno_core::BufMutView;
use deno_core::BufView;
use deno_core::GarbageCollected;
use deno_core::JsBuffer;
@ -30,20 +30,68 @@ use deno_core::RcRef;
use deno_core::Resource;
use deno_core::ResourceId;
use deno_core::WriteOutcome;
use deno_permissions::PermissionCheckError;
use deno_tls::create_client_config;
use deno_tls::SocketUse;
use deno_tls::TlsError;
use deno_tls::TlsKeys;
use deno_tls::TlsKeysHolder;
use quinn::crypto::rustls::QuicClientConfig;
use quinn::crypto::rustls::QuicServerConfig;
use quinn::rustls::client::ClientSessionMemoryCache;
use quinn::rustls::client::ClientSessionStore;
use quinn::rustls::client::Resumption;
use serde::Deserialize;
use serde::Serialize;
use crate::resolve_addr::resolve_addr;
use crate::resolve_addr::resolve_addr_sync;
use crate::DefaultTlsOptions;
use crate::NetPermissions;
use crate::UnsafelyIgnoreCertificateErrors;
#[derive(Debug, thiserror::Error)]
pub enum QuicError {
#[error("Endpoint created by 'connectQuic' cannot be used for listening")]
CannotListen,
#[error("key and cert are required")]
MissingTlsKey,
#[error("Duration is invalid")]
InvalidDuration,
#[error("Unable to resolve hostname")]
UnableToResolve,
#[error("{0}")]
StdIo(#[from] std::io::Error),
#[error("{0}")]
PermissionCheck(#[from] PermissionCheckError),
#[error("{0}")]
VarIntBoundsExceeded(#[from] quinn::VarIntBoundsExceeded),
#[error("{0}")]
Rustls(#[from] quinn::rustls::Error),
#[error("{0}")]
Tls(#[from] TlsError),
#[error("{0}")]
ConnectionError(#[from] quinn::ConnectionError),
#[error("{0}")]
ConnectError(#[from] quinn::ConnectError),
#[error("{0}")]
SendDatagramError(#[from] quinn::SendDatagramError),
#[error("{0}")]
ClosedStream(#[from] quinn::ClosedStream),
#[error("Invalid {0} resource")]
BadResource(&'static str),
#[error("Connection has reached the maximum number of concurrent outgoing {0} streams")]
MaxStreams(&'static str),
#[error("{0}")]
Core(#[from] deno_core::error::AnyError),
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CloseInfo {
close_code: u64,
reason: String,
}
#[derive(Debug, Deserialize, Serialize)]
struct Addr {
hostname: String,
@ -56,7 +104,7 @@ struct ListenArgs {
alpn_protocols: Option<Vec<String>>,
}
#[derive(Deserialize)]
#[derive(Deserialize, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
struct TransportConfig {
keep_alive_interval: Option<u64>,
@ -69,9 +117,9 @@ struct TransportConfig {
}
impl TryInto<quinn::TransportConfig> for TransportConfig {
type Error = AnyError;
type Error = QuicError;
fn try_into(self) -> Result<quinn::TransportConfig, AnyError> {
fn try_into(self) -> Result<quinn::TransportConfig, Self::Error> {
let mut cfg = quinn::TransportConfig::default();
if let Some(interval) = self.keep_alive_interval {
@ -79,7 +127,11 @@ impl TryInto<quinn::TransportConfig> for TransportConfig {
}
if let Some(timeout) = self.max_idle_timeout {
cfg.max_idle_timeout(Some(Duration::from_millis(timeout).try_into()?));
cfg.max_idle_timeout(Some(
Duration::from_millis(timeout)
.try_into()
.map_err(|_| QuicError::InvalidDuration)?,
));
}
if let Some(max) = self.max_concurrent_bidirectional_streams {
@ -111,34 +163,119 @@ impl TryInto<quinn::TransportConfig> for TransportConfig {
}
}
struct EndpointResource(quinn::Endpoint, Arc<QuicServerConfig>);
fn apply_server_transport_config(
config: &mut quinn::ServerConfig,
transport_config: TransportConfig,
) -> Result<(), QuicError> {
config.preferred_address_v4(transport_config.preferred_address_v4);
config.preferred_address_v6(transport_config.preferred_address_v6);
config.transport_config(Arc::new(transport_config.try_into()?));
Ok(())
}
struct EndpointResource {
endpoint: quinn::Endpoint,
can_listen: bool,
session_store: Arc<dyn ClientSessionStore>,
}
impl GarbageCollected for EndpointResource {}
#[op2(async)]
#[op2]
#[cppgc]
pub(crate) async fn op_quic_listen<NP>(
pub(crate) fn op_quic_endpoint_create<NP>(
state: Rc<RefCell<OpState>>,
#[serde] addr: Addr,
#[serde] args: ListenArgs,
#[serde] transport_config: TransportConfig,
#[cppgc] keys: &TlsKeysHolder,
) -> Result<EndpointResource, AnyError>
can_listen: bool,
) -> Result<EndpointResource, QuicError>
where
NP: NetPermissions + 'static,
{
state
.borrow_mut()
.borrow_mut::<NP>()
.check_net(&(&addr.hostname, Some(addr.port)), "Deno.listenQuic()")?;
let addr = resolve_addr(&addr.hostname, addr.port)
.await?
let addr = resolve_addr_sync(&addr.hostname, addr.port)?
.next()
.ok_or_else(|| generic_error("No resolved address found"))?;
.ok_or_else(|| QuicError::UnableToResolve)?;
if can_listen {
state.borrow_mut().borrow_mut::<NP>().check_net(
&(&addr.ip().to_string(), Some(addr.port())),
"new Deno.QuicEndpoint()",
)?;
} else {
// If this is not a can-listen, assert that we will bind to an ephemeral port.
assert_eq!(
addr,
SocketAddr::from((
IpAddr::from(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)),
0
))
);
}
let config = quinn::EndpointConfig::default();
let socket = std::net::UdpSocket::bind(addr)?;
let endpoint = quinn::Endpoint::new(
config,
None,
socket,
quinn::default_runtime().unwrap(),
)?;
Ok(EndpointResource {
endpoint,
can_listen,
session_store: Arc::new(ClientSessionMemoryCache::new(256)),
})
}
#[op2]
#[serde]
pub(crate) fn op_quic_endpoint_get_addr(
#[cppgc] endpoint: &EndpointResource,
) -> Result<Addr, QuicError> {
let addr = endpoint.endpoint.local_addr()?;
let addr = Addr {
hostname: format!("{}", addr.ip()),
port: addr.port(),
};
Ok(addr)
}
#[op2(fast)]
pub(crate) fn op_quic_endpoint_close(
#[cppgc] endpoint: &EndpointResource,
#[bigint] close_code: u64,
#[string] reason: String,
) -> Result<(), QuicError> {
endpoint
.endpoint
.close(quinn::VarInt::from_u64(close_code)?, reason.as_bytes());
Ok(())
}
struct ListenerResource(quinn::Endpoint, Arc<QuicServerConfig>);
impl Drop for ListenerResource {
fn drop(&mut self) {
self.0.set_server_config(None);
}
}
impl GarbageCollected for ListenerResource {}
#[op2]
#[cppgc]
pub(crate) fn op_quic_endpoint_listen(
#[cppgc] endpoint: &EndpointResource,
#[serde] args: ListenArgs,
#[serde] transport_config: TransportConfig,
#[cppgc] keys: &TlsKeysHolder,
) -> Result<ListenerResource, QuicError> {
if !endpoint.can_listen {
return Err(QuicError::CannotListen);
}
let TlsKeys::Static(deno_tls::TlsKey(cert, key)) = keys.take() else {
unreachable!()
return Err(QuicError::MissingTlsKey);
};
let mut crypto =
@ -148,6 +285,9 @@ where
.with_no_client_auth()
.with_single_cert(cert.clone(), key.clone_key())?;
// required by QUIC spec.
crypto.max_early_data_size = u32::MAX;
if let Some(alpn_protocols) = args.alpn_protocols {
crypto.alpn_protocols = alpn_protocols
.into_iter()
@ -155,66 +295,24 @@ where
.collect();
}
let server_config = Arc::new(QuicServerConfig::try_from(crypto)?);
let server_config = Arc::new(
QuicServerConfig::try_from(crypto).expect("TLS13 is explicitly configured"),
);
let mut config = quinn::ServerConfig::with_crypto(server_config.clone());
config.preferred_address_v4(transport_config.preferred_address_v4);
config.preferred_address_v6(transport_config.preferred_address_v6);
config.transport_config(Arc::new(transport_config.try_into()?));
let endpoint = quinn::Endpoint::server(config, addr)?;
apply_server_transport_config(&mut config, transport_config)?;
Ok(EndpointResource(endpoint, server_config))
endpoint.endpoint.set_server_config(Some(config));
Ok(ListenerResource(endpoint.endpoint.clone(), server_config))
}
#[op2]
#[serde]
pub(crate) fn op_quic_endpoint_get_addr(
#[cppgc] endpoint: &EndpointResource,
) -> Result<Addr, AnyError> {
let addr = endpoint.0.local_addr()?;
let addr = Addr {
hostname: format!("{}", addr.ip()),
port: addr.port(),
};
Ok(addr)
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CloseInfo {
close_code: u64,
reason: String,
}
#[op2(fast)]
pub(crate) fn op_quic_close_endpoint(
#[cppgc] endpoint: &EndpointResource,
#[bigint] close_code: u64,
#[string] reason: String,
) -> Result<(), AnyError> {
endpoint
.0
.close(quinn::VarInt::from_u64(close_code)?, reason.as_bytes());
Ok(())
}
struct ConnectionResource(quinn::Connection);
struct ConnectionResource(
quinn::Connection,
RefCell<Option<quinn::ZeroRttAccepted>>,
);
impl GarbageCollected for ConnectionResource {}
#[op2(async)]
#[cppgc]
pub(crate) async fn op_quic_accept(
#[cppgc] endpoint: &EndpointResource,
) -> Result<ConnectionResource, AnyError> {
match endpoint.0.accept().await {
Some(incoming) => {
let conn = incoming.accept()?.await?;
Ok(ConnectionResource(conn))
}
None => Err(bad_resource("QuicListener is closed")),
}
}
struct IncomingResource(
RefCell<Option<quinn::Incoming>>,
Arc<QuicServerConfig>,
@ -224,25 +322,30 @@ impl GarbageCollected for IncomingResource {}
#[op2(async)]
#[cppgc]
pub(crate) async fn op_quic_accept_incoming(
#[cppgc] endpoint: &EndpointResource,
) -> Result<IncomingResource, AnyError> {
match endpoint.0.accept().await {
pub(crate) async fn op_quic_listener_accept(
#[cppgc] resource: &ListenerResource,
) -> Result<IncomingResource, QuicError> {
match resource.0.accept().await {
Some(incoming) => Ok(IncomingResource(
RefCell::new(Some(incoming)),
endpoint.1.clone(),
resource.1.clone(),
)),
None => Err(bad_resource("QuicListener is closed")),
None => Err(QuicError::BadResource("QuicListener")),
}
}
#[op2(fast)]
pub(crate) fn op_quic_listener_stop(#[cppgc] resource: &ListenerResource) {
resource.0.set_server_config(None);
}
#[op2]
#[string]
pub(crate) fn op_quic_incoming_local_ip(
#[cppgc] incoming_resource: &IncomingResource,
) -> Result<Option<String>, AnyError> {
) -> Result<Option<String>, QuicError> {
let Some(incoming) = incoming_resource.0.borrow_mut().take() else {
return Err(bad_resource("QuicIncoming already used"));
return Err(QuicError::BadResource("QuicIncoming"));
};
Ok(incoming.local_ip().map(|ip| ip.to_string()))
}
@ -251,9 +354,9 @@ pub(crate) fn op_quic_incoming_local_ip(
#[serde]
pub(crate) fn op_quic_incoming_remote_addr(
#[cppgc] incoming_resource: &IncomingResource,
) -> Result<Addr, AnyError> {
) -> Result<Addr, QuicError> {
let Some(incoming) = incoming_resource.0.borrow_mut().take() else {
return Err(bad_resource("QuicIncoming already used"));
return Err(QuicError::BadResource("QuicIncoming"));
};
let addr = incoming.remote_address();
Ok(Addr {
@ -265,43 +368,66 @@ pub(crate) fn op_quic_incoming_remote_addr(
#[op2(fast)]
pub(crate) fn op_quic_incoming_remote_addr_validated(
#[cppgc] incoming_resource: &IncomingResource,
) -> Result<bool, AnyError> {
) -> Result<bool, QuicError> {
let Some(incoming) = incoming_resource.0.borrow_mut().take() else {
return Err(bad_resource("QuicIncoming already used"));
return Err(QuicError::BadResource("QuicIncoming"));
};
Ok(incoming.remote_address_validated())
}
fn quic_incoming_accept(
incoming_resource: &IncomingResource,
transport_config: Option<TransportConfig>,
) -> Result<quinn::Connecting, QuicError> {
let Some(incoming) = incoming_resource.0.borrow_mut().take() else {
return Err(QuicError::BadResource("QuicIncoming"));
};
match transport_config {
Some(transport_config) if transport_config != Default::default() => {
let mut config =
quinn::ServerConfig::with_crypto(incoming_resource.1.clone());
apply_server_transport_config(&mut config, transport_config)?;
Ok(incoming.accept_with(Arc::new(config))?)
}
_ => Ok(incoming.accept()?),
}
}
#[op2(async)]
#[cppgc]
pub(crate) async fn op_quic_incoming_accept(
#[cppgc] incoming_resource: &IncomingResource,
#[serde] transport_config: Option<TransportConfig>,
) -> Result<ConnectionResource, AnyError> {
let Some(incoming) = incoming_resource.0.borrow_mut().take() else {
return Err(bad_resource("QuicIncoming already used"));
};
let conn = match transport_config {
Some(transport_config) => {
let mut config =
quinn::ServerConfig::with_crypto(incoming_resource.1.clone());
config.preferred_address_v4(transport_config.preferred_address_v4);
config.preferred_address_v6(transport_config.preferred_address_v6);
config.transport_config(Arc::new(transport_config.try_into()?));
incoming.accept_with(Arc::new(config))?.await?
) -> Result<ConnectionResource, QuicError> {
let connecting = quic_incoming_accept(incoming_resource, transport_config)?;
let conn = connecting.await?;
Ok(ConnectionResource(conn, RefCell::new(None)))
}
#[op2]
#[cppgc]
pub(crate) fn op_quic_incoming_accept_0rtt(
#[cppgc] incoming_resource: &IncomingResource,
#[serde] transport_config: Option<TransportConfig>,
) -> Result<ConnectionResource, QuicError> {
let connecting = quic_incoming_accept(incoming_resource, transport_config)?;
match connecting.into_0rtt() {
Ok((conn, zrtt_accepted)) => {
Ok(ConnectionResource(conn, RefCell::new(Some(zrtt_accepted))))
}
Err(_connecting) => {
unreachable!("0.5-RTT always succeeds");
}
}
None => incoming.accept()?.await?,
};
Ok(ConnectionResource(conn))
}
#[op2]
#[serde]
pub(crate) fn op_quic_incoming_refuse(
#[cppgc] incoming: &IncomingResource,
) -> Result<(), AnyError> {
) -> Result<(), QuicError> {
let Some(incoming) = incoming.0.borrow_mut().take() else {
return Err(bad_resource("QuicIncoming already used"));
return Err(QuicError::BadResource("QuicIncoming"));
};
incoming.refuse();
Ok(())
@ -311,43 +437,47 @@ pub(crate) fn op_quic_incoming_refuse(
#[serde]
pub(crate) fn op_quic_incoming_ignore(
#[cppgc] incoming: &IncomingResource,
) -> Result<(), AnyError> {
) -> Result<(), QuicError> {
let Some(incoming) = incoming.0.borrow_mut().take() else {
return Err(bad_resource("QuicIncoming already used"));
return Err(QuicError::BadResource("QuicIncoming"));
};
incoming.ignore();
Ok(())
}
struct ConnectingResource(RefCell<Option<quinn::Connecting>>);
impl GarbageCollected for ConnectingResource {}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ConnectArgs {
addr: Addr,
ca_certs: Option<Vec<String>>,
alpn_protocols: Option<Vec<String>>,
server_name: Option<String>,
}
#[op2(async)]
#[op2]
#[cppgc]
pub(crate) async fn op_quic_connect<NP>(
pub(crate) fn op_quic_endpoint_connect<NP>(
state: Rc<RefCell<OpState>>,
#[serde] addr: Addr,
#[cppgc] endpoint: &EndpointResource,
#[serde] args: ConnectArgs,
#[serde] transport_config: TransportConfig,
#[cppgc] key_pair: &TlsKeysHolder,
) -> Result<ConnectionResource, AnyError>
) -> Result<ConnectingResource, QuicError>
where
NP: NetPermissions + 'static,
{
state
.borrow_mut()
.borrow_mut::<NP>()
.check_net(&(&addr.hostname, Some(addr.port)), "Deno.connectQuic()")?;
state.borrow_mut().borrow_mut::<NP>().check_net(
&(&args.addr.hostname, Some(args.addr.port)),
"Deno.connectQuic()",
)?;
let sock_addr = resolve_addr(&addr.hostname, addr.port)
.await?
let sock_addr = resolve_addr_sync(&args.addr.hostname, args.addr.port)?
.next()
.ok_or_else(|| generic_error("No resolved address found"))?;
.ok_or_else(|| QuicError::UnableToResolve)?;
let root_cert_store = state
.borrow()
@ -379,24 +509,50 @@ where
alpn_protocols.into_iter().map(|s| s.into_bytes()).collect();
}
let client_config = QuicClientConfig::try_from(tls_config)?;
tls_config.enable_early_data = true;
tls_config.resumption = Resumption::store(endpoint.session_store.clone());
let client_config =
QuicClientConfig::try_from(tls_config).expect("TLS13 supported");
let mut client_config = quinn::ClientConfig::new(Arc::new(client_config));
client_config.transport_config(Arc::new(transport_config.try_into()?));
let local_addr = match sock_addr.ip() {
IpAddr::V4(_) => IpAddr::from(Ipv4Addr::new(0, 0, 0, 0)),
IpAddr::V6(_) => IpAddr::from(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)),
};
let conn = quinn::Endpoint::client((local_addr, 0).into())?
.connect_with(
let connecting = endpoint.endpoint.connect_with(
client_config,
sock_addr,
&args.server_name.unwrap_or(addr.hostname),
)?
.await?;
&args.server_name.unwrap_or(args.addr.hostname),
)?;
Ok(ConnectionResource(conn))
Ok(ConnectingResource(RefCell::new(Some(connecting))))
}
#[op2(async)]
#[cppgc]
pub(crate) async fn op_quic_connecting_1rtt(
#[cppgc] connecting: &ConnectingResource,
) -> Result<ConnectionResource, QuicError> {
let Some(connecting) = connecting.0.borrow_mut().take() else {
return Err(QuicError::BadResource("QuicConnecting"));
};
let conn = connecting.await?;
Ok(ConnectionResource(conn, RefCell::new(None)))
}
#[op2]
#[cppgc]
pub(crate) fn op_quic_connecting_0rtt(
#[cppgc] connecting_res: &ConnectingResource,
) -> Option<ConnectionResource> {
let connecting = connecting_res.0.borrow_mut().take()?;
match connecting.into_0rtt() {
Ok((conn, zrtt_accepted)) => {
Some(ConnectionResource(conn, RefCell::new(Some(zrtt_accepted))))
}
Err(connecting) => {
*connecting_res.0.borrow_mut() = Some(connecting);
None
}
}
}
#[op2]
@ -412,11 +568,23 @@ pub(crate) fn op_quic_connection_get_protocol(
.map(|p| String::from_utf8_lossy(&p).into_owned())
}
#[op2]
#[string]
pub(crate) fn op_quic_connection_get_server_name(
#[cppgc] connection: &ConnectionResource,
) -> Option<String> {
connection
.0
.handshake_data()
.and_then(|h| h.downcast::<quinn::crypto::rustls::HandshakeData>().ok())
.and_then(|h| h.server_name)
}
#[op2]
#[serde]
pub(crate) fn op_quic_connection_get_remote_addr(
#[cppgc] connection: &ConnectionResource,
) -> Result<Addr, AnyError> {
) -> Result<Addr, QuicError> {
let addr = connection.0.remote_address();
Ok(Addr {
hostname: format!("{}", addr.ip()),
@ -425,11 +593,11 @@ pub(crate) fn op_quic_connection_get_remote_addr(
}
#[op2(fast)]
pub(crate) fn op_quic_close_connection(
pub(crate) fn op_quic_connection_close(
#[cppgc] connection: &ConnectionResource,
#[bigint] close_code: u64,
#[string] reason: String,
) -> Result<(), AnyError> {
) -> Result<(), QuicError> {
connection
.0
.close(quinn::VarInt::from_u64(close_code)?, reason.as_bytes());
@ -440,7 +608,7 @@ pub(crate) fn op_quic_close_connection(
#[serde]
pub(crate) async fn op_quic_connection_closed(
#[cppgc] connection: &ConnectionResource,
) -> Result<CloseInfo, AnyError> {
) -> Result<CloseInfo, QuicError> {
let e = connection.0.closed().await;
match e {
quinn::ConnectionError::LocallyClosed => Ok(CloseInfo {
@ -455,11 +623,29 @@ pub(crate) async fn op_quic_connection_closed(
}
}
struct SendStreamResource(AsyncRefCell<quinn::SendStream>);
#[op2(async)]
pub(crate) async fn op_quic_connection_handshake(
#[cppgc] connection: &ConnectionResource,
) {
let Some(zrtt_accepted) = connection.1.borrow_mut().take() else {
return;
};
zrtt_accepted.await;
}
struct SendStreamResource {
stream: AsyncRefCell<quinn::SendStream>,
stream_id: quinn::StreamId,
priority: AtomicI32,
}
impl SendStreamResource {
fn new(stream: quinn::SendStream) -> Self {
Self(AsyncRefCell::new(stream))
Self {
stream_id: stream.id(),
priority: AtomicI32::new(stream.priority().unwrap_or(0)),
stream: AsyncRefCell::new(stream),
}
}
}
@ -470,18 +656,28 @@ impl Resource for SendStreamResource {
fn write(self: Rc<Self>, view: BufView) -> AsyncResult<WriteOutcome> {
Box::pin(async move {
let mut r = RcRef::map(self, |r| &r.0).borrow_mut().await;
let nwritten = r.write(&view).await?;
let mut stream =
RcRef::map(self.clone(), |r| &r.stream).borrow_mut().await;
stream.set_priority(self.priority.load(Ordering::Relaxed))?;
let nwritten = stream.write(&view).await?;
Ok(WriteOutcome::Partial { nwritten, view })
})
}
fn close(self: Rc<Self>) {}
}
struct RecvStreamResource(AsyncRefCell<quinn::RecvStream>);
struct RecvStreamResource {
stream: AsyncRefCell<quinn::RecvStream>,
stream_id: quinn::StreamId,
}
impl RecvStreamResource {
fn new(stream: quinn::RecvStream) -> Self {
Self(AsyncRefCell::new(stream))
Self {
stream_id: stream.id(),
stream: AsyncRefCell::new(stream),
}
}
}
@ -492,21 +688,40 @@ impl Resource for RecvStreamResource {
fn read(self: Rc<Self>, limit: usize) -> AsyncResult<BufView> {
Box::pin(async move {
let mut r = RcRef::map(self, |r| &r.0).borrow_mut().await;
let mut r = RcRef::map(self, |r| &r.stream).borrow_mut().await;
let mut data = vec![0; limit];
let nread = r.read(&mut data).await?.unwrap_or(0);
data.truncate(nread);
Ok(BufView::from(data))
})
}
fn read_byob(
self: Rc<Self>,
mut buf: BufMutView,
) -> AsyncResult<(usize, BufMutView)> {
Box::pin(async move {
let mut r = RcRef::map(self, |r| &r.stream).borrow_mut().await;
let nread = r.read(&mut buf).await?.unwrap_or(0);
Ok((nread, buf))
})
}
fn shutdown(self: Rc<Self>) -> AsyncResult<()> {
Box::pin(async move {
let mut r = RcRef::map(self, |r| &r.stream).borrow_mut().await;
r.stop(quinn::VarInt::from(0u32))?;
Ok(())
})
}
}
#[op2(async)]
#[serde]
pub(crate) async fn op_quic_accept_bi(
pub(crate) async fn op_quic_connection_accept_bi(
#[cppgc] connection: &ConnectionResource,
state: Rc<RefCell<OpState>>,
) -> Result<(ResourceId, ResourceId), AnyError> {
) -> Result<(ResourceId, ResourceId), QuicError> {
match connection.0.accept_bi().await {
Ok((tx, rx)) => {
let mut state = state.borrow_mut();
@ -517,7 +732,7 @@ pub(crate) async fn op_quic_accept_bi(
Err(e) => match e {
quinn::ConnectionError::LocallyClosed
| quinn::ConnectionError::ApplicationClosed(..) => {
Err(bad_resource("QuicConn is closed"))
Err(QuicError::BadResource("QuicConnection"))
}
_ => Err(e.into()),
},
@ -526,11 +741,11 @@ pub(crate) async fn op_quic_accept_bi(
#[op2(async)]
#[serde]
pub(crate) async fn op_quic_open_bi(
pub(crate) async fn op_quic_connection_open_bi(
#[cppgc] connection: &ConnectionResource,
state: Rc<RefCell<OpState>>,
wait_for_available: bool,
) -> Result<(ResourceId, ResourceId), AnyError> {
) -> Result<(ResourceId, ResourceId), QuicError> {
let (tx, rx) = if wait_for_available {
connection.0.open_bi().await?
} else {
@ -539,7 +754,7 @@ pub(crate) async fn op_quic_open_bi(
match pin!(connection.0.open_bi()).poll(&mut cx) {
Poll::Ready(r) => r?,
Poll::Pending => {
return Err(generic_error("Connection has reached the maximum number of outgoing concurrent bidirectional streams"));
return Err(QuicError::MaxStreams("bidirectional"));
}
}
};
@ -551,10 +766,10 @@ pub(crate) async fn op_quic_open_bi(
#[op2(async)]
#[serde]
pub(crate) async fn op_quic_accept_uni(
pub(crate) async fn op_quic_connection_accept_uni(
#[cppgc] connection: &ConnectionResource,
state: Rc<RefCell<OpState>>,
) -> Result<ResourceId, AnyError> {
) -> Result<ResourceId, QuicError> {
match connection.0.accept_uni().await {
Ok(rx) => {
let rid = state
@ -566,7 +781,7 @@ pub(crate) async fn op_quic_accept_uni(
Err(e) => match e {
quinn::ConnectionError::LocallyClosed
| quinn::ConnectionError::ApplicationClosed(..) => {
Err(bad_resource("QuicConn is closed"))
Err(QuicError::BadResource("QuicConnection"))
}
_ => Err(e.into()),
},
@ -575,11 +790,11 @@ pub(crate) async fn op_quic_accept_uni(
#[op2(async)]
#[serde]
pub(crate) async fn op_quic_open_uni(
pub(crate) async fn op_quic_connection_open_uni(
#[cppgc] connection: &ConnectionResource,
state: Rc<RefCell<OpState>>,
wait_for_available: bool,
) -> Result<ResourceId, AnyError> {
) -> Result<ResourceId, QuicError> {
let tx = if wait_for_available {
connection.0.open_uni().await?
} else {
@ -588,7 +803,7 @@ pub(crate) async fn op_quic_open_uni(
match pin!(connection.0.open_uni()).poll(&mut cx) {
Poll::Ready(r) => r?,
Poll::Pending => {
return Err(generic_error("Connection has reached the maximum number of outgoing concurrent unidirectional streams"));
return Err(QuicError::MaxStreams("unidirectional"));
}
}
};
@ -600,63 +815,80 @@ pub(crate) async fn op_quic_open_uni(
}
#[op2(async)]
pub(crate) async fn op_quic_send_datagram(
pub(crate) async fn op_quic_connection_send_datagram(
#[cppgc] connection: &ConnectionResource,
#[buffer] buf: JsBuffer,
) -> Result<(), AnyError> {
) -> Result<(), QuicError> {
connection.0.send_datagram_wait(buf.to_vec().into()).await?;
Ok(())
}
#[op2(async)]
pub(crate) async fn op_quic_read_datagram(
#[buffer]
pub(crate) async fn op_quic_connection_read_datagram(
#[cppgc] connection: &ConnectionResource,
#[buffer] mut buf: JsBuffer,
) -> Result<u32, AnyError> {
) -> Result<Vec<u8>, QuicError> {
let data = connection.0.read_datagram().await?;
buf[0..data.len()].copy_from_slice(&data);
Ok(data.len() as _)
Ok(data.into())
}
#[op2(fast)]
pub(crate) fn op_quic_max_datagram_size(
pub(crate) fn op_quic_connection_get_max_datagram_size(
#[cppgc] connection: &ConnectionResource,
) -> Result<u32, AnyError> {
) -> Result<u32, QuicError> {
Ok(connection.0.max_datagram_size().unwrap_or(0) as _)
}
#[op2(fast)]
pub(crate) fn op_quic_get_send_stream_priority(
pub(crate) fn op_quic_send_stream_get_priority(
state: Rc<RefCell<OpState>>,
#[smi] rid: ResourceId,
) -> Result<i32, AnyError> {
) -> Result<i32, QuicError> {
let resource = state
.borrow()
.resource_table
.get::<SendStreamResource>(rid)?;
let r = RcRef::map(resource, |r| &r.0).try_borrow();
match r {
Some(s) => Ok(s.priority()?),
None => Err(generic_error("Unable to get priority")),
}
Ok(resource.priority.load(Ordering::Relaxed))
}
#[op2(fast)]
pub(crate) fn op_quic_set_send_stream_priority(
pub(crate) fn op_quic_send_stream_set_priority(
state: Rc<RefCell<OpState>>,
#[smi] rid: ResourceId,
priority: i32,
) -> Result<(), AnyError> {
) -> Result<(), QuicError> {
let resource = state
.borrow()
.resource_table
.get::<SendStreamResource>(rid)?;
let r = RcRef::map(resource, |r| &r.0).try_borrow();
match r {
Some(s) => {
s.set_priority(priority)?;
resource.priority.store(priority, Ordering::Relaxed);
Ok(())
}
None => Err(generic_error("Unable to set priority")),
}
}
#[op2(fast)]
#[bigint]
pub(crate) fn op_quic_send_stream_get_id(
state: Rc<RefCell<OpState>>,
#[smi] rid: ResourceId,
) -> Result<u64, QuicError> {
let resource = state
.borrow()
.resource_table
.get::<SendStreamResource>(rid)?;
let stream_id = quinn::VarInt::from(resource.stream_id).into_inner();
Ok(stream_id)
}
#[op2(fast)]
#[bigint]
pub(crate) fn op_quic_recv_stream_get_id(
state: Rc<RefCell<OpState>>,
#[smi] rid: ResourceId,
) -> Result<u64, QuicError> {
let resource = state
.borrow()
.resource_table
.get::<RecvStreamResource>(rid)?;
let stream_id = quinn::VarInt::from(resource.stream_id).into_inner();
Ok(stream_id)
}

View file

@ -47,6 +47,7 @@ use deno_kv::KvErrorKind;
use deno_kv::KvMutationError;
use deno_napi::NApiError;
use deno_net::ops::NetError;
use deno_net::QuicError;
use deno_permissions::ChildPermissionError;
use deno_permissions::NetDescriptorFromUrlParseError;
use deno_permissions::PathResolveError;
@ -1589,6 +1590,27 @@ fn get_sync_fetch_error(error: &SyncFetchError) -> &'static str {
}
}
fn get_quic_error_class(error: &QuicError) -> &'static str {
match error {
QuicError::CannotListen => "Error",
QuicError::MissingTlsKey => "TypeError",
QuicError::InvalidDuration => "TypeError",
QuicError::UnableToResolve => "Error",
QuicError::StdIo(e) => get_io_error_class(e),
QuicError::PermissionCheck(e) => get_permission_check_error_class(e),
QuicError::VarIntBoundsExceeded(_) => "RangeError",
QuicError::Rustls(_) => "Error",
QuicError::Tls(e) => get_tls_error_class(e),
QuicError::ConnectionError(_) => "Error",
QuicError::ConnectError(_) => "Error",
QuicError::SendDatagramError(_) => "Error",
QuicError::ClosedStream(_) => "BadResource",
QuicError::BadResource(_) => "BadResource",
QuicError::MaxStreams(_) => "RangeError",
QuicError::Core(e) => get_error_class_name(e).unwrap_or("Error"),
}
}
pub fn get_error_class_name(e: &AnyError) -> Option<&'static str> {
deno_core::error::get_custom_error_class(e)
.or_else(|| {
@ -1824,6 +1846,7 @@ pub fn get_error_class_name(e: &AnyError) -> Option<&'static str> {
e.downcast_ref::<deno_net::io::MapError>()
.map(get_net_map_error)
})
.or_else(|| e.downcast_ref::<QuicError>().map(get_quic_error_class))
.or_else(|| {
e.downcast_ref::<BroadcastChannelError>()
.map(get_broadcast_channel_error)

View file

@ -423,7 +423,7 @@ fn get_suggestions_for_terminal_errors(e: &JsError) -> Vec<FixSuggestion> {
"Run again with `--unstable-webgpu` flag to enable this API.",
),
];
} else if msg.contains("listenQuic is not a function") {
} else if msg.contains("QuicEndpoint is not a constructor") {
return vec![
FixSuggestion::info("listenQuic is an unstable API."),
FixSuggestion::hint(

View file

@ -1,6 +1,6 @@
// Copyright 2018-2025 the Deno authors. MIT license.
import { core } from "ext:core/mod.js";
import { core, primordials } from "ext:core/mod.js";
import {
op_net_listen_udp,
op_net_listen_unixpacket,
@ -13,7 +13,6 @@ import * as console from "ext:deno_console/01_console.js";
import * as ffi from "ext:deno_ffi/00_ffi.js";
import * as net from "ext:deno_net/01_net.js";
import * as tls from "ext:deno_net/02_tls.js";
import * as quic from "ext:deno_net/03_quic.js";
import * as serve from "ext:deno_http/00_serve.ts";
import * as http from "ext:deno_http/01_http.js";
import * as websocket from "ext:deno_http/02_websocket.ts";
@ -32,6 +31,10 @@ import * as cron from "ext:deno_cron/01_cron.ts";
import * as webgpuSurface from "ext:deno_webgpu/02_surface.js";
import * as telemetry from "ext:deno_telemetry/telemetry.ts";
const { ObjectDefineProperties } = primordials;
const loadQuic = core.createLazyLoader("ext:deno_net/03_quic.js");
const denoNs = {
Process: process.Process,
run: process.run,
@ -175,17 +178,28 @@ denoNsUnstableById[unstableIds.net] = {
op_net_listen_udp,
op_net_listen_unixpacket,
),
connectQuic: quic.connectQuic,
listenQuic: quic.listenQuic,
QuicBidirectionalStream: quic.QuicBidirectionalStream,
QuicConn: quic.QuicConn,
QuicListener: quic.QuicListener,
QuicReceiveStream: quic.QuicReceiveStream,
QuicSendStream: quic.QuicSendStream,
QuicIncoming: quic.QuicIncoming,
};
ObjectDefineProperties(denoNsUnstableById[unstableIds.net], {
connectQuic: core.propWritableLazyLoaded((q) => q.connectQuic, loadQuic),
QuicEndpoint: core.propWritableLazyLoaded((q) => q.QuicEndpoint, loadQuic),
QuicBidirectionalStream: core.propWritableLazyLoaded(
(q) => q.QuicBidirectionalStream,
loadQuic,
),
QuicConn: core.propWritableLazyLoaded((q) => q.QuicConn, loadQuic),
QuicListener: core.propWritableLazyLoaded((q) => q.QuicListener, loadQuic),
QuicReceiveStream: core.propWritableLazyLoaded(
(q) => q.QuicReceiveStream,
loadQuic,
),
QuicSendStream: core.propWritableLazyLoaded(
(q) => q.QuicSendStream,
loadQuic,
),
QuicIncoming: core.propWritableLazyLoaded((q) => q.QuicIncoming, loadQuic),
});
// denoNsUnstableById[unstableIds.unsafeProto] = { __proto__: null }
denoNsUnstableById[unstableIds.webgpu] = {

View file

@ -1,28 +1,32 @@
// Copyright 2018-2025 the Deno authors. MIT license.
import { assertEquals } from "./test_util.ts";
import { assert, assertEquals } from "./test_util.ts";
const cert = Deno.readTextFileSync("tests/testdata/tls/localhost.crt");
const key = Deno.readTextFileSync("tests/testdata/tls/localhost.key");
const caCerts = [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")];
async function pair(opt?: Deno.QuicTransportOptions): Promise<
[Deno.QuicConn, Deno.QuicConn, Deno.QuicListener]
> {
const listener = await Deno.listenQuic({
hostname: "localhost",
port: 0,
interface Pair {
server: Deno.QuicConn;
client: Deno.QuicConn;
endpoint: Deno.QuicEndpoint;
}
async function pair(opt?: Deno.QuicTransportOptions): Promise<Pair> {
const endpoint = new Deno.QuicEndpoint({ hostname: "localhost" });
const listener = endpoint.listen({
cert,
key,
alpnProtocols: ["deno-test"],
...opt,
});
assertEquals(endpoint, listener.endpoint);
const [server, client] = await Promise.all([
listener.accept(),
Deno.connectQuic({
hostname: "localhost",
port: listener.addr.port,
port: endpoint.addr.port,
caCerts,
alpnProtocols: ["deno-test"],
...opt,
@ -31,13 +35,14 @@ async function pair(opt?: Deno.QuicTransportOptions): Promise<
assertEquals(server.protocol, "deno-test");
assertEquals(client.protocol, "deno-test");
assertEquals(client.remoteAddr, listener.addr);
assertEquals(client.remoteAddr, endpoint.addr);
assertEquals(server.serverName, "localhost");
return [server, client, listener];
return { server, client, endpoint };
}
Deno.test("bidirectional stream", async () => {
const [server, client, listener] = await pair();
const { server, client, endpoint } = await pair();
const encoded = (new TextEncoder()).encode("hi!");
@ -57,12 +62,12 @@ Deno.test("bidirectional stream", async () => {
assertEquals(data, encoded);
}
listener.close({ closeCode: 0, reason: "" });
client.close({ closeCode: 0, reason: "" });
client.close();
endpoint.close();
});
Deno.test("unidirectional stream", async () => {
const [server, client, listener] = await pair();
const { server, client, endpoint } = await pair();
const encoded = (new TextEncoder()).encode("hi!");
@ -82,12 +87,12 @@ Deno.test("unidirectional stream", async () => {
assertEquals(data, encoded);
}
listener.close({ closeCode: 0, reason: "" });
client.close({ closeCode: 0, reason: "" });
endpoint.close();
client.close();
});
Deno.test("datagrams", async () => {
const [server, client, listener] = await pair();
const { server, client, endpoint } = await pair();
const encoded = (new TextEncoder()).encode("hi!");
@ -96,22 +101,20 @@ Deno.test("datagrams", async () => {
const data = await client.readDatagram();
assertEquals(data, encoded);
listener.close({ closeCode: 0, reason: "" });
client.close({ closeCode: 0, reason: "" });
endpoint.close();
client.close();
});
Deno.test("closing", async () => {
const [server, client, listener] = await pair();
const { server, client } = await pair();
server.close({ closeCode: 42, reason: "hi!" });
assertEquals(await client.closed, { closeCode: 42, reason: "hi!" });
listener.close({ closeCode: 0, reason: "" });
});
Deno.test("max concurrent streams", async () => {
const [server, client, listener] = await pair({
const { server, client, endpoint } = await pair({
maxConcurrentBidirectionalStreams: 1,
maxConcurrentUnidirectionalStreams: 1,
});
@ -136,15 +139,13 @@ Deno.test("max concurrent streams", async () => {
});
}
listener.close({ closeCode: 0, reason: "" });
server.close({ closeCode: 0, reason: "" });
client.close({ closeCode: 0, reason: "" });
endpoint.close();
client.close();
});
Deno.test("incoming", async () => {
const listener = await Deno.listenQuic({
hostname: "localhost",
port: 0,
const endpoint = new Deno.QuicEndpoint({ hostname: "localhost" });
const listener = endpoint.listen({
cert,
key,
alpnProtocols: ["deno-test"],
@ -153,7 +154,7 @@ Deno.test("incoming", async () => {
const connect = () =>
Deno.connectQuic({
hostname: "localhost",
port: listener.addr.port,
port: endpoint.addr.port,
caCerts,
alpnProtocols: ["deno-test"],
});
@ -165,8 +166,63 @@ Deno.test("incoming", async () => {
assertEquals(server.protocol, "deno-test");
assertEquals(client.protocol, "deno-test");
assertEquals(client.remoteAddr, listener.addr);
assertEquals(client.remoteAddr, endpoint.addr);
listener.close({ closeCode: 0, reason: "" });
client.close({ closeCode: 0, reason: "" });
endpoint.close();
client.close();
});
Deno.test("0rtt", async () => {
const sEndpoint = new Deno.QuicEndpoint({ hostname: "localhost" });
const listener = sEndpoint.listen({
cert,
key,
alpnProtocols: ["deno-test"],
});
(async () => {
while (true) {
let incoming;
try {
incoming = await listener.incoming();
} catch (e) {
if (e instanceof Deno.errors.BadResource) {
break;
}
throw e;
}
const conn = incoming.accept({ zeroRtt: true });
conn.handshake.then(() => {
conn.close();
});
}
})();
const endpoint = new Deno.QuicEndpoint();
const c1 = await Deno.connectQuic({
hostname: "localhost",
port: sEndpoint.addr.port,
caCerts,
alpnProtocols: ["deno-test"],
endpoint,
});
await c1.closed;
const c2 = Deno.connectQuic({
hostname: "localhost",
port: sEndpoint.addr.port,
caCerts,
alpnProtocols: ["deno-test"],
zeroRtt: true,
endpoint,
});
assert(!(c2 instanceof Promise), "0rtt should be accepted");
await c2.closed;
sEndpoint.close();
endpoint.close();
});