Pages

Saturday, May 29, 2010

Raw Socket Programming in Erlang: Reading Packets Using PF_PACKET

BSD has BPF, Solaris has DLPI and Linux, well, has had many interfaces. The latest uses a linux specific protocol family, PF_PACKET. PF_PACKET can receive whole packets from the network as well as generate them, like a combination of BPF and the BSD raw socket interface.

PCAP is an abstraction over these different interfaces. epcap uses a system process linked to the PCAP library to read ethernet frames and send them as messages into Erlang using the port interface. Using procket with the PF_PACKET socket option, I've been playing with reading packets directly off the network and generating them as well.

The PF_PACKET interface is used by passing options to socket().
int socket(int domain, int type, int protocol);
  • The protocol family is, of course, PF_PACKET.
  • The type may be either SOCK_RAW or SOCK_DGRAM. SOCK_RAW will return the whole packet, including the ethernet header. A process sending a packet must prepend a link layer header. A socket with type SOCK_DGRAM will strip off the link layer header and generate a valid header for outgoing packets.
  • The protocol is selected from one of the values in linux/if_ether.h.
    #define ETH_P_IP    0x0800
    #define ETH_P_ALL   0x0003
    
    ETH_P_ALL will retrieve all network packets and ETH_P_IP just the IP packets. The values are in host-endian format and will need to be converted to network byte order before being used as arguments to socket().

Receving Packets using recvfrom()

To send and receive packets from a socket using PF_PACKET, the normal connection-less socket operations are used: sendto() and recvfrom(). By default, socket operations will block, unless the O_NONBLOCK flag is set using fcntl(). The gen_udp module in Erlang internally calls recvfrom(), so it can deal with raw sockets. Another example of using gen_udp in this way is for sending ICMP packets and reading the ICMP ECHO replies. Alternatively, I've added a recvfrom/2 function to the procket NIF for testing.

Sniffing Packets in Erlang

To read packets from the network device, either gen_udp or the NIF recvfrom/2 can be used. Using gen_udp:
-module(packet).
-export([sniff/0]).

-define(ETH_P_IP, 16#0008).
-define(ETH_P_ALL, 16#0300).

sniff() ->
    {ok, S} = procket:listen(0, [
            {protocol, ?ETH_P_ALL},
            {type, raw},
            {family, packet}
        ]),
    {ok, S1} = gen_udp:open(0, [binary, {fd, S}, {active, false}]),
    loop(S1).

loop(S) ->
    Data = gen_udp:recv(S, 2048),
    error_logger:info_report([{data, Data}]),
    loop(State).
The definitions of ETH_P_IP and ETH_P_ALL are in big endian format. The port is irrelevant and is set to 0 in procket:listen/2. The type is raw but can also be set to dgram. Using the NIF, the process must poll the socket. procket:recvfrom/2 will return the atom nodata if the socket returns EAGAIN; the tuple {ok, binary} with the binary data representing the packet or a tuple holding the value of errno, e.g., {error, {errno, strerror(errno)}}. The return values will probably change in the future.
sniff() ->
    {ok, S} = procket:listen(0, [
            {protocol, ?ETH_P_ALL},
            {type, raw},
            {family, packet}
        ]),
    loop(S).

loop(S) ->
    case procket:recvfrom(S, 2048) of
        nodata ->
            timer:sleep(1000),
            loop(S);
        {ok, Data} ->
            error_logger:info_report([{data, Data}]),
            loop(S);
        Error ->
            error_logger:error_report(Error)
   end.

No comments:

Post a Comment