]> git.proxmox.com Git - wasi-libc.git/commitdiff
add wasip2 implementations of more socket APIs (#482)
authorJoel Dice <joel.dice@fermyon.com>
Tue, 19 Mar 2024 00:54:07 +0000 (18:54 -0600)
committerFabian Grünbichler <f.gruenbichler@proxmox.com>
Tue, 25 Jun 2024 10:13:35 +0000 (12:13 +0200)
This adds `wasm32-wasip2` implementations of `shutdown`, `getsockopt`, and
`setsockopt`.  It also extends the existing `ioctl` implementation to handle
both p1 and p2 file descriptors since we can't know until runtime which kind we
have. Once we've moved `wasm32-wasip2` fully to WASI 0.2 and remove the need for
the p1 adapter, we'll be able to switch to separate p1 and p2 `ioctl`
implementations.

Signed-off-by: Joel Dice <joel.dice@fermyon.com>
Co-authored-by: Dave Bakker <github@davebakker.io>
Makefile
expected/wasm32-wasip2/defined-symbols.txt
libc-bottom-half/cloudlibc/src/libc/sys/ioctl/ioctl.c
libc-bottom-half/sources/shutdown.c [new file with mode: 0644]
libc-bottom-half/sources/sockopt.c [new file with mode: 0644]

index f7dde27404b6440e750f4c44e2329bf53c74ac5a..b2edb95e847b06c191ecf747fb7f824311fed7d0 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -89,7 +89,9 @@ LIBC_BOTTOM_HALF_OMIT_SOURCES := \
        $(LIBC_BOTTOM_HALF_SOURCES)/sockets_utils.c \
        $(LIBC_BOTTOM_HALF_SOURCES)/bind.c \
        $(LIBC_BOTTOM_HALF_SOURCES)/listen.c \
-       $(LIBC_BOTTOM_HALF_SOURCES)/accept-wasip2.c
+       $(LIBC_BOTTOM_HALF_SOURCES)/accept-wasip2.c \
+       $(LIBC_BOTTOM_HALF_SOURCES)/shutdown.c \
+       $(LIBC_BOTTOM_HALF_SOURCES)/sockopt.c
 LIBC_BOTTOM_HALF_ALL_SOURCES := $(filter-out $(LIBC_BOTTOM_HALF_OMIT_SOURCES),$(LIBC_BOTTOM_HALF_ALL_SOURCES))
 # Omit p2-specific headers from include-all.c test.
 INCLUDE_ALL_CLAUSES := -not -name wasip2.h -not -name descriptor_table.h
@@ -100,6 +102,8 @@ ifeq ($(WASI_SNAPSHOT), p2)
 LIBC_BOTTOM_HALF_OMIT_SOURCES := \
        $(LIBC_BOTTOM_HALF_CLOUDLIBC_SRC)/libc/sys/socket/send.c \
        $(LIBC_BOTTOM_HALF_CLOUDLIBC_SRC)/libc/sys/socket/recv.c \
+       $(LIBC_BOTTOM_HALF_CLOUDLIBC_SRC)/libc/sys/socket/shutdown.c \
+       $(LIBC_BOTTOM_HALF_CLOUDLIBC_SRC)/libc/sys/socket/getsockopt.c \
        $(LIBC_BOTTOM_HALF_SOURCES)/accept-wasip1.c
 LIBC_BOTTOM_HALF_ALL_SOURCES := $(filter-out $(LIBC_BOTTOM_HALF_OMIT_SOURCES),$(LIBC_BOTTOM_HALF_ALL_SOURCES))
 endif
index 1f61dd8d5302baaa17a3f063eb660e22de9514b4..772a486b9726cb4942f93d51773cdd5013041775 100644 (file)
@@ -1,3 +1,4 @@
+NS_PER_S
 _CLOCK_MONOTONIC
 _CLOCK_REALTIME
 _Exit
@@ -1111,6 +1112,7 @@ setenv
 setkey
 setlinebuf
 setlocale
+setsockopt
 setstate
 setvbuf
 shutdown
@@ -1243,6 +1245,7 @@ tcp_bind
 tcp_borrow_tcp_socket
 tcp_create_socket_create_tcp_socket
 tcp_create_socket_result_own_tcp_socket_error_code_free
+tcp_getsockopt
 tcp_ip_socket_address_free
 tcp_listen
 tcp_method_tcp_socket_accept
@@ -1282,6 +1285,8 @@ tcp_result_u32_error_code_free
 tcp_result_u64_error_code_free
 tcp_result_u8_error_code_free
 tcp_result_void_error_code_free
+tcp_setsockopt
+tcp_shutdown
 tcp_tcp_socket_drop_borrow
 tcp_tcp_socket_drop_own
 tdelete
@@ -1332,6 +1337,7 @@ udp_borrow_outgoing_datagram_stream
 udp_borrow_udp_socket
 udp_create_socket_create_udp_socket
 udp_create_socket_result_own_udp_socket_error_code_free
+udp_getsockopt
 udp_incoming_datagram_free
 udp_incoming_datagram_stream_drop_borrow
 udp_incoming_datagram_stream_drop_own
@@ -1367,6 +1373,8 @@ udp_result_tuple2_own_incoming_datagram_stream_own_outgoing_datagram_stream_erro
 udp_result_u64_error_code_free
 udp_result_u8_error_code_free
 udp_result_void_error_code_free
+udp_setsockopt
+udp_shutdown
 udp_udp_socket_drop_borrow
 udp_udp_socket_drop_own
 uname
index 7d03cc61ad5b817a341f36f492195312a6298e27..2c968b2a05f129a3b0d0e0b591051c7f1e63fa15 100644 (file)
@@ -4,11 +4,66 @@
 
 #include <sys/ioctl.h>
 
-#include <wasi/api.h>
 #include <errno.h>
 #include <stdarg.h>
 
+#include <wasi/api.h>
+#ifdef __wasilibc_use_wasip2
+#include <wasi/descriptor_table.h>
+#endif
+
 int ioctl(int fildes, int request, ...) {
+#ifdef __wasilibc_use_wasip2
+       descriptor_table_entry_t *entry;
+       if (descriptor_table_get_ref(fildes, &entry)) {
+               switch (entry->tag) {
+               case DESCRIPTOR_TABLE_ENTRY_TCP_SOCKET: {
+                       tcp_socket_t *socket = &entry->tcp_socket;
+                       switch (request) {
+                       case FIONBIO: {
+                               va_list ap;
+                               va_start(ap, request);
+                               socket->blocking = *va_arg(ap, const int *) ==
+                                                  0;
+                               va_end(ap);
+
+                               return 0;
+                       }
+
+                       default:
+                               // TODO wasi-sockets: anything else we should support?
+                               errno = EINVAL;
+                               return -1;
+                       }
+               }
+
+               case DESCRIPTOR_TABLE_ENTRY_UDP_SOCKET: {
+                       udp_socket_t *socket = &entry->udp_socket;
+                       switch (request) {
+                       case FIONBIO: {
+                               va_list ap;
+                               va_start(ap, request);
+                               socket->blocking = *va_arg(ap, const int *) ==
+                                                  0;
+                               va_end(ap);
+
+                               return 0;
+                       }
+
+                       default:
+                               // TODO wasi-sockets: anything else we should support?
+                               errno = EINVAL;
+                               return -1;
+                       }
+               }
+
+               default:
+                       errno = ENOPROTOOPT;
+                       return -1;
+               }
+       }
+#endif // __wasilibc_use_wasip2
+
   switch (request) {
     case FIONREAD: {
       // Poll the file descriptor to determine how many bytes can be read.
diff --git a/libc-bottom-half/sources/shutdown.c b/libc-bottom-half/sources/shutdown.c
new file mode 100644 (file)
index 0000000..3ffd479
--- /dev/null
@@ -0,0 +1,80 @@
+#include <sys/socket.h>
+
+#include <errno.h>
+
+#include <wasi/api.h>
+#include <wasi/descriptor_table.h>
+#include <wasi/sockets_utils.h>
+
+int tcp_shutdown(tcp_socket_t *socket, int posix_how)
+{
+       tcp_shutdown_type_t wasi_how;
+       switch (posix_how) {
+       case SHUT_RD:
+               wasi_how = TCP_SHUTDOWN_TYPE_RECEIVE;
+               break;
+       case SHUT_WR:
+               wasi_how = TCP_SHUTDOWN_TYPE_SEND;
+               break;
+       case SHUT_RDWR:
+               wasi_how = TCP_SHUTDOWN_TYPE_BOTH;
+               break;
+       default:
+               errno = EINVAL;
+               return -1;
+       }
+
+       tcp_socket_state_connected_t connection;
+       if (socket->state.tag == TCP_SOCKET_STATE_CONNECTED) {
+               connection = socket->state.connected;
+       } else {
+               errno = ENOTCONN;
+               return -1;
+       }
+
+       network_error_code_t error;
+       tcp_borrow_tcp_socket_t socket_borrow =
+               tcp_borrow_tcp_socket(socket->socket);
+       if (!tcp_method_tcp_socket_shutdown(socket_borrow, wasi_how, &error)) {
+               errno = __wasi_sockets_utils__map_error(error);
+               return -1;
+       }
+
+       if (posix_how == SHUT_RD || posix_how == SHUT_RDWR) {
+               // TODO wasi-sockets: drop input stream (if not already). And
+               // update `recv` to take dropped input streams into account.
+       }
+
+       if (posix_how == SHUT_WR || posix_how == SHUT_RDWR) {
+               // TODO wasi-sockets: drop output stream (if not already). And
+               // update `send` to take dropped output streams into account.
+       }
+
+       return 0;
+}
+
+int udp_shutdown(udp_socket_t *socket, int posix_how)
+{
+       // UDP has nothing to shut down.
+       errno = EOPNOTSUPP;
+       return -1;
+}
+
+int shutdown(int socket, int how)
+{
+       descriptor_table_entry_t *entry;
+       if (!descriptor_table_get_ref(socket, &entry)) {
+               errno = EBADF;
+               return -1;
+       }
+
+       switch (entry->tag) {
+       case DESCRIPTOR_TABLE_ENTRY_TCP_SOCKET:
+               return tcp_shutdown(&entry->tcp_socket, how);
+       case DESCRIPTOR_TABLE_ENTRY_UDP_SOCKET:
+               return udp_shutdown(&entry->udp_socket, how);
+       default:
+               errno = EOPNOTSUPP;
+               return -1;
+       }
+}
diff --git a/libc-bottom-half/sources/sockopt.c b/libc-bottom-half/sources/sockopt.c
new file mode 100644 (file)
index 0000000..863d115
--- /dev/null
@@ -0,0 +1,683 @@
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+
+#include <errno.h>
+#include <limits.h>
+#include <string.h>
+
+#include <wasi/api.h>
+#include <wasi/descriptor_table.h>
+#include <wasi/sockets_utils.h>
+
+const uint64_t NS_PER_S = 1000000000;
+
+int tcp_getsockopt(tcp_socket_t *socket, int level, int optname,
+                  void *restrict optval, socklen_t *restrict optlen)
+{
+       int value = 0;
+
+       network_error_code_t error;
+       tcp_borrow_tcp_socket_t socket_borrow =
+               tcp_borrow_tcp_socket(socket->socket);
+
+       switch (level) {
+       case SOL_SOCKET:
+               switch (optname) {
+               case SO_TYPE: {
+                       value = SOCK_STREAM;
+                       break;
+               }
+               case SO_PROTOCOL: {
+                       value = IPPROTO_TCP;
+                       break;
+               }
+               case SO_DOMAIN: {
+                       value = __wasi_sockets_utils__posix_family(
+                               socket->family);
+                       break;
+               }
+               case SO_ERROR: {
+                       if (socket->state.tag ==
+                           TCP_SOCKET_STATE_CONNECT_FAILED) {
+                               value = __wasi_sockets_utils__map_error(
+                                       socket->state.connect_failed.error_code);
+                               socket->state.connect_failed.error_code = 0;
+                       } else {
+                               value = 0;
+                       }
+                       break;
+               }
+               case SO_ACCEPTCONN: {
+                       bool is_listening = socket->state.tag ==
+                                           TCP_SOCKET_STATE_LISTENING;
+                       if (is_listening !=
+                           tcp_method_tcp_socket_is_listening(
+                                   socket_borrow)) { // Sanity check.
+                               abort();
+                       }
+                       value = is_listening;
+                       break;
+               }
+               case SO_KEEPALIVE: {
+                       bool result;
+                       if (!tcp_method_tcp_socket_keep_alive_enabled(
+                                   socket_borrow, &result, &error)) {
+                               errno = __wasi_sockets_utils__map_error(error);
+                               return -1;
+                       }
+
+                       value = result;
+                       break;
+               }
+               case SO_RCVBUF: {
+                       uint64_t result;
+                       if (!tcp_method_tcp_socket_receive_buffer_size(
+                                   socket_borrow, &result, &error)) {
+                               errno = __wasi_sockets_utils__map_error(error);
+                               return -1;
+                       }
+
+                       if (result > INT_MAX) {
+                               abort();
+                       }
+
+                       value = result;
+                       break;
+               }
+               case SO_SNDBUF: {
+                       uint64_t result;
+                       if (!tcp_method_tcp_socket_send_buffer_size(
+                                   socket_borrow, &result, &error)) {
+                               errno = __wasi_sockets_utils__map_error(error);
+                               return -1;
+                       }
+
+                       if (result > INT_MAX) {
+                               abort();
+                       }
+
+                       value = result;
+                       break;
+               }
+               case SO_REUSEADDR: {
+                       value = socket->fake_reuseaddr;
+                       break;
+               }
+               case SO_RCVTIMEO: // TODO wasi-sockets: emulate in wasi-libc itself
+               case SO_SNDTIMEO: // TODO wasi-sockets: emulate in wasi-libc itself
+               default:
+                       errno = ENOPROTOOPT;
+                       return -1;
+               }
+               break;
+
+       case SOL_IP:
+               switch (optname) {
+               case IP_TTL: {
+                       if (socket->family != NETWORK_IP_ADDRESS_FAMILY_IPV4) {
+                               errno = EAFNOSUPPORT;
+                               return -1;
+                       }
+
+                       uint8_t result;
+                       if (!tcp_method_tcp_socket_hop_limit(socket_borrow,
+                                                            &result, &error)) {
+                               errno = __wasi_sockets_utils__map_error(error);
+                               return -1;
+                       }
+
+                       value = result;
+                       break;
+               }
+               default:
+                       errno = ENOPROTOOPT;
+                       return -1;
+               }
+               break;
+
+       case SOL_IPV6:
+               switch (optname) {
+               case IPV6_UNICAST_HOPS: {
+                       if (socket->family != NETWORK_IP_ADDRESS_FAMILY_IPV6) {
+                               errno = EAFNOSUPPORT;
+                               return -1;
+                       }
+
+                       uint8_t result;
+                       if (!tcp_method_tcp_socket_hop_limit(socket_borrow,
+                                                            &result, &error)) {
+                               errno = __wasi_sockets_utils__map_error(error);
+                               return -1;
+                       }
+
+                       value = result;
+                       break;
+               }
+               default:
+                       errno = ENOPROTOOPT;
+                       return -1;
+               }
+               break;
+
+       case SOL_TCP:
+               switch (optname) {
+               case TCP_NODELAY: {
+                       value = socket->fake_nodelay;
+                       break;
+               }
+               case TCP_KEEPIDLE: {
+                       tcp_duration_t result_ns;
+                       if (!tcp_method_tcp_socket_keep_alive_idle_time(
+                                   socket_borrow, &result_ns, &error)) {
+                               errno = __wasi_sockets_utils__map_error(error);
+                               return -1;
+                       }
+
+                       uint64_t result_s = result_ns / NS_PER_S;
+                       if (result_s == 0) {
+                               result_s =
+                                       1; // Value was rounded down to zero. Round it up instead, because 0 is an invalid value for this socket option.
+                       }
+
+                       if (result_s > INT_MAX) {
+                               abort();
+                       }
+
+                       value = result_s;
+                       break;
+               }
+               case TCP_KEEPINTVL: {
+                       tcp_duration_t result_ns;
+                       if (!tcp_method_tcp_socket_keep_alive_interval(
+                                   socket_borrow, &result_ns, &error)) {
+                               errno = __wasi_sockets_utils__map_error(error);
+                               return -1;
+                       }
+
+                       uint64_t result_s = result_ns / NS_PER_S;
+                       if (result_s == 0) {
+                               result_s =
+                                       1; // Value was rounded down to zero. Round it up instead, because 0 is an invalid value for this socket option.
+                       }
+
+                       if (result_s > INT_MAX) {
+                               abort();
+                       }
+
+                       value = result_s;
+                       break;
+               }
+               case TCP_KEEPCNT: {
+                       uint32_t result;
+                       if (!tcp_method_tcp_socket_keep_alive_count(
+                                   socket_borrow, &result, &error)) {
+                               errno = __wasi_sockets_utils__map_error(error);
+                               return -1;
+                       }
+
+                       if (result > INT_MAX) {
+                               abort();
+                       }
+
+                       value = result;
+                       break;
+                       break;
+               }
+               default:
+                       errno = ENOPROTOOPT;
+                       return -1;
+               }
+               break;
+
+       default:
+               errno = ENOPROTOOPT;
+               return -1;
+       }
+
+       // Copy out integer value.
+       memcpy(optval, &value, *optlen < sizeof(int) ? *optlen : sizeof(int));
+       *optlen = sizeof(int);
+       return 0;
+}
+
+int tcp_setsockopt(tcp_socket_t *socket, int level, int optname,
+                  const void *optval, socklen_t optlen)
+{
+       int intval = *(int *)optval;
+
+       network_error_code_t error;
+       tcp_borrow_tcp_socket_t socket_borrow =
+               tcp_borrow_tcp_socket(socket->socket);
+
+       switch (level) {
+       case SOL_SOCKET:
+               switch (optname) {
+               case SO_KEEPALIVE: {
+                       if (!tcp_method_tcp_socket_set_keep_alive_enabled(
+                                   socket_borrow, intval != 0, &error)) {
+                               errno = __wasi_sockets_utils__map_error(error);
+                               return -1;
+                       }
+
+                       return 0;
+               }
+               case SO_RCVBUF: {
+                       if (!tcp_method_tcp_socket_set_receive_buffer_size(
+                                   socket_borrow, intval, &error)) {
+                               errno = __wasi_sockets_utils__map_error(error);
+                               return -1;
+                       }
+
+                       return 0;
+               }
+               case SO_SNDBUF: {
+                       if (!tcp_method_tcp_socket_set_send_buffer_size(
+                                   socket_borrow, intval, &error)) {
+                               errno = __wasi_sockets_utils__map_error(error);
+                               return -1;
+                       }
+
+                       return 0;
+               }
+               case SO_REUSEADDR: {
+                       // As of this writing, WASI has no support for changing SO_REUSEADDR
+                       // -- it's enabled by default and cannot be disabled.  To keep
+                       // applications happy, we pretend to support enabling and disabling
+                       // it.
+                       socket->fake_reuseaddr = (intval != 0);
+                       return 0;
+               }
+               case SO_RCVTIMEO: // TODO wasi-sockets: emulate in wasi-libc itself
+               case SO_SNDTIMEO: // TODO wasi-sockets: emulate in wasi-libc itself
+               default:
+                       errno = ENOPROTOOPT;
+                       return -1;
+               }
+               break;
+
+       case SOL_IP:
+               switch (optname) {
+               case IP_TTL: {
+                       if (socket->family != NETWORK_IP_ADDRESS_FAMILY_IPV4) {
+                               errno = EAFNOSUPPORT;
+                               return -1;
+                       }
+
+                       if (intval < 0 || intval > UINT8_MAX) {
+                               errno = EINVAL;
+                               return -1;
+                       }
+
+                       if (!tcp_method_tcp_socket_set_hop_limit(
+                                   socket_borrow, intval, &error)) {
+                               errno = __wasi_sockets_utils__map_error(error);
+                               return -1;
+                       }
+
+                       return 0;
+               }
+               default:
+                       errno = ENOPROTOOPT;
+                       return -1;
+               }
+               break;
+
+       case SOL_IPV6:
+               switch (optname) {
+               case IPV6_UNICAST_HOPS: {
+                       if (socket->family != NETWORK_IP_ADDRESS_FAMILY_IPV6) {
+                               errno = EAFNOSUPPORT;
+                               return -1;
+                       }
+
+                       if (intval < 0 || intval > UINT8_MAX) {
+                               errno = EINVAL;
+                               return -1;
+                       }
+
+                       if (!tcp_method_tcp_socket_set_hop_limit(
+                                   socket_borrow, intval, &error)) {
+                               errno = __wasi_sockets_utils__map_error(error);
+                               return -1;
+                       }
+
+                       return 0;
+               }
+               default:
+                       errno = ENOPROTOOPT;
+                       return -1;
+               }
+               break;
+
+       case SOL_TCP:
+               switch (optname) {
+               case TCP_NODELAY: {
+                       // At the time of writing, WASI has no support for TCP_NODELAY.
+                       // Yet, many applications expect this option to be implemented.
+                       // To ensure those applications can run on WASI at all, we fake
+                       // support for it by recording the value, but not doing anything
+                       // with it.
+                       // If/when WASI adds true support, we can remove this workaround
+                       // and implement it properly. From the application's perspective
+                       // the "worst" thing that can then happen is that it automagically
+                       // becomes faster.
+                       socket->fake_nodelay = (intval != 0);
+                       return 0;
+               }
+               case TCP_KEEPIDLE: {
+                       tcp_duration_t duration = intval * NS_PER_S;
+                       if (!tcp_method_tcp_socket_set_keep_alive_idle_time(
+                                   socket_borrow, duration, &error)) {
+                               errno = __wasi_sockets_utils__map_error(error);
+                               return -1;
+                       }
+
+                       return 0;
+               }
+               case TCP_KEEPINTVL: {
+                       tcp_duration_t duration = intval * NS_PER_S;
+                       if (!tcp_method_tcp_socket_set_keep_alive_interval(
+                                   socket_borrow, duration, &error)) {
+                               errno = __wasi_sockets_utils__map_error(error);
+                               return -1;
+                       }
+
+                       return 0;
+               }
+               case TCP_KEEPCNT: {
+                       if (!tcp_method_tcp_socket_set_keep_alive_count(
+                                   socket_borrow, intval, &error)) {
+                               errno = __wasi_sockets_utils__map_error(error);
+                               return -1;
+                       }
+
+                       return 0;
+               }
+               default:
+                       errno = ENOPROTOOPT;
+                       return -1;
+               }
+               break;
+
+       default:
+               errno = ENOPROTOOPT;
+               return -1;
+       }
+}
+
+int udp_getsockopt(udp_socket_t *socket, int level, int optname,
+                  void *restrict optval, socklen_t *restrict optlen)
+{
+       int value;
+
+       network_error_code_t error;
+       udp_borrow_udp_socket_t socket_borrow =
+               udp_borrow_udp_socket(socket->socket);
+
+       switch (level) {
+       case SOL_SOCKET:
+               switch (optname) {
+               case SO_TYPE: {
+                       value = SOCK_DGRAM;
+                       break;
+               }
+               case SO_PROTOCOL: {
+                       value = IPPROTO_UDP;
+                       break;
+               }
+               case SO_DOMAIN: {
+                       value = __wasi_sockets_utils__posix_family(
+                               socket->family);
+                       break;
+               }
+               case SO_RCVBUF: {
+                       uint64_t result;
+                       if (!udp_method_udp_socket_receive_buffer_size(
+                                   socket_borrow, &result, &error)) {
+                               errno = __wasi_sockets_utils__map_error(error);
+                               return -1;
+                       }
+
+                       if (result > INT_MAX) {
+                               abort();
+                       }
+
+                       value = result;
+                       break;
+               }
+               case SO_SNDBUF: {
+                       uint64_t result;
+                       if (!udp_method_udp_socket_send_buffer_size(
+                                   socket_borrow, &result, &error)) {
+                               errno = __wasi_sockets_utils__map_error(error);
+                               return -1;
+                       }
+
+                       if (result > INT_MAX) {
+                               abort();
+                       }
+
+                       value = result;
+                       break;
+               }
+               case SO_RCVTIMEO: // TODO wasi-sockets: emulate in wasi-libc itself
+               case SO_SNDTIMEO: // TODO wasi-sockets: emulate in wasi-libc itself
+               default:
+                       errno = ENOPROTOOPT;
+                       return -1;
+               }
+               break;
+
+       case SOL_IP:
+               switch (optname) {
+               case IP_TTL: {
+                       if (socket->family != NETWORK_IP_ADDRESS_FAMILY_IPV4) {
+                               errno = EAFNOSUPPORT;
+                               return -1;
+                       }
+
+                       uint8_t result;
+                       if (!udp_method_udp_socket_unicast_hop_limit(
+                                   socket_borrow, &result, &error)) {
+                               errno = __wasi_sockets_utils__map_error(error);
+                               return -1;
+                       }
+
+                       value = result;
+                       break;
+               }
+               default:
+                       errno = ENOPROTOOPT;
+                       return -1;
+               }
+               break;
+
+       case SOL_IPV6:
+               switch (optname) {
+               case IPV6_UNICAST_HOPS: {
+                       if (socket->family != NETWORK_IP_ADDRESS_FAMILY_IPV6) {
+                               errno = EAFNOSUPPORT;
+                               return -1;
+                       }
+
+                       uint8_t result;
+                       if (!udp_method_udp_socket_unicast_hop_limit(
+                                   socket_borrow, &result, &error)) {
+                               errno = __wasi_sockets_utils__map_error(error);
+                               return -1;
+                       }
+
+                       value = result;
+                       break;
+               }
+               default:
+                       errno = ENOPROTOOPT;
+                       return -1;
+               }
+               break;
+
+       default:
+               errno = ENOPROTOOPT;
+               return -1;
+       }
+
+       // Copy out integer value.
+       memcpy(optval, &value, *optlen < sizeof(int) ? *optlen : sizeof(int));
+       *optlen = sizeof(int);
+       return 0;
+}
+
+int udp_setsockopt(udp_socket_t *socket, int level, int optname,
+                  const void *optval, socklen_t optlen)
+{
+       int intval = *(int *)optval;
+
+       network_error_code_t error;
+       udp_borrow_udp_socket_t socket_borrow =
+               udp_borrow_udp_socket(socket->socket);
+
+       switch (level) {
+       case SOL_SOCKET:
+               switch (optname) {
+               case SO_RCVBUF: {
+                       if (!udp_method_udp_socket_set_receive_buffer_size(
+                                   socket_borrow, intval, &error)) {
+                               errno = __wasi_sockets_utils__map_error(error);
+                               return -1;
+                       }
+
+                       return 0;
+               }
+               case SO_SNDBUF: {
+                       if (!udp_method_udp_socket_set_send_buffer_size(
+                                   socket_borrow, intval, &error)) {
+                               errno = __wasi_sockets_utils__map_error(error);
+                               return -1;
+                       }
+
+                       return 0;
+               }
+               case SO_RCVTIMEO: // TODO wasi-sockets: emulate in wasi-libc itself
+               case SO_SNDTIMEO: // TODO wasi-sockets: emulate in wasi-libc itself
+               default:
+                       errno = ENOPROTOOPT;
+                       return -1;
+               }
+               break;
+
+       case SOL_IP:
+               switch (optname) {
+               case IP_TTL: {
+                       if (socket->family != NETWORK_IP_ADDRESS_FAMILY_IPV4) {
+                               errno = EAFNOSUPPORT;
+                               return -1;
+                       }
+
+                       if (intval < 0 || intval > UINT8_MAX) {
+                               errno = EINVAL;
+                               return -1;
+                       }
+
+                       if (!udp_method_udp_socket_set_unicast_hop_limit(
+                                   socket_borrow, intval, &error)) {
+                               errno = __wasi_sockets_utils__map_error(error);
+                               return -1;
+                       }
+
+                       return 0;
+               }
+               default:
+                       errno = ENOPROTOOPT;
+                       return -1;
+               }
+               break;
+
+       case SOL_IPV6:
+               switch (optname) {
+               case IPV6_UNICAST_HOPS: {
+                       if (socket->family != NETWORK_IP_ADDRESS_FAMILY_IPV6) {
+                               errno = EAFNOSUPPORT;
+                               return -1;
+                       }
+
+                       if (intval < 0 || intval > UINT8_MAX) {
+                               errno = EINVAL;
+                               return -1;
+                       }
+
+                       if (!udp_method_udp_socket_set_unicast_hop_limit(
+                                   socket_borrow, intval, &error)) {
+                               errno = __wasi_sockets_utils__map_error(error);
+                               return -1;
+                       }
+
+                       return 0;
+               }
+               default:
+                       errno = ENOPROTOOPT;
+                       return -1;
+               }
+               break;
+
+       default:
+               errno = ENOPROTOOPT;
+               return -1;
+       }
+}
+
+int getsockopt(int sockfd, int level, int optname, void *restrict optval,
+              socklen_t *restrict optlen)
+{
+       descriptor_table_entry_t *entry;
+       if (!descriptor_table_get_ref(sockfd, &entry)) {
+               errno = EBADF;
+               return -1;
+       }
+
+       if (optval == NULL || optlen == NULL || *optlen < sizeof(int)) {
+               // FYI, the protocol-specific implementations implicitly depend on these checks.
+               errno = EINVAL;
+               return -1;
+       }
+
+       switch (entry->tag) {
+       case DESCRIPTOR_TABLE_ENTRY_TCP_SOCKET:
+               return tcp_getsockopt(&entry->tcp_socket, level, optname,
+                                     optval, optlen);
+       case DESCRIPTOR_TABLE_ENTRY_UDP_SOCKET:
+               return udp_getsockopt(&entry->udp_socket, level, optname,
+                                     optval, optlen);
+       default:
+               errno = ENOPROTOOPT;
+               return -1;
+       }
+}
+
+int setsockopt(int sockfd, int level, int optname, const void *optval,
+              socklen_t optlen)
+{
+       descriptor_table_entry_t *entry;
+       if (!descriptor_table_get_ref(sockfd, &entry)) {
+               errno = EBADF;
+               return -1;
+       }
+
+       if (optval == NULL || optlen < sizeof(int)) {
+               // FYI, the protocol-specific implementations implicitly depend on these checks.
+               errno = EINVAL;
+               return -1;
+       }
+
+       switch (entry->tag) {
+       case DESCRIPTOR_TABLE_ENTRY_TCP_SOCKET:
+               return tcp_setsockopt(&entry->tcp_socket, level, optname,
+                                     optval, optlen);
+       case DESCRIPTOR_TABLE_ENTRY_UDP_SOCKET:
+               return udp_setsockopt(&entry->udp_socket, level, optname,
+                                     optval, optlen);
+       default:
+               errno = ENOPROTOOPT;
+               return -1;
+       }
+}