arpoon 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.md +104 -0
- data/arpoon.gemspec +20 -0
- data/bin/arpoon +57 -0
- data/lib/arpoon.rb +194 -0
- data/lib/arpoon/client.rb +38 -0
- data/lib/arpoon/controller.rb +44 -0
- data/lib/arpoon/interface.rb +108 -0
- data/lib/arpoon/packet.rb +88 -0
- data/lib/arpoon/route.rb +125 -0
- data/lib/arpoon/table.rb +100 -0
- data/lib/arpoon/version.rb +15 -0
- metadata +104 -0
data/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
arpoon - man the harpoons, kill that ARP whale
|
|
2
|
+
==============================================
|
|
3
|
+
arpoon is a simple daemon that notifies about ARP packets, it can be used to implement
|
|
4
|
+
anti ARP spoofing stuff or whatever.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Examples
|
|
8
|
+
--------
|
|
9
|
+
|
|
10
|
+
Anti ARP spoofing:
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
gateways = {}
|
|
14
|
+
danger = []
|
|
15
|
+
|
|
16
|
+
# command that gets the interface name that got connected,
|
|
17
|
+
# it's gonna be used as hook for network managers and the like
|
|
18
|
+
# to tell arpoon about new interfaces or reconnected interfaces
|
|
19
|
+
command :connected do |name|
|
|
20
|
+
interface(name) # create the interface if it's not present yet
|
|
21
|
+
|
|
22
|
+
reload_table! # reload the ARP table
|
|
23
|
+
|
|
24
|
+
# get the ARP table entry for the gateway and cleanup danger notifications
|
|
25
|
+
gateways[interface] = table[gateway_for(name)]
|
|
26
|
+
|
|
27
|
+
command :disconnected, name
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
command :disconnected do |name|
|
|
31
|
+
danger.reject! { |a| a[0] == name }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# this command can be used by scripts to check for danger notifications and show
|
|
35
|
+
# them to the user
|
|
36
|
+
command :danger? do
|
|
37
|
+
send_response danger.map { |a, b| { interface: a, attacker: b } }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# for any interface, already present or newly created
|
|
41
|
+
any do
|
|
42
|
+
# when we receive an ARP reply
|
|
43
|
+
on :reply do |packet, interface|
|
|
44
|
+
# return unless we have a gateway for the interface
|
|
45
|
+
next unless gateway = gateways[interface.name]
|
|
46
|
+
|
|
47
|
+
# if the packet saying the IP for the gateway has a different MAC
|
|
48
|
+
# address someone is doing something fishy, so notify the danger
|
|
49
|
+
if packet.sender.ip == gateway.ip && packet.sender.mac != gateway.mac
|
|
50
|
+
unless danger.include?(current = [interface.name, packet.sender.mac])
|
|
51
|
+
danger << current
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# setup the already present devices and gateways
|
|
58
|
+
route.each {|entry|
|
|
59
|
+
next unless entry.gateway?
|
|
60
|
+
|
|
61
|
+
interface(entry.device)
|
|
62
|
+
gateways[entry.device] = table[entry.gateway]
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Init scripts
|
|
67
|
+
------------
|
|
68
|
+
|
|
69
|
+
Arch Linux:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
#! /bin/bash
|
|
73
|
+
|
|
74
|
+
. /etc/rc.conf
|
|
75
|
+
. /etc/rc.d/functions
|
|
76
|
+
|
|
77
|
+
case "$1" in
|
|
78
|
+
start)
|
|
79
|
+
stat_busy "Starting arpoon"
|
|
80
|
+
pkill -f "ruby.*arpoon" &> /dev/null
|
|
81
|
+
arpoon &> /dev/null &
|
|
82
|
+
add_daemon arpoon
|
|
83
|
+
stat_done
|
|
84
|
+
;;
|
|
85
|
+
|
|
86
|
+
stop)
|
|
87
|
+
stat_busy "Stopping arpoon"
|
|
88
|
+
pkill -f "ruby.*arpoon" &> /dev/nunll
|
|
89
|
+
rm_daemon arpoon
|
|
90
|
+
stat_done
|
|
91
|
+
;;
|
|
92
|
+
|
|
93
|
+
restart)
|
|
94
|
+
$0 stop
|
|
95
|
+
sleep 1
|
|
96
|
+
$0 start
|
|
97
|
+
;;
|
|
98
|
+
|
|
99
|
+
*)
|
|
100
|
+
echo "usage: $0 {start|stop|restart}"
|
|
101
|
+
esac
|
|
102
|
+
|
|
103
|
+
exit 0
|
|
104
|
+
```
|
data/arpoon.gemspec
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Kernel.load 'lib/arpoon/version.rb'
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new {|s|
|
|
4
|
+
s.name = 'arpoon'
|
|
5
|
+
s.version = Arpoon.version
|
|
6
|
+
s.author = 'meh.'
|
|
7
|
+
s.email = 'meh@paranoici.org'
|
|
8
|
+
s.homepage = 'http://github.com/meh/arpoon'
|
|
9
|
+
s.platform = Gem::Platform::RUBY
|
|
10
|
+
s.summary = 'ARP changes reporting daemon, can be used to protect against spoofing.'
|
|
11
|
+
|
|
12
|
+
s.files = `git ls-files`.split("\n")
|
|
13
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
|
14
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
15
|
+
s.require_paths = ['lib']
|
|
16
|
+
|
|
17
|
+
s.add_dependency 'bitmap'
|
|
18
|
+
s.add_dependency 'hwaddr'
|
|
19
|
+
s.add_dependency 'ffi-pcap', '>=0.2.1'
|
|
20
|
+
}
|
data/bin/arpoon
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#! /usr/bin/env ruby
|
|
2
|
+
require 'eventmachine'
|
|
3
|
+
require 'arpoon'
|
|
4
|
+
require 'optparse'
|
|
5
|
+
|
|
6
|
+
options = {}
|
|
7
|
+
|
|
8
|
+
OptionParser.new do |o|
|
|
9
|
+
options[:socket] = '/var/run/arpoon.ctl'
|
|
10
|
+
|
|
11
|
+
o.on '-e', '--execute', 'enable execute mode' do
|
|
12
|
+
options[:execute] = true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
o.on '-s', '--socket PATH', 'path to the UNIX socket' do |value|
|
|
16
|
+
options[:socket] = File.expand_path(value)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
o.on '-r', '--raw', 'send a raw JSON string' do |value|
|
|
20
|
+
options[:raw] = true
|
|
21
|
+
end
|
|
22
|
+
end.parse!
|
|
23
|
+
|
|
24
|
+
if options[:execute]
|
|
25
|
+
require 'arpoon/client'
|
|
26
|
+
|
|
27
|
+
Arpoon::Client.new(options[:socket]).tap {|c|
|
|
28
|
+
if options[:raw]
|
|
29
|
+
c.puts ARGV.join ' '
|
|
30
|
+
else
|
|
31
|
+
c.send_request *ARGV
|
|
32
|
+
end
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
UNIXSocket.new(options[:socket]).tap {|s|
|
|
36
|
+
s.puts options[:raw] ? ARGV.join(' ') : ARGV.to_json
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
exit!
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
EM.run {
|
|
43
|
+
a = Arpoon.load(ARGV.first || '/etc/arpoon.rc')
|
|
44
|
+
a.start
|
|
45
|
+
|
|
46
|
+
EM.error_handler {|e|
|
|
47
|
+
a.log e
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
%w[INT KILL].each {|sig|
|
|
51
|
+
trap sig do
|
|
52
|
+
a.stop
|
|
53
|
+
|
|
54
|
+
EM.stop_event_loop
|
|
55
|
+
end
|
|
56
|
+
}
|
|
57
|
+
}
|
data/lib/arpoon.rb
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
3
|
+
# Version 2, December 2004
|
|
4
|
+
#
|
|
5
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
6
|
+
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
7
|
+
#
|
|
8
|
+
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
|
9
|
+
#++
|
|
10
|
+
|
|
11
|
+
require 'singleton'
|
|
12
|
+
require 'eventmachine'
|
|
13
|
+
require 'stringio'
|
|
14
|
+
|
|
15
|
+
require 'arpoon/table'
|
|
16
|
+
require 'arpoon/route'
|
|
17
|
+
require 'arpoon/interface'
|
|
18
|
+
require 'arpoon/packet'
|
|
19
|
+
require 'arpoon/controller'
|
|
20
|
+
|
|
21
|
+
class Arpoon
|
|
22
|
+
include Singleton
|
|
23
|
+
|
|
24
|
+
def self.method_missing (*args, &block)
|
|
25
|
+
instance.__send__ *args, &block
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
attr_reader :interfaces
|
|
29
|
+
|
|
30
|
+
def initialize
|
|
31
|
+
@commands = {}
|
|
32
|
+
@connections = []
|
|
33
|
+
@interfaces = {}
|
|
34
|
+
@any = []
|
|
35
|
+
|
|
36
|
+
reload_table!
|
|
37
|
+
|
|
38
|
+
any {
|
|
39
|
+
on :packet do |packet|
|
|
40
|
+
if packet.request?
|
|
41
|
+
packet.interface.fire :request, packet, packet.interface
|
|
42
|
+
else
|
|
43
|
+
packet.interface.fire :reply, packet, packet.interface
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if packet.destination.broadcast?
|
|
47
|
+
packet.interface.fire :broadcast, packet, packet.interface
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
controller_at '/var/run/arpoon.ctl'
|
|
53
|
+
logs_at '/var/log/arpoon.log'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def controller_at (path)
|
|
57
|
+
@controller_at = File.expand_path(path)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def logs_at (path)
|
|
61
|
+
@logs_at = File.expand_path(path)
|
|
62
|
+
end
|
|
63
|
+
def started?; @started; end
|
|
64
|
+
|
|
65
|
+
def log (what, group = nil)
|
|
66
|
+
io = StringIO.new
|
|
67
|
+
|
|
68
|
+
io.print "[#{Time.now}#{", #{group}" if group}] "
|
|
69
|
+
|
|
70
|
+
if what.is_a? Exception
|
|
71
|
+
io.puts "#{what.class.name}: #{what.message}"
|
|
72
|
+
io.puts what.backtrace
|
|
73
|
+
else
|
|
74
|
+
io.puts what
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
io.puts ''
|
|
78
|
+
io.seek 0
|
|
79
|
+
|
|
80
|
+
io.read.tap {|text|
|
|
81
|
+
$stderr.puts text
|
|
82
|
+
|
|
83
|
+
File.open(@logs_at, 'a') { |f| f.print text }
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def start
|
|
88
|
+
return if started?
|
|
89
|
+
|
|
90
|
+
@started = true
|
|
91
|
+
|
|
92
|
+
@interfaces.each_value {|interface|
|
|
93
|
+
interface.start
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
File.umask(0).tap {|old|
|
|
97
|
+
begin
|
|
98
|
+
@signature = EM.start_server(@controller_at, Controller)
|
|
99
|
+
ensure
|
|
100
|
+
File.umask(old)
|
|
101
|
+
end
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def stop
|
|
106
|
+
return unless started?
|
|
107
|
+
|
|
108
|
+
@interfaces.each_value {|interface|
|
|
109
|
+
interface.stop
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if @signature
|
|
113
|
+
EM.stop_server @signature
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
@started = false
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def table
|
|
120
|
+
@table || reload_table!
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def reload_table!
|
|
124
|
+
@table = Table.new
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def route
|
|
128
|
+
Route.new
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def load (path = nil, &block)
|
|
132
|
+
if path
|
|
133
|
+
instance_eval File.read(File.expand_path(path)), path, 1
|
|
134
|
+
else
|
|
135
|
+
instance_exec &block
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
self
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def connected (controller)
|
|
142
|
+
@connections << controller
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def disconnected (controller)
|
|
146
|
+
@connections.delete(controller)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def broadcast (*args)
|
|
150
|
+
@connections.each {|conn|
|
|
151
|
+
conn.send_response(*args)
|
|
152
|
+
}
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def command (*args, &block)
|
|
156
|
+
if block
|
|
157
|
+
command = args.first.to_sym
|
|
158
|
+
|
|
159
|
+
@commands[command] = block
|
|
160
|
+
else
|
|
161
|
+
controller = args.shift
|
|
162
|
+
command = args.shift
|
|
163
|
+
|
|
164
|
+
controller.instance_exec *args, &@commands[command.to_sym]
|
|
165
|
+
end
|
|
166
|
+
rescue Exception => e
|
|
167
|
+
log e, "command: #{command}"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def interface (name, &block)
|
|
171
|
+
unless @interfaces.member? name
|
|
172
|
+
@interfaces[name] = Interface.new(name)
|
|
173
|
+
|
|
174
|
+
@any.each {|block|
|
|
175
|
+
@interfaces[name].load(&block)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
EM.schedule {
|
|
179
|
+
@interfaces[name].start if started?
|
|
180
|
+
}
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
@interfaces[name].load(&block) if block
|
|
184
|
+
@interfaces[name]
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def any (&block)
|
|
188
|
+
@interfaces.each_value {|interface|
|
|
189
|
+
interface.load(&block)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
@any << block
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
3
|
+
# Version 2, December 2004
|
|
4
|
+
#
|
|
5
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
6
|
+
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
7
|
+
#
|
|
8
|
+
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
|
9
|
+
#++
|
|
10
|
+
|
|
11
|
+
require 'socket'
|
|
12
|
+
require 'json'
|
|
13
|
+
|
|
14
|
+
class Arpoon
|
|
15
|
+
|
|
16
|
+
class Client
|
|
17
|
+
def initialize (path = '/var/run/arpoon.ctl')
|
|
18
|
+
@socket = UNIXSocket.new(File.expand_path(path))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def respond_to_missing? (*args)
|
|
22
|
+
@socket.respond_to? *args
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def method_missing (*args, &block)
|
|
26
|
+
@socket.__send__ *args, &block
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def send_request (name, *args)
|
|
30
|
+
self.puts [name, args].to_json
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def read_response
|
|
34
|
+
JSON.parse(?[ + self.readline + ?]).first
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
3
|
+
# Version 2, December 2004
|
|
4
|
+
#
|
|
5
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
6
|
+
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
7
|
+
#
|
|
8
|
+
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
|
9
|
+
#++
|
|
10
|
+
|
|
11
|
+
require 'eventmachine'
|
|
12
|
+
require 'json'
|
|
13
|
+
|
|
14
|
+
class Arpoon
|
|
15
|
+
|
|
16
|
+
class Controller < EventMachine::Protocols::LineAndTextProtocol
|
|
17
|
+
def post_init
|
|
18
|
+
connected self
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def receive_line (line)
|
|
22
|
+
command(self, *JSON.parse(line))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def method_missing (*args, &block)
|
|
26
|
+
Arpoon.__send__(*args, &block)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def send_line (line)
|
|
30
|
+
raise ArgumentError, 'the line already has a newline character' if line.include? "\n"
|
|
31
|
+
|
|
32
|
+
send_data line.dup.force_encoding('BINARY') << "\r\n"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def send_response (*arguments)
|
|
36
|
+
send_line (arguments.length == 1 ? arguments.first : arguments).to_json
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def unbind
|
|
40
|
+
disconnected self
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
3
|
+
# Version 2, December 2004
|
|
4
|
+
#
|
|
5
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
6
|
+
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
7
|
+
#
|
|
8
|
+
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
|
9
|
+
#++
|
|
10
|
+
|
|
11
|
+
require 'ffi/pcap'
|
|
12
|
+
|
|
13
|
+
require 'arpoon/packet'
|
|
14
|
+
|
|
15
|
+
class Arpoon
|
|
16
|
+
|
|
17
|
+
class Interface
|
|
18
|
+
class Handler < EM::Connection
|
|
19
|
+
def initialize (interface)
|
|
20
|
+
@interface = interface
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def notify_readable (*)
|
|
24
|
+
@interface.capture.dispatch {|_, packet|
|
|
25
|
+
begin
|
|
26
|
+
next unless packet = Packet.unpack(packet.body, @interface) rescue nil
|
|
27
|
+
|
|
28
|
+
@interface.fire :packet, packet
|
|
29
|
+
rescue Exception => e
|
|
30
|
+
Arpoon.log e, "interface: #{@interface.name}"
|
|
31
|
+
end
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
attr_reader :name, :capture
|
|
37
|
+
|
|
38
|
+
def initialize (name, &block)
|
|
39
|
+
@name = name.to_s
|
|
40
|
+
@events = Hash.new { |h, k| h[k] = [] }
|
|
41
|
+
|
|
42
|
+
load &block if block
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def method_missing (*args, &block)
|
|
46
|
+
Arpoon.__send__ *args, &block
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def start
|
|
50
|
+
@capture = FFI::PCap::Live.new(device: @name.to_s, promisc: true, handler: FFI::PCap::Handler)
|
|
51
|
+
@capture.nonblocking = true
|
|
52
|
+
@capture.setfilter('arp')
|
|
53
|
+
|
|
54
|
+
@handler = EM.watch @capture.selectable_fd, Handler, self
|
|
55
|
+
@handler.notify_readable = true
|
|
56
|
+
|
|
57
|
+
self
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def stop
|
|
61
|
+
@handler.detach
|
|
62
|
+
|
|
63
|
+
self
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def load (path = nil, &block)
|
|
67
|
+
if path
|
|
68
|
+
instance_eval File.read(File.expand_path(path)), path, 1
|
|
69
|
+
else
|
|
70
|
+
instance_exec &block
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
self
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def on (name = :anything, &block)
|
|
77
|
+
@events[name] << block
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def fire (name, *args)
|
|
81
|
+
delete = []
|
|
82
|
+
|
|
83
|
+
@events[name].each {|block|
|
|
84
|
+
case block.call(*args)
|
|
85
|
+
when :delete then delete << block
|
|
86
|
+
when :stop then break
|
|
87
|
+
end
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@events[name] -= delete
|
|
91
|
+
|
|
92
|
+
self
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def to_s
|
|
96
|
+
name
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def to_sym
|
|
100
|
+
name.to_sym
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def inspect
|
|
104
|
+
"#<#{self.class.name}: #{name}>"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
3
|
+
# Version 2, December 2004
|
|
4
|
+
#
|
|
5
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
6
|
+
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
7
|
+
#
|
|
8
|
+
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
|
9
|
+
#++
|
|
10
|
+
|
|
11
|
+
require 'hwaddr'
|
|
12
|
+
require 'ipaddr'
|
|
13
|
+
|
|
14
|
+
class Arpoon
|
|
15
|
+
|
|
16
|
+
class Packet
|
|
17
|
+
Operations = {
|
|
18
|
+
1 => :request,
|
|
19
|
+
2 => :reply
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
def self.unpack (data, interface = nil)
|
|
23
|
+
source = HWAddr.new(data[0, 6].unpack('C6'))
|
|
24
|
+
destination = HWAddr.new(data[6, 6].unpack('C6'))
|
|
25
|
+
|
|
26
|
+
unless data[12, 2].unpack('n').first == 0x0806
|
|
27
|
+
raise ArgumentError, 'the passed data is not an ARP packet'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
unless data[16, 2].unpack('n').first == 0x0800
|
|
31
|
+
raise ArgumentError, 'the passed data is not using the IP protocol'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
hw_size = data[18, 1].unpack('C').first
|
|
35
|
+
pr_size = data[19, 1].unpack('C').first
|
|
36
|
+
|
|
37
|
+
if hw_size != 6
|
|
38
|
+
raise ArgumentError, "#{hw_size} is an unsupported size"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
opcode = data[20, 2].unpack('n').first
|
|
42
|
+
|
|
43
|
+
if pr_size == 4
|
|
44
|
+
sender_hw = HWAddr.new(data[22, 6].unpack('C6'))
|
|
45
|
+
sender_ip = IPAddr.new_ntoh(data[28, 4])
|
|
46
|
+
|
|
47
|
+
target_hw = HWAddr.new(data[32, 6].unpack('C6'))
|
|
48
|
+
target_ip = IPAddr.new_ntoh(data[38, 4])
|
|
49
|
+
elsif pr_size == 16
|
|
50
|
+
sender_hw = HWAddr.new(data[22, 6].unpack('C6'))
|
|
51
|
+
sender_ip = IPAddr.new_ntoh(data[28, 16])
|
|
52
|
+
|
|
53
|
+
target_hw = HWAddr.new(data[44, 6].unpack('C6'))
|
|
54
|
+
target_ip = IPAddr.new_ntoh(data[50, 16])
|
|
55
|
+
else
|
|
56
|
+
raise ArgumentError, "#{pr_size} is an unsupported protocol size"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
new(interface, source, destination, Operations[opcode], sender_hw, sender_ip, target_hw, target_ip)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
attr_reader :interface, :source, :destination, :sender, :target
|
|
63
|
+
|
|
64
|
+
def initialize (interface = nil, source, destination, operation, sender_hw, sender_ip, target_hw, target_ip)
|
|
65
|
+
@interface = interface
|
|
66
|
+
@source = source
|
|
67
|
+
@destination = destination
|
|
68
|
+
|
|
69
|
+
@operation = operation.downcase
|
|
70
|
+
|
|
71
|
+
@sender = Struct.new(:mac, :ip).new(sender_hw, sender_ip)
|
|
72
|
+
@target = Struct.new(:mac, :ip).new(target_hw, target_ip)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def request?
|
|
76
|
+
@operation == :request
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def reply?
|
|
80
|
+
@operation == :reply
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def to_sym
|
|
84
|
+
@operation
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
end
|
data/lib/arpoon/route.rb
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
3
|
+
# Version 2, December 2004
|
|
4
|
+
#
|
|
5
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
6
|
+
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
7
|
+
#
|
|
8
|
+
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
|
9
|
+
#++
|
|
10
|
+
|
|
11
|
+
require 'ipaddr'
|
|
12
|
+
require 'bitmap'
|
|
13
|
+
|
|
14
|
+
class Arpoon
|
|
15
|
+
|
|
16
|
+
class Route
|
|
17
|
+
Flags = Bitmap.new(
|
|
18
|
+
up: 0x0001,
|
|
19
|
+
gateway: 0x0002,
|
|
20
|
+
|
|
21
|
+
host: 0x0004,
|
|
22
|
+
reinstate: 0x0008,
|
|
23
|
+
dynamic: 0x0010,
|
|
24
|
+
modified: 0x0020,
|
|
25
|
+
mtu: 0x0040,
|
|
26
|
+
window: 0x0080,
|
|
27
|
+
irtt: 0x0100,
|
|
28
|
+
reject: 0x0200,
|
|
29
|
+
static: 0x0400,
|
|
30
|
+
xresolve: 0x0800,
|
|
31
|
+
no_forward: 0x1000,
|
|
32
|
+
throw: 0x2000,
|
|
33
|
+
no_pmt_udisc: 0x4000,
|
|
34
|
+
|
|
35
|
+
default: 0x00010000,
|
|
36
|
+
all_on_link: 0x00020000,
|
|
37
|
+
addrconf: 0x00040000,
|
|
38
|
+
|
|
39
|
+
linkrt: 0x00100000,
|
|
40
|
+
no_next_hop: 0x00200000,
|
|
41
|
+
|
|
42
|
+
cache: 0x01000000,
|
|
43
|
+
flow: 0x02000000,
|
|
44
|
+
policy: 0x0400000
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
class Entry < Struct.new(:device, :destination, :gateway, :flags, :ref_count, :use, :metric, :mask, :mtu, :window, :irrt)
|
|
48
|
+
def destination
|
|
49
|
+
IPAddr.new_ntoh([super.to_i(16)].pack('N').reverse)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def gateway
|
|
53
|
+
IPAddr.new_ntoh([super.to_i(16)].pack('N').reverse)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def flags
|
|
57
|
+
Flags[super.to_i(16)]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
Flags.fields.each {|name|
|
|
61
|
+
define_method "#{name}?" do
|
|
62
|
+
flags.has? name
|
|
63
|
+
end
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
def ref_count
|
|
67
|
+
super.to_i
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def use?
|
|
71
|
+
super != '0'
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def metric?
|
|
75
|
+
super != '0'
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def mtu
|
|
79
|
+
super.to_i
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def window
|
|
83
|
+
super.to_i
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def irrt
|
|
87
|
+
super.to_i
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def default_gateway?
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
include Enumerable
|
|
95
|
+
|
|
96
|
+
def initialize
|
|
97
|
+
@entries = []
|
|
98
|
+
|
|
99
|
+
File.open('/proc/net/route', 'r').each_line {|line|
|
|
100
|
+
next if line.start_with? 'Iface'
|
|
101
|
+
|
|
102
|
+
@entries << Entry.new(*line.split(/\s+/))
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def each (device = nil)
|
|
107
|
+
return enum_for :each, device unless block_given?
|
|
108
|
+
|
|
109
|
+
@entries.each {|entry|
|
|
110
|
+
yield entry if !device || device.to_s == entry.device
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
self
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def gateway_for (device)
|
|
117
|
+
each(device) {|entry|
|
|
118
|
+
return entry.gateway if entry.gateway?
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
end
|
data/lib/arpoon/table.rb
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
3
|
+
# Version 2, December 2004
|
|
4
|
+
#
|
|
5
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
6
|
+
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
7
|
+
#
|
|
8
|
+
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
|
9
|
+
#++
|
|
10
|
+
|
|
11
|
+
require 'ipaddr'
|
|
12
|
+
require 'hwaddr'
|
|
13
|
+
require 'bitmap'
|
|
14
|
+
|
|
15
|
+
class Arpoon
|
|
16
|
+
|
|
17
|
+
class Table
|
|
18
|
+
Types = {
|
|
19
|
+
0 => :netrom,
|
|
20
|
+
1 => :ether,
|
|
21
|
+
2 => :eether,
|
|
22
|
+
3 => :ax25,
|
|
23
|
+
4 => :pronet,
|
|
24
|
+
5 => :chaos,
|
|
25
|
+
6 => :ieee802,
|
|
26
|
+
7 => :arcnet,
|
|
27
|
+
8 => :appletlk,
|
|
28
|
+
15 => :dlci,
|
|
29
|
+
19 => :atm,
|
|
30
|
+
23 => :metricom,
|
|
31
|
+
24 => :ieee1394,
|
|
32
|
+
27 => :eui64,
|
|
33
|
+
32 => :infiniband
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
Flags = Bitmap.new(
|
|
37
|
+
completed: 0x02,
|
|
38
|
+
permanent: 0x04,
|
|
39
|
+
publish: 0x08,
|
|
40
|
+
|
|
41
|
+
has_requested_trailers: 0x10,
|
|
42
|
+
wants_netmask: 0x20,
|
|
43
|
+
|
|
44
|
+
dont_publish: 0x40
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
class Entry < Struct.new(:ip, :type, :flags, :mac, :mask, :device)
|
|
48
|
+
def ip
|
|
49
|
+
IPAddr.new(super)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def mac
|
|
53
|
+
HWAddr.new(super)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def type
|
|
57
|
+
Types[super.to_i(16)]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def flags
|
|
61
|
+
Flags[super.to_i(16)]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
Flags.fields.each {|name|
|
|
65
|
+
define_method "#{name}?" do
|
|
66
|
+
flags.has? name
|
|
67
|
+
end
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
include Enumerable
|
|
72
|
+
|
|
73
|
+
def initialize
|
|
74
|
+
@entries = []
|
|
75
|
+
|
|
76
|
+
File.open('/proc/net/arp', 'r').each_line {|line|
|
|
77
|
+
next if line.start_with? 'IP address'
|
|
78
|
+
|
|
79
|
+
@entries << Entry.new(*line.split(/\s+/))
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def each (device = nil)
|
|
84
|
+
return enum_for :each, device unless block_given?
|
|
85
|
+
|
|
86
|
+
@entries.each {|entry|
|
|
87
|
+
yield entry if !device || device.to_s == entry.device
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
self
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def [] (what)
|
|
94
|
+
find {|entry|
|
|
95
|
+
what == entry.ip || what == entry.mac
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
3
|
+
# Version 2, December 2004
|
|
4
|
+
#
|
|
5
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
6
|
+
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
7
|
+
#
|
|
8
|
+
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
|
9
|
+
#++
|
|
10
|
+
|
|
11
|
+
class Arpoon
|
|
12
|
+
def self.version
|
|
13
|
+
'0.0.1'
|
|
14
|
+
end
|
|
15
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: arpoon
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
prerelease:
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- meh.
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2012-07-18 00:00:00.000000000 Z
|
|
13
|
+
dependencies:
|
|
14
|
+
- !ruby/object:Gem::Dependency
|
|
15
|
+
name: bitmap
|
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
|
17
|
+
none: false
|
|
18
|
+
requirements:
|
|
19
|
+
- - ! '>='
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '0'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
none: false
|
|
26
|
+
requirements:
|
|
27
|
+
- - ! '>='
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '0'
|
|
30
|
+
- !ruby/object:Gem::Dependency
|
|
31
|
+
name: hwaddr
|
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
|
33
|
+
none: false
|
|
34
|
+
requirements:
|
|
35
|
+
- - ! '>='
|
|
36
|
+
- !ruby/object:Gem::Version
|
|
37
|
+
version: '0'
|
|
38
|
+
type: :runtime
|
|
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: ffi-pcap
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
none: false
|
|
50
|
+
requirements:
|
|
51
|
+
- - ! '>='
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: 0.2.1
|
|
54
|
+
type: :runtime
|
|
55
|
+
prerelease: false
|
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
57
|
+
none: false
|
|
58
|
+
requirements:
|
|
59
|
+
- - ! '>='
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: 0.2.1
|
|
62
|
+
description:
|
|
63
|
+
email: meh@paranoici.org
|
|
64
|
+
executables:
|
|
65
|
+
- arpoon
|
|
66
|
+
extensions: []
|
|
67
|
+
extra_rdoc_files: []
|
|
68
|
+
files:
|
|
69
|
+
- README.md
|
|
70
|
+
- arpoon.gemspec
|
|
71
|
+
- bin/arpoon
|
|
72
|
+
- lib/arpoon.rb
|
|
73
|
+
- lib/arpoon/client.rb
|
|
74
|
+
- lib/arpoon/controller.rb
|
|
75
|
+
- lib/arpoon/interface.rb
|
|
76
|
+
- lib/arpoon/packet.rb
|
|
77
|
+
- lib/arpoon/route.rb
|
|
78
|
+
- lib/arpoon/table.rb
|
|
79
|
+
- lib/arpoon/version.rb
|
|
80
|
+
homepage: http://github.com/meh/arpoon
|
|
81
|
+
licenses: []
|
|
82
|
+
post_install_message:
|
|
83
|
+
rdoc_options: []
|
|
84
|
+
require_paths:
|
|
85
|
+
- lib
|
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
87
|
+
none: false
|
|
88
|
+
requirements:
|
|
89
|
+
- - ! '>='
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '0'
|
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
93
|
+
none: false
|
|
94
|
+
requirements:
|
|
95
|
+
- - ! '>='
|
|
96
|
+
- !ruby/object:Gem::Version
|
|
97
|
+
version: '0'
|
|
98
|
+
requirements: []
|
|
99
|
+
rubyforge_project:
|
|
100
|
+
rubygems_version: 1.8.24
|
|
101
|
+
signing_key:
|
|
102
|
+
specification_version: 3
|
|
103
|
+
summary: ARP changes reporting daemon, can be used to protect against spoofing.
|
|
104
|
+
test_files: []
|