Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Please expose multicast for UDP Datagram #10706

Closed
outcast opened this issue May 19, 2021 · 10 comments · Fixed by #17811
Closed

Please expose multicast for UDP Datagram #10706

outcast opened this issue May 19, 2021 · 10 comments · Fixed by #17811
Labels
runtime Relates to code in the runtime crate suggestion suggestions for new features (yet to be agreed)

Comments

@outcast
Copy link

outcast commented May 19, 2021

Please expose multicast functions for Datagram connections. The functions can be found in std::net::UdpSocket. It will mostly likely require a new function to check if an address is either IPv4 or IPv6.

@outcast outcast changed the title Please expose multicast join and leave functions for Datagram Please expose multicast for UDP Datagram May 19, 2021
@lucacasonato
Copy link
Member

Do you have have an example of something you might want to do with this? What is the API in Node? What is the API in Go? How would you want it to look in Deno?

@lucacasonato lucacasonato added runtime Relates to code in the runtime crate suggestion suggestions for new features (yet to be agreed) labels May 19, 2021
@outcast
Copy link
Author

outcast commented May 19, 2021

In node the call is socket.addMembership(multicastAddress[, multicastInterface]). However, I would image it would would work off the Deno.DatagramConn interface and would share similar function names and parameters with Rust functions. ie. std::net::join_multicast_v4

@NfNitLoop
Copy link

Came here to ask for the same thing.

I'm playing around with the new (unstable) Deno.networkInterfaces(). Thought I'd make some broadcast/multicast listeners to try it out.

For the ipv4 addresses, I can send to the broadcast address (ex: 192.168.1.255), and receive it via my local address (ex: 192.168.1.10).

However, that doesn't work for ipv6 addresses. AFAICT (I am not an expert on UDP networking 😅) they don't support broadcast addresses, only multicast, which requires the membership addition at the socket level.

I'd love it if it were a part of the call to listenDatagram, that way it could handle joining/leaving the multicast group automatically. ex:

let conn = Deno.listenDatagram({
    transport: "udp",
    port: 3030,
    hostname: "fe80::1%0",
    joinMulticast: [
        "ff02::1",
    ],
}

Questions that leaves me with (again, as a relative newbie to IPv6 UDP):

  • Would that mean that my conn gets packets that were destined for both ff02::1 and fe80::1? I think the current DenoDatagramConn doesn't give us a way to distinguish between the two. (Though, I think that's an issue for ipv4 too.)
  • If that's the case, maybe a slightly different signature that allows explicitly listening to only a multicast address would be preferable.

@sgwilym
Copy link
Contributor

sgwilym commented Feb 10, 2023

I have the time and inclination to help contribute this feature! I'm receiving a grant to add peer discovery on local networks for a distributed database, and my preferred approach would be to implement a userland module for DNS Service Discovery (used by many printers the world over) and use it in conjunction with mDNS.

For this Deno needs multicasting capabilities, and I would like to help add them. It would be great to know which kinds of APIs would be preferred before I start working on a PR.

@lucacasonato Do you have a preference between explicit calls for multicast membership (e.g. Node's addMembership / dropMembership) vs the configuration style API which @NfNitLoop suggested? Or something else entirely?

Perhaps this could even nudge Deno.listenDatagram towards stability, which may be more possible given that Deno.networkInterfaces has stabilised.

@sgwilym
Copy link
Contributor

sgwilym commented Feb 14, 2023

An explicit and imperative API for multicast, where explicit calls to joinMulticast, leaveMulticast etc. are made may be the preferable one. There are other things you may want to change on a multicast listening sessions, e.g. set multicast TTL, or toggle multicast loopback, and putting these all into a single configuration object (and assuming that configuration is the only desirable one for the lifetime of the session) doesn't feel right to me.

So Deno's DatagramConn could follow the lead of multicasting APIs in Rust and Node with these new methods:

DatagramConn.joinMulticast(addr: string, interface: string | number) // interface could be IP address or IPv6 interface index / scope ID
DatagramConn.leaveMulticast(addr: string, interface: string | number)
DatagramConn.setMulticastLoopback(loopback: boolean)
DatagramConn.setMulticastTTL(ttl: number)

And some example usage:

const listener = Deno.listenDatagram({
  transport: "udp",
  port: 5353,
  hostname: "224.0.0.251"
});

const interfaces = Deno.networkInterfaces();

// Let's pretend the first interface is IPv4
const ipv4Interface = interfaces[0];

// Join the mDNS multicast group with an IPv4 address. Deno determines whether the address is IPv4 or IPv6, and calls join_multicast_v4 or join_multicast_v6 as is appropriate.
listener.joinMulticast("224.0.0.251", interfaces[0].address);

// Or with IPv6
const ipv6Interface = interfaces[1];
listener.joinMulticast("ff02::fb", ipv6Interface.scopeid);

// Tweak multicast settings
listener.setMulticastLoopback(true);
listener.setMulticastTTL(255);

// And leave multicast groups before closing.
listener.leaveMulticast("224.0.0.251", interfaces[0].address);
listener.leaveMulticast("ff02::fb", ipv6Interface.scopeid);

// Or maybe leave automatically when the connection is closed?
listener.close();

There are already analogues for all of these methods on std::net::UdpSocket, so to my naive eyes it mostly seems like a job of wiring the JS calls up with the underlying UdpSocket.

Would a PR with an API like this for multicasting be considered?

@aapoalas
Copy link
Collaborator

I don't hold all the keys to the castle but I would welcome such a PR heartily. Only thing I might think of is if there should be separate APIs for IPv4 and 6: Since you need to know which you're using anyway for the first parameter.

@sgwilym
Copy link
Contributor

sgwilym commented Feb 15, 2023

@aapoalas I was wondering about that too. If we followed the API of std::net::UdpSocket:

DatagramConn.joinMulticastV4(addr: string, interface: string)
DatagramConn.joinMulticastV6(addr: string, interface: number) // interface is scopeid

DatagramConn.leaveMulticastV4(addr: string, interface: string)
DatagramConn.leaveMulticast6(addr: string, interface: number) // interface is scopeid

DatagramConn.setMulticastLoopbackV4(loopback: boolean)
DatagramConn.setMulticastLoopbackV6(loopback: boolean)

DatagramConn.setMulticastTTL(ttl: number) // May or may not do anything to IPv6 connections.

It's very explicit, but also kind of a lot? Node's dgram interface seems to get by with single methods that take both IPv4 and v6 addresses, and setMulticastLoopback seems to toggle both at once.

@sgwilym
Copy link
Contributor

sgwilym commented Feb 16, 2023

The good news is that I now have a branch of Deno that can receive multicast messages.

The less good news is the API isn't quite there yet. I tried the API style with a single joinMulticast / setMulticastLoopback etc., and found that you can't have a single setMulticastLoopback because the UdpSocket.set_multicast_loop_v6 method in Rust will throw if you haven't joined a IPv6 multicast group yet. Which makes sense! But is very strict compared to Node's dgram module (but maybe their API is too permissive?)

There's a part of me that wants to see if an API like this makes sense...

const membership = DatagramConn.joinMulticastV4(address, interface);

membership.setMulticastLoopback(true); // calls UdpSocket.set_multicast_loopback_V4 under the hood
membership.leave(); // calls UdpSocket.leave_multicast_v4 with the original address + interface under the hood
membership.setTTL(50); // This method would not be available on an IPv6 membership.

This way you get only the methods each kind of membership can actually perform, and can only call to leave a membership if you've already joined one to begin with.

@sgwilym
Copy link
Contributor

sgwilym commented Feb 17, 2023

PR for multicasting opened: #17811

@ghost
Copy link

ghost commented Mar 1, 2023

My five cents on this issue. We need broadcast and multicast functionality for deno DatagramConn, and for node:dgram polyfill. Current implementation are completely different. For example, you can try my dirty implementation of Wake-on-Lan for deno with node polyfills, this code compatible for both runtimes, but in deno we get error Error: bind UNKNOWN 0.0.0.0, for obvious reasons, of course:

import { Buffer } from "node:buffer";
import { createSocket } from "node:dgram";

// https://github.com/denoland/deno_std/blob/main/bytes/concat.ts
export function concat(...buf: Uint8Array[]): Uint8Array {
  let length = 0;
  for (const b of buf) {
    length += b.length;
  }

  const output = new Uint8Array(length);
  let index = 0;
  for (const b of buf) {
    output.set(b, index);
    index += b.length;
  }

  return output;
}

export const createMagickPacket = (mac: string): Uint8Array => {
  const octets = mac.match(/[0-9a-fA-F]{2}/g);

  let packet = new Uint8Array(0x06).fill(0xff);
  const macArr = Uint8Array.from(octets!.map((octet) => parseInt(octet, 16)));

  for (let i = 0; i < 16; i++) {
    packet = concat(packet, macArr);
  }

  return packet;
};

export const wake = (macAddress: string) => {
  const magicPacket = Buffer.from(createMagickPacket(macAddress));

  const socket = createSocket("udp4");

  socket.on("error", (e) => console.error(e));
  socket.once("listening", () => socket.setBroadcast(true));

  return new Promise((resolve, reject) => {
    socket.send(
      magicPacket,
      0,
      magicPacket.length,
      9,
      "255.255.255.255",
      (err, res) => {
        const result = res == magicPacket.length;
        if (err) return reject(err);
        resolve(result);
        socket.close();
      },
    );
  });
};

wake('DEVICE_MAC_ADDRESS');

Similar problems occur with multicast using node:dgram. There are methods that are not implemented and throw described errors:

notImplemented("udp.UDP.prototype.setBroadcast");
And some other methods in that file.

In other words, I'm really looking forward to the broadcast/multicast implementation for Deno.listenDatagram and for the nodejs compatibility layer to speed up porting important packages from the nodejs ecosystem.

As one reason why this is needed is to implement functionality for the smart home, where multicast traffic is often used. For interact with mDNS (RFC 6762) or DNS-SD (RFC 6763) as example.

UPD: As @sgwilym suggested broadcasting is on by default when Deno.listendatagram is called, so the problem is only relevant for polyfill node:dgram, and it is not implemented there yet. So my message is not relevant within this issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
runtime Relates to code in the runtime crate suggestion suggestions for new features (yet to be agreed)
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants