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
child 12359: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 };