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/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
+