Gen_icmp - PoC wrapper over `socket` to be able to send ICMP requests (ping)

Some people was already asking how to implement ping in their Erlang/Elixir applications. Previously it wasn’t really possible, because there was no access to the BSD sockets API without using external NIFs. With OTP 21 we got a built-in NIF with access for these, and with later updates we became allowed to use any type/protocol combination, not only these “allowed” by the Erlang team.

With all that work I wanted to present an example implementation of gen_tcp/gen_udp-like server that would provide higher-level API for one of the available protocols. That is how gen_icmp was born, which, obviously, implement Internet Control Message Protocol which is used by tools like ping.

It is currently not available on Hex, as I want to have tests first, however that may be a little bit complex, as it is not commonly accessible protocol (it requires raw sockets which are accessible only by root, with exception for Linux machines and Darwin, where it can be used on top of datagram sockets).

Usage is quite simple, to send simple ECHOREQ message (also known as PING) and receive response from 1.1.1.1 (CloudFlare DNS servers):

{:ok, socket} = :gen_icmp.open() # require macOS or Linux with user being in group that is within `net.ipv4.ping_group_range`

:ok = :gen_icmp.echoreq(socket, {1, 1, 1, 1}, <<1,2,3,4>>)

receive do
  {:icmp, socket, {1,1,1,1}, {echorep, %{data: <<1,2,3,4>>}}} ->
    IO.puts("Echo received")
end

For some reason the message need to be padded to 16 bits (it has to have even number of bytes in the message and I am trying to find out why and how to mitigate that).

12 Likes

I REALLY like the direction you are going with this. Just a couple of days ago I was about to implement something similar for my own needs. However, you have knocked off about 80% of what I need! Thanks! (Thanks also for implementing my request to bind to a specific interface!)

The only thing I am not sure that I like is that lack of negative acknowledgements (ie ping NOT received in time), especially as it potentially affects the API. Now I think in the past your thought was to leave this up to an outer wrapper, but as I see that, this means creating another process to filter through, which seems inefficient compared with implementing inside the library?

My use case is:

  • I have a very slow (iridium satellite) connection to monitor.
  • I need to decide if this is up/down to certain destinations
  • However, because it’s so slow, queues can develop on the interfaces, 5+second ping times are not uncommon in light use, you could have huge peaks
  • So it would be desirable to have a) multiple pings in flight (ie set a sequence number) b) to set quite a long timeout as some of these may turn up really quite a long time later (I don’t mind if they take 60 seconds to come home)
  • It would also remove a bunch of complexity to have the library report that packets are overdue
  • I’m undecided if it would be useful to report “arrived late” events though? Probably someone would find it useful? Seems to drop out of the code for free on linux at least?
  • I would also prefer that in most modes I only hear about replies to my own pings, ie on OSX I don’t also get all the icmp data. However, clearly you also desire to implement an “all icmp” mode as well?

The way I would see this working is that echorequest() takes an optional timeout. If this is given then internally we maintain a queue of requests in flight and their expected latest arrival time. In handle(:continue) we would need to set a wakeup timer for the earliest expiry time. If we arrive in the timer event then not reply was received, so we can remove the item from the queue and send a message. Obviously if a reply arrives we need to dequeue the request and cancel/update the timer.

I guess this would also imply that opening the socket needs to take a param as to whether you want all events or only icmpecho (I’m mostly thinking because this affects how you open the socket on linux? We can use a different socket type if we want the kernel to do the reply processing for us?)

If I’m honest, I find it hard work to speak Erlang, which makes it somewhat tricky for me to contribute code here. If you liked the idea above, would you accept a donation for an implementation!? PM me.

1 Like

You can achieve that right now. Just set seq and id in the request.

gen_icmp do not care about timeouts, because there is no need to. It is quite low level library, that do not manage whole ping on its own, and instead make it up to the user. There is no timeouts handled automatically by this library, it just sends and receives packets, without any additional magic.

As I said earlier, it is very “low level” library, so I do not think I should do any filtering in the library (unless there are some flags that can be set to the socket, but I am not aware of such). If you want to filter unwanted messages in your process, then just discard them in the controlling process.

If you have an example (can be in any language) how that does work, then please share. I can look into it, but no promises, as I can be limited to the capabilities of the socket module.

2 Likes

Answering the easiest first:

I think (untested) filtering happens automatically on linux if we open the socket in dgram, icmp mode? I haven’t retested your library, but I thought that was how you were opening the socket on linux? So in that mode the socket won’t receive random other ICMP noticed on the network. I think this is helpful for many use cases

So regarding the low level library aspect. Yes, I agree. However, do you not agree that a frequent use case is also the absence of receiving a message within a time period? (Doesn’t mean it won’t arrive later of course)

Now, it’s not hard to wrap your library in another genserver, which simply wraps it, starts a timer and relays all the messages from your genserver through our genserver and onto the user. However, I think if you consider that it leads to a moderately amount of duplication just to pass through the same API and means another genserver which is simply relaying messages. Hence the request, would you consider adding within your genserver the additional parts to set timers and report an extra message in the absence of the icmp message arriving?

I could make a similar argument for the usefulness of filtering in the library. There could be a lot of icmp messages in some setups, so the further forward we could push the filtering the better. It’s even worth pondering how to end up at active:once in the library?

What do you think?

1 Like

In that case it is supported already. gen_icmp:open/1 accepts raw option to use raw sockets, by default datagram sockets are used. So it is working right now.

In my opinion it would be infeasible to provide such functionality as gen_icmp:echoreq/3 is asynchronous. It would make sense to have timeout if that would be synchronous command (similarly to gen_server:call). So due to that I do not think that there is a reasonable way to implement that. Also as I said, it is meant to be very low-level interface, and adding such complexity there would be unneeded maintenance burden, especially that it isn’t that hard to implement on the user-side (it is mostly Process.send_after/3 with some map for storing references). It would also mean that I would need to modify the data sent in the ICMP request to contain reference ID and stuff like that. I do not want to complicate the implementation for something like that.

Yes, but making such option in the library could make the implementation quite significantly more complex, as if I would add the filtering on message type, then someone would like filtering on sender IP, message content, etc. As there is a lot of different messages, it would make the implementation more and more complex with each message type. And even if I would add new option to pass filtering function, then I do not think that would be much of the improvement about performance there.

It may me an option. As it started as PoC I created minimal implementation, but I could add active option as well as gen_icmp:recv/{1,2} functions, to manually handle incoming messages. That shouldn’t be too hard to implement.

1 Like

Hi, I think we must be talking past each other? I don’t really understand why you say “it would have to be a synchronous command”?

My confusion is because the actual response if it arrives is async? So why can we not have an async “hasn’t arrived after 2 seconds” response? I confess that I see no difference?

That said, I happen to think that a sync interface may well be very useful for many use cases? This seems trivial to implement using the reply feature of genserver where you can respond to the call from a different point in the code.

BTW: You must surely know of this library, but just adding this here for completeness:
GitHub - msantos/gen_icmp: Erlang interface to ICMP sockets

It seems complete in most ways, except that it only uses raw sockets (I want to avoid those due to the need to run as root, prefer to use the kernel filtering of dgram/icmp in the linux kernel)

What should happen if the gen_icmp is on another node than the controlling process and it unexpectedly goes down? There is no way to prove absence of the message. Additionally imagine situation like:

{:ok, socket} = :gen_icmp.open()

:gen_icmp.echoreq(socket, ip, "abba")
:gen_icmp.echoreq(socket, ip, "abba")

And there is message coming back only for one of these? How could I differentiate between them and remove respective timeout? There is no simple answer to any of these. To implement such functionality I would need to alter user-provided message to contain data that I could use to store reference returned by Process.send_after/3, and that is no-go for my implementation.

The difference is that current implementation of gen_icmp do not care at all whether, when, or even if the echorep message will ever arrive, it is as a whole left up to the user to handle that.

I think so as well, but it is not-goal for gen_icmp which is meant to provide such higher-level features. These are left up to the user or authors of higher-level libraries that will be built on top of the gen_icmp.

Yes, I am aware. I will probably extract some of the features of this library to my implementation, but for sure ping/{1,2,3} will not be part of my API at all. The goal there is to always leave that functionality up to the authors of the higher-level APIs and library users. I see that they have filtering options available there, and these options work on the OS level, this may be something that I would like to add to my implementation as well.