arpoon 0.0.1

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