packetgen 0.1.0

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.
@@ -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'