dhcp 0.0.1 → 0.0.3

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/lib/dhcp/dhcp.rb CHANGED
@@ -1,854 +1,10 @@
1
+ #!/usr/bin/env ruby
1
2
  # encoding: ASCII-8BIT
2
- #
3
- # --
4
- #
5
- # Ruby DHCP module for parsing and creating IPv4 DHCP packets
6
- # - See http://www.aarongifford.com/computers/dhcp/
7
- #
8
- # --
9
- #
10
- # Written by Aaron D. Gifford - http://www.aarongifford.com/
11
- #
12
- # Copyright (c) 2010-2011 InfoWest, Inc. and Aaron D. Gifford
13
- #
14
- # Permission is hereby granted, free of charge, to any person obtaining a copy
15
- # of this software and associated documentation files (the "Software"), to deal
16
- # in the Software without restriction, including without limitation the rights
17
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
- # copies of the Software, and to permit persons to whom the Software is
19
- # furnished to do so, subject to the following conditions:
20
- #
21
- # The above copyright notice and this permission notice shall be included in
22
- # all copies or substantial portions of the Software.
23
- #
24
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
30
- # THE SOFTWARE.
31
- #
32
- # --
33
- #
34
- # NOTE: All strings in this module should be BINARY (ASCII-8BIT) encoded
35
- # or things won't work correctly.
36
- #
37
-
38
- ## Monkeypatch String so it will have a working #ord method (for 1.8):
39
- unless RUBY_VERSION >= '1.9.1'
40
- class String
41
- def ord
42
- self[0]
43
- end
44
- end
45
- end
46
3
 
47
4
  ## http://github.org/bluemonk/ipaddress - A very nice IP address utility gem
48
5
  require 'ipaddress'
49
6
 
50
7
  module DHCP
51
- ## Base class from which all DHCP options in a DHCP packet derive:
52
- class Opt
53
- def initialize(opt, name, ignore=nil)
54
- @opt = opt
55
- @name = name
56
- end
57
- attr_reader :opt, :name
58
-
59
- def opt_header
60
- "OPTION[#{opt}:#{@name}]"
61
- end
62
-
63
- def to_s
64
- opt_header
65
- end
66
-
67
- def to_opt
68
- @opt.chr
69
- end
70
- end
71
-
72
-
73
- ## Class for DHCP options that contain data
74
- class OptData < Opt
75
- def initialize(opt, name, data=nil)
76
- super(opt, name)
77
- @data = data.nil? ? '' : data_to_bin(data)
78
- end
79
- attr_accessor :data
80
-
81
- def data
82
- @data
83
- end
84
-
85
- def data=(data)
86
- @data = data.dup
87
- self ## Chainable
88
- end
89
-
90
- def set(data)
91
- self.data = data_to_bin(data)
92
- self ## Chainable
93
- end
94
-
95
- def get
96
- bin_to_data(@data)
97
- end
98
-
99
- def data_to_bin(data) ## Override this in subclasses to interpret data
100
- data
101
- end
102
-
103
- def bin_to_data(data) ## Override this in subclasses to interpret data
104
- data
105
- end
106
-
107
- def opt_header
108
- super + "(#{data.size})"
109
- end
110
-
111
- def to_s
112
- opt_header + "='#{bin_to_data(@data)}'"
113
- end
114
-
115
- def to_opt
116
- super + @data.size.chr + @data
117
- end
118
- end
119
-
120
-
121
- ## Class for DHCP options containing a fixed number of bytes
122
- class OptFixedData < OptData
123
- @size = 0 ## Override this in subclasses
124
- class << self
125
- attr_accessor :size
126
- end
127
-
128
- def initialize(opt, name, data=nil)
129
- super(opt, name, data)
130
- ## Prefill with zeros if needed:
131
- @data = 0.chr * self.class.size if data.nil? && self.class.size > 0
132
- end
133
-
134
- def data=(data)
135
- raise "Invalid size for #{self.class} (expected #{size} bytes, not #{data.size} bytes)" unless self.class.size == data.size
136
- super(data)
137
- end
138
- end
139
-
140
- ## Class for DHCP options that contain a lists (like lists of IPs)
141
- class OptListData < OptData
142
- include Enumerable
143
- def initialize(opt, name, data=nil)
144
- super(opt, name)
145
- @size = 0
146
- set(data) unless data.nil?
147
- end
148
-
149
- def data=(data)
150
- set(split_data(data))
151
- end
152
-
153
- def get
154
- split_data(@data) ## Splits and interprets binary data
155
- end
156
-
157
- def set(list)
158
- list = [list] unless is_list?(list)
159
- @data = ''
160
- @size = 0
161
- list.each do |item|
162
- append(item)
163
- end
164
- self ## Chainable
165
- end
166
-
167
- def is_list?(list) ## Override if needed in child class
168
- list.is_a?(Array)
169
- end
170
-
171
- def append(item)
172
- @size += 1
173
- @data += data_to_bin(item)
174
- self ## Chainable
175
- end
176
-
177
- def split_data(data) ## Override in child class to split and interpret binary data
178
- raise "Child class #{data.class} MUST override this"
179
- end
180
-
181
- def size
182
- @size
183
- end
184
-
185
- def to_s
186
- opt_header + '=[' + map{|x| x.to_s}.join(',') + ']'
187
- end
188
-
189
- def each
190
- split_data(@data).each do |item|
191
- yield item
192
- end
193
- end
194
- end
195
-
196
- ## Class for DHCP option suboptions:
197
- class SubOpt < OptData
198
- def opt_header
199
- "suboption[#{opt}:#{@name}]"
200
- end
201
- end
202
-
203
- ## Class for DHCP option suboptions containing lists
204
- class SubOptList < OptListData
205
- def opt_header
206
- "suboption[#{opt}:#{@name}]"
207
- end
208
- end
209
-
210
- ## Class for DHCP suboption for vendor specific information
211
- class SubOptVSRInfo < SubOptList
212
- def is_list?(list)
213
- raise "Invalid suboption sublist/entry" unless list.is_a?(Array)
214
- return false if list.size == 2 && list[0].is_a?(Fixnum) && list[1].is_a?(String)
215
- list.each do |item|
216
- raise "Invalid suboption sublistlist" unless item.is_a?(Array) && item.size == 2 && item[0].is_a?(Fixnum) && item[1].is_a?(String)
217
- end
218
- return true
219
- end
220
-
221
- def split_data(data)
222
- data = data.dup
223
- list = []
224
- while data.size > 0
225
- raise "Invalid suboption data" unless data.size >= 5
226
- len = data[4,1].ord
227
- raise "Invalid vendor-specific relay info. data length" unless data.size >= len + 5
228
- list << [ data[0,4].unpack('N')[0], data[5,len] ]
229
- data[0,5+len] = ''
230
- end
231
- list
232
- end
233
-
234
- def bin_to_data(data)
235
- raise "Invalid data size" unless data.size >= 5 && data.size == data[4,1].ord + 5
236
- [ data[0,1].ord, data[2,data.size-2] ]
237
- end
238
-
239
- def data_to_bin(data)
240
- raise "Invalid data" unless data.is_a?(Array) && data.size == 2 && data[0].is_a?(Fixnum) && data[1].is_a?(String)
241
- raise "Invalid data size" unless data[1].size < 256
242
- data[0].chr + data[1].size.chr + data[1]
243
- end
244
- end
245
-
246
- ## Class for DHCP options that contain sublists (like vendor specific information or relay agent information)
247
- class OptSubList < OptListData
248
- def is_list?(list)
249
- raise "Invalid suboption list/entry" unless list.is_a?(Array)
250
- return false if list.size == 2 && list[0].is_a?(Fixnum) && list[1].is_a?(String)
251
- list.each do |item|
252
- raise "Invalid suboption list" unless item.is_a?(Array) && item.size == 2 && item[0].is_a?(Fixnum) && item[1].is_a?(String)
253
- end
254
- return true
255
- end
256
-
257
- def split_data(data)
258
- data = data.dup
259
- list = []
260
- while data.size > 0
261
- raise "Invalid data size" unless data.size >= 2
262
- len = data[1,1].ord
263
- raise "Invalid data size" unless data.size >= len + 2
264
- list << [ data[0,1].ord, data[2,len] ]
265
- data[0,len+2] = ''
266
- end
267
- list
268
- end
269
-
270
- def bin_to_data(data)
271
- raise "Invalid data size" unless data.size >= 2 && data.size == data[1,1].ord + 2
272
- [ data[0,1].ord, data[2,data.size-2] ]
273
- end
274
-
275
- def data_to_bin(data)
276
- raise "Invalid data" unless data.is_a?(Array) && data.size == 2 && data[0].is_a?(Fixnum) && data[1].is_a?(String)
277
- raise "Invalid data size" unless data[1].size < 256
278
- data[0].chr + data[1].size.chr + data[1]
279
- end
280
-
281
- def to_s
282
- opt_header + "(#{@size})=[" + map do |i|
283
- val = ''
284
- name = case i[0]
285
- when 1
286
- val = i[1].scan(/./m).map{|b| b.unpack('H2')[0].upcase}.join(':')
287
- 'AgentCircuitID'
288
- when 2
289
- val = i[1].scan(/./m).map{|b| b.unpack('H2')[0].upcase}.join(':')
290
- 'AgentRemoteID'
291
- when 9
292
- val = (SubOptVSRInfo.new(9, :vendor_specific_relay_suboption).data=i[1]).to_s
293
- 'VendorSpecificRelaySuboption'
294
- else
295
- val = i[1].scan(/./m).map{|b| b.unpack('H2')[0].upcase}.join(':')
296
- 'Unknown'
297
- end
298
- "#{name}:#{i[0]}(#{i[1].size})='#{val}'"
299
- end.join(',') + ']'
300
- end
301
- end
302
-
303
- ## Class for DHCP options that contain lists of fixed sized data
304
- class OptListFixedData < OptListData
305
- @size = 0 ## Override this in subclasses
306
- class << self
307
- attr_accessor :size
308
- end
309
-
310
- def split_data(data)
311
- raise "Child class #{self.class} MUST override class size variable with non-zero value!" if self.class.size == 0
312
- raise "Invalid data length #{data.size} (expected even multiple of #{self.class.size})" unless data.size % self.class.size == 0
313
- list = []
314
- data = data.dup
315
- while data.size > 0
316
- list << bin_to_data(data.slice!(0,self.class.size))
317
- end
318
- list
319
- end
320
-
321
- def data_to_bin(item) ## Override in child, but call super(item)
322
- ## with the resulting translated data after
323
- ## data translation so the size check is
324
- ## applied (or do a size check in the child):
325
- raise "Invalid data item length #{item.size} (expected #{self.class.size})" unless item.size == self.class.size
326
- item
327
- end
328
- end
329
-
330
- ## Class for DHCP options that contain a single IPv4 address
331
- class OptIP < OptFixedData
332
- @size = 4
333
-
334
- def bin_to_data(data)
335
- IPAddress::IPv4::parse_data(data).to_s
336
- end
337
-
338
- def data_to_bin(data)
339
- IPAddress::IPv4.new(data).data ## Will raise exception if data is not a valid IP
340
- end
341
- end
342
-
343
- ## Class for DHCP options that contain a list of IPv4 addresses
344
- class OptIPList < OptListFixedData
345
- @size = 4
346
-
347
- def bin_to_data(data)
348
- IPAddress::IPv4::parse_data(data).to_s
349
- end
350
-
351
- def data_to_bin(data)
352
- IPAddress::IPv4.new(data).data ## Will raise exception if data is not a valid IP
353
- end
354
- end
355
-
356
- ## Class for DHCP option 33 (static routes) - Use option 121 instead if possible
357
- ## WARNING: Option 33 can only handle class A, B, or C networks, not classless
358
- ## networks with an arbitrary netmask.
359
- class OptStaticRoutes < OptListFixedData
360
- @size = 8
361
-
362
- def is_list?(list)
363
- raise "Invalid route list/entry" unless list.is_a?(Array)
364
- if list.size == 2
365
- return false if list[0].is_a?(String) && list[1].is_a?(String)
366
- return true if list[0].is_a?(Array) && list[1].is_a?(Array)
367
- raise "Invalid route list/entry"
368
- end
369
- list.each do |item|
370
- raise "Invalid route list" unless item.is_a?(Array) && item[0].is_a?(String) && item[1].is_a?(String)
371
- end
372
- return true
373
- end
374
-
375
- def data_to_bin(data)
376
- raise "Invalid static route" unless data.is_a?(Array) && data.size == 2
377
- net, gateway = *data
378
- net = IPAddress::IPv4.new(net)
379
- raise "Invalid classful static route network" unless net.network?
380
- raise "Invalid classful static route network" unless (
381
- (net.a? && net.prefix == 8 ) ||
382
- (net.b? && net.prefix == 16) ||
383
- (net.c? && net.prefix == 24)
384
- )
385
- gateway = IPAddress::IPv4.new("#{gateway}/#{net.prefix}")
386
- raise "Invalid classful static route gateway" unless gateway.member?(net)
387
- net.data + gateway.data
388
- end
389
-
390
- def bin_to_data(data)
391
- [IPAddress::IPv4::parse_classful_data(data[0,4]).net.to_string, IPAddress::IPv4::parse_data(data[4,4]).to_s]
392
- end
393
-
394
- def to_s
395
- opt_header + '=[' + map{|i| i[0] + '=>' + i[1]}.join(',') + ']'
396
- end
397
- end
398
-
399
- ## Class for DHCP options containing lists of IPv4 CIDR routes (like option 121 or MS's 249)
400
- ## See RFC 3442 "compact encoding" of destination
401
- class OptRouteList < OptListData
402
- def split_data(data)
403
- data = data.dup
404
- list = []
405
- while data.size > 0
406
- raise "Invalid binary data" unless data.size > 4 || data[0,1].ord > 32
407
- octets = (data[0,1].ord + 7)/8
408
- raise "Invalid binary data" unless data.size >= octets + 5
409
- list << bin_to_data(data.slice!(0,octets+5))
410
- end
411
- list
412
- end
413
-
414
- def data_to_bin(data)
415
- raise "Invalid classless static route" unless data.is_a?(Array) && data.size == 2
416
- net, gateway = *data
417
- raise "Invalid classless static route network" if net.index('/').nil?
418
- net = IPAddress::IPv4.new(net)
419
- raise "Invalid classless static route network" unless net.network?
420
- gateway = IPAddress::IPv4.new("#{gateway}/#{net.prefix}")
421
- raise "Invalid classless static route gateway" unless gateway.member?(net)
422
- net.prefix.to_i.chr + net.data[0,(net.prefix+7)/8] + gateway.data
423
- end
424
-
425
- def bin_to_data(data)
426
- raise "Invalid binary classless route data" unless data.size > 4 || data[0,1].ord > 32
427
- maskbits = data[0,1].ord
428
- octets = (maskbits+7)/8
429
- raise "Invalid binary classless route data" unless data.size == octets + 5
430
- dest = IPAddress::IPv4.parse_data(data[1,octets] + 0.chr * (4 - octets))
431
- dest.prefix = maskbits
432
- gateway = IPAddress::IPv4.parse_data(data[octets+1,4])
433
- gateway.prefix = maskbits ## Unnecessary...
434
- ## Should an "Invalid classless static route" exception be raised
435
- ## here if gateway is not a member of the destination network?
436
- [dest.to_string, gateway.to_s]
437
- end
438
- end
439
-
440
- ## Class for boolean DHCP options
441
- class OptBool < OptFixedData
442
- @size = 1
443
-
444
- def data_to_bin(data)
445
- raise "Invalid boolean data #{data.class} (expected TrueClass or FalseClass)" unless data.is_a?(TrueClass) || data.is_a?(FalseClass)
446
- data ? 1.chr : 0.chr
447
- end
448
-
449
- def bin_to_data(data)
450
- raise "Invalid boolean binary data" if data.size != 1 || data.ord > 1
451
- data.ord == 0 ? false : true
452
- end
453
- end
454
-
455
- ## Class for single-byte unsigned integer value DHCP options
456
- ## Also acts as parent class for multi-byte value DHCP options
457
- class OptByte < OptFixedData
458
- @size = 1
459
-
460
- def data_to_bin(data)
461
- raise "Invalid numeric data" unless data.is_a?(Fixnum) && data >= 0
462
- raise "Invalid number" unless data == data & ([0xff] * self.class.size).inject(0){|sum,byte| sum<<8|byte}
463
- bytes = ''
464
- while data != 0
465
- bytes = (data & 0xff).chr + bytes
466
- data >>= 8
467
- end
468
- raise "Impossible: Numeric byte size #{bytes.size} exceeds #{self.class.size}" if bytes.size > self.class.size
469
- 0.chr * (self.class.size - bytes.size) + bytes
470
- end
471
-
472
- def bin_to_data(data)
473
- data.each_byte.inject(0){|sum,byte| sum<<8|byte}
474
- end
475
-
476
- def to_s
477
- opt_header + "=#{self.get}"
478
- end
479
- end
480
-
481
- ## Class for two-byte unsigned integer value DHCP options
482
- class OptInt16 < OptByte
483
- @size = 2
484
- end
485
-
486
- ## Class for four-byte unsigned integer value DHCP options
487
- class OptInt32 < OptByte
488
- @size = 4
489
- end
490
-
491
- ## Class for four-byte signed integer value DHCP options
492
- class OptSInt32 < OptInt32
493
- @size = 4
494
- def data_to_bin(data)
495
- super(data % 2**32)
496
- end
497
-
498
- def bin_to_data(data)
499
- (super(data) + 2**31) % 2**32 - 2**31
500
- end
501
- end
502
-
503
- ## Class for DHCP options containing a list of single byte integers (i.e. lists of requested DHCP options)
504
- class OptByteList < OptListFixedData
505
- @size = 1
506
-
507
- def bin_to_data(data)
508
- data.each_byte.inject(0){|sum,byte| sum<<8|byte}
509
- end
510
-
511
- def data_to_bin(data)
512
- raise "Invalid numeric data" unless data.is_a?(Fixnum) && data >= 0
513
- raise "Invalid number" unless data == data & ([0xff] * self.class.size).inject(0){|sum,byte| sum<<8|byte}
514
- bytes = ''
515
- while data != 0
516
- bytes = (data & 0xff).chr + bytes
517
- data >>= 8
518
- end
519
- raise "Impossible: Numeric byte size #{bytes.size} exceeds #{self.class.size}" if bytes.size > self.class.size
520
- 0.chr * (self.class.size - bytes.size) + bytes
521
- end
522
-
523
- def to_s
524
- opt_header + '=[' + map{|x| x.to_s}.join(',') + ']'
525
- end
526
- end
527
-
528
- ## Class for DHCP options containing data that is most often displayed as a string of hexadecimal digit pairs joined by colons (i.e. ethernet MAC addresses)
529
- class OptHexString < OptData
530
- def data_to_bin(data)
531
- data.split(/[ \.:\-]/).map{|b| [('0'+b)[-2,2]].pack('H2')}.join
532
- end
533
-
534
- def bin_to_data(data)
535
- data.scan(/./m).map{|b| b.unpack('H2')[0].upcase}.join(':')
536
- end
537
- end
538
-
539
- ## Class for DHCP options containing DNS host names
540
- class OptHost < OptData
541
- def data_to_bin(data)
542
- raise "Invalid host name" unless /^(?:[a-zA-Z0-9][a-zA-Z0-9-]{0,62}\.)*[a-zA-Z0-9][a-zA-Z0-9-]{0,62}$/.match(data)
543
- data
544
- end
545
- end
546
-
547
- ## Class for DHCP options containing DNS domain names
548
- class OptDomain < OptData
549
- def data_to_bin(data)
550
- raise "Invalid domain name" unless /^(?:[a-zA-Z0-9][a-zA-Z0-9-]{0,62}\.)*[a-zA-Z0-9][a-zA-Z0-9-]{0,62}\.?$/.match(data)
551
- end
552
- end
553
-
554
- ## Class representing a DHCP packet (a request or a response):
555
- class Packet
556
- def initialize(op=nil)
557
- raise "Invalid/unsupported operation type #{op}" unless op.nil? || op == BOOTREQUEST || op == BOOTREPLY
558
- @op = op || BOOTREQUEST ## 1: Operation (BOOTREQUEST=1/BOOTREPLY=2)
559
- @htype_name = :htype_10mb_ethernet
560
- @htype = HTYPE[@htype_name][0] ## 1: Hardware address type
561
- @hlen = HTYPE[@htype_name][1] ## 1: Hardware address length
562
- @hops = 0 ## 1: Client sets to zero, relays may increment
563
- @xid = 0 ## 4: Client picks random XID (session ID of sorts)
564
- @secs = 0 ## 4: Seconds elapsed since client started transaction
565
- @flags = 0 ## 2: Leftmost bit is the 'BROADCAST' flag (if set) - Others are zero (reserved for future use)
566
- @ciaddr = 0.chr * 4 ## 4: "Client IP" -- Only set by client if client state is BOUND/RENEW/REBINDING and client can respond to ARP requests
567
- @yiaddr = 0.chr * 4 ## 4: "Your IP" -- Server assigns IP to client
568
- @siaddr = 0.chr * 4 ## 4: "Server IP" -- IP of server to use in NEXT step of client bootstrap process
569
- @giaddr = 0.chr * 4 ## 4: "Gateway IP" -- Relay agent will set this to itself and modify replies
570
- ## 16: Client hardware address (see htype and hlen)
571
- @chaddr = 0.chr * @hlen ## ^^^ See note above ^^^
572
- @sname = '' ## 64: Server host name (optional) as C-style null/zero terminated string (may instead contain options)
573
- @file = '' ## 128: Boot file name (optional) as C-style null/zero terminated string (may instead contain options)
574
- @options = '' ## variable: Options - Up to 312 bytes in a 576-byte DHCP message - First four bytes are MAGIC
575
- @optlist = []
576
- @type = nil ## Unknown until set
577
- @type_name = 'UNKNOWN'
578
- end
579
- attr_reader :op, :htype_name, :htype, :hlen, :hops, :xid, :secs, :flags, :type, :type_name, :options, :optlist
580
- attr_accessor :secs, :xid, :sname, :file
581
-
582
- def initialize_copy(orig)
583
- self.ciaddr = orig.ciaddr
584
- self.yiaddr = orig.yiaddr
585
- self.siaddr = orig.siaddr
586
- self.giaddr = orig.giaddr
587
- @chaddr = orig.raw_chaddr.dup
588
- @file = orig.file.dup
589
- @sname = orig.sname.dup
590
- @options = orig.options.dup
591
- @optlist = []
592
- orig.optlist.each do |opt|
593
- @optlist << opt.dup
594
- end
595
- end
596
-
597
- def append_opt(opt)
598
- if opt.name == :dhcp_message_type
599
- unless @type.nil?
600
- raise "DHCP message type ALREADY SET in packet"
601
- end
602
- set_type(opt)
603
- end
604
- @optlist << opt
605
- end
606
-
607
- def _find_htype(htype)
608
- HTYPE.each do |name, htype|
609
- if htype[0] == @htype
610
- return name
611
- end
612
- end
613
- return nil
614
- end
615
-
616
- def parse(msg)
617
- raise "Packet is too short (#{msg.size} < 241)" if (msg.size < 241)
618
- @op = msg[0,1].ord
619
- raise 'Invalid OP (expected BOOTREQUEST or BOOTREPLY)' if @op != BOOTREQUEST && @op != BOOTREPLY
620
- self.htype = msg[1,1].ord ## This will do sanity checking and raise an exception on unsupported HTYPE
621
- raise "Invalid hardware address length #{msg[2,1].ord} (expected #{@hlen})" if msg[2,1].ord != @hlen
622
- @hops = msg[3,1].ord
623
- @xid = msg[4,4].unpack('N')[0]
624
- @secs = msg[8,2].unpack('n')[0]
625
- @flags = msg[10,2].unpack('n')[0]
626
- @ciaddr = msg[12,4]
627
- @yiaddr = msg[16,4]
628
- @siaddr = msg[20,4]
629
- @giaddr = msg[24,4]
630
- @chaddr = msg[28,16]
631
- @sname = msg[44,64]
632
- @file = msg[108,128]
633
- magic = msg[236,4]
634
- raise "Invalid DHCP OPTION MAGIC #{magic.each_byte.map{|b| ('0'+b.to_s(16).upcase)[-2,2]}.join(':')} != #{MAGIC.each_byte.map{|b| ('0'+b.to_s(16).upcase)[-2,2]}.join(':')}" if magic != MAGIC
635
- @options = msg[240,msg.size-240]
636
- @optlist = []
637
- parse_opts(@options)
638
- opt = get_option(:option_overload)
639
- unless opt.nil?
640
- ## RFC 2131: If "option overload" present, parse FILE field first, then SNAME (depending on overload value)
641
- parse_opts(@file) if opt.get == 1 || opt.get == 3
642
- parse_opts(@sname) if opt.get == 2 || opt.get == 3
643
- raise "Invalid option overload value" if opt.val > 1 || opt.val > 3
644
- end
645
- opt = get_option(:dhcp_message_type)
646
- raise "Not a valid DHCP packet (may be BOOTP): Missing DHCP MESSAGE TYPE" if opt.nil?
647
- set_type(opt)
648
- end
649
-
650
- def set_type(opt)
651
- @type = opt.get
652
- case @type
653
- when DHCPDISCOVER
654
- @type_name = 'DHCPDISCOVER'
655
- raise "Invalid OP #{@op} for #{@type_name}" unless @op == BOOTREQUEST
656
- when DHCPOFFER
657
- @type_name = 'DHCPOFFER'
658
- raise "Invalid OP #{@op} for #{@type_name}" unless @op == BOOTREPLY
659
- when DHCPREQUEST
660
- @type_name = 'DHCPREQUEST'
661
- raise "Invalid OP #{@op} for #{@type_name}" unless @op == BOOTREQUEST
662
- when DHCPDECLINE
663
- @type_name = 'DHCPDECLINE'
664
- raise "Invalid OP #{@op} for #{@type_name}" unless @op == BOOTREQUEST
665
- when DHCPACK
666
- @type_name = 'DHCPACK'
667
- raise "Invalid OP #{@op} for #{@type_name}" unless @op == BOOTREPLY
668
- when DHCPNAK
669
- @type_name = 'DHCPNAK'
670
- raise "Invalid OP #{@op} for #{@type_name}" unless @op == BOOTREPLY
671
- when DHCPRELEASE
672
- @type_name = 'DHCPRELEASE'
673
- raise "Invalid OP #{@op} for #{@type_name}" unless @op == BOOTREQUEST
674
- when DHCPINFORM
675
- @type_name = 'DHCPINFORM'
676
- raise "Invalid OP #{@op} for #{@type_name}" unless @op == BOOTREQUEST
677
- when DHCPFORCERENEW
678
- @type_name = 'DHCPFORCERENEW'
679
- raise "Invalid OP #{@op} for #{@type_name}" unless @op == BOOTREPLY
680
- when DHCPLEASEQUERY
681
- @type_name = 'DHCPLEASEQUERY'
682
- raise "Invalid OP #{@op} for #{@type_name}" unless @op == BOOTREQUEST
683
- when DHCPLEASEUNASSIGNED
684
- @type_name = 'DHCPLEASEUNASSIGNED'
685
- raise "Invalid OP #{@op} for #{@type_name}" unless @op == BOOTREPLY
686
- when DHCPLEASEUNKNOWN
687
- @type_name = 'DHCPLEASEUNKNOWN'
688
- raise "Invalid OP #{@op} for #{@type_name}" unless @op == BOOTREPLY
689
- when DHCPLEASEACTIVE
690
- @type_name = 'DHCPLEASEACTIVE'
691
- raise "Invalid OP #{@op} for #{@type_name}" unless @op == BOOTREPLY
692
- else
693
- raise "Invalid DHCP MESSAGE TYPE" if opt.val < 1 || opt.val > 8
694
- end
695
- end
696
-
697
- ## Look through a packet's options for the option in question:
698
- def get_option(opt)
699
- @optlist.each do |o|
700
- return o if (opt.is_a?(Symbol) && o.name == opt) || (opt.is_a?(Fixnum) && o.opt == opt)
701
- end
702
- nil
703
- end
704
-
705
- def parse_opts(opts)
706
- msg = opts.dup
707
- while msg.size > 0
708
- opt = msg[0,1].ord
709
- if opt == 0
710
- ## Don't add padding options to our list...
711
- msg[0,1] = ''
712
- elsif opt == 255
713
- ## Options end... Assume all the rest is padding (if any)
714
- @optlist << Opt.new(255, :end)
715
- msg = ''
716
- else
717
- ## TODO: If an option value can't fit within a single option,
718
- ## it may span several and the values should be merged. We
719
- ## don't support this yet for parsing.
720
- raise "Options end too soon" if msg.size == 1
721
- len = msg[1,1].ord
722
- raise "Options end too abruptly (expected #{len} more bytes, but found only #{msg.size - 2})" if msg.size < len + 2
723
- val = msg[2,len]
724
- msg[0,len+2] = ''
725
- o = get_option(opt)
726
- if o.nil?
727
- o = DHCP::make_opt(opt)
728
- if o.nil?
729
- puts "WARNING: Ignoring unsupported option #{opt} (#{len} bytes)"
730
- else
731
- o.data = val unless len == 0
732
- @optlist << o
733
- end
734
- else
735
- ## See above TODO note...
736
- puts "WARNING: Duplicate option #{opt} (#{o.name}) of #{len} bytes skipped/ignored"
737
- end
738
- end
739
- end
740
- end
741
-
742
- def to_packet
743
- packet =
744
- @op.chr + @htype.chr + @hlen.chr + @hops.chr +
745
- [@xid, @secs, @flags].pack('Nnn') +
746
- @ciaddr + @yiaddr + @siaddr + @giaddr +
747
- @chaddr + (0.chr * (16-@chaddr.size)) +
748
- @sname + (0.chr * (64-@sname.size)) +
749
- @file + (0.chr * (128-@file.size)) +
750
- MAGIC +
751
- @optlist.map{|x| x.to_opt}.join
752
- packet + (packet.size < 300 ? 0.chr * (300 - packet.size) : '') ## Pad to minimum of 300 bytes (BOOTP min. packet size)
753
- end
754
-
755
- def to_s
756
- str = "op=#{@op} "
757
- case @op
758
- when BOOTREQUEST
759
- str += '(BOOTREQUEST)'
760
- when BOOTREPLY
761
- str += '(BOOTREPLY)'
762
- else
763
- str += '(UNKNOWN)'
764
- end
765
- str += "\n"
766
-
767
- str += "htype=#{@htype} "
768
- found = false
769
- HTYPE.each do |name, htype|
770
- if htype[0] == @htype
771
- found = true
772
- str += name.to_s.upcase + "\n" + 'hlen=' + htype[1].to_s + "\n"
773
- str += "*** INVALID HLEN #{@hlen} != #{htype[1]} ***\n" if @hlen != htype[1]
774
- break
775
- end
776
- end
777
- str += "UNKNOWN\nhlen=" + @hlen.to_s + "\n" unless found
778
- str += "hops=#{@hops}\n"
779
- str += "xid=#{@xid} (0x" + [@xid].pack('N').each_byte.map{|b| ('0'+b.to_s(16).upcase)[-2,2]}.join + ")\n"
780
- str += "secs=#{@secs}\n"
781
- str += "flags=#{@flags} (" + (broadcast? ? 'BROADCAST' : 'NON-BROADCAST') + ")\n"
782
- str += 'ciaddr=' + ciaddr + "\n"
783
- str += 'yiaddr=' + yiaddr + "\n"
784
- str += 'siaddr=' + siaddr + "\n"
785
- str += 'giaddr=' + giaddr + "\n"
786
- str += 'chaddr=' + chaddr + "\n"
787
- str += "sname='#{@sname.sub(/\x00.*$/,'')}' (#{@sname.sub(/\x00.*$/,'').size})\n"
788
- str += "file='#{@file.sub(/\x00.*$/,'')}' (#{@file.sub(/\x00.*$/,'').size})\n"
789
- str += 'MAGIC: (0x' + MAGIC.each_byte.map{|b| ('0'+b.to_s(16).upcase)[-2,2]}.join + ")\n"
790
- str += "OPTIONS(#{@optlist.size}) = [\n "
791
- str += @optlist.map{|x| x.to_s}.join(",\n ") + "\n]\n"
792
- str += "DHCP_PACKET_TYPE='#{@type_name}' (#{@type}) " unless @type.nil?
793
- str
794
- end
795
-
796
- def htype=(htype)
797
- @htype_name = _find_htype(htype)
798
- raise "Invalid/unsupported hardware type #{htype}" if @htype_name.nil?
799
- @hlen = HTYPE[@htype_name][1]
800
- @htype = HTYPE[@htype_name][0]
801
- end
802
-
803
- ## Broadcast flag:
804
- def broadcast?
805
- @flags & 0x8000 != 0
806
- end
807
- def broadcast!
808
- @flags |= 0x8000
809
- end
810
-
811
- ## Hardware address (ethernet MAC style):
812
- def chaddr
813
- @chaddr[0,@hlen].each_byte.map{|b| ('0'+b.to_s(16).upcase)[-2,2]}.join(':')
814
- end
815
- def raw_chaddr
816
- @chaddr
817
- end
818
- def chaddr=(addr)
819
- raise "Invalid hardware address" if addr.size - @hlen + 1 != @hlen * 2 || !/^(?:[a-fA-F0-9]{2}[ \.:_\-])*[a-fA-F0-9]{2}$/.match(addr)
820
- @chaddr = addr.split(/[ .:_-]/).map{|b| b.to_i(16).chr}.join
821
- end
822
-
823
- ## IP accessors:
824
- def ciaddr
825
- IPAddress::IPv4::parse_data(@ciaddr).to_s
826
- end
827
- def ciaddr=(ip)
828
- @ciaddr = IPAddress::IPv4.new(ip).data
829
- end
830
-
831
- def yiaddr
832
- IPAddress::IPv4::parse_data(@yiaddr).to_s
833
- end
834
- def yiaddr=(ip)
835
- @yiaddr = IPAddress::IPv4.new(ip).data
836
- end
837
-
838
- def siaddr
839
- IPAddress::IPv4::parse_data(@siaddr).to_s
840
- end
841
- def siaddr=(ip)
842
- @siaddr = IPAddress::IPv4.new(ip).data
843
- end
844
-
845
- def giaddr
846
- IPAddress::IPv4::parse_data(@giaddr).to_s
847
- end
848
- def giaddr=(ip)
849
- @giaddr = IPAddress::IPv4.new(ip).data
850
- end
851
- end
852
8
 
853
9
  ## BOOTP TYPES:
854
10
  BOOTREQUEST = 1
@@ -875,87 +31,45 @@ module DHCP
875
31
  DHCPLEASEUNKNOWN = 12
876
32
  DHCPLEASEACTIVE = 13
877
33
 
878
- ## OPTIONS:
879
- MAGIC = [99, 130, 83, 99].pack('C4')
34
+ ## Map message type string to integer type:
35
+ MSG_STR_TO_TYPE = {
36
+ 'DHCPDISCOVER' => DHCPDISCOVER,
37
+ 'DHCPOFFER' => DHCPOFFER,
38
+ 'DHCPREQUEST' => DHCPREQUEST,
39
+ 'DHCPDECLINE' => DHCPDECLINE,
40
+ 'DHCPACK' => DHCPACK,
41
+ 'DHCPNAK' => DHCPNAK,
42
+ 'DHCPRELEASE' => DHCPRELEASE,
43
+ 'DHCPINFORM' => DHCPINFORM,
44
+ 'DHCPFORCERENEW' => DHCPFORCERENEW,
45
+ 'DHCPLEASEQUERY' => DHCPLEASEQUERY,
46
+ 'DHCPLEASEUNASSIGNED' => DHCPLEASEUNASSIGNED,
47
+ 'DHCPLEASEUNKNOWN' => DHCPLEASEUNKNOWN,
48
+ 'DHCPLEASEACTIVE' => DHCPLEASEACTIVE
49
+ }
880
50
 
881
- ## Options 0-18 and 254 are defined in RFC 1497 (BOOTP)
882
- ## TODO: Add in as yet unhandled options
883
- OPTIONS = {
884
- :pad => [ 0, Opt ],
885
- :subnet_mask => [ 1, OptIP ],
886
- :time_offset => [ 2, OptSInt32 ], ## Offset from GMT (signed 32-bit integer seconds)
887
- :routers => [ 3, OptIPList ], ## Default gateway(s)
888
- :time_servers => [ 4, OptIPList ],
889
- :name_servers => [ 5, OptIPList ], ## IEN-116 name servers
890
- :dns_servers => [ 6, OptIPList ], ## DNS server(s) (RFC-1034/1025)
891
- :log_servers => [ 7, OptIPList ], ## Log server(s) (MIT-LCS UDP log servers)
892
- :cookie_servers => [ 8, OptIPList ], ## Cookie/Quote-of-the-day (RFC 865) server(s)
893
- :lpr_servers => [ 9, OptIPList ], ## LPR server(s) (RFC 1179)
894
- :impress_servers => [ 10, OptIPList ], ## Impress server(s) (in pref. order)
895
- :rlp_servers => [ 11, OptIPList ], ## RLP server(s) (RFC 887)
896
- :host_name => [ 12, OptHost ], ## May or may not be qualified with local domain name (RFC 1035)
897
- :boot_file_size => [ 13, OptInt16 ], ## Boot file size (number of 512-byte blocks as unsigned 16-bit integer)
898
- :merit_dump_file => [ 14, OptData ], ## File name client should dump core to
899
- :domain_name => [ 15, OptHost ], ## RFC 1034/1035 domain name
900
- :swap_server => [ 16, OptIP ], ## Swap server
901
- :root_path => [ 17, OptData ], ## Pathname to mount as root disk
902
- :extensions_path => [ 18, OptData ], ## TFTP-available file containing info to be interpreted the same way as 64-byte vendor-extension field in a BOOTP response with some exceptions (See RFC 1497)
903
- :ip_forwarding => [ 19, OptBool ], ## Host should enable/disable IP forwarding (0=disable/1=enable)
904
- :nonlocal_source_routing => [ 20, OptBool ], ## Enable/disable source routing
905
- :interface_mtu => [ 26, OptInt16 ],
906
- :broadcast_address => [ 28, OptIP ],
907
- :perform_mask_discovery => [ 29, OptBool ], ## This server always sets to NO/FALSE
908
- :mask_supplier => [ 30, OptBool ], ## This server always sets to NO/FALSE
909
- :perform_router_discovery => [ 31, OptBool ], ## This server always sets to NO/FALSE - RFC 1265
910
- :router_solicitation_address => [ 32, OptIP ],
911
- :static_routes => [ 33, OptStaticRoutes ], ## Use option 121 instead - Must NOT specify default route with this
912
- :arp_cache_timeout => [ 35, OptInt32 ], ## Unsigned integer no. of seconds for ARP cache timeout
913
- :ethernet_encapsulation => [ 36, OptBool ], ## 0/false = Eth. v2 RFC 894 encapsulation, 1/true = 802.3 RFC 1042 encapsulation
914
- :ntp_servers => [ 42, OptIPList ],
915
- :vendor_specific_information => [ 43, OptSubList ],
916
- :netbios_name_server => [ 44, OptIPList ], ## NetBIOS name server list
917
- :netbios_over_tcpip_node_type => [ 46, OptByte ], ## NetBIOS node type: 1=B-node, 2=P-node, 4=M-node, 8=H-node
918
- :netbios_over_tcpip_scope => [ 47, OptData ], ## NetBIOS scope
919
- :requested_ip_address => [ 50, OptIP ], ## Client's requested IP
920
- :ip_address_lease_time => [ 51, OptInt32 ], ## How long the lease lasts
921
- :option_overload => [ 52, OptByte ], ## 1, 2, or 3 == 'file' has options, 'sname' has options, both have options
922
- :dhcp_message_type => [ 53, OptByte ], ## One of the above-defined DHCP MESSAGE TYPEs
923
- :server_identifier => [ 54, OptIP ], ## How the client differentiates between DHCP servers
924
- :parameter_request_list => [ 55, OptByteList ], ## List of options the CLIENT is requesting in response
925
- :message => [ 56, OptData ], ## Message in DHCPNAK or DHCPDECLINE saying why that response was sent
926
- :maximum_dhcp_message_size => [ 57, OptInt16 ], ## Client tells server max. message size it will accept
927
- :vendor_class_identifier => [ 60, OptData ], ## MS boxes send "MSFT 98" or "MSFT 5.0"
928
- :client_identifier => [ 61, OptHexString ], ## Client's identifier (client picks ANYTHING)
929
- :smtp_servers => [ 69, OptIPList ],
930
- :tftp_server_name => [ 66, OptData ], ## TFTP 'sname' value if 'sname' is overloaded with options
931
- :bootfile_name => [ 67, OptData ], ## File name in 'file' if 'file' is overloaded with options
932
- :pop3_servers => [ 70, OptIPList ],
933
- :client_fqdn => [ 81, OptData ], ## Client's requested FQDN (DHCP server could use to update dynamic DNS)
934
- :relay_agent_information => [ 82, OptSubList ], ## VERY USEFUL with Cisco CMTS and Motorola Canopy
935
- :isns_servers => [ 83, OptData ], ## RFC 4184 Internet Storage Name Servers DHCP option (primary and backup)
936
- :authentication => [ 90, OptData ], ## RFC 3118 authentication option -- NOT IMPLEMENTED
937
- :client_last_transaction_time => [ 91, OptInt32 ], ## RFC 4388 leasequery option
938
- :associated_ip => [ 92, OptIPList ], ## RFC 4388 leasequery option
939
- :tz_posix => [ 100, OptData ], ## RFC 4833 timezone TZ-POSIX string (a POSIX time zone string like "MST7MDT6,M3.2.0/02:00,M11.1.0/02:00" which specifies an offset of 7 hours behind UTC during standard time, 6 during daylight time, with daylight beginning the 2nd Sunday in March at 2:00 AM local time and continuing until the 1st Sunday in November at 2:00 AM local time)
940
- :tz_database => [ 101, OptData ], ## RFC 4833 timezone TZ-Database string (the name of a time zone in a database, like "America/Denver")
941
- :classless_static_routes => [ 121, OptRouteList ], ## RFC 3442 classless static routes - obsoletes option 33 - Ignore opt. 33 if 121 is present - Should specify default routes using option 3 if this option is also present (can specify them in this option too) so if a client ignores 121, a default route will still be set up -- If client requests CLASSLESS STATIC ROUTES and either ROUTERS and/or STATIC ROUTES, ONLY respond with this option (see p. 6 RFC 3442)
942
- ## START SITE-SPECIFIC OPTIONS (128..254 inclusive):
943
- :ms_classless_static_routes => [ 249, OptRouteList ], ## Microsoft version of option 121 - does NOT ignore opt. 33 if present (differs from opt. 121)
944
- :site_local_auto_proxy_config => [ 252, OptData ], ## WPAD site-local proxy configuration
945
- ## END SITE-SPECIFIC OPTIONS
946
- :end => [ 255, Opt ]
51
+ ## Map message integer type to string:
52
+ MSG_TYPE_TO_STR = MSG_STR_TO_TYPE.invert
53
+
54
+ ## Map message type to correct packet operation (BOOTREQUEST/BOOTREPLY):
55
+ MSG_TYPE_TO_OP = {
56
+ DHCPDISCOVER => BOOTREQUEST,
57
+ DHCPOFFER => BOOTREPLY,
58
+ DHCPREQUEST => BOOTREQUEST,
59
+ DHCPDECLINE => BOOTREPLY,
60
+ DHCPACK => BOOTREPLY,
61
+ DHCPNAK => BOOTREPLY,
62
+ DHCPRELEASE => BOOTREQUEST,
63
+ DHCPINFORM => BOOTREQUEST,
64
+ DHCPFORCERENEW => BOOTREPLY,
65
+ DHCPLEASEQUERY => BOOTREQUEST,
66
+ DHCPLEASEUNASSIGNED => BOOTREPLY,
67
+ DHCPLEASEUNKNOWN => BOOTREPLY,
68
+ DHCPLEASEACTIVE => BOOTREPLY
947
69
  }
948
70
 
949
- def self.make_opt_name(name, data=nil)
950
- raise "Unknown/unhandled option '#{name}'" unless OPTIONS.key?(name)
951
- OPTIONS[name][1].new(OPTIONS[name][0], name, data)
952
- end
71
+ ## DHCP MAGIC:
72
+ MAGIC = [99, 130, 83, 99].pack('C4')
953
73
 
954
- def self.make_opt(opt, data=nil)
955
- OPTIONS.each do |name, info|
956
- return info[1].new(info[0], name, data) if info[0] == opt
957
- end
958
- return nil
959
- end
960
74
  end
961
75