Scope id -> interface number
In IPv6, there is a concept of a scope_id of an address that is supposed to indicate a context for the IP address, and generally just means what interface it is reachable on. While scopes have OS specific names, each just translates to an interface number, with 0 usually meaning the system's default.
In IPv6 multicast, this scope_id is provided directly to IP_ADD_MEMBERSHIP and IP_MULTICAST_IF instead of providing an ip associated with the interface as IPv4 does.
wrapper smoothing of v6's differences
node (via libuv) hides this difference for you in addMembership
by looking up the scope_id from the "interface address" you provide.
Unfortunately, starting from just an IP and getting a scope doesn't make a lot of sense (the whole point of the scope is that the IP could have different uses in different scopes.) So libuv is only able to fill in a scope if you explicitly provide it at the end of the address, using the %[scope]
format.
Using Addresses with Explicit Scopes
The way around this seems to be:
sock.bind('36912', '::', () => {
sock.addMembership('ff02::1:3', '::%eth2');
...
sock.send(buf, 0, buf.length, 36912, 'ff02::1:3%eth2');
Where:
using ::
(or no address) in bind is necessary since you are combining receive which will filter the multicast address on this with send which needs a normal address.
using %[iface#]
forces the scope of this interface #.
The second argument of addMembership could really start with any address since we are forcing the scope and the rest is discarded.
Usually the send socket is separated and given a different port or an anonymous port as you are either limited in what you can configure or in danger of getting EADDRINUSE errors for having sockets that are too similar.