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

@ -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,53 +497,20 @@ declare namespace Deno {
* @default {100}
*/
maxConcurrentUnidirectionalStreams?: number;
}
/**
* **UNSTABLE**: New API, yet to be vetted.
* @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"}
* The congestion control algorithm used when sending data over this connection.
* @default {"default"}
*/
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[];
congestionControl?: "throughput" | "low-latency" | "default";
}
/**
* **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
}