如何仅使用标准套接字库在Ruby中实现ICMP ping?

时间:2012-01-19 22:42:18

标签: ruby ping icmp

应该可以使用Ruby套接字库发送和接收ICMP数据包,但我没有看到任何关于此的良好文档。

我不想使用net-ping,icmp,ping,以及因跨平台问题而失败的所有其他库,需要devkit和自定义构建,在构建过程中失败,被忽略并且没有经过很长时间的更新,和/或只是一般的马车。

有没有人有关于如何完成此任务的任何好文档?我想发送ICMP回应回复,而不是TCP或UDP数据包。

2 个答案:

答案 0 :(得分:2)

在他的Net-ping项目中阅读Daniel Berger的代码,我能够看到他是如何做到的。

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

答案 1 :(得分:0)

我最近挖了这个问题,想做一个自成一体的答案。我在开发中使用 Linux 或 macOS,在生产中使用 Linux。

在 2011 年,a patch was introduced 允许创建一个套接字,内核在其中处理一些 ICMP 内容,例如提供 ID 和使用回显请求计算校验和。它也适用于 macOS。我用 ICMP 回显请求和回复做了一些测试。

在 macOS 下:

  • 您通过 ICMP 回显回复获得 IP 标头(20 个字节,不带选项),但您单独发送 ICMP 回显请求。
  • 您必须自己计算校验和。

在 Linux 下,必须相应地设置权限以允许来自组的用户在未以 root 运行的情况下创建套接字。在 macOS 上不需要它。

sysctl net.ipv4.ping_group_range='1000 1000'

某些代码已从 net-ping 中提取或改编。 Unix Network Programming: The Sockets Networking Api 书(第 1 卷,第 3 版,Stevens、Fenner 和 Rudoff,Addison-Wesley,2004 年)非常有趣,特别是有关原始套接字的章节(第 28 章)及其第 28.4 节(“原始套接字输入”)和 28.5(“ping 程序”)。

#!/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

在 macOS 上我们得到:

$ ./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

在 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

注意接收到的数据包之间的区别。请注意,我们以自午夜(当地时间)以来的毫秒数形式将当前时间放入数据包中,并且我们没有考虑第二天收到回复的请求(例如,在 23:59:59 发送并在第二天 00:00:01 收到,2 秒后)。

如果我们使用原始套接字,则需要一些代码。

  • 选择一个唯一的 ID(进程 ID)
  • 计算校验和。
  • 通过检查 ID 是否匹配来检查收到的 ICMP 回显回复是否适用于我们的代码。