Repository: bytecodealliance/wasmtime
Pull request #7518 (2023-11-09 21:03:27)

[15.0.0] wasi-filesystem: delete access modes
This is a backport of #7510


Patch:
diff --git a/crates/wasi-http/wit/deps/filesystem/types.wit b/crates/wasi-http/wit/deps/filesystem/types.wit
index 37700332b..4ab5dc055 100644
--- a/crates/wasi-http/wit/deps/filesystem/types.wit
+++ b/crates/wasi-http/wit/deps/filesystem/types.wit
@@ -142,29 +142,6 @@ interface types {
         truncate,
     }
 
-    /// Permissions mode used by `open-at`, `change-file-permissions-at`, and
-    /// similar.
-    flags modes {
-        /// True if the resource is considered readable by the containing
-        /// filesystem.
-        readable,
-        /// True if the resource is considered writable by the containing
-        /// filesystem.
-        writable,
-        /// True if the resource is considered executable by the containing
-        /// filesystem. This does not apply to directories.
-        executable,
-    }
-
-    /// Access type used by `access-at`.
-    variant access-type {
-        /// Test for readability, writeability, or executability.
-        access(modes),
-
-        /// Test whether the path exists.
-        exists,
-    }
-
     /// Number of hard links to an inode.
     type link-count = u64;
 
@@ -538,8 +515,6 @@ interface types {
             open-flags: open-flags,
             /// Flags to use for the resulting descriptor.
             %flags: descriptor-flags,
-            /// Permissions to use when creating a new file.
-            modes: modes
         ) -> result<descriptor, error-code>;
 
         /// Read the contents of a symbolic link.
@@ -588,25 +563,6 @@ interface types {
             new-path: string,
         ) -> result<_, error-code>;
 
-        /// Check accessibility of a filesystem path.
-        ///
-        /// Check whether the given filesystem path names an object which is
-        /// readable, writable, or executable, or whether it exists.
-        ///
-        /// This does not a guarantee that subsequent accesses will succeed, as
-        /// filesystem permissions may be modified asynchronously by external
-        /// entities.
-        ///
-        /// Note: This is similar to `faccessat` with the `AT_EACCESS` flag in POSIX.
-        access-at: func(
-            /// Flags determining the method of how the path is resolved.
-            path-flags: path-flags,
-            /// The relative path to check.
-            path: string,
-            /// The type of check to perform.
-            %type: access-type
-        ) -> result<_, error-code>;
-
         /// Unlink a filesystem object that is not a directory.
         ///
         /// Return `error-code::is-directory` if the path refers to a directory.
@@ -616,40 +572,6 @@ interface types {
             path: string,
         ) -> result<_, error-code>;
 
-        /// Change the permissions of a filesystem object that is not a directory.
-        ///
-        /// Note that the ultimate meanings of these permissions is
-        /// filesystem-specific.
-        ///
-        /// Note: This is similar to `fchmodat` in POSIX.
-        change-file-permissions-at: func(
-            /// Flags determining the method of how the path is resolved.
-            path-flags: path-flags,
-            /// The relative path to operate on.
-            path: string,
-            /// The new permissions for the filesystem object.
-            modes: modes,
-        ) -> result<_, error-code>;
-
-        /// Change the permissions of a directory.
-        ///
-        /// Note that the ultimate meanings of these permissions is
-        /// filesystem-specific.
-        ///
-        /// Unlike in POSIX, the `executable` flag is not reinterpreted as a "search"
-        /// flag. `read` on a directory implies readability and searchability, and
-        /// `execute` is not valid for directories.
-        ///
-        /// Note: This is similar to `fchmodat` in POSIX.
-        change-directory-permissions-at: func(
-            /// Flags determining the method of how the path is resolved.
-            path-flags: path-flags,
-            /// The relative path to operate on.
-            path: string,
-            /// The new permissions for the directory.
-            modes: modes,
-        ) -> result<_, error-code>;
-
         /// Test whether two descriptors refer to the same filesystem object.
         ///
         /// In POSIX, this corresponds to testing whether the two descriptors have the
diff --git a/crates/wasi-http/wit/deps/sockets/tcp.wit b/crates/wasi-http/wit/deps/sockets/tcp.wit
index b9ef0625a..f0d875095 100644
--- a/crates/wasi-http/wit/deps/sockets/tcp.wit
+++ b/crates/wasi-http/wit/deps/sockets/tcp.wit
@@ -2,6 +2,7 @@
 interface tcp {
     use wasi:io/streams@0.2.0-rc-2023-11-05.{input-stream, output-stream};
     use wasi:io/poll@0.2.0-rc-2023-11-05.{pollable};
+    use wasi:clocks/monotonic-clock@0.2.0-rc-2023-11-05.{duration};
     use network.{network, error-code, ip-socket-address, ip-address-family};
 
     enum shutdown-type {
@@ -125,8 +126,11 @@ interface tcp {
         /// The returned socket is bound and in the Connection state. The following properties are inherited from the listener socket:
         /// - `address-family`
         /// - `ipv6-only`
-        /// - `keep-alive`
-        /// - `unicast-hop-limit`
+        /// - `keep-alive-enabled`
+        /// - `keep-alive-idle-time`
+        /// - `keep-alive-interval`
+        /// - `keep-alive-count`
+        /// - `hop-limit`
         /// - `receive-buffer-size`
         /// - `send-buffer-size`
         ///
@@ -176,6 +180,11 @@ interface tcp {
         /// - <https://man.freebsd.org/cgi/man.cgi?query=getpeername&sektion=2&n=1>
         remote-address: func() -> result<ip-socket-address, error-code>;
 
+        /// Whether the socket is listening for new connections.
+        ///
+        /// Equivalent to the SO_ACCEPTCONN socket option.
+        is-listening: func() -> bool;
+
         /// Whether this is a IPv4 or IPv6 socket.
         ///
         /// Equivalent to the SO_DOMAIN socket option.
@@ -194,36 +203,87 @@ interface tcp {
 
         /// Hints the desired listen queue size. Implementations are free to ignore this.
         ///
+        /// If the provided value is 0, an `invalid-argument` error is returned.
+        /// Any other value will never cause an error, but it might be silently clamped and/or rounded.
+        ///
         /// # Typical errors
         /// - `not-supported`:        (set) The platform does not support changing the backlog size after the initial listen.
+        /// - `invalid-argument`:     (set) The provided value was 0.
         /// - `invalid-state`:        (set) The socket is already in the Connection state.
         set-listen-backlog-size: func(value: u64) -> result<_, error-code>;
 
+        /// Enables or disables keepalive.
+        ///
+        /// The keepalive behavior can be adjusted using:
+        /// - `keep-alive-idle-time`
+        /// - `keep-alive-interval`
+        /// - `keep-alive-count`
+        /// These properties can be configured while `keep-alive-enabled` is false, but only come into effect when `keep-alive-enabled` is true.
+        ///
         /// Equivalent to the SO_KEEPALIVE socket option.
-        keep-alive: func() -> result<bool, error-code>;
-        set-keep-alive: func(value: bool) -> result<_, error-code>;
+        keep-alive-enabled: func() -> result<bool, error-code>;
+        set-keep-alive-enabled: func(value: bool) -> result<_, error-code>;
+
+        /// Amount of time the connection has to be idle before TCP starts sending keepalive packets.
+        ///
+        /// If the provided value is 0, an `invalid-argument` error is returned.
+        /// Any other value will never cause an error, but it might be silently clamped and/or rounded.
+        /// I.e. after setting a value, reading the same setting back may return a different value.
+        ///
+        /// Equivalent to the TCP_KEEPIDLE socket option. (TCP_KEEPALIVE on MacOS)
+        ///
+        /// # Typical errors
+        /// - `invalid-argument`:     (set) The provided value was 0.
+        keep-alive-idle-time: func() -> result<duration, error-code>;
+        set-keep-alive-idle-time: func(value: duration) -> result<_, error-code>;
+
+        /// The time between keepalive packets.
+        ///
+        /// If the provided value is 0, an `invalid-argument` error is returned.
+        /// Any other value will never cause an error, but it might be silently clamped and/or rounded.
+        /// I.e. after setting a value, reading the same setting back may return a different value.
+        ///
+        /// Equivalent to the TCP_KEEPINTVL socket option.
+        ///
+        /// # Typical errors
+        /// - `invalid-argument`:     (set) The provided value was 0.
+        keep-alive-interval: func() -> result<duration, error-code>;
+        set-keep-alive-interval: func(value: duration) -> result<_, error-code>;
+
+        /// The maximum amount of keepalive packets TCP should send before aborting the connection.
+        ///
+        /// If the provided value is 0, an `invalid-argument` error is returned.
+        /// Any other value will never cause an error, but it might be silently clamped and/or rounded.
+        /// I.e. after setting a value, reading the same setting back may return a different value.
+        ///
+        /// Equivalent to the TCP_KEEPCNT socket option.
+        ///
+        /// # Typical errors
+        /// - `invalid-argument`:     (set) The provided value was 0.
+        keep-alive-count: func() -> result<u32, error-code>;
+        set-keep-alive-count: func(value: u32) -> result<_, error-code>;
 
         /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options.
         ///
+        /// If the provided value is 0, an `invalid-argument` error is returned.
+        ///
         /// # Typical errors
         /// - `invalid-argument`:     (set) The TTL value must be 1 or higher.
         /// - `invalid-state`:        (set) The socket is already in the Connection state.
         /// - `invalid-state`:        (set) The socket is already in the Listener state.
-        unicast-hop-limit: func() -> result<u8, error-code>;
-        set-unicast-hop-limit: func(value: u8) -> result<_, error-code>;
+        hop-limit: func() -> result<u8, error-code>;
+        set-hop-limit: func(value: u8) -> result<_, error-code>;
 
         /// The kernel buffer space reserved for sends/receives on this socket.
         ///
-        /// Note #1: an implementation may choose to cap or round the buffer size when setting the value.
-        ///     In other words, after setting a value, reading the same setting back may return a different value.
-        ///
-        /// Note #2: there is not necessarily a direct relationship between the kernel buffer size and the bytes of
-        ///     actual data to be sent/received by the application, because the kernel might also use the buffer space
-        ///     for internal metadata structures.
+        /// If the provided value is 0, an `invalid-argument` error is returned.
+        /// Any other value will never cause an error, but it might be silently clamped and/or rounded.
+        /// I.e. after setting a value, reading the same setting back may return a different value.
         ///
         /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options.
         ///
         /// # Typical errors
+        /// - `invalid-argument`:     (set) The provided value was 0.
         /// - `invalid-state`:        (set) The socket is already in the Connection state.
         /// - `invalid-state`:        (set) The socket is already in the Listener state.
         receive-buffer-size: func() -> result<u64, error-code>;
diff --git a/crates/wasi-http/wit/deps/sockets/udp.wit b/crates/wasi-http/wit/deps/sockets/udp.wit
index 1ee1843a9..9dc1e990b 100644
--- a/crates/wasi-http/wit/deps/sockets/udp.wit
+++ b/crates/wasi-http/wit/deps/sockets/udp.wit
@@ -154,19 +154,24 @@ interface udp {
         set-ipv6-only: func(value: bool) -> result<_, error-code>;
 
         /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options.
+        ///
+        /// If the provided value is 0, an `invalid-argument` error is returned.
+        ///
+        /// # Typical errors
+        /// - `invalid-argument`:     (set) The TTL value must be 1 or higher.
         unicast-hop-limit: func() -> result<u8, error-code>;
         set-unicast-hop-limit: func(value: u8) -> result<_, error-code>;
 
         /// The kernel buffer space reserved for sends/receives on this socket.
         ///
-        /// Note #1: an implementation may choose to cap or round the buffer size when setting the value.
-        /// 	In other words, after setting a value, reading the same setting back may return a different value.
-        ///
-        /// Note #2: there is not necessarily a direct relationship between the kernel buffer size and the bytes of
-        /// 	actual data to be sent/received by the application, because the kernel might also use the buffer space
-        /// 	for internal metadata structures.
+        /// If the provided value is 0, an `invalid-argument` error is returned.
+        /// Any other value will never cause an error, but it might be silently clamped and/or rounded.
+        /// I.e. after setting a value, reading the same setting back may return a different value.
         ///
         /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options.
+        ///
+        /// # Typical errors
+        /// - `invalid-argument`:     (set) The provided value was 0.
         receive-buffer-size: func() -> result<u64, error-code>;
         set-receive-buffer-size: func(value: u64) -> result<_, error-code>;
         send-buffer-size: func() -> result<u64, error-code>;
diff --git a/crates/wasi-preview1-component-adapter/src/lib.rs b/crates/wasi-preview1-component-adapter/src/lib.rs
index e9f5c6f0f..ef42ac10c 100644
--- a/crates/wasi-preview1-component-adapter/src/lib.rs
+++ b/crates/wasi-preview1-component-adapter/src/lib.rs
@@ -1456,7 +1456,6 @@ pub unsafe extern "C" fn path_open(
     let at_flags = at_flags_from_lookupflags(dirflags);
     let o_flags = o_flags_from_oflags(oflags);
     let flags = descriptor_flags_from_flags(fs_rights_base, fdflags);
-    let mode = filesystem::Modes::READABLE | filesystem::Modes::WRITABLE;
     let append = fdflags & wasi::FDFLAGS_APPEND == wasi::FDFLAGS_APPEND;
 
     State::with(|state| {
@@ -1464,7 +1463,7 @@ pub unsafe extern "C" fn path_open(
             .descriptors()
             .get_dir(fd)?
             .fd
-            .open_at(at_flags, path, o_flags, flags, mode)?;
+            .open_at(at_flags, path, o_flags, flags)?;
         let descriptor_type = result.get_type()?;
         let desc = Descriptor::Streams(Streams {
             input: OnceCell::new(),
diff --git a/crates/wasi/src/preview2/host/filesystem.rs b/crates/wasi/src/preview2/host/filesystem.rs
index f011d69e2..39559ed43 100644
--- a/crates/wasi/src/preview2/host/filesystem.rs
+++ b/crates/wasi/src/preview2/host/filesystem.rs
@@ -487,9 +487,6 @@ impl<T: WasiView> HostDescriptor for T {
         path: String,
         oflags: types::OpenFlags,
         flags: types::DescriptorFlags,
-        // TODO: These are the permissions to use when creating a new file.
-        // Not implemented yet.
-        _mode: types::Modes,
     ) -> FsResult<Resource<types::Descriptor>> {
         use cap_fs_ext::{FollowSymlinks, OpenOptionsFollowExt, OpenOptionsMaybeDirExt};
         use system_interface::fs::{FdFlags, GetSetFdFlags};
@@ -698,36 +695,6 @@ impl<T: WasiView> HostDescriptor for T {
             .await?)
     }
 
-    async fn access_at(
-        &mut self,
-        _fd: Resource<types::Descriptor>,
-        _path_flags: types::PathFlags,
-        _path: String,
-        _access: types::AccessType,
-    ) -> FsResult<()> {
-        todo!("filesystem access_at is not implemented")
-    }
-
-    async fn change_file_permissions_at(
-        &mut self,
-        _fd: Resource<types::Descriptor>,
-        _path_flags: types::PathFlags,
-        _path: String,
-        _mode: types::Modes,
-    ) -> FsResult<()> {
-        todo!("filesystem change_file_permissions_at is not implemented")
-    }
-
-    async fn change_directory_permissions_at(
-        &mut self,
-        _fd: Resource<types::Descriptor>,
-        _path_flags: types::PathFlags,
-        _path: String,
-        _mode: types::Modes,
-    ) -> FsResult<()> {
-        todo!("filesystem change_directory_permissions_at is not implemented")
-    }
-
     fn read_via_stream(
         &mut self,
         fd: Resource<types::Descriptor>,
diff --git a/crates/wasi/src/preview2/host/filesystem/sync.rs b/crates/wasi/src/preview2/host/filesystem/sync.rs
index 28a9b77f5..3b3d83e9d 100644
--- a/crates/wasi/src/preview2/host/filesystem/sync.rs
+++ b/crates/wasi/src/preview2/host/filesystem/sync.rs
@@ -175,7 +175,6 @@ impl<T: async_filesystem::HostDescriptor> sync_filesystem::HostDescriptor for T
         path: String,
         oflags: sync_filesystem::OpenFlags,
         flags: sync_filesystem::DescriptorFlags,
-        mode: sync_filesystem::Modes,
     ) -> FsResult<Resource<sync_filesystem::Descriptor>> {
         in_tokio(async {
             async_filesystem::HostDescriptor::open_at(
@@ -185,7 +184,6 @@ impl<T: async_filesystem::HostDescriptor> sync_filesystem::HostDescriptor for T
                 path,
                 oflags.into(),
                 flags.into(),
-                mode.into(),
             )
             .await
         })
@@ -244,63 +242,6 @@ impl<T: async_filesystem::HostDescriptor> sync_filesystem::HostDescriptor for T
         in_tokio(async { async_filesystem::HostDescriptor::unlink_file_at(self, fd, path).await })
     }
 
-    fn access_at(
-        &mut self,
-        fd: Resource<sync_filesystem::Descriptor>,
-        path_flags: sync_filesystem::PathFlags,
-        path: String,
-        access: sync_filesystem::AccessType,
-    ) -> FsResult<()> {
-        in_tokio(async {
-            async_filesystem::HostDescriptor::access_at(
-                self,
-                fd,
-                path_flags.into(),
-                path,
-                access.into(),
-            )
-            .await
-        })
-    }
-
-    fn change_file_permissions_at(
-        &mut self,
-        fd: Resource<sync_filesystem::Descriptor>,
-        path_flags: sync_filesystem::PathFlags,
-        path: String,
-        mode: sync_filesystem::Modes,
-    ) -> FsResult<()> {
-        in_tokio(async {
-            async_filesystem::HostDescriptor::change_file_permissions_at(
-                self,
-                fd,
-                path_flags.into(),
-                path,
-                mode.into(),
-            )
-            .await
-        })
-    }
-
-    fn change_directory_permissions_at(
-        &mut self,
-        fd: Resource<sync_filesystem::Descriptor>,
-        path_flags: sync_filesystem::PathFlags,
-        path: String,
-        mode: sync_filesystem::Modes,
-    ) -> FsResult<()> {
-        in_tokio(async {
-            async_filesystem::HostDescriptor::change_directory_permissions_at(
-                self,
-                fd,
-                path_flags.into(),
-                path,
-                mode.into(),
-            )
-            .await
-        })
-    }
-
     fn read_via_stream(
         &mut self,
         fd: Resource<sync_filesystem::Descriptor>,
@@ -566,30 +507,6 @@ impl From<sync_filesystem::DescriptorFlags> for async_filesystem::DescriptorFlag
         out
     }
 }
-impl From<sync_filesystem::Modes> for async_filesystem::Modes {
-    fn from(other: sync_filesystem::Modes) -> Self {
-        let mut out = Self::empty();
-        if other.contains(sync_filesystem::Modes::READABLE) {
-            out |= Self::READABLE;
-        }
-        if other.contains(sync_filesystem::Modes::WRITABLE) {
-            out |= Self::WRITABLE;
-        }
-        if other.contains(sync_filesystem::Modes::EXECUTABLE) {
-            out |= Self::EXECUTABLE;
-        }
-        out
-    }
-}
-impl From<sync_filesystem::AccessType> for async_filesystem::AccessType {
-    fn from(other: sync_filesystem::AccessType) -> Self {
-        use sync_filesystem::AccessType;
-        match other {
-            AccessType::Access(modes) => Self::Access(modes.into()),
-            AccessType::Exists => Self::Exists,
-        }
-    }
-}
 impl From<async_filesystem::MetadataHashValue> for sync_filesystem::MetadataHashValue {
     fn from(other: async_filesystem::MetadataHashValue) -> Self {
         Self {
diff --git a/crates/wasi/src/preview2/host/network.rs b/crates/wasi/src/preview2/host/network.rs
index ce6f2f155..ad8d7ed7b 100644
--- a/crates/wasi/src/preview2/host/network.rs
+++ b/crates/wasi/src/preview2/host/network.rs
@@ -213,6 +213,7 @@ impl From<cap_net_ext::AddressFamily> for IpAddressFamily {
 
 pub(crate) mod util {
     use std::net::{IpAddr, Ipv6Addr, SocketAddr};
+    use std::time::Duration;
 
     use crate::preview2::bindings::sockets::network::ErrorCode;
     use crate::preview2::network::SocketAddressFamily;
@@ -371,6 +372,52 @@ pub(crate) mod util {
         }
     }
 
+    pub fn set_tcp_keepidle<Fd: AsFd>(sockfd: Fd, value: Duration) -> rustix::io::Result<()> {
+        if value <= Duration::ZERO {
+            return Err(Errno::INVAL);
+        }
+
+        // Ensure that the value passed to the actual syscall never gets rounded down to 0.
+        const MIN_SECS: u64 = 1;
+
+        // Cap it at Linux' maximum, which appears to have the lowest limit across our supported platforms.
+        const MAX_SECS: u64 = i16::MAX as u64;
+
+        sockopt::set_tcp_keepidle(
+            sockfd,
+            value.clamp(Duration::from_secs(MIN_SECS), Duration::from_secs(MAX_SECS)),
+        )
+    }
+
+    pub fn set_tcp_keepintvl<Fd: AsFd>(sockfd: Fd, value: Duration) -> rustix::io::Result<()> {
+        if value <= Duration::ZERO {
+            return Err(Errno::INVAL);
+        }
+
+        // Ensure that any fractional value passed to the actual syscall never gets rounded down to 0.
+        const MIN_SECS: u64 = 1;
+
+        // Cap it at Linux' maximum, which appears to have the lowest limit across our supported platforms.
+        const MAX_SECS: u64 = i16::MAX as u64;
+
+        sockopt::set_tcp_keepintvl(
+            sockfd,
+            value.clamp(Duration::from_secs(MIN_SECS), Duration::from_secs(MAX_SECS)),
+        )
+    }
+
+    pub fn set_tcp_keepcnt<Fd: AsFd>(sockfd: Fd, value: u32) -> rustix::io::Result<()> {
+        if value == 0 {
+            return Err(Errno::INVAL);
+        }
+
+        const MIN_CNT: u32 = 1;
+        // Cap it at Linux' maximum, which appears to have the lowest limit across our supported platforms.
+        const MAX_CNT: u32 = i8::MAX as u32;
+
+        sockopt::set_tcp_keepcnt(sockfd, value.clamp(MIN_CNT, MAX_CNT))
+    }
+
     pub fn get_ip_ttl<Fd: AsFd>(sockfd: Fd) -> rustix::io::Result<u8> {
         sockopt::get_ip_ttl(sockfd)?
             .try_into()
@@ -430,6 +477,10 @@ pub(crate) mod util {
         sockfd: Fd,
         value: usize,
     ) -> rustix::io::Result<()> {
+        if value == 0 {
+            return Err(Errno::INVAL);
+        }
+
         let value = normalize_set_buffer_size(value);
 
         match sockopt::set_socket_recv_buffer_size(sockfd, value) {
@@ -450,6 +501,10 @@ pub(crate) mod util {
         sockfd: Fd,
         value: usize,
     ) -> rustix::io::Result<()> {
+        if value == 0 {
+            return Err(Errno::INVAL);
+        }
+
         let value = normalize_set_buffer_size(value);
 
         match sockopt::set_socket_send_buffer_size(sockfd, value) {
diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs
index 665b2a85a..1c805467b 100644
--- a/crates/wasi/src/preview2/host/tcp.rs
+++ b/crates/wasi/src/preview2/host/tcp.rs
@@ -15,6 +15,7 @@ use io_lifetimes::AsSocketlike;
 use rustix::io::Errno;
 use rustix::net::sockopt;
 use std::net::SocketAddr;
+use std::time::Duration;
 use tokio::io::Interest;
 use wasmtime::component::Resource;
 
@@ -264,6 +265,10 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T {
             {
                 _ = util::set_ipv6_unicast_hops(&connection, ttl); // Ignore potential error.
             }
+
+            if let Some(value) = socket.keep_alive_idle_time {
+                _ = util::set_tcp_keepidle(&connection, value); // Ignore potential error.
+            }
         }
 
         let mut tcp_socket = TcpSocket::from_tcp_stream(connection, socket.family)?;
@@ -317,6 +322,16 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T {
         Ok(addr.into())
     }
 
+    fn is_listening(&mut self, this: Resource<tcp::TcpSocket>) -> Result<bool, anyhow::Error> {
+        let table = self.table();
+        let socket = table.get(&this)?;
+
+        match socket.tcp_state {
+            TcpState::Listening => Ok(true),
+            _ => Ok(false),
+        }
+    }
+
     fn address_family(
         &mut self,
         this: Resource<tcp::TcpSocket>,
@@ -335,7 +350,7 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T {
         let socket = table.get(&this)?;
 
         // Instead of just calling the OS we return our own internal state, because
-        // MacOS doesn't propogate the V6ONLY state on to accepted client sockets.
+        // MacOS doesn't propagate the V6ONLY state on to accepted client sockets.
 
         match socket.family {
             SocketAddressFamily::Ipv4 => Err(ErrorCode::NotSupported.into()),
@@ -372,6 +387,10 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T {
         let table = self.table_mut();
         let socket = table.get_mut(&this)?;
 
+        if value == 0 {
+            return Err(ErrorCode::InvalidArgument.into());
+        }
+
         // Silently clamp backlog size. This is OK for us to do, because operating systems do this too.
         let value = value
             .try_into()
@@ -403,19 +422,84 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T {
         }
     }
 
-    fn keep_alive(&mut self, this: Resource<tcp::TcpSocket>) -> SocketResult<bool> {
+    fn keep_alive_enabled(&mut self, this: Resource<tcp::TcpSocket>) -> SocketResult<bool> {
         let table = self.table();
         let socket = table.get(&this)?;
         Ok(sockopt::get_socket_keepalive(socket.tcp_socket())?)
     }
 
-    fn set_keep_alive(&mut self, this: Resource<tcp::TcpSocket>, value: bool) -> SocketResult<()> {
+    fn set_keep_alive_enabled(
+        &mut self,
+        this: Resource<tcp::TcpSocket>,
+        value: bool,
+    ) -> SocketResult<()> {
         let table = self.table();
         let socket = table.get(&this)?;
         Ok(sockopt::set_socket_keepalive(socket.tcp_socket(), value)?)
     }
 
-    fn unicast_hop_limit(&mut self, this: Resource<tcp::TcpSocket>) -> SocketResult<u8> {
+    fn keep_alive_idle_time(&mut self, this: Resource<tcp::TcpSocket>) -> SocketResult<u64> {
+        let table = self.table();
+        let socket = table.get(&this)?;
+        Ok(sockopt::get_tcp_keepidle(socket.tcp_socket())?.as_nanos() as u64)
+    }
+
+    fn set_keep_alive_idle_time(
+        &mut self,
+        this: Resource<tcp::TcpSocket>,
+        value: u64,
+    ) -> SocketResult<()> {
+        let table = self.table_mut();
+        let socket = table.get_mut(&this)?;
+
+        let duration = Duration::from_nanos(value);
+
+        util::set_tcp_keepidle(socket.tcp_socket(), duration)?;
+
+        #[cfg(target_os = "macos")]
+        {
+            socket.keep_alive_idle_time = Some(duration);
+        }
+
+        Ok(())
+    }
+
+    fn keep_alive_interval(&mut self, this: Resource<tcp::TcpSocket>) -> SocketResult<u64> {
+        let table = self.table();
+        let socket = table.get(&this)?;
+        Ok(sockopt::get_tcp_keepintvl(socket.tcp_socket())?.as_nanos() as u64)
+    }
+
+    fn set_keep_alive_interval(
+        &mut self,
+        this: Resource<tcp::TcpSocket>,
+        value: u64,
+    ) -> SocketResult<()> {
+        let table = self.table();
+        let socket = table.get(&this)?;
+        Ok(util::set_tcp_keepintvl(
+            socket.tcp_socket(),
+            Duration::from_nanos(value),
+        )?)
+    }
+
+    fn keep_alive_count(&mut self, this: Resource<tcp::TcpSocket>) -> SocketResult<u32> {
+        let table = self.table();
+        let socket = table.get(&this)?;
+        Ok(sockopt::get_tcp_keepcnt(socket.tcp_socket())?)
+    }
+
+    fn set_keep_alive_count(
+        &mut self,
+        this: Resource<tcp::TcpSocket>,
+        value: u32,
+    ) -> SocketResult<()> {
+        let table = self.table();
+        let socket = table.get(&this)?;
+        Ok(util::set_tcp_keepcnt(socket.tcp_socket(), value)?)
+    }
+
+    fn hop_limit(&mut self, this: Resource<tcp::TcpSocket>) -> SocketResult<u8> {
         let table = self.table();
         let socket = table.get(&this)?;
 
@@ -427,11 +511,7 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T {
         Ok(ttl)
     }
 
-    fn set_unicast_hop_limit(
-        &mut self,
-        this: Resource<tcp::TcpSocket>,
-        value: u8,
-    ) -> SocketResult<()> {
+    fn set_hop_limit(&mut self, this: Resource<tcp::TcpSocket>, value: u8) -> SocketResult<()> {
         let table = self.table_mut();
         let socket = table.get_mut(&this)?;
 
diff --git a/crates/wasi/src/preview2/preview1.rs b/crates/wasi/src/preview2/preview1.rs
index cc976f1dd..7760bc418 100644
--- a/crates/wasi/src/preview2/preview1.rs
+++ b/crates/wasi/src/preview2/preview1.rs
@@ -1893,14 +1893,7 @@ impl<
         };
         drop(t);
         let fd = self
-            .open_at(
-                dirfd,
-                dirflags.into(),
-                path,
-                oflags.into(),
-                flags,
-                filesystem::Modes::READABLE | filesystem::Modes::WRITABLE,
-            )
+            .open_at(dirfd, dirflags.into(), path, oflags.into(), flags)
             .await
             .map_err(|e| {
                 e.try_into()
diff --git a/crates/wasi/src/preview2/tcp.rs b/crates/wasi/src/preview2/tcp.rs
index b6ebffe7e..23061760c 100644
--- a/crates/wasi/src/preview2/tcp.rs
+++ b/crates/wasi/src/preview2/tcp.rs
@@ -64,15 +64,17 @@ pub struct TcpSocket {
 
     pub(crate) family: SocketAddressFamily,
 
-    /// The manually configured buffer size. `None` means: no preference, use system default.
+    // The socket options below are not automatically inherited from the listener
+    // on all platforms. So we keep track of which options have been explicitly
+    // set and manually apply those values to newly accepted clients.
     #[cfg(target_os = "macos")]
     pub(crate) receive_buffer_size: Option<usize>,
-    /// The manually configured buffer size. `None` means: no preference, use system default.
     #[cfg(target_os = "macos")]
     pub(crate) send_buffer_size: Option<usize>,
-    /// The manually configured TTL. `None` means: no preference, use system default.
     #[cfg(target_os = "macos")]
     pub(crate) hop_limit: Option<u8>,
+    #[cfg(target_os = "macos")]
+    pub(crate) keep_alive_idle_time: Option<std::time::Duration>,
 }
 
 pub(crate) struct TcpReadStream {
@@ -301,6 +303,8 @@ impl TcpSocket {
             send_buffer_size: None,
             #[cfg(target_os = "macos")]
             hop_limit: None,
+            #[cfg(target_os = "macos")]
+            keep_alive_idle_time: None,
         })
     }
 
diff --git a/crates/wasi/wit/deps/filesystem/types.wit b/crates/wasi/wit/deps/filesystem/types.wit
index 37700332b..4ab5dc055 100644
--- a/crates/wasi/wit/deps/filesystem/types.wit
+++ b/crates/wasi/wit/deps/filesystem/types.wit
@@ -142,29 +142,6 @@ interface types {
         truncate,
     }
 
-    /// Permissions mode used by `open-at`, `change-file-permissions-at`, and
-    /// similar.
-    flags modes {
-        /// True if the resource is considered readable by the containing
-        /// filesystem.
-        readable,
-        /// True if the resource is considered writable by the containing
-        /// filesystem.
-        writable,
-        /// True if the resource is considered executable by the containing
-        /// filesystem. This does not apply to directories.
-        executable,
-    }
-
-    /// Access type used by `access-at`.
-    variant access-type {
-        /// Test for readability, writeability, or executability.
-        access(modes),
-
-        /// Test whether the path exists.
-        exists,
-    }
-
     /// Number of hard links to an inode.
     type link-count = u64;
 
@@ -538,8 +515,6 @@ interface types {
             open-flags: open-flags,
             /// Flags to use for the resulting descriptor.
             %flags: descriptor-flags,
-            /// Permissions to use when creating a new file.
-            modes: modes
         ) -> result<descriptor, error-code>;
 
         /// Read the contents of a symbolic link.
@@ -588,25 +563,6 @@ interface types {
             new-path: string,
         ) -> result<_, error-code>;
 
-        /// Check accessibility of a filesystem path.
-        ///
-        /// Check whether the given filesystem path names an object which is
-        /// readable, writable, or executable, or whether it exists.
-        ///
-        /// This does not a guarantee that subsequent accesses will succeed, as
-        /// filesystem permissions may be modified asynchronously by external
-        /// entities.
-        ///
-        /// Note: This is similar to `faccessat` with the `AT_EACCESS` flag in POSIX.
-        access-at: func(
-            /// Flags determining the method of how the path is resolved.
-            path-flags: path-flags,
-            /// The relative path to check.
-            path: string,
-            /// The type of check to perform.
-            %type: access-type
-        ) -> result<_, error-code>;
-
         /// Unlink a filesystem object that is not a directory.
         ///
         /// Return `error-code::is-directory` if the path refers to a directory.
@@ -616,40 +572,6 @@ interface types {
             path: string,
         ) -> result<_, error-code>;
 
-        /// Change the permissions of a filesystem object that is not a directory.
-        ///
-        /// Note that the ultimate meanings of these permissions is
-        /// filesystem-specific.
-        ///
-        /// Note: This is similar to `fchmodat` in POSIX.
-        change-file-permissions-at: func(
-            /// Flags determining the method of how the path is resolved.
-            path-flags: path-flags,
-            /// The relative path to operate on.
-            path: string,
-            /// The new permissions for the filesystem object.
-            modes: modes,
-        ) -> result<_, error-code>;
-
-        /// Change the permissions of a directory.
-        ///
-        /// Note that the ultimate meanings of these permissions is
-        /// filesystem-specific.
-        ///
-        /// Unlike in POSIX, the `executable` flag is not reinterpreted as a "search"
-        /// flag. `read` on a directory implies readability and searchability, and
-        /// `execute` is not valid for directories.
-        ///
-        /// Note: This is similar to `fchmodat` in POSIX.
-        change-directory-permissions-at: func(
-            /// Flags determining the method of how the path is resolved.
-            path-flags: path-flags,
-            /// The relative path to operate on.
-            path: string,
-            /// The new permissions for the directory.
-            modes: modes,
-        ) -> result<_, error-code>;
-
         /// Test whether two descriptors refer to the same filesystem object.
         ///
         /// In POSIX, this corresponds to testing whether the two descriptors have the
diff --git a/crates/wasi/wit/deps/sockets/tcp.wit b/crates/wasi/wit/deps/sockets/tcp.wit
index b9ef0625a..f0d875095 100644
--- a/crates/wasi/wit/deps/sockets/tcp.wit
+++ b/crates/wasi/wit/deps/sockets/tcp.wit
@@ -2,6 +2,7 @@
 interface tcp {
     use wasi:io/streams@0.2.0-rc-2023-11-05.{input-stream, output-stream};
     use wasi:io/poll@0.2.0-rc-2023-11-05.{pollable};
+    use wasi:clocks/monotonic-clock@0.2.0-rc-2023-11-05.{duration};
     use network.{network, error-code, ip-socket-address, ip-address-family};
 
     enum shutdown-type {
@@ -125,8 +126,11 @@ interface tcp {
         /// The returned socket is bound and in the Connection state. The following properties are inherited from the listener socket:
         /// - `address-family`
         /// - `ipv6-only`
-        /// - `keep-alive`
-        /// - `unicast-hop-limit`
+        /// - `keep-alive-enabled`
+        /// - `keep-alive-idle-time`
+        /// - `keep-alive-interval`
+        /// - `keep-alive-count`
+        /// - `hop-limit`
         /// - `receive-buffer-size`
         /// - `send-buffer-size`
         ///
@@ -176,6 +180,11 @@ interface tcp {
         /// - <https://man.freebsd.org/cgi/man.cgi?query=getpeername&sektion=2&n=1>
         remote-address: func() -> result<ip-socket-address, error-code>;
 
+        /// Whether the socket is listening for new connections.
+        ///
+        /// Equivalent to the SO_ACCEPTCONN socket option.
+        is-listening: func() -> bool;
+
         /// Whether this is a IPv4 or IPv6 socket.
         ///
         /// Equivalent to the SO_DOMAIN socket option.
@@ -194,36 +203,87 @@ interface tcp {
 
         /// Hints the desired listen queue size. Implementations are free to ignore this.
         ///
+        /// If the provided value is 0, an `invalid-argument` error is returned.
+        /// Any other value will never cause an error, but it might be silently clamped and/or rounded.
+        ///
         /// # Typical errors
         /// - `not-supported`:        (set) The platform does not support changing the backlog size after the initial listen.
+        /// - `invalid-argument`:     (set) The provided value was 0.
         /// - `invalid-state`:        (set) The socket is already in the Connection state.
         set-listen-backlog-size: func(value: u64) -> result<_, error-code>;
 
+        /// Enables or disables keepalive.
+        ///
+        /// The keepalive behavior can be adjusted using:
+        /// - `keep-alive-idle-time`
+        /// - `keep-alive-interval`
+        /// - `keep-alive-count`
+        /// These properties can be configured while `keep-alive-enabled` is false, but only come into effect when `keep-alive-enabled` is true.
+        ///
         /// Equivalent to the SO_KEEPALIVE socket option.
-        keep-alive: func() -> result<bool, error-code>;
-        set-keep-alive: func(value: bool) -> result<_, error-code>;
+        keep-alive-enabled: func() -> result<bool, error-code>;
+        set-keep-alive-enabled: func(value: bool) -> result<_, error-code>;
+
+        /// Amount of time the connection has to be idle before TCP starts sending keepalive packets.
+        ///
+        /// If the provided value is 0, an `invalid-argument` error is returned.
+        /// Any other value will never cause an error, but it might be silently clamped and/or rounded.
+        /// I.e. after setting a value, reading the same setting back may return a different value.
+        ///
+        /// Equivalent to the TCP_KEEPIDLE socket option. (TCP_KEEPALIVE on MacOS)
+        ///
+        /// # Typical errors
+        /// - `invalid-argument`:     (set) The provided value was 0.
+        keep-alive-idle-time: func() -> result<duration, error-code>;
+        set-keep-alive-idle-time: func(value: duration) -> result<_, error-code>;
+
+        /// The time between keepalive packets.
+        ///
+        /// If the provided value is 0, an `invalid-argument` error is returned.
+        /// Any other value will never cause an error, but it might be silently clamped and/or rounded.
+        /// I.e. after setting a value, reading the same setting back may return a different value.
+        ///
+        /// Equivalent to the TCP_KEEPINTVL socket option.
+        ///
+        /// # Typical errors
+        /// - `invalid-argument`:     (set) The provided value was 0.
+        keep-alive-interval: func() -> result<duration, error-code>;
+        set-keep-alive-interval: func(value: duration) -> result<_, error-code>;
+
+        /// The maximum amount of keepalive packets TCP should send before aborting the connection.
+        ///
+        /// If the provided value is 0, an `invalid-argument` error is returned.
+        /// Any other value will never cause an error, but it might be silently clamped and/or rounded.
+        /// I.e. after setting a value, reading the same setting back may return a different value.
+        ///
+        /// Equivalent to the TCP_KEEPCNT socket option.
+        ///
+        /// # Typical errors
+        /// - `invalid-argument`:     (set) The provided value was 0.
+        keep-alive-count: func() -> result<u32, error-code>;
+        set-keep-alive-count: func(value: u32) -> result<_, error-code>;
 
         /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options.
         ///
+        /// If the provided value is 0, an `invalid-argument` error is returned.
+        ///
         /// # Typical errors
         /// - `invalid-argument`:     (set) The TTL value must be 1 or higher.
         /// - `invalid-state`:        (set) The socket is already in the Connection state.
         /// - `invalid-state`:        (set) The socket is already in the Listener state.
-        unicast-hop-limit: func() -> result<u8, error-code>;
-        set-unicast-hop-limit: func(value: u8) -> result<_, error-code>;
+        hop-limit: func() -> result<u8, error-code>;
+        set-hop-limit: func(value: u8) -> result<_, error-code>;
 
         /// The kernel buffer space reserved for sends/receives on this socket.
         ///
-        /// Note #1: an implementation may choose to cap or round the buffer size when setting the value.
-        ///     In other words, after setting a value, reading the same setting back may return a different value.
-        ///
-        /// Note #2: there is not necessarily a direct relationship between the kernel buffer size and the bytes of
-        ///     actual data to be sent/received by the application, because the kernel might also use the buffer space
-        ///     for internal metadata structures.
+        /// If the provided value is 0, an `invalid-argument` error is returned.
+        /// Any other value will never cause an error, but it might be silently clamped and/or rounded.
+        /// I.e. after setting a value, reading the same setting back may return a different value.
         ///
         /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options.
         ///
         /// # Typical errors
+        /// - `invalid-argument`:     (set) The provided value was 0.
         /// - `invalid-state`:        (set) The socket is already in the Connection state.
         /// - `invalid-state`:        (set) The socket is already in the Listener state.
         receive-buffer-size: func() -> result<u64, error-code>;
diff --git a/crates/wasi/wit/deps/sockets/udp.wit b/crates/wasi/wit/deps/sockets/udp.wit
index 1ee1843a9..9dc1e990b 100644
--- a/crates/wasi/wit/deps/sockets/udp.wit
+++ b/crates/wasi/wit/deps/sockets/udp.wit
@@ -154,19 +154,24 @@ interface udp {
         set-ipv6-only: func(value: bool) -> result<_, error-code>;
 
         /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options.
+        ///
+        /// If the provided value is 0, an `invalid-argument` error is returned.
+        ///
+        /// # Typical errors
+        /// - `invalid-argument`:     (set) The TTL value must be 1 or higher.
         unicast-hop-limit: func() -> result<u8, error-code>;
         set-unicast-hop-limit: func(value: u8) -> result<_, error-code>;
 
         /// The kernel buffer space reserved for sends/receives on this socket.
         ///
-        /// Note #1: an implementation may choose to cap or round the buffer size when setting the value.
-        /// 	In other words, after setting a value, reading the same setting back may return a different value.
-        ///
-        /// Note #2: there is not necessarily a direct relationship between the kernel buffer size and the bytes of
-        /// 	actual data to be sent/received by the application, because the kernel might also use the buffer space
-        /// 	for internal metadata structures.
+        /// If the provided value is 0, an `invalid-argument` error is returned.
+        /// Any other value will never cause an error, but it might be silently clamped and/or rounded.
+        /// I.e. after setting a value, reading the same setting back may return a different value.
         ///
         /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options.
+        ///
+        /// # Typical errors
+        /// - `invalid-argument`:     (set) The provided value was 0.
         receive-buffer-size: func() -> result<u64, error-code>;
         set-receive-buffer-size: func(value: u64) -> result<_, error-code>;
         send-buffer-size: func() -> result<u64, error-code>;


Test patch:
diff --git a/crates/test-programs/src/bin/preview2_tcp_sockopts.rs b/crates/test-programs/src/bin/preview2_tcp_sockopts.rs
index c54fbccfe..8bc1ef49b 100644
--- a/crates/test-programs/src/bin/preview2_tcp_sockopts.rs
+++ b/crates/test-programs/src/bin/preview2_tcp_sockopts.rs
@@ -3,6 +3,8 @@ use test_programs::wasi::sockets::network::{
 };
 use test_programs::wasi::sockets::tcp::TcpSocket;
 
+const SECOND: u64 = 1_000_000_000;
+
 fn test_tcp_sockopt_defaults(family: IpAddressFamily) {
     let sock = TcpSocket::new(family).unwrap();
 
@@ -12,8 +14,11 @@ fn test_tcp_sockopt_defaults(family: IpAddressFamily) {
         sock.ipv6_only().unwrap(); // Only verify that it has a default value at all, but either value is valid.
     }
 
-    sock.keep_alive().unwrap(); // Only verify that it has a default value at all, but either value is valid.
-    assert!(sock.unicast_hop_limit().unwrap() > 0);
+    sock.keep_alive_enabled().unwrap(); // Only verify that it has a default value at all, but either value is valid.
+    assert!(sock.keep_alive_idle_time().unwrap() > 0);
+    assert!(sock.keep_alive_interval().unwrap() > 0);
+    assert!(sock.keep_alive_count().unwrap() > 0);
+    assert!(sock.hop_limit().unwrap() > 0);
     assert!(sock.receive_buffer_size().unwrap() > 0);
     assert!(sock.send_buffer_size().unwrap() > 0);
 }
@@ -26,22 +31,59 @@ fn test_tcp_sockopt_input_ranges(family: IpAddressFamily) {
         assert!(matches!(sock.set_ipv6_only(false), Ok(_)));
     }
 
-    assert!(matches!(sock.set_listen_backlog_size(0), Ok(_))); // Unsupported sizes should be silently capped.
+    assert!(matches!(
+        sock.set_listen_backlog_size(0),
+        Err(ErrorCode::InvalidArgument)
+    ));
+    assert!(matches!(sock.set_listen_backlog_size(1), Ok(_))); // Unsupported sizes should be silently capped.
     assert!(matches!(sock.set_listen_backlog_size(u64::MAX), Ok(_))); // Unsupported sizes should be silently capped.
 
-    assert!(matches!(sock.set_keep_alive(true), Ok(_)));
-    assert!(matches!(sock.set_keep_alive(false), Ok(_)));
+    assert!(matches!(sock.set_keep_alive_enabled(true), Ok(_)));
+    assert!(matches!(sock.set_keep_alive_enabled(false), Ok(_)));
 
     assert!(matches!(
-        sock.set_unicast_hop_limit(0),
+        sock.set_keep_alive_idle_time(0),
         Err(ErrorCode::InvalidArgument)
     ));
-    assert!(matches!(sock.set_unicast_hop_limit(1), Ok(_)));
-    assert!(matches!(sock.set_unicast_hop_limit(u8::MAX), Ok(_)));
+    assert!(matches!(sock.set_keep_alive_idle_time(1), Ok(_))); // Unsupported sizes should be silently clamped.
+    let idle_time = sock.keep_alive_idle_time().unwrap(); // Check that the special 0/reset behavior was not triggered by the previous line.
+    assert!(idle_time > 0 && idle_time <= 1 * SECOND);
+    assert!(matches!(sock.set_keep_alive_idle_time(u64::MAX), Ok(_))); // Unsupported sizes should be silently clamped.
 
-    assert!(matches!(sock.set_receive_buffer_size(0), Ok(_))); // Unsupported sizes should be silently capped.
+    assert!(matches!(
+        sock.set_keep_alive_interval(0),
+        Err(ErrorCode::InvalidArgument)
+    ));
+    assert!(matches!(sock.set_keep_alive_interval(1), Ok(_))); // Unsupported sizes should be silently clamped.
+    let idle_time = sock.keep_alive_interval().unwrap(); // Check that the special 0/reset behavior was not triggered by the previous line.
+    assert!(idle_time > 0 && idle_time <= 1 * SECOND);
+    assert!(matches!(sock.set_keep_alive_interval(u64::MAX), Ok(_))); // Unsupported sizes should be silently clamped.
+
+    assert!(matches!(
+        sock.set_keep_alive_count(0),
+        Err(ErrorCode::InvalidArgument)
+    ));
+    assert!(matches!(sock.set_keep_alive_count(1), Ok(_))); // Unsupported sizes should be silently clamped.
+    assert!(matches!(sock.set_keep_alive_count(u32::MAX), Ok(_))); // Unsupported sizes should be silently clamped.
+
+    assert!(matches!(
+        sock.set_hop_limit(0),
+        Err(ErrorCode::InvalidArgument)
+    ));
+    assert!(matches!(sock.set_hop_limit(1), Ok(_)));
+    assert!(matches!(sock.set_hop_limit(u8::MAX), Ok(_)));
+
+    assert!(matches!(
+        sock.set_receive_buffer_size(0),
+        Err(ErrorCode::InvalidArgument)
+    ));
+    assert!(matches!(sock.set_receive_buffer_size(1), Ok(_))); // Unsupported sizes should be silently capped.
     assert!(matches!(sock.set_receive_buffer_size(u64::MAX), Ok(_))); // Unsupported sizes should be silently capped.
-    assert!(matches!(sock.set_send_buffer_size(0), Ok(_))); // Unsupported sizes should be silently capped.
+    assert!(matches!(
+        sock.set_send_buffer_size(0),
+        Err(ErrorCode::InvalidArgument)
+    ));
+    assert!(matches!(sock.set_send_buffer_size(1), Ok(_))); // Unsupported sizes should be silently capped.
     assert!(matches!(sock.set_send_buffer_size(u64::MAX), Ok(_))); // Unsupported sizes should be silently capped.
 }
 
@@ -55,13 +97,22 @@ fn test_tcp_sockopt_readback(family: IpAddressFamily) {
         assert_eq!(sock.ipv6_only().unwrap(), false);
     }
 
-    sock.set_keep_alive(true).unwrap();
-    assert_eq!(sock.keep_alive().unwrap(), true);
-    sock.set_keep_alive(false).unwrap();
-    assert_eq!(sock.keep_alive().unwrap(), false);
+    sock.set_keep_alive_enabled(true).unwrap();
+    assert_eq!(sock.keep_alive_enabled().unwrap(), true);
+    sock.set_keep_alive_enabled(false).unwrap();
+    assert_eq!(sock.keep_alive_enabled().unwrap(), false);
+
+    sock.set_keep_alive_idle_time(42 * SECOND).unwrap();
+    assert_eq!(sock.keep_alive_idle_time().unwrap(), 42 * SECOND);
+
+    sock.set_keep_alive_interval(42 * SECOND).unwrap();
+    assert_eq!(sock.keep_alive_interval().unwrap(), 42 * SECOND);
+
+    sock.set_keep_alive_count(42).unwrap();
+    assert_eq!(sock.keep_alive_count().unwrap(), 42);
 
-    sock.set_unicast_hop_limit(42).unwrap();
-    assert_eq!(sock.unicast_hop_limit().unwrap(), 42);
+    sock.set_hop_limit(42).unwrap();
+    assert_eq!(sock.hop_limit().unwrap(), 42);
 
     sock.set_receive_buffer_size(0x10000).unwrap();
     assert_eq!(sock.receive_buffer_size().unwrap(), 0x10000);
@@ -75,7 +126,7 @@ fn test_tcp_sockopt_inheritance(net: &Network, family: IpAddressFamily) {
     let listener = TcpSocket::new(family).unwrap();
 
     let default_ipv6_only = listener.ipv6_only().unwrap_or(false);
-    let default_keep_alive = listener.keep_alive().unwrap();
+    let default_keep_alive = listener.keep_alive_enabled().unwrap();
 
     // Configure options on listener:
     {
@@ -83,8 +134,13 @@ fn test_tcp_sockopt_inheritance(net: &Network, family: IpAddressFamily) {
             listener.set_ipv6_only(!default_ipv6_only).unwrap();
         }
 
-        listener.set_keep_alive(!default_keep_alive).unwrap();
-        listener.set_unicast_hop_limit(42).unwrap();
+        listener
+            .set_keep_alive_enabled(!default_keep_alive)
+            .unwrap();
+        listener.set_keep_alive_idle_time(42 * SECOND).unwrap();
+        listener.set_keep_alive_interval(42 * SECOND).unwrap();
+        listener.set_keep_alive_count(42).unwrap();
+        listener.set_hop_limit(42).unwrap();
         listener.set_receive_buffer_size(0x10000).unwrap();
         listener.set_send_buffer_size(0x10000).unwrap();
     }
@@ -102,24 +158,39 @@ fn test_tcp_sockopt_inheritance(net: &Network, family: IpAddressFamily) {
             assert_eq!(accepted_client.ipv6_only().unwrap(), !default_ipv6_only);
         }
 
-        assert_eq!(accepted_client.keep_alive().unwrap(), !default_keep_alive);
-        assert_eq!(accepted_client.unicast_hop_limit().unwrap(), 42);
+        assert_eq!(
+            accepted_client.keep_alive_enabled().unwrap(),
+            !default_keep_alive
+        );
+        assert_eq!(accepted_client.keep_alive_idle_time().unwrap(), 42 * SECOND);
+        assert_eq!(accepted_client.keep_alive_interval().unwrap(), 42 * SECOND);
+        assert_eq!(accepted_client.keep_alive_count().unwrap(), 42);
+        assert_eq!(accepted_client.hop_limit().unwrap(), 42);
         assert_eq!(accepted_client.receive_buffer_size().unwrap(), 0x10000);
         assert_eq!(accepted_client.send_buffer_size().unwrap(), 0x10000);
     }
 
     // Update options on listener to something else:
     {
-        listener.set_keep_alive(default_keep_alive).unwrap();
-        listener.set_unicast_hop_limit(43).unwrap();
+        listener.set_keep_alive_enabled(default_keep_alive).unwrap();
+        listener.set_keep_alive_idle_time(43 * SECOND).unwrap();
+        listener.set_keep_alive_interval(43 * SECOND).unwrap();
+        listener.set_keep_alive_count(43).unwrap();
+        listener.set_hop_limit(43).unwrap();
         listener.set_receive_buffer_size(0x20000).unwrap();
         listener.set_send_buffer_size(0x20000).unwrap();
     }
 
     // Verify that the already accepted socket was not affected:
     {
-        assert_eq!(accepted_client.keep_alive().unwrap(), !default_keep_alive);
-        assert_eq!(accepted_client.unicast_hop_limit().unwrap(), 42);
+        assert_eq!(
+            accepted_client.keep_alive_enabled().unwrap(),
+            !default_keep_alive
+        );
+        assert_eq!(accepted_client.keep_alive_idle_time().unwrap(), 42 * SECOND);
+        assert_eq!(accepted_client.keep_alive_interval().unwrap(), 42 * SECOND);
+        assert_eq!(accepted_client.keep_alive_count().unwrap(), 42);
+        assert_eq!(accepted_client.hop_limit().unwrap(), 42);
         assert_eq!(accepted_client.receive_buffer_size().unwrap(), 0x10000);
         assert_eq!(accepted_client.send_buffer_size().unwrap(), 0x10000);
     }
@@ -132,12 +203,17 @@ fn test_tcp_sockopt_after_listen(net: &Network, family: IpAddressFamily) {
     listener.blocking_listen().unwrap();
     let bound_addr = listener.local_address().unwrap();
 
-    let default_keep_alive = listener.keep_alive().unwrap();
+    let default_keep_alive = listener.keep_alive_enabled().unwrap();
 
     // Update options while the socket is already listening:
     {
-        listener.set_keep_alive(!default_keep_alive).unwrap();
-        listener.set_unicast_hop_limit(42).unwrap();
+        listener
+            .set_keep_alive_enabled(!default_keep_alive)
+            .unwrap();
+        listener.set_keep_alive_idle_time(42 * SECOND).unwrap();
+        listener.set_keep_alive_interval(42 * SECOND).unwrap();
+        listener.set_keep_alive_count(42).unwrap();
+        listener.set_hop_limit(42).unwrap();
         listener.set_receive_buffer_size(0x10000).unwrap();
         listener.set_send_buffer_size(0x10000).unwrap();
     }
@@ -148,8 +224,14 @@ fn test_tcp_sockopt_after_listen(net: &Network, family: IpAddressFamily) {
 
     // Verify options on accepted socket:
     {
-        assert_eq!(accepted_client.keep_alive().unwrap(), !default_keep_alive);
-        assert_eq!(accepted_client.unicast_hop_limit().unwrap(), 42);
+        assert_eq!(
+            accepted_client.keep_alive_enabled().unwrap(),
+            !default_keep_alive
+        );
+        assert_eq!(accepted_client.keep_alive_idle_time().unwrap(), 42 * SECOND);
+        assert_eq!(accepted_client.keep_alive_interval().unwrap(), 42 * SECOND);
+        assert_eq!(accepted_client.keep_alive_count().unwrap(), 42);
+        assert_eq!(accepted_client.hop_limit().unwrap(), 42);
         assert_eq!(accepted_client.receive_buffer_size().unwrap(), 0x10000);
         assert_eq!(accepted_client.send_buffer_size().unwrap(), 0x10000);
     }
diff --git a/crates/test-programs/src/bin/preview2_tcp_states.rs b/crates/test-programs/src/bin/preview2_tcp_states.rs
index e4439af81..308c6cfd6 100644
--- a/crates/test-programs/src/bin/preview2_tcp_states.rs
+++ b/crates/test-programs/src/bin/preview2_tcp_states.rs
@@ -32,6 +32,7 @@ fn test_tcp_unbound_state_invariants(family: IpAddressFamily) {
         sock.remote_address(),
         Err(ErrorCode::InvalidState)
     ));
+    assert_eq!(sock.is_listening(), false);
     assert_eq!(sock.address_family(), family);
 
     if family == IpAddressFamily::Ipv6 {
@@ -49,10 +50,16 @@ fn test_tcp_unbound_state_invariants(family: IpAddressFamily) {
     }
 
     assert!(matches!(sock.set_listen_backlog_size(32), Ok(_)));
-    assert!(matches!(sock.keep_alive(), Ok(_)));
-    assert!(matches!(sock.set_keep_alive(false), Ok(_)));
-    assert!(matches!(sock.unicast_hop_limit(), Ok(_)));
-    assert!(matches!(sock.set_unicast_hop_limit(255), Ok(_)));
+    assert!(matches!(sock.keep_alive_enabled(), Ok(_)));
+    assert!(matches!(sock.set_keep_alive_enabled(false), Ok(_)));
+    assert!(matches!(sock.keep_alive_idle_time(), Ok(_)));
+    assert!(matches!(sock.set_keep_alive_idle_time(1), Ok(_)));
+    assert!(matches!(sock.keep_alive_interval(), Ok(_)));
+    assert!(matches!(sock.set_keep_alive_interval(1), Ok(_)));
+    assert!(matches!(sock.keep_alive_count(), Ok(_)));
+    assert!(matches!(sock.set_keep_alive_count(1), Ok(_)));
+    assert!(matches!(sock.hop_limit(), Ok(_)));
+    assert!(matches!(sock.set_hop_limit(255), Ok(_)));
     assert!(matches!(sock.receive_buffer_size(), Ok(_)));
     assert!(matches!(sock.set_receive_buffer_size(16000), Ok(_)));
     assert!(matches!(sock.send_buffer_size(), Ok(_)));
@@ -90,6 +97,7 @@ fn test_tcp_bound_state_invariants(net: &Network, family: IpAddressFamily) {
         sock.remote_address(),
         Err(ErrorCode::InvalidState)
     ));
+    assert_eq!(sock.is_listening(), false);
     assert_eq!(sock.address_family(), family);
 
     if family == IpAddressFamily::Ipv6 {
@@ -107,10 +115,16 @@ fn test_tcp_bound_state_invariants(net: &Network, family: IpAddressFamily) {
     }
 
     assert!(matches!(sock.set_listen_backlog_size(32), Ok(_)));
-    assert!(matches!(sock.keep_alive(), Ok(_)));
-    assert!(matches!(sock.set_keep_alive(false), Ok(_)));
-    assert!(matches!(sock.unicast_hop_limit(), Ok(_)));
-    assert!(matches!(sock.set_unicast_hop_limit(255), Ok(_)));
+    assert!(matches!(sock.keep_alive_enabled(), Ok(_)));
+    assert!(matches!(sock.set_keep_alive_enabled(false), Ok(_)));
+    assert!(matches!(sock.keep_alive_idle_time(), Ok(_)));
+    assert!(matches!(sock.set_keep_alive_idle_time(1), Ok(_)));
+    assert!(matches!(sock.keep_alive_interval(), Ok(_)));
+    assert!(matches!(sock.set_keep_alive_interval(1), Ok(_)));
+    assert!(matches!(sock.keep_alive_count(), Ok(_)));
+    assert!(matches!(sock.set_keep_alive_count(1), Ok(_)));
+    assert!(matches!(sock.hop_limit(), Ok(_)));
+    assert!(matches!(sock.set_hop_limit(255), Ok(_)));
     assert!(matches!(sock.receive_buffer_size(), Ok(_)));
     assert!(matches!(sock.set_receive_buffer_size(16000), Ok(_)));
     assert!(matches!(sock.send_buffer_size(), Ok(_)));
@@ -152,6 +166,7 @@ fn test_tcp_listening_state_invariants(net: &Network, family: IpAddressFamily) {
         sock.remote_address(),
         Err(ErrorCode::InvalidState)
     ));
+    assert_eq!(sock.is_listening(), true);
     assert_eq!(sock.address_family(), family);
 
     if family == IpAddressFamily::Ipv6 {
@@ -172,10 +187,16 @@ fn test_tcp_listening_state_invariants(net: &Network, family: IpAddressFamily) {
         sock.set_listen_backlog_size(32),
         Ok(_) | Err(ErrorCode::NotSupported)
     ));
-    assert!(matches!(sock.keep_alive(), Ok(_)));
-    assert!(matches!(sock.set_keep_alive(false), Ok(_)));
-    assert!(matches!(sock.unicast_hop_limit(), Ok(_)));
-    assert!(matches!(sock.set_unicast_hop_limit(255), Ok(_)));
+    assert!(matches!(sock.keep_alive_enabled(), Ok(_)));
+    assert!(matches!(sock.set_keep_alive_enabled(false), Ok(_)));
+    assert!(matches!(sock.keep_alive_idle_time(), Ok(_)));
+    assert!(matches!(sock.set_keep_alive_idle_time(1), Ok(_)));
+    assert!(matches!(sock.keep_alive_interval(), Ok(_)));
+    assert!(matches!(sock.set_keep_alive_interval(1), Ok(_)));
+    assert!(matches!(sock.keep_alive_count(), Ok(_)));
+    assert!(matches!(sock.set_keep_alive_count(1), Ok(_)));
+    assert!(matches!(sock.hop_limit(), Ok(_)));
+    assert!(matches!(sock.set_hop_limit(255), Ok(_)));
     assert!(matches!(sock.receive_buffer_size(), Ok(_)));
     assert!(matches!(sock.set_receive_buffer_size(16000), Ok(_)));
     assert!(matches!(sock.send_buffer_size(), Ok(_)));
@@ -214,6 +235,7 @@ fn test_tcp_connected_state_invariants(net: &Network, family: IpAddressFamily) {
 
     assert!(matches!(sock.local_address(), Ok(_)));
     assert!(matches!(sock.remote_address(), Ok(_)));
+    assert_eq!(sock.is_listening(), false);
     assert_eq!(sock.address_family(), family);
 
     if family == IpAddressFamily::Ipv6 {
@@ -230,10 +252,16 @@ fn test_tcp_connected_state_invariants(net: &Network, family: IpAddressFamily) {
         ));
     }
 
-    assert!(matches!(sock.keep_alive(), Ok(_)));
-    assert!(matches!(sock.set_keep_alive(false), Ok(_)));
-    assert!(matches!(sock.unicast_hop_limit(), Ok(_)));
-    assert!(matches!(sock.set_unicast_hop_limit(255), Ok(_)));
+    assert!(matches!(sock.keep_alive_enabled(), Ok(_)));
+    assert!(matches!(sock.set_keep_alive_enabled(false), Ok(_)));
+    assert!(matches!(sock.keep_alive_idle_time(), Ok(_)));
+    assert!(matches!(sock.set_keep_alive_idle_time(1), Ok(_)));
+    assert!(matches!(sock.keep_alive_interval(), Ok(_)));
+    assert!(matches!(sock.set_keep_alive_interval(1), Ok(_)));
+    assert!(matches!(sock.keep_alive_count(), Ok(_)));
+    assert!(matches!(sock.set_keep_alive_count(1), Ok(_)));
+    assert!(matches!(sock.hop_limit(), Ok(_)));
+    assert!(matches!(sock.set_hop_limit(255), Ok(_)));
     assert!(matches!(sock.receive_buffer_size(), Ok(_)));
     assert!(matches!(sock.set_receive_buffer_size(16000), Ok(_)));
     assert!(matches!(sock.send_buffer_size(), Ok(_)));
diff --git a/crates/test-programs/src/bin/preview2_udp_sockopts.rs b/crates/test-programs/src/bin/preview2_udp_sockopts.rs
index b9cebbcb3..f705b476c 100644
--- a/crates/test-programs/src/bin/preview2_udp_sockopts.rs
+++ b/crates/test-programs/src/bin/preview2_udp_sockopts.rs
@@ -30,9 +30,17 @@ fn test_udp_sockopt_input_ranges(family: IpAddressFamily) {
     assert!(matches!(sock.set_unicast_hop_limit(1), Ok(_)));
     assert!(matches!(sock.set_unicast_hop_limit(u8::MAX), Ok(_)));
 
-    assert!(matches!(sock.set_receive_buffer_size(0), Ok(_))); // Unsupported sizes should be silently capped.
+    assert!(matches!(
+        sock.set_receive_buffer_size(0),
+        Err(ErrorCode::InvalidArgument)
+    ));
+    assert!(matches!(sock.set_receive_buffer_size(1), Ok(_))); // Unsupported sizes should be silently capped.
     assert!(matches!(sock.set_receive_buffer_size(u64::MAX), Ok(_))); // Unsupported sizes should be silently capped.
-    assert!(matches!(sock.set_send_buffer_size(0), Ok(_))); // Unsupported sizes should be silently capped.
+    assert!(matches!(
+        sock.set_send_buffer_size(0),
+        Err(ErrorCode::InvalidArgument)
+    ));
+    assert!(matches!(sock.set_send_buffer_size(1), Ok(_))); // Unsupported sizes should be silently capped.
     assert!(matches!(sock.set_send_buffer_size(u64::MAX), Ok(_))); // Unsupported sizes should be silently capped.
 }