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 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: []