1---
2-- Minimalistic DHCP6 (Dynamic Host Configuration Protocol for IPv6)
3-- implementation supporting basic DHCP6 Solicit requests The library
4-- is structured around the following classes:
5-- * DHCP6.Option - DHCP6 options encoders (for requests) and decoders
6--                  (for responses)
7-- * DHCP6.Request - DHCP6 request encoder and decoder
8-- * DHCP6.Response - DHCP6 response encoder and decoder
9-- * Helper - The helper class, primary script interface
10--
11-- The following sample code sends a DHCP6 Solicit request and returns a
12-- response suitable for script output:
13-- <code>
14--   local helper = DHCP6.Helper:new("eth0")
15--   local status, response = helper:solicit()
16--   if ( status ) then
17--      return stdnse.format_output(true, response)
18--   end
19-- </code>
20--
21-- @author Patrik Karlsson <patrik@cqure.net>
22--
23
24local datetime = require "datetime"
25local ipOps = require "ipOps"
26local math = require "math"
27local nmap = require "nmap"
28local os = require "os"
29local stdnse = require "stdnse"
30local string = require "string"
31local table = require "table"
32_ENV = stdnse.module("dhcp6", stdnse.seeall)
33
34DHCP6 = {}
35
36-- DHCP6 request and response types
37DHCP6.Type = {
38  SOLICIT = 1,
39  ADVERTISE = 2,
40  REQUEST = 3,
41}
42
43-- DHCP6 type as string
44DHCP6.TypeStr = {
45  [DHCP6.Type.SOLICIT] = "Solicit",
46  [DHCP6.Type.ADVERTISE] = "Advertise",
47  [DHCP6.Type.REQUEST] = "Request",
48}
49
50-- DHCP6 option types
51DHCP6.OptionTypes = {
52  OPTION_CLIENTID = 0x01,
53  OPTION_SERVERID = 0x02,
54  OPTION_IA_NA = 0x03,
55  OPTION_IAADDR = 0x05,
56  OPTION_ELAPSED_TIME = 0x08,
57  OPTION_STATUS_CODE = 0x0d,
58  OPTION_DNS_SERVERS = 0x17,
59  OPTION_DOMAIN_LIST = 0x18,
60  OPTION_IA_PD = 0x19,
61  OPTION_SNTP_SERVERS = 0x1f,
62  OPTION_CLIENT_FQDN = 0x27,
63}
64
65-- DHCP6 options
66DHCP6.Option = {
67
68  [DHCP6.OptionTypes.OPTION_ELAPSED_TIME] = {
69
70    -- Create a new class instance
71    -- @param time in ms since last request
72    -- @return o new instance of class
73    new = function(self, time)
74      local o = {
75        type = DHCP6.OptionTypes.OPTION_ELAPSED_TIME,
76        time = time,
77        -- in case no time was created, we need this to be able to
78        -- calculate time since instantiation
79        created = os.time(),
80      }
81      setmetatable(o, self)
82      self.__index = self
83      return o
84    end,
85
86    -- Converts option to a string
87    -- @return str string containing the class instance as string
88    __tostring = function(self)
89      local data
90      if ( self.time ) then
91        data = string.pack(">I2", self.time)
92      else
93        data = string.pack(">I2", (os.time() - self.created) * 1000)
94      end
95      return string.pack(">I2s2", self.type, data)
96    end,
97
98  },
99
100  [DHCP6.OptionTypes.OPTION_CLIENTID] = {
101
102    -- Create a new class instance
103    -- @param mac string containing the mac address
104    -- @param duid number the duid of the client
105    -- @param hwtype number the hwtype of the client
106    -- @param time number time since 2000-01-01 00:00:00
107    -- @return o new instance of class
108    new = function(self, mac, duid, hwtype, time)
109      local o = {
110        type = DHCP6.OptionTypes.OPTION_CLIENTID,
111        duid = duid or 1,
112        hwtype = hwtype or 1,
113        time = time or os.time() - os.time({year=2000, day=1, month=1, hour=0, min=0, sec=0}),
114        mac = mac,
115      }
116      setmetatable(o, self)
117      self.__index = self
118      return o
119    end,
120
121    -- Parse the data string and create an instance of the class
122    -- @param data string containing the data as received over the socket
123    -- @return opt new instance of option
124    parse = function(data)
125      local opt = DHCP6.Option[DHCP6.OptionTypes.OPTION_CLIENTID]:new()
126      local pos
127      opt.duid, pos = string.unpack(">I2", data, pos)
128      if ( 1 ~= opt.duid ) then
129        stdnse.debug1("Unexpected DUID type (%d)", opt.duid)
130        return
131      end
132      opt.hwtype, opt.time = string.unpack(">I2I4", data, pos)
133      opt.mac = data:sub(pos)
134      opt.time = opt.time + os.time({year=2000, day=1, month=1, hour=0, min=0, sec=0})
135      return opt
136    end,
137
138    -- Converts option to a string
139    -- @return str string containing the class instance as string
140    __tostring = function(self)
141      local data = string.pack(">I2I2I4", self.duid, self.hwtype, self.time) .. self.mac
142      return string.pack(">I2s2", self.type, data)
143    end,
144  },
145
146  [DHCP6.OptionTypes.OPTION_SERVERID] = {
147    -- Create a new class instance
148    -- @param mac string containing the mac address
149    -- @param duid number the duid of the client
150    -- @param hwtype number the hwtype of the client
151    -- @param time number time since 2000-01-01 00:00:00
152    -- @return o new instance of class
153    new = function(...) return DHCP6.Option[DHCP6.OptionTypes.OPTION_CLIENTID].new(...) end,
154
155    -- Parse the data string and create an instance of the class
156    -- @param data string containing the data as received over the socket
157    -- @return opt new instance of option
158    parse = function(...) return DHCP6.Option[DHCP6.OptionTypes.OPTION_CLIENTID].parse(...) end,
159
160    -- Converts option to a string
161    -- @return str string containing the class instance as string
162    __tostring = function(...) return DHCP6.Option[DHCP6.OptionTypes.OPTION_CLIENTID].__tostring(...) end,
163  },
164
165  [DHCP6.OptionTypes.OPTION_STATUS_CODE] = {
166
167    -- Create a new class instance
168    -- @param code number containing the error code
169    -- @param msg string containing the error message
170    -- @return o new instance of class
171    new = function(self, code, msg)
172      local o = {
173        type = DHCP6.OptionTypes.OPTION_STATUS_CODE,
174        code = code,
175        msg = msg,
176      }
177      setmetatable(o, self)
178      self.__index = self
179      return o
180    end,
181
182    -- Parse the data string and create an instance of the class
183    -- @param data string containing the data as received over the socket
184    -- @return opt new instance of option
185    parse = function(data)
186      local opt = DHCP6.Option[DHCP6.OptionTypes.OPTION_STATUS_CODE]:new()
187
188      local pos
189      opt.code, pos = string.unpack(">I2", data)
190      opt.msg = data:sub(pos)
191
192      return opt
193    end,
194
195  },
196
197  [DHCP6.OptionTypes.OPTION_DNS_SERVERS] = {
198
199    -- Create a new class instance
200    -- @param servers table containing DNS servers
201    -- @return o new instance of class
202    new = function(self, servers)
203      local o = {
204        type = DHCP6.OptionTypes.OPTION_DNS_SERVERS,
205        servers = servers or {},
206      }
207      setmetatable(o, self)
208      self.__index = self
209      return o
210    end,
211
212    -- Parse the data string and create an instance of the class
213    -- @param data string containing the data as received over the socket
214    -- @return opt new instance of option
215    parse = function(data)
216      local opt = DHCP6.Option[DHCP6.OptionTypes.OPTION_DNS_SERVERS]:new()
217      local pos, count = 1, #data/16
218
219      for i=1,count do
220        local srv
221        srv, pos = string.unpack(">c16", data, pos)
222        table.insert(opt.servers, srv)
223      end
224      return opt
225    end,
226
227    -- Converts option to a string
228    -- @return str string containing the class instance as string
229    __tostring = function(self)
230      local data = {}
231      for _, ipv6 in ipairs(self.servers) do
232        data[#data+1] = ipOps.ip_to_str(ipv6)
233      end
234      data = table.concat(data)
235      return string.pack(">I2s2", self.type, data)
236    end
237  },
238
239  [DHCP6.OptionTypes.OPTION_DOMAIN_LIST] = {
240
241    -- Create a new class instance
242    -- @param domain table containing the search domains
243    -- @return o new instance of class
244    new = function(self, domains)
245      local o = {
246        type = DHCP6.OptionTypes.OPTION_DOMAIN_LIST,
247        domains = domains or {},
248      }
249      setmetatable(o, self)
250      self.__index = self
251      return o
252    end,
253
254    -- Parse the data string and create an instance of the class
255    -- @param data string containing the data as received over the socket
256    -- @return opt new instance of option
257    parse = function(data)
258      local opt = DHCP6.Option[DHCP6.OptionTypes.OPTION_DOMAIN_LIST]:new()
259      local pos = 1
260
261      repeat
262        local domain = {}
263        repeat
264          local part
265          part, pos = string.unpack("s1", data, pos)
266          if ( part ~= "" ) then
267            table.insert(domain, part)
268          end
269        until( part == "" )
270        table.insert(opt.domains, table.concat(domain, "."))
271      until( pos > #data )
272      return opt
273    end,
274
275
276  },
277
278  [DHCP6.OptionTypes.OPTION_IA_PD] = {
279
280    -- Create a new class instance
281    -- @param iad number containing iad
282    -- @param t1 number containing t1
283    -- @param t2 number containing t2
284    -- @param option string containing any options
285    -- @return o new instance of class
286    new = function(self, iaid, t1, t2, options)
287      local o = {
288        type = DHCP6.OptionTypes.OPTION_IA_PD,
289        iaid = iaid,
290        t1 = t1 or 0,
291        t2 = t2 or 0,
292        options = options or "",
293      }
294      setmetatable(o, self)
295      self.__index = self
296      return o
297    end,
298
299    -- Converts option to a string
300    -- @return str string containing the class instance as string
301    __tostring = function(self)
302      local data = string.pack(">I4I4I4", self.iaid, self.t1, self.t2) .. self.options
303      return string.pack(">I2s2", self.type, data)
304    end,
305
306  },
307
308  [DHCP6.OptionTypes.OPTION_IA_NA] = {
309
310    -- Create a new class instance
311    -- @param iad number containing iad
312    -- @param t1 number containing t1
313    -- @param t2 number containing t2
314    -- @param option table containing any options
315    -- @return o new instance of class
316    new = function(self, iaid, t1, t2, options)
317      local o = {
318        type = DHCP6.OptionTypes.OPTION_IA_NA,
319        iaid = iaid,
320        t1 = t1 or 0,
321        t2 = t2 or 0,
322        options = options or {},
323      }
324      setmetatable(o, self)
325      self.__index = self
326      return o
327    end,
328
329    -- Parse the data string and create an instance of the class
330    -- @param data string containing the data as received over the socket
331    -- @return opt new instance of option
332    parse = function(data)
333      local opt = DHCP6.Option[DHCP6.OptionTypes.OPTION_IA_NA]:new()
334      local pos
335
336      opt.iaid, opt.t1, opt.t2, pos = string.unpack(">I4I4I4", data)
337
338      -- do we have any options
339      while ( pos < #data ) do
340        local typ, len, ipv6, pref_lt, valid_lt, options
341        typ, len, pos = string.unpack(">I2I2", data, pos)
342
343        if ( 5 == DHCP6.OptionTypes.OPTION_IAADDR ) then
344          local addr = { type = DHCP6.OptionTypes.OPTION_IAADDR }
345          addr.ipv6, addr.pref_lt, addr.valid_lt, pos = string.unpack(">c16I4I4", data, pos)
346          table.insert(opt.options, addr)
347        else
348          pos = pos + len
349        end
350      end
351      return opt
352    end,
353
354    -- Converts option to a string
355    -- @return str string containing the class instance as string
356    __tostring = function(self)
357      local data = string.pack(">I4I4I4", self.iaid, self.t1, self.t2)
358
359      -- TODO: we don't cover self.options here, we should probably add that
360      return string.pack(">I2s2", self.type, data)
361    end,
362  },
363
364  [DHCP6.OptionTypes.OPTION_SNTP_SERVERS] = {
365
366    -- Create a new class instance
367    -- @param servers table containing the NTP servers
368    -- @return o new instance of class
369    new = function(self, servers)
370      local o = {
371        type = DHCP6.OptionTypes.OPTION_SNTP_SERVERS,
372        servers = servers or {},
373      }
374      setmetatable(o, self)
375      self.__index = self
376      return o
377    end,
378
379    -- Parse the data string and create an instance of the class
380    -- @param data string containing the data as received over the socket
381    -- @return opt new instance of option
382    parse = function(data)
383      local opt = DHCP6.Option[DHCP6.OptionTypes.OPTION_SNTP_SERVERS]:new()
384      local pos, server
385
386      repeat
387        server, pos = string.unpack(">c16", data, pos)
388        table.insert( opt.servers, ipOps.str_to_ip(server) )
389      until( pos > #data )
390      return opt
391    end,
392  },
393
394  [DHCP6.OptionTypes.OPTION_CLIENT_FQDN] = {
395
396    -- Create a new class instance
397    -- @param fqdn string containing the fqdn
398    -- @return o new instance of class
399    new = function(self, fqdn)
400      local o = {
401        type = DHCP6.OptionTypes.OPTION_CLIENT_FQDN,
402        fqdn = fqdn or "",
403      }
404      setmetatable(o, self)
405      self.__index = self
406      return o
407    end,
408
409    -- Parse the data string and create an instance of the class
410    -- @param data string containing the data as received over the socket
411    -- @return opt new instance of option
412    parse = function(data)
413      local opt = DHCP6.Option[DHCP6.OptionTypes.OPTION_CLIENT_FQDN]:new()
414      local pos = 2
415      local pieces = {}
416
417      repeat
418        local tmp
419        tmp, pos = string.unpack("s1", data, pos)
420        table.insert(pieces, tmp)
421      until(pos >= #data)
422      opt.fqdn = table.concat(pieces, ".")
423      return opt
424    end,
425
426  }
427
428}
429
430
431DHCP6.Request = {
432
433  -- Create a new class instance
434  -- @param msgtype number containing the message type
435  -- @param xid number containing the transaction id
436  -- @param opts table containing any request options
437  -- @return o new instance of class
438  new = function(self, msgtype, xid, opts)
439    local o = {
440      type = msgtype,
441      xid = xid or math.random(1048575),
442      opts = opts or {}
443    }
444    setmetatable(o, self)
445    self.__index = self
446    return o
447  end,
448
449  -- Adds a new DHCP6 option to the request
450  -- @param opt instance of object to add to the request
451  addOption = function(self, opt)
452    table.insert(self.opts, opt)
453  end,
454
455  -- Converts option to a string
456  -- @return str string containing the class instance as string
457  __tostring = function(self)
458    local tmp = (self.type << 24) + self.xid
459    local data = {}
460
461    for _, opt in ipairs(self.opts) do
462      data[#data+1] = tostring(opt)
463    end
464    return string.pack(">I4", tmp) .. table.concat(data)
465  end,
466
467}
468
469-- The Response class handles responses from the server
470DHCP6.Response = {
471
472  -- Creates a new instance of the response class
473  -- @param msgtype number containing the type of DHCP6 message
474  -- @param xid number containing the transaction ID
475  new = function(self, msgtype, xid, opts)
476    local o = {
477      msgtype = msgtype,
478      xid = xid,
479      opts = opts or {},
480    }
481    setmetatable(o, self)
482    self.__index = self
483    return o
484  end,
485
486  -- Parse the data string and create an instance of the class
487  -- @param data string containing the data as received over the socket
488  -- @return opt new instance of option
489  parse = function(data)
490    local resp = DHCP6.Response:new()
491    local tmp, pos = string.unpack(">I4", data)
492
493    resp.msgtype = (tmp & 0xFF000000)
494    resp.msgtype = (resp.msgtype >> 24)
495    resp.xid = (tmp & 0x00FFFFFF)
496    while( pos < #data ) do
497      local opt = {}
498      opt.type, opt.data, pos = string.unpack(">I2s2", data, pos)
499      if ( DHCP6.Option[opt.type] and DHCP6.Option[opt.type].parse ) then
500        local opt_parsed = DHCP6.Option[opt.type].parse(opt.data)
501        if ( not(opt_parsed) ) then
502          table.insert(resp.opts, { type = opt.type, raw = opt.data })
503        else
504          table.insert(resp.opts, { type = opt.type, resp = opt_parsed, raw = opt.data })
505        end
506      else
507        stdnse.debug2("No option decoder for type: %d; len: %d", opt.type, #(opt.data or ""))
508        table.insert(resp.opts, { type = opt.type, raw = opt.data })
509      end
510    end
511    return resp
512  end
513
514}
515
516-- Table of option to string converters
517-- Each option should have its own function to convert an instance of option
518-- to a printable string.
519--
520-- TODO: These functions could eventually be moved to a method in its
521-- respective class.
522OptionToString = {
523
524  [DHCP6.OptionTypes.OPTION_CLIENTID] = function(opt)
525    local HWTYPE_ETHER = 1
526    if ( HWTYPE_ETHER == opt.hwtype ) then
527      local mac = stdnse.tohex(opt.mac):upper()
528      mac = mac:gsub("..", "%1:"):sub(1, -2)
529      local tm = datetime.format_timestamp(opt.time)
530      return "Client identifier", ("MAC: %s; Time: %s"):format(mac, tm)
531    end
532  end,
533
534  [DHCP6.OptionTypes.OPTION_SERVERID] = function(opt)
535    local topic, str = OptionToString[DHCP6.OptionTypes.OPTION_CLIENTID](opt)
536    return "Server identifier", str
537  end,
538
539  [DHCP6.OptionTypes.OPTION_IA_NA] = function(opt)
540    if ( opt.options and 1 == #opt.options ) then
541      local ipv6 = ipOps.str_to_ip(opt.options[1].ipv6)
542      return "Non-temporary Address", ipv6
543    end
544  end,
545
546  [DHCP6.OptionTypes.OPTION_DNS_SERVERS] = function(opt)
547    local servers = {}
548    for _, srv in ipairs(opt.servers) do
549      local ipv6 = ipOps.str_to_ip(srv)
550      table.insert(servers, ipv6)
551    end
552    return "DNS Servers", table.concat(servers, ",")
553  end,
554
555  [DHCP6.OptionTypes.OPTION_DOMAIN_LIST] = function(opt)
556    return "Domain Search", table.concat(opt.domains, ", ")
557  end,
558
559  [DHCP6.OptionTypes.OPTION_STATUS_CODE] = function(opt)
560    return "Error", ("Code: %d; Message: %s"):format(opt.code, opt.msg)
561  end,
562
563  [DHCP6.OptionTypes.OPTION_SNTP_SERVERS] = function(opt)
564    return "NTP Servers", table.concat(opt.servers, ", ")
565  end,
566}
567
568-- The Helper class serves as the main interface to scripts
569Helper = {
570
571  -- Creates a new Helper class instance
572  -- @param iface string containing the interface name
573  -- @param options table containing any options, currently
574  --        <code>timeout</code> - socket timeout in ms
575  -- @return o new instance of Helper
576  new = function(self, iface, options)
577    local o = {
578      iface = iface,
579      options = options or {},
580    }
581    setmetatable(o, self)
582    self.__index = self
583
584    local info, err = nmap.get_interface_info(iface)
585    -- if we fail to get interface info, don't return a helper
586    -- this is true on OS X for interfaces like: p2p0 and vboxnet0
587    if ( not(info) and err ) then
588      return
589    end
590    o.mac = info.mac
591    o.socket = nmap.new_socket("udp")
592    o.socket:bind(nil, 546)
593    o.socket:set_timeout(o.options.timeout or 5000)
594    return o
595  end,
596
597  -- Sends a DHCP6 Solicit message to the server, essentially requesting a new
598  -- IPv6 non-temporary address
599  -- @return table of results suitable for use with
600  --         <code>stdnse.format_output</code>
601  solicit = function(self)
602    local req = DHCP6.Request:new( DHCP6.Type.SOLICIT )
603    local option = DHCP6.Option
604    req:addOption(option[DHCP6.OptionTypes.OPTION_ELAPSED_TIME]:new())
605    req:addOption(option[DHCP6.OptionTypes.OPTION_CLIENTID]:new(self.mac))
606
607    local iaid = string.unpack(">I4", self.mac:sub(3))
608    req:addOption(option[DHCP6.OptionTypes.OPTION_IA_NA]:new(iaid, 3600, 5400))
609
610    self.host, self.port = { ip = "ff02::1:2" }, { number = 547, protocol = "udp"}
611    local status, err = self.socket:sendto( self.host, self.port, tostring(req) )
612    if ( not(status) ) then
613      self.host.ip = ("%s%%%s"):format(self.host.ip, self.iface)
614      status, err = self.socket:sendto( self.host, self.port, tostring(req) )
615      if ( not(status) ) then
616        return false, "Failed to send DHCP6 request to server"
617      end
618    end
619
620    local resp, retries = {}, 3
621    repeat
622      retries = retries - 1
623      local status, data = self.socket:receive()
624      if ( not(status) ) then
625        return false, "Failed to receive DHCP6 request from server"
626      end
627
628      resp = DHCP6.Response.parse(data)
629      if ( not(resp) ) then
630        return false, "Failed to decode DHCP6 response from server"
631      end
632    until( req.xid == resp.xid or retries == 0 )
633
634    if ( req.xid ~= resp.xid ) then
635      return false, "Failed to receive DHCP6 response from server"
636    end
637
638    local result, result_options = {}, { name = "Options" }
639    local resptype = DHCP6.TypeStr[resp.msgtype] or ("Unknown (%d)"):format(resp.msgtype)
640
641    table.insert(result, ("Message type: %s"):format(resptype))
642    table.insert(result, ("Transaction id: %d"):format(resp.xid))
643
644    for _, opt in ipairs(resp.opts or {}) do
645      if ( OptionToString[opt.type] ) then
646        local topic, str = OptionToString[opt.type](opt.resp)
647        if ( topic and str ) then
648          table.insert(result_options, ("%s: %s"):format(topic, str))
649        end
650      else
651        stdnse.debug2("No decoder for option type: %d", opt.type)
652      end
653    end
654    table.insert(result, result_options)
655    return true, result
656  end,
657}
658
659
660return _ENV;
661