aboutsummaryrefslogtreecommitdiffstats
path: root/src/mslookup/mdns.c
blob: 4742a7cb5cf6985dd6a2c38031270be187a8a901 (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
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
/* mslookup specific functions for encoding and decoding mslookup queries/results into mDNS packets, using the high
 * level functions from mdns_msg.c and mdns_record.c to build the request/answer messages. */

/* Copyright 2019 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
 *
 * All Rights Reserved
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <osmocom/hlr/logging.h>
#include <osmocom/core/msgb.h>
#include <osmocom/mslookup/mslookup.h>
#include <osmocom/mslookup/mdns_msg.h>
#include <osmocom/mslookup/mdns_rfc.h>
#include <errno.h>
#include <inttypes.h>

static struct msgb *osmo_mdns_msgb_alloc(const char *label)
{
	return msgb_alloc(1024, label);
}

/*! Combine the mslookup query service, ID and ID type into a domain string.
 * \param[in] domain_suffix  is appended to each domain in the queries to avoid colliding with the top-level domains
 *                           administrated by IANA. Example: "mdns.osmocom.org"
 * \returns allocated buffer with the resulting domain (i.e. "sip.voice.123.msisdn.mdns.osmocom.org") on success,
 * 	    NULL on failure.
 */
static char *domain_from_query(void *ctx, const struct osmo_mslookup_query *query, const char *domain_suffix)
{
	const char *id;

	/* Get id from query */
	switch (query->id.type) {
		case OSMO_MSLOOKUP_ID_IMSI:
			id = query->id.imsi;
			break;
		case OSMO_MSLOOKUP_ID_MSISDN:
			id = query->id.msisdn;
			break;
		default:
			LOGP(DMSLOOKUP, LOGL_ERROR, "can't encode mslookup query id type %i", query->id.type);
			return NULL;
	}

	return talloc_asprintf(ctx, "%s.%s.%s.%s", query->service, id, osmo_mslookup_id_type_name(query->id.type),
			       domain_suffix);
}

/*! Split up query service, ID and ID type from a domain string into a mslookup query.
 * \param[in] domain  with domain_suffix, e.g. "sip.voice.123.msisdn.mdns.osmocom.org"
 * \param[in] domain_suffix  is appended to each domain in the queries to avoid colliding with the top-level domains
 *                           administrated by IANA. It is not part of the resulting struct osmo_mslookup_query, so we
 *                           remove it in this function. Example: "mdns.osmocom.org"
 */
int query_from_domain(struct osmo_mslookup_query *query, const char *domain, const char *domain_suffix)
{
	int domain_len = strlen(domain) - strlen(domain_suffix) - 1;
	char domain_buf[OSMO_MDNS_RFC_MAX_NAME_LEN];

	if (domain_len <= 0 || domain_len >= sizeof(domain_buf))
		return -EINVAL;

	if (domain[domain_len] != '.' || strcmp(domain + domain_len + 1, domain_suffix) != 0)
		return -EINVAL;

	memcpy(domain_buf, domain, domain_len);
	domain_buf[domain_len] = '\0';
	return osmo_mslookup_query_init_from_domain_str(query, domain_buf);
}

/*! Encode a mslookup query into a mDNS packet.
 * \param[in] domain_suffix  is appended to each domain in the queries to avoid colliding with the top-level domains
 *                           administrated by IANA. Example: "mdns.osmocom.org"
 * \returns msgb, or NULL on error.
 */
struct msgb *osmo_mdns_query_encode(void *ctx, uint16_t packet_id, const struct osmo_mslookup_query *query,
				    const char *domain_suffix)
{
	struct osmo_mdns_msg_request req = {0};
	struct msgb *msg = osmo_mdns_msgb_alloc(__func__);

	req.id = packet_id;
	req.type = OSMO_MDNS_RFC_RECORD_TYPE_ALL;
	req.domain = domain_from_query(ctx, query, domain_suffix);
	if (!req.domain)
		goto error;
	if (osmo_mdns_msg_request_encode(ctx, msg, &req))
		goto error;
	talloc_free(req.domain);
	return msg;
error:
	msgb_free(msg);
	talloc_free(req.domain);
	return NULL;
}

/*! Decode a mDNS request packet into a mslookup query.
 * \param[out] packet_id  the result must be sent with the same packet_id.
 * \param[in] domain_suffix  is appended to each domain in the queries to avoid colliding with the top-level domains
 *                           administrated by IANA. Example: "mdns.osmocom.org"
 * \returns allocated mslookup query on success, NULL on error.
 */
struct osmo_mslookup_query *osmo_mdns_query_decode(void *ctx, const uint8_t *data, size_t data_len,
						   uint16_t *packet_id, const char *domain_suffix)
{
	struct osmo_mdns_msg_request *req = NULL;
	struct osmo_mslookup_query *query = NULL;

	req = osmo_mdns_msg_request_decode(ctx, data, data_len);
	if (!req)
		return NULL;

	query = talloc_zero(ctx, struct osmo_mslookup_query);
	OSMO_ASSERT(query);
	if (query_from_domain(query, req->domain, domain_suffix) < 0)
		goto error_free;

	*packet_id = req->id;
	talloc_free(req);
	return query;
error_free:
	talloc_free(req);
	talloc_free(query);
	return NULL;
}

/*! Parse sockaddr_str from mDNS record, so the mslookup result can be filled with it.
 * \param[out] sockaddr_str resulting IPv4 or IPv6 sockaddr_str.
 * \param[in] rec  single record of the abstracted list of mDNS records
 * \returns 0 on success, -EINVAL on error.
 */
static int sockaddr_str_from_mdns_record(struct osmo_sockaddr_str *sockaddr_str, struct osmo_mdns_record *rec)
{
	switch (rec->type) {
	case OSMO_MDNS_RFC_RECORD_TYPE_A:
		if (rec->length != 4) {
			LOGP(DMSLOOKUP, LOGL_ERROR, "unexpected length of A record\n");
			return -EINVAL;
		}
		osmo_sockaddr_str_from_32(sockaddr_str, *(uint32_t *)rec->data, 0);
		break;
	case OSMO_MDNS_RFC_RECORD_TYPE_AAAA:
		if (rec->length != 16) {
			LOGP(DMSLOOKUP, LOGL_ERROR, "unexpected length of AAAA record\n");
			return -EINVAL;
		}
		osmo_sockaddr_str_from_in6_addr(sockaddr_str, (struct in6_addr*)rec->data, 0);
		break;
	default:
		LOGP(DMSLOOKUP, LOGL_ERROR, "unexpected record type\n");
		return -EINVAL;
	}
	return 0;
}

/*! Encode a successful mslookup result, along with the original query and packet_id into one mDNS answer packet.
 *
 * The records in the packet are ordered as follows:
 * 1) "age", ip_v4/v6, "port" (only IPv4 or IPv6 present) or
 * 2) "age", ip_v4, "port", ip_v6, "port" (both IPv4 and v6 present).
 * "age" and "port" are TXT records, ip_v4 is an A record, ip_v6 is an AAAA record.
 *
 * \param[in] packet_id  as received in osmo_mdns_query_decode().
 * \param[in] query  the original query, so we can send the domain back in the answer (i.e. "sip.voice.1234.msisdn").
 * \param[in] result  holds the age, IPs and ports of the queried service.
 * \param[in] domain_suffix  is appended to each domain in the queries to avoid colliding with the top-level domains
 *                           administrated by IANA. Example: "mdns.osmocom.org"
 * \returns msg on success, NULL on error.
 */
struct msgb *osmo_mdns_result_encode(void *ctx, uint16_t packet_id, const struct osmo_mslookup_query *query,
				     const struct osmo_mslookup_result *result, const char *domain_suffix)
{
	struct osmo_mdns_msg_answer ans = {};
	struct osmo_mdns_record *rec_age = NULL;
	struct osmo_mdns_record rec_ip_v4 = {0};
	struct osmo_mdns_record rec_ip_v6 = {0};
	struct osmo_mdns_record *rec_ip_v4_port = NULL;
	struct osmo_mdns_record *rec_ip_v6_port = NULL;
	struct in_addr rec_ip_v4_in;
	struct in6_addr rec_ip_v6_in;
	struct msgb *msg = osmo_mdns_msgb_alloc(__func__);
	char buf[256];

	ctx = talloc_named(ctx, 0, "osmo_mdns_result_encode");

	/* Prepare answer (ans) */
	ans.domain = domain_from_query(ctx, query, domain_suffix);
	if (!ans.domain)
		goto error;
	ans.id = packet_id;
	INIT_LLIST_HEAD(&ans.records);

	/* Record for age */
	rec_age = osmo_mdns_record_txt_keyval_encode(ctx, "age", "%"PRIu32, result->age);
	OSMO_ASSERT(rec_age);
	llist_add_tail(&rec_age->list, &ans.records);

	/* Records for IPv4 */
	if (osmo_sockaddr_str_is_set(&result->host_v4)) {
		if (osmo_sockaddr_str_to_in_addr(&result->host_v4, &rec_ip_v4_in) < 0) {
			LOGP(DMSLOOKUP, LOGL_ERROR, "failed to encode ipv4: %s\n",
			     osmo_mslookup_result_name_b(buf, sizeof(buf), query, result));
			goto error;
		}
		rec_ip_v4.type = OSMO_MDNS_RFC_RECORD_TYPE_A;
		rec_ip_v4.data = (uint8_t *)&rec_ip_v4_in;
		rec_ip_v4.length = sizeof(rec_ip_v4_in);
		llist_add_tail(&rec_ip_v4.list, &ans.records);

		rec_ip_v4_port = osmo_mdns_record_txt_keyval_encode(ctx, "port", "%"PRIu16, result->host_v4.port);
		OSMO_ASSERT(rec_ip_v4_port);
		llist_add_tail(&rec_ip_v4_port->list, &ans.records);
	}

	/* Records for IPv6 */
	if (osmo_sockaddr_str_is_set(&result->host_v6)) {
		if (osmo_sockaddr_str_to_in6_addr(&result->host_v6, &rec_ip_v6_in) < 0) {
			LOGP(DMSLOOKUP, LOGL_ERROR, "failed to encode ipv6: %s\n",
			     osmo_mslookup_result_name_b(buf, sizeof(buf), query, result));
			goto error;
		}
		rec_ip_v6.type = OSMO_MDNS_RFC_RECORD_TYPE_AAAA;
		rec_ip_v6.data = (uint8_t *)&rec_ip_v6_in;
		rec_ip_v6.length = sizeof(rec_ip_v6_in);
		llist_add_tail(&rec_ip_v6.list, &ans.records);

		rec_ip_v6_port = osmo_mdns_record_txt_keyval_encode(ctx, "port", "%"PRIu16, result->host_v6.port);
		OSMO_ASSERT(rec_ip_v6_port);
		llist_add_tail(&rec_ip_v6_port->list, &ans.records);
	}

	if (osmo_mdns_msg_answer_encode(ctx, msg, &ans)) {
		LOGP(DMSLOOKUP, LOGL_ERROR, "failed to encode mDNS answer: %s\n",
		     osmo_mslookup_result_name_b(buf, sizeof(buf), query, result));
		goto error;
	}
	talloc_free(ctx);
	return msg;
error:
	msgb_free(msg);
	talloc_free(ctx);
	return NULL;
}

static int decode_uint32_t(const char *str, uint32_t *val)
{
	long long int lld;
	char *endptr = NULL;
	*val = 0;
	errno = 0;
	lld = strtoll(str, &endptr, 10);
	if (errno || !endptr || *endptr)
		return -EINVAL;
	if (lld < 0 || lld > UINT32_MAX)
		return -EINVAL;
	*val = lld;
	return 0;
}

static int decode_port(const char *str, uint16_t *port)
{
	uint32_t val;
	if (decode_uint32_t(str, &val))
		return -EINVAL;
	if (val > 65535)
		return -EINVAL;
	*port = val;
	return 0;
}

/*! Read expected mDNS records into mslookup result.
 *
 * The records in the packet must be ordered as follows:
 * 1) "age", ip_v4/v6, "port" (only IPv4 or IPv6 present) or
 * 2) "age", ip_v4, "port", ip_v6, "port" (both IPv4 and v6 present).
 * "age" and "port" are TXT records, ip_v4 is an A record, ip_v6 is an AAAA record.
 *
 * \param[out] result  holds the age, IPs and ports of the queried service.
 * \param[in] ans  abstracted mDNS answer with a list of resource records.
 * \returns 0 on success, -EINVAL on error.
 */
int osmo_mdns_result_from_answer(struct osmo_mslookup_result *result, const struct osmo_mdns_msg_answer *ans)
{
	struct osmo_mdns_record *rec;
	char txt_key[64];
	char txt_value[64];
	bool found_age = false;
	bool found_ip_v4 = false;
	bool found_ip_v6 = false;
	struct osmo_sockaddr_str *expect_port_for = NULL;

	*result = (struct osmo_mslookup_result){};

	result->rc = OSMO_MSLOOKUP_RC_NONE;

	llist_for_each_entry(rec, &ans->records, list) {
		switch (rec->type) {
			case OSMO_MDNS_RFC_RECORD_TYPE_A:
				if (expect_port_for) {
					LOGP(DMSLOOKUP, LOGL_ERROR,
					     "'A' record found, but still expecting a 'port' value first\n");
					return -EINVAL;
				}
				if (found_ip_v4) {
					LOGP(DMSLOOKUP, LOGL_ERROR, "'A' record found twice in mDNS answer\n");
					return -EINVAL;
				}
				found_ip_v4 = true;
				expect_port_for = &result->host_v4;
				if (sockaddr_str_from_mdns_record(expect_port_for, rec)) {
					LOGP(DMSLOOKUP, LOGL_ERROR, "'A' record with invalid address data\n");
					return -EINVAL;
				}
				break;
			case OSMO_MDNS_RFC_RECORD_TYPE_AAAA:
				if (expect_port_for) {
					LOGP(DMSLOOKUP, LOGL_ERROR,
					     "'AAAA' record found, but still expecting a 'port' value first\n");
					return -EINVAL;
				}
				if (found_ip_v6) {
					LOGP(DMSLOOKUP, LOGL_ERROR, "'AAAA' record found twice in mDNS answer\n");
					return -EINVAL;
				}
				found_ip_v6 = true;
				expect_port_for = &result->host_v6;
				if (sockaddr_str_from_mdns_record(expect_port_for, rec) != 0) {
					LOGP(DMSLOOKUP, LOGL_ERROR, "'AAAA' record with invalid address data\n");
					return -EINVAL;
				}
				break;
			case OSMO_MDNS_RFC_RECORD_TYPE_TXT:
				if (osmo_mdns_record_txt_keyval_decode(rec, txt_key, sizeof(txt_key),
								       txt_value, sizeof(txt_value)) != 0) {
					LOGP(DMSLOOKUP, LOGL_ERROR, "failed to decode txt record\n");
					return -EINVAL;
				}
				if (strcmp(txt_key, "age") == 0) {
					if (found_age) {
						LOGP(DMSLOOKUP, LOGL_ERROR, "duplicate 'TXT' record for 'age'\n");
						return -EINVAL;
					}
					found_age = true;
					if (decode_uint32_t(txt_value, &result->age)) {
						LOGP(DMSLOOKUP, LOGL_ERROR,
						     "'TXT' record: invalid 'age' value ('age=%s')\n", txt_value);
						return -EINVAL;
					}
				} else if (strcmp(txt_key, "port") == 0) {
					if (!expect_port_for) {
						LOGP(DMSLOOKUP, LOGL_ERROR,
						     "'TXT' record for 'port' without previous 'A' or 'AAAA' record\n");
						return -EINVAL;
					}
					if (decode_port(txt_value, &expect_port_for->port)) {
						LOGP(DMSLOOKUP, LOGL_ERROR,
						     "'TXT' record: invalid 'port' value ('port=%s')\n", txt_value);
						return -EINVAL;
					}
					expect_port_for = NULL;
				} else {
					LOGP(DMSLOOKUP, LOGL_ERROR, "unexpected key '%s' in TXT record\n", txt_key);
					return -EINVAL;
				}
				break;
			default:
				LOGP(DMSLOOKUP, LOGL_ERROR, "unexpected record type\n");
				return -EINVAL;
		}
	}

	/* Check if everything was found */
	if (!found_age || !(found_ip_v4 || found_ip_v6) || expect_port_for) {
		LOGP(DMSLOOKUP, LOGL_ERROR, "missing resource records in mDNS answer\n");
		return -EINVAL;
	}

	result->rc = OSMO_MSLOOKUP_RC_RESULT;
	return 0;
}

/*! Decode a mDNS answer packet into a mslookup result, query and packet_id.
 * \param[out] packet_id  same ID as sent in the request packet.
 * \param[out] query  the original query (service, ID, ID type).
 * \param[out] result  holds the age, IPs and ports of the queried service.
 * \param[in] domain_suffix  is appended to each domain in the queries to avoid colliding with the top-level domains
 *                           administrated by IANA. Example: "mdns.osmocom.org"
 * \returns 0 on success, -EINVAL on error.
 */
int osmo_mdns_result_decode(void *ctx, const uint8_t *data, size_t data_len, uint16_t *packet_id,
			    struct osmo_mslookup_query *query, struct osmo_mslookup_result *result,
			    const char *domain_suffix)
{
	int rc = -EINVAL;
	struct osmo_mdns_msg_answer *ans;
	ans = osmo_mdns_msg_answer_decode(ctx, data, data_len);
	if (!ans)
		goto exit_free;

	if (query_from_domain(query, ans->domain, domain_suffix) < 0)
		goto exit_free;

	if (osmo_mdns_result_from_answer(result, ans) < 0)
		goto exit_free;

	*packet_id = ans->id;
	rc = 0;

exit_free:
	talloc_free(ans);
	return rc;
}