17

Is it possible to use ICMP sockets under the IP protocol? Maybe something like:

socket(PF_INET, <type>, IPPROTO_ICMP)?

What should I put in the <type> field? I saw some examples using SOCK_RAW, but won't that prevent the OS from doing his job handling the IP protocol?

And another thing. How can the OS know to which process he should send the ICMP datagrams, since there are no ports involved with the protocol?

Ludovic Kuty
  • 4,868
  • 3
  • 28
  • 42
Kei Nivky
  • 367
  • 2
  • 5
  • 12

2 Answers2

38

Linux have a special ICMP socket type you can use with:

  socket(PF_INET, SOCK_DGRAM, IPPROTO_ICMP);

This allows you to only send ICMP echo requests The kernel will handle it specially (match request/responses, fill in the checksum).

This only works if a special sysctl is set. By default not even root can use this kind of socket. You specify the user groups that can access it. To allow root (group 0) to use ICMP sockets, do:

 sysctl -w net.ipv4.ping_group_range="0 0"

Here is an example program to demonstrate the very basic usage of sending an ICMP echo request:

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip_icmp.h>
#include <arpa/inet.h>
#include <sys/select.h>

//note, to allow root to use icmp sockets, run:
//sysctl -w net.ipv4.ping_group_range="0 0"

void ping_it(struct in_addr *dst)
{
    struct icmphdr icmp_hdr;
    struct sockaddr_in addr;
    int sequence = 0;
    int sock = socket(AF_INET,SOCK_DGRAM,IPPROTO_ICMP);
    if (sock < 0) {
        perror("socket");
        return ;
    }

    memset(&addr, 0, sizeof addr);
    addr.sin_family = AF_INET;
    addr.sin_addr = *dst;

    memset(&icmp_hdr, 0, sizeof icmp_hdr);
    icmp_hdr.type = ICMP_ECHO;
    icmp_hdr.un.echo.id = 1234;//arbitrary id

    for (;;) {
        unsigned char data[2048];
        int rc;
        struct timeval timeout = {3, 0}; //wait max 3 seconds for a reply
        fd_set read_set;
        socklen_t slen;
        struct icmphdr rcv_hdr;

        icmp_hdr.un.echo.sequence = sequence++;
        memcpy(data, &icmp_hdr, sizeof icmp_hdr);
        memcpy(data + sizeof icmp_hdr, "hello", 5); //icmp payload
        rc = sendto(sock, data, sizeof icmp_hdr + 5,
                        0, (struct sockaddr*)&addr, sizeof addr);
        if (rc <= 0) {
            perror("Sendto");
            break;
        }
        puts("Sent ICMP\n");

        memset(&read_set, 0, sizeof read_set);
        FD_SET(sock, &read_set);

        //wait for a reply with a timeout
        rc = select(sock + 1, &read_set, NULL, NULL, &timeout);
        if (rc == 0) {
            puts("Got no reply\n");
            continue;
        } else if (rc < 0) {
            perror("Select");
            break;
        }

        //we don't care about the sender address in this example..
        slen = 0;
        rc = recvfrom(sock, data, sizeof data, 0, NULL, &slen);
        if (rc <= 0) {
            perror("recvfrom");
            break;
        } else if (rc < sizeof rcv_hdr) {
            printf("Error, got short ICMP packet, %d bytes\n", rc);
            break;
        }
        memcpy(&rcv_hdr, data, sizeof rcv_hdr);
        if (rcv_hdr.type == ICMP_ECHOREPLY) {
            printf("ICMP Reply, id=0x%x, sequence =  0x%x\n",
                            icmp_hdr.un.echo.id, icmp_hdr.un.echo.sequence);
        } else {
            printf("Got ICMP packet with type 0x%x ?!?\n", rcv_hdr.type);
        }
    }
}

int main(int argc, char *argv[])
{
    if (argc != 2) {
        printf("usage: %s destination_ip\n", argv[0]);
        return 1;
    }
    struct in_addr dst;

    if (inet_aton(argv[1], &dst) == 0) {

        perror("inet_aton");
        printf("%s isn't a valid IP address\n", argv[1]);
        return 1;
    }

    ping_it(&dst);
    return 0;
}

Note that the kernel will reject and fail the sendto() call if the data sent does not have room for a proper ICMP header, and the ICMP type must be 8 (ICMP_ECHO) and the ICMP code must be 0.

Ludovic Kuty
  • 4,868
  • 3
  • 28
  • 42
nos
  • 223,662
  • 58
  • 417
  • 506
  • Wouldn't you socket creation be wrong? SOCK_DRAM is part of the Transport layer of the OSI/ISO 7 layer stack... ICMP is part of layer 3 in the network layer. I've been reading Unix Socket Programming by Stevens and in order to do this you need to declare a SOCK_RAW in order to get icmp packets. check out this code [here](http://www.binarytides.com/packet-sniffer-code-c-linux/) – Jacob Bryan Feb 10 '15 at 04:23
  • 3
    @Florida_Jake SOCK_DGRAM is not tied to layer 7. The Unix Socket Programming book doesn't describe how to use the linux specific ICMP echo sockets, the example here works. The traditional way of sending/receiving ICMP messages is indeed to use SOCK_RAW, and build up the ICMP messages yourself. The code I posted uses an alternative, linux specific feature to send and receive ICMP echo messages. – nos Feb 10 '15 at 08:17
  • Nice, worked great for me to quickly get a result. Now is the time to look at IPv6 ping. :) – Pavel Šimerda Jul 03 '15 at 21:22
  • I found the sysctl mention here very helpful, I was trying to make this kind of socket as root and getting permission denied and was like wtf! – Kenneth Wilke Apr 20 '16 at 16:41
  • 2
    I just adapted this code to a higher-level language, and it works great. The only clarification I would add is that there is no need to assign `icmp_hdr.un.echo.id` to anything. Linux will choose a identifier on its own. According to [the patch](https://lwn.net/Articles/420800/): "The id is set to the number (local port) of the socket, the checksum is always recomputed." – Andrew Thaddeus Martin Dec 14 '18 at 20:14
10

Yes it is possible, since the ping command does ICMP.

To find out the syscalls involved, you can strace that command (under root).

You could also glance into that command's source code, e.g. Debian's ping

And there is the liboping library to help you...

Basile Starynkevitch
  • 223,805
  • 18
  • 296
  • 547