gtphub: handle Delete PDP Context.

During resolution of the header TEI, also return the tunnel struct that
resolved the TEI, so the Delete PDP Ctx code does not need to look it up
again.

Upon Delete PDP Ctx Request, remember the IEs and that a request was made.
Upon Delete PDP Ctx Response, find the pending delete and remove the
corresponding tunnel, iff the response indicates success.

Add a context deletion to regression tests, rename the test appropriately.

Sponsored-by: On-Waves ehi
diff --git a/openbsc/include/openbsc/gtphub.h b/openbsc/include/openbsc/gtphub.h
index c72a0cf..ea0f964 100644
--- a/openbsc/include/openbsc/gtphub.h
+++ b/openbsc/include/openbsc/gtphub.h
@@ -443,6 +443,7 @@
 	struct nr_pool tei_pool;
 
 	struct llist_head tunnels; /* struct gtphub_tunnel */
+	struct llist_head pending_deletes; /* opaque (gtphub.c) */
 
 	struct llist_head ggsn_lookups; /* opaque (gtphub_ares.c) */
 	struct llist_head resolved_ggsns; /* struct gtphub_resolved_ggsn */
diff --git a/openbsc/src/gprs/gtphub.c b/openbsc/src/gprs/gtphub.c
index 465520e..dcfa823 100644
--- a/openbsc/src/gprs/gtphub.c
+++ b/openbsc/src/gprs/gtphub.c
@@ -95,6 +95,15 @@
 	union gtpie_member *ie[GTPIE_SIZE];
 };
 
+struct pending_delete {
+	struct llist_head entry;
+	struct expiring_item expiry_entry;
+
+	struct gtphub_tunnel *tun;
+	uint8_t teardown_ind;
+	uint8_t nsapi;
+};
+
 
 /* counters */
 
@@ -1325,7 +1334,8 @@
 
 static struct gtphub_tunnel_endpoint *gtphub_unmap_tei(struct gtphub *hub,
 						       struct gtp_packet_desc *p,
-						       struct gtphub_peer_port *from)
+						       struct gtphub_peer_port *from,
+						       struct gtphub_tunnel **unmapped_from_tun)
 {
 	OSMO_ASSERT(from);
 	int other_side = other_side_idx(p->side_idx);
@@ -1341,9 +1351,14 @@
 		    && gsn_addr_same(&te_from->peer->peer_addr->addr,
 				     &from->peer_addr->addr)) {
 			gtphub_tunnel_refresh(hub, tun, p->timestamp);
+			if (unmapped_from_tun)
+				*unmapped_from_tun = tun;
 			return te_to;
 		}
 	}
+
+	if (unmapped_from_tun)
+		*unmapped_from_tun = NULL;
 	return NULL;
 }
 
@@ -1376,12 +1391,15 @@
 }
 
 static int gtphub_unmap_header_tei(struct gtphub_peer_port **to_port_p,
+				   struct gtphub_tunnel **unmapped_from_tun,
 				   struct gtphub *hub,
 				   struct gtp_packet_desc *p,
 				   struct gtphub_peer_port *from_port)
 {
 	OSMO_ASSERT(p->version == 1);
 	*to_port_p = NULL;
+	if (unmapped_from_tun)
+		*unmapped_from_tun = NULL;
 
 	/* If the header's TEI is zero, no PDP context has been established
 	 * yet. If nonzero, a mapping should actually already exist for this
@@ -1392,7 +1410,7 @@
 	/* to_peer has previously announced a TEI, which was stored and
 	 * mapped in a tunnel struct. */
 	struct gtphub_tunnel_endpoint *to;
-	to = gtphub_unmap_tei(hub, p, from_port);
+	to = gtphub_unmap_tei(hub, p, from_port, unmapped_from_tun);
 	if (!to) {
 		LOG(LOGL_ERROR, "Received unknown TEI %" PRIx32 " from %s\n",
 		    p->header_tei_rx, gtphub_port_str(from_port));
@@ -1549,12 +1567,132 @@
 	return 0;
 }
 
+static void pending_delete_del_cb(struct expiring_item *expi)
+{
+	struct pending_delete *pd;
+	pd = container_of(expi, struct pending_delete, expiry_entry);
+
+	llist_del(&pd->entry);
+	INIT_LLIST_HEAD(&pd->entry);
+
+	pd->expiry_entry.del_cb = 0;
+	expiring_item_del(&pd->expiry_entry);
+
+	talloc_free(pd);
+}
+
+static struct pending_delete *pending_delete_new(void)
+{
+	struct pending_delete *pd = talloc_zero(osmo_gtphub_ctx, struct pending_delete);
+	INIT_LLIST_HEAD(&pd->entry);
+	expiring_item_init(&pd->expiry_entry);
+	pd->expiry_entry.del_cb = pending_delete_del_cb;
+	return pd;
+}
+
 static int gtphub_handle_delete_pdp_ctx(struct gtphub *hub,
 					struct gtp_packet_desc *p,
+					struct gtphub_tunnel *known_tun,
 					struct gtphub_peer_port *from_ctrl,
 					struct gtphub_peer_port *to_ctrl)
 {
-	/* TODO */
+	if (p->type == GTP_DELETE_PDP_REQ) {
+		if (!known_tun) {
+			LOG(LOGL_ERROR, "Cannot find tunnel for Delete PDP Context Request.\n");
+			return -1;
+		}
+
+		/* Store the Delete Request until a successful Response is seen. */
+		uint8_t teardown_ind;
+		uint8_t nsapi;
+
+		if (gtpie_gettv1(p->ie, GTPIE_TEARDOWN, 0, &teardown_ind) != 0) {
+			LOG(LOGL_ERROR, "Missing Teardown Ind IE in Delete PDP Context Request.\n");
+			return -1;
+		}
+
+		if (gtpie_gettv1(p->ie, GTPIE_NSAPI, 0, &nsapi) != 0) {
+			LOG(LOGL_ERROR, "Missing NSAPI IE in Delete PDP Context Request.\n");
+			return -1;
+		}
+
+		struct pending_delete *pd = NULL;
+
+		struct pending_delete *pdi = NULL;
+		llist_for_each_entry(pdi, &hub->pending_deletes, entry) {
+			if ((pdi->tun == known_tun)
+			    && (pdi->teardown_ind == teardown_ind)
+			    && (pdi->nsapi == nsapi)) {
+				pd = pdi;
+				break;
+			}
+		}
+
+		if (!pd) {
+			pd = pending_delete_new();
+			pd->tun = known_tun;
+			pd->teardown_ind = teardown_ind;
+			pd->nsapi = nsapi;
+
+			LOG(LOGL_DEBUG, "Tunnel delete pending: %s\n",
+			    gtphub_tunnel_str(known_tun));
+			llist_add(&pd->entry, &hub->pending_deletes);
+		}
+
+		/* Add or refresh timeout. */
+		expiry_add(&hub->expire_quickly, &pd->expiry_entry, p->timestamp);
+
+		/* If a pending_delete should expire before the response to
+		 * indicate success comes in, the responding peer will have the
+		 * tunnel deactivated, while the requesting peer gets no reply
+		 * and keeps the tunnel. The hope is that the requesting peer
+		 * will try again and get a useful response. */
+	} else if (p->type == GTP_DELETE_PDP_RSP) {
+		/* Find the Delete Request for this Response. */
+		struct pending_delete *pd = NULL;
+
+		struct pending_delete *pdi;
+		llist_for_each_entry(pdi, &hub->pending_deletes, entry) {
+			if (known_tun == pdi->tun) {
+				pd = pdi;
+				break;
+			}
+		}
+
+		if (!pd) {
+			LOG(LOGL_ERROR, "Delete PDP Context Response:"
+			    " Cannot find matching request.");
+			/* If we delete the tunnel now, anyone can send a
+			 * Delete response to kill tunnels at will. */
+			return -1;
+		}
+
+		/* TODO handle teardown_ind and nsapi */
+
+		expiring_item_del(&pd->expiry_entry);
+
+		uint8_t cause;
+		if (gtpie_gettv1(p->ie, GTPIE_CAUSE, 0, &cause) != 0) {
+			LOG(LOGL_ERROR, "Delete PDP Context Response:"
+			    " Missing Cause IE.");
+			/* If we delete the tunnel now, at least one of the
+			 * peers may still think it is active. */
+			return -1;
+		}
+
+		if (cause != GTPCAUSE_ACC_REQ) {
+			LOG(LOGL_NOTICE,
+			    "Delete PDP Context Response indicates failure;"
+			    "for %s\n",
+			    gtphub_tunnel_str(known_tun));
+			return -1;
+		}
+
+		LOG(LOGL_DEBUG, "Delete PDP Context: removing tunnel %s\n",
+		    gtphub_tunnel_str(known_tun));
+		expiring_item_del(&known_tun->expiry_entry);
+	}
+
 	return 0;
 }
 
@@ -1573,6 +1711,7 @@
  * the packet p. */
 static int gtphub_handle_pdp_ctx(struct gtphub *hub,
 				 struct gtp_packet_desc *p,
+				 struct gtphub_tunnel *known_tun,
 				 struct gtphub_peer_port *from_ctrl,
 				 struct gtphub_peer_port *to_ctrl)
 {
@@ -1586,7 +1725,7 @@
 
 	case GTP_DELETE_PDP_REQ:
 	case GTP_DELETE_PDP_RSP:
-		return gtphub_handle_delete_pdp_ctx(hub, p,
+		return gtphub_handle_delete_pdp_ctx(hub, p, known_tun,
 						    from_ctrl, to_ctrl);
 
 	case GTP_UPDATE_PDP_REQ:
@@ -1601,7 +1740,6 @@
 
 }
 
-
 static int gtphub_write(const struct osmo_fd *to,
 			const struct osmo_sockaddr *to_addr,
 			const uint8_t *buf, size_t buf_len)
@@ -1663,7 +1801,7 @@
 			struct gtphub_peer_port *to_proxy,
 			struct gtphub_peer_port **final_unmapped,
 			struct gtphub_peer_port **unmapped_from_seq,
-			struct gtphub_peer_port **unmapped_from_tei)
+			struct gtphub_tunnel **unmapped_from_tun)
 {
 	/* Always (try to) unmap sequence and TEI numbers, which need to be
 	 * replaced in the packet. Either way, give precedence to the proxy, if
@@ -1672,17 +1810,18 @@
 	struct gtphub_peer_port *from_seq = NULL;
 	struct gtphub_peer_port *from_tei = NULL;
 	struct gtphub_peer_port *unmapped = NULL;
+	struct gtphub_tunnel *tun = NULL;
 
 	if (unmapped_from_seq)
 		*unmapped_from_seq = from_seq;
-	if (unmapped_from_tei)
-		*unmapped_from_tei = from_tei;
+	if (unmapped_from_tun)
+		*unmapped_from_tun = tun;
 	if (final_unmapped)
 		*final_unmapped = unmapped;
 
 	from_seq = gtphub_unmap_seq(p, from);
 
-	if (gtphub_unmap_header_tei(&from_tei, hub, p, from) < 0)
+	if (gtphub_unmap_header_tei(&from_tei, &tun, hub, p, from) < 0)
 		return -1;
 
 	struct gtphub_peer *from_peer = from->peer_addr->peer;
@@ -1719,8 +1858,8 @@
 
 	if (unmapped_from_seq)
 		*unmapped_from_seq = from_seq;
-	if (unmapped_from_tei)
-		*unmapped_from_tei = from_tei;
+	if (unmapped_from_tun)
+		*unmapped_from_tun = tun;
 	if (final_unmapped)
 		*final_unmapped = unmapped;
 	return 0;
@@ -1894,11 +2033,10 @@
 
 	struct gtphub_peer_port *to_peer_from_seq;
 	struct gtphub_peer_port *to_peer;
+	struct gtphub_tunnel *tun;
 	if (gtphub_unmap(hub, &p, from_peer,
 			 hub->proxy[other_side_idx(side_idx)][plane_idx],
-			 &to_peer, &to_peer_from_seq,
-			 NULL /* not interested, got it in &to_peer already */
-			)
+			 &to_peer, &to_peer_from_seq, &tun)
 	    != 0) {
 		return -1;
 	}
@@ -1918,7 +2056,7 @@
 		/* This may be a Create PDP Context response. If it is, there
 		 * are other addresses in the GTP message to set up apart from
 		 * the sender. */
-		if (gtphub_handle_pdp_ctx(hub, &p, from_peer, to_peer)
+		if (gtphub_handle_pdp_ctx(hub, &p, tun, from_peer, to_peer)
 		    != 0)
 			return -1;
 	}
@@ -2112,6 +2250,7 @@
 	gtphub_zero(hub);
 
 	INIT_LLIST_HEAD(&hub->tunnels);
+	INIT_LLIST_HEAD(&hub->pending_deletes);
 
 	expiry_init(&hub->expire_quickly, GTPH_EXPIRE_QUICKLY_SECS);
 	expiry_init(&hub->expire_slowly, GTPH_EXPIRE_SLOWLY_MINUTES * 60);
diff --git a/openbsc/tests/gtphub/gtphub_test.c b/openbsc/tests/gtphub/gtphub_test.c
index b0ede11..fea55e0 100644
--- a/openbsc/tests/gtphub/gtphub_test.c
+++ b/openbsc/tests/gtphub/gtphub_test.c
@@ -954,9 +954,75 @@
 	return 1;
 }
 
-static void test_create_pdp_ctx(void)
+#define MSG_DEL_PDP_CTX_REQ(tei, seq) \
+		"32" 	/* 0b001'1 0010: version 1, protocol GTP, with seq nr. */ \
+		"14" 	/* type 20: Delete PDP Context Request */ \
+		"0008"	/* msg length = 8 + len (2 octets) */ \
+		tei	/* TEI Ctrl */ \
+		seq	/* Sequence nr (2 octets) */ \
+		"00"	/* N-PDU 0 */ \
+		"00"	/* No extensions */ \
+		/* IEs */ \
+		"13fe"  /* 19: Teardown ind = 0 */ \
+		"1400"	/* 20: NSAPI = 0*/ \
+
+#define MSG_DEL_PDP_CTX_RSP(tei, seq) \
+		"32" 	/* 0b001'1 0010: version 1, protocol GTP, with seq nr. */ \
+		"15" 	/* type 21: Delete PDP Context Response */ \
+		"0006"	/* msg length = 8 + len (2 octets) */ \
+		tei	/* TEI Ctrl */ \
+		seq	/* Sequence nr (2 octets) */ \
+		"00"	/* N-PDU 0 */ \
+		"00"	/* No extensions */ \
+		/* IEs */ \
+		"01"	/* 1: Cause */ \
+		  "80"	/* value = 0b10000000 = response, no rejection. */ \
+
+static int delete_pdp_ctx(void)
 {
-	LOG("test_create_pdp_ctx");
+	now += GTPH_EXPIRE_QUICKLY_SECS + 1;
+	gtphub_gc(hub, now);
+
+	LVL2_ASSERT(tunnels_are(
+		"192.168.42.23 (TEI C 321=1 / U 123=2)"
+		" <-> 192.168.43.34 (TEI C 765=3 / U 567=4)"
+		" @21945\n"));
+
+	/* TEI Ctrl from above and next sequence after abcd. */
+	const char *gtp_req_from_sgsn = MSG_DEL_PDP_CTX_REQ("00000003", "abce");
+	const char *gtp_req_to_ggsn = MSG_DEL_PDP_CTX_REQ("00000765", "6d32");
+
+	LVL2_ASSERT(msg_from_sgsn_c(&sgsn_sender,
+				    &resolved_ggsn_addr,
+				    gtp_req_from_sgsn,
+				    gtp_req_to_ggsn));
+
+	/* 21945 + 31 = 21976 */
+	LVL2_ASSERT(tunnels_are(
+		"192.168.42.23 (TEI C 321=1 / U 123=2)"
+		" <-> 192.168.43.34 (TEI C 765=3 / U 567=4)"
+		" @21976\n"));
+
+	const char *gtp_resp_from_ggsn =
+		MSG_DEL_PDP_CTX_RSP("00000001", "6d32");
+	const char *gtp_resp_to_sgsn =
+		MSG_DEL_PDP_CTX_RSP("00000321", "abce");
+
+	/* The response should go back to whichever port the request came from
+	 * (unmapped by sequence nr) */
+	LVL2_ASSERT(msg_from_ggsn_c(&resolved_ggsn_addr,
+				    &sgsn_sender,
+				    gtp_resp_from_ggsn,
+				    gtp_resp_to_sgsn));
+
+	LVL2_ASSERT(tunnels_are(""));
+
+	return 1;
+}
+
+static void test_one_pdp_ctx(void)
+{
+	LOG("test_one_pdp_ctx");
 	OSMO_ASSERT(setup_test_hub());
 
 	OSMO_ASSERT(create_pdp_ctx());
@@ -982,6 +1048,10 @@
 		"192.168.42.23 (TEI C 321=1 / U 123=2)"
 		" <-> 192.168.43.34 (TEI C 765=3 / U 567=4)"
 		" @21945\n"));
+
+	OSMO_ASSERT(delete_pdp_ctx());
+	OSMO_ASSERT(tunnels_are(""));
+
 	OSMO_ASSERT(clear_test_hub());
 }
 
@@ -1108,7 +1178,7 @@
 	test_nr_map_wrap();
 	test_expiry();
 	test_echo();
-	test_create_pdp_ctx();
+	test_one_pdp_ctx();
 	test_user_data();
 	printf("Done\n");
 
diff --git a/openbsc/tests/gtphub/gtphub_test.ok b/openbsc/tests/gtphub/gtphub_test.ok
index 24b9aad..8f66b9d 100644
--- a/openbsc/tests/gtphub/gtphub_test.ok
+++ b/openbsc/tests/gtphub/gtphub_test.ok
@@ -1,5 +1,5 @@
 test_echo
-test_create_pdp_ctx
+test_one_pdp_ctx
 - __wrap_gtphub_resolve_ggsn_addr():
   returning GGSN addr from imsi 240010123456789 ni internet: 192.168.43.34 port 2123
 test_user_data