Repository: denoland/deno
Pull request: fix(ext/node): allow IPv6 multicast addresses in UDP addMembership/dropMembership

The validation in `addMembership` and `dropMembership` was only checking for IPv4 addresses using `isValidIPv4Address()`, making the IPv6 code path unreachable:

```ts
addMembership(multicastAddress: string, interfaceAddress?: string): number {
  if (!isValidIPv4Address(multicastAddress, interfaceAddress)) {
    return codeMap.get("EINVAL")!;  // Always fails for IPv6!
  }
  // ...
  if (this.#family === "IPv6") {
    op_node_udp_join_multi_v6(this.#rid, multicastAddress, 0);  // Unreachable
  }
}
```

This refactors validation to check addresses based on the socket family:
- IPv4 sockets validate IPv4 multicast + optional interface addresses
- IPv6 sockets validate IPv6 multicast addresses (interface is by index, not address)

Also uses the existing `isIP()` helper instead of manual octet parsing.

Changed files:
filename: ext/node/polyfills/internal_binding/udp_wrap.ts
patch: @@ -63,27 +63,30 @@ const AF_INET6 = 10;
 
 const UDP_DGRAM_MAXSIZE = 64 * 1024;
 
-/** Validate that the multicast and optional interface addresses are parseable IPv4 addresses. */
-function isValidIPv4Address(
+/** Validate that the address is a parseable IPv4 address. */
+function isValidIPv4Address(address: string): boolean {
+  return isIP(address) === 4;
+}
+
+/** Validate multicast address matches the socket family. */
+function isValidMulticastAddress(
   multicastAddress: string,
+  family: string | undefined,
   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;
+  if (family === "IPv6") {
+    // IPv6 multicast - interface is specified by index, not address
+    return isIP(multicastAddress) === 6;
+  } else {
+    // IPv4 multicast
+    if (!isValidIPv4Address(multicastAddress)) return false;
+    if (
+      interfaceAddress !== undefined && !isValidIPv4Address(interfaceAddress)
+    ) {
+      return false;
+    }
+    return true;
+  }
 }
 
 export class SendWrap extends AsyncWrap {
@@ -144,7 +147,9 @@ export class UDP extends HandleWrap {
   }
 
   addMembership(multicastAddress: string, interfaceAddress?: string): number {
-    if (!isValidIPv4Address(multicastAddress, interfaceAddress)) {
+    if (
+      !isValidMulticastAddress(multicastAddress, this.#family, interfaceAddress)
+    ) {
       return codeMap.get("EINVAL")!;
     }
 
@@ -265,7 +270,9 @@ export class UDP extends HandleWrap {
     multicastAddress: string,
     interfaceAddress?: string,
   ): number {
-    if (!isValidIPv4Address(multicastAddress, interfaceAddress)) {
+    if (
+      !isValidMulticastAddress(multicastAddress, this.#family, interfaceAddress)
+    ) {
       return codeMap.get("EINVAL")!;
     }