gtphub: add first bits of GRX ares

For the resolving function, change the function signature to return a
gtphub_peer_port. In consequence, publish two functions concerned with
gtphub_peer_port instances for use in test and gtphub_ext.c.

Add GGSN resolution queue, callback and cache. Simple implementation: if an
SGSN asks for a GGSN, it will first get no answer, and I hope it will ask again
once the GGSN is in the cache.

Within gtphub_ext.c, have a dummy sgsn struct, as the sgsn_ares code currently
depends on it (half the functions pass an sgsn instance pointer around, but the
other half use the global one).

In the unit tests, wrap away the ares initialization so that they can work
without a DNS server around. The netcat test breaks because of this, will
remove it.

Using sgsn_ares, implement the gtphub_resolve_ggsn_addr() function, I hope:
untested.

Minor cosmetics just to see if you're paying attention... ;)

Sponsored-by: On-Waves ehi
diff --git a/openbsc/src/gprs/gtphub_ext.c b/openbsc/src/gprs/gtphub_ext.c
index 0a4164c..d739614 100644
--- a/openbsc/src/gprs/gtphub_ext.c
+++ b/openbsc/src/gprs/gtphub_ext.c
@@ -24,35 +24,161 @@
  */
 
 #include <string.h>
+#include <unistd.h>
 
 #include <openbsc/gtphub.h>
+#include <openbsc/debug.h>
+
 #include <osmocom/core/utils.h>
+#include <osmocom/gsm/apn.h>
 
-#define __llist_first(head) (((head)->next == (head)) ? NULL : (head)->next)
-#define llist_first(head, type, entry) llist_entry(__llist_first(head), type, entry)
+/* TODO split GRX ares from sgsn into a separate struct and allow use without
+ * globals. */
+#include <openbsc/sgsn.h>
+extern struct sgsn_instance *sgsn;
 
-int gtphub_resolve_ggsn_addr(struct gtphub *hub,
-			     struct osmo_sockaddr *result,
-			     struct gtp_packet_desc *p)
+struct sgsn_instance sgsn_inst = { 0 };
+struct sgsn_instance *sgsn = &sgsn_inst;
+
+extern void *osmo_gtphub_ctx;
+
+int gtphub_ares_init(struct gtphub *hub)
 {
-	/* TODO  This is just hardcodedly returning the first known address.
-	 * Should resolve from actual subscriber data. */
-	struct gtphub_peer *peer = llist_first(&hub->to_ggsns[GTPH_PLANE_CTRL].peers,
-					       struct gtphub_peer, entry);
-	if (!peer)
-		return -1;
-
-	struct gtphub_peer_addr *pa = llist_first(&peer->addresses,
-						  struct gtphub_peer_addr, entry);
-	if (!pa)
-		return -1;
-
-	struct gtphub_peer_port *pp = llist_first(&pa->ports,
-						  struct gtphub_peer_port, entry);
-	if (!pp)
-		return -1;
-
-	*result = pp->sa;
-	return 0;
+	return sgsn_ares_init(sgsn);
 }
 
+struct ggsn_lookup {
+	struct llist_head entry;
+	struct expiring_item expiry_entry;
+
+	struct gtphub *hub;
+
+	char imsi_str[GSM_IMSI_LENGTH];
+	char apn_ni_str[GSM_APN_LENGTH];
+	char apn_oi_str[GSM_APN_LENGTH];
+	int have_3dig_mnc;
+};
+
+static int start_ares_query(struct ggsn_lookup *lookup);
+
+static void ggsn_lookup_cb(void *arg, int status, int timeouts, struct hostent *hostent)
+{
+	struct ggsn_lookup *lookup = arg;
+
+	if (status != ARES_SUCCESS) {
+		LOGP(DGTPHUB, LOGL_ERROR, "DNS query failed.\n");
+
+		/* Need to try with three digits now */
+		if (!lookup->have_3dig_mnc) {
+			lookup->have_3dig_mnc = 1;
+			if (start_ares_query(lookup) == 0)
+				return;
+		}
+
+		LOGP(DGTPHUB, LOGL_ERROR, "Failed to resolve GGSN.\n");
+		goto remove_from_queue;
+	}
+
+	struct gsn_addr resolved_addr;
+	if (hostent->h_length > sizeof(resolved_addr.buf)) {
+		LOGP(DGTPHUB, LOGL_ERROR, "Addr size too large: %d > %d\n",
+		     (int)hostent->h_length, (int)sizeof(resolved_addr.buf));
+		goto remove_from_queue;
+	}
+
+	/* Get the first addr from the list */
+	char *addr0 = hostent->h_addr_list[0];
+	if (!addr0) {
+		LOGP(DGTPHUB, LOGL_ERROR, "No host address.\n");
+		goto remove_from_queue;
+	}
+
+	memcpy(&resolved_addr.buf, addr0, hostent->h_length);
+	resolved_addr.len = hostent->h_length;
+
+	gtphub_resolved_ggsn(lookup->hub, lookup->apn_oi_str, &resolved_addr,
+			     gtphub_now());
+
+remove_from_queue:
+	expiring_item_del(&lookup->expiry_entry);
+}
+
+static void make_addr_str(struct ggsn_lookup *lookup)
+{
+	char *apn_oi_str;
+	apn_oi_str = osmo_apn_qualify_from_imsi(lookup->imsi_str,
+						lookup->apn_ni_str,
+						lookup->have_3dig_mnc);
+	strncpy(lookup->apn_oi_str, apn_oi_str, sizeof(lookup->apn_oi_str));
+	lookup->apn_oi_str[sizeof(lookup->apn_oi_str)-1] = '\0';
+}
+
+static int start_ares_query(struct ggsn_lookup *lookup)
+{
+	LOGP(DGTPHUB, LOGL_DEBUG, "Going to query %s\n", lookup->apn_oi_str);
+
+	int rc = sgsn_ares_query(sgsn, lookup->apn_oi_str, ggsn_lookup_cb, &lookup);
+	if (rc != 0)
+		LOGP(DGTPHUB, LOGL_ERROR, "Failed to start ares query.\n");
+	return rc;
+}
+
+static void ggsn_lookup_del_cb(struct expiring_item *expi)
+{
+	struct ggsn_lookup *ggsn;
+	ggsn = container_of(expi, struct ggsn_lookup, expiry_entry);
+
+	ggsn->expiry_entry.del_cb = 0;
+	expiring_item_del(expi);
+
+	llist_del(&ggsn->entry);
+	talloc_free(ggsn);
+}
+
+struct gtphub_peer_port *gtphub_resolve_ggsn_addr(struct gtphub *hub,
+						  const char *imsi_str,
+						  const char *apn_ni_str)
+{
+	struct ggsn_lookup *lookup = talloc_zero(osmo_gtphub_ctx, struct ggsn_lookup);
+
+	lookup->hub = hub;
+
+	strncpy(lookup->imsi_str, imsi_str, sizeof(lookup->imsi_str));
+	lookup->imsi_str[sizeof(lookup->imsi_str)-1] = '\0';
+
+	strncpy(lookup->apn_ni_str, apn_ni_str, sizeof(lookup->apn_ni_str));
+	lookup->apn_ni_str[sizeof(lookup->apn_ni_str)-1] = '\0';
+
+	make_addr_str(lookup);
+
+	struct ggsn_lookup *active;
+	llist_for_each_entry(active, &hub->ggsn_lookups, entry) {
+		if (strncmp(active->apn_oi_str, lookup->apn_oi_str,
+			    sizeof(lookup->apn_oi_str)) == 0) {
+			/* A query already pending. Just tip our hat. */
+			return NULL;
+		}
+	}
+
+	struct gtphub_resolved_ggsn *resolved;
+	llist_for_each_entry(resolved, &hub->resolved_ggsns, entry) {
+		if (strncmp(resolved->apn_oi_str, lookup->apn_oi_str,
+			    sizeof(lookup->apn_oi_str)) == 0) {
+			/* Already resolved. */
+			return resolved->peer;
+		}
+	}
+
+	/* Kick off a resolution, but so far return nothing. The hope is that
+	 * the peer will resend the request (a couple of times), and by then
+	 * the GGSN will be resolved. */
+
+	llist_add(&lookup->entry, &hub->ggsn_lookups);
+
+	lookup->expiry_entry.del_cb = ggsn_lookup_del_cb;
+	expiry_add(&hub->expire_seq_maps, &lookup->expiry_entry, gtphub_now());
+
+	start_ares_query(lookup);
+
+	return NULL;
+}