icmp4em 0.0.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|