Path: blob/main/core/kernel/src/wasm/posix/socket.ts
1070 views
/*1This is an implementation of POSIX Sockets.23Of course, much of the code for this is written in zig in the posix-node package4in this file packages/posix-node/src/socket.zig567RELATED WORK: I wrote most of this, then searched and found it is solving8a similar problem to emscripten's "Full POSIX Sockets over WebSocket Proxy Server":910- https://emscripten.org/docs/porting/networking.html#full-posix-sockets-over-websocket-proxy-server11- https://github.com/emscripten-core/emscripten/tree/main/tools/websocket_to_posix_proxy12- https://github.com/emscripten-core/emscripten/pull/7670 (interesting discussion)131415Of course, the architecture of the CoWasm solution is massively different.16It looks like Emscripten's is a full multithreaded standalone C++ program17for proxying many network connections from (presumably) many clients.18In contrast, CoWasm's solution is partly written in zig as a node extension19and integrated with Javascript. It can be used standalone with other Javascript20projects having nothing to do with cowasm, by using the posix-node package,21so there is potential for more community feedback and development.22We would likely have to have one Javascript worker/thread per client, but23this could be integrated with accessing other resources on the server.24It's also nice that CoWasm also can operate with exactly the same code25in a mode that doesn't involving proxying to a backend server, with26everything in the same process (which is all that is implemented here27right now!), since that should be fast and good for automated testing.28*/2930import Errno from "./errno";31import {32nativeToWasmFamily,33wasmToNativeFamily,34wasmToNativeSocktype,35sendSockaddr,36} from "./netdb";37import constants from "./constants";38import { constants as wasi_constants, SOCKET_DEFAULT_RIGHTS } from "wasi-js";39import debug from "debug";4041const log = debug("posix:socket");4243export default function socket({44callFunction,45posix,46recv,47wasi,48send,49memory,50}) {51function sendNativeSockaddr(sockaddr, ptr: number) {52sendSockaddr(53send,54memory,55ptr,56nativeToWasmFamily(posix, sockaddr.sa_family),57sockaddr.sa_len,58sockaddr.sa_data59);60}6162function getSockaddr(sockaddrPtr: number, address_len: number) {63const sa_family = wasmToNativeFamily(64posix,65callFunction("recv_sockaddr_sa_family", sockaddrPtr)66);67const sa_len = address_len - 2;68const sa_data = recv.buffer(69callFunction("recv_sockaddr_sa_data", sockaddrPtr),70sa_len71);72for (let i = sa_len; i < sa_len; i++) {73sa_data[i] = 0;74}75return { sa_family, sa_len, sa_data };76}7778function native_fd(virtual_fd: number): number {79const data = wasi.FD_MAP.get(virtual_fd);80if (data == null) {81throw Error(`invalid fd=${virtual_fd}`);82}83return data.real;84}8586function getSocktype(virtual_fd: number): number {87const data = wasi.FD_MAP.get(virtual_fd);88if (data == null) {89throw Error(`unknown sock type for fd=${virtual_fd}`);90}91return data.socktype;92}9394// Convert flags from wasi to native. (Right now it looks95// like only MSG_WAITALL is different.)96function native_flags(wasi_flags: number): number {97let flags = 0;98for (const name of [99"MSG_OOB",100"MSG_PEEK",101"MSG_WAITALL",102"MSG_DONTROUTE",103]) {104if (wasi_flags & constants[name]) {105flags |= posix.constants[name];106}107}108return flags;109}110111function native_level(level: number): number {112if (level == constants.SOL_SOCKET) {113return posix.constants.SOL_SOCKET;114}115return level;116}117118function native_option_name(option_name: number): number {119for (const name in constants) {120if (name.startsWith("SO_") && option_name == constants[name]) {121const x = posix.constants[name];122if (x == null) {123throw Error(124`unsupported option name "${name}" -- defined in WebAssembly but not natively`125);126}127return x;128}129}130throw Error(`unknown option name ${option_name}`);131}132133function createWasiFd(native_fd: number, socktype: number): number {134// TODO: I'm starting the socket fd's at value over 1000 entirely because135// if wstart at the default smallest possible when doing136// "python-wasm -m pip" it crashes, since the fd=4 gets assigned137// to some socket for a moment, then freed and then 4 gets used138// for a directory (maybe at the same time), and this somehow139// confuses things. Maybe there is a bug somewhere in WASI or Python.140// For now we just workaround it by putting the socket fd's141// way out of reach of the normal file fd's.142const wasi_fd = wasi.getUnusedFileDescriptor(1000);143wasi.FD_MAP.set(wasi_fd, {144real: native_fd,145rights: {146base: SOCKET_DEFAULT_RIGHTS,147inheriting: BigInt(0),148},149filetype: wasi_constants.WASI_FILETYPE_SOCKET_STREAM,150socktype, // constants.SOCK_STREAM (tcp) or constants.SOCK_DGRAM (udp)151});152return wasi_fd;153}154155return {156socket(family: number, socktype: number, protocol: number): number {157if (posix.socket == null) {158throw Errno("ENOTSUP");159}160log("socket", { family, socktype, protocol });161162const familyNative = wasmToNativeFamily(posix, family);163164let inheritable;165if (constants.SOCK_CLOEXEC & socktype) {166// SOCK_CLOEXEC is defined on Linux and WASI (weirdly) but not MacOS,167// so we manually implement it to avoid weird hacks in C code.168socktype &= ~constants.SOCK_CLOEXEC; // remove it169inheritable = false; // below we will do what SOCK_CLOEXEC would do manually.170} else {171inheritable = true;172}173174const socktypeNative = wasmToNativeSocktype(posix, socktype);175176// TODO? I don't know how to translate this or if it is necessary.177const protocolNative = protocol;178179const native_fd = posix.socket(180familyNative,181socktypeNative,182protocolNative183);184185if (!inheritable) {186posix.set_inheritable(native_fd, inheritable);187}188return createWasiFd(native_fd, socktype);189},190191// int bind(int socket, const struct sockaddr *address, socklen_t address_len);192bind(socket: number, sockaddrPtr: number, address_len: number): number {193log("bind", socket);194if (posix.bind == null) {195throw Errno("ENOTSUP");196}197const sockaddr = getSockaddr(sockaddrPtr, address_len);198log("bind: address", sockaddr);199posix.bind(native_fd(socket), sockaddr);200201return 0;202},203204connect(socket: number, sockaddrPtr: number, address_len: number): number {205if (posix.connect == null) {206throw Errno("ENOTSUP");207}208const sockaddr = getSockaddr(sockaddrPtr, address_len);209log("connect", { socket, sockaddr, address_len });210posix.connect(native_fd(socket), sockaddr);211return 0;212},213214/*215int getsockname(int sockfd, struct sockaddr* addr, socklen_t* addrlen);216*/217getsockname(218socket: number,219sockaddrPtr: number,220addressLenPtr: number221): number {222if (posix.getsockname == null) {223throw Errno("ENOTSUP");224}225log("getsockname", socket);226const sockaddr = posix.getsockname(native_fd(socket));227sendNativeSockaddr(sockaddr, sockaddrPtr);228send.u32(addressLenPtr, sockaddr.sa_len);229return 0;230},231232/*233int getpeername(int sockfd, struct sockaddr* addr, socklen_t* addrlen);234*/235getpeername(236socket: number,237sockaddrPtr: number,238addressLenPtr: number239): number {240log("getpeername", socket);241const sockaddr = posix.getpeername(native_fd(socket));242sendNativeSockaddr(sockaddr, sockaddrPtr);243send.u32(addressLenPtr, sockaddr.sa_len);244return 0;245},246247/*248ssize_t recv(int socket, void *buffer, size_t length, int flags);249250NOTE: send and recv are less efficient than they might otherwise251be due to a lot of extra copying of data to/from dynamically allocated252Buffers. Probably the cost of calling to Javascript at all exceeds253this though.254*/255recv(256socket: number,257bufPtr: number,258length: number,259flags: number260): number {261log("recv", { socket, bufPtr, length, flags });262if (posix.recv == null) {263throw Errno("ENOTSUP");264}265const buffer = Buffer.alloc(length);266const bytesReceived = posix.recv(267native_fd(socket),268buffer,269native_flags(flags)270);271//log("recv got ", { buffer, bytesReceived });272send.buffer(buffer, bufPtr);273return bytesReceived;274},275276/*277TODO:278ssize_t279recvfrom(int socket, void *buffer, size_t length, int flags,280struct sockaddr *address, socklen_t *address_len);281*/282recvfrom(283socket: number,284bufPtr: number,285length: number,286flags: number,287sockaddrPtr: number,288sockaddrLenPtr: number289): number {290log("recvfrom", {291socket,292bufPtr,293length,294flags,295sockaddrPtr,296sockaddrLenPtr,297});298if (posix.recvfrom == null) {299throw Errno("ENOTSUP");300}301const buffer = Buffer.alloc(length);302const { bytesReceived, sockaddr } = posix.recvfrom(303native_fd(socket),304buffer,305native_flags(flags)306);307log("recvfrom got ", { buffer, bytesReceived, sockaddr });308send.buffer(buffer, bufPtr);309sendNativeSockaddr(sockaddr, sockaddrPtr);310send.u32(sockaddrLenPtr, sockaddr.sa_len);311return bytesReceived;312},313314/*315ssize_t send(int socket, const void *buffer, size_t length, int flags);316*/317send(318socket: number,319bufPtr: number,320length: number,321flags: number322): number {323log("send", { socket, bufPtr, length, flags });324if (posix.send == null) {325throw Errno("ENOTSUP");326}327const buffer = recv.buffer(bufPtr, length);328return posix.send(native_fd(socket), buffer, native_flags(flags));329},330331/*332TODO:333334ssize_t335sendto(int socket, const void *buffer, size_t length, int flags,336const struct sockaddr *dest_addr, socklen_t dest_len);337*/338sendto(339socket: number,340bufPtr: number,341length: number,342flags: number,343addressPtr: number,344addressLen: number345): number {346log("sendto", {347socket,348bufPtr,349length,350flags,351addressPtr,352addressLen,353});354if (posix.sendto == null) {355throw Errno("ENOTSUP");356}357const buffer = Buffer.alloc(length);358const destination = getSockaddr(addressPtr, addressLen);359const bytesSent = posix.sendto(360native_fd(socket),361buffer,362native_flags(flags),363destination364);365366log("sendto sent ", bytesSent);367return bytesSent;368},369370/*371int shutdown(int socket, int how);372*/373shutdown(socket: number, how: number): number {374log("shutdown", { socket, how });375if (posix.shutdown == null) {376throw Errno("ENOTSUP");377}378let real_how = -1;379for (const name of ["SHUT_RD", "SHUT_WR", "SHUT_RDWR"]) {380if (how == constants[name]) {381real_how = posix.constants[name];382break;383}384}385if (real_how == -1) {386throw Errno("EINVAL");387}388posix.shutdown(native_fd(socket), real_how);389return 0;390},391392/*393listen – listen for connections on a socket394395int listen(int socket, int backlog);396*/397listen(socket: number, backlog: number): number {398log("listen", { socket, backlog });399if (posix.listen == null) {400throw Errno("ENOTSUP");401}402return posix.listen(native_fd(socket), backlog);403},404405/*406accept – accept a connection on a socket407int accept(int socket, struct sockaddr *address, socklen_t *address_len);408*/409accept(socket: number, sockaddrPtr: number, socklenPtr: number): number {410log("accept", { socket });411if (posix.accept == null) {412throw Errno("ENOTSUP");413}414const { fd, sockaddr } = posix.accept(native_fd(socket));415sendNativeSockaddr(sockaddr, sockaddrPtr);416send.u32(socklenPtr, sockaddr.sa_len);417log("accept got back ", { fd, sockaddr });418return createWasiFd(fd, getSocktype(socket));419},420421/*422int getsockopt(int socket, int level, int option_name, void *option_value,423socklen_t *option_len);424425TODO: This is of very limited value right now since the result426native, hence just wrong, and it's so opaque I don't know how to convert427it back in general. That said, I did socktype as a special case properly.428*/429getsockopt(430socket: number,431level: number,432option_name: number,433option_value_ptr: number,434option_len_ptr: number435): number {436log("getsockopt", {437socket,438level,439option_name,440option_value_ptr,441option_len_ptr,442});443444if (level == constants.SOL_SOCKET && option_name == constants.SO_TYPE) {445// special case we can handle easily -- getting the type of a socket.446const socktype = getSocktype(socket);447const ab = new ArrayBuffer(4);448const dv = new DataView(ab);449dv.setUint32(0, socktype, true);450const option = Buffer.from(ab);451send.buffer(option, option_value_ptr);452send.i32(option_len_ptr, option.length);453return 0;454}455456if (posix.getsockopt == null) {457throw Errno("ENOTSUP");458}459const option = posix.getsockopt(460native_fd(socket),461native_level(level),462native_option_name(option_name),463recv.i32(option_len_ptr)464);465send.buffer(option, option_value_ptr);466send.i32(option_len_ptr, option.length);467return 0;468},469470/*471int setsockopt(int socket, int level, int option_name, const void *option_value,472socklen_t option_len);473*/474setsockopt(475socket: number,476level: number,477option_name: number,478option_value_ptr: number,479option_len: number480): number {481log("setsockopt", {482socket,483level,484option_name,485option_value_ptr,486option_len,487});488if (posix.setsockopt == null) {489throw Errno("ENOTSUP");490}491const option = recv.buffer(option_value_ptr, option_len);492posix.setsockopt(493native_fd(socket),494native_level(level),495native_option_name(option_name),496option497);498return 0;499},500501pollSocket(502socket: number,503type: "read" | "write",504timeout_ms: number505): number {506//log("pollForSocket", { socket, type, timeout_ms });507//return 0;508// TODO: The code below doesn't work properly -- it ALWAYS waits for the full timeout_ms,509// which is very annoying in practice, e.g., making pip hang 15s before each operation.510// Thus we replace the timeout with the min of the timeout and 250ms for now.511if (posix.pollSocket == null) {512return 0; // stub513// return wasi_constants.WASI_ENOSYS;514}515posix.pollSocket(516native_fd(socket),517type == "read" ? constants.POLLIN : constants.POLLOUT,518Math.min(250, timeout_ms)519);520return 0;521},522};523}524525526