gprs: Use PURGE MS messages

When a subscriber entry is going to be deleted by SGSN and when the
subscriber info has been obtained from a remote peer via GSUP, the
peer should be informed before the entry is really deleted. For this
purpose, MAP defines the PURGE MS procedure (see GSM 09.02, 19.1.4).

This patch adds support for the PURGE_MS_REQ/_ERR/_RES messages and
invokes the procedure when the subscriber entry is going to be
removed. This only applies if GSUP is being used, the Update
Location procedure has been completed successfully, and the
subscriber has not been cancelled. The removal of the entry is
delayed until a PURGE_MS_RES or PURGE_MS_ERR message is received.

Note that GSM 09.02, 19.1.4.4 implies that the subscriber data is not
to be removed when the procedure fails which is not the way the
feature has been implemented.

Note that handling 'P-TMSI freezing' is not implemented.

Ticket: OW#1338
Sponsored-by: On-Waves ehf
diff --git a/openbsc/include/openbsc/gprs_sgsn.h b/openbsc/include/openbsc/gprs_sgsn.h
index e707c77..2ed46ff 100644
--- a/openbsc/include/openbsc/gprs_sgsn.h
+++ b/openbsc/include/openbsc/gprs_sgsn.h
@@ -304,6 +304,7 @@
 #define GPRS_SUBSCRIBER_UPDATE_AUTH_INFO_PENDING	(1 << 16)
 #define GPRS_SUBSCRIBER_UPDATE_LOCATION_PENDING		(1 << 17)
 #define GPRS_SUBSCRIBER_CANCELLED			(1 << 18)
+#define GPRS_SUBSCRIBER_ENABLE_PURGE			(1 << 19)
 
 #define GPRS_SUBSCRIBER_UPDATE_PENDING_MASK ( \
 		GPRS_SUBSCRIBER_UPDATE_LOCATION_PENDING | \
diff --git a/openbsc/src/gprs/gprs_subscriber.c b/openbsc/src/gprs/gprs_subscriber.c
index 9d79f00..58203ba 100644
--- a/openbsc/src/gprs/gprs_subscriber.c
+++ b/openbsc/src/gprs/gprs_subscriber.c
@@ -104,10 +104,27 @@
 	LOGGSUBSCRP(LOGL_INFO, subscr,
 		    "Expired, deleting subscriber entry\n");
 
+	subscr_get(subscr);
+
+	/* Check, whether to cleanup immediately */
+	if (!(subscr->flags & GPRS_SUBSCRIBER_ENABLE_PURGE))
+		goto force_cleanup;
+
+	/* Send a 'purge MS' message to the HLR */
+	if (gprs_subscr_purge(subscr) < 0)
+		goto force_cleanup;
+
+	/* Purge request has been sent */
+
+	subscr_put(subscr);
+	return;
+
+force_cleanup:
 	/* Make sure, the timer is cleaned up */
 	subscr->keep_in_ram = 0;
 	gprs_subscr_stop_timer(subscr);
 	/* The subscr is freed now, if the timer was the last user */
+	subscr_put(subscr);
 }
 
 static struct sgsn_subscriber_data *sgsn_subscriber_data_alloc(void *ctx)
@@ -169,6 +186,7 @@
 {
 	subscr->authorized = 0;
 	subscr->flags |= GPRS_SUBSCRIBER_CANCELLED;
+	subscr->flags &= ~GPRS_SUBSCRIBER_ENABLE_PURGE;
 
 	gprs_subscr_update(subscr);
 
@@ -258,6 +276,8 @@
 	subscr->authorized = 1;
 	subscr->sgsn_data->error_cause = 0;
 
+	subscr->flags |= GPRS_SUBSCRIBER_ENABLE_PURGE;
+
 	gprs_subscr_update(subscr);
 	return 0;
 }
@@ -377,6 +397,51 @@
 	return -gsup_msg->cause;
 }
 
+static int gprs_subscr_handle_gsup_purge_res(struct gsm_subscriber *subscr,
+					     struct gprs_gsup_message *gsup_msg)
+{
+	LOGGSUBSCRP(LOGL_INFO, subscr, "Completing purge MS\n");
+
+	/* Force silent cancellation */
+	subscr->sgsn_data->error_cause = 0;
+	gprs_subscr_put_and_cancel(subscr_get(subscr));
+
+	return 0;
+}
+
+static int gprs_subscr_handle_gsup_purge_err(struct gsm_subscriber *subscr,
+					     struct gprs_gsup_message *gsup_msg)
+{
+	LOGGSUBSCRP(LOGL_NOTICE, subscr,
+		    "Purge MS has failed with cause '%s' (%d)\n",
+		    get_value_string(gsm48_gmm_cause_names, gsup_msg->cause),
+		    gsup_msg->cause);
+
+	/* In GSM 09.02, 19.1.4.4, the text and the SDL diagram imply that
+	 * the subscriber data is not removed if the request has failed. On the
+	 * other hand, keeping the subscriber data in either error case
+	 * (subscriber unknown, syntactical message error, connection error)
+	 * doesn't seem to give any advantage, since the data will be restored
+	 * on the next Attach Request anyway.
+	 * This approach ensures, that the subscriber record will not stick if
+	 * an error happens.
+	 */
+
+	/* TODO: Check whether this behaviour is acceptable and either just
+	 * remove this TODO-notice or change the implementation to not delete
+	 * the subscriber data (eventually resetting the ENABLE_PURGE flag and
+	 * restarting the expiry timer based on the cause).
+	 *
+	 * Subscriber Unknown: cancel subscr
+	 * Temporary network problems: do nothing (handled by timer based retry)
+	 * Message problems (syntax, nyi, ...): cancel subscr (retry won't help)
+	 */
+
+	gprs_subscr_handle_gsup_purge_res(subscr, gsup_msg);
+
+	return -gsup_msg->cause;
+}
+
 int gprs_subscr_rx_gsup_message(struct msgb *msg)
 {
 	uint8_t *data = msgb_l2(msg);
@@ -435,7 +500,13 @@
 		break;
 
 	case GPRS_GSUP_MSGT_PURGE_MS_ERROR:
+		rc = gprs_subscr_handle_gsup_purge_err(subscr, &gsup_msg);
+		break;
+
 	case GPRS_GSUP_MSGT_PURGE_MS_RESULT:
+		rc = gprs_subscr_handle_gsup_purge_res(subscr, &gsup_msg);
+		break;
+
 	case GPRS_GSUP_MSGT_INSERT_DATA_REQUEST:
 	case GPRS_GSUP_MSGT_DELETE_DATA_REQUEST:
 		LOGGSUBSCRP(LOGL_ERROR, subscr,
@@ -458,6 +529,16 @@
 	return rc;
 }
 
+int gprs_subscr_purge(struct gsm_subscriber *subscr)
+{
+	struct gprs_gsup_message gsup_msg = {0};
+
+	LOGGSUBSCRP(LOGL_INFO, subscr, "purging MS subscriber\n");
+
+	gsup_msg.message_type = GPRS_GSUP_MSGT_PURGE_MS_REQUEST;
+	return gprs_subscr_tx_gsup_message(subscr, &gsup_msg);
+}
+
 int gprs_subscr_query_auth_info(struct gsm_subscriber *subscr)
 {
 	struct gprs_gsup_message gsup_msg = {0};
@@ -514,6 +595,7 @@
 	if (!subscr) {
 		subscr = gprs_subscr_get_or_create(mmctx->imsi);
 		subscr->flags |= GSM_SUBSCRIBER_FIRST_CONTACT;
+		subscr->flags &= ~GPRS_SUBSCRIBER_ENABLE_PURGE;
 	}
 
 	if (strcpy(subscr->equipment.imei, mmctx->imei) != 0) {
diff --git a/openbsc/src/gprs/sgsn_vty.c b/openbsc/src/gprs/sgsn_vty.c
index 1241c17..ef4c8d8 100644
--- a/openbsc/src/gprs/sgsn_vty.c
+++ b/openbsc/src/gprs/sgsn_vty.c
@@ -463,7 +463,7 @@
 	}
 
 	if (subscr->flags)
-		vty_out(vty, "    Flags: %s%s%s%s%s",
+		vty_out(vty, "    Flags: %s%s%s%s%s%s",
 			subscr->flags & GSM_SUBSCRIBER_FIRST_CONTACT ?
 			"FIRST_CONTACT " : "",
 			subscr->flags & GPRS_SUBSCRIBER_CANCELLED ?
@@ -472,6 +472,8 @@
 			"UPDATE_LOCATION_PENDING " : "",
 			subscr->flags & GPRS_SUBSCRIBER_UPDATE_AUTH_INFO_PENDING ?
 			"AUTH_INFO_PENDING " : "",
+			subscr->flags & GPRS_SUBSCRIBER_ENABLE_PURGE ?
+			"ENABLE_PURGE " : "",
 			VTY_NEWLINE);
 
 	vty_out(vty, "    Use count: %u%s", subscr->use_count, VTY_NEWLINE);
diff --git a/openbsc/tests/sgsn/sgsn_test.c b/openbsc/tests/sgsn/sgsn_test.c
index d612652..8181061 100644
--- a/openbsc/tests/sgsn/sgsn_test.c
+++ b/openbsc/tests/sgsn/sgsn_test.c
@@ -1109,12 +1109,16 @@
 
 int my_gprs_gsup_client_send(struct gprs_gsup_client *gsupc, struct msgb *msg)
 {
-	struct gprs_gsup_message to_peer;
-	struct gprs_gsup_message from_peer;
+	struct gprs_gsup_message to_peer = {0};
+	struct gprs_gsup_message from_peer = {0};
 	struct msgb *reply_msg;
+	int rc;
 
 	/* Simulate the GSUP peer */
-	gprs_gsup_decode(msgb_data(msg), msgb_length(msg), &to_peer);
+	rc = gprs_gsup_decode(msgb_data(msg), msgb_length(msg), &to_peer);
+	OSMO_ASSERT(rc >= 0);
+	OSMO_ASSERT(to_peer.imsi[0] != 0);
+	strncpy(from_peer.imsi, to_peer.imsi, sizeof(from_peer.imsi));
 
 	/* This invalidates the pointers in to_peer */
 	msgb_free(msg);
@@ -1129,13 +1133,13 @@
 		return my_subscr_request_auth_info_gsup_auth(NULL);
 
 	case GPRS_GSUP_MSGT_PURGE_MS_REQUEST:
-		/* TODO: send RES/ERR */
-		return 0;
+		from_peer.message_type = GPRS_GSUP_MSGT_PURGE_MS_RESULT;
+		break;
 
 	default:
 		if ((to_peer.message_type & 0b00000011) == 0) {
 			/* Unhandled request */
-			/* TODO: send error(NOT_IMPL) */
+			/* Send error(NOT_IMPL) */
 			from_peer.message_type = to_peer.message_type + 1;
 			from_peer.cause = GMM_CAUSE_MSGT_NOTEXIST_NOTIMPL;
 			break;