How do I implement ICMP ping in Ruby using only the standard the socket library?
Asked Answered
K

2

9

It should be possible send and receive ICMP packets using the Ruby socket library but I do not see any good documentation on this.

I do not want to use net-ping, icmp, ping, and all of these other libraries that either fail because of cross-platform issues, require devkit and custom building, which fail during the build process, are neglected and have not been updated for a lengthy time, and/or are just in general buggy.

Does anyone have any good documentation on how to accomplish this? I want to send ICMP echo replies, not TCP or UDP packets.

Kinchen answered 19/1, 2012 at 22:42 Comment(3)
Does the ruby socket library allow specifying the ICMP protocol? It is likely closely based on "unix" sockets.Rooseveltroost
Well I can.. sock = Socket.new(Socket::PF_INET, Socket::SOCK_RAW, Socket::IPPROTO_ICMP)Kinchen
With the documentation however it is not clear where to go from there. There is plenty of documentation on how to make a UDP or TCP connection. Nothing on how to use the ICMP piece.Kinchen
K
5

Reading Daniel Berger's code on his Net-ping project I was able to see how he did it.

http://rubygems.org/gems/net-ping

Kinchen answered 21/1, 2012 at 23:29 Comment(0)
C
2

I recently dug this problem and wanted to make an self-contained answer. I use Linux or macOS in development and Linux in production.

In 2011, a patch was introduced to allow the creation of a socket where the kernel handles some ICMP stuff like providing an ID and computing the checksum with echo requests. It is also available for macOS. I made some tests with ICMP echo requests and replies.

Under macOS:

  • You get the IP header (20 bytes without options) with the ICMP echo reply but you send the ICMP echo request alone.
  • You have to compute the checksum yourself.

Under Linux, permissions must be set accordingly to allow users from a group to create the socket if not run as root. On macOS there is no need for it.

sysctl net.ipv4.ping_group_range='1000 1000'

Some code has been taken or adapted from net-ping. The book Unix Network Programming: The Sockets Networking Api (Volume 1, 3rd edition, by Stevens, Fenner and Rudoff, Addison-Wesley, 2004) is really interesting and in particular the chapter concerning raw sockets (chapter 28) and its sections 28.4 ("Raw Socket Input") and 28.5 ("ping Program").

#!/usr/bin/env ruby

require 'socket'

def bin_to_hex(s, sep = " ")
  s.each_byte.map { |b| "%02x" % b.to_i }.join(sep)
end

def checksum(msg)
  length = msg.length
  num_short = length / 2
  check = msg.unpack("n#{num_short}").sum
  if length % 2 > 0
    check += msg[length-1, 1].unpack1('C') << 8
  end
  check = (check >> 16) + (check & 0xffff)
  return (~((check >> 16) + check) & 0xffff)
end

def send_ping(socket, host, seq, data)
  id = 0
  checksum = 0
  icmp_packet = [8, 0, checksum, id, seq].pack('C2 n3') << data
  puts "icmp_packet bef checksum: #{bin_to_hex(icmp_packet)}"
  checksum = checksum(icmp_packet)
  icmp_packet = [8, 0, checksum, id, seq].pack('C2 n3') << data
  puts "icmp_packet aft checksum: #{bin_to_hex(icmp_packet)}"
  saddr = Socket.pack_sockaddr_in(0, host)
  socket.send(icmp_packet, 0, saddr)
  return icmp_packet
end

def receive_ping(socket, timeout)
  io_array = select([socket], nil, nil, timeout)
  if io_array.nil? || io_array[0].empty?
    return nil, nil
  end
  # length is either 12 bytes of ICMP alone or 20 bytes of IP header + 12 bytes of ICMP = 32 bytes
  # data = socket.recv(32) # IP header 20 + 12
  data = socket.recv(32)
  puts "received packet: #{bin_to_hex(data)}"
  rcvd_at = Time.now
  if data.size == 32
    if data.unpack1("C") == 0x45
      # We have an IP header
      offset = 20
    else
      # Looks like an IP header but it is not!
      return rcvd_at, nil
    end
  else
    # data.size == 12
    offset = 0
  end

  icmp_type, icmp_code = data[0 + offset, 2].unpack('C2')
  if icmp_type == 0 && icmp_code == 0
    echo_reply_id, echo_reply_seq = data[4 + offset, 4].unpack('n2')

    # Check if using a raw socket (SOCK_RAW)
    # Means we need sent id (and seq if we want to)
    # if id == echo_reply_id && seq == echo_reply_seq
      return rcvd_at, data[offset..]
    # end
  end
  return rcvd_at, nil
end

sock = Socket.open(Socket::PF_INET, Socket::SOCK_DGRAM, Socket::IPPROTO_ICMP)
# sock = Socket.open(Socket::PF_INET, Socket::SOCK_RAW, Socket::IPPROTO_ICMP)

# No need unless we use a raw socket
# id = Process.pid & 0xffff
seq = 1

sent_at = Time.now
sent_at_ms = (sent_at.hour * 3600 + sent_at.min * 60 + sent_at.sec) * 1000 + sent_at.tv_nsec / 1000000
sent = send_ping(sock, ARGV[0], seq, [sent_at_ms].pack("N"))
puts "sent icmp packet: #{bin_to_hex(sent)}"

# The loop is necessary in case of a raw socket because perhaps we did not receive a reply for our request
# loop do
  rcvd_at, rcvd = receive_ping(sock, 5000)
  if rcvd
    rcvd_at_ms  = (rcvd_at.hour * 3600 + rcvd_at.min * 60 + rcvd_at.sec) * 1000 + rcvd_at.tv_nsec / 1000000
    sent_at_ms = rcvd[8, 4].unpack1("N")
    latency = rcvd_at_ms - sent_at_ms
    puts "size: #{rcvd.size}, latency: #{latency}, rcvd icmp: #{bin_to_hex(rcvd)}"
    # break
  # else
    # puts "received bytes is not our reply"
  end
# end

sock.close

On macOS we get:

$ ./ping.rb google.com
icmp_packet bef checksum: 08 00 00 00 00 00 00 01 02 17 b3 5e
icmp_packet aft checksum: 08 00 42 89 00 00 00 01 02 17 b3 5e
sent icmp packet: 08 00 42 89 00 00 00 01 02 17 b3 5e
received packet: 45 60 0c 00 00 00 00 00 73 01 c2 10 ac d9 17 6e c0 a8 00 7d 00 00 4a 89 00 00 00 01 02 17 b3 5e
size: 12, latency: 29, rcvd icmp: 00 00 4a 89 00 00 00 01 02 17 b3 5e

On Debian 10:

$ ./ping.rb google.com
icmp_packet bef checksum: 08 00 00 00 00 00 00 01 02 18 e0 f9
icmp_packet aft checksum: 08 00 14 ed 00 00 00 01 02 18 e0 f9
sent icmp packet: 08 00 14 ed 00 00 00 01 02 18 e0 f9
received packet: 00 00 1c 9c 00 51 00 01 02 18 e0 f9
size: 12, latency: 14, rcvd icmp: 00 00 1c 9c 00 51 00 01 02 18 e0 f9

Note the difference between received packets. Note that we put current time in the packet in the form of the number of milliseconds since midnight (local time) and that we did not account for a request for which the reply is received the next day (e.g. sent at 23:59:59 and received at 00:00:01 the next day, 2sec later).

Some code is necessary if we use a raw socket.

  • Choose a unique id (process id)
  • Compute the checksum.
  • Check that the received ICMP echo reply is meant for our code by checking that the IDs match.
Carlisle answered 22/6, 2021 at 7:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.