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/Makefile.am b/openbsc/src/gprs/Makefile.am
index 0dd38f9..5212c67 100644
--- a/openbsc/src/gprs/Makefile.am
+++ b/openbsc/src/gprs/Makefile.am
@@ -35,7 +35,9 @@
 			-lgtp $(OSMO_LIBS) $(LIBOSMOABIS_LIBS) $(LIBCARES_LIBS) \
 			$(LIBCRYPTO_LIBS) -lrt
 
-osmo_gtphub_SOURCES =	gtphub_main.c gtphub.c gtphub_ext.c gtphub_vty.c
+osmo_gtphub_SOURCES =	gtphub_main.c gtphub.c gtphub_ext.c gtphub_vty.c \
+			sgsn_ares.c gprs_utils.c
 osmo_gtphub_LDADD = 	\
 			$(top_builddir)/src/libcommon/libcommon.a \
-			-lgtp $(LIBOSMOCORE_LIBS) $(LIBOSMOVTY_LIBS) -lrt
+			-lgtp $(LIBOSMOCORE_LIBS) $(LIBOSMOGSM_LIBS) $(LIBOSMOVTY_LIBS) \
+			$(LIBCARES_LIBS) -lrt
diff --git a/openbsc/src/gprs/gprs_utils.c b/openbsc/src/gprs/gprs_utils.c
index 2293f02..ad479db 100644
--- a/openbsc/src/gprs/gprs_utils.c
+++ b/openbsc/src/gprs/gprs_utils.c
@@ -114,7 +114,9 @@
 }
 
 /* TODO: Move these conversion functions to a utils file. */
-/**
+/* TODO: consolidate with gprs_apn2str(). */
+/** memmove apn_enc to out_str, replacing the length octets in apn_enc with '.'
+ * (omitting the first one) and terminating with a '\0'.
  * out_str needs to have rest_chars amount of bytes or 1 whatever is bigger.
  */
 char * gprs_apn_to_str(char *out_str, const uint8_t *apn_enc, size_t rest_chars)
diff --git a/openbsc/src/gprs/gtphub.c b/openbsc/src/gprs/gtphub.c
index 440f00f..eae4cf2 100644
--- a/openbsc/src/gprs/gtphub.c
+++ b/openbsc/src/gprs/gtphub.c
@@ -33,11 +33,13 @@
 
 #include <openbsc/gtphub.h>
 #include <openbsc/debug.h>
+#include <openbsc/gprs_utils.h>
 
 #include <osmocom/core/utils.h>
 #include <osmocom/core/logging.h>
 #include <osmocom/core/socket.h>
 
+
 #define GTPHUB_DEBUG 1
 
 static const int GTPH_GC_TICK_SECONDS = 1;
@@ -345,13 +347,13 @@
  * The first IEI is reached by passing i = 0.
  * imsi must point at allocated space of (at least) 8 bytes.
  * Return 1 on success, or 0 if not found. */
-static int get_ie_imsi(union gtpie_member *ie[], uint8_t *imsi, int i)
+static int get_ie_imsi(union gtpie_member *ie[], int i, uint8_t *imsi)
 {
 	return gtpie_gettv0(ie, GTPIE_IMSI, i, imsi, 8) == 0;
 }
 
 /* Analogous to get_ie_imsi(). nsapi must point at a single uint8_t. */
-static int get_ie_nsapi(union gtpie_member *ie[], uint8_t *nsapi, int i)
+static int get_ie_nsapi(union gtpie_member *ie[], int i, uint8_t *nsapi)
 {
 	return gtpie_gettv1(ie, GTPIE_NSAPI, i, nsapi) == 0;
 }
@@ -379,6 +381,33 @@
 	return str;
 }
 
+static const char *get_ie_imsi_str(union gtpie_member *ie[], int i)
+{
+	uint8_t imsi_buf[8];
+	if (!get_ie_imsi(ie, i, imsi_buf))
+		return NULL;
+	return imsi_to_str(imsi_buf);
+}
+
+static const char *get_ie_apn_str(union gtpie_member *ie[])
+{
+	static char apn_buf[GSM_APN_LENGTH];
+	unsigned int len;
+	if (gtpie_gettlv(ie, GTPIE_APN, 0,
+			 &len, apn_buf, sizeof(apn_buf)) != 0)
+		return NULL;
+
+	if (!len)
+		return NULL;
+
+	if (len > (sizeof(apn_buf) - 1))
+		len = sizeof(apn_buf) - 1;
+	apn_buf[len] = '\0';
+
+	return  gprs_apn_to_str(apn_buf, (uint8_t*)apn_buf, len);
+}
+
+
 /* Validate header, and index information elements. Write decoded packet
  * information to *res. res->data will point at the given data buffer. On
  * error, p->rc is set <= 0 (see enum gtp_rc). */
@@ -416,15 +445,15 @@
 	int i;
 
 	for (i = 0; i < 10; i++) {
-		uint8_t imsi[8];
-		if (!get_ie_imsi(res->ie, imsi, i))
+		const char *imsi = get_ie_imsi_str(res->ie, i);
+		if (!imsi)
 			break;
-		LOG("| IMSI %s\n", imsi_to_str(imsi));
+		LOG("| IMSI %s\n", imsi);
 	}
 
 	for (i = 0; i < 10; i++) {
 		uint8_t nsapi;
-		if (!get_ie_nsapi(res->ie, &nsapi, i))
+		if (!get_ie_nsapi(res->ie, i, &nsapi))
 			break;
 		LOG("| NSAPI %d\n", (int)nsapi);
 	}
@@ -481,7 +510,6 @@
 			expiring_item_del(m);
 			expired ++;
 		} else {
-			LOG("Not expired: %d > %d\n", (int)m->expiry, (int)now);
 			/* The items are added sorted by expiry. So when we hit
 			 * an unexpired entry, only more unexpired ones will
 			 * follow. */
@@ -654,19 +682,15 @@
 						    struct gtp_packet_desc *p);
 
 /* See gtphub_ext.c (wrapped by unit test) */
-int gtphub_resolve_ggsn_addr(struct gtphub *hub,
-			     struct osmo_sockaddr *result,
-			     struct gtp_packet_desc *p);
+struct gtphub_peer_port *gtphub_resolve_ggsn_addr(struct gtphub *hub,
+						  const char *imsi_str,
+						  const char *apn_ni_str);
+int gtphub_ares_init(struct gtphub *hub);
 
 static struct gtphub_peer_port *gtphub_port_find(const struct gtphub_bind *bind,
 						 const struct gsn_addr *addr,
 						 uint16_t port);
 
-static struct gtphub_peer_port *gtphub_port_have(struct gtphub *hub,
-						 struct gtphub_bind *bind,
-						 const struct gsn_addr *addr,
-						 uint16_t port);
-
 static void gtphub_zero(struct gtphub *hub)
 {
 	ZERO_STRUCT(hub);
@@ -833,7 +857,7 @@
 	return gtphub_peer_strb(peer, buf, sizeof(buf));
 }
 
-static const char *gtphub_port_str(struct gtphub_peer_port *port)
+const char *gtphub_port_str(struct gtphub_peer_port *port)
 {
 	static char buf[256];
 	return gtphub_port_strb(port, buf, sizeof(buf));
@@ -1562,6 +1586,49 @@
 	return received;
 }
 
+static void resolved_gssn_del_cb(struct expiring_item *expi)
+{
+	struct gtphub_resolved_ggsn *ggsn;
+	ggsn = container_of(expi, struct gtphub_resolved_ggsn, expiry_entry);
+
+	gtphub_port_ref_count_dec(ggsn->peer);
+	llist_del(&ggsn->entry);
+
+	ggsn->expiry_entry.del_cb = 0;
+	expiring_item_del(&ggsn->expiry_entry);
+
+	talloc_free(ggsn);
+}
+
+void gtphub_resolved_ggsn(struct gtphub *hub, const char *apn_oi_str,
+			  struct gsn_addr *resolved_addr,
+			  time_t now)
+{
+	struct gtphub_peer_port *pp;
+	struct gtphub_resolved_ggsn *ggsn;
+
+	pp = gtphub_port_have(hub, &hub->to_ggsns[GTPH_PLANE_CTRL],
+			      resolved_addr, 2123);
+	if (!pp) {
+		LOGERR("Internal: Cannot create/find peer '%s'\n",
+		       gsn_addr_to_str(resolved_addr));
+		return;
+	}
+
+	ggsn = talloc_zero(osmo_gtphub_ctx, struct gtphub_resolved_ggsn);
+	OSMO_ASSERT(ggsn);
+
+	ggsn->peer = pp;
+	gtphub_port_ref_count_inc(pp);
+
+	strncpy(ggsn->apn_oi_str, apn_oi_str, sizeof(ggsn->apn_oi_str));
+
+	ggsn->expiry_entry.del_cb = resolved_gssn_del_cb;
+	expiry_add(&hub->expire_tei_maps, &ggsn->expiry_entry, now);
+
+	llist_add(&ggsn->entry, &hub->resolved_ggsns);
+}
+
 static int gtphub_gc_peer_port(struct gtphub_peer_port *pp)
 {
 	return pp->ref_count == 0;
@@ -1654,6 +1721,8 @@
 {
 	gtphub_zero(hub);
 
+	INIT_LLIST_HEAD(&hub->resolved_ggsns);
+
 	expiry_init(&hub->expire_seq_maps, GTPH_SEQ_MAPPING_EXPIRY_SECS);
 	expiry_init(&hub->expire_tei_maps, GTPH_TEI_MAPPING_EXPIRY_MINUTES * 60);
 
@@ -1693,6 +1762,7 @@
 	int rc;
 
 	gtphub_init(hub);
+	gtphub_ares_init(hub);
 
 	int plane_idx;
 	for (plane_idx = 0; plane_idx < GTPH_PLANE_N; plane_idx++) {
@@ -1882,10 +1952,10 @@
 	return pp;
 }
 
-static struct gtphub_peer_port *gtphub_port_have(struct gtphub *hub,
-						 struct gtphub_bind *bind,
-						 const struct gsn_addr *addr,
-						 uint16_t port)
+struct gtphub_peer_port *gtphub_port_have(struct gtphub *hub,
+					  struct gtphub_bind *bind,
+					  const struct gsn_addr *addr,
+					  uint16_t port)
 {
 	struct gtphub_peer_addr *a = gtphub_addr_have(hub, bind, addr);
 
@@ -1896,47 +1966,12 @@
 	return gtphub_addr_add_port(a, port);
 }
 
-/* port_override: <=0: use port from addr; >0: use this number as port. Return
- * NULL if the address cannot be parsed. */
-static struct gtphub_peer_port *gtphub_port_have_sockaddr(struct gtphub *hub,
-							  struct gtphub_bind *bind,
-							  const struct osmo_sockaddr *addr,
-							  int port_override)
-{
-	struct gsn_addr gsna;
-	uint16_t port;
-	if (gsn_addr_from_sockaddr(&gsna, &port, addr) != 0)
-		return NULL;
-
-	if (port_override > 0)
-		port = port_override;
-	return gtphub_port_have(hub, bind, &gsna, port);
-}
-
-static struct gtphub_peer_port *gtphub_have_ggsn(struct gtphub *hub,
-						 struct osmo_sockaddr *addr,
-						 unsigned int plane_idx,
-						 int port_override)
-{
-	if (port_override == 0)
-		port_override = gtphub_plane_idx_default_port[plane_idx];
-
-	return gtphub_port_have_sockaddr(hub, &hub->to_ggsns[plane_idx], addr,
-					 port_override);
-}
-
 static struct gtphub_peer_port *gtphub_resolve_ggsn(struct gtphub *hub,
 						    struct gtp_packet_desc *p)
 {
-	int rc;
-
-	struct osmo_sockaddr addr;
-
-	rc = gtphub_resolve_ggsn_addr(hub, &addr, p);
-	if (rc < 0)
-		return NULL;
-
-	return gtphub_have_ggsn(hub, &addr, p->plane_idx, -1);
+	return gtphub_resolve_ggsn_addr(hub,
+					get_ie_imsi_str(p->ie, 0),
+					get_ie_apn_str(p->ie));
 }
 
 
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;
+}
diff --git a/openbsc/src/gprs/sgsn_vty.c b/openbsc/src/gprs/sgsn_vty.c
index 3a73c9f..3f61163 100644
--- a/openbsc/src/gprs/sgsn_vty.c
+++ b/openbsc/src/gprs/sgsn_vty.c
@@ -110,6 +110,11 @@
 
 
 #define GSM48_MAX_APN_LEN	102	/* 10.5.6.1 */
+/* TODO: consolidate with gprs_apn_to_str(). */
+/** Copy apn to a static buffer, replacing the length octets in apn_enc with '.'
+ * and terminating with a '\0'. Return the static buffer.
+ * len: the length of the encoded APN (which has no terminating zero).
+ */
 static char *gprs_apn2str(uint8_t *apn, unsigned int len)
 {
 	static char apnbuf[GSM48_MAX_APN_LEN+1];