I have yet to find a working example of this, so I am obligated to post one. Especially considering the frustration I and a colleague more intelligent than I had to undergo before we found a working solution.
Please note that you will not get away with using an old version of OTP. IPv6 Multicast joins were broken for a long time.
Here is a simple script that will listen for a single packet, print it out, and exit.
require Logger
address={0xff1e, 0, 0, 0, 0, 0, 0x70, 0x25}
iface=0
{:ok, socket} = :gen_udp.open(8208, [
{:ip, address},
{:add_membership, {address, iface}},
{:debug, true},
:binary,
{:reuseaddr, true},
{:recbuf, 8388608},
])
Logger.info("UDP socket opened successfully #{inspect(socket)}")
{:ok, message} = :gen_udp.recv(socket, 9)
Logger.info("Received message: #{inspect(message)}")
An interface index of 0 works for global multicast addresses. Your kernel should figure it out based on the routing table. For a link-local or unique-local address, you will need to set that to your actual interface index.
NOTE THE ORDER OF THE OPTIONS
If you move :add_membership lower down, you will get a nonsensical eaddrinuse error. Yeah, that took a while to figure out. The other options can be moved around, but add_membership needs to be up towards the top. We assume this is because the options are processed in the order in which they are provided instead of the order of which they should be.
So let's move on to the gen_server implementation to expose the next gotcha.
defmodule Listen do
use GenServer
require Logger
@address {0xff1e, 0, 0, 0, 0, 0, 0x70, 0x25}
@iface 0
#@address {224,0,0,1}
#@iface {0,0,0,0}
def start_link(_) do
GenServer.start_link(__MODULE__, {}, name: __MODULE__)
end
def init(_opts) do
{:ok, socket} = :gen_udp.open(8208, [
{:ip, @address},
{:add_membership, {@address, @iface}},
{:debug, true},
:binary,
{:reuseaddr, true},
{:recbuf, 8388608},
{:active, true}
])
Logger.info("UDP socket opened successfully #{inspect(socket)}")
{:ok, %{socket: socket}}
end
def handle_info(msg, state) do
Logger.info("Received message: #{inspect(msg)} #{inspect(state)}")
{:noreply, state}
end
end
Listen.start_link([])
Process.sleep(:infinity)
Here, you must add {:active, true}, or handle_info() will never be called. Elixir will just leave the packets in the network queue. This is not true for IPv4. I left in the commented out IPv4 options if you want to prove this to yourself.
I would be remiss if I didn't provide you a command to test these.
echo "Elixir sucks" > /dev/udp/ff1e::70:25/8208
Before I leave, I will urge you to hold off on using elixir for important things. It is an immature pet project with poor performance, and you will be pressed to find engineers well versed in it who can maintain it with any efficiency.