icmp4em 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+