blob: 4742a7cb5cf6985dd6a2c38031270be187a8a901 [file] [log] [blame]
Oliver Smith3a9f2672019-11-20 10:56:35 +01001/* mslookup specific functions for encoding and decoding mslookup queries/results into mDNS packets, using the high
2 * level functions from mdns_msg.c and mdns_record.c to build the request/answer messages. */
3
4/* Copyright 2019 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
5 *
6 * All Rights Reserved
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License along
19 * with this program. If not, see <http://www.gnu.org/licenses/>.
20 */
21
22#include <osmocom/hlr/logging.h>
23#include <osmocom/core/msgb.h>
24#include <osmocom/mslookup/mslookup.h>
25#include <osmocom/mslookup/mdns_msg.h>
26#include <osmocom/mslookup/mdns_rfc.h>
27#include <errno.h>
28#include <inttypes.h>
29
30static struct msgb *osmo_mdns_msgb_alloc(const char *label)
31{
32 return msgb_alloc(1024, label);
33}
34
35/*! Combine the mslookup query service, ID and ID type into a domain string.
36 * \param[in] domain_suffix is appended to each domain in the queries to avoid colliding with the top-level domains
37 * administrated by IANA. Example: "mdns.osmocom.org"
38 * \returns allocated buffer with the resulting domain (i.e. "sip.voice.123.msisdn.mdns.osmocom.org") on success,
39 * NULL on failure.
40 */
41static char *domain_from_query(void *ctx, const struct osmo_mslookup_query *query, const char *domain_suffix)
42{
43 const char *id;
44
45 /* Get id from query */
46 switch (query->id.type) {
47 case OSMO_MSLOOKUP_ID_IMSI:
48 id = query->id.imsi;
49 break;
50 case OSMO_MSLOOKUP_ID_MSISDN:
51 id = query->id.msisdn;
52 break;
53 default:
54 LOGP(DMSLOOKUP, LOGL_ERROR, "can't encode mslookup query id type %i", query->id.type);
55 return NULL;
56 }
57
58 return talloc_asprintf(ctx, "%s.%s.%s.%s", query->service, id, osmo_mslookup_id_type_name(query->id.type),
59 domain_suffix);
60}
61
62/*! Split up query service, ID and ID type from a domain string into a mslookup query.
63 * \param[in] domain with domain_suffix, e.g. "sip.voice.123.msisdn.mdns.osmocom.org"
64 * \param[in] domain_suffix is appended to each domain in the queries to avoid colliding with the top-level domains
65 * administrated by IANA. It is not part of the resulting struct osmo_mslookup_query, so we
66 * remove it in this function. Example: "mdns.osmocom.org"
67 */
68int query_from_domain(struct osmo_mslookup_query *query, const char *domain, const char *domain_suffix)
69{
70 int domain_len = strlen(domain) - strlen(domain_suffix) - 1;
71 char domain_buf[OSMO_MDNS_RFC_MAX_NAME_LEN];
72
73 if (domain_len <= 0 || domain_len >= sizeof(domain_buf))
74 return -EINVAL;
75
76 if (domain[domain_len] != '.' || strcmp(domain + domain_len + 1, domain_suffix) != 0)
77 return -EINVAL;
78
79 memcpy(domain_buf, domain, domain_len);
80 domain_buf[domain_len] = '\0';
81 return osmo_mslookup_query_init_from_domain_str(query, domain_buf);
82}
83
84/*! Encode a mslookup query into a mDNS packet.
85 * \param[in] domain_suffix is appended to each domain in the queries to avoid colliding with the top-level domains
86 * administrated by IANA. Example: "mdns.osmocom.org"
87 * \returns msgb, or NULL on error.
88 */
89struct msgb *osmo_mdns_query_encode(void *ctx, uint16_t packet_id, const struct osmo_mslookup_query *query,
90 const char *domain_suffix)
91{
92 struct osmo_mdns_msg_request req = {0};
93 struct msgb *msg = osmo_mdns_msgb_alloc(__func__);
94
95 req.id = packet_id;
96 req.type = OSMO_MDNS_RFC_RECORD_TYPE_ALL;
97 req.domain = domain_from_query(ctx, query, domain_suffix);
98 if (!req.domain)
99 goto error;
100 if (osmo_mdns_msg_request_encode(ctx, msg, &req))
101 goto error;
102 talloc_free(req.domain);
103 return msg;
104error:
105 msgb_free(msg);
106 talloc_free(req.domain);
107 return NULL;
108}
109
110/*! Decode a mDNS request packet into a mslookup query.
111 * \param[out] packet_id the result must be sent with the same packet_id.
112 * \param[in] domain_suffix is appended to each domain in the queries to avoid colliding with the top-level domains
113 * administrated by IANA. Example: "mdns.osmocom.org"
114 * \returns allocated mslookup query on success, NULL on error.
115 */
116struct osmo_mslookup_query *osmo_mdns_query_decode(void *ctx, const uint8_t *data, size_t data_len,
117 uint16_t *packet_id, const char *domain_suffix)
118{
119 struct osmo_mdns_msg_request *req = NULL;
120 struct osmo_mslookup_query *query = NULL;
121
122 req = osmo_mdns_msg_request_decode(ctx, data, data_len);
123 if (!req)
124 return NULL;
125
126 query = talloc_zero(ctx, struct osmo_mslookup_query);
127 OSMO_ASSERT(query);
128 if (query_from_domain(query, req->domain, domain_suffix) < 0)
129 goto error_free;
130
131 *packet_id = req->id;
132 talloc_free(req);
133 return query;
134error_free:
135 talloc_free(req);
136 talloc_free(query);
137 return NULL;
138}
139
140/*! Parse sockaddr_str from mDNS record, so the mslookup result can be filled with it.
141 * \param[out] sockaddr_str resulting IPv4 or IPv6 sockaddr_str.
142 * \param[in] rec single record of the abstracted list of mDNS records
143 * \returns 0 on success, -EINVAL on error.
144 */
145static int sockaddr_str_from_mdns_record(struct osmo_sockaddr_str *sockaddr_str, struct osmo_mdns_record *rec)
146{
147 switch (rec->type) {
148 case OSMO_MDNS_RFC_RECORD_TYPE_A:
149 if (rec->length != 4) {
150 LOGP(DMSLOOKUP, LOGL_ERROR, "unexpected length of A record\n");
151 return -EINVAL;
152 }
153 osmo_sockaddr_str_from_32(sockaddr_str, *(uint32_t *)rec->data, 0);
154 break;
155 case OSMO_MDNS_RFC_RECORD_TYPE_AAAA:
156 if (rec->length != 16) {
157 LOGP(DMSLOOKUP, LOGL_ERROR, "unexpected length of AAAA record\n");
158 return -EINVAL;
159 }
160 osmo_sockaddr_str_from_in6_addr(sockaddr_str, (struct in6_addr*)rec->data, 0);
161 break;
162 default:
163 LOGP(DMSLOOKUP, LOGL_ERROR, "unexpected record type\n");
164 return -EINVAL;
165 }
166 return 0;
167}
168
169/*! Encode a successful mslookup result, along with the original query and packet_id into one mDNS answer packet.
170 *
171 * The records in the packet are ordered as follows:
172 * 1) "age", ip_v4/v6, "port" (only IPv4 or IPv6 present) or
173 * 2) "age", ip_v4, "port", ip_v6, "port" (both IPv4 and v6 present).
174 * "age" and "port" are TXT records, ip_v4 is an A record, ip_v6 is an AAAA record.
175 *
176 * \param[in] packet_id as received in osmo_mdns_query_decode().
177 * \param[in] query the original query, so we can send the domain back in the answer (i.e. "sip.voice.1234.msisdn").
178 * \param[in] result holds the age, IPs and ports of the queried service.
179 * \param[in] domain_suffix is appended to each domain in the queries to avoid colliding with the top-level domains
180 * administrated by IANA. Example: "mdns.osmocom.org"
181 * \returns msg on success, NULL on error.
182 */
183struct msgb *osmo_mdns_result_encode(void *ctx, uint16_t packet_id, const struct osmo_mslookup_query *query,
184 const struct osmo_mslookup_result *result, const char *domain_suffix)
185{
186 struct osmo_mdns_msg_answer ans = {};
187 struct osmo_mdns_record *rec_age = NULL;
188 struct osmo_mdns_record rec_ip_v4 = {0};
189 struct osmo_mdns_record rec_ip_v6 = {0};
190 struct osmo_mdns_record *rec_ip_v4_port = NULL;
191 struct osmo_mdns_record *rec_ip_v6_port = NULL;
192 struct in_addr rec_ip_v4_in;
193 struct in6_addr rec_ip_v6_in;
194 struct msgb *msg = osmo_mdns_msgb_alloc(__func__);
195 char buf[256];
196
197 ctx = talloc_named(ctx, 0, "osmo_mdns_result_encode");
198
199 /* Prepare answer (ans) */
200 ans.domain = domain_from_query(ctx, query, domain_suffix);
201 if (!ans.domain)
202 goto error;
203 ans.id = packet_id;
204 INIT_LLIST_HEAD(&ans.records);
205
206 /* Record for age */
207 rec_age = osmo_mdns_record_txt_keyval_encode(ctx, "age", "%"PRIu32, result->age);
208 OSMO_ASSERT(rec_age);
209 llist_add_tail(&rec_age->list, &ans.records);
210
211 /* Records for IPv4 */
212 if (osmo_sockaddr_str_is_set(&result->host_v4)) {
213 if (osmo_sockaddr_str_to_in_addr(&result->host_v4, &rec_ip_v4_in) < 0) {
214 LOGP(DMSLOOKUP, LOGL_ERROR, "failed to encode ipv4: %s\n",
215 osmo_mslookup_result_name_b(buf, sizeof(buf), query, result));
216 goto error;
217 }
218 rec_ip_v4.type = OSMO_MDNS_RFC_RECORD_TYPE_A;
219 rec_ip_v4.data = (uint8_t *)&rec_ip_v4_in;
220 rec_ip_v4.length = sizeof(rec_ip_v4_in);
221 llist_add_tail(&rec_ip_v4.list, &ans.records);
222
223 rec_ip_v4_port = osmo_mdns_record_txt_keyval_encode(ctx, "port", "%"PRIu16, result->host_v4.port);
224 OSMO_ASSERT(rec_ip_v4_port);
225 llist_add_tail(&rec_ip_v4_port->list, &ans.records);
226 }
227
228 /* Records for IPv6 */
229 if (osmo_sockaddr_str_is_set(&result->host_v6)) {
230 if (osmo_sockaddr_str_to_in6_addr(&result->host_v6, &rec_ip_v6_in) < 0) {
231 LOGP(DMSLOOKUP, LOGL_ERROR, "failed to encode ipv6: %s\n",
232 osmo_mslookup_result_name_b(buf, sizeof(buf), query, result));
233 goto error;
234 }
235 rec_ip_v6.type = OSMO_MDNS_RFC_RECORD_TYPE_AAAA;
236 rec_ip_v6.data = (uint8_t *)&rec_ip_v6_in;
237 rec_ip_v6.length = sizeof(rec_ip_v6_in);
238 llist_add_tail(&rec_ip_v6.list, &ans.records);
239
240 rec_ip_v6_port = osmo_mdns_record_txt_keyval_encode(ctx, "port", "%"PRIu16, result->host_v6.port);
241 OSMO_ASSERT(rec_ip_v6_port);
242 llist_add_tail(&rec_ip_v6_port->list, &ans.records);
243 }
244
245 if (osmo_mdns_msg_answer_encode(ctx, msg, &ans)) {
246 LOGP(DMSLOOKUP, LOGL_ERROR, "failed to encode mDNS answer: %s\n",
247 osmo_mslookup_result_name_b(buf, sizeof(buf), query, result));
248 goto error;
249 }
250 talloc_free(ctx);
251 return msg;
252error:
253 msgb_free(msg);
254 talloc_free(ctx);
255 return NULL;
256}
257
258static int decode_uint32_t(const char *str, uint32_t *val)
259{
260 long long int lld;
261 char *endptr = NULL;
262 *val = 0;
263 errno = 0;
264 lld = strtoll(str, &endptr, 10);
265 if (errno || !endptr || *endptr)
266 return -EINVAL;
267 if (lld < 0 || lld > UINT32_MAX)
268 return -EINVAL;
269 *val = lld;
270 return 0;
271}
272
273static int decode_port(const char *str, uint16_t *port)
274{
275 uint32_t val;
276 if (decode_uint32_t(str, &val))
277 return -EINVAL;
278 if (val > 65535)
279 return -EINVAL;
280 *port = val;
281 return 0;
282}
283
284/*! Read expected mDNS records into mslookup result.
285 *
286 * The records in the packet must be ordered as follows:
287 * 1) "age", ip_v4/v6, "port" (only IPv4 or IPv6 present) or
288 * 2) "age", ip_v4, "port", ip_v6, "port" (both IPv4 and v6 present).
289 * "age" and "port" are TXT records, ip_v4 is an A record, ip_v6 is an AAAA record.
290 *
291 * \param[out] result holds the age, IPs and ports of the queried service.
292 * \param[in] ans abstracted mDNS answer with a list of resource records.
293 * \returns 0 on success, -EINVAL on error.
294 */
295int osmo_mdns_result_from_answer(struct osmo_mslookup_result *result, const struct osmo_mdns_msg_answer *ans)
296{
297 struct osmo_mdns_record *rec;
298 char txt_key[64];
299 char txt_value[64];
300 bool found_age = false;
301 bool found_ip_v4 = false;
302 bool found_ip_v6 = false;
303 struct osmo_sockaddr_str *expect_port_for = NULL;
304
305 *result = (struct osmo_mslookup_result){};
306
307 result->rc = OSMO_MSLOOKUP_RC_NONE;
308
309 llist_for_each_entry(rec, &ans->records, list) {
310 switch (rec->type) {
311 case OSMO_MDNS_RFC_RECORD_TYPE_A:
312 if (expect_port_for) {
313 LOGP(DMSLOOKUP, LOGL_ERROR,
314 "'A' record found, but still expecting a 'port' value first\n");
315 return -EINVAL;
316 }
317 if (found_ip_v4) {
318 LOGP(DMSLOOKUP, LOGL_ERROR, "'A' record found twice in mDNS answer\n");
319 return -EINVAL;
320 }
321 found_ip_v4 = true;
322 expect_port_for = &result->host_v4;
323 if (sockaddr_str_from_mdns_record(expect_port_for, rec)) {
324 LOGP(DMSLOOKUP, LOGL_ERROR, "'A' record with invalid address data\n");
325 return -EINVAL;
326 }
327 break;
328 case OSMO_MDNS_RFC_RECORD_TYPE_AAAA:
329 if (expect_port_for) {
330 LOGP(DMSLOOKUP, LOGL_ERROR,
331 "'AAAA' record found, but still expecting a 'port' value first\n");
332 return -EINVAL;
333 }
334 if (found_ip_v6) {
335 LOGP(DMSLOOKUP, LOGL_ERROR, "'AAAA' record found twice in mDNS answer\n");
336 return -EINVAL;
337 }
338 found_ip_v6 = true;
339 expect_port_for = &result->host_v6;
340 if (sockaddr_str_from_mdns_record(expect_port_for, rec) != 0) {
341 LOGP(DMSLOOKUP, LOGL_ERROR, "'AAAA' record with invalid address data\n");
342 return -EINVAL;
343 }
344 break;
345 case OSMO_MDNS_RFC_RECORD_TYPE_TXT:
346 if (osmo_mdns_record_txt_keyval_decode(rec, txt_key, sizeof(txt_key),
347 txt_value, sizeof(txt_value)) != 0) {
348 LOGP(DMSLOOKUP, LOGL_ERROR, "failed to decode txt record\n");
349 return -EINVAL;
350 }
351 if (strcmp(txt_key, "age") == 0) {
352 if (found_age) {
353 LOGP(DMSLOOKUP, LOGL_ERROR, "duplicate 'TXT' record for 'age'\n");
354 return -EINVAL;
355 }
356 found_age = true;
357 if (decode_uint32_t(txt_value, &result->age)) {
358 LOGP(DMSLOOKUP, LOGL_ERROR,
359 "'TXT' record: invalid 'age' value ('age=%s')\n", txt_value);
360 return -EINVAL;
361 }
362 } else if (strcmp(txt_key, "port") == 0) {
363 if (!expect_port_for) {
364 LOGP(DMSLOOKUP, LOGL_ERROR,
365 "'TXT' record for 'port' without previous 'A' or 'AAAA' record\n");
366 return -EINVAL;
367 }
368 if (decode_port(txt_value, &expect_port_for->port)) {
369 LOGP(DMSLOOKUP, LOGL_ERROR,
370 "'TXT' record: invalid 'port' value ('port=%s')\n", txt_value);
371 return -EINVAL;
372 }
373 expect_port_for = NULL;
374 } else {
375 LOGP(DMSLOOKUP, LOGL_ERROR, "unexpected key '%s' in TXT record\n", txt_key);
376 return -EINVAL;
377 }
378 break;
379 default:
380 LOGP(DMSLOOKUP, LOGL_ERROR, "unexpected record type\n");
381 return -EINVAL;
382 }
383 }
384
385 /* Check if everything was found */
386 if (!found_age || !(found_ip_v4 || found_ip_v6) || expect_port_for) {
387 LOGP(DMSLOOKUP, LOGL_ERROR, "missing resource records in mDNS answer\n");
388 return -EINVAL;
389 }
390
391 result->rc = OSMO_MSLOOKUP_RC_RESULT;
392 return 0;
393}
394
395/*! Decode a mDNS answer packet into a mslookup result, query and packet_id.
396 * \param[out] packet_id same ID as sent in the request packet.
397 * \param[out] query the original query (service, ID, ID type).
398 * \param[out] result holds the age, IPs and ports of the queried service.
399 * \param[in] domain_suffix is appended to each domain in the queries to avoid colliding with the top-level domains
400 * administrated by IANA. Example: "mdns.osmocom.org"
401 * \returns 0 on success, -EINVAL on error.
402 */
403int osmo_mdns_result_decode(void *ctx, const uint8_t *data, size_t data_len, uint16_t *packet_id,
404 struct osmo_mslookup_query *query, struct osmo_mslookup_result *result,
405 const char *domain_suffix)
406{
407 int rc = -EINVAL;
408 struct osmo_mdns_msg_answer *ans;
409 ans = osmo_mdns_msg_answer_decode(ctx, data, data_len);
410 if (!ans)
411 goto exit_free;
412
413 if (query_from_domain(query, ans->domain, domain_suffix) < 0)
414 goto exit_free;
415
416 if (osmo_mdns_result_from_answer(result, ans) < 0)
417 goto exit_free;
418
419 *packet_id = ans->id;
420 rc = 0;
421
422exit_free:
423 talloc_free(ans);
424 return rc;
425}