Oliver Smith | 06455ea | 2019-11-20 10:56:35 +0100 | [diff] [blame] | 1 | /* 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 | |
| 30 | static 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 | */ |
| 41 | static 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 | */ |
| 68 | int 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 | */ |
| 89 | struct 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; |
| 104 | error: |
| 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 | */ |
| 116 | struct 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; |
| 134 | error_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 | */ |
| 145 | static 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 | */ |
| 183 | struct 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; |
| 252 | error: |
| 253 | msgb_free(msg); |
| 254 | talloc_free(ctx); |
| 255 | return NULL; |
| 256 | } |
| 257 | |
| 258 | static 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 | |
| 273 | static 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 | */ |
| 295 | int 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 | */ |
| 403 | int 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 | |
| 422 | exit_free: |
| 423 | talloc_free(ans); |
| 424 | return rc; |
| 425 | } |