Pages

Monday, May 24, 2010

ICMP Ping in Erlang

(Also see ICMP Ping in Erlang, part 2)

ICMP ECHO Packet Structure


RFC 792 describes an ICMP ECHO packet as:
  • Type:8
  • Code:8
  • Checksum:16
  • Identifier:16
  • Sequence Number:16
  • Data1:8
  • ...
  • DataN:8

The number after the colon represents the number of bits in the field.
  • The type field for ICMP ECHO is set to 8. The response (ICMP ECHO REPLY) has a value of 0.
  • The code is 0.
  • The checksum is a one's complement checksum that covers both the ICMP header and the data portion of the packet. An Erlang version looks like:
    makesum(Hdr) -> 16#FFFF - checksum(Hdr).
    
    checksum(Hdr) ->
        lists:foldl(fun compl/2, 0, [ W || <<W:16>> <= Hdr ]).
    
    compl(N) when N =< 16#FFFF -> N;
    compl(N) -> (N band 16#FFFF) + (N bsr 16).
    compl(N,S) -> compl(N+S).
    
  • The identifier and sequence number allow clients on a host to differentiate their packets, for example, if multiple ping's are running. The client will usually increment the sequence number for each ICMP ECHO packet sent.
  • Data is the payload. Traditionally, it holds a struct timeval so the client can calculate the delay without having to maintain state, but any value can be used, such as the output of erlang:now/0. The remainder is padded with ASCII characters.
The description of an ICMP packet in Erlang is very close to the specification. For ICMP ECHO:
<<8:8, 0:8, Checksum:16, Id:16, Sequence:16, Payload/binary>>
The ICMP ECHO reply is the same packet returned, with the type field set to 0 and an updated checksum:
<<0:8, 0:8, Checksum:16, Id:16, Sequence:16, Payload/binary>>

Opening a Socket

Sending out ICMP packets requires opening a raw socket. Aside from the issues of having the appropriate privileges, Erlang does not have native support for handling raw sockets. I used procket to handle the privileged socket operations and pass the file descriptor into Erlang. Once the socket is returned to Erlang, we can perform operations on it as an unprivileged user. Since there isn't a gen_icmp module, we need some way of calling sendto()/recvfrom() on the socket. gen_udp uses sendto(), so we can misuse it (with some quirks) for our icmp packets.
% Get an ICMP raw socket
{ok, FD} = procket:listen(0, [{protocol, icmp}]),
% Use the file descriptor to create an Erlang socket structure
{ok, S} = gen_udp:open(0, [binary, {fd, FD}]),
The port is meaningless, so 0 is passed in as an argument. We create the packet payload twice: first with a zero'ed checksum, then with the results of the checksum.
make_packet(Id, Seq) ->
    {Mega,Sec,USec} = erlang:now(),
    Payload = list_to_binary(lists:seq(32, 75)),
    CS = makesum(<<?ICMP_ECHO:8, 0:8, 0:16, Id:16, Seq:16, Mega:32, Sec:32, USec:32, Payload/binary>>),
    <<
        8:8,    % Type
        0:8,    % Code
        CS:16,  % Checksum
        Id:16,  % Id
        Seq:16, % Sequence
        Mega:32, Sec:32, USec:32,   % Payload: time
        Payload/binary
    >>.
The packet can be sent via the raw socket using gen_udp:send/4, with the port again set to 0.
ok = gen_udp:send(S, IP, 0, Packet)
Since we're abusing gen_udp, we can wait for a message to be sent to the process:
receive
    {udp, S, _IP, _Port, <<_:20/bytes, Data/binary>>} ->
        {ICMP, <<Mega:32/integer, Sec:32/integer, Micro:32/integer, Payload/binary>>} = icmp(Data),
        error_logger:info_report([
            {type, ICMP#icmp.type},
            {code, ICMP#icmp.code},
            {checksum, ICMP#icmp.checksum},
            {id, ICMP#icmp.id},
            {sequence, ICMP#icmp.sequence},
            {payload, Payload},
            {time, timer:now_diff(erlang:now(), {Mega, Sec, Micro})}
        ]),
after
    5000 ->
        error_logger:error_report([{noresponse, Packet}])
end
In the above code snippet, you may have noticed the first 20 bytes of the payload is stripped off. Comparing the ICMP packet we sent and the response handed to the process by gen_udp:
icmp: <<8,0,186,30,80,228,0,0,0,0,4,250,0,12,16,77,0,1,69,0,32,33,34,35,36,
            37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,
            59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75>>
    response: <<69,0,0,84,101,155,64,0,64,1,154,44,192,168,220,187,192,168,220,
                212,0,0,194,30,80,228,0,0,0,0,4,250,0,12,16,77,0,1,69,0,32,33,
                34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,
                55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75>>
While the process sent a 64 byte ICMP packet, gen_udp hands it an 84 byte packet which includes the 20 byte IPv4 header. An example of an Erlang ping is included with procket on github. The example will just print out the packets using error_logger:info_report/1:
1> icmp:ping("192.168.213.1").

=INFO REPORT==== 24-May-2010::16:21:37 ===
    type: 0
    code: 0
    checksum: 52034
    id: 14837
    sequence: 0
    payload: <<" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJK">>
    time: 16790

No comments:

Post a Comment