arpoon 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|