icmp4em 0.0.2 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +4 -0
- data/Gemfile.lock +28 -0
- data/LICENSE.txt +22 -0
- data/README.rdoc +82 -0
- data/Rakefile +1 -0
- data/bin/icmp-proxy +30 -0
- data/lib/icmp4em.rb +16 -6
- data/lib/icmp4em/icmp_handler.rb +21 -0
- data/lib/icmp4em/manager.rb +102 -0
- data/lib/icmp4em/packet.rb +106 -0
- data/lib/icmp4em/proxy/icmp_handler.rb +43 -0
- data/lib/icmp4em/proxy/request.rb +46 -0
- data/lib/icmp4em/proxy/udp_handler.rb +60 -0
- data/lib/icmp4em/request.rb +42 -0
- data/lib/icmp4em/timeout.rb +4 -0
- data/lib/icmp4em/udp_handler.rb +20 -0
- data/lib/icmp4em/version.rb +3 -0
- metadata +98 -53
- data/README +0 -60
- data/examples/simple_example.rb +0 -22
- data/examples/stateful_example.rb +0 -22
- data/lib/icmp4em/common.rb +0 -140
- data/lib/icmp4em/handler.rb +0 -53
- data/lib/icmp4em/icmpv4.rb +0 -173
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
icmp4em (1.0.0)
|
5
|
+
eventmachine (>= 1.0.0)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
diff-lcs (1.2.4)
|
11
|
+
eventmachine (1.0.3)
|
12
|
+
rspec (2.13.0)
|
13
|
+
rspec-core (~> 2.13.0)
|
14
|
+
rspec-expectations (~> 2.13.0)
|
15
|
+
rspec-mocks (~> 2.13.0)
|
16
|
+
rspec-core (2.13.1)
|
17
|
+
rspec-expectations (2.13.0)
|
18
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
19
|
+
rspec-mocks (2.13.1)
|
20
|
+
yard (0.8.6.1)
|
21
|
+
|
22
|
+
PLATFORMS
|
23
|
+
ruby
|
24
|
+
|
25
|
+
DEPENDENCIES
|
26
|
+
icmp4em!
|
27
|
+
rspec
|
28
|
+
yard
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Norman Elton
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
= ICMP Library for EventMachine
|
2
|
+
|
3
|
+
== Summary
|
4
|
+
|
5
|
+
This gem allows you to send ICMP echo requests and handle ICMP echo replies.
|
6
|
+
|
7
|
+
|
8
|
+
== Features
|
9
|
+
|
10
|
+
Version 0.2.0 supports:
|
11
|
+
|
12
|
+
* Sending and receiving ICMP echo requests / replies
|
13
|
+
* A configurable timeout / retry count
|
14
|
+
* A separate proxy process that, running as root, can send ICMP packets on behalf of other non-root processes. See below.
|
15
|
+
|
16
|
+
Future revisions of this library will support:
|
17
|
+
|
18
|
+
* Ruby fibers
|
19
|
+
|
20
|
+
|
21
|
+
== Simple Example
|
22
|
+
|
23
|
+
EM.run {
|
24
|
+
manager = ICMP4EM::Manager.new
|
25
|
+
|
26
|
+
request = manager.ping "8.8.8.8"
|
27
|
+
|
28
|
+
request.callback { puts "SUCCESS" }
|
29
|
+
request.errback { |e| puts "FAILURE, got error #{e}" }
|
30
|
+
}
|
31
|
+
|
32
|
+
|
33
|
+
== ICMP Proxy
|
34
|
+
|
35
|
+
Typically, sending ICMP packets requires a process to have root privileges. This is often less than ideal. Work arounds are also less than wonderful, often involving sending a TCP or UDP packet in the hopes of receiving a response.
|
36
|
+
|
37
|
+
This library supports the notion of an ICMP proxy via the included +icmp-proxy+ script. Running as root, it accepts UDP requests from non-root processes to handle the sending of ICMP packets.
|
38
|
+
|
39
|
+
With the +icmp-proxy+ running on the local host, it's trivial to configure:
|
40
|
+
|
41
|
+
manager = ICMP4EM::Manager.new(:proxy => true)
|
42
|
+
|
43
|
+
*Note* - The +icmp-proxy+ must run as root, and accepts incoming connections. By default, it binds to the localhost and will not accept connections from other hosts. This is configurable (run the proxy with +--help+ to see options). While it is possible to accept requests from other hosts, any root-owned process accepting packets from the Internet is an inherent security risk. Help improve the proxy by familiarizing yourself with its code before trusting it to the Internet.
|
44
|
+
|
45
|
+
|
46
|
+
== Running tests
|
47
|
+
|
48
|
+
A simple rspec test is included.
|
49
|
+
|
50
|
+
|
51
|
+
== Configuration parameters
|
52
|
+
|
53
|
+
The following parameters may be passed to the ICMP4EM::Manager constructor (to affect all pings) or as arguments following the IP address when pinging an individual host:
|
54
|
+
|
55
|
+
* *timeout* - Number of seconds to wait for a response
|
56
|
+
* *retries* - Number of retries before eventually failing
|
57
|
+
|
58
|
+
The following parameter may be passed to the ICMP4EM::Manager constructor to enable use of the ICMP proxy (see above):
|
59
|
+
|
60
|
+
* *proxy* - Set to <tt>true</tt> to use a proxy on the localhost, or <tt>host:port</tt> to use a proxy on another host.
|
61
|
+
|
62
|
+
|
63
|
+
== Acknowledgements
|
64
|
+
|
65
|
+
* The previous library, https://github.com/jakedouglas/icmp4em
|
66
|
+
* EventMachine[http://rubyeventmachine.com], by Francis Cianfrocca and Aman Gupta
|
67
|
+
* All the helpful folks on the Freenode #eventmachine channel
|
68
|
+
|
69
|
+
|
70
|
+
== Change Log
|
71
|
+
|
72
|
+
Version 0.2.0:
|
73
|
+
|
74
|
+
* ICMP proxy support
|
75
|
+
|
76
|
+
Version 0.1.0:
|
77
|
+
|
78
|
+
* First import
|
79
|
+
|
80
|
+
== Credits
|
81
|
+
|
82
|
+
Author: Norman Elton mailto:normelton@gmail.com
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/icmp-proxy
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "icmp4em"
|
4
|
+
require "optparse"
|
5
|
+
require "pp"
|
6
|
+
|
7
|
+
options = {:bind_host => "127.0.0.1", :bind_port => 63312}
|
8
|
+
|
9
|
+
OptionParser.new do |opts|
|
10
|
+
opts.on("-d", "--debug", "Debug mode") do |v|
|
11
|
+
$debug = true
|
12
|
+
end
|
13
|
+
|
14
|
+
opts.on("-h", "--host [HOST]", "Bind to a specific host") do |v|
|
15
|
+
options[:bind_host] = v
|
16
|
+
end
|
17
|
+
|
18
|
+
opts.on("-p", "--port [PORT]", "Bind to a specific port") do |v|
|
19
|
+
options[:bind_port] = v.to_i
|
20
|
+
end
|
21
|
+
end.parse!
|
22
|
+
|
23
|
+
EventMachine.run do
|
24
|
+
@pending_requests = {}
|
25
|
+
@icmp_socket = Socket.new(Socket::PF_INET, Socket::SOCK_RAW, Socket::IPPROTO_ICMP)
|
26
|
+
|
27
|
+
EventMachine.watch(@icmp_socket, ICMP4EM::Proxy::IcmpHandler, :pending_requests => @pending_requests) {|c| c.notify_readable = true}
|
28
|
+
|
29
|
+
@udp_socket = EventMachine::open_datagram_socket(options[:bind_host], options[:bind_port], ICMP4EM::Proxy::UdpHandler, :pending_requests => @pending_requests, :icmp_socket => @icmp_socket)
|
30
|
+
end
|
data/lib/icmp4em.rb
CHANGED
@@ -1,6 +1,16 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
1
|
+
require "eventmachine"
|
2
|
+
|
3
|
+
require "icmp4em/version"
|
4
|
+
require "icmp4em/manager"
|
5
|
+
require "icmp4em/udp_handler"
|
6
|
+
require "icmp4em/icmp_handler"
|
7
|
+
require "icmp4em/packet"
|
8
|
+
require "icmp4em/request"
|
9
|
+
require "icmp4em/timeout"
|
10
|
+
|
11
|
+
require "icmp4em/proxy/udp_handler"
|
12
|
+
require "icmp4em/proxy/icmp_handler"
|
13
|
+
require "icmp4em/proxy/request"
|
14
|
+
|
15
|
+
module ICMP4EM
|
16
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ICMP4EM
|
2
|
+
module IcmpHandler
|
3
|
+
def initialize args = {}
|
4
|
+
@manager = args[:manager]
|
5
|
+
end
|
6
|
+
|
7
|
+
def notify_readable
|
8
|
+
data, host = @io.recvfrom(1500)
|
9
|
+
icmp_data = data[20, data.length]
|
10
|
+
|
11
|
+
begin
|
12
|
+
@manager.handle_reply Packet.from_bytes(icmp_data)
|
13
|
+
rescue ArgumentError
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def unbind
|
18
|
+
@socket.close if @socket
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module ICMP4EM
|
2
|
+
class Manager
|
3
|
+
# The first 16 bits of the header data is unique to this particular ping process
|
4
|
+
MAX_IDENTIFIER = 2**16 - 1
|
5
|
+
|
6
|
+
# We use the next 12 bits as the request identifier, regardless of the retry ...
|
7
|
+
MAX_SEQUENCE = 2**12 - 1
|
8
|
+
|
9
|
+
# ... and the remaining 4 bits to identify the retry
|
10
|
+
MAX_RETRIES = 2**4 - 1
|
11
|
+
|
12
|
+
@pending_requests = {}
|
13
|
+
@socket = nil
|
14
|
+
|
15
|
+
attr_accessor :id
|
16
|
+
attr_accessor :socket
|
17
|
+
attr_accessor :timeout
|
18
|
+
attr_accessor :retries
|
19
|
+
|
20
|
+
def initialize args = {}
|
21
|
+
@timeout = args[:timeout] || 1
|
22
|
+
@retries = args[:retries] || 3
|
23
|
+
@id = rand(MAX_IDENTIFIER)
|
24
|
+
@pending_requests = {}
|
25
|
+
@next_request_id = 0
|
26
|
+
|
27
|
+
if args[:proxy]
|
28
|
+
if args[:proxy].is_a?(String)
|
29
|
+
proxy_host = args[:proxy].split(":")[0]
|
30
|
+
proxy_port = args[:proxy].split(":")[1] || 63312
|
31
|
+
@proxy_addr = Socket.pack_sockaddr_in(proxy_port, proxy_host)
|
32
|
+
else
|
33
|
+
@proxy_addr = Socket.pack_sockaddr_in(63312, "127.0.0.1")
|
34
|
+
end
|
35
|
+
|
36
|
+
@socket = Socket.new(Socket::PF_INET, Socket::SOCK_DGRAM)
|
37
|
+
EventMachine.watch(@socket, UdpHandler, :manager => self) {|c| c.notify_readable = true}
|
38
|
+
|
39
|
+
else
|
40
|
+
@socket = Socket.new(Socket::PF_INET, Socket::SOCK_RAW, Socket::IPPROTO_ICMP)
|
41
|
+
EventMachine.watch(@socket, IcmpHandler, :manager => self) {|c| c.notify_readable = true}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def proxy_enabled?
|
46
|
+
@proxy_addr
|
47
|
+
end
|
48
|
+
|
49
|
+
def ping host, args = {}
|
50
|
+
while @pending_requests.include?(@next_request_id)
|
51
|
+
@next_request_id += 1
|
52
|
+
@next_request_id %= MAX_SEQUENCE
|
53
|
+
end
|
54
|
+
|
55
|
+
request = Request.new args.merge(:host => host, :manager => self, :id => @next_request_id)
|
56
|
+
|
57
|
+
@pending_requests[request.id] = request
|
58
|
+
|
59
|
+
request.callback do
|
60
|
+
@pending_requests.delete request.id
|
61
|
+
end
|
62
|
+
|
63
|
+
request.errback do
|
64
|
+
@pending_requests.delete request.id
|
65
|
+
end
|
66
|
+
|
67
|
+
request.send
|
68
|
+
|
69
|
+
request
|
70
|
+
end
|
71
|
+
|
72
|
+
def handle_reply reply
|
73
|
+
return unless reply.valid_checksum?
|
74
|
+
return unless reply.is_reply?
|
75
|
+
|
76
|
+
request = @pending_requests.delete(reply.request_id)
|
77
|
+
return if request.nil?
|
78
|
+
|
79
|
+
request.succeed
|
80
|
+
end
|
81
|
+
|
82
|
+
def send_packet args = {}
|
83
|
+
begin
|
84
|
+
if proxy_enabled?
|
85
|
+
proxy_request = ICMP4EM::Proxy::Request.new
|
86
|
+
proxy_request.dest_ip = args[:to]
|
87
|
+
proxy_request.packet = args[:packet]
|
88
|
+
|
89
|
+
@socket.send proxy_request.to_bytes, 0, @proxy_addr
|
90
|
+
|
91
|
+
else
|
92
|
+
sock_addr = Socket.pack_sockaddr_in(0, args[:to])
|
93
|
+
@socket.send args[:packet].to_bytes, 0, sock_addr
|
94
|
+
end
|
95
|
+
rescue
|
96
|
+
puts "Got exception #{$!}"
|
97
|
+
fail $!
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module ICMP4EM
|
2
|
+
class Packet
|
3
|
+
ICMP_CODE = 0
|
4
|
+
ICMP_ECHO_REQUEST = 8
|
5
|
+
ICMP_ECHO_REPLY = 0
|
6
|
+
|
7
|
+
attr_accessor :type
|
8
|
+
attr_accessor :code
|
9
|
+
attr_accessor :checksum
|
10
|
+
attr_accessor :manager_id
|
11
|
+
attr_accessor :request_id
|
12
|
+
attr_accessor :retry_id
|
13
|
+
attr_accessor :payload
|
14
|
+
|
15
|
+
def self.from_bytes data
|
16
|
+
raise ArgumentError, "Must provide at least eight bytes in order to craft an ICMP packet" unless data.length >= 8
|
17
|
+
|
18
|
+
packet = Packet.new
|
19
|
+
fields = data.unpack("C2 n3 A*")
|
20
|
+
|
21
|
+
packet.type = fields.shift
|
22
|
+
packet.code = fields.shift
|
23
|
+
packet.checksum = fields.shift
|
24
|
+
packet.manager_id = fields.shift
|
25
|
+
|
26
|
+
sequence = fields.shift
|
27
|
+
packet.request_id = sequence >> 4
|
28
|
+
packet.retry_id = sequence & (2**4 - 1)
|
29
|
+
|
30
|
+
packet.payload = fields.shift
|
31
|
+
|
32
|
+
packet
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize args = {}
|
36
|
+
@type = args[:type] || ICMP_ECHO_REQUEST
|
37
|
+
@code = args[:code] || ICMP_CODE
|
38
|
+
@manager_id = args[:manager_id]
|
39
|
+
@request_id = args[:request_id]
|
40
|
+
@retry_id = args[:retry_id]
|
41
|
+
|
42
|
+
if args[:payload].nil?
|
43
|
+
@payload = ""
|
44
|
+
elsif args[:payload].is_a? Integer
|
45
|
+
@payload = "A" * args[:payload]
|
46
|
+
else
|
47
|
+
@payload = args[:payload]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def is_request?
|
52
|
+
@type == ICMP_ECHO_REQUEST
|
53
|
+
end
|
54
|
+
|
55
|
+
def is_reply?
|
56
|
+
@type == ICMP_ECHO_REPLY
|
57
|
+
end
|
58
|
+
|
59
|
+
def valid_checksum?
|
60
|
+
@checksum == compute_checksum
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_bytes
|
64
|
+
[@type, @code, compute_checksum, @manager_id, sequence, @payload].pack("C2 n3 A*")
|
65
|
+
end
|
66
|
+
|
67
|
+
def sequence
|
68
|
+
(@request_id << 4) + @retry_id
|
69
|
+
end
|
70
|
+
|
71
|
+
def key
|
72
|
+
[@manager_id, sequence].pack("n2")
|
73
|
+
end
|
74
|
+
|
75
|
+
def key_string
|
76
|
+
key.unpack("H*").first
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
# Perform a checksum on the message. This is the sum of all the short
|
82
|
+
# words and it folds the high order bits into the low order bits.
|
83
|
+
# This method was stolen directly from the old icmp4em - normelton
|
84
|
+
# ... which was stolen directly from net-ping - yaki
|
85
|
+
|
86
|
+
def compute_checksum
|
87
|
+
msg = [@type, @code, 0, @manager_id, sequence, @payload].pack("C2 n3 A*")
|
88
|
+
|
89
|
+
length = msg.length
|
90
|
+
num_short = length / 2
|
91
|
+
check = 0
|
92
|
+
|
93
|
+
msg.unpack("n#{num_short}").each do |short|
|
94
|
+
check += short
|
95
|
+
end
|
96
|
+
|
97
|
+
if length % 2 > 0
|
98
|
+
check += msg[length-1, 1].unpack('C').first << 8
|
99
|
+
end
|
100
|
+
|
101
|
+
check = (check >> 16) + (check & 0xffff)
|
102
|
+
return (~((check >> 16) + check) & 0xffff)
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module ICMP4EM
|
2
|
+
module Proxy
|
3
|
+
module IcmpHandler
|
4
|
+
def initialize args = {}
|
5
|
+
@pending_requests = args[:pending_requests]
|
6
|
+
end
|
7
|
+
|
8
|
+
def log msg
|
9
|
+
puts "[#{Time.now}] #{msg}" if $debug
|
10
|
+
end
|
11
|
+
|
12
|
+
def notify_readable
|
13
|
+
data, host = @io.recvfrom(1500)
|
14
|
+
icmp_data = data[20, data.length]
|
15
|
+
|
16
|
+
log "Received ICMP response from #{host.ip_address}"
|
17
|
+
|
18
|
+
begin
|
19
|
+
packet = Packet.from_bytes(icmp_data)
|
20
|
+
rescue ArgumentError
|
21
|
+
log " Got exception while parsing packet: #{$!}"
|
22
|
+
return
|
23
|
+
end
|
24
|
+
|
25
|
+
log " Key = #{packet.key_string}"
|
26
|
+
|
27
|
+
request = @pending_requests.delete(packet.key)
|
28
|
+
|
29
|
+
if request.nil?
|
30
|
+
log " Unexpected packet, dropping"
|
31
|
+
return
|
32
|
+
end
|
33
|
+
|
34
|
+
request.succeed packet
|
35
|
+
end
|
36
|
+
|
37
|
+
def unbind
|
38
|
+
@socket.close if @socket
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module ICMP4EM
|
2
|
+
module Proxy
|
3
|
+
class Request
|
4
|
+
include EM::Deferrable
|
5
|
+
|
6
|
+
attr_accessor :source_ip
|
7
|
+
attr_accessor :source_port
|
8
|
+
attr_accessor :dest_ip
|
9
|
+
attr_accessor :packet
|
10
|
+
|
11
|
+
def self.from_bytes args = {}
|
12
|
+
request = Request.new
|
13
|
+
|
14
|
+
data = args[:data]
|
15
|
+
address_length = data.unpack("n").first
|
16
|
+
|
17
|
+
request.source_ip = args[:source_ip]
|
18
|
+
request.source_port = args[:source_port]
|
19
|
+
request.dest_ip = data[2,address_length]
|
20
|
+
request.packet = Packet.from_bytes data[address_length + 2, data.length]
|
21
|
+
|
22
|
+
request
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize args = {}
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_bytes
|
29
|
+
[@dest_ip.length].pack("n") + @dest_ip + @packet.to_bytes
|
30
|
+
end
|
31
|
+
|
32
|
+
def key
|
33
|
+
@packet.key
|
34
|
+
end
|
35
|
+
|
36
|
+
def key_string
|
37
|
+
key.unpack("H*").first
|
38
|
+
end
|
39
|
+
|
40
|
+
def send args = {}
|
41
|
+
sock_addr = Socket.pack_sockaddr_in(0, @dest_ip)
|
42
|
+
args[:socket].send @packet.to_bytes, 0, sock_addr
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module ICMP4EM
|
2
|
+
module Proxy
|
3
|
+
module UdpHandler
|
4
|
+
def initialize args
|
5
|
+
@icmp_socket = args[:icmp_socket]
|
6
|
+
@pending_requests = args[:pending_requests]
|
7
|
+
|
8
|
+
log "Listening for incoming requests"
|
9
|
+
end
|
10
|
+
|
11
|
+
def log msg
|
12
|
+
puts "[#{Time.now}] #{msg}" if $debug
|
13
|
+
end
|
14
|
+
|
15
|
+
def receive_data data
|
16
|
+
source_port, source_ip = Socket.unpack_sockaddr_in(get_peername)
|
17
|
+
|
18
|
+
log "Received incoming request from #{source_ip}:#{source_port}"
|
19
|
+
|
20
|
+
begin
|
21
|
+
request = Request.from_bytes :source_ip => source_ip, :source_port => source_port, :data => data
|
22
|
+
rescue ArgumentError
|
23
|
+
log " Received error - #{$!}"
|
24
|
+
return
|
25
|
+
end
|
26
|
+
|
27
|
+
unless request.packet.is_request?
|
28
|
+
log " Incoming packet is not an ICMP request"
|
29
|
+
return
|
30
|
+
end
|
31
|
+
|
32
|
+
log " Key = #{request.key_string}"
|
33
|
+
log " Sending to #{request.dest_ip}"
|
34
|
+
|
35
|
+
begin
|
36
|
+
request.send :socket => @icmp_socket
|
37
|
+
rescue
|
38
|
+
log " Got exception while sending packet #{request.dest_ip} #{$!}"
|
39
|
+
return
|
40
|
+
end
|
41
|
+
|
42
|
+
request.timeout(30)
|
43
|
+
|
44
|
+
request.callback do |packet|
|
45
|
+
log " Got reply for request #{request.key_string}"
|
46
|
+
log " Sending reply back to #{source_ip}:#{source_port}"
|
47
|
+
send_datagram packet.to_bytes, source_ip, source_port
|
48
|
+
@pending_requests.delete request.key
|
49
|
+
end
|
50
|
+
|
51
|
+
request.errback do
|
52
|
+
log "Request #{request.key_string} has timed out"
|
53
|
+
@pending_requests.delete request.key
|
54
|
+
end
|
55
|
+
|
56
|
+
@pending_requests[request.key] = request
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module ICMP4EM
|
2
|
+
class Request
|
3
|
+
include EM::Deferrable
|
4
|
+
|
5
|
+
attr_reader :id
|
6
|
+
|
7
|
+
def initialize args = {}
|
8
|
+
@host = args[:host]
|
9
|
+
@manager = args[:manager]
|
10
|
+
@id = args[:id]
|
11
|
+
@max_retries = args[:retries] || @manager.retries
|
12
|
+
@timeout = args[:timeout] || @manager.timeout
|
13
|
+
|
14
|
+
@retry_id = 0
|
15
|
+
@timeout_timer = nil
|
16
|
+
|
17
|
+
callback do
|
18
|
+
@timeout_timer.cancel
|
19
|
+
end
|
20
|
+
|
21
|
+
errback do
|
22
|
+
@timeout_timer.cancel
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def send
|
27
|
+
@timeout_timer.cancel if @timeout_timer.is_a?(EventMachine::Timer)
|
28
|
+
|
29
|
+
@timeout_timer = EventMachine::Timer.new(@timeout) do
|
30
|
+
if @max_retries > @retry_id
|
31
|
+
send
|
32
|
+
@retry_id += 1
|
33
|
+
else
|
34
|
+
fail Timeout.new
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
packet = Packet.new(:type => Packet::ICMP_ECHO_REQUEST, :manager_id => @manager.id, :request_id => @id, :retry_id => @retry_id)
|
39
|
+
@manager.send_packet :packet => packet, :to => @host
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ICMP4EM
|
2
|
+
module UdpHandler
|
3
|
+
def initialize args = {}
|
4
|
+
@manager = args[:manager]
|
5
|
+
end
|
6
|
+
|
7
|
+
def notify_readable
|
8
|
+
data, host = @io.recvfrom(1500)
|
9
|
+
|
10
|
+
begin
|
11
|
+
@manager.handle_reply Packet.from_bytes(data)
|
12
|
+
rescue ArgumentError
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def unbind
|
17
|
+
@socket.close if @socket
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
metadata
CHANGED
@@ -1,68 +1,113 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: icmp4em
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
5
6
|
platform: ruby
|
6
|
-
authors:
|
7
|
-
-
|
7
|
+
authors:
|
8
|
+
- Norman Elton
|
8
9
|
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
dependencies:
|
15
|
-
- !ruby/object:Gem::Dependency
|
12
|
+
date: 2013-06-12 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
16
15
|
name: eventmachine
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.0.0
|
17
22
|
type: :runtime
|
18
|
-
|
19
|
-
version_requirements: !ruby/object:Gem::Requirement
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.0.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: yard
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
description: A high-performance ICMP engine build on EventMachine
|
63
|
+
email:
|
64
|
+
- normelton@gmail.com
|
65
|
+
executables:
|
66
|
+
- icmp-proxy
|
29
67
|
extensions: []
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
-
|
35
|
-
-
|
36
|
-
-
|
68
|
+
extra_rdoc_files:
|
69
|
+
- README.rdoc
|
70
|
+
files:
|
71
|
+
- lib/icmp4em/icmp_handler.rb
|
72
|
+
- lib/icmp4em/manager.rb
|
73
|
+
- lib/icmp4em/packet.rb
|
74
|
+
- lib/icmp4em/proxy/icmp_handler.rb
|
75
|
+
- lib/icmp4em/proxy/request.rb
|
76
|
+
- lib/icmp4em/proxy/udp_handler.rb
|
77
|
+
- lib/icmp4em/request.rb
|
78
|
+
- lib/icmp4em/timeout.rb
|
79
|
+
- lib/icmp4em/udp_handler.rb
|
80
|
+
- lib/icmp4em/version.rb
|
37
81
|
- lib/icmp4em.rb
|
38
|
-
-
|
39
|
-
-
|
40
|
-
-
|
41
|
-
|
42
|
-
|
82
|
+
- bin/icmp-proxy
|
83
|
+
- Gemfile
|
84
|
+
- Gemfile.lock
|
85
|
+
- LICENSE.txt
|
86
|
+
- Rakefile
|
87
|
+
- README.rdoc
|
88
|
+
homepage: ''
|
89
|
+
licenses: []
|
43
90
|
post_install_message:
|
44
91
|
rdoc_options: []
|
45
|
-
|
46
|
-
require_paths:
|
92
|
+
require_paths:
|
47
93
|
- lib
|
48
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
none: false
|
96
|
+
requirements:
|
97
|
+
- - ! '>='
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '0'
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
|
+
none: false
|
102
|
+
requirements:
|
103
|
+
- - ! '>='
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
60
106
|
requirements: []
|
61
|
-
|
62
|
-
|
63
|
-
rubygems_version: 1.3.1
|
107
|
+
rubyforge_project:
|
108
|
+
rubygems_version: 1.8.25
|
64
109
|
signing_key:
|
65
|
-
specification_version:
|
66
|
-
summary:
|
110
|
+
specification_version: 3
|
111
|
+
summary: A high-performance ICMP engine build on EventMachine
|
67
112
|
test_files: []
|
68
|
-
|
113
|
+
has_rdoc:
|
data/README
DELETED
@@ -1,60 +0,0 @@
|
|
1
|
-
icmp4em - ICMP ping using EventMachine
|
2
|
-
|
3
|
-
http://www.github.com/yakischloba/icmp4em
|
4
|
-
|
5
|
-
Asynchronous implementation of ICMP ping using EventMachine. Can be used to ping many hosts
|
6
|
-
at once in a non-blocking (or blocking) fashion, with callbacks for success, timeout, and host failure/recovery
|
7
|
-
based on specified threshold numbers. It was developed as a component for the purpose of monitoring
|
8
|
-
multiple WAN connections on a Linux gateway host, and altering the routing table accordingly, to
|
9
|
-
provide optimal routing and failover in a situation where only consumer-grade broadband is available.
|
10
|
-
Should be useful in many situations. Still planning on adding ICMPv6 sometime..
|
11
|
-
|
12
|
-
Should work fine with Ruby 1.9.1.
|
13
|
-
|
14
|
-
This must be run as effective root user to use the ICMP sockets.
|
15
|
-
|
16
|
-
Installation:
|
17
|
-
|
18
|
-
git clone git://github.com/yakischloba/icmp4em.git
|
19
|
-
cd icmp4em
|
20
|
-
gem build icmp4em.gemspec
|
21
|
-
sudo gem install icmp4em-<version>.gem
|
22
|
-
|
23
|
-
|
24
|
-
Simple example (see the examples/ directory for the cooler stuff):
|
25
|
-
============================================================================
|
26
|
-
require 'rubygems'
|
27
|
-
require 'icmp4em'
|
28
|
-
|
29
|
-
Signal.trap("INT") { EventMachine::stop_event_loop }
|
30
|
-
|
31
|
-
host = ICMP4EM::ICMPv4.new("127.0.0.1")
|
32
|
-
host.on_success {|host, seq, latency| puts "Got echo sequence number #{seq} from host #{host}. It took #{latency}ms." }
|
33
|
-
host.on_expire {|host, seq, exception| puts "I shouldn't fail on loopback interface, but in case I did: #{exception.to_s}"}
|
34
|
-
|
35
|
-
EM.run { host.schedule }
|
36
|
-
=>
|
37
|
-
Got echo sequence number 1 from host 127.0.0.1. It took 0.214ms.
|
38
|
-
Got echo sequence number 2 from host 127.0.0.1. It took 0.193ms.
|
39
|
-
Got echo sequence number 3 from host 127.0.0.1. It took 0.166ms.
|
40
|
-
Got echo sequence number 4 from host 127.0.0.1. It took 0.172ms.
|
41
|
-
Got echo sequence number 5 from host 127.0.0.1. It took 0.217ms.
|
42
|
-
^C
|
43
|
-
============================================================================
|
44
|
-
|
45
|
-
One issue may run into is that when using this with a lot of hosts, or using EventMachine for other things,
|
46
|
-
extra apparent latency will be added. This is simply due to other operations blocking the receipt of the ICMP
|
47
|
-
response. For when accurate latency measurement is important, and you can afford to block the reactor, I have
|
48
|
-
added a blocking receive mode. To use this simply pass :block => true to the constructor. This will wait for
|
49
|
-
the response for the duration of the timeout before passing control back to EventMachine.
|
50
|
-
|
51
|
-
|
52
|
-
jakecdouglas@gmail.com
|
53
|
-
yakischloba on Freenode
|
54
|
-
|
55
|
-
|
56
|
-
Thanks to the #eventmachine guys for providing a great tool and always being such patient teachers.
|
57
|
-
|
58
|
-
|
59
|
-
Thanks to imperator and the others that worked on the net-ping library. I used the packet construction and checksum
|
60
|
-
code from that implementation. See their README for detailed acknowledgements.
|
data/examples/simple_example.rb
DELETED
@@ -1,22 +0,0 @@
|
|
1
|
-
require 'rubygems'
|
2
|
-
require 'icmp4em'
|
3
|
-
|
4
|
-
# This is an example of non-stateful usage. Only the callbacks provided in on_success and on_expire are used,
|
5
|
-
# and the object does not keep track of up/down or execute callbacks on_failure/on_recovery.
|
6
|
-
# The data string can be set to anything, as long as the total packet size is less than MTU of the network.
|
7
|
-
|
8
|
-
pings = []
|
9
|
-
pings << ICMP4EM::ICMPv4.new("google.com")
|
10
|
-
pings << ICMP4EM::ICMPv4.new("slashdot.org")
|
11
|
-
pings << ICMP4EM::ICMPv4.new("10.99.99.99") # host that will not respond.
|
12
|
-
|
13
|
-
Signal.trap("INT") { EventMachine::stop_event_loop }
|
14
|
-
|
15
|
-
EM.run {
|
16
|
-
pings.each do |ping|
|
17
|
-
ping.data = "bar of foozz"
|
18
|
-
ping.on_success {|host, seq, latency| puts "SUCCESS from #{host}, sequence number #{seq}, Latency #{latency}ms"}
|
19
|
-
ping.on_expire {|host, seq, exception| puts "FAILURE from #{host}, sequence number #{seq}, Reason: #{exception.to_s}"}
|
20
|
-
ping.schedule
|
21
|
-
end
|
22
|
-
}
|
@@ -1,22 +0,0 @@
|
|
1
|
-
require 'rubygems'
|
2
|
-
require 'icmp4em'
|
3
|
-
|
4
|
-
# This example shows stateful usage, which tracks up/down state of the host based on consecutive number
|
5
|
-
# of successful or failing pings specified in failures_required and recoveries_required. Hosts start in
|
6
|
-
# 'up' state.
|
7
|
-
|
8
|
-
pings = []
|
9
|
-
pings << ICMP4EM::ICMPv4.new("google.com", :stateful => true)
|
10
|
-
pings << ICMP4EM::ICMPv4.new("10.1.0.175", :stateful => true) # host that will not respond.
|
11
|
-
|
12
|
-
Signal.trap("INT") { EventMachine::stop_event_loop }
|
13
|
-
|
14
|
-
EM.run {
|
15
|
-
pings.each do |ping|
|
16
|
-
ping.on_success {|host, seq, latency, count_to_recovery| puts "SUCCESS from #{host}, sequence number #{seq}, Latency #{latency}ms, Recovering in #{count_to_recovery} more"}
|
17
|
-
ping.on_expire {|host, seq, exception, count_to_failure| puts "FAILURE from #{host}, sequence number #{seq}, Reason: #{exception.to_s}, Failing in #{count_to_failure} more"}
|
18
|
-
ping.on_failure {|host| puts "HOST STATE WENT TO DOWN: #{host} at #{Time.now}"}
|
19
|
-
ping.on_recovery {|host| puts "HOST STATE WENT TO UP: #{host} at #{Time.now}"}
|
20
|
-
ping.schedule
|
21
|
-
end
|
22
|
-
}
|
data/lib/icmp4em/common.rb
DELETED
@@ -1,140 +0,0 @@
|
|
1
|
-
module ICMP4EM
|
2
|
-
|
3
|
-
class Timeout < Exception; end;
|
4
|
-
|
5
|
-
module Common
|
6
|
-
|
7
|
-
ICMP_ECHOREPLY = 0
|
8
|
-
ICMP_ECHO = 8
|
9
|
-
ICMP_SUBCODE = 0
|
10
|
-
|
11
|
-
private
|
12
|
-
|
13
|
-
# Perform a checksum on the message. This is the sum of all the short
|
14
|
-
# words and it folds the high order bits into the low order bits.
|
15
|
-
# (This method was stolen directly from net-ping - yaki)
|
16
|
-
def generate_checksum(msg)
|
17
|
-
length = msg.length
|
18
|
-
num_short = length / 2
|
19
|
-
check = 0
|
20
|
-
|
21
|
-
msg.unpack("n#{num_short}").each do |short|
|
22
|
-
check += short
|
23
|
-
end
|
24
|
-
|
25
|
-
if length % 2 > 0
|
26
|
-
check += msg[length-1, 1].unpack('C').first << 8
|
27
|
-
end
|
28
|
-
|
29
|
-
check = (check >> 16) + (check & 0xffff)
|
30
|
-
return (~((check >> 16) + check) & 0xffff)
|
31
|
-
end
|
32
|
-
|
33
|
-
end
|
34
|
-
|
35
|
-
module HostCommon
|
36
|
-
|
37
|
-
# Set failure callback. The provided Proc or block will be called and yielded the host and sequence number, whenever the failure count exceeds the defined threshold.
|
38
|
-
def on_failure(proc = nil, &block)
|
39
|
-
@failure = proc || block unless proc.nil? and block.nil?
|
40
|
-
@failure
|
41
|
-
end
|
42
|
-
|
43
|
-
# Set recovery callback. The provided Proc or block will be called and yielded the host and sequence number, whenever the recovery count exceeds the defined threshold.
|
44
|
-
def on_recovery(proc = nil, &block)
|
45
|
-
@recovery = proc || block unless proc.nil? and block.nil?
|
46
|
-
@recovery
|
47
|
-
end
|
48
|
-
|
49
|
-
# Set success callback. This will be called and yielded the host, sequence number, and latency every time a ping returns successfully.
|
50
|
-
def on_success(proc = nil, &block)
|
51
|
-
@success = proc || block unless proc.nil? and block.nil?
|
52
|
-
@success
|
53
|
-
end
|
54
|
-
|
55
|
-
# Set 'expiry' callback. This will be called and yielded the host, sequence number, and Exception every time a ping fails.
|
56
|
-
# This is not just for timeouts! This can be triggered by failure of the ping for any reason.
|
57
|
-
def on_expire(proc = nil, &block)
|
58
|
-
@expiry = proc || block unless proc.nil? and block.nil?
|
59
|
-
@expiry
|
60
|
-
end
|
61
|
-
|
62
|
-
# Set the number of consecutive 'failure' pings required to switch host state to 'down' and trigger failure callback, assuming the host is up.
|
63
|
-
def failures_required=(failures)
|
64
|
-
@failures_required = failures
|
65
|
-
end
|
66
|
-
|
67
|
-
# Set the number of consecutive 'recovery' pings required to switch host state to 'up' and trigger recovery callback, assuming the host is down.
|
68
|
-
def recoveries_required=(recoveries)
|
69
|
-
@recoveries_required = recoveries
|
70
|
-
end
|
71
|
-
|
72
|
-
private
|
73
|
-
|
74
|
-
def success(seq, latency)
|
75
|
-
if @success
|
76
|
-
if @stateful
|
77
|
-
count_to_recover = @up ? 0 : @recoveries_required - @failcount.abs
|
78
|
-
@success.call(@host, seq, latency, count_to_recover)
|
79
|
-
else
|
80
|
-
@success.call(@host, seq, latency)
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
def expiry(seq, reason)
|
86
|
-
if @expiry
|
87
|
-
if @stateful
|
88
|
-
count_to_fail = @up ? @failures_required - @failcount : 0
|
89
|
-
@expiry.call(@host, seq, reason, count_to_fail)
|
90
|
-
else
|
91
|
-
@expiry.call(@host, seq, reason)
|
92
|
-
end
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
# Executes specified failure callback, passing the host to the block.
|
97
|
-
def fail
|
98
|
-
@failure.call(@host) if @failure
|
99
|
-
@up = false
|
100
|
-
end
|
101
|
-
|
102
|
-
# Executes specified recovery callback, passing the host to the block.
|
103
|
-
def recover
|
104
|
-
@recovery.call(@host) if @recovery
|
105
|
-
@up = true
|
106
|
-
end
|
107
|
-
|
108
|
-
# Trigger failure/recovery if either threshold is exceeded...
|
109
|
-
def check_for_fail_or_recover
|
110
|
-
if @failcount > 0
|
111
|
-
fail if @failcount >= @failures_required && @up
|
112
|
-
elsif @failcount <= -1
|
113
|
-
recover if @failcount.abs >= @recoveries_required && !@up
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
# Adjusts the failure counter after each ping. The failure counter is incremented positively to count failures,
|
118
|
-
# and decremented into negative numbers to indicate successful pings towards recovery after a failure.
|
119
|
-
# This is an awful mess..just like the rest of this file.
|
120
|
-
def adjust_failure_count(direction)
|
121
|
-
if direction == :down
|
122
|
-
if @failcount > -1
|
123
|
-
@failcount += 1
|
124
|
-
elsif @failcount <= -1
|
125
|
-
@failcount = 1
|
126
|
-
end
|
127
|
-
elsif direction == :up && !@up
|
128
|
-
if @failcount > 0
|
129
|
-
@failcount = -1
|
130
|
-
elsif @failcount <= -1
|
131
|
-
@failcount -= 1
|
132
|
-
end
|
133
|
-
else
|
134
|
-
@failcount = 0
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
end
|
139
|
-
|
140
|
-
end
|
data/lib/icmp4em/handler.rb
DELETED
@@ -1,53 +0,0 @@
|
|
1
|
-
module ICMP4EM
|
2
|
-
|
3
|
-
module Handler
|
4
|
-
|
5
|
-
include Common
|
6
|
-
|
7
|
-
def initialize(socket)
|
8
|
-
@socket = socket
|
9
|
-
end
|
10
|
-
|
11
|
-
def notify_readable
|
12
|
-
receive(@socket)
|
13
|
-
end
|
14
|
-
|
15
|
-
def unbind
|
16
|
-
@socket.close if @socket
|
17
|
-
end
|
18
|
-
|
19
|
-
private
|
20
|
-
|
21
|
-
def receive(socket)
|
22
|
-
# The data was available now
|
23
|
-
time = Time.now
|
24
|
-
# Get data
|
25
|
-
host, data = read_socket(socket)
|
26
|
-
# Rebuild message array
|
27
|
-
msg = data[20,30].unpack("C2 n3 A*")
|
28
|
-
# Verify the packet type is echo reply and verify integrity against the checksum it provided
|
29
|
-
return unless msg.first == ICMP_ECHOREPLY && verify_checksum?(msg)
|
30
|
-
# Find which object it is supposed to go to
|
31
|
-
recipient = ICMPv4.instances[msg[3]]
|
32
|
-
# Send time and seq number to recipient object
|
33
|
-
recipient.send(:receive, msg[4], time) unless recipient.nil?
|
34
|
-
end
|
35
|
-
|
36
|
-
def read_socket(socket)
|
37
|
-
# Recieve a common MTU, 1500 bytes.
|
38
|
-
data, sender = socket.recvfrom(1500)
|
39
|
-
# Get the host in case we want to use that later.
|
40
|
-
host = Socket.unpack_sockaddr_in(sender).last
|
41
|
-
[host, data]
|
42
|
-
end
|
43
|
-
|
44
|
-
def verify_checksum?(ary)
|
45
|
-
cs = ary[2]
|
46
|
-
ary_copy = ary.dup
|
47
|
-
ary_copy[2] = 0
|
48
|
-
cs == generate_checksum(ary_copy.pack("C2 n3 A*"))
|
49
|
-
end
|
50
|
-
|
51
|
-
end
|
52
|
-
|
53
|
-
end
|
data/lib/icmp4em/icmpv4.rb
DELETED
@@ -1,173 +0,0 @@
|
|
1
|
-
module ICMP4EM
|
2
|
-
|
3
|
-
class ICMPv4
|
4
|
-
|
5
|
-
include Common
|
6
|
-
include HostCommon
|
7
|
-
|
8
|
-
@instances = {}
|
9
|
-
@recvsocket = nil
|
10
|
-
|
11
|
-
class << self
|
12
|
-
|
13
|
-
attr_reader :instances
|
14
|
-
attr_accessor :recvsocket, :handler
|
15
|
-
|
16
|
-
end
|
17
|
-
|
18
|
-
attr_accessor :bind_host, :interval, :threshold, :timeout, :data, :block
|
19
|
-
attr_reader :id, :failures_required, :recoveries_required, :seq
|
20
|
-
|
21
|
-
# Create a new ICMP object (host). This takes a host and an optional hash of options for modifying the behavior. They are:
|
22
|
-
# * :bind_host
|
23
|
-
# Bind the socket to this address. The operating system will figure this out on it's own unless you need to set it for a special situation.
|
24
|
-
# * :timeout
|
25
|
-
# Timeout, in seconds, before the ping is considered expired and the appropriate callbacks are executed. This should be a numeric class.
|
26
|
-
# * :block
|
27
|
-
# True or false, default is false. True enables a blocking receive mode, for when accurate latency measurement is important. Due to the nature
|
28
|
-
# of event loop architecture, a noticable delay in latency can be added when other things are going on in the reactor.
|
29
|
-
# * :interval
|
30
|
-
# Interval, in seconds, for how often the ping should be sent. Should be a numeric class.
|
31
|
-
# * :stateful
|
32
|
-
# True or false, default is false. Indicates whether or not this ping object should keep track of it's successes and failures and execute
|
33
|
-
# the on_failure/on_recovery callbacks when the specified limits are hit.
|
34
|
-
# * :failures_required
|
35
|
-
# Indicates how many consequtive failures are required to switch to the 'failed' state and execute the on_failure callback. Applies only when :stateful => true
|
36
|
-
# * :recoveries_required
|
37
|
-
# Indicates how many consequtive successes are required to switch to the 'recovered' state and execute the on_recovery callback. Applies only when :stateful => true
|
38
|
-
def initialize(host, options = {})
|
39
|
-
raise 'requires root privileges' if Process.euid > 0
|
40
|
-
@host = host
|
41
|
-
@ipv4_sockaddr = Socket.pack_sockaddr_in(0, @host)
|
42
|
-
@interval = options[:interval] || 1
|
43
|
-
@timeout = options[:timeout] || 1
|
44
|
-
@stateful = options[:stateful] || false
|
45
|
-
@bind_host = options[:bind_host] || nil
|
46
|
-
@block = options[:block] || false
|
47
|
-
@recoveries_required = options[:recoveries_required] || 5
|
48
|
-
@failures_required = options[:failures_required] || 5
|
49
|
-
@up = true
|
50
|
-
@waiting = {}
|
51
|
-
set_id
|
52
|
-
@seq, @failcount = 0, 0
|
53
|
-
@data = "Ping from EventMachine"
|
54
|
-
end
|
55
|
-
|
56
|
-
# This must be called when the object will no longer be used, to remove
|
57
|
-
# the object from the class variable hash that is searched for recipients when
|
58
|
-
# an ICMP echo comes in. Also cancels the periodic timer. Better way to do this whole thing?...
|
59
|
-
def stop
|
60
|
-
@ptimer.cancel if @ptimer
|
61
|
-
self.class.instances[@id] = nil
|
62
|
-
end
|
63
|
-
|
64
|
-
# Send the echo request to @host and add sequence number to the waiting queue.
|
65
|
-
def ping
|
66
|
-
raise "EM not running" unless EM.reactor_running?
|
67
|
-
init_handler if self.class.recvsocket.nil?
|
68
|
-
@seq = ping_send
|
69
|
-
if @block
|
70
|
-
blocking_receive
|
71
|
-
else
|
72
|
-
EM.add_timer(@timeout) { self.send(:expire, @seq, Timeout.new("Ping timed out")) } unless @timeout == 0
|
73
|
-
end
|
74
|
-
@seq
|
75
|
-
end
|
76
|
-
|
77
|
-
# Uses a periodic timer to ping the host at @interval.
|
78
|
-
def schedule
|
79
|
-
raise "EM not running" unless EM.reactor_running?
|
80
|
-
@ptimer = EM::PeriodicTimer.new(@interval) { self.ping }
|
81
|
-
end
|
82
|
-
|
83
|
-
private
|
84
|
-
|
85
|
-
# Expire a sequence number from the waiting queue.
|
86
|
-
# Should only be called by the timer setup in #ping or the rescue Exception in #ping_send.
|
87
|
-
def expire(seq, exception = nil)
|
88
|
-
waiting = @waiting[seq]
|
89
|
-
if waiting
|
90
|
-
@waiting[seq] = nil
|
91
|
-
adjust_failure_count(:down) if @stateful
|
92
|
-
expiry(seq, exception)
|
93
|
-
check_for_fail_or_recover if @stateful
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
# Should only be called by the Handler. Passes the receive time and sequence number.
|
98
|
-
def receive(seq, time)
|
99
|
-
waiting = @waiting[seq]
|
100
|
-
if waiting
|
101
|
-
latency = (time - waiting) * 1000
|
102
|
-
adjust_failure_count(:up) if @stateful
|
103
|
-
success(seq, latency)
|
104
|
-
check_for_fail_or_recover if @stateful
|
105
|
-
@waiting[seq] = nil
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
# Construct and send the ICMP echo request packet.
|
110
|
-
def ping_send
|
111
|
-
seq = (@seq + 1) % 65536
|
112
|
-
|
113
|
-
socket = self.class.recvsocket
|
114
|
-
|
115
|
-
# Generate msg with checksum
|
116
|
-
msg = [ICMP_ECHO, ICMP_SUBCODE, 0, @id, seq, @data].pack("C2 n3 A*")
|
117
|
-
msg[2..3] = [generate_checksum(msg)].pack('n')
|
118
|
-
|
119
|
-
# Enqueue so we can expire properly if there is an exception raised during #send
|
120
|
-
@waiting[seq] = Time.now
|
121
|
-
|
122
|
-
begin
|
123
|
-
# Fire it off
|
124
|
-
socket.send(msg, 0, @ipv4_sockaddr)
|
125
|
-
# Re-enqueue AFTER sendto() returns. This ensures we aren't adding latency if the socket blocks.
|
126
|
-
@waiting[seq] = Time.now
|
127
|
-
# Return sequence number to caller
|
128
|
-
seq
|
129
|
-
rescue Exception => err
|
130
|
-
expire(seq, err)
|
131
|
-
seq
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
# Initialize the receiving socket and handler for incoming ICMP packets.
|
136
|
-
def init_handler
|
137
|
-
self.class.recvsocket = Socket.new(
|
138
|
-
Socket::PF_INET,
|
139
|
-
Socket::SOCK_RAW,
|
140
|
-
Socket::IPPROTO_ICMP
|
141
|
-
)
|
142
|
-
if @bind_host
|
143
|
-
saddr = Socket.pack_sockaddr_in(0, @bind_host)
|
144
|
-
self.class.recvsocket.bind(saddr)
|
145
|
-
end
|
146
|
-
self.class.handler = EM.attach self.class.recvsocket, Handler, self.class.recvsocket
|
147
|
-
end
|
148
|
-
|
149
|
-
# Sets the instance id to a unique 16 bit integer so it can fit inside relevent the ICMP field.
|
150
|
-
# Also adds self to the pool so that incoming messages that it requested can be delivered.
|
151
|
-
def set_id
|
152
|
-
while @id.nil?
|
153
|
-
id = rand(65535)
|
154
|
-
unless self.class.instances[id]
|
155
|
-
@id = id
|
156
|
-
self.class.instances[@id] = self
|
157
|
-
end
|
158
|
-
end
|
159
|
-
end
|
160
|
-
|
161
|
-
def blocking_receive
|
162
|
-
r = select([self.class.recvsocket], nil, nil, @timeout)
|
163
|
-
|
164
|
-
if r and r.first.include?(self.class.recvsocket)
|
165
|
-
self.class.handler.notify_readable
|
166
|
-
else
|
167
|
-
expire(@seq, Timeout.new("Ping timed out"))
|
168
|
-
end
|
169
|
-
end
|
170
|
-
|
171
|
-
end
|
172
|
-
|
173
|
-
end
|