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 ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in icmp4em.gemspec
4
+ gemspec
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
- $:.unshift File.expand_path(File.dirname(File.expand_path(__FILE__)))
2
- require 'eventmachine'
3
- require 'socket'
4
- require 'icmp4em/common'
5
- require 'icmp4em/handler'
6
- require 'icmp4em/icmpv4'
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,4 @@
1
+ module ICMP4EM
2
+ class Timeout < Exception
3
+ end
4
+ 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
@@ -0,0 +1,3 @@
1
+ module ICMP4EM
2
+ VERSION = "1.0.0"
3
+ 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.2
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
5
6
  platform: ruby
6
- authors:
7
- - Jake Douglas
7
+ authors:
8
+ - Norman Elton
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
-
12
- date: 2009-04-03 00:00:00 -07:00
13
- default_executable:
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
- version_requirement:
19
- version_requirements: !ruby/object:Gem::Requirement
20
- requirements:
21
- - - ">="
22
- - !ruby/object:Gem::Version
23
- version: "0"
24
- version:
25
- description: Asynchronous implementation of ICMP ping using EventMachine. Can be used to ping many hosts at once in a non-blocking fashion, with callbacks for success, timeout, and host failure/recovery based on specified threshold numbers.
26
- email: jakecdouglas@gmail.com
27
- executables: []
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
- extra_rdoc_files: []
32
-
33
- files:
34
- - README
35
- - examples/simple_example.rb
36
- - examples/stateful_example.rb
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
- - lib/icmp4em/common.rb
39
- - lib/icmp4em/handler.rb
40
- - lib/icmp4em/icmpv4.rb
41
- has_rdoc: true
42
- homepage: http://www.github.com/yakischloba/icmp4em
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
- requirements:
50
- - - ">="
51
- - !ruby/object:Gem::Version
52
- version: "0"
53
- version:
54
- required_rubygems_version: !ruby/object:Gem::Requirement
55
- requirements:
56
- - - ">="
57
- - !ruby/object:Gem::Version
58
- version: "0"
59
- version:
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
- rubyforge_project: icmp4em
63
- rubygems_version: 1.3.1
107
+ rubyforge_project:
108
+ rubygems_version: 1.8.25
64
109
  signing_key:
65
- specification_version: 2
66
- summary: Asynchronous implementation of ICMP ping using EventMachine
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.
@@ -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
- }
@@ -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
@@ -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
@@ -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