add mDNS lookup method to libosmo-mslookup (#2)

Add the first actually useful lookup method to the mslookup library: multicast
DNS.

The server side is added in a subsequent commit, when the mslookup server is
implemented for the osmo-hlr program.

Use custom DNS encoding instead of libc-ares (which we use in OsmoSGSN
already), because libc-ares is only a DNS client implementation and we will
need both client and server.

Resubmit of f10463c5fc6d9e786ab7c648d99f7450f9a25906 after being
reverted in 110a49f69f29fed844d8743b76fd748f4a14812a. This new version
skips the mslookup_client_mdns test if multicast is not supported in the
build environment. I have verified that it doesn't break the build
anymore in my own OBS namespace.

Related: OS#4237, OS#4361
Patch-by: osmith, nhofmeyr
Change-Id: I3c340627181b632dd6a0d577aa2ea2a7cd035c0c
diff --git a/src/mslookup/mslookup_client_mdns.c b/src/mslookup/mslookup_client_mdns.c
new file mode 100644
index 0000000..7ba3502
--- /dev/null
+++ b/src/mslookup/mslookup_client_mdns.c
@@ -0,0 +1,235 @@
+/* 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 <string.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+#include <unistd.h>
+#include <errno.h>
+#include <osmocom/core/select.h>
+#include <osmocom/gsm/gsm_utils.h>
+#include <osmocom/hlr/logging.h>
+#include <osmocom/mslookup/mdns.h>
+#include <osmocom/mslookup/mdns_sock.h>
+#include <osmocom/mslookup/mslookup_client.h>
+#include <osmocom/mslookup/mslookup_client_mdns.h>
+
+struct osmo_mdns_method_state {
+	/* Parameters passed by _add_method_dns() */
+	struct osmo_sockaddr_str bind_addr;
+	const char *domain_suffix;
+
+	struct osmo_mdns_sock *mc;
+
+	struct osmo_mslookup_client *client;
+	struct llist_head requests;
+	uint16_t next_packet_id;
+};
+
+struct osmo_mdns_method_request {
+	struct llist_head entry;
+	uint32_t request_handle;
+	struct osmo_mslookup_query query;
+	uint16_t packet_id;
+};
+
+static int request_handle_by_query(uint32_t *request_handle, struct osmo_mdns_method_state *state,
+				   struct osmo_mslookup_query *query, uint16_t packet_id)
+{
+	struct osmo_mdns_method_request *request;
+
+	llist_for_each_entry(request, &state->requests, entry) {
+		if (strcmp(request->query.service, query->service) != 0)
+			continue;
+		if (osmo_mslookup_id_cmp(&request->query.id, &query->id) != 0)
+			continue;
+
+		/* Match! */
+		*request_handle = request->request_handle;
+		return 0;
+	}
+	return -1;
+}
+
+static int mdns_method_recv(struct osmo_fd *osmo_fd, unsigned int what)
+{
+	struct osmo_mdns_method_state *state = osmo_fd->data;
+	struct osmo_mslookup_result result;
+	struct osmo_mslookup_query query;
+	uint16_t packet_id;
+	int n;
+	uint8_t buffer[1024];
+	uint32_t request_handle = 0;
+	void *ctx = state;
+
+	n = read(osmo_fd->fd, buffer, sizeof(buffer));
+	if (n < 0) {
+		LOGP(DMSLOOKUP, LOGL_ERROR, "failed to read from socket\n");
+		return n;
+	}
+
+	if (osmo_mdns_result_decode(ctx, buffer, n, &packet_id, &query, &result, state->domain_suffix) < 0)
+		return -EINVAL;
+
+	if (request_handle_by_query(&request_handle, state, &query, packet_id) != 0)
+		return -EINVAL;
+
+	osmo_mslookup_client_rx_result(state->client, request_handle, &result);
+	return n;
+}
+
+static void mdns_method_request(struct osmo_mslookup_client_method *method, const struct osmo_mslookup_query *query,
+				uint32_t request_handle)
+{
+	char buf[256];
+	struct osmo_mdns_method_state *state = method->priv;
+	struct msgb *msg;
+	struct osmo_mdns_method_request *r = talloc_zero(method->client, struct osmo_mdns_method_request);
+
+	*r = (struct osmo_mdns_method_request){
+		.request_handle = request_handle,
+		.query = *query,
+		.packet_id = state->next_packet_id,
+	};
+	llist_add(&r->entry, &state->requests);
+	state->next_packet_id++;
+
+	msg = osmo_mdns_query_encode(method->client, r->packet_id, query, state->domain_suffix);
+	if (!msg) {
+		LOGP(DMSLOOKUP, LOGL_ERROR, "Cannot encode request: %s\n",
+		     osmo_mslookup_result_name_b(buf, sizeof(buf), query, NULL));
+	}
+
+	/* Send over the wire */
+	LOGP(DMSLOOKUP, LOGL_DEBUG, "sending mDNS query: %s.%s\n", query->service,
+	     osmo_mslookup_id_name_b(buf, sizeof(buf), &query->id));
+	if (osmo_mdns_sock_send(state->mc, msg) == -1)
+		LOGP(DMSLOOKUP, LOGL_ERROR, "sending mDNS query failed\n");
+}
+
+static void mdns_method_request_cleanup(struct osmo_mslookup_client_method *method, uint32_t request_handle)
+{
+	struct osmo_mdns_method_state *state = method->priv;
+
+	/* Tear down any state associated with this handle. */
+	struct osmo_mdns_method_request *r;
+	llist_for_each_entry(r, &state->requests, entry) {
+		if (r->request_handle != request_handle)
+			continue;
+		llist_del(&r->entry);
+		talloc_free(r);
+		return;
+	}
+}
+
+static void mdns_method_destruct(struct osmo_mslookup_client_method *method)
+{
+	struct osmo_mdns_method_state *state = method->priv;
+	struct osmo_mdns_method_request *e, *n;
+	if (!state)
+		return;
+
+	/* Drop all DNS lookup request state. Triggering a timeout event and cleanup for mslookup client users will
+	 * happen in the mslookup_client.c, we will simply stop responding from this lookup method. */
+	llist_for_each_entry_safe(e, n, &state->requests, entry) {
+		llist_del(&e->entry);
+	}
+
+	osmo_mdns_sock_cleanup(state->mc);
+}
+
+/*! Initialize the mDNS lookup method.
+ * \param[in] client  the client to attach the method to.
+ * \param[in] ip  IPv4 or IPv6 address string.
+ * \param[in] port  The port to bind to.
+ * \param[in] initial_packet_id  Used in the first mslookup query, then increased by one in each following query. All
+ *				 servers answer to each query with the same packet ID. Set to -1 to use a random
+ *				 initial ID (recommended unless you need deterministic output). This ID is for visually
+ *				 distinguishing the packets in packet sniffers, the mslookup client uses not just the
+ *				 ID, but all query parameters (service type, ID, ID type), to determine if a reply is
+ *				 relevant.
+ * \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" */
+struct osmo_mslookup_client_method *osmo_mslookup_client_add_mdns(struct osmo_mslookup_client *client, const char *ip,
+								  uint16_t port, int initial_packet_id,
+								  const char *domain_suffix)
+{
+	struct osmo_mdns_method_state *state;
+	struct osmo_mslookup_client_method *m;
+
+	m = talloc_zero(client, struct osmo_mslookup_client_method);
+	OSMO_ASSERT(m);
+
+	state = talloc_zero(m, struct osmo_mdns_method_state);
+	OSMO_ASSERT(state);
+	INIT_LLIST_HEAD(&state->requests);
+	if (osmo_sockaddr_str_from_str(&state->bind_addr, ip, port)) {
+		LOGP(DMSLOOKUP, LOGL_ERROR, "mslookup mDNS: invalid address/port: %s %u\n",
+		     ip, port);
+		goto error_cleanup;
+	}
+
+	if (initial_packet_id == -1) {
+		if (osmo_get_rand_id((uint8_t *)&state->next_packet_id, 2) < 0) {
+			LOGP(DMSLOOKUP, LOGL_ERROR, "mslookup mDNS: failed to generate random initial packet ID\n");
+			goto error_cleanup;
+		}
+	} else
+		state->next_packet_id = initial_packet_id;
+
+	state->client = client;
+	state->domain_suffix = domain_suffix;
+
+	state->mc = osmo_mdns_sock_init(state, ip, port, mdns_method_recv, state, 0);
+	if (!state->mc)
+		goto error_cleanup;
+
+	*m = (struct osmo_mslookup_client_method){
+		.name = "mDNS",
+		.priv = state,
+		.request = mdns_method_request,
+		.request_cleanup = mdns_method_request_cleanup,
+		.destruct = mdns_method_destruct,
+	};
+
+	osmo_mslookup_client_method_add(client, m);
+	return m;
+
+error_cleanup:
+	talloc_free(m);
+	return NULL;
+}
+
+const struct osmo_sockaddr_str *osmo_mslookup_client_method_mdns_get_bind_addr(struct osmo_mslookup_client_method *dns_method)
+{
+	struct osmo_mdns_method_state *state;
+	if (!dns_method || !dns_method->priv)
+		return NULL;
+	state = dns_method->priv;
+	return &state->bind_addr;
+}
+
+const char *osmo_mslookup_client_method_mdns_get_domain_suffix(struct osmo_mslookup_client_method *dns_method)
+{
+	struct osmo_mdns_method_state *state;
+	if (!dns_method || !dns_method->priv)
+		return NULL;
+	state = dns_method->priv;
+	return state->domain_suffix;
+}