Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"attribution": {
"commit": "",
"pr": ""
}
}
22 changes: 22 additions & 0 deletions site/source/docs/porting/networking.rst
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,28 @@ alternative, the WebRTC specification provides a mechanism to perform UDP-like
communication with WebRTC Data Channels. Currently Emscripten does not provide a
C/C++ API for interacting with WebRTC.

Direct Sockets API (Isolated Web Apps)
=======================================

The `Direct Sockets API <https://wicg.github.io/direct-sockets/>`_ provides
real TCP and UDP socket access from the browser, without needing a proxy server.
This API is only available inside Chrome Isolated Web Apps (IWAs).

Emscripten can route POSIX socket syscalls through the Direct Sockets API using
``TCPSocket``, ``TCPServerSocket``, and ``UDPSocket``. This enables existing
C/C++ networking code (including libraries like OpenSSL and Tor) to work
with real network sockets inside an IWA.

To enable Direct Sockets support, compile and link with
``-sDIRECT_SOCKETS``. This also requires ``-sASYNCIFY`` (or JSPI).

The following POSIX socket functions are supported:
- ``socket()``, ``bind()``, ``connect()``, ``listen()``, ``accept()``,
``send()``, ``recv()``, ``sendto()``, ``recvfrom()``, ``sendmsg()``,
``recvmsg()``, ``shutdown()``, ``getsockname()``, ``getpeername()``,
``setsockopt()``, ``getsockopt()``, ``poll()``, ``ioctl()``,
``fcntl()``, ``pipe()``, ``socketpair()``

WebTransport and QUIC
=====================

Expand Down
168 changes: 43 additions & 125 deletions src/lib/libdirectsockets.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
*/

#if DIRECT_SOCKETS
#if !ASYNCIFY
#error "DIRECT_SOCKETS requires ASYNCIFY or JSPI to be enabled"
#endif

var DirectSocketsLibrary = {

Expand All @@ -21,9 +24,6 @@ var DirectSocketsLibrary = {
// Monotonic counter for unique socket/pipe node names
nextId: 0,

// DNS cache: hostname -> {addresses: [...], expires: timestamp}
dnsCache: {},

// FS mount point for socket nodes (initialized lazily)
root: null,

Expand Down Expand Up @@ -121,8 +121,7 @@ var DirectSocketsLibrary = {

createSocketState(family, type, protocol) {
#if ASSERTIONS
if (typeof globalThis.TCPSocket === 'undefined' &&
typeof globalThis.UDPSocket === 'undefined') {
if (!globalThis.TCPSocket && !globalThis.UDPSocket) {
abort('Direct Sockets API is not available. DIRECT_SOCKETS requires a Chrome Isolated Web App (IWA) context. See https://wicg.github.io/direct-sockets/');
}
#endif
Expand Down Expand Up @@ -247,15 +246,8 @@ var DirectSocketsLibrary = {
if (info.errno) return { errno: info.errno };
// readSockaddr returns addr as a string like "1.2.3.4" and port as a number.

// First check our DoH reverse cache - if this IP was resolved by us,
// map it back to the hostname so TCPSocket can do its own resolution
// Reverse lookup: map fake 172.29.x.x IPs back to hostnames
var addr = info.addr;
var reverseHostname = DIRECT_SOCKETS.dnsCache['_reverse_' + addr];
if (reverseHostname) {
return { family: info.family, addr: reverseHostname, port: info.port };
}

// Fall back to emscripten's DNS reverse lookup (for fake 172.29.x.x IPs)
var resolvedAddr = DNS.lookup_addr(addr) || addr;
return { family: info.family, addr: resolvedAddr, port: info.port };
},
Expand All @@ -267,15 +259,25 @@ var DirectSocketsLibrary = {
if (sock.options.keepAliveDelay > 0) opts.keepAliveDelay = sock.options.keepAliveDelay;
if (sock.options.sendBufferSize > 0) opts.sendBufferSize = sock.options.sendBufferSize;
if (sock.options.receiveBufferSize > 0) opts.receiveBufferSize = sock.options.receiveBufferSize;
if (sock.family === {{{ cDefs.AF_INET6 }}}) opts.dnsQueryType = 'ipv6';
// Set dnsQueryType per the Direct Sockets spec (SocketDnsQueryType)
// to ensure Chrome resolves the correct record type for this socket family
if (sock.family === {{{ cDefs.AF_INET6 }}}) {
opts.dnsQueryType = 'ipv6';
} else if (sock.family === {{{ cDefs.AF_INET }}}) {
opts.dnsQueryType = 'ipv4';
}
return opts;
},

buildUDPOptions(sock) {
var opts = {};
if (sock.options.sendBufferSize > 0) opts.sendBufferSize = sock.options.sendBufferSize;
if (sock.options.receiveBufferSize > 0) opts.receiveBufferSize = sock.options.receiveBufferSize;
if (sock.family === {{{ cDefs.AF_INET6 }}}) opts.dnsQueryType = 'ipv6';
if (sock.family === {{{ cDefs.AF_INET6 }}}) {
opts.dnsQueryType = 'ipv6';
} else if (sock.family === {{{ cDefs.AF_INET }}}) {
opts.dnsQueryType = 'ipv4';
}
return opts;
},

Expand Down Expand Up @@ -446,40 +448,6 @@ var DirectSocketsLibrary = {
return revents;
},

// Async DNS resolution via DNS-over-HTTPS (DoH)
async resolveDNS(hostname, family) {
// Check cache first
var cached = DIRECT_SOCKETS.dnsCache[hostname];
if (cached && cached.expires > Date.now()) return cached.addresses[0];

// DoH query via fetch
var type = (family === {{{ cDefs.AF_INET6 }}}) ? 'AAAA' : 'A';
var typeNum = (type === 'A') ? 1 : 28;
try {
var resp = await fetch(
'https://dns.google/resolve?name=' + encodeURIComponent(hostname) + '&type=' + type,
{ headers: { 'Accept': 'application/dns-json' } }
);
var json = await resp.json();

if (json.Answer && json.Answer.length > 0) {
var addresses = json.Answer
.filter(function(a) { return a.type === typeNum; })
.map(function(a) { return a.data; });
if (addresses.length === 0) return null;
var ttl = Math.max((json.Answer[0].TTL || 300), 60);
DIRECT_SOCKETS.dnsCache[hostname] = {
addresses: addresses, expires: Date.now() + ttl * 1000
};
return addresses[0];
}
} catch (e) {
#if SOCKET_DEBUG
dbg('direct_sockets: DoH resolution failed for ' + hostname + ': ' + e);
#endif
}
return null; // NXDOMAIN
},
},

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1146,28 +1114,20 @@ var DirectSocketsLibrary = {

// Direct Sockets only supports a few options, and they must be set at
// construction time. We defer them and apply when connect/bind is called.
// musl socket option constants (stable ABI):
var SO_REUSEADDR = 2, SO_TYPE = 3, SO_SNDBUF = 7, SO_RCVBUF = 8;
var SO_KEEPALIVE = 9, SO_REUSEPORT = 15;
var TCP_NODELAY = 1, TCP_KEEPIDLE = 4, TCP_KEEPINTVL = 5;
var IP_MULTICAST_TTL = 33, IP_MULTICAST_LOOP = 34;
var IP_ADD_MEMBERSHIP = 35, IP_DROP_MEMBERSHIP = 36;
var IPV6_MULTICAST_LOOP = 18, IPV6_MULTICAST_HOPS = 19;
var IPV6_JOIN_GROUP = 20, IPV6_LEAVE_GROUP = 21;

if (level === {{{ cDefs.SOL_SOCKET }}}) {
switch (optname) {
case SO_REUSEADDR:
case SO_REUSEPORT:
case {{{ cDefs.SO_REUSEADDR }}}:
case {{{ cDefs.SO_REUSEPORT }}}:
// Silently accept - no equivalent, but harmless
return 0;
case SO_SNDBUF:
case {{{ cDefs.SO_SNDBUF }}}:
sock.options.sendBufferSize = {{{ makeGetValue('optval', 0, 'i32') }}};
return 0;
case SO_RCVBUF:
case {{{ cDefs.SO_RCVBUF }}}:
sock.options.receiveBufferSize = {{{ makeGetValue('optval', 0, 'i32') }}};
return 0;
case SO_KEEPALIVE:
case {{{ cDefs.SO_KEEPALIVE }}}:
// Will be used as keepAliveDelay if enabled - use a default of 60s
var enabled = {{{ makeGetValue('optval', 0, 'i32') }}};
if (enabled && sock.options.keepAliveDelay === 0) {
Expand All @@ -1185,11 +1145,11 @@ var DirectSocketsLibrary = {
}
} else if (level === {{{ cDefs.IPPROTO_TCP }}}) {
switch (optname) {
case TCP_NODELAY:
case {{{ cDefs.TCP_NODELAY }}}:
sock.options.noDelay = !!{{{ makeGetValue('optval', 0, 'i32') }}};
return 0;
case TCP_KEEPIDLE:
case TCP_KEEPINTVL:
case {{{ cDefs.TCP_KEEPIDLE }}}:
case {{{ cDefs.TCP_KEEPINTVL }}}:
// Map to keepAliveDelay (in milliseconds)
sock.options.keepAliveDelay = {{{ makeGetValue('optval', 0, 'i32') }}} * 1000;
return 0;
Expand All @@ -1199,32 +1159,32 @@ var DirectSocketsLibrary = {
#endif
return 0;
}
} else if (level === 0 /* IPPROTO_IP */) {
} else if (level === {{{ cDefs.IPPROTO_IP }}}) {
switch (optname) {
case IP_MULTICAST_TTL:
case {{{ cDefs.IP_MULTICAST_TTL }}}:
sock.options.multicastTtl = HEAPU8[optval];
return 0;
case IP_MULTICAST_LOOP:
case {{{ cDefs.IP_MULTICAST_LOOP }}}:
sock.options.multicastLoopback = !!HEAPU8[optval];
return 0;
case IP_ADD_MEMBERSHIP:
case {{{ cDefs.IP_ADD_MEMBERSHIP }}}:
return DIRECT_SOCKETS.joinMulticastGroup(sock, DIRECT_SOCKETS.parseIpMreq(optval, optlen));
case IP_DROP_MEMBERSHIP:
case {{{ cDefs.IP_DROP_MEMBERSHIP }}}:
return DIRECT_SOCKETS.leaveMulticastGroup(sock, DIRECT_SOCKETS.parseIpMreq(optval, optlen));
default:
return 0;
}
} else if (level === 41 /* IPPROTO_IPV6 */) {
} else if (level === {{{ cDefs.IPPROTO_IPV6 }}}) {
switch (optname) {
case IPV6_MULTICAST_LOOP:
case {{{ cDefs.IPV6_MULTICAST_LOOP }}}:
sock.options.multicastLoopback = !!HEAPU8[optval];
return 0;
case IPV6_MULTICAST_HOPS:
case {{{ cDefs.IPV6_MULTICAST_HOPS }}}:
sock.options.multicastTtl = HEAPU8[optval];
return 0;
case IPV6_JOIN_GROUP:
case {{{ cDefs.IPV6_JOIN_GROUP }}}:
return DIRECT_SOCKETS.joinMulticastGroup(sock, DIRECT_SOCKETS.parseIpv6Mreq(optval, optlen));
case IPV6_LEAVE_GROUP:
case {{{ cDefs.IPV6_LEAVE_GROUP }}}:
return DIRECT_SOCKETS.leaveMulticastGroup(sock, DIRECT_SOCKETS.parseIpv6Mreq(optval, optlen));
default:
return 0;
Expand All @@ -1239,15 +1199,14 @@ var DirectSocketsLibrary = {
var sock = DIRECT_SOCKETS.getSocket(fd);
if (!sock) return -{{{ cDefs.EBADF }}};

var SO_TYPE = 3;
if (level === {{{ cDefs.SOL_SOCKET }}}) {
if (optname === {{{ cDefs.SO_ERROR }}}) {
{{{ makeSetValue('optval', 0, 'sock.error', 'i32') }}};
{{{ makeSetValue('optlen', 0, 4, 'i32') }}};
sock.error = 0;
return 0;
}
if (optname === SO_TYPE) {
if (optname === {{{ cDefs.SO_TYPE }}}) {
{{{ makeSetValue('optval', 0, 'sock.type', 'i32') }}};
{{{ makeSetValue('optlen', 0, 4, 'i32') }}};
return 0;
Expand Down Expand Up @@ -1543,8 +1502,9 @@ var DirectSocketsLibrary = {
__syscall_socketpair__deps: ['$DIRECT_SOCKETS_PIPES'],
__syscall_socketpair: (domain, type, protocol, sv) => {
// Two cross-connected pipes: fd0's write goes to fd1's read and vice versa
var fd0 = DIRECT_SOCKETS_PIPES.allocatePipeFd('sockpair[0.' + Object.keys(DIRECT_SOCKETS_PIPES.pipes).length + ']');
var fd1 = DIRECT_SOCKETS_PIPES.allocatePipeFd('sockpair[1.' + Object.keys(DIRECT_SOCKETS_PIPES.pipes).length + ']');
var id = DIRECT_SOCKETS.nextId++;
var fd0 = DIRECT_SOCKETS_PIPES.allocatePipeFd('sockpair[0.' + id + ']');
var fd1 = DIRECT_SOCKETS_PIPES.allocatePipeFd('sockpair[1.' + id + ']');

// Create pipe objects directly (no intermediate fds needed)
var spPipe0to1 = {
Expand Down Expand Up @@ -1638,56 +1598,14 @@ var DirectSocketsLibrary = {
},

// ---------------------------------------------------------------------------
// DNS resolution - async DoH-based getaddrinfo support
// DNS resolution - override emscripten's default to handle Direct Sockets
// ---------------------------------------------------------------------------

_emscripten_lookup_name__deps: ['$DNS', '$inetPton4', '$UTF8ToString'],
_emscripten_lookup_name__async: true,
_emscripten_lookup_name: async (name) => {
_emscripten_lookup_name: (name) => {
var hostname = UTF8ToString(name);

// Handle special cases that don't need DoH
// DNS.lookup_name returns a string; inetPton4 converts to packed uint32
if (hostname === 'localhost' || hostname === '127.0.0.1') {
return inetPton4(DNS.lookup_name('localhost'));
}

// Check if it's already an IP address (dotted decimal)
if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname)) {
return inetPton4(DNS.lookup_name(hostname));
}

// Try DoH resolution for real hostnames
var realIp = await DIRECT_SOCKETS.resolveDNS(hostname, {{{ cDefs.AF_INET }}});
if (realIp) {
#if SOCKET_DEBUG
dbg('direct_sockets: DoH resolved ' + hostname + ' -> ' + realIp);
#endif
// Convert IP string to packed 32-bit integer (little-endian, same as inetPton4)
var parts = realIp.split('.');
var packed = ((parseInt(parts[0])) |
(parseInt(parts[1]) << 8) |
(parseInt(parts[2]) << 16) |
(parseInt(parts[3]) << 24)) >>> 0;

// Register reverse mapping: real IP -> hostname
// So parseSockaddr can resolve it back for TCPSocket constructor
if (DNS.address_map) {
if (!DNS.address_map.addrs) DNS.address_map.addrs = {};
if (!DNS.address_map.names) DNS.address_map.names = {};
DNS.address_map.addrs[hostname] = realIp;
DNS.address_map.names[realIp] = hostname;
}

// Also store in our cache for parseSockaddr
DIRECT_SOCKETS.dnsCache['_real_' + hostname] = realIp;
DIRECT_SOCKETS.dnsCache['_reverse_' + realIp] = hostname;

return packed;
}

// Fallback to Emscripten's fake DNS
return DNS.lookup_name(hostname);
// DNS.lookup_name returns a string ip addr, inetPton4 packs it to uint32
return inetPton4(DNS.lookup_name(hostname));
},

// Internal helper for closing - not a syscall but used by shutdown and close
Expand Down
28 changes: 26 additions & 2 deletions src/struct_info.json
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,25 @@
"defines": [
"IPPROTO_UDP",
"IPPROTO_TCP",
"INADDR_LOOPBACK"
"IPPROTO_IP",
"IPPROTO_IPV6",
"INADDR_LOOPBACK",
"IP_MULTICAST_TTL",
"IP_MULTICAST_LOOP",
"IP_ADD_MEMBERSHIP",
"IP_DROP_MEMBERSHIP",
"IPV6_MULTICAST_LOOP",
"IPV6_MULTICAST_HOPS",
"IPV6_JOIN_GROUP",
"IPV6_LEAVE_GROUP"
]
},
{
"file": "netinet/tcp.h",
"defines": [
"TCP_NODELAY",
"TCP_KEEPIDLE",
"TCP_KEEPINTVL"
]
},
{
Expand Down Expand Up @@ -312,7 +330,13 @@
"AF_UNSPEC",
"AF_INET6",
"SOL_SOCKET",
"SO_ERROR"
"SO_ERROR",
"SO_REUSEADDR",
"SO_TYPE",
"SO_SNDBUF",
"SO_RCVBUF",
"SO_KEEPALIVE",
"SO_REUSEPORT"
]
},
{
Expand Down