Repository: bytecodealliance/wasmtime
Pull request #7493 (2023-11-07 14:22:09)

[15.0.0] Backports of recent PRs
This is a backport of a few PRs recently:

* https://github.com/bytecodealliance/wasmtime/pull/7488 - get new defaults for semicolons/WIT format out sooner
* https://github.com/bytecodealliance/wasmtime/pull/7423 - tweaks to tcp/udp errors as well as making the next PR cherry-pick-able
* https://github.com/bytecodealliance/wasmtime/pull/7483 - modified wasi:sockets WITs
* https://github.com/bytecodealliance/wasmtime/pull/7487 - release notes


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 65b555e89..5e6c74595 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2638,7 +2638,7 @@ version = "0.0.0"
 dependencies = [
  "cargo_metadata",
  "heck",
- "wit-component",
+ "wit-component 0.18.0",
 ]
 
 [[package]]
@@ -3101,18 +3101,18 @@ checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
 
 [[package]]
 name = "wasm-encoder"
-version = "0.36.1"
+version = "0.36.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53ae0be20bf87918df4fa831bfbbd0b491d24aee407ed86360eae4c2c5608d38"
+checksum = "822b645bf4f2446b949776ffca47e2af60b167209ffb70814ef8779d299cd421"
 dependencies = [
  "leb128",
 ]
 
 [[package]]
 name = "wasm-metadata"
-version = "0.10.10"
+version = "0.10.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5621910462c61a8efc3248fdfb1739bf649bb335b0df935c27b340418105f9d8"
+checksum = "2167ce53b2faa16a92c6cafd4942cff16c9a4fa0c5a5a0a41131ee4e49fc055f"
 dependencies = [
  "anyhow",
  "indexmap 2.0.0",
@@ -3126,9 +3126,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-mutate"
-version = "0.2.39"
+version = "0.2.40"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b39266e421acd09370e738fc042a5224d0014036ed919e68f1dd9a98a9b28f5a"
+checksum = "49afec9e74f8dd90367c8f9b60440e6b84bdfd46e8ca133dd6a040f296bbc328"
 dependencies = [
  "egg",
  "log",
@@ -3140,9 +3140,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-smith"
-version = "0.12.22"
+version = "0.12.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39217efc459328aa5a29a5bbf792e76f1ae7ebeedbf8878f8320b6a0a6544b06"
+checksum = "426255ec37d1127b020492d957b1a48c0828e667ad2693d98af942e276cfbd86"
 dependencies = [
  "arbitrary",
  "flagset",
@@ -3192,9 +3192,9 @@ dependencies = [
 
 [[package]]
 name = "wasmparser"
-version = "0.116.0"
+version = "0.116.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53290b1276c5c2d47d694fb1a920538c01f51690e7e261acbe1d10c5fc306ea1"
+checksum = "a58e28b80dd8340cb07b8242ae654756161f6fc8d0038123d679b7b99964fa50"
 dependencies = [
  "indexmap 2.0.0",
  "semver",
@@ -3211,9 +3211,9 @@ dependencies = [
 
 [[package]]
 name = "wasmprinter"
-version = "0.2.71"
+version = "0.2.72"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f98260aa20f939518bcec1fac32c78898d5c68872e7363a4651f21f791b6c7e"
+checksum = "9aff4df0cdf1906ec040e97d78c3fc8fd26d3f8d70adaac81f07f80957b63b54"
 dependencies = [
  "anyhow",
  "wasmparser",
@@ -3388,7 +3388,7 @@ dependencies = [
  "wasmtime-wasi-nn",
  "wasmtime-wasi-threads",
  "wasmtime-wast",
- "wast 67.0.0",
+ "wast 67.0.1",
  "wat",
  "windows-sys",
 ]
@@ -3419,7 +3419,7 @@ dependencies = [
  "wasmtime",
  "wasmtime-component-util",
  "wasmtime-wit-bindgen",
- "wit-parser",
+ "wit-parser 0.13.0",
 ]
 
 [[package]]
@@ -3774,7 +3774,7 @@ dependencies = [
  "anyhow",
  "log",
  "wasmtime",
- "wast 67.0.0",
+ "wast 67.0.1",
 ]
 
 [[package]]
@@ -3799,7 +3799,7 @@ dependencies = [
  "anyhow",
  "heck",
  "indexmap 2.0.0",
- "wit-parser",
+ "wit-parser 0.13.0",
 ]
 
 [[package]]
@@ -3817,9 +3817,9 @@ dependencies = [
 
 [[package]]
 name = "wast"
-version = "67.0.0"
+version = "67.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36c2933efd77ff2398b83817a98984ffe4b67aefd9aa1d2c8e68e19b553f1c38"
+checksum = "a974d82fac092b5227c1663e16514e7a85f32014e22e6fdcb08b71aec9d3fb1e"
 dependencies = [
  "leb128",
  "memchr",
@@ -3829,11 +3829,11 @@ dependencies = [
 
 [[package]]
 name = "wat"
-version = "1.0.78"
+version = "1.0.79"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c02905d13751dcb18f4e19f489d37a1bf139f519feaeef28d072a41a78e69a74"
+checksum = "adb220934f92f8551144c0003d1bc57a060674c99139f45ed623fbbf6d9262e7"
 dependencies = [
- "wast 67.0.0",
+ "wast 67.0.1",
 ]
 
 [[package]]
@@ -4104,8 +4104,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c8bf1fddccaff31a1ad57432d8bfb7027a7e552969b6c68d6d8820dcf5c2371f"
 dependencies = [
  "anyhow",
- "wit-component",
- "wit-parser",
+ "wit-component 0.17.0",
+ "wit-parser 0.12.2",
 ]
 
 [[package]]
@@ -4118,7 +4118,7 @@ dependencies = [
  "heck",
  "wasm-metadata",
  "wit-bindgen-core",
- "wit-component",
+ "wit-component 0.17.0",
 ]
 
 [[package]]
@@ -4133,7 +4133,7 @@ dependencies = [
  "syn 2.0.29",
  "wit-bindgen-core",
  "wit-bindgen-rust",
- "wit-component",
+ "wit-component 0.17.0",
 ]
 
 [[package]]
@@ -4152,7 +4152,26 @@ dependencies = [
  "wasm-encoder",
  "wasm-metadata",
  "wasmparser",
- "wit-parser",
+ "wit-parser 0.12.2",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634d0371ac5e57e42991aa53f0d79e53e53484afbf54777a5347605b0b229b9d"
+dependencies = [
+ "anyhow",
+ "bitflags 2.4.1",
+ "indexmap 2.0.0",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser 0.13.0",
 ]
 
 [[package]]
@@ -4172,6 +4191,23 @@ dependencies = [
  "unicode-xid",
 ]
 
+[[package]]
+name = "wit-parser"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15df6b7b28ce94b8be39d8df5cb21a08a4f3b9f33b631aedb4aa5776f785ead3"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap 2.0.0",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+]
+
 [[package]]
 name = "witx"
 version = "0.9.1"
diff --git a/Cargo.toml b/Cargo.toml
index 513105811..cc373071f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -213,14 +213,14 @@ wit-bindgen = { version = "0.13.1", default-features = false }
 
 # wasm-tools family:
 wasmparser = "0.116.0"
-wat = "1.0.78"
-wast = "67.0.0"
-wasmprinter = "0.2.71"
-wasm-encoder = "0.36.1"
-wasm-smith = "0.12.22"
-wasm-mutate = "0.2.39"
-wit-parser = "0.12.2"
-wit-component = "0.17.0"
+wat = "1.0.79"
+wast = "67.0.1"
+wasmprinter = "0.2.72"
+wasm-encoder = "0.36.2"
+wasm-smith = "0.12.23"
+wasm-mutate = "0.2.40"
+wit-parser = "0.13.0"
+wit-component = "0.18.0"
 
 # Non-Bytecode Alliance maintained dependencies:
 # --------------------------
diff --git a/RELEASES.md b/RELEASES.md
index 1988ab0aa..53b88a009 100644
--- a/RELEASES.md
+++ b/RELEASES.md
@@ -6,8 +6,119 @@ Unreleased.
 
 ### Added
 
+* Multiple versions of interfaces are now supported in `bindgen!`.
+  [#7172](https://github.com/bytecodealliance/wasmtime/pull/7172)
+
+* UDP has been implemented in `wasi:sockets`.
+  [#7148](https://github.com/bytecodealliance/wasmtime/pull/7148)
+  [#7243](https://github.com/bytecodealliance/wasmtime/pull/7243)
+
+* Support for custom stack memory allocation has been added.
+  [#7209](https://github.com/bytecodealliance/wasmtime/pull/7209)
+
+* The `memory_init_cow` setting can now be configured in the C API.
+  [#7227](https://github.com/bytecodealliance/wasmtime/pull/7227)
+
+* The `splice` method of WASI streams has been implemented.
+  [#7234](https://github.com/bytecodealliance/wasmtime/pull/7234)
+
+* Wasmtime binary releases now have a `wasmtime-min` executable in addition to
+  `libwasmtime-min.*` libraries for the C API. These showcase a minimal
+  build of Wasmtime for comparison.
+  [#7282](https://github.com/bytecodealliance/wasmtime/pull/7282)
+  [#7315](https://github.com/bytecodealliance/wasmtime/pull/7315)
+  [#7350](https://github.com/bytecodealliance/wasmtime/pull/7350)
+
 ### Changed
 
+* Many changes to `wasi:http` WITs have happened to keep up with the proposal as
+  it prepares to reach a more stable status.
+  [#7161](https://github.com/bytecodealliance/wasmtime/pull/7161)
+  [#7406](https://github.com/bytecodealliance/wasmtime/pull/7406)
+  [#7383](https://github.com/bytecodealliance/wasmtime/pull/7383)
+  [#7417](https://github.com/bytecodealliance/wasmtime/pull/7417)
+  [#7451](https://github.com/bytecodealliance/wasmtime/pull/7451)
+
+* Add an error resource to WASI streams.
+  [#7152](https://github.com/bytecodealliance/wasmtime/pull/7152)
+
+* Syntax in `bindgen!`'s `trappable_error_type` configuration has changed.
+  [#7170](https://github.com/bytecodealliance/wasmtime/pull/7170)
+
+* TCP errors in `wasi:sockets` have been simplified and clarified.
+  [#7120](https://github.com/bytecodealliance/wasmtime/pull/7120)
+
+* Wasmtime/Cranelift now require Rust 1.71.0 to compile.
+  [#7206](https://github.com/bytecodealliance/wasmtime/pull/7206)
+
+* Logging in Wasmtime is now configured with `WASMTIME_LOG` instead of
+  `RUST_LOG`.
+  [#7239](https://github.com/bytecodealliance/wasmtime/pull/7239)
+
+* Fuel-related APIs on `Store` have been refactored and reimplemented with two
+  new methods `set_fuel` and `reset_fuel`. Previous methods have been removed.
+  [#7240](https://github.com/bytecodealliance/wasmtime/pull/7240)
+  [#7298](https://github.com/bytecodealliance/wasmtime/pull/7298)
+
+* The `forward` method of WASI streams has been removed.
+  [#7234](https://github.com/bytecodealliance/wasmtime/pull/7234)
+
+* The WebAssembly `threads`, `multi-memory`, and `relaxed-simd` proposals are
+  now enabled by default.
+  [#7285](https://github.com/bytecodealliance/wasmtime/pull/7285)
+
+* Logging is now implemented for `wasmtime serve`.
+  [#7366](https://github.com/bytecodealliance/wasmtime/pull/7366)
+
+* Filesystem locking has been temporarily removed from WASI.
+  [#7355](https://github.com/bytecodealliance/wasmtime/pull/7355)
+
+* Wasmtime's implementation of WASI preview1 built on top of preview2
+  (`-Spreview2`) has been enabled by default.
+  [#7365](https://github.com/bytecodealliance/wasmtime/pull/7365)
+
+* The `wasi:clocks` interface now has two `subscribe` functions and a `duration`
+  type.
+  [#7358](https://github.com/bytecodealliance/wasmtime/pull/7358)
+
+* The `wasi:io/poll` interface has seen some refactoring.
+  [#7427](https://github.com/bytecodealliance/wasmtime/pull/7427)
+
+### Fixed
+
+* Profiling the first function in a module now works.
+  [#7254](https://github.com/bytecodealliance/wasmtime/pull/7254)
+
+* Consecutive writes to files in preview2 have been fixed.
+  [#7394](https://github.com/bytecodealliance/wasmtime/pull/7394)
+
+* Copy-on-write initialization of linear memories has been fixed for components.
+  [#7459](https://github.com/bytecodealliance/wasmtime/pull/7459)
+
+### Cranelift
+
+* Support for proof-carrying code has been added to Cranelift to assist with an
+  extra layer of validation about properties such as WebAssembly memory accesses
+  in the future.
+  [#7165](https://github.com/bytecodealliance/wasmtime/pull/7165)
+  [#7180](https://github.com/bytecodealliance/wasmtime/pull/7180)
+  [#7219](https://github.com/bytecodealliance/wasmtime/pull/7219)
+  [#7231](https://github.com/bytecodealliance/wasmtime/pull/7231)
+  [#7262](https://github.com/bytecodealliance/wasmtime/pull/7262)
+  [#7263](https://github.com/bytecodealliance/wasmtime/pull/7263)
+  [#7274](https://github.com/bytecodealliance/wasmtime/pull/7274)
+  [#7280](https://github.com/bytecodealliance/wasmtime/pull/7280)
+  [#7281](https://github.com/bytecodealliance/wasmtime/pull/7281)
+  [#7352](https://github.com/bytecodealliance/wasmtime/pull/7352)
+  [#7389](https://github.com/bytecodealliance/wasmtime/pull/7389)
+  [#7468](https://github.com/bytecodealliance/wasmtime/pull/7468)
+
+* Rematerialization of values no longer accidentally overrides LICM.
+  [#7306](https://github.com/bytecodealliance/wasmtime/pull/7306)
+
+* Inline stack probes no longer make Valgrind unhappy.
+  [#7470](https://github.com/bytecodealliance/wasmtime/pull/7470)
+
 --------------------------------------------------------------------------------
 
 ## 14.0.4
diff --git a/crates/wasi-http/wit/deps/sockets/ip-name-lookup.wit b/crates/wasi-http/wit/deps/sockets/ip-name-lookup.wit
index 5ffe2a217..270e70258 100644
--- a/crates/wasi-http/wit/deps/sockets/ip-name-lookup.wit
+++ b/crates/wasi-http/wit/deps/sockets/ip-name-lookup.wit
@@ -1,40 +1,30 @@
 
 interface ip-name-lookup {
     use wasi:io/poll@0.2.0-rc-2023-11-05.{pollable};
-    use network.{network, error-code, ip-address, ip-address-family};
+    use network.{network, error-code, ip-address};
 
 
     /// Resolve an internet host name to a list of IP addresses.
     ///
-    /// See the wasi-socket proposal README.md for a comparison with getaddrinfo.
-    ///
-    /// # Parameters
-    /// - `name`: The name to look up. IP addresses are not allowed. Unicode domain names are automatically converted
-    ///     to ASCII using IDNA encoding.
-    /// - `address-family`: If provided, limit the results to addresses of this specific address family.
-    /// - `include-unavailable`: When set to true, this function will also return addresses of which the runtime
-    ///   thinks (or knows) can't be connected to at the moment. For example, this will return IPv6 addresses on
-    ///   systems without an active IPv6 interface. Notes:
-    ///     - Even when no public IPv6 interfaces are present or active, names like "localhost" can still resolve to an IPv6 address.
-    ///     - Whatever is "available" or "unavailable" is volatile and can change everytime a network cable is unplugged.
+    /// Unicode domain names are automatically converted to ASCII using IDNA encoding.
+    /// If the input is an IP address string, the address is parsed and returned
+    /// as-is without making any external requests.
     ///
-    /// This function never blocks. It either immediately fails or immediately returns successfully with a `resolve-address-stream`
-    /// that can be used to (asynchronously) fetch the results.
+    /// See the wasi-socket proposal README.md for a comparison with getaddrinfo.
     ///
-    /// At the moment, the stream never completes successfully with 0 items. Ie. the first call
-    /// to `resolve-next-address` never returns `ok(none)`. This may change in the future.
+    /// This function never blocks. It either immediately fails or immediately
+    /// returns successfully with a `resolve-address-stream` that can be used
+    /// to (asynchronously) fetch the results.
     ///
     /// # Typical errors
-    /// - `invalid-argument`:     `name` is a syntactically invalid domain name.
-    /// - `invalid-argument`:     `name` is an IP address.
-    /// - `not-supported`:        The specified `address-family` is not supported. (EAI_FAMILY)
+    /// - `invalid-argument`: `name` is a syntactically invalid domain name or IP address.
     ///
     /// # References:
     /// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/getaddrinfo.html>
     /// - <https://man7.org/linux/man-pages/man3/getaddrinfo.3.html>
     /// - <https://learn.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfo>
     /// - <https://man.freebsd.org/cgi/man.cgi?query=getaddrinfo&sektion=3>
-    resolve-addresses: func(network: borrow<network>, name: string, address-family: option<ip-address-family>, include-unavailable: bool) -> result<resolve-address-stream, error-code>;
+    resolve-addresses: func(network: borrow<network>, name: string) -> result<resolve-address-stream, error-code>;
 
     resource resolve-address-stream {
         /// Returns the next address from the resolver.
diff --git a/crates/wasi-http/wit/deps/sockets/tcp.wit b/crates/wasi-http/wit/deps/sockets/tcp.wit
index b172b2077..b9ef0625a 100644
--- a/crates/wasi-http/wit/deps/sockets/tcp.wit
+++ b/crates/wasi-http/wit/deps/sockets/tcp.wit
@@ -81,7 +81,7 @@ interface tcp {
         /// - `connection-refused`:        The connection was forcefully rejected. (ECONNREFUSED)
         /// - `connection-reset`:          The connection was reset. (ECONNRESET)
         /// - `connection-aborted`:        The connection was aborted. (ECONNABORTED)
-        /// - `remote-unreachable`:        The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN)
+        /// - `remote-unreachable`:        The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET)
         /// - `address-in-use`:            Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD)
         /// - `not-in-progress`:           A `connect` operation is not in progress.
         /// - `would-block`:               Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN)
diff --git a/crates/wasi-http/wit/deps/sockets/udp.wit b/crates/wasi-http/wit/deps/sockets/udp.wit
index 232c2b41c..1ee1843a9 100644
--- a/crates/wasi-http/wit/deps/sockets/udp.wit
+++ b/crates/wasi-http/wit/deps/sockets/udp.wit
@@ -97,7 +97,8 @@ interface udp {
         /// - `invalid-argument`:          The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL)
         /// - `invalid-state`:             The socket is not bound.
         /// - `address-in-use`:            Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD)
-        /// - `remote-unreachable`:        The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN)
+        /// - `remote-unreachable`:        The remote address is not reachable. (ECONNRESET, ENETRESET, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET)
+        /// - `connection-refused`:        The connection was refused. (ECONNREFUSED)
         ///
         /// # References
         /// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/connect.html>
@@ -190,7 +191,8 @@ interface udp {
         /// This function never returns `error(would-block)`.
         ///
         /// # Typical errors
-        /// - `remote-unreachable`: The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN)
+        /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET)
+        /// - `connection-refused`: The connection was refused. (ECONNREFUSED)
         ///
         /// # References
         /// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/recvfrom.html>
@@ -246,7 +248,8 @@ interface udp {
         /// - `invalid-argument`:        The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL)
         /// - `invalid-argument`:        The socket is in "connected" mode and `remote-address` is `some` value that does not match the address passed to `stream`. (EISCONN)
         /// - `invalid-argument`:        The socket is not "connected" and no value for `remote-address` was provided. (EDESTADDRREQ)
-        /// - `remote-unreachable`:      The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN)
+        /// - `remote-unreachable`:      The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET)
+        /// - `connection-refused`:      The connection was refused. (ECONNREFUSED)
         /// - `datagram-too-large`:      The datagram is too large. (EMSGSIZE)
         ///
         /// # References
diff --git a/crates/wasi/src/preview2/host/mod.rs b/crates/wasi/src/preview2/host/mod.rs
index 651d2cd38..7aa4a8741 100644
--- a/crates/wasi/src/preview2/host/mod.rs
+++ b/crates/wasi/src/preview2/host/mod.rs
@@ -4,7 +4,7 @@ mod exit;
 pub(crate) mod filesystem;
 mod instance_network;
 mod io;
-mod network;
+pub(crate) mod network;
 mod random;
 mod tcp;
 mod tcp_create_socket;
diff --git a/crates/wasi/src/preview2/host/network.rs b/crates/wasi/src/preview2/host/network.rs
index aa01cb4f4..ce6f2f155 100644
--- a/crates/wasi/src/preview2/host/network.rs
+++ b/crates/wasi/src/preview2/host/network.rs
@@ -1,7 +1,8 @@
 use crate::preview2::bindings::sockets::network::{
-    self, ErrorCode, IpAddressFamily, IpSocketAddress, Ipv4Address, Ipv4SocketAddress, Ipv6Address,
+    self, ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, Ipv4SocketAddress,
     Ipv6SocketAddress,
 };
+use crate::preview2::network::{from_ipv4_addr, from_ipv6_addr, to_ipv4_addr, to_ipv6_addr};
 use crate::preview2::{SocketError, WasiView};
 use rustix::io::Errno;
 use std::io;
@@ -104,6 +105,15 @@ impl From<Errno> for ErrorCode {
     }
 }
 
+impl From<std::net::IpAddr> for IpAddress {
+    fn from(addr: std::net::IpAddr) -> Self {
+        match addr {
+            std::net::IpAddr::V4(v4) => Self::Ipv4(from_ipv4_addr(v4)),
+            std::net::IpAddr::V6(v6) => Self::Ipv6(from_ipv6_addr(v6)),
+        }
+    }
+}
+
 impl From<IpSocketAddress> for std::net::SocketAddr {
     fn from(addr: IpSocketAddress) -> Self {
         match addr {
@@ -159,26 +169,6 @@ impl From<std::net::SocketAddrV6> for Ipv6SocketAddress {
     }
 }
 
-fn to_ipv4_addr(addr: Ipv4Address) -> std::net::Ipv4Addr {
-    let (x0, x1, x2, x3) = addr;
-    std::net::Ipv4Addr::new(x0, x1, x2, x3)
-}
-
-fn from_ipv4_addr(addr: std::net::Ipv4Addr) -> Ipv4Address {
-    let [x0, x1, x2, x3] = addr.octets();
-    (x0, x1, x2, x3)
-}
-
-fn to_ipv6_addr(addr: Ipv6Address) -> std::net::Ipv6Addr {
-    let (x0, x1, x2, x3, x4, x5, x6, x7) = addr;
-    std::net::Ipv6Addr::new(x0, x1, x2, x3, x4, x5, x6, x7)
-}
-
-fn from_ipv6_addr(addr: std::net::Ipv6Addr) -> Ipv6Address {
-    let [x0, x1, x2, x3, x4, x5, x6, x7] = addr.segments();
-    (x0, x1, x2, x3, x4, x5, x6, x7)
-}
-
 impl std::net::ToSocketAddrs for IpSocketAddress {
     type Iter = <std::net::SocketAddr as std::net::ToSocketAddrs>::Iter;
 
@@ -220,3 +210,251 @@ impl From<cap_net_ext::AddressFamily> for IpAddressFamily {
         }
     }
 }
+
+pub(crate) mod util {
+    use std::net::{IpAddr, Ipv6Addr, SocketAddr};
+
+    use crate::preview2::bindings::sockets::network::ErrorCode;
+    use crate::preview2::network::SocketAddressFamily;
+    use crate::preview2::SocketResult;
+    use cap_net_ext::{Blocking, TcpBinder, TcpConnecter, TcpListenerExt, UdpBinder};
+    use cap_std::net::{TcpListener, TcpStream, UdpSocket};
+    use rustix::fd::AsFd;
+    use rustix::io::Errno;
+    use rustix::net::sockopt;
+
+    pub fn validate_unicast(addr: &SocketAddr) -> SocketResult<()> {
+        match to_canonical(&addr.ip()) {
+            IpAddr::V4(ipv4) => {
+                if ipv4.is_multicast() || ipv4.is_broadcast() {
+                    Err(ErrorCode::InvalidArgument.into())
+                } else {
+                    Ok(())
+                }
+            }
+            IpAddr::V6(ipv6) => {
+                if ipv6.is_multicast() {
+                    Err(ErrorCode::InvalidArgument.into())
+                } else {
+                    Ok(())
+                }
+            }
+        }
+    }
+
+    pub fn validate_remote_address(addr: &SocketAddr) -> SocketResult<()> {
+        if to_canonical(&addr.ip()).is_unspecified() {
+            return Err(ErrorCode::InvalidArgument.into());
+        }
+
+        if addr.port() == 0 {
+            return Err(ErrorCode::InvalidArgument.into());
+        }
+
+        Ok(())
+    }
+
+    pub fn validate_address_family(
+        addr: &SocketAddr,
+        socket_family: &SocketAddressFamily,
+    ) -> SocketResult<()> {
+        match (socket_family, addr.ip()) {
+            (SocketAddressFamily::Ipv4, IpAddr::V4(_)) => Ok(()),
+            (SocketAddressFamily::Ipv6 { v6only }, IpAddr::V6(ipv6)) => {
+                if is_deprecated_ipv4_compatible(&ipv6) {
+                    // Reject IPv4-*compatible* IPv6 addresses. They have been deprecated
+                    // since 2006, OS handling of them is inconsistent and our own
+                    // validations don't take them into account either.
+                    // Note that these are not the same as IPv4-*mapped* IPv6 addresses.
+                    Err(ErrorCode::InvalidArgument.into())
+                } else if *v6only && ipv6.to_ipv4_mapped().is_some() {
+                    Err(ErrorCode::InvalidArgument.into())
+                } else {
+                    Ok(())
+                }
+            }
+            _ => Err(ErrorCode::InvalidArgument.into()),
+        }
+    }
+
+    // Can be removed once `IpAddr::to_canonical` becomes stable.
+    pub fn to_canonical(addr: &IpAddr) -> IpAddr {
+        match addr {
+            IpAddr::V4(ipv4) => IpAddr::V4(*ipv4),
+            IpAddr::V6(ipv6) => {
+                if let Some(ipv4) = ipv6.to_ipv4_mapped() {
+                    IpAddr::V4(ipv4)
+                } else {
+                    IpAddr::V6(*ipv6)
+                }
+            }
+        }
+    }
+
+    fn is_deprecated_ipv4_compatible(addr: &Ipv6Addr) -> bool {
+        matches!(addr.segments(), [0, 0, 0, 0, 0, 0, _, _])
+            && *addr != Ipv6Addr::UNSPECIFIED
+            && *addr != Ipv6Addr::LOCALHOST
+    }
+
+    /*
+     * Syscalls wrappers with (opinionated) portability fixes.
+     */
+
+    pub fn tcp_bind(listener: &TcpListener, binder: &TcpBinder) -> std::io::Result<()> {
+        binder
+            .bind_existing_tcp_listener(listener)
+            .map_err(|error| match Errno::from_io_error(&error) {
+                #[cfg(windows)]
+                Some(Errno::NOBUFS) => Errno::ADDRINUSE.into(), // Windows returns WSAENOBUFS when the ephemeral ports have been exhausted.
+                _ => error,
+            })
+    }
+
+    pub fn udp_bind(socket: &UdpSocket, binder: &UdpBinder) -> std::io::Result<()> {
+        binder.bind_existing_udp_socket(socket).map_err(|error| {
+            match Errno::from_io_error(&error) {
+                #[cfg(windows)]
+                Some(Errno::NOBUFS) => Errno::ADDRINUSE.into(), // Windows returns WSAENOBUFS when the ephemeral ports have been exhausted.
+                _ => error,
+            }
+        })
+    }
+
+    pub fn tcp_connect(listener: &TcpListener, connecter: &TcpConnecter) -> std::io::Result<()> {
+        connecter.connect_existing_tcp_listener(listener).map_err(
+            |error| match Errno::from_io_error(&error) {
+                // On POSIX, non-blocking `connect` returns `EINPROGRESS`. Windows returns `WSAEWOULDBLOCK`.
+                #[cfg(windows)]
+                Some(Errno::WOULDBLOCK) => Errno::INPROGRESS.into(),
+                _ => error,
+            },
+        )
+    }
+
+    pub fn tcp_accept(
+        listener: &TcpListener,
+        blocking: Blocking,
+    ) -> std::io::Result<(TcpStream, SocketAddr)> {
+        listener
+            .accept_with(blocking)
+            .map_err(|error| match Errno::from_io_error(&error) {
+                #[cfg(windows)]
+                Some(Errno::INPROGRESS) => Errno::INTR.into(), // "A blocking Windows Sockets 1.1 call is in progress, or the service provider is still processing a callback function."
+
+                // Normalize Linux' non-standard behavior.
+                // "Linux accept() passes already-pending network errors on the new socket as an error code from accept(). This behavior differs from other BSD socket implementations."
+                #[cfg(target_os = "linux")]
+                Some(
+                    Errno::CONNRESET
+                    | Errno::NETRESET
+                    | Errno::HOSTUNREACH
+                    | Errno::HOSTDOWN
+                    | Errno::NETDOWN
+                    | Errno::NETUNREACH
+                    | Errno::PROTO
+                    | Errno::NOPROTOOPT
+                    | Errno::NONET
+                    | Errno::OPNOTSUPP,
+                ) => Errno::CONNABORTED.into(),
+
+                _ => error,
+            })
+    }
+
+    pub fn udp_disconnect<Fd: AsFd>(sockfd: Fd) -> rustix::io::Result<()> {
+        match rustix::net::connect_unspec(sockfd) {
+            // BSD platforms return an error even if the UDP socket was disconnected successfully.
+            #[cfg(target_os = "macos")]
+            Err(Errno::INVAL | Errno::AFNOSUPPORT) => Ok(()),
+            r => r,
+        }
+    }
+
+    pub fn get_ip_ttl<Fd: AsFd>(sockfd: Fd) -> rustix::io::Result<u8> {
+        sockopt::get_ip_ttl(sockfd)?
+            .try_into()
+            .map_err(|_| Errno::OPNOTSUPP)
+    }
+
+    pub fn get_ipv6_unicast_hops<Fd: AsFd>(sockfd: Fd) -> rustix::io::Result<u8> {
+        sockopt::get_ipv6_unicast_hops(sockfd)
+    }
+
+    pub fn set_ip_ttl<Fd: AsFd>(sockfd: Fd, value: u8) -> rustix::io::Result<()> {
+        match value {
+            // A well-behaved IP application should never send out new packets with TTL 0.
+            // We validate the value ourselves because OS'es are not consistent in this.
+            // On Linux the validation is even inconsistent between their IPv4 and IPv6 implementation.
+            0 => Err(Errno::INVAL),
+            _ => sockopt::set_ip_ttl(sockfd, value.into()),
+        }
+    }
+
+    pub fn set_ipv6_unicast_hops<Fd: AsFd>(sockfd: Fd, value: u8) -> rustix::io::Result<()> {
+        match value {
+            // A well-behaved IP application should never send out new packets with TTL 0.
+            // We validate the value ourselves because OS'es are not consistent in this.
+            // On Linux the validation is even inconsistent between their IPv4 and IPv6 implementation.
+            0 => Err(Errno::INVAL),
+            _ => sockopt::set_ipv6_unicast_hops(sockfd, Some(value)),
+        }
+    }
+
+    fn normalize_get_buffer_size(value: usize) -> usize {
+        if cfg!(target_os = "linux") {
+            // Linux doubles the value passed to setsockopt to allow space for bookkeeping overhead.
+            // getsockopt returns this internally doubled value.
+            // We'll half the value to at least get it back into the same ballpark that the application requested it in.
+            value / 2
+        } else {
+            value
+        }
+    }
+
+    fn normalize_set_buffer_size(value: usize) -> usize {
+        value.clamp(1, i32::MAX as usize)
+    }
+
+    pub fn get_socket_recv_buffer_size<Fd: AsFd>(sockfd: Fd) -> rustix::io::Result<usize> {
+        let value = sockopt::get_socket_recv_buffer_size(sockfd)?;
+        Ok(normalize_get_buffer_size(value))
+    }
+
+    pub fn get_socket_send_buffer_size<Fd: AsFd>(sockfd: Fd) -> rustix::io::Result<usize> {
+        let value = sockopt::get_socket_send_buffer_size(sockfd)?;
+        Ok(normalize_get_buffer_size(value))
+    }
+
+    pub fn set_socket_recv_buffer_size<Fd: AsFd>(
+        sockfd: Fd,
+        value: usize,
+    ) -> rustix::io::Result<()> {
+        let value = normalize_set_buffer_size(value);
+
+        match sockopt::set_socket_recv_buffer_size(sockfd, value) {
+            // Most platforms (Linux, Windows, Fuchsia, Solaris, Illumos, Haiku, ESP-IDF, ..and more?) treat the value
+            // passed to SO_SNDBUF/SO_RCVBUF as a performance tuning hint and silently clamp the input if it exceeds
+            // their capability.
+            // As far as I can see, only the *BSD family views this option as a hard requirement and fails when the
+            // value is out of range. We normalize this behavior in favor of the more commonly understood
+            // "performance hint" semantics. In other words; even ENOBUFS is "Ok".
+            // A future improvement could be to query the corresponding sysctl on *BSD platforms and clamp the input
+            // `size` ourselves, to completely close the gap with other platforms.
+            Err(Errno::NOBUFS) => Ok(()),
+            r => r,
+        }
+    }
+
+    pub fn set_socket_send_buffer_size<Fd: AsFd>(
+        sockfd: Fd,
+        value: usize,
+    ) -> rustix::io::Result<()> {
+        let value = normalize_set_buffer_size(value);
+
+        match sockopt::set_socket_send_buffer_size(sockfd, value) {
+            Err(Errno::NOBUFS) => Ok(()), // See set_socket_recv_buffer_size
+            r => r,
+        }
+    }
+}
diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs
index 72b48625e..665b2a85a 100644
--- a/crates/wasi/src/preview2/host/tcp.rs
+++ b/crates/wasi/src/preview2/host/tcp.rs
@@ -1,3 +1,4 @@
+use crate::preview2::host::network::util;
 use crate::preview2::tcp::{TcpSocket, TcpState};
 use crate::preview2::{
     bindings::{
@@ -5,7 +6,7 @@ use crate::preview2::{
         sockets::network::{ErrorCode, IpAddressFamily, IpSocketAddress, Network},
         sockets::tcp::{self, ShutdownType},
     },
-    tcp::SocketAddressFamily,
+    network::SocketAddressFamily,
 };
 use crate::preview2::{Pollable, SocketResult, WasiView};
 use cap_net_ext::{Blocking, PoolExt, TcpListenerExt};
@@ -13,7 +14,7 @@ use cap_std::net::TcpListener;
 use io_lifetimes::AsSocketlike;
 use rustix::io::Errno;
 use rustix::net::sockopt;
-use std::net::{IpAddr, Ipv6Addr, SocketAddr};
+use std::net::SocketAddr;
 use tokio::io::Interest;
 use wasmtime::component::Resource;
 
@@ -37,20 +38,21 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T {
             _ => return Err(ErrorCode::InvalidState.into()),
         }
 
-        validate_unicast(&local_address)?;
-        validate_address_family(&socket, &local_address)?;
+        util::validate_unicast(&local_address)?;
+        util::validate_address_family(&local_address, &socket.family)?;
 
-        let binder = network.pool.tcp_binder(local_address)?;
-
-        // Perform the OS bind call.
-        binder
-            .bind_existing_tcp_listener(&*socket.tcp_socket().as_socketlike_view::<TcpListener>())
-            .map_err(|error| match Errno::from_io_error(&error) {
-                Some(Errno::AFNOSUPPORT) => ErrorCode::InvalidArgument, // Just in case our own validations weren't sufficient.
-                #[cfg(windows)]
-                Some(Errno::NOBUFS) => ErrorCode::AddressInUse, // Windows returns WSAENOBUFS when the ephemeral ports have been exhausted.
-                _ => ErrorCode::from(error),
+        {
+            let binder = network.pool.tcp_binder(local_address)?;
+            let listener = &*socket.tcp_socket().as_socketlike_view::<TcpListener>();
+
+            // Perform the OS bind call.
+            util::tcp_bind(listener, &binder).map_err(|error| {
+                match Errno::from_io_error(&error) {
+                    Some(Errno::AFNOSUPPORT) => ErrorCode::InvalidArgument, // Just in case our own validations weren't sufficient.
+                    _ => ErrorCode::from(error),
+                }
             })?;
+        }
 
         let socket = table.get_mut(&this)?;
         socket.tcp_state = TcpState::BindStarted;
@@ -96,18 +98,15 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T {
                 | TcpState::BindStarted => return Err(ErrorCode::ConcurrencyConflict.into()),
             }
 
-            validate_unicast(&remote_address)?;
-            validate_remote_address(&remote_address)?;
-            validate_address_family(&socket, &remote_address)?;
+            util::validate_unicast(&remote_address)?;
+            util::validate_remote_address(&remote_address)?;
+            util::validate_address_family(&remote_address, &socket.family)?;
 
             let connecter = network.pool.tcp_connecter(remote_address)?;
+            let listener = &*socket.tcp_socket().as_socketlike_view::<TcpListener>();
 
             // Do an OS `connect`. Our socket is non-blocking, so it'll either...
-            {
-                let view = &*socket.tcp_socket().as_socketlike_view::<TcpListener>();
-                let r = connecter.connect_existing_tcp_listener(view);
-                r
-            }
+            util::tcp_connect(listener, &connecter)
         };
 
         match r {
@@ -118,7 +117,7 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T {
                 return Ok(());
             }
             // continue in progress,
-            Err(err) if Errno::from_io_error(&err) == Some(INPROGRESS) => {}
+            Err(err) if Errno::from_io_error(&err) == Some(Errno::INPROGRESS) => {}
             // or fail immediately.
             Err(err) => {
                 return Err(match Errno::from_io_error(&err) {
@@ -241,34 +240,10 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T {
 
         // Do the OS accept call.
         let tcp_socket = socket.tcp_socket();
-        let (connection, _addr) = tcp_socket
-            .try_io(Interest::READABLE, || {
-                tcp_socket
-                    .as_socketlike_view::<TcpListener>()
-                    .accept_with(Blocking::No)
-            })
-            .map_err(|error| match Errno::from_io_error(&error) {
-                #[cfg(windows)]
-                Some(Errno::INPROGRESS) => ErrorCode::WouldBlock, // "A blocking Windows Sockets 1.1 call is in progress, or the service provider is still processing a callback function."
-
-                // Normalize Linux' non-standard behavior.
-                // "Linux accept() passes already-pending network errors on the new socket as an error code from accept(). This behavior differs from other BSD socket implementations."
-                #[cfg(target_os = "linux")]
-                Some(
-                    Errno::CONNRESET
-                    | Errno::NETRESET
-                    | Errno::HOSTUNREACH
-                    | Errno::HOSTDOWN
-                    | Errno::NETDOWN
-                    | Errno::NETUNREACH
-                    | Errno::PROTO
-                    | Errno::NOPROTOOPT
-                    | Errno::NONET
-                    | Errno::OPNOTSUPP,
-                ) => ErrorCode::ConnectionAborted,
-
-                _ => ErrorCode::from(error),
-            })?;
+        let (connection, _addr) = tcp_socket.try_io(Interest::READABLE, || {
+            let listener = &*tcp_socket.as_socketlike_view::<TcpListener>();
+            util::tcp_accept(listener, Blocking::No)
+        })?;
 
         #[cfg(target_os = "macos")]
         {
@@ -277,18 +252,17 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T {
             // and only if a specific value was explicitly set on the listener.
 
             if let Some(size) = socket.receive_buffer_size {
-                _ = sockopt::set_socket_recv_buffer_size(&connection, size); // Ignore potential error.
+                _ = util::set_socket_recv_buffer_size(&connection, size); // Ignore potential error.
             }
 
             if let Some(size) = socket.send_buffer_size {
-                _ = sockopt::set_socket_send_buffer_size(&connection, size); // Ignore potential error.
+                _ = util::set_socket_send_buffer_size(&connection, size); // Ignore potential error.
             }
 
             // For some reason, IP_TTL is inherited, but IPV6_UNICAST_HOPS isn't.
             if let (SocketAddressFamily::Ipv6 { .. }, Some(ttl)) = (socket.family, socket.hop_limit)
             {
-                _ = sockopt::set_ipv6_unicast_hops(&connection, Some(ttl));
-                // Ignore potential error.
+                _ = util::set_ipv6_unicast_hops(&connection, ttl); // Ignore potential error.
             }
         }
 
@@ -330,6 +304,9 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T {
 
         match socket.tcp_state {
             TcpState::Connected => {}
+            TcpState::Connecting | TcpState::ConnectReady => {
+                return Err(ErrorCode::ConcurrencyConflict.into())
+            }
             _ => return Err(ErrorCode::InvalidState.into()),
         }
 
@@ -443,12 +420,8 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T {
         let socket = table.get(&this)?;
 
         let ttl = match socket.family {
-            SocketAddressFamily::Ipv4 => sockopt::get_ip_ttl(socket.tcp_socket())?
-                .try_into()
-                .unwrap(),
-            SocketAddressFamily::Ipv6 { .. } => {
-                sockopt::get_ipv6_unicast_hops(socket.tcp_socket())?
-            }
+            SocketAddressFamily::Ipv4 => util::get_ip_ttl(socket.tcp_socket())?,
+            SocketAddressFamily::Ipv6 { .. } => util::get_ipv6_unicast_hops(socket.tcp_socket())?,
         };
 
         Ok(ttl)
@@ -462,17 +435,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 {
-            // A well-behaved IP application should never send out new packets with TTL 0.
-            // We validate the value ourselves because OS'es are not consistent in this.
-            // On Linux the validation is even inconsistent between their IPv4 and IPv6 implementation.
-            return Err(ErrorCode::InvalidArgument.into());
-        }
-
         match socket.family {
-            SocketAddressFamily::Ipv4 => sockopt::set_ip_ttl(socket.tcp_socket(), value.into())?,
+            SocketAddressFamily::Ipv4 => util::set_ip_ttl(socket.tcp_socket(), value)?,
             SocketAddressFamily::Ipv6 { .. } => {
-                sockopt::set_ipv6_unicast_hops(socket.tcp_socket(), Some(value))?
+                util::set_ipv6_unicast_hops(socket.tcp_socket(), value)?
             }
         }
 
@@ -488,8 +454,8 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T {
         let table = self.table();
         let socket = table.get(&this)?;
 
-        let value = sockopt::get_socket_recv_buffer_size(socket.tcp_socket())? as u64;
-        Ok(normalize_getsockopt_buffer_size(value))
+        let value = util::get_socket_recv_buffer_size(socket.tcp_socket())?;
+        Ok(value as u64)
     }
 
     fn set_receive_buffer_size(
@@ -499,20 +465,9 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T {
     ) -> SocketResult<()> {
         let table = self.table_mut();
         let socket = table.get_mut(&this)?;
-        let value = normalize_setsockopt_buffer_size(value);
-
-        match sockopt::set_socket_recv_buffer_size(socket.tcp_socket(), value) {
-            // Most platforms (Linux, Windows, Fuchsia, Solaris, Illumos, Haiku, ESP-IDF, ..and more?) treat the value
-            // passed to SO_SNDBUF/SO_RCVBUF as a performance tuning hint and silently clamp the input if it exceeds
-            // their capability.
-            // As far as I can see, only the *BSD family views this option as a hard requirement and fails when the
-            // value is out of range. We normalize this behavior in favor of the more commonly understood
-            // "performance hint" semantics. In other words; even ENOBUFS is "Ok".
-            // A future improvement could be to query the corresponding sysctl on *BSD platforms and clamp the input
-            // `size` ourselves, to completely close the gap with other platforms.
-            Err(Errno::NOBUFS) => Ok(()),
-            r => r,
-        }?;
+        let value = value.try_into().unwrap_or(usize::MAX);
+
+        util::set_socket_recv_buffer_size(socket.tcp_socket(), value)?;
 
         #[cfg(target_os = "macos")]
         {
@@ -526,8 +481,8 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T {
         let table = self.table();
         let socket = table.get(&this)?;
 
-        let value = sockopt::get_socket_send_buffer_size(socket.tcp_socket())? as u64;
-        Ok(normalize_getsockopt_buffer_size(value))
+        let value = util::get_socket_send_buffer_size(socket.tcp_socket())?;
+        Ok(value as u64)
     }
 
     fn set_send_buffer_size(
@@ -537,12 +492,9 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T {
     ) -> SocketResult<()> {
         let table = self.table_mut();
         let socket = table.get_mut(&this)?;
-        let value = normalize_setsockopt_buffer_size(value);
+        let value = value.try_into().unwrap_or(usize::MAX);
 
-        match sockopt::set_socket_send_buffer_size(socket.tcp_socket(), value) {
-            Err(Errno::NOBUFS) => Ok(()), // See `set_receive_buffer_size`
-            r => r,
-        }?;
+        util::set_socket_send_buffer_size(socket.tcp_socket(), value)?;
 
         #[cfg(target_os = "macos")]
         {
@@ -596,99 +548,3 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T {
         Ok(())
     }
 }
-
-// On POSIX, non-blocking TCP socket `connect` uses `EINPROGRESS`.
-// <https://pubs.opengroup.org/onlinepubs/9699919799/functions/connect.html>
-#[cfg(not(windows))]
-const INPROGRESS: Errno = Errno::INPROGRESS;
-
-// On Windows, non-blocking TCP socket `connect` uses `WSAEWOULDBLOCK`.
-// <https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-connect>
-#[cfg(windows)]
-const INPROGRESS: Errno = Errno::WOULDBLOCK;
-
-fn validate_unicast(addr: &SocketAddr) -> SocketResult<()> {
-    match to_canonical(&addr.ip()) {
-        IpAddr::V4(ipv4) => {
-            if ipv4.is_multicast() || ipv4.is_broadcast() {
-                Err(ErrorCode::InvalidArgument.into())
-            } else {
-                Ok(())
-            }
-        }
-        IpAddr::V6(ipv6) => {
-            if ipv6.is_multicast() {
-                Err(ErrorCode::InvalidArgument.into())
-            } else {
-                Ok(())
-            }
-        }
-    }
-}
-
-fn validate_remote_address(addr: &SocketAddr) -> SocketResult<()> {
-    if to_canonical(&addr.ip()).is_unspecified() {
-        return Err(ErrorCode::InvalidArgument.into());
-    }
-
-    if addr.port() == 0 {
-        return Err(ErrorCode::InvalidArgument.into());
-    }
-
-    Ok(())
-}
-
-fn validate_address_family(socket: &TcpSocket, addr: &SocketAddr) -> SocketResult<()> {
-    match (socket.family, addr.ip()) {
-        (SocketAddressFamily::Ipv4, IpAddr::V4(_)) => Ok(()),
-        (SocketAddressFamily::Ipv6 { v6only }, IpAddr::V6(ipv6)) => {
-            if is_deprecated_ipv4_compatible(&ipv6) {
-                // Reject IPv4-*compatible* IPv6 addresses. They have been deprecated
-                // since 2006, OS handling of them is inconsistent and our own
-                // validations don't take them into account either.
-                // Note that these are not the same as IPv4-*mapped* IPv6 addresses.
-                Err(ErrorCode::InvalidArgument.into())
-            } else if v6only && ipv6.to_ipv4_mapped().is_some() {
-                Err(ErrorCode::InvalidArgument.into())
-            } else {
-                Ok(())
-            }
-        }
-        _ => Err(ErrorCode::InvalidArgument.into()),
-    }
-}
-
-// Can be removed once `IpAddr::to_canonical` becomes stable.
-fn to_canonical(addr: &IpAddr) -> IpAddr {
-    match addr {
-        IpAddr::V4(ipv4) => IpAddr::V4(*ipv4),
-        IpAddr::V6(ipv6) => {
-            if let Some(ipv4) = ipv6.to_ipv4_mapped() {
-                IpAddr::V4(ipv4)
-            } else {
-                IpAddr::V6(*ipv6)
-            }
-        }
-    }
-}
-
-fn is_deprecated_ipv4_compatible(addr: &Ipv6Addr) -> bool {
-    matches!(addr.segments(), [0, 0, 0, 0, 0, 0, _, _])
-        && *addr != Ipv6Addr::UNSPECIFIED
-        && *addr != Ipv6Addr::LOCALHOST
-}
-
-fn normalize_setsockopt_buffer_size(value: u64) -> usize {
-    value.clamp(1, i32::MAX as u64).try_into().unwrap()
-}
-
-fn normalize_getsockopt_buffer_size(value: u64) -> u64 {
-    if cfg!(target_os = "linux") {
-        // Linux doubles the value passed to setsockopt to allow space for bookkeeping overhead.
-        // getsockopt returns this internally doubled value.
-        // We'll half the value to at least get it back into the same ballpark that the application requested it in.
-        value / 2
-    } else {
-        value
-    }
-}
diff --git a/crates/wasi/src/preview2/host/udp.rs b/crates/wasi/src/preview2/host/udp.rs
index e2d1e239f..52a796f33 100644
--- a/crates/wasi/src/preview2/host/udp.rs
+++ b/crates/wasi/src/preview2/host/udp.rs
@@ -1,5 +1,5 @@
-use std::net::SocketAddr;
-
+use crate::preview2::host::network::util;
+use crate::preview2::network::SocketAddressFamily;
 use crate::preview2::{
     bindings::{
         sockets::network::{ErrorCode, IpAddressFamily, IpSocketAddress, Network},
@@ -11,10 +11,11 @@ use crate::preview2::{
 use crate::preview2::{Pollable, SocketError, SocketResult, WasiView};
 use anyhow::anyhow;
 use async_trait::async_trait;
-use cap_net_ext::{AddressFamily, PoolExt};
+use cap_net_ext::PoolExt;
 use io_lifetimes::AsSocketlike;
 use rustix::io::Errno;
 use rustix::net::sockopt;
+use std::net::SocketAddr;
 use tokio::io::Interest;
 use wasmtime::component::Resource;
 
@@ -43,14 +44,22 @@ impl<T: WasiView> udp::HostUdpSocket for T {
             UdpState::Bound | UdpState::Connected => return Err(ErrorCode::InvalidState.into()),
         }
 
-        let binder = network.pool.udp_binder(local_address)?;
+        util::validate_address_family(&local_address, &socket.family)?;
 
-        // Perform the OS bind call.
-        binder.bind_existing_udp_socket(
-            &*socket
+        {
+            let binder = network.pool.udp_binder(local_address)?;
+            let udp_socket = &*socket
                 .udp_socket()
-                .as_socketlike_view::<cap_std::net::UdpSocket>(),
-        )?;
+                .as_socketlike_view::<cap_std::net::UdpSocket>();
+
+            // Perform the OS bind call.
+            util::udp_bind(udp_socket, &binder).map_err(|error| {
+                match Errno::from_io_error(&error) {
+                    Some(Errno::AFNOSUPPORT) => ErrorCode::InvalidArgument, // Just in case our own validations weren't sufficient.
+                    _ => ErrorCode::from(error),
+                }
+            })?;
+        }
 
         let socket = table.get_mut(&this)?;
         socket.udp_state = UdpState::BindStarted;
@@ -105,13 +114,25 @@ impl<T: WasiView> udp::HostUdpSocket for T {
 
         // Step #1: Disconnect
         if let UdpState::Connected = socket.udp_state {
-            disconnect(socket.udp_socket())?;
+            util::udp_disconnect(socket.udp_socket())?;
             socket.udp_state = UdpState::Bound;
         }
 
         // Step #2: (Re)connect
         if let Some(connect_addr) = remote_address {
-            rustix::net::connect(socket.udp_socket(), &connect_addr)?;
+            util::validate_remote_address(&connect_addr)?;
+            util::validate_address_family(&connect_addr, &socket.family)?;
+
+            rustix::net::connect(socket.udp_socket(), &connect_addr).map_err(
+                |error| match error {
+                    Errno::AFNOSUPPORT => ErrorCode::InvalidArgument, // Just in case our own validations weren't sufficient.
+                    Errno::INPROGRESS => {
+                        log::debug!("UDP connect returned EINPROGRESS, which should never happen");
+                        ErrorCode::Unknown
+                    }
+                    _ => ErrorCode::from(error),
+                },
+            )?;
             socket.udp_state = UdpState::Connected;
         }
 
@@ -122,6 +143,7 @@ impl<T: WasiView> udp::HostUdpSocket for T {
         let outgoing_stream = OutgoingDatagramStream {
             inner: socket.inner.clone(),
             remote_address,
+            family: socket.family,
             send_state: SendState::Idle,
         };
 
@@ -134,6 +156,13 @@ impl<T: WasiView> udp::HostUdpSocket for T {
     fn local_address(&mut self, this: Resource<udp::UdpSocket>) -> SocketResult<IpSocketAddress> {
         let table = self.table();
         let socket = table.get(&this)?;
+
+        match socket.udp_state {
+            UdpState::Default => return Err(ErrorCode::InvalidState.into()),
+            UdpState::BindStarted => return Err(ErrorCode::ConcurrencyConflict.into()),
+            _ => {}
+        }
+
         let addr = socket
             .udp_socket()
             .as_socketlike_view::<std::net::UdpSocket>()
@@ -144,6 +173,12 @@ impl<T: WasiView> udp::HostUdpSocket for T {
     fn remote_address(&mut self, this: Resource<udp::UdpSocket>) -> SocketResult<IpSocketAddress> {
         let table = self.table();
         let socket = table.get(&this)?;
+
+        match socket.udp_state {
+            UdpState::Connected => {}
+            _ => return Err(ErrorCode::InvalidState.into()),
+        }
+
         let addr = socket
             .udp_socket()
             .as_socketlike_view::<std::net::UdpSocket>()
@@ -157,39 +192,51 @@ impl<T: WasiView> udp::HostUdpSocket for T {
     ) -> Result<IpAddressFamily, anyhow::Error> {
         let table = self.table();
         let socket = table.get(&this)?;
+
         match socket.family {
-            AddressFamily::Ipv4 => Ok(IpAddressFamily::Ipv4),
-            AddressFamily::Ipv6 => Ok(IpAddressFamily::Ipv6),
+            SocketAddressFamily::Ipv4 => Ok(IpAddressFamily::Ipv4),
+            SocketAddressFamily::Ipv6 { .. } => Ok(IpAddressFamily::Ipv6),
         }
     }
 
     fn ipv6_only(&mut self, this: Resource<udp::UdpSocket>) -> SocketResult<bool> {
         let table = self.table();
         let socket = table.get(&this)?;
-        Ok(sockopt::get_ipv6_v6only(socket.udp_socket())?)
+
+        match socket.family {
+            SocketAddressFamily::Ipv4 => Err(ErrorCode::NotSupported.into()),
+            SocketAddressFamily::Ipv6 { v6only } => Ok(v6only),
+        }
     }
 
     fn set_ipv6_only(&mut self, this: Resource<udp::UdpSocket>, value: bool) -> SocketResult<()> {
-        let table = self.table();
-        let socket = table.get(&this)?;
-        Ok(sockopt::set_ipv6_v6only(socket.udp_socket(), value)?)
+        let table = self.table_mut();
+        let socket = table.get_mut(&this)?;
+
+        match socket.family {
+            SocketAddressFamily::Ipv4 => Err(ErrorCode::NotSupported.into()),
+            SocketAddressFamily::Ipv6 { .. } => match socket.udp_state {
+                UdpState::Default => {
+                    sockopt::set_ipv6_v6only(socket.udp_socket(), value)?;
+                    socket.family = SocketAddressFamily::Ipv6 { v6only: value };
+                    Ok(())
+                }
+                UdpState::BindStarted => Err(ErrorCode::ConcurrencyConflict.into()),
+                _ => Err(ErrorCode::InvalidState.into()),
+            },
+        }
     }
 
     fn unicast_hop_limit(&mut self, this: Resource<udp::UdpSocket>) -> SocketResult<u8> {
         let table = self.table();
         let socket = table.get(&this)?;
 
-        // We don't track whether the socket is IPv4 or IPv6 so try one and
-        // fall back to the other.
-        match sockopt::get_ipv6_unicast_hops(socket.udp_socket()) {
-            Ok(value) => Ok(value),
-            Err(Errno::NOPROTOOPT) => {
-                let value = sockopt::get_ip_ttl(socket.udp_socket())?;
-                let value = value.try_into().unwrap();
-                Ok(value)
-            }
-            Err(err) => Err(err.into()),
-        }
+        let ttl = match socket.family {
+            SocketAddressFamily::Ipv4 => util::get_ip_ttl(socket.udp_socket())?,
+            SocketAddressFamily::Ipv6 { .. } => util::get_ipv6_unicast_hops(socket.udp_socket())?,
+        };
+
+        Ok(ttl)
     }
 
     fn set_unicast_hop_limit(
@@ -200,19 +247,22 @@ impl<T: WasiView> udp::HostUdpSocket for T {
         let table = self.table();
         let socket = table.get(&this)?;
 
-        // We don't track whether the socket is IPv4 or IPv6 so try one and
-        // fall back to the other.
-        match sockopt::set_ipv6_unicast_hops(socket.udp_socket(), Some(value)) {
-            Ok(()) => Ok(()),
-            Err(Errno::NOPROTOOPT) => Ok(sockopt::set_ip_ttl(socket.udp_socket(), value.into())?),
-            Err(err) => Err(err.into()),
+        match socket.family {
+            SocketAddressFamily::Ipv4 => util::set_ip_ttl(socket.udp_socket(), value)?,
+            SocketAddressFamily::Ipv6 { .. } => {
+                util::set_ipv6_unicast_hops(socket.udp_socket(), value)?
+            }
         }
+
+        Ok(())
     }
 
     fn receive_buffer_size(&mut self, this: Resource<udp::UdpSocket>) -> SocketResult<u64> {
         let table = self.table();
         let socket = table.get(&this)?;
-        Ok(sockopt::get_socket_recv_buffer_size(socket.udp_socket())? as u64)
+
+        let value = util::get_socket_recv_buffer_size(socket.udp_socket())?;
+        Ok(value as u64)
     }
 
     fn set_receive_buffer_size(
@@ -222,17 +272,18 @@ impl<T: WasiView> udp::HostUdpSocket for T {
     ) -> SocketResult<()> {
         let table = self.table();
         let socket = table.get(&this)?;
-        let value = value.try_into().map_err(|_| ErrorCode::OutOfMemory)?;
-        Ok(sockopt::set_socket_recv_buffer_size(
-            socket.udp_socket(),
-            value,
-        )?)
+        let value = value.try_into().unwrap_or(usize::MAX);
+
+        util::set_socket_recv_buffer_size(socket.udp_socket(), value)?;
+        Ok(())
     }
 
     fn send_buffer_size(&mut self, this: Resource<udp::UdpSocket>) -> SocketResult<u64> {
         let table = self.table();
         let socket = table.get(&this)?;
-        Ok(sockopt::get_socket_send_buffer_size(socket.udp_socket())? as u64)
+
+        let value = util::get_socket_send_buffer_size(socket.udp_socket())?;
+        Ok(value as u64)
     }
 
     fn set_send_buffer_size(
@@ -242,11 +293,10 @@ impl<T: WasiView> udp::HostUdpSocket for T {
     ) -> SocketResult<()> {
         let table = self.table();
         let socket = table.get(&this)?;
-        let value = value.try_into().map_err(|_| ErrorCode::OutOfMemory)?;
-        Ok(sockopt::set_socket_send_buffer_size(
-            socket.udp_socket(),
-            value,
-        )?)
+        let value = value.try_into().unwrap_or(usize::MAX);
+
+        util::set_socket_send_buffer_size(socket.udp_socket(), value)?;
+        Ok(())
     }
 
     fn subscribe(&mut self, this: Resource<udp::UdpSocket>) -> anyhow::Result<Resource<Pollable>> {
@@ -277,6 +327,7 @@ impl<T: WasiView> udp::HostIncomingDatagramStream for T {
         ) -> SocketResult<Option<udp::IncomingDatagram>> {
             let mut buf = [0; MAX_UDP_DATAGRAM_SIZE];
             let (size, received_addr) = stream.inner.try_recv_from(&mut buf)?;
+            debug_assert!(size <= buf.len());
 
             match stream.remote_address {
                 Some(connected_addr) if connected_addr != received_addr => {
@@ -397,6 +448,9 @@ impl<T: WasiView> udp::HostOutgoingDatagramStream for T {
                 _ => return Err(ErrorCode::InvalidArgument.into()),
             };
 
+            util::validate_remote_address(&addr)?;
+            util::validate_address_family(&addr, &stream.family)?;
+
             // FIXME: check permission to send to `addr`.
             if stream.remote_address == Some(addr) {
                 stream.inner.try_send(&datagram.data)?;
@@ -487,12 +541,3 @@ impl Subscribe for OutgoingDatagramStream {
         }
     }
 }
-
-fn disconnect<Fd: rustix::fd::AsFd>(sockfd: Fd) -> rustix::io::Result<()> {
-    match rustix::net::connect_unspec(sockfd) {
-        // BSD platforms return an error even if the socket was disconnected successfully.
-        #[cfg(target_os = "macos")]
-        Err(rustix::io::Errno::INVAL | rustix::io::Errno::AFNOSUPPORT) => Ok(()),
-        r => r,
-    }
-}
diff --git a/crates/wasi/src/preview2/ip_name_lookup.rs b/crates/wasi/src/preview2/ip_name_lookup.rs
index b832bd501..ab69f8f1b 100644
--- a/crates/wasi/src/preview2/ip_name_lookup.rs
+++ b/crates/wasi/src/preview2/ip_name_lookup.rs
@@ -1,14 +1,18 @@
 use crate::preview2::bindings::sockets::ip_name_lookup::{Host, HostResolveAddressStream};
-use crate::preview2::bindings::sockets::network::{ErrorCode, IpAddress, IpAddressFamily, Network};
+use crate::preview2::bindings::sockets::network::{ErrorCode, IpAddress, Network};
+use crate::preview2::host::network::util;
 use crate::preview2::poll::{subscribe, Pollable, Subscribe};
 use crate::preview2::{spawn_blocking, AbortOnDropJoinHandle, SocketError, WasiView};
 use anyhow::Result;
 use std::mem;
-use std::net::{SocketAddr, ToSocketAddrs};
+use std::net::{Ipv6Addr, ToSocketAddrs};
 use std::pin::Pin;
+use std::str::FromStr;
 use std::vec;
 use wasmtime::component::Resource;
 
+use super::network::{from_ipv4_addr, from_ipv6_addr};
+
 pub enum ResolveAddressStream {
     Waiting(AbortOnDropJoinHandle<Result<Vec<IpAddress>, SocketError>>),
     Done(Result<vec::IntoIter<IpAddress>, SocketError>),
@@ -20,60 +24,16 @@ impl<T: WasiView> Host for T {
         &mut self,
         network: Resource<Network>,
         name: String,
-        family: Option<IpAddressFamily>,
-        include_unavailable: bool,
     ) -> Result<Resource<ResolveAddressStream>, SocketError> {
         let network = self.table().get(&network)?;
 
-        // `Host::parse` serves us two functions:
-        // 1. validate the input is not an IP address,
-        // 2. convert unicode domains to punycode.
-        let name = match url::Host::parse(&name).map_err(|_| ErrorCode::InvalidArgument)? {
-            url::Host::Domain(name) => name,
-            url::Host::Ipv4(_) => return Err(ErrorCode::InvalidArgument.into()),
-            url::Host::Ipv6(_) => return Err(ErrorCode::InvalidArgument.into()),
-        };
+        let host = parse(&name)?;
 
         if !network.allow_ip_name_lookup {
             return Err(ErrorCode::PermanentResolverFailure.into());
         }
 
-        // ignored for now, should probably have a future PR to actually take
-        // this into account. This would require invoking `getaddrinfo` directly
-        // rather than using the standard library to do it for us.
-        let _ = include_unavailable;
-
-        // For now use the standard library to perform actual resolution through
-        // the usage of the `ToSocketAddrs` trait. This blocks the current
-        // thread, so use `spawn_blocking`. Finally note that this is only
-        // resolving names, not ports, so force the port to be 0.
-        let task = spawn_blocking(move || -> Result<Vec<_>, SocketError> {
-            let result = (name.as_str(), 0)
-                .to_socket_addrs()
-                .map_err(|_| ErrorCode::NameUnresolvable)?; // If/when we use `getaddrinfo` directly, map the error properly.
-            Ok(result
-                .filter_map(|addr| {
-                    // In lieu of preventing these addresses from being resolved
-                    // in the first place, filter them out here.
-                    match addr {
-                        SocketAddr::V4(addr) => match family {
-                            None | Some(IpAddressFamily::Ipv4) => {
-                                let [a, b, c, d] = addr.ip().octets();
-                                Some(IpAddress::Ipv4((a, b, c, d)))
-                            }
-                            Some(IpAddressFamily::Ipv6) => None,
-                        },
-                        SocketAddr::V6(addr) => match family {
-                            None | Some(IpAddressFamily::Ipv6) => {
-                                let [a, b, c, d, e, f, g, h] = addr.ip().segments();
-                                Some(IpAddress::Ipv6((a, b, c, d, e, f, g, h)))
-                            }
-                            Some(IpAddressFamily::Ipv4) => None,
-                        },
-                    }
-                })
-                .collect())
-        });
+        let task = spawn_blocking(move || blocking_resolve(&host));
         let resource = self.table_mut().push(ResolveAddressStream::Waiting(task))?;
         Ok(resource)
     }
@@ -126,3 +86,40 @@ impl Subscribe for ResolveAddressStream {
         }
     }
 }
+
+fn parse(name: &str) -> Result<url::Host, SocketError> {
+    // `url::Host::parse` serves us two functions:
+    // 1. validate the input is a valid domain name or IP,
+    // 2. convert unicode domains to punycode.
+    match url::Host::parse(&name) {
+        Ok(host) => Ok(host),
+
+        // `url::Host::parse` doesn't understand bare IPv6 addresses without [brackets]
+        Err(_) => {
+            if let Ok(addr) = Ipv6Addr::from_str(name) {
+                Ok(url::Host::Ipv6(addr))
+            } else {
+                Err(ErrorCode::InvalidArgument.into())
+            }
+        }
+    }
+}
+
+fn blocking_resolve(host: &url::Host) -> Result<Vec<IpAddress>, SocketError> {
+    match host {
+        url::Host::Ipv4(v4addr) => Ok(vec![IpAddress::Ipv4(from_ipv4_addr(*v4addr))]),
+        url::Host::Ipv6(v6addr) => Ok(vec![IpAddress::Ipv6(from_ipv6_addr(*v6addr))]),
+        url::Host::Domain(domain) => {
+            // For now use the standard library to perform actual resolution through
+            // the usage of the `ToSocketAddrs` trait. This is only
+            // resolving names, not ports, so force the port to be 0.
+            let addresses = (domain.as_str(), 0)
+                .to_socket_addrs()
+                .map_err(|_| ErrorCode::NameUnresolvable)? // If/when we use `getaddrinfo` directly, map the error properly.
+                .map(|addr| util::to_canonical(&addr.ip()).into())
+                .collect();
+
+            Ok(addresses)
+        }
+    }
+}
diff --git a/crates/wasi/src/preview2/network.rs b/crates/wasi/src/preview2/network.rs
index 1f49e06f3..65c380bd1 100644
--- a/crates/wasi/src/preview2/network.rs
+++ b/crates/wasi/src/preview2/network.rs
@@ -1,3 +1,4 @@
+use crate::preview2::bindings::sockets::network::{Ipv4Address, Ipv6Address};
 use crate::preview2::bindings::wasi::sockets::network::ErrorCode;
 use crate::preview2::{TableError, TrappableError};
 use cap_std::net::Pool;
@@ -28,3 +29,29 @@ impl From<rustix::io::Errno> for SocketError {
         ErrorCode::from(error).into()
     }
 }
+
+#[derive(Copy, Clone)]
+pub enum SocketAddressFamily {
+    Ipv4,
+    Ipv6 { v6only: bool },
+}
+
+pub(crate) fn to_ipv4_addr(addr: Ipv4Address) -> std::net::Ipv4Addr {
+    let (x0, x1, x2, x3) = addr;
+    std::net::Ipv4Addr::new(x0, x1, x2, x3)
+}
+
+pub(crate) fn from_ipv4_addr(addr: std::net::Ipv4Addr) -> Ipv4Address {
+    let [x0, x1, x2, x3] = addr.octets();
+    (x0, x1, x2, x3)
+}
+
+pub(crate) fn to_ipv6_addr(addr: Ipv6Address) -> std::net::Ipv6Addr {
+    let (x0, x1, x2, x3, x4, x5, x6, x7) = addr;
+    std::net::Ipv6Addr::new(x0, x1, x2, x3, x4, x5, x6, x7)
+}
+
+pub(crate) fn from_ipv6_addr(addr: std::net::Ipv6Addr) -> Ipv6Address {
+    let [x0, x1, x2, x3, x4, x5, x6, x7] = addr.segments();
+    (x0, x1, x2, x3, x4, x5, x6, x7)
+}
diff --git a/crates/wasi/src/preview2/tcp.rs b/crates/wasi/src/preview2/tcp.rs
index dd782c4c6..b6ebffe7e 100644
--- a/crates/wasi/src/preview2/tcp.rs
+++ b/crates/wasi/src/preview2/tcp.rs
@@ -1,3 +1,4 @@
+use super::network::SocketAddressFamily;
 use super::{HostInputStream, HostOutputStream, StreamError};
 use crate::preview2::{
     with_ambient_tokio_runtime, AbortOnDropJoinHandle, InputStream, OutputStream, Subscribe,
@@ -74,12 +75,6 @@ pub struct TcpSocket {
     pub(crate) hop_limit: Option<u8>,
 }
 
-#[derive(Copy, Clone)]
-pub(crate) enum SocketAddressFamily {
-    Ipv4,
-    Ipv6 { v6only: bool },
-}
-
 pub(crate) struct TcpReadStream {
     stream: Arc<tokio::net::TcpStream>,
     closed: bool,
diff --git a/crates/wasi/src/preview2/udp.rs b/crates/wasi/src/preview2/udp.rs
index 504dba0b1..16335d601 100644
--- a/crates/wasi/src/preview2/udp.rs
+++ b/crates/wasi/src/preview2/udp.rs
@@ -7,6 +7,8 @@ use std::io;
 use std::net::SocketAddr;
 use std::sync::Arc;
 
+use super::network::SocketAddressFamily;
+
 /// The state of a UDP socket.
 ///
 /// This represents the various states a socket can be in during the
@@ -39,7 +41,7 @@ pub struct UdpSocket {
     pub(crate) udp_state: UdpState,
 
     /// Socket address family.
-    pub(crate) family: AddressFamily,
+    pub(crate) family: SocketAddressFamily,
 }
 
 #[async_trait]
@@ -52,10 +54,19 @@ impl Subscribe for UdpSocket {
 impl UdpSocket {
     /// Create a new socket in the given family.
     pub fn new(family: AddressFamily) -> io::Result<Self> {
+        let socket = Self::new_tokio_socket(family)?;
+
+        let socket_address_family = match family {
+            AddressFamily::Ipv4 => SocketAddressFamily::Ipv4,
+            AddressFamily::Ipv6 => SocketAddressFamily::Ipv6 {
+                v6only: rustix::net::sockopt::get_ipv6_v6only(&socket)?,
+            },
+        };
+
         Ok(UdpSocket {
-            inner: Arc::new(Self::new_tokio_socket(family)?),
+            inner: Arc::new(socket),
             udp_state: UdpState::Default,
-            family,
+            family: socket_address_family,
         })
     }
 
@@ -89,6 +100,9 @@ pub struct OutgoingDatagramStream {
     /// If this has a value, the stream is "connected".
     pub(crate) remote_address: Option<SocketAddr>,
 
+    /// Socket address family.
+    pub(crate) family: SocketAddressFamily,
+
     pub(crate) send_state: SendState,
 }
 
diff --git a/crates/wasi/wit/deps/sockets/ip-name-lookup.wit b/crates/wasi/wit/deps/sockets/ip-name-lookup.wit
index 5ffe2a217..270e70258 100644
--- a/crates/wasi/wit/deps/sockets/ip-name-lookup.wit
+++ b/crates/wasi/wit/deps/sockets/ip-name-lookup.wit
@@ -1,40 +1,30 @@
 
 interface ip-name-lookup {
     use wasi:io/poll@0.2.0-rc-2023-11-05.{pollable};
-    use network.{network, error-code, ip-address, ip-address-family};
+    use network.{network, error-code, ip-address};
 
 
     /// Resolve an internet host name to a list of IP addresses.
     ///
-    /// See the wasi-socket proposal README.md for a comparison with getaddrinfo.
-    ///
-    /// # Parameters
-    /// - `name`: The name to look up. IP addresses are not allowed. Unicode domain names are automatically converted
-    ///     to ASCII using IDNA encoding.
-    /// - `address-family`: If provided, limit the results to addresses of this specific address family.
-    /// - `include-unavailable`: When set to true, this function will also return addresses of which the runtime
-    ///   thinks (or knows) can't be connected to at the moment. For example, this will return IPv6 addresses on
-    ///   systems without an active IPv6 interface. Notes:
-    ///     - Even when no public IPv6 interfaces are present or active, names like "localhost" can still resolve to an IPv6 address.
-    ///     - Whatever is "available" or "unavailable" is volatile and can change everytime a network cable is unplugged.
+    /// Unicode domain names are automatically converted to ASCII using IDNA encoding.
+    /// If the input is an IP address string, the address is parsed and returned
+    /// as-is without making any external requests.
     ///
-    /// This function never blocks. It either immediately fails or immediately returns successfully with a `resolve-address-stream`
-    /// that can be used to (asynchronously) fetch the results.
+    /// See the wasi-socket proposal README.md for a comparison with getaddrinfo.
     ///
-    /// At the moment, the stream never completes successfully with 0 items. Ie. the first call
-    /// to `resolve-next-address` never returns `ok(none)`. This may change in the future.
+    /// This function never blocks. It either immediately fails or immediately
+    /// returns successfully with a `resolve-address-stream` that can be used
+    /// to (asynchronously) fetch the results.
     ///
     /// # Typical errors
-    /// - `invalid-argument`:     `name` is a syntactically invalid domain name.
-    /// - `invalid-argument`:     `name` is an IP address.
-    /// - `not-supported`:        The specified `address-family` is not supported. (EAI_FAMILY)
+    /// - `invalid-argument`: `name` is a syntactically invalid domain name or IP address.
     ///
     /// # References:
     /// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/getaddrinfo.html>
     /// - <https://man7.org/linux/man-pages/man3/getaddrinfo.3.html>
     /// - <https://learn.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfo>
     /// - <https://man.freebsd.org/cgi/man.cgi?query=getaddrinfo&sektion=3>
-    resolve-addresses: func(network: borrow<network>, name: string, address-family: option<ip-address-family>, include-unavailable: bool) -> result<resolve-address-stream, error-code>;
+    resolve-addresses: func(network: borrow<network>, name: string) -> result<resolve-address-stream, error-code>;
 
     resource resolve-address-stream {
         /// Returns the next address from the resolver.
diff --git a/crates/wasi/wit/deps/sockets/tcp.wit b/crates/wasi/wit/deps/sockets/tcp.wit
index b172b2077..b9ef0625a 100644
--- a/crates/wasi/wit/deps/sockets/tcp.wit
+++ b/crates/wasi/wit/deps/sockets/tcp.wit
@@ -81,7 +81,7 @@ interface tcp {
         /// - `connection-refused`:        The connection was forcefully rejected. (ECONNREFUSED)
         /// - `connection-reset`:          The connection was reset. (ECONNRESET)
         /// - `connection-aborted`:        The connection was aborted. (ECONNABORTED)
-        /// - `remote-unreachable`:        The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN)
+        /// - `remote-unreachable`:        The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET)
         /// - `address-in-use`:            Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD)
         /// - `not-in-progress`:           A `connect` operation is not in progress.
         /// - `would-block`:               Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN)
diff --git a/crates/wasi/wit/deps/sockets/udp.wit b/crates/wasi/wit/deps/sockets/udp.wit
index 232c2b41c..1ee1843a9 100644
--- a/crates/wasi/wit/deps/sockets/udp.wit
+++ b/crates/wasi/wit/deps/sockets/udp.wit
@@ -97,7 +97,8 @@ interface udp {
         /// - `invalid-argument`:          The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL)
         /// - `invalid-state`:             The socket is not bound.
         /// - `address-in-use`:            Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD)
-        /// - `remote-unreachable`:        The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN)
+        /// - `remote-unreachable`:        The remote address is not reachable. (ECONNRESET, ENETRESET, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET)
+        /// - `connection-refused`:        The connection was refused. (ECONNREFUSED)
         ///
         /// # References
         /// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/connect.html>
@@ -190,7 +191,8 @@ interface udp {
         /// This function never returns `error(would-block)`.
         ///
         /// # Typical errors
-        /// - `remote-unreachable`: The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN)
+        /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET)
+        /// - `connection-refused`: The connection was refused. (ECONNREFUSED)
         ///
         /// # References
         /// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/recvfrom.html>
@@ -246,7 +248,8 @@ interface udp {
         /// - `invalid-argument`:        The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL)
         /// - `invalid-argument`:        The socket is in "connected" mode and `remote-address` is `some` value that does not match the address passed to `stream`. (EISCONN)
         /// - `invalid-argument`:        The socket is not "connected" and no value for `remote-address` was provided. (EDESTADDRREQ)
-        /// - `remote-unreachable`:      The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN)
+        /// - `remote-unreachable`:      The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET)
+        /// - `connection-refused`:      The connection was refused. (ECONNREFUSED)
         /// - `datagram-too-large`:      The datagram is too large. (EMSGSIZE)
         ///
         /// # References
diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock
index eec621f33..6f72ca4e7 100644
--- a/supply-chain/imports.lock
+++ b/supply-chain/imports.lock
@@ -852,6 +852,13 @@ user-id = 1
 user-login = "alexcrichton"
 user-name = "Alex Crichton"
 
+[[publisher.wasm-encoder]]
+version = "0.36.2"
+when = "2023-11-06"
+user-id = 1
+user-login = "alexcrichton"
+user-name = "Alex Crichton"
+
 [[publisher.wasm-metadata]]
 version = "0.10.9"
 when = "2023-10-14"
@@ -866,6 +873,13 @@ user-id = 1
 user-login = "alexcrichton"
 user-name = "Alex Crichton"
 
+[[publisher.wasm-metadata]]
+version = "0.10.11"
+when = "2023-11-06"
+user-id = 1
+user-login = "alexcrichton"
+user-name = "Alex Crichton"
+
 [[publisher.wasm-mutate]]
 version = "0.2.38"
 when = "2023-10-14"
@@ -880,6 +894,13 @@ user-id = 1
 user-login = "alexcrichton"
 user-name = "Alex Crichton"
 
+[[publisher.wasm-mutate]]
+version = "0.2.40"
+when = "2023-11-06"
+user-id = 1
+user-login = "alexcrichton"
+user-name = "Alex Crichton"
+
 [[publisher.wasm-smith]]
 version = "0.12.21"
 when = "2023-10-14"
@@ -894,6 +915,13 @@ user-id = 1
 user-login = "alexcrichton"
 user-name = "Alex Crichton"
 
+[[publisher.wasm-smith]]
+version = "0.12.23"
+when = "2023-11-06"
+user-id = 1
+user-login = "alexcrichton"
+user-name = "Alex Crichton"
+
 [[publisher.wasmparser]]
 version = "0.115.0"
 when = "2023-10-14"
@@ -908,6 +936,13 @@ user-id = 1
 user-login = "alexcrichton"
 user-name = "Alex Crichton"
 
+[[publisher.wasmparser]]
+version = "0.116.1"
+when = "2023-11-06"
+user-id = 1
+user-login = "alexcrichton"
+user-name = "Alex Crichton"
+
 [[publisher.wasmprinter]]
 version = "0.2.70"
 when = "2023-10-14"
@@ -922,6 +957,13 @@ user-id = 1
 user-login = "alexcrichton"
 user-name = "Alex Crichton"
 
+[[publisher.wasmprinter]]
+version = "0.2.72"
+when = "2023-11-06"
+user-id = 1
+user-login = "alexcrichton"
+user-name = "Alex Crichton"
+
 [[publisher.wasmtime]]
 version = "14.0.2"
 when = "2023-10-26"
@@ -1086,6 +1128,13 @@ user-id = 1
 user-login = "alexcrichton"
 user-name = "Alex Crichton"
 
+[[publisher.wast]]
+version = "67.0.1"
+when = "2023-11-06"
+user-id = 1
+user-login = "alexcrichton"
+user-name = "Alex Crichton"
+
 [[publisher.wat]]
 version = "1.0.77"
 when = "2023-10-14"
@@ -1100,6 +1149,13 @@ user-id = 1
 user-login = "alexcrichton"
 user-name = "Alex Crichton"
 
+[[publisher.wat]]
+version = "1.0.79"
+when = "2023-11-06"
+user-id = 1
+user-login = "alexcrichton"
+user-name = "Alex Crichton"
+
 [[publisher.wiggle]]
 version = "14.0.2"
 when = "2023-10-26"
@@ -1278,6 +1334,13 @@ user-id = 1
 user-login = "alexcrichton"
 user-name = "Alex Crichton"
 
+[[publisher.wit-component]]
+version = "0.18.0"
+when = "2023-11-06"
+user-id = 1
+user-login = "alexcrichton"
+user-name = "Alex Crichton"
+
 [[publisher.wit-parser]]
 version = "0.12.1"
 when = "2023-10-18"
@@ -1292,6 +1355,13 @@ user-id = 1
 user-login = "alexcrichton"
 user-name = "Alex Crichton"
 
+[[publisher.wit-parser]]
+version = "0.13.0"
+when = "2023-11-06"
+user-id = 1
+user-login = "alexcrichton"
+user-name = "Alex Crichton"
+
 [[audits.embark-studios.wildcard-audits.spdx]]
 who = "Jake Shadle <opensource@embark-studios.com>"
 criteria = "safe-to-deploy"


Test patch:
diff --git a/crates/test-programs/src/bin/preview2_ip_name_lookup.rs b/crates/test-programs/src/bin/preview2_ip_name_lookup.rs
index 29c851a44..dbad2e807 100644
--- a/crates/test-programs/src/bin/preview2_ip_name_lookup.rs
+++ b/crates/test-programs/src/bin/preview2_ip_name_lookup.rs
@@ -1,36 +1,70 @@
-use test_programs::wasi::clocks::*;
-use test_programs::wasi::io::*;
+use test_programs::wasi::sockets::network::{ErrorCode, IpAddress};
 use test_programs::wasi::sockets::*;
 
 fn main() {
-    let network = instance_network::instance_network();
+    // Valid domains
+    resolve("localhost").unwrap();
+    resolve("example.com").unwrap();
+    resolve("münchen.de").unwrap();
+
+    // Valid IP addresses
+    assert_eq!(resolve_one("0.0.0.0").unwrap(), IpAddress::IPV4_UNSPECIFIED);
+    assert_eq!(resolve_one("127.0.0.1").unwrap(), IpAddress::IPV4_LOOPBACK);
+    assert_eq!(
+        resolve_one("192.0.2.0").unwrap(),
+        IpAddress::Ipv4((192, 0, 2, 0))
+    );
+    assert_eq!(resolve_one("::").unwrap(), IpAddress::IPV6_UNSPECIFIED);
+    assert_eq!(resolve_one("::1").unwrap(), IpAddress::IPV6_LOOPBACK);
+    assert_eq!(resolve_one("[::]").unwrap(), IpAddress::IPV6_UNSPECIFIED);
+    assert_eq!(
+        resolve_one("2001:0db8:0:0:0:0:0:0").unwrap(),
+        IpAddress::Ipv6((0x2001, 0x0db8, 0, 0, 0, 0, 0, 0))
+    );
+    assert_eq!(
+        resolve_one("dead:beef::").unwrap(),
+        IpAddress::Ipv6((0xdead, 0xbeef, 0, 0, 0, 0, 0, 0))
+    );
+    assert_eq!(
+        resolve_one("dead:beef::0").unwrap(),
+        IpAddress::Ipv6((0xdead, 0xbeef, 0, 0, 0, 0, 0, 0))
+    );
+    assert_eq!(
+        resolve_one("DEAD:BEEF::0").unwrap(),
+        IpAddress::Ipv6((0xdead, 0xbeef, 0, 0, 0, 0, 0, 0))
+    );
 
-    let addresses =
-        ip_name_lookup::resolve_addresses(&network, "example.com", None, false).unwrap();
-    let pollable = addresses.subscribe();
-    pollable.block();
-    assert!(addresses.resolve_next_address().is_ok());
+    // Invalid inputs
+    assert_eq!(resolve("").unwrap_err(), ErrorCode::InvalidArgument);
+    assert_eq!(resolve(" ").unwrap_err(), ErrorCode::InvalidArgument);
+    assert_eq!(resolve("a.b<&>").unwrap_err(), ErrorCode::InvalidArgument);
+    assert_eq!(
+        resolve("127.0.0.1:80").unwrap_err(),
+        ErrorCode::InvalidArgument
+    );
+    assert_eq!(resolve("[::]:80").unwrap_err(), ErrorCode::InvalidArgument);
+    assert_eq!(
+        resolve("http://example.com/").unwrap_err(),
+        ErrorCode::InvalidArgument
+    );
+}
 
-    let result = ip_name_lookup::resolve_addresses(&network, "a.b<&>", None, false);
-    assert!(matches!(result, Err(network::ErrorCode::InvalidArgument)));
+fn resolve(name: &str) -> Result<Vec<IpAddress>, ErrorCode> {
+    let network = instance_network::instance_network();
 
-    // Try resolving a valid address and ensure that it eventually terminates.
-    // To help prevent this test from being flaky this additionally times out
-    // the resolution and allows errors.
-    let addresses = ip_name_lookup::resolve_addresses(&network, "github.com", None, false).unwrap();
-    let lookup = addresses.subscribe();
-    let timeout = monotonic_clock::subscribe_duration(1_000_000_000);
-    let ready = poll::poll(&[&lookup, &timeout]);
-    assert!(ready.len() > 0);
-    match ready[0] {
-        0 => loop {
-            match addresses.resolve_next_address() {
-                Ok(Some(_)) => {}
-                Ok(None) => break,
-                Err(_) => break,
-            }
-        },
-        1 => {}
-        _ => unreachable!(),
+    match network.blocking_resolve_addresses(name) {
+        // The following error codes signal that the input passed validation
+        // and a lookup was actually attempted, but failed. Ignore these to
+        // make the CI tests less flaky:
+        Err(
+            ErrorCode::NameUnresolvable
+            | ErrorCode::TemporaryResolverFailure
+            | ErrorCode::PermanentResolverFailure,
+        ) => Ok(vec![]),
+        r => r,
     }
 }
+
+fn resolve_one(name: &str) -> Result<IpAddress, ErrorCode> {
+    Ok(resolve(name)?.first().unwrap().to_owned())
+}
diff --git a/crates/test-programs/src/bin/preview2_tcp_connect.rs b/crates/test-programs/src/bin/preview2_tcp_connect.rs
index 58d5a84c4..f304e46a5 100644
--- a/crates/test-programs/src/bin/preview2_tcp_connect.rs
+++ b/crates/test-programs/src/bin/preview2_tcp_connect.rs
@@ -27,7 +27,7 @@ fn test_tcp_connect_port_0(net: &Network, family: IpAddressFamily) {
     ));
 }
 
-/// Bind should validate the address family.
+/// Connect should validate the address family.
 fn test_tcp_connect_wrong_family(net: &Network, family: IpAddressFamily) {
     let wrong_ip = match family {
         IpAddressFamily::Ipv4 => IpAddress::IPV6_LOOPBACK,
diff --git a/crates/test-programs/src/bin/preview2_tcp_sample_application.rs b/crates/test-programs/src/bin/preview2_tcp_sample_application.rs
index a2b648106..6aaa313c4 100644
--- a/crates/test-programs/src/bin/preview2_tcp_sample_application.rs
+++ b/crates/test-programs/src/bin/preview2_tcp_sample_application.rs
@@ -3,7 +3,7 @@ use test_programs::wasi::sockets::network::{
 };
 use test_programs::wasi::sockets::tcp::TcpSocket;
 
-fn test_sample_application(family: IpAddressFamily, bind_address: IpSocketAddress) {
+fn test_tcp_sample_application(family: IpAddressFamily, bind_address: IpSocketAddress) {
     let first_message = b"Hello, world!";
     let second_message = b"Greetings, planet!";
 
@@ -54,14 +54,14 @@ fn test_sample_application(family: IpAddressFamily, bind_address: IpSocketAddres
 }
 
 fn main() {
-    test_sample_application(
+    test_tcp_sample_application(
         IpAddressFamily::Ipv4,
         IpSocketAddress::Ipv4(Ipv4SocketAddress {
             port: 0,                 // use any free port
             address: (127, 0, 0, 1), // localhost
         }),
     );
-    test_sample_application(
+    test_tcp_sample_application(
         IpAddressFamily::Ipv6,
         IpSocketAddress::Ipv6(Ipv6SocketAddress {
             port: 0,                           // use any free port
diff --git a/crates/test-programs/src/bin/preview2_udp_bind.rs b/crates/test-programs/src/bin/preview2_udp_bind.rs
new file mode 100644
index 000000000..5fb5e5421
--- /dev/null
+++ b/crates/test-programs/src/bin/preview2_udp_bind.rs
@@ -0,0 +1,133 @@
+use test_programs::wasi::sockets::network::{
+    ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, Network,
+};
+use test_programs::wasi::sockets::udp::UdpSocket;
+
+/// Bind a socket and let the system determine a port.
+fn test_udp_bind_ephemeral_port(net: &Network, ip: IpAddress) {
+    let bind_addr = IpSocketAddress::new(ip, 0);
+
+    let sock = UdpSocket::new(ip.family()).unwrap();
+    sock.blocking_bind(net, bind_addr).unwrap();
+
+    let bound_addr = sock.local_address().unwrap();
+
+    assert_eq!(bind_addr.ip(), bound_addr.ip());
+    assert_ne!(bind_addr.port(), bound_addr.port());
+}
+
+/// Bind a socket on a specified port.
+fn test_udp_bind_specific_port(net: &Network, ip: IpAddress) {
+    const PORT: u16 = 54321;
+
+    let bind_addr = IpSocketAddress::new(ip, PORT);
+
+    let sock = UdpSocket::new(ip.family()).unwrap();
+    match sock.blocking_bind(net, bind_addr) {
+        Ok(()) => {}
+
+        // Concurrent invocations of this test can yield `AddressInUse` and that
+        // same error can show up on Windows as `AccessDenied`.
+        Err(ErrorCode::AddressInUse | ErrorCode::AccessDenied) => {}
+        r => r.unwrap(),
+    }
+
+    let bound_addr = sock.local_address().unwrap();
+
+    assert_eq!(bind_addr.ip(), bound_addr.ip());
+    assert_eq!(bind_addr.port(), bound_addr.port());
+}
+
+/// Two sockets may not be actively bound to the same address at the same time.
+fn test_udp_bind_addrinuse(net: &Network, ip: IpAddress) {
+    let bind_addr = IpSocketAddress::new(ip, 0);
+
+    let sock1 = UdpSocket::new(ip.family()).unwrap();
+    sock1.blocking_bind(net, bind_addr).unwrap();
+
+    let bound_addr = sock1.local_address().unwrap();
+
+    let sock2 = UdpSocket::new(ip.family()).unwrap();
+    assert!(matches!(
+        sock2.blocking_bind(net, bound_addr),
+        Err(ErrorCode::AddressInUse)
+    ));
+}
+
+// Try binding to an address that is not configured on the system.
+fn test_udp_bind_addrnotavail(net: &Network, ip: IpAddress) {
+    let bind_addr = IpSocketAddress::new(ip, 0);
+
+    let sock = UdpSocket::new(ip.family()).unwrap();
+
+    assert!(matches!(
+        sock.blocking_bind(net, bind_addr),
+        Err(ErrorCode::AddressNotBindable)
+    ));
+}
+
+/// Bind should validate the address family.
+fn test_udp_bind_wrong_family(net: &Network, family: IpAddressFamily) {
+    let wrong_ip = match family {
+        IpAddressFamily::Ipv4 => IpAddress::IPV6_LOOPBACK,
+        IpAddressFamily::Ipv6 => IpAddress::IPV4_LOOPBACK,
+    };
+
+    let sock = UdpSocket::new(family).unwrap();
+    let result = sock.blocking_bind(net, IpSocketAddress::new(wrong_ip, 0));
+
+    assert!(matches!(result, Err(ErrorCode::InvalidArgument)));
+}
+
+fn test_udp_bind_dual_stack(net: &Network) {
+    let sock = UdpSocket::new(IpAddressFamily::Ipv6).unwrap();
+    let addr = IpSocketAddress::new(IpAddress::IPV4_MAPPED_LOOPBACK, 0);
+
+    // Even on platforms that don't support dualstack sockets,
+    // setting ipv6_only to true (disabling dualstack mode) should work.
+    sock.set_ipv6_only(true).unwrap();
+
+    // Binding an IPv4-mapped-IPv6 address on a ipv6-only socket should fail:
+    assert!(matches!(
+        sock.blocking_bind(net, addr),
+        Err(ErrorCode::InvalidArgument)
+    ));
+
+    sock.set_ipv6_only(false).unwrap();
+
+    sock.blocking_bind(net, addr).unwrap();
+
+    let bound_addr = sock.local_address().unwrap();
+
+    assert_eq!(bound_addr.family(), IpAddressFamily::Ipv6);
+}
+
+fn main() {
+    const RESERVED_IPV4_ADDRESS: IpAddress = IpAddress::Ipv4((192, 0, 2, 0)); // Reserved for documentation and examples.
+    const RESERVED_IPV6_ADDRESS: IpAddress = IpAddress::Ipv6((0x2001, 0x0db8, 0, 0, 0, 0, 0, 0)); // Reserved for documentation and examples.
+
+    let net = Network::default();
+
+    test_udp_bind_ephemeral_port(&net, IpAddress::IPV4_LOOPBACK);
+    test_udp_bind_ephemeral_port(&net, IpAddress::IPV6_LOOPBACK);
+    test_udp_bind_ephemeral_port(&net, IpAddress::IPV4_UNSPECIFIED);
+    test_udp_bind_ephemeral_port(&net, IpAddress::IPV6_UNSPECIFIED);
+
+    test_udp_bind_specific_port(&net, IpAddress::IPV4_LOOPBACK);
+    test_udp_bind_specific_port(&net, IpAddress::IPV6_LOOPBACK);
+    test_udp_bind_specific_port(&net, IpAddress::IPV4_UNSPECIFIED);
+    test_udp_bind_specific_port(&net, IpAddress::IPV6_UNSPECIFIED);
+
+    test_udp_bind_addrinuse(&net, IpAddress::IPV4_LOOPBACK);
+    test_udp_bind_addrinuse(&net, IpAddress::IPV6_LOOPBACK);
+    test_udp_bind_addrinuse(&net, IpAddress::IPV4_UNSPECIFIED);
+    test_udp_bind_addrinuse(&net, IpAddress::IPV6_UNSPECIFIED);
+
+    test_udp_bind_addrnotavail(&net, RESERVED_IPV4_ADDRESS);
+    test_udp_bind_addrnotavail(&net, RESERVED_IPV6_ADDRESS);
+
+    test_udp_bind_wrong_family(&net, IpAddressFamily::Ipv4);
+    test_udp_bind_wrong_family(&net, IpAddressFamily::Ipv6);
+
+    test_udp_bind_dual_stack(&net);
+}
diff --git a/crates/test-programs/src/bin/preview2_udp_connect.rs b/crates/test-programs/src/bin/preview2_udp_connect.rs
index 5100525e8..7e21c7e79 100644
--- a/crates/test-programs/src/bin/preview2_udp_connect.rs
+++ b/crates/test-programs/src/bin/preview2_udp_connect.rs
@@ -3,6 +3,8 @@ use test_programs::wasi::sockets::network::{
 };
 use test_programs::wasi::sockets::udp::UdpSocket;
 
+const SOME_PORT: u16 = 47; // If the tests pass, this will never actually be connected to.
+
 fn test_udp_connect_disconnect_reconnect(net: &Network, family: IpAddressFamily) {
     let unspecified_addr = IpSocketAddress::new(IpAddress::new_unspecified(family), 0);
     let remote1 = IpSocketAddress::new(IpAddress::new_loopback(family), 4321);
@@ -33,9 +35,107 @@ fn test_udp_connect_disconnect_reconnect(net: &Network, family: IpAddressFamily)
     assert_eq!(client.remote_address(), Ok(remote1));
 }
 
+/// `0.0.0.0` / `::` is not a valid remote address in WASI.
+fn test_udp_connect_unspec(net: &Network, family: IpAddressFamily) {
+    let addr = IpSocketAddress::new(IpAddress::new_unspecified(family), SOME_PORT);
+    let sock = UdpSocket::new(family).unwrap();
+    sock.blocking_bind_unspecified(&net).unwrap();
+
+    assert!(matches!(
+        sock.stream(Some(addr)),
+        Err(ErrorCode::InvalidArgument)
+    ));
+}
+
+/// 0 is not a valid remote port.
+fn test_udp_connect_port_0(net: &Network, family: IpAddressFamily) {
+    let addr = IpSocketAddress::new(IpAddress::new_loopback(family), 0);
+    let sock = UdpSocket::new(family).unwrap();
+    sock.blocking_bind_unspecified(&net).unwrap();
+
+    assert!(matches!(
+        sock.stream(Some(addr)),
+        Err(ErrorCode::InvalidArgument)
+    ));
+}
+
+/// Connect should validate the address family.
+fn test_udp_connect_wrong_family(net: &Network, family: IpAddressFamily) {
+    let wrong_ip = match family {
+        IpAddressFamily::Ipv4 => IpAddress::IPV6_LOOPBACK,
+        IpAddressFamily::Ipv6 => IpAddress::IPV4_LOOPBACK,
+    };
+    let remote_addr = IpSocketAddress::new(wrong_ip, SOME_PORT);
+
+    let sock = UdpSocket::new(family).unwrap();
+    sock.blocking_bind_unspecified(&net).unwrap();
+
+    assert!(matches!(
+        sock.stream(Some(remote_addr)),
+        Err(ErrorCode::InvalidArgument)
+    ));
+}
+
+fn test_udp_connect_dual_stack(net: &Network) {
+    // Set-up:
+    let v4_server = UdpSocket::new(IpAddressFamily::Ipv4).unwrap();
+    v4_server
+        .blocking_bind(&net, IpSocketAddress::new(IpAddress::IPV4_LOOPBACK, 0))
+        .unwrap();
+
+    let v4_server_addr = v4_server.local_address().unwrap();
+    let v6_server_addr =
+        IpSocketAddress::new(IpAddress::IPV4_MAPPED_LOOPBACK, v4_server_addr.port());
+
+    // Tests:
+    {
+        let v6_client = UdpSocket::new(IpAddressFamily::Ipv6).unwrap();
+
+        // Even on platforms that don't support dualstack sockets,
+        // setting ipv6_only to true (disabling dualstack mode) should work.
+        v6_client.set_ipv6_only(true).unwrap();
+
+        v6_client.blocking_bind_unspecified(&net).unwrap();
+
+        // Connecting to an IPv4-mapped-IPv6 address on an ipv6-only socket should fail:
+        assert!(matches!(
+            v6_client.stream(Some(v6_server_addr)),
+            Err(ErrorCode::InvalidArgument)
+        ));
+    }
+
+    {
+        let v6_client = UdpSocket::new(IpAddressFamily::Ipv6).unwrap();
+
+        v6_client.set_ipv6_only(false).unwrap();
+        v6_client.blocking_bind_unspecified(&net).unwrap();
+        v6_client.stream(Some(v6_server_addr)).unwrap();
+
+        assert_eq!(
+            v6_client.local_address().unwrap().family(),
+            IpAddressFamily::Ipv6
+        );
+        assert_eq!(
+            v6_client.remote_address().unwrap().family(),
+            IpAddressFamily::Ipv6
+        );
+    }
+}
+
 fn main() {
     let net = Network::default();
 
     test_udp_connect_disconnect_reconnect(&net, IpAddressFamily::Ipv4);
     test_udp_connect_disconnect_reconnect(&net, IpAddressFamily::Ipv6);
+
+    test_udp_connect_unspec(&net, IpAddressFamily::Ipv4);
+    test_udp_connect_unspec(&net, IpAddressFamily::Ipv6);
+
+    test_udp_connect_port_0(&net, IpAddressFamily::Ipv4);
+    test_udp_connect_port_0(&net, IpAddressFamily::Ipv6);
+
+    test_udp_connect_wrong_family(&net, IpAddressFamily::Ipv4);
+    test_udp_connect_wrong_family(&net, IpAddressFamily::Ipv6);
+
+    test_udp_connect_dual_stack(&net);
 }
diff --git a/crates/test-programs/src/bin/preview2_udp_sample_application.rs b/crates/test-programs/src/bin/preview2_udp_sample_application.rs
index 41da701e6..171258e5e 100644
--- a/crates/test-programs/src/bin/preview2_udp_sample_application.rs
+++ b/crates/test-programs/src/bin/preview2_udp_sample_application.rs
@@ -3,7 +3,7 @@ use test_programs::wasi::sockets::network::{
 };
 use test_programs::wasi::sockets::udp::{OutgoingDatagram, UdpSocket};
 
-fn test_sample_application(family: IpAddressFamily, bind_address: IpSocketAddress) {
+fn test_udp_sample_application(family: IpAddressFamily, bind_address: IpSocketAddress) {
     let unspecified_addr = IpSocketAddress::new(IpAddress::new_unspecified(family), 0);
 
     let first_message = &[];
@@ -72,15 +72,61 @@ fn test_sample_application(family: IpAddressFamily, bind_address: IpSocketAddres
     }
 }
 
+fn test_udp_dual_stack_conversation() {
+    let net = Network::default();
+
+    let v4_server = UdpSocket::new(IpAddressFamily::Ipv4).unwrap();
+    v4_server
+        .blocking_bind(&net, IpSocketAddress::new(IpAddress::IPV4_LOOPBACK, 0))
+        .unwrap();
+    let (server_incoming, server_outgoing) = v4_server.stream(None).unwrap();
+
+    let v4_server_addr = v4_server.local_address().unwrap();
+    let v6_server_addr =
+        IpSocketAddress::new(IpAddress::IPV4_MAPPED_LOOPBACK, v4_server_addr.port());
+
+    let v6_client = UdpSocket::new(IpAddressFamily::Ipv6).unwrap();
+
+    v6_client.set_ipv6_only(false).unwrap();
+    v6_client.blocking_bind_unspecified(&net).unwrap();
+    let (client_incoming, client_outgoing) = v6_client.stream(None).unwrap();
+
+    // Send from v6 client to v4 server:
+    client_outgoing
+        .blocking_send(&[OutgoingDatagram {
+            data: "Hi!".into(),
+            remote_address: Some(v6_server_addr),
+        }])
+        .unwrap();
+
+    // Receive from v6 client on v4 server:
+    let results = server_incoming.blocking_receive(1..1).unwrap();
+    let msg = results.first().unwrap();
+    assert_eq!(msg.remote_address.family(), IpAddressFamily::Ipv4);
+
+    // Send from v4 server to v6 client:
+    server_outgoing
+        .blocking_send(&[OutgoingDatagram {
+            data: msg.data.clone(),
+            remote_address: Some(msg.remote_address),
+        }])
+        .unwrap();
+
+    // Receive from v4 server on v6 client:
+    let results = client_incoming.blocking_receive(1..1).unwrap();
+    let msg = results.first().unwrap();
+    assert_eq!(msg.remote_address.family(), IpAddressFamily::Ipv6);
+}
+
 fn main() {
-    test_sample_application(
+    test_udp_sample_application(
         IpAddressFamily::Ipv4,
         IpSocketAddress::Ipv4(Ipv4SocketAddress {
             port: 0,                 // use any free port
             address: (127, 0, 0, 1), // localhost
         }),
     );
-    test_sample_application(
+    test_udp_sample_application(
         IpAddressFamily::Ipv6,
         IpSocketAddress::Ipv6(Ipv6SocketAddress {
             port: 0,                           // use any free port
@@ -89,4 +135,6 @@ fn main() {
             scope_id: 0,
         }),
     );
+
+    test_udp_dual_stack_conversation();
 }
diff --git a/crates/test-programs/src/bin/preview2_udp_sockopts.rs b/crates/test-programs/src/bin/preview2_udp_sockopts.rs
new file mode 100644
index 000000000..b9cebbcb3
--- /dev/null
+++ b/crates/test-programs/src/bin/preview2_udp_sockopts.rs
@@ -0,0 +1,68 @@
+use test_programs::wasi::sockets::network::{ErrorCode, IpAddressFamily};
+use test_programs::wasi::sockets::udp::UdpSocket;
+
+fn test_udp_sockopt_defaults(family: IpAddressFamily) {
+    let sock = UdpSocket::new(family).unwrap();
+
+    assert_eq!(sock.address_family(), family);
+
+    if family == IpAddressFamily::Ipv6 {
+        sock.ipv6_only().unwrap(); // Only verify that it has a default value at all, but either value is valid.
+    }
+
+    assert!(sock.unicast_hop_limit().unwrap() > 0);
+    assert!(sock.receive_buffer_size().unwrap() > 0);
+    assert!(sock.send_buffer_size().unwrap() > 0);
+}
+
+fn test_udp_sockopt_input_ranges(family: IpAddressFamily) {
+    let sock = UdpSocket::new(family).unwrap();
+
+    if family == IpAddressFamily::Ipv6 {
+        assert!(matches!(sock.set_ipv6_only(true), Ok(_)));
+        assert!(matches!(sock.set_ipv6_only(false), Ok(_)));
+    }
+
+    assert!(matches!(
+        sock.set_unicast_hop_limit(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_receive_buffer_size(0), 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(u64::MAX), Ok(_))); // Unsupported sizes should be silently capped.
+}
+
+fn test_udp_sockopt_readback(family: IpAddressFamily) {
+    let sock = UdpSocket::new(family).unwrap();
+
+    if family == IpAddressFamily::Ipv6 {
+        sock.set_ipv6_only(true).unwrap();
+        assert_eq!(sock.ipv6_only().unwrap(), true);
+        sock.set_ipv6_only(false).unwrap();
+        assert_eq!(sock.ipv6_only().unwrap(), false);
+    }
+
+    sock.set_unicast_hop_limit(42).unwrap();
+    assert_eq!(sock.unicast_hop_limit().unwrap(), 42);
+
+    sock.set_receive_buffer_size(0x10000).unwrap();
+    assert_eq!(sock.receive_buffer_size().unwrap(), 0x10000);
+
+    sock.set_send_buffer_size(0x10000).unwrap();
+    assert_eq!(sock.send_buffer_size().unwrap(), 0x10000);
+}
+
+fn main() {
+    test_udp_sockopt_defaults(IpAddressFamily::Ipv4);
+    test_udp_sockopt_defaults(IpAddressFamily::Ipv6);
+
+    test_udp_sockopt_input_ranges(IpAddressFamily::Ipv4);
+    test_udp_sockopt_input_ranges(IpAddressFamily::Ipv6);
+
+    test_udp_sockopt_readback(IpAddressFamily::Ipv4);
+    test_udp_sockopt_readback(IpAddressFamily::Ipv6);
+}
diff --git a/crates/test-programs/src/bin/preview2_udp_states.rs b/crates/test-programs/src/bin/preview2_udp_states.rs
new file mode 100644
index 000000000..4081b971e
--- /dev/null
+++ b/crates/test-programs/src/bin/preview2_udp_states.rs
@@ -0,0 +1,135 @@
+use test_programs::wasi::sockets::network::{
+    ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, Network,
+};
+use test_programs::wasi::sockets::udp::UdpSocket;
+
+fn test_udp_unbound_state_invariants(family: IpAddressFamily) {
+    let sock = UdpSocket::new(family).unwrap();
+
+    // Skipping: udp::start_bind
+    assert!(matches!(sock.finish_bind(), Err(ErrorCode::NotInProgress)));
+
+    assert!(matches!(sock.stream(None), Err(ErrorCode::InvalidState)));
+
+    assert!(matches!(sock.local_address(), Err(ErrorCode::InvalidState)));
+    assert!(matches!(
+        sock.remote_address(),
+        Err(ErrorCode::InvalidState)
+    ));
+    assert_eq!(sock.address_family(), family);
+
+    if family == IpAddressFamily::Ipv6 {
+        assert!(matches!(sock.ipv6_only(), Ok(_)));
+
+        // Even on platforms that don't support dualstack sockets,
+        // setting ipv6_only to true (disabling dualstack mode) should work.
+        assert!(matches!(sock.set_ipv6_only(true), Ok(_)));
+    } else {
+        assert!(matches!(sock.ipv6_only(), Err(ErrorCode::NotSupported)));
+        assert!(matches!(
+            sock.set_ipv6_only(true),
+            Err(ErrorCode::NotSupported)
+        ));
+    }
+
+    assert!(matches!(sock.unicast_hop_limit(), Ok(_)));
+    assert!(matches!(sock.set_unicast_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(_)));
+    assert!(matches!(sock.set_send_buffer_size(16000), Ok(_)));
+}
+
+fn test_udp_bound_state_invariants(net: &Network, family: IpAddressFamily) {
+    let bind_address = IpSocketAddress::new(IpAddress::new_loopback(family), 0);
+    let sock = UdpSocket::new(family).unwrap();
+    sock.blocking_bind(net, bind_address).unwrap();
+
+    assert!(matches!(
+        sock.start_bind(net, bind_address),
+        Err(ErrorCode::InvalidState)
+    ));
+    assert!(matches!(sock.finish_bind(), Err(ErrorCode::NotInProgress)));
+    // Skipping: udp::stream
+
+    assert!(matches!(sock.local_address(), Ok(_)));
+    assert!(matches!(
+        sock.remote_address(),
+        Err(ErrorCode::InvalidState)
+    ));
+    assert_eq!(sock.address_family(), family);
+
+    if family == IpAddressFamily::Ipv6 {
+        assert!(matches!(sock.ipv6_only(), Ok(_)));
+        assert!(matches!(
+            sock.set_ipv6_only(true),
+            Err(ErrorCode::InvalidState)
+        ));
+    } else {
+        assert!(matches!(sock.ipv6_only(), Err(ErrorCode::NotSupported)));
+        assert!(matches!(
+            sock.set_ipv6_only(true),
+            Err(ErrorCode::NotSupported)
+        ));
+    }
+
+    assert!(matches!(sock.unicast_hop_limit(), Ok(_)));
+    assert!(matches!(sock.set_unicast_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(_)));
+    assert!(matches!(sock.set_send_buffer_size(16000), Ok(_)));
+}
+
+fn test_udp_connected_state_invariants(net: &Network, family: IpAddressFamily) {
+    let bind_address = IpSocketAddress::new(IpAddress::new_loopback(family), 0);
+    let connect_address = IpSocketAddress::new(IpAddress::new_loopback(family), 54321);
+    let sock = UdpSocket::new(family).unwrap();
+    sock.blocking_bind(net, bind_address).unwrap();
+    sock.stream(Some(connect_address)).unwrap();
+
+    assert!(matches!(
+        sock.start_bind(net, bind_address),
+        Err(ErrorCode::InvalidState)
+    ));
+    assert!(matches!(sock.finish_bind(), Err(ErrorCode::NotInProgress)));
+    // Skipping: udp::stream
+
+    assert!(matches!(sock.local_address(), Ok(_)));
+    assert!(matches!(sock.remote_address(), Ok(_)));
+    assert_eq!(sock.address_family(), family);
+
+    if family == IpAddressFamily::Ipv6 {
+        assert!(matches!(sock.ipv6_only(), Ok(_)));
+        assert!(matches!(
+            sock.set_ipv6_only(true),
+            Err(ErrorCode::InvalidState)
+        ));
+    } else {
+        assert!(matches!(sock.ipv6_only(), Err(ErrorCode::NotSupported)));
+        assert!(matches!(
+            sock.set_ipv6_only(true),
+            Err(ErrorCode::NotSupported)
+        ));
+    }
+
+    assert!(matches!(sock.unicast_hop_limit(), Ok(_)));
+    assert!(matches!(sock.set_unicast_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(_)));
+    assert!(matches!(sock.set_send_buffer_size(16000), Ok(_)));
+}
+
+fn main() {
+    let net = Network::default();
+
+    test_udp_unbound_state_invariants(IpAddressFamily::Ipv4);
+    test_udp_unbound_state_invariants(IpAddressFamily::Ipv6);
+
+    test_udp_bound_state_invariants(&net, IpAddressFamily::Ipv4);
+    test_udp_bound_state_invariants(&net, IpAddressFamily::Ipv6);
+
+    test_udp_connected_state_invariants(&net, IpAddressFamily::Ipv4);
+    test_udp_connected_state_invariants(&net, IpAddressFamily::Ipv6);
+}
diff --git a/crates/test-programs/src/sockets.rs b/crates/test-programs/src/sockets.rs
index 2d1a0c99d..1d0e7f089 100644
--- a/crates/test-programs/src/sockets.rs
+++ b/crates/test-programs/src/sockets.rs
@@ -2,6 +2,7 @@ use crate::wasi::clocks::monotonic_clock;
 use crate::wasi::io::poll::{self, Pollable};
 use crate::wasi::io::streams::{InputStream, OutputStream, StreamError};
 use crate::wasi::sockets::instance_network;
+use crate::wasi::sockets::ip_name_lookup;
 use crate::wasi::sockets::network::{
     ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, Ipv4SocketAddress, Ipv6SocketAddress,
     Network,
@@ -53,6 +54,31 @@ impl Network {
     pub fn default() -> Network {
         instance_network::instance_network()
     }
+
+    pub fn blocking_resolve_addresses(&self, name: &str) -> Result<Vec<IpAddress>, ErrorCode> {
+        let stream = ip_name_lookup::resolve_addresses(&self, name)?;
+
+        let timeout = monotonic_clock::subscribe_duration(TIMEOUT_NS);
+        let pollable = stream.subscribe();
+
+        let mut addresses = vec![];
+
+        loop {
+            match stream.resolve_next_address() {
+                Ok(Some(addr)) => {
+                    addresses.push(addr);
+                }
+                Ok(None) => match addresses[..] {
+                    [] => return Err(ErrorCode::NameUnresolvable),
+                    _ => return Ok(addresses),
+                },
+                Err(ErrorCode::WouldBlock) => {
+                    pollable.wait_until(&timeout)?;
+                }
+                Err(err) => return Err(err),
+            }
+        }
+    }
 }
 
 impl TcpSocket {
@@ -140,6 +166,13 @@ impl UdpSocket {
             }
         }
     }
+
+    pub fn blocking_bind_unspecified(&self, network: &Network) -> Result<(), ErrorCode> {
+        let ip = IpAddress::new_unspecified(self.address_family());
+        let port = 0;
+
+        self.blocking_bind(network, IpSocketAddress::new(ip, port))
+    }
 }
 
 impl OutgoingDatagramStream {
diff --git a/crates/wasi/tests/all/async_.rs b/crates/wasi/tests/all/async_.rs
index 8e44d8210..ba6a76f3d 100644
--- a/crates/wasi/tests/all/async_.rs
+++ b/crates/wasi/tests/all/async_.rs
@@ -323,8 +323,12 @@ async fn preview2_tcp_bind() {
     run(PREVIEW2_TCP_BIND_COMPONENT, false).await.unwrap()
 }
 #[test_log::test(tokio::test(flavor = "multi_thread"))]
-async fn preview2_udp_connect() {
-    run(PREVIEW2_UDP_CONNECT_COMPONENT, false).await.unwrap()
+async fn preview2_tcp_connect() {
+    run(PREVIEW2_TCP_CONNECT_COMPONENT, false).await.unwrap()
+}
+#[test_log::test(tokio::test(flavor = "multi_thread"))]
+async fn preview2_udp_sockopts() {
+    run(PREVIEW2_UDP_SOCKOPTS_COMPONENT, false).await.unwrap()
 }
 #[test_log::test(tokio::test(flavor = "multi_thread"))]
 async fn preview2_udp_sample_application() {
@@ -333,8 +337,16 @@ async fn preview2_udp_sample_application() {
         .unwrap()
 }
 #[test_log::test(tokio::test(flavor = "multi_thread"))]
-async fn preview2_tcp_connect() {
-    run(PREVIEW2_TCP_CONNECT_COMPONENT, false).await.unwrap()
+async fn preview2_udp_states() {
+    run(PREVIEW2_UDP_STATES_COMPONENT, false).await.unwrap()
+}
+#[test_log::test(tokio::test(flavor = "multi_thread"))]
+async fn preview2_udp_bind() {
+    run(PREVIEW2_UDP_BIND_COMPONENT, false).await.unwrap()
+}
+#[test_log::test(tokio::test(flavor = "multi_thread"))]
+async fn preview2_udp_connect() {
+    run(PREVIEW2_UDP_CONNECT_COMPONENT, false).await.unwrap()
 }
 #[test_log::test(tokio::test(flavor = "multi_thread"))]
 async fn preview2_stream_pollable_correct() {
diff --git a/crates/wasi/tests/all/sync.rs b/crates/wasi/tests/all/sync.rs
index 25e7b5ba3..09094d166 100644
--- a/crates/wasi/tests/all/sync.rs
+++ b/crates/wasi/tests/all/sync.rs
@@ -266,16 +266,28 @@ fn preview2_tcp_bind() {
     run(PREVIEW2_TCP_BIND_COMPONENT, false).unwrap()
 }
 #[test_log::test]
-fn preview2_udp_connect() {
-    run(PREVIEW2_UDP_CONNECT_COMPONENT, false).unwrap()
+fn preview2_tcp_connect() {
+    run(PREVIEW2_TCP_CONNECT_COMPONENT, false).unwrap()
+}
+#[test_log::test]
+fn preview2_udp_sockopts() {
+    run(PREVIEW2_UDP_SOCKOPTS_COMPONENT, false).unwrap()
 }
 #[test_log::test]
 fn preview2_udp_sample_application() {
     run(PREVIEW2_UDP_SAMPLE_APPLICATION_COMPONENT, false).unwrap()
 }
 #[test_log::test]
-fn preview2_tcp_connect() {
-    run(PREVIEW2_TCP_CONNECT_COMPONENT, false).unwrap()
+fn preview2_udp_states() {
+    run(PREVIEW2_UDP_STATES_COMPONENT, false).unwrap()
+}
+#[test_log::test]
+fn preview2_udp_bind() {
+    run(PREVIEW2_UDP_BIND_COMPONENT, false).unwrap()
+}
+#[test_log::test]
+fn preview2_udp_connect() {
+    run(PREVIEW2_UDP_CONNECT_COMPONENT, false).unwrap()
 }
 #[test_log::test]
 fn preview2_stream_pollable_correct() {