summaryrefslogtreecommitdiffstats
path: root/nselib/stun.lua
blob: 438cdbcc1d8bf836e369e2f2151d8288fc486e22 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
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;