| /* 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; |
| } |