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