Repository: denoland/deno
Pull request: fix(ext/node): make UDP socket ops synchronous, implement missing dgram methods

## Summary

- Creates a new `NodeUdpSocketResource` in `ext/node` that stores `tokio::net::UdpSocket` directly (no `AsyncRefCell`), making all socket config operations synchronous
- Replaces `udp_wrap.ts`'s dependency on `ext:deno_net/01_net.js` DatagramConn with direct sync ops
- Implements previously unimplemented methods: `setTTL`, `setMulticastInterface`, `addSourceSpecificMembership`, `dropSourceSpecificMembership`
- Removes artificial 64KB cap on `recvBufferSize`/`sendBufferSize` options

### Why

Node.js expects `addMembership`, `setBroadcast`, `setMulticastTTL`, etc. to be synchronous. The previous implementation wrapped Deno's `DatagramConn` which stored the socket in `AsyncRefCell`, forcing all socket config to be async. This caused `addMembership` to be fire-and-forget (never awaited), silently swallowing errors and breaking multicast (e.g. SSDP discovery).

### New ops in `ext/node/ops/udp.rs`

**Sync ops** (socket config):
- `op_node_udp_bind` — bind with socket2, returns `(rid, hostname, port)`
- `op_node_udp_join_multi_v4/v6`, `op_node_udp_leave_multi_v4/v6`
- `op_node_udp_set_broadcast`, `op_node_udp_set_multicast_loopback`, `op_node_udp_set_multicast_ttl`
- `op_node_udp_set_ttl`, `op_node_udp_set_multicast_interface`
- `op_node_udp_join_source_specific`, `op_node_udp_leave_source_specific`

**Async ops** (data transfer):
- `op_node_udp_send`, `op_node_udp_recv`

Supersedes #32287.

Fixes #18324
Fixes #28421
Fixes #30486

## Test plan
- [x] `cargo check -p deno_node` — compiles cleanly, no warnings
- [x] New unit tests for `addMembership`/`dropMembership`/`setBroadcast`/`setMulticastTTL`
- [x] New unit tests for `setTTL`, `setMulticastInterface`, source-specific multicast
- [x] New unit test for large `recvBufferSize`/`sendBufferSize` (4MB)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Changed files:
filename: ext/node/lib.rs
patch: @@ -337,6 +337,20 @@ deno_core::extension!(deno_node,
     ops::inspector::op_inspector_disconnect,
     ops::inspector::op_inspector_emit_protocol_event,
     ops::inspector::op_inspector_enabled,
+    ops::udp::op_node_udp_bind,
+    ops::udp::op_node_udp_join_multi_v4,
+    ops::udp::op_node_udp_leave_multi_v4,
+    ops::udp::op_node_udp_join_multi_v6,
+    ops::udp::op_node_udp_leave_multi_v6,
+    ops::udp::op_node_udp_set_broadcast,
+    ops::udp::op_node_udp_set_multicast_loopback,
+    ops::udp::op_node_udp_set_multicast_ttl,
+    ops::udp::op_node_udp_set_ttl,
+    ops::udp::op_node_udp_set_multicast_interface,
+    ops::udp::op_node_udp_join_source_specific,
+    ops::udp::op_node_udp_leave_source_specific,
+    ops::udp::op_node_udp_send,
+    ops::udp::op_node_udp_recv,
   ],
   objects = [
     ops::perf_hooks::EldHistogram,

filename: ext/node/ops/mod.rs
patch: @@ -19,6 +19,7 @@ pub mod process;
 pub mod require;
 pub mod shell;
 pub mod tls;
+pub mod udp;
 pub mod util;
 pub mod v8;
 pub mod vm;

filename: ext/node/ops/udp.rs
patch: @@ -0,0 +1,446 @@
+// Copyright 2018-2026 the Deno authors. MIT license.
+
+use std::borrow::Cow;
+use std::cell::RefCell;
+use std::net::Ipv4Addr;
+use std::net::Ipv6Addr;
+use std::net::SocketAddr;
+use std::rc::Rc;
+use std::str::FromStr;
+
+use deno_core::CancelFuture;
+use deno_core::CancelHandle;
+use deno_core::JsBuffer;
+use deno_core::OpState;
+use deno_core::RcRef;
+use deno_core::Resource;
+use deno_core::ResourceId;
+use deno_core::op2;
+use deno_permissions::PermissionsContainer;
+use socket2::Domain;
+use socket2::Protocol;
+use socket2::Socket;
+use socket2::Type;
+use tokio::net::UdpSocket;
+
+#[derive(Debug, thiserror::Error, deno_error::JsError)]
+pub enum NodeUdpError {
+  #[class(inherit)]
+  #[error("{0}")]
+  Io(#[from] std::io::Error),
+  #[class(generic)]
+  #[error("{0}")]
+  AddrParse(#[from] std::net::AddrParseError),
+  #[class(inherit)]
+  #[error("{0}")]
+  Resource(#[from] deno_core::error::ResourceError),
+  #[class(inherit)]
+  #[error("{0}")]
+  Canceled(#[from] deno_core::Canceled),
+  #[class(generic)]
+  #[error("No resolved address found")]
+  NoResolvedAddress,
+  #[class(type)]
+  #[error("Invalid hostname: '{0}'")]
+  InvalidHostname(String),
+  #[class(inherit)]
+  #[error(transparent)]
+  Permission(#[from] deno_permissions::PermissionCheckError),
+}
+
+pub struct NodeUdpSocketResource {
+  pub socket: UdpSocket,
+  pub cancel: CancelHandle,
+}
+
+impl Resource for NodeUdpSocketResource {
+  fn name(&self) -> Cow<'_, str> {
+    "nodeUdpSocket".into()
+  }
+
+  fn close(self: Rc<Self>) {
+    self.cancel.cancel()
+  }
+}
+
+#[op2]
+#[serde]
+pub fn op_node_udp_bind(
+  state: &mut OpState,
+  #[string] hostname: &str,
+  #[smi] port: u16,
+  reuse_address: bool,
+) -> Result<(ResourceId, String, u16), NodeUdpError> {
+  state
+    .borrow_mut::<PermissionsContainer>()
+    .check_net(&(hostname, Some(port)), "dgram.createSocket()")?;
+
+  let addr = deno_net::resolve_addr::resolve_addr_sync(hostname, port)?
+    .next()
+    .ok_or(NodeUdpError::NoResolvedAddress)?;
+
+  let domain = if addr.is_ipv4() {
+    Domain::IPV4
+  } else {
+    Domain::IPV6
+  };
+  let sock = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))?;
+  if reuse_address {
+    #[cfg(any(
+      target_os = "windows",
+      target_os = "android",
+      target_os = "linux"
+    ))]
+    sock.set_reuse_address(true)?;
+    #[cfg(all(unix, not(any(target_os = "android", target_os = "linux"))))]
+    sock.set_reuse_port(true)?;
+  }
+  let socket_addr = socket2::SockAddr::from(addr);
+  sock.bind(&socket_addr)?;
+  sock.set_nonblocking(true)?;
+
+  let std_socket: std::net::UdpSocket = sock.into();
+  let socket = UdpSocket::from_std(std_socket)?;
+  let local_addr = socket.local_addr()?;
+
+  let resource = NodeUdpSocketResource {
+    socket,
+    cancel: Default::default(),
+  };
+  let rid = state.resource_table.add(resource);
+
+  Ok((rid, local_addr.ip().to_string(), local_addr.port()))
+}
+
+#[op2]
+pub fn op_node_udp_join_multi_v4(
+  state: &mut OpState,
+  #[smi] rid: ResourceId,
+  #[string] address: &str,
+  #[string] multi_iface: Option<String>,
+) -> Result<(), NodeUdpError> {
+  let resource = state.resource_table.get::<NodeUdpSocketResource>(rid)?;
+
+  let addr = Ipv4Addr::from_str(address)?;
+  let iface = multi_iface
+    .as_deref()
+    .map(Ipv4Addr::from_str)
+    .transpose()?
+    .unwrap_or(Ipv4Addr::UNSPECIFIED);
+
+  resource.socket.join_multicast_v4(addr, iface)?;
+  Ok(())
+}
+
+#[op2]
+pub fn op_node_udp_leave_multi_v4(
+  state: &mut OpState,
+  #[smi] rid: ResourceId,
+  #[string] address: &str,
+  #[string] multi_iface: Option<String>,
+) -> Result<(), NodeUdpError> {
+  let resource = state.resource_table.get::<NodeUdpSocketResource>(rid)?;
+
+  let addr = Ipv4Addr::from_str(address)?;
+  let iface = multi_iface
+    .as_deref()
+    .map(Ipv4Addr::from_str)
+    .transpose()?
+    .unwrap_or(Ipv4Addr::UNSPECIFIED);
+
+  resource.socket.leave_multicast_v4(addr, iface)?;
+  Ok(())
+}
+
+#[op2(fast)]
+pub fn op_node_udp_join_multi_v6(
+  state: &mut OpState,
+  #[smi] rid: ResourceId,
+  #[string] address: &str,
+  #[smi] multi_iface: u32,
+) -> Result<(), NodeUdpError> {
+  let resource = state.resource_table.get::<NodeUdpSocketResource>(rid)?;
+
+  let addr = Ipv6Addr::from_str(address)?;
+  resource.socket.join_multicast_v6(&addr, multi_iface)?;
+  Ok(())
+}
+
+#[op2(fast)]
+pub fn op_node_udp_leave_multi_v6(
+  state: &mut OpState,
+  #[smi] rid: ResourceId,
+  #[string] address: &str,
+  #[smi] multi_iface: u32,
+) -> Result<(), NodeUdpError> {
+  let resource = state.resource_table.get::<NodeUdpSocketResource>(rid)?;
+
+  let addr = Ipv6Addr::from_str(address)?;
+  resource.socket.leave_multicast_v6(&addr, multi_iface)?;
+  Ok(())
+}
+
+#[op2(fast)]
+pub fn op_node_udp_set_broadcast(
+  state: &mut OpState,
+  #[smi] rid: ResourceId,
+  on: bool,
+) -> Result<(), NodeUdpError> {
+  let resource = state.resource_table.get::<NodeUdpSocketResource>(rid)?;
+  resource.socket.set_broadcast(on)?;
+  Ok(())
+}
+
+#[op2(fast)]
+pub fn op_node_udp_set_multicast_loopback(
+  state: &mut OpState,
+  #[smi] rid: ResourceId,
+  is_v4: bool,
+  on: bool,
+) -> Result<(), NodeUdpError> {
+  let resource = state.resource_table.get::<NodeUdpSocketResource>(rid)?;
+  if is_v4 {
+    resource.socket.set_multicast_loop_v4(on)?;
+  } else {
+    resource.socket.set_multicast_loop_v6(on)?;
+  }
+  Ok(())
+}
+
+#[op2(fast)]
+pub fn op_node_udp_set_multicast_ttl(
+  state: &mut OpState,
+  #[smi] rid: ResourceId,
+  #[smi] ttl: u32,
+) -> Result<(), NodeUdpError> {
+  let resource = state.resource_table.get::<NodeUdpSocketResource>(rid)?;
+  resource.socket.set_multicast_ttl_v4(ttl)?;
+  Ok(())
+}
+
+#[op2(fast)]
+pub fn op_node_udp_set_ttl(
+  state: &mut OpState,
+  #[smi] rid: ResourceId,
+  #[smi] ttl: u32,
+) -> Result<(), NodeUdpError> {
+  let resource = state.resource_table.get::<NodeUdpSocketResource>(rid)?;
+  let sock_ref = socket2::SockRef::from(&resource.socket);
+  sock_ref.set_ttl(ttl)?;
+  Ok(())
+}
+
+#[op2(fast)]
+pub fn op_node_udp_set_multicast_interface(
+  state: &mut OpState,
+  #[smi] rid: ResourceId,
+  is_ipv6: bool,
+  #[string] interface_address: &str,
+) -> Result<(), NodeUdpError> {
+  let resource = state.resource_table.get::<NodeUdpSocketResource>(rid)?;
+  let sock_ref = socket2::SockRef::from(&resource.socket);
+  if is_ipv6 {
+    let index = interface_address.parse::<u32>().unwrap_or(0);
+    sock_ref.set_multicast_if_v6(index)?;
+  } else {
+    let addr: Ipv4Addr = interface_address.parse().map_err(|_| {
+      NodeUdpError::Io(std::io::Error::new(
+        std::io::ErrorKind::InvalidInput,
+        "invalid IPv4 address",
+      ))
+    })?;
+    sock_ref.set_multicast_if_v4(&addr)?;
+  }
+  Ok(())
+}
+
+fn source_specific_multicast(
+  socket: &UdpSocket,
+  source_addr: Ipv4Addr,
+  group_addr: Ipv4Addr,
+  interface_addr: Ipv4Addr,
+  option: i32,
+) -> Result<(), NodeUdpError> {
+  #[cfg(unix)]
+  {
+    let mreq = libc::ip_mreq_source {
+      imr_multiaddr: libc::in_addr {
+        s_addr: u32::from(group_addr).to_be(),
+      },
+      imr_sourceaddr: libc::in_addr {
+        s_addr: u32::from(source_addr).to_be(),
+      },
+      imr_interface: libc::in_addr {
+        s_addr: u32::from(interface_addr).to_be(),
+      },
+    };
+
+    // SAFETY: We pass a valid socket fd, level, option, and correctly-sized struct.
+    let ret = unsafe {
+      libc::setsockopt(
+        std::os::fd::AsRawFd::as_raw_fd(socket),
+        libc::IPPROTO_IP,
+        option,
+        &mreq as *const libc::ip_mreq_source as *const libc::c_void,
+        std::mem::size_of::<libc::ip_mreq_source>() as libc::socklen_t,
+      )
+    };
+    if ret != 0 {
+      return Err(std::io::Error::last_os_error().into());
+    }
+  }
+
+  #[cfg(windows)]
+  {
+    use std::os::windows::io::AsRawSocket;
+
+    #[repr(C)]
+    struct IpMreqSource {
+      imr_multiaddr: u32,
+      imr_sourceaddr: u32,
+      imr_interface: u32,
+    }
+
+    let mreq = IpMreqSource {
+      imr_multiaddr: u32::from(group_addr).to_be(),
+      imr_sourceaddr: u32::from(source_addr).to_be(),
+      imr_interface: u32::from(interface_addr).to_be(),
+    };
+
+    // SAFETY: We pass a valid socket, level, option, and correctly-sized struct.
+    let ret = unsafe {
+      windows_sys::Win32::Networking::WinSock::setsockopt(
+        socket.as_raw_socket() as usize,
+        windows_sys::Win32::Networking::WinSock::IPPROTO_IP,
+        option,
+        &mreq as *const IpMreqSource as *const u8,
+        std::mem::size_of::<IpMreqSource>() as i32,
+      )
+    };
+    if ret != 0 {
+      return Err(std::io::Error::last_os_error().into());
+    }
+  }
+
+  Ok(())
+}
+
+#[op2(fast)]
+pub fn op_node_udp_join_source_specific(
+  state: &mut OpState,
+  #[smi] rid: ResourceId,
+  #[string] source_address: &str,
+  #[string] group_address: &str,
+  #[string] interface_address: &str,
+) -> Result<(), NodeUdpError> {
+  let resource = state.resource_table.get::<NodeUdpSocketResource>(rid)?;
+
+  let source_addr = Ipv4Addr::from_str(source_address)?;
+  let group_addr = Ipv4Addr::from_str(group_address)?;
+  let interface_addr = Ipv4Addr::from_str(interface_address)?;
+
+  #[cfg(unix)]
+  let option = libc::IP_ADD_SOURCE_MEMBERSHIP;
+  #[cfg(windows)]
+  let option =
+    windows_sys::Win32::Networking::WinSock::IP_ADD_SOURCE_MEMBERSHIP;
+
+  source_specific_multicast(
+    &resource.socket,
+    source_addr,
+    group_addr,
+    interface_addr,
+    option,
+  )
+}
+
+#[op2(fast)]
+pub fn op_node_udp_leave_source_specific(
+  state: &mut OpState,
+  #[smi] rid: ResourceId,
+  #[string] source_address: &str,
+  #[string] group_address: &str,
+  #[string] interface_address: &str,
+) -> Result<(), NodeUdpError> {
+  let resource = state.resource_table.get::<NodeUdpSocketResource>(rid)?;
+
+  let source_addr = Ipv4Addr::from_str(source_address)?;
+  let group_addr = Ipv4Addr::from_str(group_address)?;
+  let interface_addr = Ipv4Addr::from_str(interface_address)?;
+
+  #[cfg(unix)]
+  let option = libc::IP_DROP_SOURCE_MEMBERSHIP;
+  #[cfg(windows)]
+  let option =
+    windows_sys::Win32::Networking::WinSock::IP_DROP_SOURCE_MEMBERSHIP;
+
+  source_specific_multicast(
+    &resource.socket,
+    source_addr,
+    group_addr,
+    interface_addr,
+    option,
+  )
+}
+
+#[op2]
+#[smi]
+pub async fn op_node_udp_send(
+  state: Rc<RefCell<OpState>>,
+  #[smi] rid: ResourceId,
+  #[buffer] buf: JsBuffer,
+  #[string] hostname: String,
+  #[smi] port: u16,
+) -> Result<usize, NodeUdpError> {
+  let resource = state
+    .borrow()
+    .resource_table
+    .get::<NodeUdpSocketResource>(rid)?;
+
+  let addr: SocketAddr =
+    deno_net::resolve_addr::resolve_addr_sync(&hostname, port)?
+      .next()
+      .ok_or(NodeUdpError::NoResolvedAddress)?;
+
+  let cancel = RcRef::map(&resource, |r| &r.cancel);
+  let nwritten = resource
+    .socket
+    .send_to(&buf, &addr)
+    .or_cancel(cancel)
+    .await??;
+
+  Ok(nwritten)
+}
+
+#[derive(serde::Serialize)]
+pub struct RecvResult {
+  pub nread: usize,
+  pub hostname: String,
+  pub port: u16,
+}
+
+#[op2]
+#[serde]
+pub async fn op_node_udp_recv(
+  state: Rc<RefCell<OpState>>,
+  #[smi] rid: ResourceId,
+  #[buffer] mut buf: JsBuffer,
+) -> Result<RecvResult, NodeUdpError> {
+  let resource = state
+    .borrow()
+    .resource_table
+    .get::<NodeUdpSocketResource>(rid)?;
+
+  let cancel = RcRef::map(&resource, |r| &r.cancel);
+  let (nread, remote_addr) = resource
+    .socket
+    .recv_from(&mut buf)
+    .or_cancel(cancel)
+    .await??;
+
+  Ok(RecvResult {
+    nread,
+    hostname: remote_addr.ip().to_string(),
+    port: remote_addr.port(),
+  })
+}

filename: ext/node/polyfills/internal_binding/udp_wrap.ts
patch: @@ -23,10 +23,22 @@
 // TODO(petamoriken): enable prefer-primordials for node polyfills
 // deno-lint-ignore-file prefer-primordials
 
+import { core } from "ext:core/mod.js";
 import {
-  op_net_validate_multicast,
-  op_node_unstable_net_listen_udp,
-  op_node_unstable_net_listen_unixpacket,
+  op_node_udp_bind,
+  op_node_udp_join_multi_v4,
+  op_node_udp_join_multi_v6,
+  op_node_udp_join_source_specific,
+  op_node_udp_leave_multi_v4,
+  op_node_udp_leave_multi_v6,
+  op_node_udp_leave_source_specific,
+  op_node_udp_recv,
+  op_node_udp_send,
+  op_node_udp_set_broadcast,
+  op_node_udp_set_multicast_interface,
+  op_node_udp_set_multicast_loopback,
+  op_node_udp_set_multicast_ttl,
+  op_node_udp_set_ttl,
 } from "ext:core/ops";
 
 import {
@@ -41,22 +53,39 @@ import { notImplemented } from "ext:deno_node/_utils.ts";
 import { Buffer } from "node:buffer";
 import type { ErrnoException } from "ext:deno_node/internal/errors.ts";
 import { isIP } from "ext:deno_node/internal/net.ts";
-import * as net from "ext:deno_net/01_net.js";
 import { isLinux, isWindows } from "ext:deno_node/_util/os.ts";
 import { os } from "ext:deno_node/internal_binding/constants.ts";
 
-const DenoListenDatagram = net.createListenDatagram(
-  op_node_unstable_net_listen_udp,
-  op_node_unstable_net_listen_unixpacket,
-);
-
 type MessageType = string | Uint8Array | Buffer | DataView;
 
 const AF_INET = 2;
 const AF_INET6 = 10;
 
 const UDP_DGRAM_MAXSIZE = 64 * 1024;
 
+/** Validate that the multicast and optional interface addresses are parseable IPv4 addresses. */
+function isValidIPv4Address(
+  multicastAddress: string,
+  interfaceAddress?: string,
+): boolean {
+  // Quick validation: each octet must be 0-255
+  const parts = multicastAddress.split(".");
+  if (parts.length !== 4) return false;
+  for (const part of parts) {
+    const n = Number(part);
+    if (!Number.isInteger(n) || n < 0 || n > 255) return false;
+  }
+  if (interfaceAddress !== undefined) {
+    const ifaceParts = interfaceAddress.split(".");
+    if (ifaceParts.length !== 4) return false;
+    for (const part of ifaceParts) {
+      const n = Number(part);
+      if (!Number.isInteger(n) || n < 0 || n > 255) return false;
+    }
+  }
+  return true;
+}
+
 export class SendWrap extends AsyncWrap {
   list!: MessageType[];
   address!: string;
@@ -81,8 +110,9 @@ export class UDP extends HandleWrap {
   #remoteFamily?: string;
   #remotePort?: number;
 
-  #listener?: Deno.DatagramConn;
+  #rid?: number;
   #receiving = false;
+  #recvPromiseId?: number;
   #unrefed = false;
 
   #recvBufferSize = UDP_DGRAM_MAXSIZE;
@@ -114,38 +144,57 @@ export class UDP extends HandleWrap {
   }
 
   addMembership(multicastAddress: string, interfaceAddress?: string): number {
-    try {
-      op_net_validate_multicast(multicastAddress, interfaceAddress);
-    } catch {
+    if (!isValidIPv4Address(multicastAddress, interfaceAddress)) {
       return codeMap.get("EINVAL")!;
     }
 
-    if (!this.#listener) {
+    if (this.#rid === undefined) {
       return codeMap.get("EBADF")!;
     }
 
-    if (this.#family === "IPv6") {
-      this.#listener.joinMulticastV6(multicastAddress, interfaceAddress);
-    } else if (this.#family === "IPv4") {
-      this.#listener.joinMulticastV4(multicastAddress, interfaceAddress);
+    try {
+      if (this.#family === "IPv6") {
+        op_node_udp_join_multi_v6(this.#rid, multicastAddress, 0);
+      } else {
+        op_node_udp_join_multi_v4(
+          this.#rid,
+          multicastAddress,
+          interfaceAddress ?? null,
+        );
+      }
+      return 0;
+    } catch {
+      return codeMap.get("EINVAL")!;
     }
-
-    return 0;
   }
 
   addSourceSpecificMembership(
     sourceAddress: string,
     groupAddress: string,
     interfaceAddress?: string,
   ): number {
+    if (
+      !isValidIPv4Address(sourceAddress) ||
+      !isValidIPv4Address(groupAddress)
+    ) {
+      return codeMap.get("EINVAL")!;
+    }
+
+    if (this.#rid === undefined) {
+      return codeMap.get("EBADF")!;
+    }
+
     try {
-      op_net_validate_multicast(groupAddress, interfaceAddress);
-      op_net_validate_multicast(sourceAddress, interfaceAddress);
+      op_node_udp_join_source_specific(
+        this.#rid,
+        sourceAddress,
+        groupAddress,
+        interfaceAddress ?? "0.0.0.0",
+      );
     } catch {
       return codeMap.get("EINVAL")!;
     }
-
-    notImplemented("udp.UDP.prototype.addSourceSpecificMembership");
+    return 0;
   }
 
   /**
@@ -173,15 +222,8 @@ export class UDP extends HandleWrap {
     buffer: boolean,
     ctx: Record<string, string | number>,
   ): number | undefined {
-    let err: string | undefined;
-
-    if (size > UDP_DGRAM_MAXSIZE) {
-      err = "EINVAL";
-    } else if (!this.#address) {
-      err = isWindows ? "ENOTSOCK" : "EBADF";
-    }
-
-    if (err) {
+    if (!this.#address) {
+      const err = isWindows ? "ENOTSOCK" : "EBADF";
       ctx.errno = codeMap.get(err)!;
       ctx.code = err;
       ctx.message = errorMap.get(ctx.errno)![1];
@@ -223,38 +265,57 @@ export class UDP extends HandleWrap {
     multicastAddress: string,
     interfaceAddress?: string,
   ): number {
-    try {
-      op_net_validate_multicast(multicastAddress, interfaceAddress);
-    } catch {
+    if (!isValidIPv4Address(multicastAddress, interfaceAddress)) {
       return codeMap.get("EINVAL")!;
     }
 
-    if (!this.#listener) {
+    if (this.#rid === undefined) {
       return codeMap.get("EBADF")!;
     }
 
-    net.dropMembership(
-      this.#listener,
-      this.#family === "IPv6",
-      multicastAddress,
-      interfaceAddress,
-    );
-    return 0;
+    try {
+      if (this.#family === "IPv6") {
+        op_node_udp_leave_multi_v6(this.#rid, multicastAddress, 0);
+      } else {
+        op_node_udp_leave_multi_v4(
+          this.#rid,
+          multicastAddress,
+          interfaceAddress ?? null,
+        );
+      }
+      return 0;
+    } catch {
+      return codeMap.get("EINVAL")!;
+    }
   }
 
   dropSourceSpecificMembership(
     sourceAddress: string,
     groupAddress: string,
     interfaceAddress?: string,
   ): number {
+    if (
+      !isValidIPv4Address(sourceAddress) ||
+      !isValidIPv4Address(groupAddress)
+    ) {
+      return codeMap.get("EINVAL")!;
+    }
+
+    if (this.#rid === undefined) {
+      return codeMap.get("EBADF")!;
+    }
+
     try {
-      op_net_validate_multicast(groupAddress, interfaceAddress);
-      op_net_validate_multicast(sourceAddress, interfaceAddress);
+      op_node_udp_leave_source_specific(
+        this.#rid,
+        sourceAddress,
+        groupAddress,
+        interfaceAddress ?? "0.0.0.0",
+      );
     } catch {
       return codeMap.get("EINVAL")!;
     }
-
-    notImplemented("udp.UDP.prototype.dropSourceSpecificMembership");
+    return 0;
   }
 
   /**
@@ -325,7 +386,6 @@ export class UDP extends HandleWrap {
   }
 
   override ref() {
-    this.#listener?.ref();
     this.#unrefed = false;
   }
 
@@ -348,85 +408,112 @@ export class UDP extends HandleWrap {
   }
 
   setBroadcast(bool: 0 | 1): number {
-    if (!this.#listener) {
+    if (this.#rid === undefined) {
       return codeMap.get("EBADF")!;
     }
 
-    net.setDatagramBroadcast(this.#listener, bool === 1);
-    return 0;
+    try {
+      op_node_udp_set_broadcast(this.#rid, bool === 1);
+      return 0;
+    } catch {
+      return codeMap.get("EINVAL")!;
+    }
   }
 
-  setMulticastInterface(_interfaceAddress: string): number {
-    notImplemented("udp.UDP.prototype.setMulticastInterface");
+  setMulticastInterface(interfaceAddress: string): number {
+    if (this.#rid === undefined) {
+      return codeMap.get("EBADF")!;
+    }
+
+    try {
+      op_node_udp_set_multicast_interface(
+        this.#rid,
+        this.#family === "IPv6",
+        interfaceAddress,
+      );
+      return 0;
+    } catch {
+      return codeMap.get("EINVAL")!;
+    }
   }
 
   setMulticastLoopback(bool: 0 | 1): number {
-    if (!this.#listener) {
+    if (this.#rid === undefined) {
       return codeMap.get("EBADF")!;
     }
 
-    net.setMulticastLoopback(
-      this.#listener,
-      this.#family === "IPv6",
-      bool === 1,
-    );
-    return 0;
+    try {
+      op_node_udp_set_multicast_loopback(
+        this.#rid,
+        this.#family === "IPv4",
+        bool === 1,
+      );
+      return 0;
+    } catch {
+      return codeMap.get("EINVAL")!;
+    }
   }
 
   setMulticastTTL(ttl: number): number {
     if (ttl < 1 || ttl > 255) {
       return codeMap.get("EINVAL")!;
     }
 
-    if (!this.#listener) {
+    if (this.#rid === undefined) {
       return codeMap.get("EBADF")!;
     }
 
     try {
       if (this.#family === "IPv4") {
-        net.setMulticastTTL(this.#listener, ttl);
+        op_node_udp_set_multicast_ttl(this.#rid, ttl);
       }
       return 0;
     } catch {
       return codeMap.get("EINVAL")!;
     }
   }
 
-  setTTL(_ttl: number): number {
-    notImplemented("udp.UDP.prototype.setTTL");
+  setTTL(ttl: number): number {
+    if (ttl < 1 || ttl > 255) {
+      return codeMap.get("EINVAL")!;
+    }
+
+    if (this.#rid === undefined) {
+      return codeMap.get("EBADF")!;
+    }
+
+    try {
+      op_node_udp_set_ttl(this.#rid, ttl);
+      return 0;
+    } catch {
+      return codeMap.get("EINVAL")!;
+    }
   }
 
   override unref() {
-    this.#listener?.unref();
     this.#unrefed = true;
   }
 
   #doBind(ip: string, port: number, flags: number, family: number): number {
-    const listenOptions = {
-      port,
-      hostname: ip,
-      transport: "udp" as const,
-      reuseAddress: (flags & os.UV_UDP_REUSEADDR) !== 0,
-    };
-
-    let listener;
-
     try {
-      listener = DenoListenDatagram(listenOptions);
+      const [rid, hostname, boundPort] = op_node_udp_bind(
+        ip,
+        port,
+        (flags & os.UV_UDP_REUSEADDR) !== 0,
+      );
+      this.#rid = rid;
+      this.#address = hostname;
+      this.#port = boundPort;
+      this.#family = family === AF_INET6
+        ? ("IPv6" as const)
+        : ("IPv4" as const);
+      return 0;
     } catch (e) {
       if (e instanceof Deno.errors.NotCapable) {
         throw e;
       }
       return codeMap.get(e.code ?? "UNKNOWN") ?? codeMap.get("UNKNOWN")!;
     }
-
-    const address = listener.addr as Deno.NetAddr;
-    this.#address = address.hostname;
-    this.#port = address.port;
-    this.#family = family === AF_INET6 ? ("IPv6" as const) : ("IPv4" as const);
-    this.#listener = listener;
-
-    return 0;
   }
 
   #doConnect(ip: string, port: number, family: number): number {
@@ -456,13 +543,6 @@ export class UDP extends HandleWrap {
       hasCallback = args[0] as boolean;
     }
 
-    const addr: Deno.NetAddr = {
-      hostname: this.#remoteAddress!,
-      port: this.#remotePort!,
-      transport: "udp",
-    };
-
-    // Deno.DatagramConn.prototype.send accepts only one Uint8Array
     const payload = new Uint8Array(
       Buffer.concat(
         bufs.map((buf) => {
@@ -480,9 +560,13 @@ export class UDP extends HandleWrap {
       let err: number | null = null;
 
       try {
-        sent = await this.#listener!.send(payload, addr);
+        sent = await op_node_udp_send(
+          this.#rid!,
+          payload,
+          this.#remoteAddress!,
+          this.#remotePort!,
+        );
       } catch (e) {
-        // TODO(cmorten): map errors to appropriate error codes.
         if (e instanceof Deno.errors.BadResource) {
           err = codeMap.get("EBADF")!;
         } else if (
@@ -516,23 +600,20 @@ export class UDP extends HandleWrap {
 
     const p = new Uint8Array(this.#recvBufferSize);
 
-    let buf: Uint8Array;
-    let remoteAddr: Deno.NetAddr | null;
-    let nread: number | null;
-
-    if (this.#unrefed) {
-      this.#listener!.unref();
-    }
+    let nread: number;
+    let remoteHostname: string | null = null;
+    let remotePort: number | null = null;
 
     try {
-      [buf, remoteAddr] = (await this.#listener!.receive(p)) as [
-        Uint8Array,
-        Deno.NetAddr,
-      ];
-
-      nread = buf.length;
+      const promise = op_node_udp_recv(this.#rid!, p);
+      if (this.#unrefed) {
+        core.unrefOpPromise(promise);
+      }
+      const result = await promise;
+      nread = result.nread;
+      remoteHostname = result.hostname;
+      remotePort = result.port;
     } catch (e) {
-      // TODO(cmorten): map errors to appropriate error codes.
       if (
         e instanceof Deno.errors.Interrupted ||
         e instanceof Deno.errors.BadResource
@@ -541,25 +622,24 @@ export class UDP extends HandleWrap {
       } else {
         nread = codeMap.get("UNKNOWN")!;
       }
-
-      buf = new Uint8Array(0);
-      remoteAddr = null;
     }
 
-    nread ??= 0;
-
-    const rinfo = remoteAddr
+    const rinfo = remoteHostname !== null
       ? {
-        address: remoteAddr.hostname,
-        port: remoteAddr.port,
-        family: isIP(remoteAddr.hostname) === 6
+        address: remoteHostname,
+        port: remotePort!,
+        family: isIP(remoteHostname) === 6
           ? ("IPv6" as const)
           : ("IPv4" as const),
       }
       : undefined;
 
+    const buf = remoteHostname !== null
+      ? Buffer.from(p.buffer, p.byteOffset, nread)
+      : Buffer.alloc(0);
+
     try {
-      this.onmessage(nread, this, Buffer.from(buf), rinfo);
+      this.onmessage(nread, this, buf, rinfo);
     } catch {
       // swallow callback errors.
     }
@@ -575,14 +655,15 @@ export class UDP extends HandleWrap {
     this.#port = undefined;
     this.#family = undefined;
 
-    try {
-      this.#listener!.close();
-    } catch {
-      // listener already closed
+    if (this.#rid !== undefined) {
+      try {
+        core.close(this.#rid);
+      } catch {
+        // already closed
+      }
+      this.#rid = undefined;
     }
 
-    this.#listener = undefined;
-
     return 0;
   }
 }

filename: tests/unit_node/dgram_test.ts
patch: @@ -1,6 +1,6 @@
 // Copyright 2018-2026 the Deno authors. MIT license.
 
-import { assertEquals } from "@std/assert";
+import { assert, assertEquals } from "@std/assert";
 import { execCode } from "../unit/test_util.ts";
 import { createSocket, type Socket } from "node:dgram";
 
@@ -82,3 +82,97 @@ Deno.test("[node/dgram] createSocket, reuseAddr option", async () => {
   socket0.close();
   socket1?.close();
 });
+
+Deno.test("[node/dgram] addMembership, setBroadcast, setMulticastTTL after bind", async () => {
+  const { promise, resolve, reject } = Promise.withResolvers<void>();
+
+  const socket = createSocket({ type: "udp4", reuseAddr: true });
+
+  socket.on("error", (err) => {
+    reject(err);
+  });
+
+  socket.bind(0, "0.0.0.0", () => {
+    try {
+      socket.addMembership("239.255.255.250");
+      socket.setBroadcast(true);
+      socket.setMulticastTTL(4);
+      socket.dropMembership("239.255.255.250");
+      resolve();
+    } catch (err) {
+      reject(err);
+    } finally {
+      socket.close();
+    }
+  });
+
+  await promise;
+});
+
+Deno.test("[node/dgram] setTTL sets unicast TTL without error", async () => {
+  const { promise, resolve } = Promise.withResolvers<void>();
+  const socket = createSocket("udp4");
+  socket.bind(0, () => {
+    socket.setTTL(128);
+    socket.close(() => resolve());
+  });
+  await promise;
+});
+
+Deno.test("[node/dgram] setTTL throws on invalid TTL", async () => {
+  const { promise, resolve } = Promise.withResolvers<void>();
+  const socket = createSocket("udp4");
+  socket.bind(0, () => {
+    try {
+      socket.setTTL(0);
+      assert(false, "should have thrown");
+    } catch (e) {
+      assert(e instanceof Error);
+    }
+    try {
+      socket.setTTL(256);
+      assert(false, "should have thrown");
+    } catch (e) {
+      assert(e instanceof Error);
+    }
+    socket.close(() => resolve());
+  });
+  await promise;
+});
+
+Deno.test("[node/dgram] setMulticastInterface sets interface without error", async () => {
+  const { promise, resolve } = Promise.withResolvers<void>();
+  const socket = createSocket("udp4");
+  socket.bind(0, () => {
+    socket.setMulticastInterface("0.0.0.0");
+    socket.close(() => resolve());
+  });
+  await promise;
+});
+
+Deno.test("[node/dgram] addSourceSpecificMembership and dropSourceSpecificMembership", async () => {
+  const { promise, resolve } = Promise.withResolvers<void>();
+  const socket = createSocket("udp4");
+  socket.bind(0, () => {
+    socket.addSourceSpecificMembership("127.0.0.1", "232.1.1.1");
+    socket.dropSourceSpecificMembership("127.0.0.1", "232.1.1.1");
+    socket.close(() => resolve());
+  });
+  await promise;
+});
+
+Deno.test("[node/dgram] large recvBufferSize and sendBufferSize do not throw", async () => {
+  const { promise, resolve, reject } = Promise.withResolvers<void>();
+  const socket = createSocket({
+    type: "udp4",
+    recvBufferSize: 4194304,
+    sendBufferSize: 4194304,
+  });
+  socket.on("error", (err) => {
+    reject(err);
+  });
+  socket.bind(0, () => {
+    socket.close(() => resolve());
+  });
+  await promise;
+});