четверг, 7 апреля 2011 г.

Erlang interface to low level socket operations

Procket is an Erlang socket interface which can be used for requesting
socket features that usually require superuser privileges.
procket uses the experimental NIF interface first introduced in Erlang
R13B03.
И его частный случай - ICMP Ping
описание в блоге автора

ICMP Ping in Erlang, part 2



ICMP Ping in Erlang, part 2

I've covered sending ICMP packets from Erlang using BSD raw sockets and Linux's PF_PACKET socket option.

gen_icmp tries to be a simple interface for ICMP sockets using the BSD raw socket interface for portability. It should work on both Linux and BSD's (I've tested on Ubuntu and Mac OS X).

SENDING PING'S

To ping a host:
1> gen_icmp:ping("erlang.org").
[{ok,{193,180,168,20},
     {{33786,0,129305},
      <<" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJK">>}}]
The response is a list of 3-tuples. The third element is a 2-tuple holding the ICMP echo request ID, the sequence number, the elapsed time and the payload.

A bad response looks like:
2> gen_icmp:ping("192.168.213.4").
[{{error,host_unreachable},
  {192,168,213,4},
  {{34491,0},
   <<69,0,0,84,0,0,64,0,64,1,14,220,192,168,213,119,192,
     168,213,4,8,0,196,...>>}}]
The argument to gen_icmp:ping/1 takes either a string or a list of strings. For example, to ping every host on a /24 network:
1> gen_icmp:ping([ {192,168,213,N} || N <- lists:seq(1,254) ]).
[{{error,host_unreachable},
  {192,168,213,254},
  {{54370,0},
   <<69,0,0,84,0,0,64,0,64,1,13,226,192,168,213,119,192,
     168,213,254,8,0,82,...>>}},
 {{error,host_unreachable},
  {192,168,213,190},
  {{54370,0},
   <<69,0,0,84,0,0,64,0,64,1,14,34,192,168,213,119,192,168,
     213,190,8,0,...>>}},
gen_icmp:ping/1 takes care of opening and closing the raw socket. This operation is somewhat expensive because Erlang is spawning a setuid executable to get the socket. If you'll be doing a lot of ping's, it's better to keep the socket around and use ping/3:
1> {ok,Socket} = gen_icmp:open().
{ok,<0.308.0>}

2> gen_icmp:ping(Socket, ["www.yahoo.com", "erlang.org", {192,168,213,1}],
    [{id, 123}, {sequence, 0}, {timeout, 5000}]).
[{ok,{193,180,168,20},
     {{123,0,126270},
      <<" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJK">>}},
     {ok,{69,147,125,65},
     {{123,0,29377},
      <<" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJK">>}},
     {ok,{192,168,213,1},
     {{123,0,3586},
      <<" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJK">>}}]

3> gen_icmp:close(Socket).
ok

CREATING OTHER ICMP PACKET TYPES


ICMP destination unreachable and time exceeded packets return at least the first 64 bits of the header and payload of the original packet. Here is an example of generating an ICMP port unreachable for a fake TCP packet sent to port 80.
-module(icmperr).
-export([unreachable/3]).
-include("epcap_net.hrl").

-define(IPV4HDRLEN, 20).
-define(TCPHDRLEN, 20).

unreachable(Saddr, Daddr, Data) ->
    {ok, Socket} = gen_icmp:open(),

    IP = #ipv4{
        p = ?IPPROTO_TCP,
        len = ?IPV4HDRLEN + ?TCPHDRLEN + byte_size(Data),
        saddr = Daddr,
        daddr = Saddr
    },

    TCP = #tcp{
        dport = 80,
        sport = crypto:rand_uniform(0, 16#FFFF),
        seqno = crypto:rand_uniform(0, 16#FFFF),
        syn = 1
    },

    IPsum = epcap_net:makesum(IP),
    TCPsum = epcap_net:makesum([IP, TCP, Data]),

    Packet = <<
        (epcap_net:ipv4(IP#ipv4{sum = IPsum}))/bits,
        (epcap_net:tcp(TCP#tcp{sum = TCPsum}))/bits,
        Data/bits
        >>,

    ICMP = gen_icmp:packet([
        {type, ?ICMP_DEST_UNREACH},
        {code, ?ICMP_UNREACH_PORT}
    ], Packet),

    ok = gen_icmp:send(Socket, Daddr, ICMP),
    gen_icmp:close(Socket).

To create the IPv4 and TCP headers, we make the protocol records and use the epcap_net module functions to encode the headers with the proper checksums. For creating the ICMP packet, we use the gen_icmp:packet/2 function (which again simply calls epcap_net).


ICMP PING TUNNEL


We can tunnel any data we like in the payload of an ICMP packet. In this example, we'll use ICMP echo requests to tunnel an ssh connection between 2 hosts. The ICMP echo replies sent back by the peer OS ensure the data was received, like the ACK in a TCP connection.

The tunnel exports 2 functions:

  • ptun:server(ClientAddress, LocalPort) -> void()

    Types   ClientAddress = tuple()
                LocalPort = 0..65534
    
        ClientAddress is the IPv4 address of the peer represented as a tuple.
    
        The server listens on LocalPort for TCP connections and will close
        the port after a TCP client connects.  Data received on this port
        will be sent to the peer as the payload of the ICMP packets.
    

  • ptun:client(ServerAddress, LocalPort) -> void()

    Types   ServerAddress = tuple()
                LocalPort = 0..65534
    
        ServerAddress is the IPv4 address of the peer.
    
        When the client receives an ICMP echo request, the client opens a
        TCP connection to the LocalPort on localhost, proxying the data.
    
-module(ptun).
-include("epcap_net.hrl").
-export([client/2, server/2]).
-define(TIMEOUT, 5000).
-define(PORT, 8787).
-record(state, {
        addr,
        port,
        is,
        ts,
        id,
        seq = 1
    }).
server(Addr, Port) ->
    {ok, ICMP} = gen_icmp:open(),
    {ok, Socket} = gen_tcp:listen(Port, [
            binary,
            {packet, 0},
            {active, true},
            {reuseaddr, true},
            {ip, {127,0,0,1}}
        ]),
    accept(Addr, ICMP, Socket).
client(Addr, Port) ->
    {ok, ICMP} = gen_icmp:open(),
    State = #state{
        addr = Addr,
        port = Port,
        is = ICMP,
        id = crypto:rand_uniform(0, 16#FFFF)
    },
    proxy(State).
accept(Addr, ICMP, Listen) ->
    {ok, Socket} = gen_tcp:accept(Listen),
    gen_tcp:close(Listen),
    State = #state{
        addr = Addr,
        is = ICMP,
        ts = Socket,
        id = crypto:rand_uniform(0, 16#FFFF)
    },
    [{ok, Addr, _}] = gen_icmp:ping(ICMP, [Addr], [
            {id, State#state.id},
            {sequence, 0},
            {timeout, ?TIMEOUT}
        ]),
    proxy(State).
proxy(#state{
        is = IS,
        ts = TS,
        addr = Addr,
        port = Port
    } = State) ->
    receive
        % TCP socket events
        {tcp, TS, Data} ->
            Seq = send(Data, State),
            proxy(State#state{seq = Seq});
        {tcp_closed, TS} ->
            ok;
        {tcp_error, TS, Error} ->
            {error, Error};
        % ICMP socket events
        % client: open a connection on receiving the first ICMP ping
        {icmp, IS, Addr,
            <<?ICMP_ECHO:8, 0:8, _Checksum:16, _Id:16, Seq:16, _Data/binary>>}
            when TS == undefined, Seq == 0 ->
            {ok, Socket} = gen_tcp:connect("127.0.0.1", Port, [binary, {packet, 0}]),
            error_logger:info_report([{connect, {{127,0,0,1},Port}}]),
            proxy(State#state{ts = Socket});
        {icmp, IS, Addr,
            <<?ICMP_ECHO:8, 0:8, _Checksum:16, _Id:16, _Seq:16, Len:16, Data/binary>>} ->
            <<Data1:Len/bytes, _/binary>> = Data,
            ok = gen_tcp:send(TS, Data1),
            proxy(State#state{ts = TS});
        {icmp, IS, Addr, Packet} ->
            error_logger:info_report([{dropping, Packet},{address, Addr}]),
            proxy(State)
    end.
% To keep it simple, we use 64 byte packets
% 4 bytes header, 2 bytes type, 2 bytes code, 12 bytes timestamp, 2 bytes data length, 42 bytes data
send(<<Data:42/bytes, Rest/binary>>, #state{is = Socket, addr = Addr, id = Id, seq = Seq} = State) ->
    [{ok, Addr, _}] = gen_icmp:ping(Socket, [Addr], [
            {id, Id},
            {sequence, Seq},
            {timeout, ?TIMEOUT},
            {data, <<(byte_size(Data)):16, Data/bytes>>}
        ]),
    send(Rest, State#state{seq = Seq + 1});
send(Data, #state{is = Socket, addr = Addr, id = Id, seq = Seq}) ->
    Len = byte_size(Data),
    [{ok, Addr, _}] = gen_icmp:ping(Socket, [Addr], [
            {id, Id},
            {sequence, Seq},
            {timeout, ?TIMEOUT},
            {data, <<Len:16, Data/bytes, 0:((42-Len)*8)>>}
        ]),
    Seq+1.
view rawptun.erlThis Gist brought to you by GitHub.
To start the tunnel, you'll need 2 hosts. In this example, 192.168.213.7 is the client and 192.168.213.119 is the server. 192.168.213.7 forwards any tunnelled data it receives to a local SSH server. On 192.168.213.7:
1> ptun:client({192,168,213,119}, 22).
On 192.168.213.119:
1> ptun:server({192,168,213,7}, 8787).
Open another shell and start an SSH connection to port 8787 on localhost:
ssh -p 8787 127.0.0.1
<...>
$ ifconfig eth0 | awk '/inet addr/{print $2}'
addr:192.168.213.7
Оригинал 

ICMP Ping in Erlang Part 1



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 withprocket 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

Оригинал