summaryrefslogtreecommitdiffstats
path: root/nselib/stun.lua
diff options
context:
space:
mode:
Diffstat (limited to 'nselib/stun.lua')
-rw-r--r--nselib/stun.lua388
1 files changed, 388 insertions, 0 deletions
diff --git a/nselib/stun.lua b/nselib/stun.lua
new file mode 100644
index 0000000..438cdbc
--- /dev/null
+++ b/nselib/stun.lua
@@ -0,0 +1,388 @@
+---
+-- A library that implements the basics of the STUN protocol (Session
+-- Traversal Utilities for NAT) per RFC3489 and RFC5389. A protocol
+-- overview is available at http://en.wikipedia.org/wiki/STUN.
+--
+-- @args stun.mode Mode container to use. Supported containers: "modern"
+-- (default) or "classic"
+--
+-- @author Patrik Karlsson <patrik@cqure.net>
+--
+
+local ipOps = require "ipOps"
+local match = require "match"
+local rand = require "rand"
+local nmap = require "nmap"
+local stdnse = require "stdnse"
+local string = require "string"
+local table = require "table"
+_ENV = stdnse.module("stun", stdnse.seeall)
+
+-- The supported request types
+MessageType = {
+ BINDING_REQUEST = 0x0001,
+ BINDING_RESPONSE = 0x0101,
+}
+
+-- The header used in both request and responses
+Header = {
+
+ -- the header size in bytes
+ size = 20,
+
+ --- creates a new instance of Header
+ -- @param type number the request/response type
+ -- @param trans_id string the 128-bit transaction id
+ -- @param length number the packet length
+ -- @return new instance of Header
+ -- @name Header.new
+ new = function(self, type, trans_id, length)
+ local o = { type = type, trans_id = trans_id, length = length or 0 }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ --- parses an opaque string and creates a new Header instance
+ -- @param data opaque string
+ -- @return new instance of Header
+ -- @name Header.parse
+ parse = function(data)
+ local header = Header:new()
+ header.type, header.length, header.trans_id = string.unpack(">I2I2 c16", data)
+ return header
+ end,
+
+ -- converts the header to an opaque string
+ -- @return string containing the header instance
+ __tostring = function(self)
+ return string.pack(">I2I2", self.type, self.length) .. self.trans_id
+ end,
+}
+
+Request = {
+
+ -- The binding request
+ Bind = {
+
+ --- Creates a new Bind request
+ -- @param trans_id string containing the 128 bit transaction ID
+ -- @return new instance of the Bind request
+ -- @name Request.Bind.new
+ new = function(self, trans_id)
+ local o = {
+ header = Header:new(MessageType.BINDING_REQUEST, trans_id),
+ attributes = {}
+ }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ -- converts the instance to an opaque string
+ -- @return string containing the Bind request as string
+ __tostring = function(self)
+ local data = ""
+ for _, attrib in ipairs(self.attributes) do
+ data = data .. tostring(attrib)
+ end
+ self.header.length = #data
+ return tostring(self.header) .. data
+ end,
+ }
+
+}
+
+-- The attribute class
+Attribute = {
+
+ MAPPED_ADDRESS = 0x0001,
+ RESPONSE_ADDRESS = 0x0002,
+ CHANGE_REQUEST = 0x0003,
+ SOURCE_ADDRESS = 0x0004,
+ CHANGED_ADDRESS = 0x0005,
+ USERNAME = 0x0006,
+ PASSWORD = 0x0007,
+ MESSAGE_INTEGRITY = 0x0008,
+ ERROR_CODE = 0x0009,
+ UNKNOWN_ATTRIBUTES = 0x000a,
+ REFLECTED_FROM = 0x000b,
+ SERVER = 0x8022,
+
+ --- creates a new attribute instance
+ -- @param type number containing the attribute type
+ -- @param data string containing the attribute value
+ -- @return instance of attribute
+ -- @name Attribute.new
+ new = function(self, type, data)
+ local o = {
+ type = type,
+ length = (data and #data or 0),
+ data = data,
+ }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ --- parses a string and creates an Attribute instance
+ -- @param data string containing the raw attribute
+ -- @return new attribute instance
+ -- @name Attribute.parse
+ parse = function(data)
+ local attr = Attribute:new()
+ local pos = 1
+
+ attr.type, attr.length, pos = string.unpack(">I2I2", data, pos)
+
+ local function parseAddress(data, pos)
+ local addr = {}
+ addr.family, addr.port, addr.ip, pos = string.unpack(">xBI2c4", data, pos)
+ addr.ip = ipOps.str_to_ip(addr.ip)
+ return addr
+ end
+
+ if ( ( attr.type == Attribute.MAPPED_ADDRESS ) or
+ ( attr.type == Attribute.RESPONSE_ADDRESS ) or
+ ( attr.type == Attribute.SOURCE_ADDRESS ) or
+ ( attr.type == Attribute.CHANGED_ADDRESS ) ) then
+ if ( attr.length ~= 8 ) then
+ stdnse.debug2("Incorrect attribute length")
+ end
+ attr.addr = parseAddress(data, pos)
+ elseif( attr.type == Attribute.SERVER ) then
+ attr.server = data:sub(pos, pos + attr.length - 1)
+ end
+
+ return attr
+ end,
+
+ -- converts an attribute to string
+ -- @return string containing the serialized attribute
+ __tostring = function(self)
+ return string.pack(">I2I2", self.type, self.length) .. (self.data or "")
+ end,
+
+}
+
+-- Response class container
+Response = {
+
+ -- Bind response class
+ Bind = {
+
+ --- creates a new instance of the Bind response
+ -- @param trans_id string containing the 128 bit transaction id
+ -- @return new Bind instance
+ -- @name Response.Bind.new
+ new = function(self, trans_id)
+ local o = { header = Header:new(MessageType.BINDING_RESPONSE, trans_id) }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ --- parses a raw string and creates a new Bind instance
+ -- @param data string containing the raw data
+ -- @return a new Bind instance
+ -- @name Response.Bind.parse
+ parse = function(data)
+ local resp = Response.Bind:new()
+ local pos = Header.size + 1
+
+ resp.header = Header.parse(data)
+ resp.attributes = {}
+
+ while( pos < #data ) do
+ local attr = Attribute.parse(data:sub(pos))
+ table.insert(resp.attributes, attr)
+ pos = pos + attr.length + 4
+ end
+ return resp
+ end
+ }
+}
+
+-- The communication class
+Comm = {
+
+ --- creates a new Comm instance
+ -- @param host table
+ -- @param port table
+ -- @param options table, currently supporting:
+ -- <code>timeout</code> - socket timeout in ms.
+ -- @return new instance of Comm
+ -- @name Comm.new
+ new = function(self, host, port, options)
+ local o = {
+ host = host,
+ port = port,
+ options = options or { timeout = 10000 },
+ socket = nmap.new_socket(),
+ }
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ --- connects the socket to the server
+ -- @return status true on success, false on failure
+ -- @return err string containing an error message, if status is false
+ -- @name Comm.connect
+ connect = function(self)
+ self.socket:set_timeout(self.options.timeout)
+ return self.socket:connect(self.host, self.port)
+ end,
+
+ --- sends a request to the server
+ -- @return status true on success, false on failure
+ -- @return err string containing an error message, if status is false
+ -- @name Comm.send
+ send = function(self, data)
+ return self.socket:send(data)
+ end,
+
+ --- receives a response from the server
+ -- @return status true on success, false on failure
+ -- @return response containing a response instance, or
+ -- err string containing an error message, if status is false
+ -- @name Comm.recv
+ recv = function(self)
+ local status, hdr_data = self.socket:receive_buf(match.numbytes(Header.size), true)
+ if ( not(status) ) then
+ return false, "Failed to receive response from server"
+ end
+
+ local header = Header.parse(hdr_data)
+ if ( not(header) ) then
+ return false, "Failed to parse response header"
+ end
+
+ local status, data = self.socket:receive_buf(match.numbytes(header.length), true)
+ if ( header.type == MessageType.BINDING_RESPONSE ) then
+ local resp = Response.Bind.parse(hdr_data .. data)
+ return true, resp
+ end
+
+ return false, "Unknown response message received"
+ end,
+
+ --- sends the request instance to the server and receives the response
+ -- @param req request class instance
+ -- @return status true on success, false on failure
+ -- @return response containing a response instance, or
+ -- err string containing an error message, if status is false
+ -- @name Comm.exch
+ exch = function(self, req)
+ local status, err = self:send(tostring(req))
+ if ( not(status) ) then
+ return false, "Failed to send request to server"
+ end
+ return self:recv()
+ end,
+
+ --- closes the connection to the server
+ -- @return status true on success, false on failure
+ -- @return err string containing an error message, if status is false
+ -- @name Comm.close
+ close = function(self)
+ self.socket:close()
+ end,
+}
+
+-- The Helper class
+Helper = {
+
+ --- creates a new Helper instance
+ -- @param host table
+ -- @param port table
+ -- @param options table, currently supporting:
+ -- <code>timeout</code> - socket timeout in ms.
+ -- @param mode containing the mode container. Supported containers: "modern"
+ -- (default) or "classic"
+ -- @return o new instance of Helper
+ -- @name Helper.new
+ new = function(self, host, port, options, mode)
+ local o = {
+ mode = mode or stdnse.get_script_args("stun.mode") or "modern",
+ comm = Comm:new(host, port, options),
+ }
+ assert(o.mode == "modern" or o.mode == "classic", "Unsupported mode")
+ setmetatable(o, self)
+ self.__index = self
+ return o
+ end,
+
+ --- connects to the server
+ -- @return status true on success, false on failure
+ -- @return err string containing an error message, if status is false
+ -- @name Helper.connect
+ connect = function(self)
+ return self.comm:connect()
+ end,
+
+ --- Gets the external public IP
+ -- @return status true on success, false on failure
+ -- @return result containing the IP as string
+ -- @name Helper.getExternalAddress
+ getExternalAddress = function(self)
+ local trans_id
+
+ if ( self.mode == "classic" ) then
+ trans_id = rand.random_string(16)
+ else
+ trans_id = "\x21\x12\xA4\x42" .. rand.random_string(12)
+ end
+ local req = Request.Bind:new(trans_id)
+
+ local status, response = self.comm:exch(req)
+ if ( not(status) ) then
+ return false, "Failed to send data to server"
+ end
+
+ local result
+ for k, attr in pairs(response.attributes) do
+ if (attr.type == Attribute.MAPPED_ADDRESS ) then
+ result = ( attr.addr and attr.addr.ip or "<unknown>" )
+ end
+ if ( attr.type == Attribute.SERVER ) then
+ self.cache = self.cache or {}
+ self.cache.server = attr.server
+ end
+ end
+
+ if ( not(result) and not(self.cache) ) then
+ return false, "Server returned no response"
+ end
+
+ return status, result
+ end,
+
+ --- Gets the server version if it was returned by the server
+ -- @return status true on success, false on failure
+ -- @return version string containing the server product and version
+ -- @name Helper.getVersion
+ getVersion = function(self)
+ local status, response = false, nil
+ -- check if the server version was cached
+ if ( not(self.cache) or not(self.cache.version) ) then
+ local status, response = self:getExternalAddress()
+ if ( status ) then
+ return true, (self.cache and self.cache.server or "")
+ end
+ return false, response
+ end
+ return true, (self.cache and self.cache.server or "")
+ end,
+
+ --- closes the connection to the server
+ -- @return status true on success, false on failure
+ -- @return err string containing an error message, if status is false
+ -- @name Helper.close
+ close = function(self)
+ return self.comm:close()
+ end,
+
+}
+
+return _ENV;