packetgen 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,215 @@
1
+ require 'ipaddr'
2
+
3
+ module PacketGen
4
+ module Header
5
+
6
+ # IPv6 header class
7
+ # @author Sylvain Daubert
8
+ class IPv6 < Struct.new(:version, :traffic_class, :flow_label, :length,
9
+ :next, :hop, :src, :dst, :body)
10
+ include StructFu
11
+ include HeaderMethods
12
+ extend HeaderClassMethods
13
+
14
+ # IPv6 address, as a group of 8 2-byte words
15
+ # @author Sylvain Daubert
16
+ class Addr < Struct.new(:a1, :a2, :a3, :a4, :a5, :a6, :a7, :a8)
17
+ include StructFu
18
+
19
+ # @param [Hash] options
20
+ # @option options [Integer] :a1
21
+ # @option options [Integer] :a2
22
+ # @option options [Integer] :a3
23
+ # @option options [Integer] :a4
24
+ # @option options [Integer] :a5
25
+ # @option options [Integer] :a6
26
+ # @option options [Integer] :a7
27
+ # @option options [Integer] :a8
28
+ def initialize(options={})
29
+ super Int16.new(options[:a1]),
30
+ Int16.new(options[:a2]),
31
+ Int16.new(options[:a3]),
32
+ Int16.new(options[:a4]),
33
+ Int16.new(options[:a5]),
34
+ Int16.new(options[:a6]),
35
+ Int16.new(options[:a7]),
36
+ Int16.new(options[:a8])
37
+ end
38
+
39
+ # Parse a colon-delimited address
40
+ # @param [String] str
41
+ # @return [self]
42
+ def parse(str)
43
+ return self if str.nil?
44
+ addr = IPAddr.new(str)
45
+ raise ArgumentError, 'string is not a IPv6 address' unless addr.ipv6?
46
+ addri = addr.to_i
47
+ self.a1 = addri >> 112
48
+ self.a2 = addri >> 96 & 0xffff
49
+ self.a3 = addri >> 80 & 0xffff
50
+ self.a4 = addri >> 64 & 0xffff
51
+ self.a5 = addri >> 48 & 0xffff
52
+ self.a6 = addri >> 32 & 0xffff
53
+ self.a7 = addri >> 16 & 0xffff
54
+ self.a8 = addri & 0xffff
55
+ self
56
+ end
57
+
58
+ # Read a Addr6 from a binary string
59
+ # @param [String] str
60
+ # @return [self]
61
+ def read(str)
62
+ force_binary str
63
+ self[:a1].read str[0, 2]
64
+ self[:a2].read str[2, 2]
65
+ self[:a3].read str[4, 2]
66
+ self[:a4].read str[6, 2]
67
+ self[:a5].read str[8, 2]
68
+ self[:a6].read str[10, 2]
69
+ self[:a7].read str[12, 2]
70
+ self[:a8].read str[14, 2]
71
+ self
72
+ end
73
+
74
+ %i(a1 a2 a3 a4 a5 a6 a7 a8).each do |sym|
75
+ class_eval "def #{sym}; self[:#{sym}].to_i; end\n" \
76
+ "def #{sym}=(v); self[:#{sym}].read v; end"
77
+ end
78
+
79
+ # Addr6 in human readable form (colon-delimited hex string)
80
+ # @return [String]
81
+ def to_x
82
+ IPAddr.new(to_a.map { |a| a.to_i.to_s(16) }.join(':')).to_s
83
+ end
84
+ end
85
+
86
+ # @param [Hash] options
87
+ # @option options [Integer] :version
88
+ # @option options [Integer] :traffic_length
89
+ # @option options [Integer] :flow_label
90
+ # @option options [Integer] :length payload length
91
+ # @option options [Integer] :next
92
+ # @option options [Integer] :hop
93
+ # @option options [String] :src colon-delimited source address
94
+ # @option options [String] :dst colon-delimited destination address
95
+ # @option options [String] :body binary string
96
+ def initialize(options={})
97
+ super options[:version] || 6,
98
+ options[:traffic_class] || 0,
99
+ options[:flow_label] || 0,
100
+ Int16.new(options[:length]),
101
+ Int8.new(options[:next]),
102
+ Int8.new(options[:hop] || 64),
103
+ Addr.new.parse(options[:src] || '::1'),
104
+ Addr.new.parse(options[:dst] || '::1'),
105
+ StructFu::String.new.read(options[:body])
106
+ end
107
+
108
+ # Read a IP header from a string
109
+ # @param [String] str binary string
110
+ # @return [self]
111
+ def read(str)
112
+ return self if str.nil?
113
+ raise ParseError, 'string too short for Eth' if str.size < self.sz
114
+ force_binary str
115
+ first32 = str[0, 4].unpack('N').first
116
+ self.version = first32 >> 28
117
+ self.traffic_class = (first32 >> 20) & 0xff
118
+ self.flow_label = first32 & 0xfffff
119
+
120
+ self[:length].read str[4, 2]
121
+ self[:next].read str[6, 1]
122
+ self[:hop].read str[7, 1]
123
+ self[:src].read str[8, 16]
124
+ self[:dst].read str[24, 16]
125
+ self[:body].read str[40..-1]
126
+ self
127
+ end
128
+
129
+ # Compute length and set +len+ field
130
+ # @return [Integer]
131
+ def calc_length
132
+ self.length = body.length
133
+ end
134
+
135
+ # Getter for length attribute
136
+ # @return [Integer]
137
+ def length
138
+ self[:length].to_i
139
+ end
140
+
141
+ # Setter for length attribute
142
+ # @param [Integer] i
143
+ # @return [Integer]
144
+ def length=(i)
145
+ self[:length].read i
146
+ end
147
+
148
+ # Getter for next attribute
149
+ # @return [Integer]
150
+ def next
151
+ self[:next].to_i
152
+ end
153
+
154
+ # Setter for next attribute
155
+ # @param [Integer] i
156
+ # @return [Integer]
157
+ def next=(i)
158
+ self[:next].read i
159
+ end
160
+
161
+ # Getter for hop attribute
162
+ # @return [Integer]
163
+ def hop
164
+ self[:hop].to_i
165
+ end
166
+
167
+ # Setter for hop attribute
168
+ # @param [Integer] i
169
+ # @return [Integer]
170
+ def hop=(i)
171
+ self[:hop].read i
172
+ end
173
+
174
+ # Getter for src attribute
175
+ # @return [String]
176
+ def src
177
+ self[:src].to_x
178
+ end
179
+ alias :source :src
180
+
181
+ # Setter for src attribute
182
+ # @param [String] addr
183
+ # @return [Integer]
184
+ def src=(addr)
185
+ self[:src].parse addr
186
+ end
187
+ alias :source= :src=
188
+
189
+ # Getter for dst attribute
190
+ # @return [String]
191
+ def dst
192
+ self[:dst].to_x
193
+ end
194
+ alias :destination :dst
195
+
196
+ # Setter for dst attribute
197
+ # @param [String] addr
198
+ # @return [Integer]
199
+ def dst=(addr)
200
+ self[:dst].parse addr
201
+ end
202
+ alias :destination= :dst=
203
+
204
+ # Get binary string
205
+ # @return [String]
206
+ def to_s
207
+ first32 = (version << 28) | (traffic_class << 20) | flow_label
208
+ [first32].pack('N') << to_a[3..-1].map { |field| field.to_s }.join
209
+ end
210
+ end
211
+
212
+ Eth.bind_header IPv6, proto: 0x86DD
213
+ IP.bind_header IPv6, proto: 41 # 6to4
214
+ end
215
+ end
@@ -0,0 +1,133 @@
1
+ module PacketGen
2
+ module Header
3
+
4
+ # UDP header class
5
+ # @author Sylvain Daubert
6
+ class UDP < Struct.new(:sport, :dport, :length, :sum, :body)
7
+ include StructFu
8
+ include HeaderMethods
9
+ extend HeaderClassMethods
10
+
11
+ # IP protocol number for UDP
12
+ IP_PROTOCOL = 17
13
+
14
+ # @param [Hash] options
15
+ # @option options [Integer] :sport source port
16
+ # @option options [Integer] :dport destination port
17
+ # @option options [Integer] :length UDP length. Default: calculated
18
+ # @option options [Integer] :sum. UDP checksum. Default: 0
19
+ def initialize(options={})
20
+ super Int16.new(options[:sport]),
21
+ Int16.new(options[:dport]),
22
+ Int16.new(options[:length]),
23
+ Int16.new(options[:sum]),
24
+ StructFu::String.new.read(options[:body])
25
+ unless options[:length]
26
+ calc_length
27
+ end
28
+ end
29
+
30
+ # Read a IP header from a string
31
+ # @param [String] str binary string
32
+ # @return [self]
33
+ def read(str)
34
+ return self if str.nil?
35
+ raise ParseError, 'string too short for Eth' if str.size < self.sz
36
+ force_binary str
37
+ self[:sport].read str[0, 2]
38
+ self[:dport].read str[2, 2]
39
+ self[:length].read str[4, 2]
40
+ self[:sum].read str[6, 2]
41
+ self[:body].read str[8..-1]
42
+ end
43
+
44
+ # Compute checksum and set +sum+ field
45
+ # @return [Integer]
46
+ def calc_sum
47
+ ip = ip_header(self)
48
+ sum = ip[:src].to_i >> 16
49
+ sum += ip[:src].to_i & 0xffff
50
+ sum += ip[:dst].to_i >> 16
51
+ sum += ip[:dst].to_i & 0xffff
52
+ sum += IP_PROTOCOL
53
+ sum += length
54
+ sum += sport
55
+ sum += dport
56
+ sum += length
57
+ payload = body.to_s
58
+ payload << "\x00" unless payload.size % 2 == 0
59
+ payload.unpack('n*').each { |x| sum += x }
60
+
61
+ while sum > 0xffff do
62
+ sum = (sum & 0xffff) + (sum >> 16)
63
+ end
64
+ sum = ~sum & 0xffff
65
+ self[:sum].value = (sum == 0) ? 0xffff : sum
66
+ end
67
+
68
+ # Compute length and set +length+ field
69
+ # @return [Integer]
70
+ def calc_length
71
+ self[:length].value = self.sz
72
+ end
73
+
74
+ # Getter for source port
75
+ # @return [Integer]
76
+ def sport
77
+ self[:sport].to_i
78
+ end
79
+ alias :source_port :sport
80
+
81
+ # Setter for source port
82
+ # @param [Integer] port
83
+ # @return [Integer]
84
+ def sport=(port)
85
+ self[:sport].read port
86
+ end
87
+ alias :source_port= :sport=
88
+
89
+ # Getter for destination port
90
+ # @return [Integer]
91
+ def dport
92
+ self[:dport].to_i
93
+ end
94
+ alias :destination_port :dport
95
+
96
+ # Setter for destination port
97
+ # @param [Integer] port
98
+ # @return [Integer]
99
+ def dport=(port)
100
+ self[:dport].read port
101
+ end
102
+ alias :destination_port= :dport=
103
+
104
+ # Getter for length attribuute
105
+ # @return [Integer]
106
+ def length
107
+ self[:length].to_i
108
+ end
109
+
110
+ # Setter for length attribuute
111
+ # @param [Integer] port
112
+ # @return [Integer]
113
+ def length=(len)
114
+ self[:length].read len
115
+ end
116
+
117
+ # Getter for sum attribuute
118
+ # @return [Integer]
119
+ def sum
120
+ self[:sum].to_i
121
+ end
122
+
123
+ # Setter for sum attribuute
124
+ # @param [Integer] sum
125
+ # @return [Integer]
126
+ def sum=(sum)
127
+ self[:sum].read sum
128
+ end
129
+ end
130
+
131
+ IP.bind_header UDP, proto: UDP::IP_PROTOCOL
132
+ end
133
+ end
@@ -0,0 +1,357 @@
1
+ require 'pcaprub'
2
+
3
+ module PacketGen
4
+
5
+ # An object of type {Packet} handles a network packet. This packet may contain
6
+ # multiple protocol headers, starting from MAC layer or from Network (OSI) layer.
7
+ #
8
+ # Creating a packet is fairly simple:
9
+ # Packet.gen 'IP', src: '192.168.1.1', dst: '192.168.1.2'
10
+ #
11
+ # == Create a packet
12
+ # Packets may be hand-made or parsed from a binary string:
13
+ # Packet.gen('IP', src: '192.168.1.1', dst: '192.168.1.2').add('UDP', sport: 45000, dport: 23)
14
+ # Packet.parse(binary_string)
15
+ #
16
+ # == Access packet information
17
+ # pkt = Packet.gen('IP').add('UDP')
18
+ # # read information
19
+ # pkt.udp.sport
20
+ # pkt.ip.ttl
21
+ # # set information
22
+ # pkt.udp.dport = 2323
23
+ # pkt.ip.ttl = 1
24
+ # pkt.ip(ttl: 1, id: 1234)
25
+ #
26
+ # == Save a packet to a file
27
+ # pkt.write('file.pcapng')
28
+ #
29
+ # == Get packets
30
+ # Packets may be captured from wire:
31
+ # Packet.capture('eth0') do |packet|
32
+ # do_some_stuffs
33
+ # end
34
+ # packets = Packet.capture('eth0', max: 5) # get 5 packets
35
+ #
36
+ # Packets may also be read from a file:
37
+ # packets = Packet.read(file.pcapng)
38
+ #
39
+ # == Save packets to a file
40
+ # Packet.write 'file.pcapng', packets
41
+ class Packet
42
+ # @return [Array<Header::Base]
43
+ attr_reader :headers
44
+
45
+ # @private maximum number of characters on a line for INSPECT
46
+ INSPECT_MAX_WIDTH = 70
47
+
48
+ # Create a new Packet
49
+ # @param [String] protocol base protocol for packet
50
+ # @param [Hash] options specific options for +protocol+
51
+ # @return [Packet]
52
+ def self.gen(protocol, options={})
53
+ self.new.add protocol, options
54
+ end
55
+
56
+ # Parse a binary string and generate a Packet from it.
57
+ # # auto-detect first header
58
+ # Packet.parse str
59
+ # # force decoding a Ethernet header for first header
60
+ # Packet.parse str, first_header: 'Eth'
61
+ # @param [String] binary_str
62
+ # @param [String,nil] first_header First protocol header. +nil+ means discover it!
63
+ # @return [Packet]
64
+ # @raise [ArgumentError] +first_header+ is an unknown header
65
+ def self.parse(binary_str, first_header: nil)
66
+ pkt = new
67
+
68
+ if first_header.nil?
69
+ # No decoding forced for first header. Have to guess it!
70
+ Header.all.each do |hklass|
71
+ hdr = hklass.new
72
+ hdr.read binary_str
73
+ # First header is found when:
74
+ # * for one known header,
75
+ # * it exists a known binding with a upper header
76
+ hklass.known_headers.each do |nh, binding|
77
+ if hdr.send(binding.key) == binding.value
78
+ first_header = hklass.to_s.gsub(/.*::/, '')
79
+ break
80
+ end
81
+ end
82
+ break unless first_header.nil?
83
+ end
84
+ if first_header.nil?
85
+ raise ParseError, 'cannot identify first header in string'
86
+ end
87
+ end
88
+
89
+ pkt.add(first_header)
90
+ pkt.headers.last.read binary_str
91
+
92
+ # Decode upper headers recursively
93
+ decode_packet_bottom_up = true
94
+ while decode_packet_bottom_up do
95
+ last_known_hdr = pkt.headers.last
96
+ last_known_hdr.class.known_headers.each do |nh, binding|
97
+ if last_known_hdr.send(binding.key) == binding.value
98
+ str = last_known_hdr.body
99
+ pkt.add nh.to_s.gsub(/.*::/, '')
100
+ pkt.headers.last.read str
101
+ break
102
+ end
103
+ end
104
+ decode_packet_bottom_up = (pkt.headers.last != last_known_hdr)
105
+ end
106
+
107
+ pkt
108
+ end
109
+
110
+ # Capture packets from +iface+
111
+ # @param [String] iface interface name
112
+ # @param [Hash] options capture options
113
+ # @option options [Integer] :max maximum number of packets to capture
114
+ # @option options [Integer] :timeout maximum number of seconds before end
115
+ # of capture
116
+ # @option options [String] :filter bpf filter
117
+ # @option options [Boolean] :promiscuous
118
+ # @yieldparam [Packet] packet if a block is given, yield each captured packet
119
+ # @return [Array<Packet>] captured packet
120
+ def self.capture(iface, options={})
121
+ capture = Capture.new(iface, options)
122
+ if block_given?
123
+ capture.start { |packet| yield packet }
124
+ else
125
+ capture.start
126
+ end
127
+ capture.packets
128
+ end
129
+
130
+ # Read packets from +filename+.
131
+ #
132
+ # For more control, see {PcapNG::File}.
133
+ # @param [String] filename PcapNG file
134
+ # @return [Array<Packet>]
135
+ def self.read(filename)
136
+ PcapNG::File.new.read_packets filename
137
+ end
138
+
139
+ # Write packets to +filename+
140
+ #
141
+ # For more options, see {PcapNG::File}.
142
+ # @param [String] filename
143
+ # @param [Array<Packet>] packets packets to write
144
+ # @return [void]
145
+ def self.write(filename, packets)
146
+ pf = PcapNG::File.new
147
+ pf.array_to_file packets
148
+ pf.to_f filename
149
+ end
150
+
151
+ # @private
152
+ def initialize
153
+ @headers = []
154
+ end
155
+
156
+ # Add a protocol on packet stack
157
+ # @param [String] protocol
158
+ # @param [Hash] options protocol specific options
159
+ # @return [self]
160
+ # @raise [ArgumentError] unknown protocol
161
+ def add(protocol, options={})
162
+ klass = check_protocol(protocol)
163
+
164
+ header = klass.new(options)
165
+ prev_header = @headers.last
166
+ if prev_header
167
+ binding = prev_header.class.known_headers[klass]
168
+ if binding.nil?
169
+ msg = "#{prev_header.class} knowns no layer association with #{protocol}. "
170
+ msg << "Try #{prev_header.class}.bind_layer(PacketGen::Header::#{protocol}, "
171
+ msg << "#{prev_header.class.to_s.gsub(/(.*)::/, '').downcase}_proto_field: "
172
+ msg << "value_for_#{protocol.downcase})"
173
+ raise ArgumentError, msg
174
+ end
175
+ prev_header[binding.key].read binding.value
176
+ prev_header.body = header
177
+ end
178
+ header.packet = self
179
+ @headers << header
180
+ unless respond_to? protocol.downcase
181
+ self.class.class_eval "def #{protocol.downcase}(arg=nil);" \
182
+ "header('#{protocol}', arg); end"
183
+ end
184
+ self
185
+ end
186
+
187
+ # Check if a protocol header is embedded in packet
188
+ # @return [Boolean]
189
+ # @raise [ArgumentError] unknown protocol
190
+ def is?(protocol)
191
+ klass = check_protocol protocol
192
+ @headers.any? { |h| h.is_a? klass }
193
+ end
194
+
195
+ # Recalculate all packet checksums
196
+ # @return [void]
197
+ def calc_sum
198
+ @headers.reverse.each do |header|
199
+ header.calc_sum if header.respond_to? :calc_sum
200
+ end
201
+ end
202
+
203
+ # Recalculate all packet length fields
204
+ # @return [void]
205
+ def calc_length
206
+ @headers.each do |header|
207
+ header.calc_length if header.respond_to? :calc_length
208
+ end
209
+ end
210
+
211
+ # Recalculate all calculatable fields (for now: length and sum)
212
+ # @return [void]
213
+ def calc
214
+ calc_sum
215
+ calc_length
216
+ end
217
+
218
+ # Get packet body
219
+ # @return [StructFu]
220
+ def body
221
+ @headers.last.body
222
+ end
223
+
224
+ # Set packet body
225
+ # @param [String]
226
+ # @return [void]
227
+ def body=(str)
228
+ @headers.last.body = str
229
+ end
230
+
231
+ # Get binary string
232
+ # @return [String]
233
+ def to_s
234
+ @headers.first.to_s
235
+ end
236
+
237
+ # Write a PCapNG file to disk.
238
+ # @param [String] filename
239
+ # @return [Array] see return from {PcapNG::File#to_file}
240
+ # @see File
241
+ def to_f(filename)
242
+ File.new.array_to_file(filename: filename, array: [self])
243
+ end
244
+ alias :write :to_f
245
+
246
+ # send packet on wire. Use first header +#to_w+ method.
247
+ # @param [String] iface interface name. Default to first non-loopback interface
248
+ # @return [void]
249
+ def to_w(iface=nil)
250
+ iface ||= PacketGen.default_iface
251
+ if @headers.first.respond_to? :to_w
252
+ @headers.first.to_w(iface)
253
+ else
254
+ type = @headers.first.class.to_s.gsub(/.*::/, '')
255
+ raise WireError, "don't known how to send a #{type} packet on wire"
256
+ end
257
+ end
258
+
259
+ # @return [String]
260
+ def inspect
261
+ str = dashed_line(self.class)
262
+ @headers.each do |header|
263
+ str << dashed_line(header.class, 2)
264
+ header.to_h.each do |attr, value|
265
+ next if attr == :body
266
+ str << inspect_line(attr, value, 2)
267
+ end
268
+ end
269
+ str << inspect_body
270
+ end
271
+
272
+ # @param [Packet] other
273
+ # @return [Boolean]
274
+ def ==(other)
275
+ to_s == other.to_s
276
+ end
277
+
278
+ private
279
+
280
+ # @overload header(protocol, layer=1)
281
+ # @param [String] protocol
282
+ # @param [Integer] layer
283
+ # @overload header(protocol, options)
284
+ # @param [String] protocol
285
+ # @param [Hash] options
286
+ # @return [Header::Base]
287
+ # @raise [ArgumentError] unknown protocol
288
+ def header(protocol, arg)
289
+ klass = check_protocol protocol
290
+
291
+ headers = @headers.select { |h| h.is_a? klass }
292
+ layer = arg.is_a?(Integer) ? arg : 1
293
+ header = headers[layer - 1]
294
+
295
+ if arg.is_a? Hash
296
+ arg.each do |key, value|
297
+ unless header.respond_to? "#{key}="
298
+ raise ArgumentError, "unknown #{key} attribute for #{header.class}"
299
+ end
300
+ header.send "#{key}=", value
301
+ end
302
+ end
303
+
304
+ header
305
+ end
306
+
307
+ # check if protocol is known
308
+ # @param [String] protocol
309
+ # @raise [ArgumentError] unknown protocol
310
+ def check_protocol(protocol)
311
+ unless Header.const_defined? protocol
312
+ raise ArgumentError, "unknown #{protocol} protocol"
313
+ end
314
+ klass = Header.const_get(protocol)
315
+ raise ArgumentError, "unknown #{protocol} protocol" unless klass.is_a? Class
316
+ klass
317
+ end
318
+
319
+ def dashed_line(name, level=1)
320
+ str = '--' * level << " #{name} "
321
+ str << '-' * (INSPECT_MAX_WIDTH - str.length) << "\n"
322
+ end
323
+
324
+ def inspect_line(attr, value, level=1)
325
+ str = ' ' + ' ' * level
326
+ val = if value.is_a? StructFu::Int
327
+ sz = value.to_s.size
328
+ "%-10s (0x%0#{2*sz}x)" % [value.to_i, value.to_i]
329
+ elsif value.respond_to? :to_x
330
+ value.to_x
331
+ else
332
+ value.to_s
333
+ end
334
+ str << "%7s %10s: %s" % [value.class.to_s.sub(/.*::/, ''), attr, val]
335
+ str << "\n"
336
+ end
337
+
338
+ def inspect_body
339
+ str = dashed_line('Body', 2)
340
+ str << (0..15).to_a.map { |v| " %02d" % v}.join << "\n"
341
+ str << '-' * INSPECT_MAX_WIDTH << "\n"
342
+ if body.size > 0
343
+ (body.size / 16 + 1).times do |i|
344
+ octets = body.to_s[i*16, 16].unpack('C*')
345
+ o_str = octets.map { |v| " %02x" % v}.join
346
+ str << o_str
347
+ str << ' ' * (3*16 - o_str.size) unless o_str.size >= 3*16
348
+ str << ' ' << octets.map { |v| v < 128 && v > 13 ? v.chr : '.' }.join
349
+ str << "\n"
350
+ end
351
+ end
352
+ str << '-' * INSPECT_MAX_WIDTH << "\n"
353
+ end
354
+ end
355
+ end
356
+
357
+ require_relative 'header'