Repository: bytecodealliance/wasmtime
Pull request #7514 (2023-11-09 18:42:58)

[15.0.0] wasi-sockets: Add & refine socket options. (SO_ACCEPTCONN, TCP_KEEPIDLE/INTVL/CNT)
This is a backport of #7512 to the 15.0.0 branch.


Patch:
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/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/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/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.
 }