dhcp 0.0.1 → 0.0.3

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