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 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