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.
sock = Socket.new(Socket::PF_INET, Socket::SOCK_RAW, Socket::IPPROTO_ICMP)
– Kinchen