Mercurial > prosody-hg
comparison net/stun.lua @ 12356:0f77e28df5c8
net.stun: New library that implements STUN/TURN parsing/serialization
| author | Matthew Wild <mwild1@gmail.com> |
|---|---|
| date | Fri, 04 Mar 2022 15:23:32 +0000 |
| parents | |
| children | f81488951f81 |
comparison
equal
deleted
inserted
replaced
| 12355:a0ff5c438e9d | 12356:0f77e28df5c8 |
|---|---|
| 1 local base64 = require "util.encodings".base64; | |
| 2 local hashes = require "util.hashes"; | |
| 3 local net = require "util.net"; | |
| 4 local random = require "util.random"; | |
| 5 local struct = require "util.struct"; | |
| 6 | |
| 7 --- Private helpers | |
| 8 | |
| 9 -- XORs a string with another string | |
| 10 local function sxor(x, y) | |
| 11 local r = {}; | |
| 12 for i = 1, #x do | |
| 13 r[i] = string.char(bit32.bxor(x:byte(i), y:byte(i))); | |
| 14 end | |
| 15 return table.concat(r); | |
| 16 end | |
| 17 | |
| 18 --- Public helpers | |
| 19 | |
| 20 -- Following draft-uberti-behave-turn-rest-00, convert a 'secret' string | |
| 21 -- into a username/password pair that can be used to auth to a TURN server | |
| 22 local function get_user_pass_from_secret(secret, ttl, opt_username) | |
| 23 ttl = ttl or 86400; | |
| 24 local username; | |
| 25 if opt_username then | |
| 26 username = ("%d:%s"):format(os.time() + ttl, opt_username); | |
| 27 else | |
| 28 username = ("%d"):format(os.time() + ttl); | |
| 29 end | |
| 30 local password = base64.encode(hashes.hmac_sha1(secret, username)); | |
| 31 return username, password, ttl; | |
| 32 end | |
| 33 | |
| 34 -- Following RFC 8489 9.2, convert credentials to a HMAC key for signing | |
| 35 local function get_long_term_auth_key(realm, username, password) | |
| 36 return hashes.md5(username..":"..realm..":"..password); | |
| 37 end | |
| 38 | |
| 39 --- Packet building/parsing | |
| 40 | |
| 41 local packet_methods = {}; | |
| 42 local packet_mt = { __index = packet_methods }; | |
| 43 | |
| 44 local magic_cookie = string.char(0x21, 0x12, 0xA4, 0x42); | |
| 45 | |
| 46 local methods = { | |
| 47 binding = 0x001; | |
| 48 -- TURN | |
| 49 allocate = 0x003; | |
| 50 refresh = 0x004; | |
| 51 send = 0x006; | |
| 52 data = 0x007; | |
| 53 create_permission = 0x008; | |
| 54 channel_bind = 0x009; | |
| 55 }; | |
| 56 local method_lookup = {}; | |
| 57 for name, value in pairs(methods) do | |
| 58 method_lookup[name] = value; | |
| 59 method_lookup[value] = name; | |
| 60 end | |
| 61 | |
| 62 local classes = { | |
| 63 request = 0; | |
| 64 indication = 1; | |
| 65 success = 2; | |
| 66 error = 3; | |
| 67 }; | |
| 68 local class_lookup = {}; | |
| 69 for name, value in pairs(classes) do | |
| 70 class_lookup[name] = value; | |
| 71 class_lookup[value] = name; | |
| 72 end | |
| 73 | |
| 74 local attributes = { | |
| 75 ["mapped-address"] = 0x0001; | |
| 76 ["username"] = 0x0006; | |
| 77 ["message-integrity"] = 0x0008; | |
| 78 ["error-code"] = 0x0009; | |
| 79 ["unknown-attributes"] = 0x000A; | |
| 80 ["realm"] = 0x0014; | |
| 81 ["nonce"] = 0x0015; | |
| 82 ["xor-mapped-address"] = 0x0020; | |
| 83 ["software"] = 0x8022; | |
| 84 ["alternate-server"] = 0x8023; | |
| 85 ["fingerprint"] = 0x8028; | |
| 86 ["message-integrity-sha256"] = 0x001C; | |
| 87 ["password-algorithm"] = 0x001D; | |
| 88 ["userhash"] = 0x001E; | |
| 89 ["password-algorithms"] = 0x8002; | |
| 90 ["alternate-domains"] = 0x8003; | |
| 91 | |
| 92 -- TURN | |
| 93 ["requested-transport"] = 0x0019; | |
| 94 }; | |
| 95 local attribute_lookup = {}; | |
| 96 for name, value in pairs(attributes) do | |
| 97 attribute_lookup[name] = value; | |
| 98 attribute_lookup[value] = name; | |
| 99 end | |
| 100 | |
| 101 function packet_methods:serialize_header(length) | |
| 102 assert(#self.transaction_id == 12, "invalid transaction id length"); | |
| 103 local header = struct.pack(">I2I2", | |
| 104 self.type, | |
| 105 length | |
| 106 )..magic_cookie..self.transaction_id; | |
| 107 return header; | |
| 108 end | |
| 109 | |
| 110 function packet_methods:serialize() | |
| 111 local payload = table.concat(self.attributes); | |
| 112 return self:serialize_header(#payload)..payload; | |
| 113 end | |
| 114 | |
| 115 function packet_methods:is_request() | |
| 116 return bit32.band(self.type, 0x0110) == 0x0000; | |
| 117 end | |
| 118 | |
| 119 function packet_methods:is_indication() | |
| 120 return bit32.band(self.type, 0x0110) == 0x0010; | |
| 121 end | |
| 122 | |
| 123 function packet_methods:is_success_resp() | |
| 124 return bit32.band(self.type, 0x0110) == 0x0100; | |
| 125 end | |
| 126 | |
| 127 function packet_methods:is_err_resp() | |
| 128 return bit32.band(self.type, 0x0110) == 0x0110; | |
| 129 end | |
| 130 | |
| 131 function packet_methods:get_method() | |
| 132 local method = bit32.bor( | |
| 133 bit32.rshift(bit32.band(self.type, 0x3E00), 2), | |
| 134 bit32.rshift(bit32.band(self.type, 0x00E0), 1), | |
| 135 bit32.band(self.type, 0x000F) | |
| 136 ); | |
| 137 return method, method_lookup[method]; | |
| 138 end | |
| 139 | |
| 140 function packet_methods:get_class() | |
| 141 local class = bit32.bor( | |
| 142 bit32.rshift(bit32.band(self.type, 0x0100), 7), | |
| 143 bit32.rshift(bit32.band(self.type, 0x0010), 4) | |
| 144 ); | |
| 145 return class, class_lookup[class]; | |
| 146 end | |
| 147 | |
| 148 function packet_methods:set_type(method, class) | |
| 149 if type(method) == "string" then | |
| 150 method = assert(method_lookup[method:lower()], "unknown method: "..method); | |
| 151 end | |
| 152 if type(class) == "string" then | |
| 153 class = assert(classes[class], "unknown class: "..class); | |
| 154 end | |
| 155 self.type = bit32.bor( | |
| 156 bit32.lshift(bit32.band(method, 0x1F80), 2), | |
| 157 bit32.lshift(bit32.band(method, 0x0070), 1), | |
| 158 bit32.band(method, 0x000F), | |
| 159 bit32.lshift(bit32.band(class, 0x0002), 7), | |
| 160 bit32.lshift(bit32.band(class, 0x0001), 4) | |
| 161 ); | |
| 162 end | |
| 163 | |
| 164 local function _serialize_attribute(attr_type, value) | |
| 165 local len = #value; | |
| 166 local padding = string.rep("\0", (4 - len)%4); | |
| 167 return struct.pack(">I2I2", | |
| 168 attr_type, len | |
| 169 )..value..padding; | |
| 170 end | |
| 171 | |
| 172 function packet_methods:add_attribute(attr_type, value) | |
| 173 if type(attr_type) == "string" then | |
| 174 attr_type = assert(attributes[attr_type], "unknown attribute: "..attr_type); | |
| 175 end | |
| 176 table.insert(self.attributes, _serialize_attribute(attr_type, value)); | |
| 177 end | |
| 178 | |
| 179 function packet_methods:deserialize(bytes) | |
| 180 local type, len, cookie = struct.unpack(">I2I2I4", bytes); | |
| 181 assert(#bytes == (len + 20), "incorrect packet length"); | |
| 182 assert(cookie == 0x2112A442, "invalid magic cookie"); | |
| 183 self.type = type; | |
| 184 self.transaction_id = bytes:sub(9, 20); | |
| 185 self.attributes = {}; | |
| 186 local pos = 21; | |
| 187 while pos < #bytes do | |
| 188 local attr_hdr = bytes:sub(pos, pos+3); | |
| 189 assert(#attr_hdr == 4, "packet truncated in attribute header"); | |
| 190 local attr_type, attr_len = struct.unpack(">I2I2", attr_hdr); --luacheck: ignore 211/attr_type | |
| 191 if attr_len == 0 then | |
| 192 table.insert(self.attributes, attr_hdr); | |
| 193 pos = pos + 20; | |
| 194 else | |
| 195 local data = bytes:sub(pos + 4, pos + 3 + attr_len); | |
| 196 assert(#data == attr_len, "packet truncated in attribute value"); | |
| 197 table.insert(self.attributes, attr_hdr..data); | |
| 198 local n_padding = (4 - attr_len)%4; | |
| 199 pos = pos + 4 + attr_len + n_padding; | |
| 200 end | |
| 201 end | |
| 202 return self; | |
| 203 end | |
| 204 | |
| 205 function packet_methods:get_attribute(attr_type) | |
| 206 if type(attr_type) == "string" then | |
| 207 attr_type = assert(attribute_lookup[attr_type:lower()], "unknown attribute: "..attr_type); | |
| 208 end | |
| 209 for _, attribute in ipairs(self.attributes) do | |
| 210 if struct.unpack(">I2", attribute) == attr_type then | |
| 211 return attribute:sub(5); | |
| 212 end | |
| 213 end | |
| 214 end | |
| 215 | |
| 216 local addr_families = { "IPv4", "IPv6" }; | |
| 217 function packet_methods:get_mapped_address() | |
| 218 local data = self:get_attribute("mapped-address"); | |
| 219 if not data then return; end | |
| 220 local family, port = struct.unpack("x>BI2", data); | |
| 221 local addr = data:sub(5); | |
| 222 return { | |
| 223 family = addr_families[family] or "unknown"; | |
| 224 port = port; | |
| 225 address = net.ntop(addr); | |
| 226 }; | |
| 227 end | |
| 228 | |
| 229 function packet_methods:get_xor_mapped_address() | |
| 230 local data = self:get_attribute("xor-mapped-address"); | |
| 231 if not data then return; end | |
| 232 local family, port = struct.unpack("x>BI2", data); | |
| 233 local addr = sxor(data:sub(5), magic_cookie..self.transaction_id); | |
| 234 return { | |
| 235 family = addr_families[family] or "unknown"; | |
| 236 port = bit32.bxor(port, 0x2112); | |
| 237 address = net.ntop(addr); | |
| 238 address_raw = data:sub(5); | |
| 239 }; | |
| 240 end | |
| 241 | |
| 242 function packet_methods:add_message_integrity(key) | |
| 243 -- Add attribute with a dummy value so we can artificially increase | |
| 244 -- the packet 'length' | |
| 245 self:add_attribute("message-integrity", string.rep("\0", 20)); | |
| 246 -- Get the packet data, minus the message-integrity attribute itself | |
| 247 local pkt = self:serialize():sub(1, -25); | |
| 248 local hash = hashes.hmac_sha1(key, pkt, false); | |
| 249 self.attributes[#self.attributes] = nil; | |
| 250 assert(#hash == 20, "invalid hash length"); | |
| 251 self:add_attribute("message-integrity", hash); | |
| 252 end | |
| 253 | |
| 254 do | |
| 255 local transports = { | |
| 256 udp = 0x11; | |
| 257 }; | |
| 258 function packet_methods:add_requested_transport(transport) | |
| 259 local transport_code = transports[transport]; | |
| 260 assert(transport_code, "unsupported transport: "..tostring(transport)); | |
| 261 self:add_attribute("requested-transport", string.char( | |
| 262 transport_code, 0x00, 0x00, 0x00 | |
| 263 )); | |
| 264 end | |
| 265 end | |
| 266 | |
| 267 function packet_methods:get_error() | |
| 268 local err_attr = self:get_attribute("error-code"); | |
| 269 if not err_attr then | |
| 270 return nil; | |
| 271 end | |
| 272 local number = err_attr:byte(4); | |
| 273 local class = bit32.band(0x07, err_attr:byte(3)); | |
| 274 local msg = err_attr:sub(5); | |
| 275 return (class*100)+number, msg; | |
| 276 end | |
| 277 | |
| 278 local function new_packet(method, class) | |
| 279 local p = setmetatable({ | |
| 280 transaction_id = random.bytes(12); | |
| 281 length = 0; | |
| 282 attributes = {}; | |
| 283 }, packet_mt); | |
| 284 p:set_type(method or "binding", class or "request"); | |
| 285 return p; | |
| 286 end | |
| 287 | |
| 288 return { | |
| 289 new_packet = new_packet; | |
| 290 get_user_pass_from_secret = get_user_pass_from_secret; | |
| 291 get_long_term_auth_key = get_long_term_auth_key; | |
| 292 }; |
