icmp4em 0.0.1

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/README ADDED
@@ -0,0 +1,78 @@
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 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. ICMPv6 is to be added soon.
11
+
12
+ This must be run as effective root user to use the ICMP sockets.
13
+
14
+ Installation:
15
+
16
+ git clone git://github.com/yakischloba/icmp4em.git
17
+ cd icmp4em
18
+ gem build icmp4em.gemspec
19
+ sudo gem install icmp4em-<version>.gem
20
+
21
+
22
+ Simple example (see the examples/ directory for the cooler stuff):
23
+ ============================================================================
24
+ require 'rubygems'
25
+ require 'icmp4em'
26
+
27
+ Signal.trap("INT") { EventMachine::stop_event_loop }
28
+
29
+ host = ICMP4EM::ICMPv4.new("127.0.0.1")
30
+ host.on_success {|host, seq, latency| puts "Got echo sequence number #{seq} from host #{host}. It took #{latency}ms." }
31
+ host.on_expire {|host, seq, exception| puts "I shouldn't fail on loopback interface, but in case I did: #{exception.to_s}"}
32
+
33
+ EM.run { host.schedule }
34
+ =>
35
+ Got echo sequence number 1 from host 127.0.0.1. It took 0.214ms.
36
+ Got echo sequence number 2 from host 127.0.0.1. It took 0.193ms.
37
+ Got echo sequence number 3 from host 127.0.0.1. It took 0.166ms.
38
+ Got echo sequence number 4 from host 127.0.0.1. It took 0.172ms.
39
+ Got echo sequence number 5 from host 127.0.0.1. It took 0.217ms.
40
+ ^C
41
+ ============================================================================
42
+
43
+ Please let me know what is wrong with it!
44
+
45
+ jakecdouglas@gmail.com
46
+ yakischloba on Freenode
47
+
48
+
49
+ Thanks to the #eventmachine guys for providing a great tool and always being such patient teachers.
50
+
51
+ Thanks to imperator and the others that worked on the net-ping library. I used the packet construction and checksum
52
+ code from that implementation. Here is the pertinent information from their README, so that nothing is missed:
53
+
54
+ Acknowledgements from "net-ping-1.2.2/doc/ping.txt":
55
+
56
+ = Acknowledgements
57
+ The Ping::ICMP#ping method is based largely on the identical method from
58
+ the Net::Ping Perl module by Rob Brown. Much of the code was ported by
59
+ Jos Backus on ruby-talk.
60
+
61
+ = Future Plans
62
+ Add support for syn pings.
63
+
64
+ = License
65
+ Ruby's
66
+
67
+ = Copyright
68
+ (C) 2003-2008 Daniel J. Berger, All Rights Reserved
69
+
70
+ = Warranty
71
+ This package is provided "as is" and without any express or
72
+ implied warranties, including, without limitation, the implied
73
+ warranties of merchantability and fitness for a particular purpose.
74
+
75
+ = Author
76
+ Daniel J. Berger
77
+ djberg96 at gmail dot com
78
+ imperator on IRC (irc.freenode.net)
@@ -0,0 +1,22 @@
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
+ }
@@ -0,0 +1,22 @@
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
+ }
@@ -0,0 +1,141 @@
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
+
95
+ end
96
+
97
+ # Executes specified failure callback, passing the host to the block.
98
+ def fail
99
+ @failure.call(@host) if @failure
100
+ @up = false
101
+ end
102
+
103
+ # Executes specified recovery callback, passing the host to the block.
104
+ def recover
105
+ @recovery.call(@host) if @recovery
106
+ @up = true
107
+ end
108
+
109
+ # Trigger failure/recovery if either threshold is exceeded...
110
+ def check_for_fail_or_recover
111
+ if @failcount > 0
112
+ fail if @failcount >= @failures_required && @up
113
+ elsif @failcount <= -1
114
+ recover if @failcount.abs >= @recoveries_required && !@up
115
+ end
116
+ end
117
+
118
+ # Adjusts the failure counter after each ping. The failure counter is incremented positively to count failures,
119
+ # and decremented into negative numbers to indicate successful pings towards recovery after a failure.
120
+ # This is an awful mess..just like the rest of this file.
121
+ def adjust_failure_count(direction)
122
+ if direction == :down
123
+ if @failcount > -1
124
+ @failcount += 1
125
+ elsif @failcount <= -1
126
+ @failcount = 1
127
+ end
128
+ elsif direction == :up && !@up
129
+ if @failcount > 0
130
+ @failcount = -1
131
+ elsif @failcount <= -1
132
+ @failcount -= 1
133
+ end
134
+ else
135
+ @failcount = 0
136
+ end
137
+ end
138
+
139
+ end
140
+
141
+ end
@@ -0,0 +1,53 @@
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
@@ -0,0 +1,150 @@
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
15
+
16
+ end
17
+
18
+ attr_accessor :bind_host, :interval, :threshold, :timeout, :data
19
+ attr_reader :id, :failures_required, :recoveries_required, :seq
20
+
21
+ # Create a new ICMP object (host). Must be passed either IP address or hostname, and
22
+ # optionally the interval at which it should be pinged, and timeout for the pings, in seconds.
23
+ def initialize(host, options = {})
24
+ raise 'requires root privileges' if Process.euid > 0
25
+ @host = host
26
+ @ipv4_sockaddr = Socket.pack_sockaddr_in(0, @host)
27
+ @interval = options[:interval] || 1
28
+ @timeout = options[:timeout] || 1
29
+ @stateful = options[:stateful] || false
30
+ @bind_host = options[:bind_host] || nil
31
+ @recoveries_required = options[:recoveries_required] || 5
32
+ @failures_required = options[:failures_required] || 5
33
+ @up = true
34
+ @waiting = {}
35
+ set_id
36
+ @seq, @failcount = 0, 0
37
+ @data = "Ping from EventMachine"
38
+ end
39
+
40
+ # This must be called when the object will no longer be used, to remove
41
+ # the object from the class variable array that is searched for recipients when
42
+ # an ICMP echo comes in. Better way to do this whole thing?...
43
+ def destroy
44
+ self.class.instances[@id] = nil
45
+ end
46
+
47
+ # Send the echo request to @host and add sequence number to the waiting queue.
48
+ def ping
49
+ raise "EM not running" unless EM.reactor_running?
50
+ init_handler if self.class.recvsocket.nil?
51
+ seq = ping_send
52
+ EM.add_timer(@timeout) { self.send(:expire, seq, Timeout.new("Ping timed out")) } unless @timeout == 0
53
+ @seq
54
+ end
55
+
56
+ # Uses EM.add_periodic_timer to ping the host at @interval.
57
+ def schedule
58
+ raise "EM not running" unless EM.reactor_running?
59
+ EM.add_periodic_timer(@interval) { self.ping }
60
+ end
61
+
62
+ private
63
+
64
+ # Expire a sequence number from the waiting queue.
65
+ # Should only be called by the timer setup in #ping or the rescue Exception in #ping_send.
66
+ def expire(seq, exception = nil)
67
+ waiting = @waiting[seq]
68
+ if waiting
69
+ @waiting[seq] = nil
70
+ adjust_failure_count(:down) if @stateful
71
+ expiry(seq, exception)
72
+ check_for_fail_or_recover if @stateful
73
+ end
74
+ end
75
+
76
+ # Should only be called by the Handler. Passes the receive time and sequence number.
77
+ def receive(seq, time)
78
+ waiting = @waiting[seq]
79
+ if waiting
80
+ latency = (time - waiting) * 1000
81
+ adjust_failure_count(:up) if @stateful
82
+ success(seq, latency)
83
+ check_for_fail_or_recover if @stateful
84
+ @waiting[seq] = nil
85
+ end
86
+ end
87
+
88
+ # Construct and send the ICMP echo request packet.
89
+ def ping_send
90
+ @seq = (@seq + 1) % 65536
91
+
92
+ socket = Socket.new(
93
+ Socket::PF_INET,
94
+ Socket::SOCK_RAW,
95
+ Socket::IPPROTO_ICMP
96
+ )
97
+
98
+ if @bind_host
99
+ saddr = Socket.pack_sockaddr_in(0, @bind_host)
100
+ socket.bind(saddr)
101
+ end
102
+
103
+ # Generate msg with checksum
104
+ msg = [ICMP_ECHO, ICMP_SUBCODE, 0, @id, @seq, @data].pack("C2 n3 A*")
105
+ msg[2..3] = [generate_checksum(msg)].pack('n')
106
+
107
+ # Enqueue
108
+ @waiting[seq] = Time.now
109
+
110
+ begin
111
+ # Fire it off
112
+ socket.send(msg, 0, @ipv4_sockaddr)
113
+ # Return sequence number to caller
114
+ @seq
115
+ rescue Exception => err
116
+ expire(@seq, err)
117
+ ensure
118
+ socket.close if socket
119
+ end
120
+ end
121
+
122
+ # Initialize the receiving socket and handler for incoming ICMP packets.
123
+ def init_handler
124
+ self.class.recvsocket = Socket.new(
125
+ Socket::PF_INET,
126
+ Socket::SOCK_RAW,
127
+ Socket::IPPROTO_ICMP
128
+ )
129
+ if @bind_host
130
+ saddr = Socket.pack_sockaddr_in(0, @bind_host)
131
+ self.class.recvsocket.bind(saddr)
132
+ end
133
+ EM.attach self.class.recvsocket, Handler, self.class.recvsocket
134
+ end
135
+
136
+ # Sets the instance id to a unique 16 bit integer so it can fit inside relevent the ICMP field.
137
+ # Also adds self to the pool so that incoming messages that it requested can be delivered.
138
+ def set_id
139
+ while @id.nil?
140
+ id = rand(65535)
141
+ unless self.class.instances[id]
142
+ @id = id
143
+ self.class.instances[@id] = self
144
+ end
145
+ end
146
+ end
147
+
148
+ end
149
+
150
+ end
data/lib/icmp4em.rb ADDED
@@ -0,0 +1,6 @@
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'
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: icmp4em
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Jake Douglas
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-01-12 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: eventmachine
17
+ 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
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - README
35
+ - examples/simple_example.rb
36
+ - examples/stateful_example.rb
37
+ - lib/icmp4em.rb
38
+ - lib/icmp4em/common.rb
39
+ - lib/icmp4em/handler.rb
40
+ - lib/icmp4em/icmpv4.rb
41
+ has_rdoc: false
42
+ homepage: http://www.github.com/yakischloba/icmp4em
43
+ post_install_message:
44
+ rdoc_options: []
45
+
46
+ require_paths:
47
+ - 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:
60
+ requirements: []
61
+
62
+ rubyforge_project: icmp4em
63
+ rubygems_version: 1.2.0
64
+ signing_key:
65
+ specification_version: 2
66
+ summary: Asynchronous implementation of ICMP ping using EventMachine
67
+ test_files: []
68
+