--- -- A minimal Internet Storage Name Service (iSNS) implementation -- -- @author "Patrik Karlsson " -- local bin = require('bin') local bit = require('bit') local ipops = require('ipOps') local match = require('match') local nmap = require('nmap') local stdnse = require('stdnse') local tab = require('tab') local table = require('table') _ENV = stdnse.module("isns", stdnse.seeall); iSCSI = { NodeType = { TARGET = 1, INITIATOR = 2, CONTROL = 4, } } Header = { VERSION = 1, -- -- Creates a header instance -- -- @param func_id number containing the function ID of the message -- @param pdu_len number containing the length of the PDU -- @param flags number containing the message flags -- @param trans_id number containing the transaction id -- @param seq_id number containing the sequence id -- @return o new class instance new = function(self, func_id, pdu_len, flags, trans_id, seq_id) local o = { ver = Header.VERSION, func_id = func_id, flags = flags, trans_id = trans_id, seq_id = seq_id, pdu_len = pdu_len, } setmetatable(o, self) self.__index = self return o end, -- -- Parses a opaque string and creates a new Header instance -- -- @param data opaques string containing the raw data -- @return hdr new instance of Header parse = function(data) local hdr = Header:new() local pos pos, hdr.ver, hdr.func_id, hdr.pdu_len, hdr.flags, hdr.trans_id, hdr.seq_id = bin.unpack(">SSSSSS", data) return hdr end, -- -- Converts the instance to an opaque string -- @return str containing an opaque string __tostring = function(self) return bin.pack(">SSSSSS", self.ver, self.func_id, self.pdu_len, self.flags, self.trans_id, self.seq_id ) end } Attribute = { Tag = { ISNS_TAG_DELIMITER = 0, ISNS_TAG_ENTITY_IDENTIFIER = 1, ISNS_TAG_ENTITY_PROTOCOL = 2, ISNS_TAG_MGMT_IP_ADDRESS = 3, ISNS_TAG_TIMESTAMP = 4, ISNS_TAG_PROTOCOL_VERSION_RANGE = 5, ISNS_TAG_REGISTRATION_PERIOD = 6, ISNS_TAG_ENTITY_INDEX = 7, ISNS_TAG_ENTITY_NEXT_INDEX = 8, ISNS_TAG_ENTITY_ISAKMP_PHASE_1 = 11, ISNS_TAG_ENTITY_CERTIFICATE = 12, ISNS_TAG_PORTAL_IP_ADDRESS = 16, ISNS_TAG_PORTAL_TCP_UDP_PORT = 17, ISNS_TAG_PORTAL_SYMBOLIC_NAME = 18, ISNS_TAG_ESI_INTERVAL = 19, ISNS_TAG_ESI_PORT = 20, ISNS_TAG_PORTAL_INDEX = 22, ISNS_TAG_SCN_PORT = 23, ISNS_TAG_PORTAL_NEXT_INDEX = 24, ISNS_TAG_PORTAL_SECURITY_BITMAP = 27, ISNS_TAG_PORTAL_ISAKMP_PHASE_1 = 28, ISNS_TAG_PORTAL_ISAKMP_PHASE_2 = 29, ISNS_TAG_PORTAL_CERTIFICATE = 31, ISNS_TAG_ISCSI_NAME = 32, ISNS_TAG_ISCSI_NODE_TYPE = 33, ISNS_TAG_ISCSI_ALIAS = 34, ISNS_TAG_ISCSI_SCN_BITMAP = 35, ISNS_TAG_ISCSI_NODE_INDEX = 36, ISNS_TAG_WWNN_TOKEN = 37, ISNS_TAG_ISCSI_NODE_NEXT_INDEX = 38, ISNS_TAG_ISCSI_AUTHMETHOD = 42, ISNS_TAG_PG_ISCSI_NAME = 48, ISNS_TAG_PG_PORTAL_IP_ADDR = 49, ISNS_TAG_PG_PORTAL_TCP_UDP_PORT = 50, ISNS_TAG_PG_TAG = 51, ISNS_TAG_PG_INDEX = 52, ISNS_TAG_PG_NEXT_INDEX = 53, ISNS_TAG_FC_PORT_NAME_WWPN = 64, ISNS_TAG_PORT_ID = 65, ISNS_TAG_FC_PORT_TYPE = 66, ISNS_TAG_SYMBOLIC_PORT_NAME = 67, ISNS_TAG_FABRIC_PORT_NAME = 68, ISNS_TAG_HARD_ADDRESS = 69, ISNS_TAG_PORT_IP_ADDRESS = 70, ISNS_TAG_CLASS_OF_SERVICE = 71, ISNS_TAG_FC4_TYPES = 72, ISNS_TAG_FC4_DESCRIPTOR = 73, ISNS_TAG_FC4_FEATURES = 74, ISNS_TAG_IFCP_SCN_BITMAP = 75, ISNS_TAG_PORT_ROLE = 76, ISNS_TAG_PERMANENT_PORT_NAME = 77, ISNS_TAG_FC4_TYPE_CODE = 95, ISNS_TAG_FC_NODE_NAME_WWNN = 96, ISNS_TAG_SYMBOLIC_NODE_NAME = 97, ISNS_TAG_NODE_IP_ADDRESS = 98, ISNS_TAG_NODE_IPA = 99, ISNS_TAG_PROXY_ISCSI_NAME = 101, ISNS_TAG_SWITCH_NAME = 128, ISNS_TAG_PREFERRED_ID = 129, ISNS_TAG_ASSIGNED_ID = 130, ISNS_TAG_VIRTUAL_FABRIC_ID = 131, ISNS_TAG_SERVER_VENDOR_OUI = 256, ISNS_TAG_DD_SET_ID = 2049, ISNS_TAG_DD_SET_SYMBOLIC_NAME = 2050, ISNS_TAG_DD_SET_STATUS = 2051, ISNS_TAG_DD_SET_NEXT_ID = 2052, ISNS_TAG_DD_ID = 2065, ISNS_TAG_DD_SYMBOLIC_NAME = 2066, ISNS_TAG_DD_MEMBER_ISCSI_INDEX = 2067, ISNS_TAG_DD_MEMBER_ISCSI_NAME = 2068, ISNS_TAG_DD_MEMBER_FC_PORT_NAME = 2069, ISNS_TAG_DD_MEMBER_PORTAL_INDEX = 2070, ISNS_TAG_DD_MEMBER_PORTAL_IP_ADDR = 2071, ISNS_TAG_DD_MEMBER_PORTAL_TCP_UDP_PORT = 2072, ISNS_TAG_DD_FEATURES = 2078, ISNS_TAG_DD_NEXT_ID = 2079, ISNS_VENDOR_SPECIFIC_SERVER_BASE = 257, ISNS_VENDOR_SPECIFIC_ENTITY_BASE = 385, ISNS_VENDOR_SPECIFIC_PORTAL_BASE = 513, ISNS_VENDOR_SPECIFIC_NODE_BASE = 641, ISNS_VENDOR_SPECIFIC_DD_BASE = 1024, ISNS_VENDOR_SPECIFIC_DDSET_BASE = 1281, ISNS_VENDOR_SPECIFIC_OTHER_BASE = 1537, }, -- -- Creates a new Attribute instance -- -- @param tag number containing the tag number -- @param val string containing the tag value -- @param len number containing the tag length -- @return o new Attribute instance new = function(self, tag, val, len) local o = { tag = tag, len = ( len or (val and #val or 0) ), val = val or "" } setmetatable(o, self) self.__index = self return o end, -- -- Creates a new Attribute instance -- -- @param data string containing an opaque string of raw data -- @return attr new instance of Attribute parse = function(data) local attr = Attribute:new() local pos pos, attr.tag, attr.len = bin.unpack(">II", data) pos, attr.val = bin.unpack(">A" .. attr.len, pos) return attr end, -- -- Converts the instance to an opaque string -- @return str containing an opaque string __tostring = function(self) return bin.pack(">IIA", self.tag, self.len, self.val) end, } Attributes = { -- -- Creates a new Attributes table -- @return o new instance of Attributes new = function(self) local o = { attribs = {} } setmetatable(o, self) self.__index = self return o end, -- -- Adds a new Attribute to the table -- @param tag number containing the tag number -- @param val string containing the tag value -- @param len number containing the tag length add = function(self, tag, val, len) table.insert(self, Attribute:new(tag, val, len)) end, -- -- Converts the instance to an opaque string -- @return str containing an opaque string __tostring = function(self) local str = "" for _, attr in ipairs(self) do str = str .. tostring(attr) end return str end, } Request = { FuncId = { DevAttrReg = 0x0001, DevAttrQry = 0x0002, DevGetNext = 0x0003, DevDereg = 0x0004, SCNReg = 0x0005, SCNDereg = 0x0006, SCNEvent = 0x0007, SCN = 0x0008, DDReg = 0x0009, DDDereg = 0x000A, DDSReg = 0x000B, DDSDereg = 0x000C, ESI = 0x000D, Heartbeat = 0x000E, }, -- -- Creates a new Request message -- @param func_id number containing the function ID of the message -- @param flags number containing the message flags -- @param data string containing the opaque raw data -- @param auth string containing the opaqur raw auth data -- @param trans_id number containing the transaction id -- @param seq_id number containing the sequence id -- @return o new instance of Request new = function(self, func_id, flags, data, auth, trans_id, seq_id) local o = { header = Header:new(func_id, ( data and #data ) or 0, flags, ( trans_id or -1 ), ( seq_id or -1 )), data = data or "" } setmetatable(o, self) self.__index = self return o end, -- -- Converts the instance to an opaque string -- @return str containing an opaque string __tostring = function(self) return tostring(self.header) .. tostring(self.data) .. ( self.auth and self.auth or "" ) end, } Response = { Error = { [0] = "Successful", [1] = "Unknown Error", [2] = "Message Format Error", [3] = "Invalid Registration", [4] = "RESERVED", [5] = "Invalid Query", [6] = "Source Unknown", [7] = "Source Absent", [8] = "Source Unauthorized", [9] = "No Such Entry", [10] = "Version Not Supported", [11] = "Internal Error", [12] = "Busy", [13] = "Option Not Understood", [14] = "Invalid Update", [15] = "Message (FUNCTION_ID) Not Supported", [16] = "SCN Event Rejected", [17] = "SCN Registration Rejected", [18] = "Attribute Not Implemented", [19] = "FC_DOMAIN_ID Not Available", [20] = "FC_DOMAIN_ID Not Allocated", [21] = "ESI Not Available", [22] = "Invalid Deregistration", [23] = "Registration Feature Not Supported", }, -- -- Creates a new Response instance -- @return o new instance of Response new = function(self) local o = { attrs = Attributes:new() } setmetatable(o, self) self.__index = self return o end, -- -- Creates a new Response instance -- -- @param data string containing an opaque string of raw data -- @return attr new instance of Response parse = function(data) local hdr = Header.parse(data) local pos = #(tostring(hdr)) + 1 local resp = Response:new() pos, resp.error = bin.unpack(">I", data, pos) if ( resp.error ~= 0 ) then return resp end while( pos < #data ) do local tag, len, val pos, tag, len = bin.unpack(">II", data, pos) pos, val = bin.unpack("A" .. len, data, pos) resp.attrs:add( tag, val, len ) end return resp end, } Session = { -- -- Creates a new Session instance -- @param host table -- @param port table -- @return o instance of Session new = function(self, host, port) local o = { host = host, port = port, seq_id = 0, trans_id = 0, } setmetatable(o, self) self.__index = self return o end, -- -- Connects to the server -- @return status true on success, false on failure connect = function(self) self.socket = nmap.new_socket() self.socket:set_timeout(5000) return self.socket:connect(self.host, self.port) end, -- -- Sends data to the server -- @return status true on success, false on failure -- @return err string containing the error message on failure send = function(self, req) if ( not(req.header) or not(req.header.seq_id) or not(req.header.trans_id) ) then return false, "Failed to send invalid request" end -- update the sequence and transaction ID's req.header.seq_id = self.seq_id req.header.trans_id = self.trans_id local status, err = self.socket:send(tostring(req)) self.trans_id = self.trans_id + 1 return status, err end, -- -- Receives data from the server -- @return status true on success, false on failure -- @return response instance of response receive = function(self) -- receive the 24 byte header local status, buf_hdr = self.socket:receive_buf(match.numbytes(12), true) if ( not(status) ) then return status, buf_hdr end local hdr = Header.parse(buf_hdr) -- receive the data status, buf_data = self.socket:receive_buf(match.numbytes(hdr.pdu_len), true) if ( not(status) ) then return status, buf_data end return true, Response.parse(buf_hdr .. buf_data) end, close = function(self) return self.close() end } Helper = { -- -- Creates a new Helper instance -- @param host param -- @param port param -- @return o new instance of Helper new = function(self, host, port) local o = { session = Session:new(host, port) } setmetatable(o, self) self.__index = self return o end, -- -- Connects to the server -- @return status true on success, false on failure connect = function(self) return self.session:connect() end, -- -- Lists portals -- @return status true on success, false on failure -- @return resulst list of iSCSI nodes, err string on failure listPortals = function(self) local attribs, name = Attributes:new(), "iqn.control.node\0por" attribs:add(Attribute.Tag.ISNS_TAG_ISCSI_NAME, name) attribs:add(Attribute.Tag.ISNS_TAG_PORTAL_IP_ADDRESS) attribs:add(Attribute.Tag.ISNS_TAG_DELIMITER) attribs:add(Attribute.Tag.ISNS_TAG_PORTAL_IP_ADDRESS) attribs:add(Attribute.Tag.ISNS_TAG_PORTAL_TCP_UDP_PORT) attribs:add(Attribute.Tag.ISNS_TAG_ENTITY_IDENTIFIER) local flags = 0x8c00 -- Sender is iSNS client, Last PDU, First PDU local req = Request:new(Request.FuncId.DevAttrQry, flags, tostring(attribs)) if ( not(self.session:send(req)) ) then return false, "Failed to send message to server" end local status, resp = self.session:receive() if ( not(status) ) then return false, "Failed to receive message from server" end local results = {} local addr, proto, port for _, attr in ipairs(resp.attrs) do if ( attr.tag == Attribute.Tag.ISNS_TAG_PORTAL_IP_ADDRESS ) then addr = attr.val local pos, is_ipv4 = bin.unpack("A12", addr) if ( is_ipv4 == "\0\0\0\0\0\0\0\0\0\0\xFF\xFF" ) then local pos, bin_ip = bin.unpack("B4", addr, 13) addr = ipops.bin_to_ip(bin_ip) else local pos, bin_ip = bin.unpack("B16", addr) addr = ipops.bin_to_ip(bin_ip) end elseif ( attr.tag == Attribute.Tag.ISNS_TAG_PORTAL_TCP_UDP_PORT ) then local pos, s1 pos, s1, port = bin.unpack(">SS", attr.val) if ( s1 == 1 ) then proto = "udp" elseif ( s1 == 0 ) then proto = "tcp" else proto = "UNKNOWN" end elseif ( addr and proto and port ) then table.insert(results, { addr = addr, proto = proto, port = port } ) addr, proto, port = nil, nil, nil end end return true, results end, -- -- Lists iSCSI nodes -- @return status true on success, false on failure -- @return resulst list of iSCSI nodes, err string on failure listISCINodes = function(self) local attribs = Attributes:new() local name = "iqn.control.node\0por" attribs:add(Attribute.Tag.ISNS_TAG_ISCSI_NAME, name) attribs:add(Attribute.Tag.ISNS_TAG_ISCSI_NAME) attribs:add(Attribute.Tag.ISNS_TAG_DELIMITER) attribs:add(Attribute.Tag.ISNS_TAG_ISCSI_NAME) attribs:add(Attribute.Tag.ISNS_TAG_ISCSI_NODE_TYPE) local flags = 0x8c00 -- Sender is iSNS client, Last PDU, First PDU local req = Request:new(Request.FuncId.DevAttrQry, flags, tostring(attribs)) if ( not(self.session:send(req)) ) then return false, "Failed to send message to server" end local status, resp = self.session:receive() if ( not(status) ) then return false, "Failed to receive message from server" end local name, ntype local results = {} for _, attr in ipairs(resp.attrs) do if ( attr.tag == Attribute.Tag.ISNS_TAG_ISCSI_NAME ) then name = attr.val elseif( attr.tag == Attribute.Tag.ISNS_TAG_ISCSI_NODE_TYPE ) then local _, val = bin.unpack(">I", attr.val) if ( val == iSCSI.NodeType.CONTROL ) then ntype = "Control" elseif ( val == iSCSI.NodeType.INITIATOR ) then ntype = "Initiator" elseif ( val == iSCSI.NodeType.TARGET ) then ntype = "Target" else ntype = "Unknown" end end if ( name and ntype ) then table.insert(results, { name = name:match("^([^\0]*)"), type = ntype }) name, ntype = nil, nil end end return true, results end, close = function(self) return self.session:close() end, } return _ENV;