Skip to content

Latest commit

 

History

History
550 lines (388 loc) · 19.5 KB

explainer.md

File metadata and controls

550 lines (388 loc) · 19.5 KB

Direct Sockets

Background

This Direct Sockets API proposal relates to the Discourse post Filling the remaining gap between WebSocket, WebRTC and WebTransport.

This API is currently planned as a part of the Isolated Web Apps proposal - check out Telnet Client Demo for a showcase of the API's capabilities.

Use cases

The initial motivating use case is to support creating a web app that talks to servers and devices that have their own protocols incompatible with what’s available on the web. The web app should be able to talk to a legacy system, without requiring users to change or replace that system.

Alternatives

Web apps can already establish data communications using XMLHttpRequest, WebSocket and WebRTC.

These facilities are designed to be tightly constrained (example design requirements), and don't allow raw TCP or UDP communication. Such facilities were requested but not provided due to the potential for abuse.

If the web server has network access to the required server or device, it can act as relay. For example, a web mail application might use XMLHttpRequest for communication between the browser and the web server, and use SMTP and IMAP between the web server and a mail server.

In scenarios like the following, a vendor would require their customers to deploy a relay web server on each site where devices/servers are in use:

  • The target devices might be behind firewalls, not accessible to a central server.
  • Communication might be bandwidth-intensive or latency-sensitive.
  • There might be legal constraints on where data can be sent.
  • On-site communication may need to be resilient to ISP outages.

Developers have used browser plugins - such as Java applets, ActiveX, Adobe Flex or Microsoft Silverlight - to establish raw TCP or UDP communication from the browser, without relaying through a web server.

With the shift away from browser plugins, native apps now provide the main alternative. Widely used APIs include POSIX sockets, Winsock, java.net and System.Net.Sockets.

JavaScript APIs for socket communication have been developed for B2G OS (TCP, UDP) and Chrome Apps (TCP, UDP). An earlier proposed API for the web platform is the TCP and UDP Socket API, which the System Applications Working Group published as an informative Working Group Note and is no longer progressing.

Permissions Policy integration

This specification defines a policy-controlled permission identified by the string direct-sockets. Its default allowlist is self.

Permissions-Policy: direct-sockets=(self)

This Permissions-Policy header determines whether a new TCPSocket(...), new UDPSocket(...) or new TCPServerSocket(...) call immediately rejects with a NotAllowedError DOMException.

Security Considerations

The API is planned to be available only in Isolated Web Apps which themselves provide a decent level of security thanks to a transparent update model and strict Content Security Policy.

Threat

Third party iframes (such as ads) might initiate connections.

Mitigation

The direct-sockets permissions policy will control access, preventing third party use by default. To further safeguard from potential third-party attacks, IWAs employ a strict Content Security Policy that makes using external resources (i.e. the ones not originating from the Web Bundle itself) difficult and enforce cross-origin-isolation.

Threat

Use of the API may violate organization policies, that control which protocols may be used.

Mitigation

User agents may restrict use of the API when enterprise software policies are in effect. For example, user agents might by default not allow use of this API unless the user has permission to install new binaries.

Threat

MITM attackers may hijack plaintext connections created using the API.

Mitigation

This API is supposed to be used in Isolated Web Apps which employ a strict Content Security Policy that makes using external resources (i.e. the ones not originating from the Web Bundle itself) difficult -- in particular, prohibit eval() calls on the retrieved data thanks to script-src 'self' in the CSP.

We should also facilitate use of TLS on TCP connections.

One option would be to allow TLS to be requested when opening a connection, like the TCP and UDP Socket API's useSecureTransport.

Another option would be to provide a method that upgrades an existing TCP connection to use TLS. Use cases would include SMTP STARTTLS, IMAP STARTTLS and POP STLS.

TCPSocket

Applications will be able to request a TCP socket by creating a TCPSocket class using the new operator and then waiting for the connection to be established. Refer to the snippets below for a deeper dive.

IDL Definitions

enum SocketDnsQueryType { "ipv4", "ipv6" };

dictionary TCPSocketOptions {
  boolean noDelay = false;
  [EnforceRange] unsigned long keepAliveDelay;
  [EnforceRange] unsigned long sendBufferSize;
  [EnforceRange] unsigned long receiveBufferSize;
  SocketDnsQueryType dnsQueryType;
};

dictionary TCPSocketOpenInfo {
  ReadableStream readable;
  WritableStream writable;

  DOMString remoteAddress;
  unsigned short remotePort;

  DOMString localAddress;
  unsigned short localPort;
};

interface TCPSocket {
  constructor(
    DOMString remoteAddress,
    unsigned short remotePort,
    optional TCPSocketOptions options = {});

  readonly attribute Promise<TCPSocketOpenInfo> opened;
  readonly attribute Promise<void> closed;

  Promise<void> close();
};

Examples

Learn more about using TCPSocket.

Opening/Closing the socket

const remoteAddress = 'example.com';
const remotePort = 7;

const options = {
  noDelay: false,
  keepAlive: true,
  keepAliveDelay: 720_000
};

let tcpSocket = new TCPSocket(remoteAddress, remotePort, options);
// If rejected by permissions-policy...
if (!tcpSocket) {
  return;
}

// Wait for the connection to be established...
let { readable, writable } = await tcpSocket.opened;

// do stuff with the socket
...

// Close the socket. Note that this operation will succeeed if and only if neither readable not writable streams are locked.
tcpSocket.close();

IO operations

The TCP socket can be used for reading and writing.

Reading

See ReadableStream spec for more examples. TCPSocket supports both ReadableStreamDefaultReader and ReadableStreamBYOBReader.

let tcpSocket = new TCPSocket(...);

let { readable } = await tcpSocket.opened;
let reader = readable.getReader();

let { value, done } = await reader.read();
if (done) {
  // this happens either if the socket is exhausted (i.e. when the remote peer
  // closes the connection gracefully), or after manual readable.releaseLock()/reader.cancel().
  return;
}

const decoder = new TextDecoder();
let message = decoder.decode(value);
...

// Don't forget to call releaseLock() or cancel() on the reader once done.

Writing

See WritableStream spec for more examples.

let tcpSocket = new TCPSocket(...);

let { writable } = await tcpSocket.opened;
let writer = writable.getWriter();

const encoder = new TextEncoder();
let message = "Some user-created tcp data";

await writer.ready;
writer.write(
  encoder.encode(message)
).catch(err => console.log(err));
...

// Don't forget to call releaseLock() or cancel()/abort() on the writer once done.

UDPSocket

Applications will be able to request a UDP socket by creating a UDPSocket class using the new operator and then waiting for the socket to be opened.

UDPSocket operates in different modes depending on the provided set of options:

  • In bound mode the socket is bound to a specific local IP endpoint (defined by localAddress and optionally localPort) and is capable of sending/receiving datagrams to/from arbitrary destinations.
    • localAddress must be a valid IP address.
    • localPort can be omitted to let the OS pick one on its own.
    • remoteAddress and remotePort must be specified on a per-packet basis as part of UDPMessage.
    • remoteAddress (in UDPMessage) can either be an IP address or a domain name when sending.
  • In connected mode the socket is bound to an arbitrary local IP endpoint and sends/receives datagrams from/to a single destination (defined by remoteAddress and remotePort). *remoteAddress can either be an IP address or a domain name.

remoteAddress/remotePort and localAddress/localPort pairs cannot be specified together.

IDL Definitions

enum SocketDnsQueryType { "ipv4", "ipv6" };

dictionary UDPSocketOptions {
  DOMString remoteAddress;
  [EnforceRange] unsigned short remotePort;

  DOMString localAddress;
  [EnforceRange] unsigned short localPort;

  SocketDnsQueryType dnsQueryType;

  [EnforceRange] unsigned long sendBufferSize;
  [EnforceRange] unsigned long receiveBufferSize;
};

dictionary UDPSocketOpenInfo {
  ReadableStream readable;
  WritableStream writable;

  DOMString remoteAddress;
  unsigned short remotePort;

  DOMString localAddress;
  unsigned short localPort;
};

interface UDPSocket {
  constructor(UDPSocketOptions options);

  readonly attribute Promise<UDPSocketOpenInfo> opened;
  readonly attribute Promise<void> closed;

  Promise<void> close();
};

Examples

Learn more about using UDPSocket.

Opening/Closing the socket

Connected version

const remoteAddress = 'example.com'; // could be a raw IP address too
const remotePort = 7;

let udpSocket = new UDPSocket({ remoteAddress, remotePort });
// If rejected by permissions-policy...
if (!udpSocket) {
  return;
}

// Wait for the connection to be established...
let { readable, writable } = await udpSocket.opened;

// do stuff with the socket
...

// Close the socket. Note that this operation will succeeed if and only if neither readable not writable streams are locked.
udpSocket.close();

Bound version

const localAddress = '127.0.0.1';

// Omitting |localPort| allows the OS to pick one on its own.
let udpSocket = new UDPSocket({ localAddress });
// If rejected by permissions-policy...
if (!udpSocket) {
  return;
}

// Wait for the connection to be established...
let { readable, writable } = await udpSocket.opened;

// do stuff with the socket
...

// Close the socket. Note that this operation will succeeed if and only if neither readable not writable streams are locked.
udpSocket.close();

IO operations

The UDP socket can be used for reading and writing. Both streams operate on the UDPMessage object which is defined as follows (idl):

dictionary UDPMessage {
  BufferSource   data;
  DOMString      remoteAddress;
  unsigned short remotePort;
};

Reading

See ReadableStream spec for more examples.

Connected mode
let udpSocket = new UDPSocket({ remoteAddress, remotePort });
let { readable } = await udpSocket.opened;

let reader = readable.getReader();

let { value, done } = await reader.read();
if (done) {
  // this happens only if readable.releaseLock() or reader.cancel() is called manually.
  return;
}

const decoder = new TextDecoder();

// |value| is a UDPMessage object.
// |remoteAddress| and |remotePort| members of UDPMessage are guaranteed to be null.
let { data } = value;
let message = decoder.decode(data);
...

// Don't forget to call releaseLock() or cancel() on the reader once done.
Bound mode
let udpSocket = new UDPSocket({ localAddress });
let { readable } = await udpSocket.opened;

let reader = readable.getReader();

let { value, done } = await reader.read();
if (done) {
  // this happens only if readable.releaseLock() or reader.cancel() is called manually.
  return;
}

const decoder = new TextDecoder();

// |value| is a UDPMessage object.
// |remoteAddress| and |remotePort| members of UDPMessage indicate the remote host
// where the datagram came from.
let { data, remoteAddress, remotePort } = value;
let message = decoder.decode(data);
...

// Don't forget to call releaseLock() or cancel() on the reader once done.

Writing

See WritableStream spec for more examples.

Connected mode
let udpSocket = new UDPSocket({ remoteAddress, remotePort });
let { writable } = await udpSocket.opened;

let writer = writable.getWriter();

const encoder = new TextEncoder();
let message = "Some user-created datagram";

// Sends a UDPMessage object where |data| is a Uint8Array.
// Note that |remoteAddress| and |remotePort| must not be specified.
await writer.ready;
writer.write({
    data: encoder.encode(message)
}).catch(err => console.log(err));

// Sends a UDPMessage object where |data| is an ArrayBuffer.
// Note that |remoteAddress| and |remotePort| must not be specified.
await writer.ready;
writer.write({
    data: encoder.encode(message).buffer
}).catch(err => console.log(err));
...

// Don't forget to call releaseLock() or cancel()/abort() on the writer once done.
Bound mode
let udpSocket = new UDPSocket({ localAddress });
let { writable } = await udpSocket.opened;

let writer = writable.getWriter();

const encoder = new TextEncoder();
let message = "Some user-created datagram";

// Sends a UDPMessage object where |data| is a Uint8Array.
// Note that both |remoteAddress| and |remotePort| must be specified in this case.
// Specifying a domain name as |remoteAddress| requires DNS lookups for each write()
// which is quite inefficient; applications should replace it with the IP address
// of the peer after receiving a response packet.
await writer.ready;
writer.write({
    data: encoder.encode(message),
    remoteAddress: 'example.com',
    remotePort: 7
}).catch(err => console.log(err));

// Sends a UDPMessage object where |data| is an ArrayBuffer.
// Note that both |remoteAddress| and |remotePort| must be specified in this case.
await writer.ready;
writer.write({
    data: encoder.encode(message).buffer,
    remoteAddress: '98.76.54.32',
    remotePort: 18
}).catch(err => console.log(err));
...

// Don't forget to call releaseLock() or cancel()/abort() on the writer once done.

TCPServerSocket

Applications will be able to request a TCP server socket by creating a TCPServerSocket class using the new operator and then waiting for the socket to be opened.

  • localPort can be omitted to let the OS pick one on its own.
  • backlog sets the size of the OS accept queue; if not specified, will be substituted by platform default.

IDL Definitions

dictionary TCPServerSocketOptions {
  [EnforceRange] unsigned short localPort,
  [EnforceRange] unsigned long backlog;
};

dictionary TCPServerSocketOpenInfo {
  ReadableStream readable;

  DOMString localAddress;
  unsigned short localPort;
};

interface TCPServerSocket {
  constructor(
    DOMString localAddress,
    optional TCPServerSocketOptions options = {});

  readonly attribute Promise<TCPServerSocketOpenInfo> opened;
  readonly attribute Promise<void> closed;

  Promise<void> close();
};

Examples

Learn more about TCPServerSocket.

Opening/Closing the socket

let tcpServerSocket = new TCPServerSocket('::');
// If rejected by permissions-policy...
if (!tcpServerSocket) {
  return;
}

// Wait for the connection to be established...
let { readable } = await tcpServerSocket.opened;

// do stuff with the socket
...

// Close the socket. Note that this operation will succeeed if and only if readable stream is not locked.
tcpServerSocket.close();

Accepting connections

Connection accepted by TCPServerSocket are delivered via its ReadableStream in the form of ready-to-use TCPSocket objects.

See ReadableStream spec for more examples.

let tcpServerSocket = new TCPServerSocket('::');
let { readable: tcpServerSocketReadable } = await udpSocket.opened;

let tcpServerSocketReader = tcpServerSocketReadable.getReader();

// |value| is an accepted TCPSocket.
let { value: tcpSocket, done } = await tcpServerSocketReader.read();
if (done) {
  // this happens only if readable.releaseLock() or reader.cancel() is called manually.
  return;
}
tcpServerSocketReader.releaseLock();

// Send a packet using the newly accepted socket.
const { writable: tcpSocketWritable } = await tcpSocket.opened;

const tcpSocketWriter = tcpSocketWritable.getWriter();

const encoder = new TextEncoder();
const message = "Some user-created tcp data";

await tcpSocketWriter.ready;
await tcpSocketWriter.write(encoder.encode(message));

// Don't forget to call releaseLock() or close()/abort() on the writer once done.