应该可以使用 Ruby 套接字库发送和接收 ICMP 数据包,但我没有看到任何好的文档。
我不想使用 net-ping、icmp、ping 和所有这些由于跨平台问题而失败、需要 devkit 和自定义构建的库,这些库在构建过程中失败,被忽略且尚未更新很长一段时间,和/或只是一般的越野车。
有没有人有关于如何做到这一点的任何好的文档?我想发送 ICMP 回显回复,而不是 TCP 或 UDP 数据包。
阅读 Daniel Berger 在他的 Net-ping 项目中的代码,我能够看到他是如何做到的。
我最近挖了这个问题,想做一个独立的答案。我在开发中使用 Linux 或 macOS,在生产中使用 Linux。
2011 年,引入了一个补丁,允许创建一个套接字,内核在其中处理一些 ICMP 内容,例如提供 ID 和使用回显请求计算校验和。它也适用于 macOS。我对 ICMP 回显请求和回复进行了一些测试。
在 macOS 下:
在 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 秒后收到)。
如果我们使用原始套接字,则需要一些代码。