SGSN: Implement network-initiated PDP CTX DEACT when GGSN restarts

If the GGSN restarts, its restart counter will increase.  We can
detect that and accordingly release/delete all PDP contexts for
that GGSN.
diff --git a/openbsc/include/openbsc/gprs_gmm.h b/openbsc/include/openbsc/gprs_gmm.h
index b91b489..bd129ae 100644
--- a/openbsc/include/openbsc/gprs_gmm.h
+++ b/openbsc/include/openbsc/gprs_gmm.h
@@ -4,6 +4,7 @@
 #include <osmocore/msgb.h>
 #include <openbsc/gprs_sgsn.h>
 
+int gsm48_tx_gsm_deact_pdp_req(struct sgsn_pdp_ctx *pdp, uint8_t sm_cause);
 int gsm48_tx_gsm_act_pdp_rej(struct sgsn_mm_ctx *mm, uint8_t tid,
 			     uint8_t cause, uint8_t pco_len, uint8_t *pco_v);
 int gsm48_tx_gsm_act_pdp_acc(struct sgsn_pdp_ctx *pdp);
diff --git a/openbsc/include/openbsc/gprs_sgsn.h b/openbsc/include/openbsc/gprs_sgsn.h
index b32c5cd..fb1b288 100644
--- a/openbsc/include/openbsc/gprs_sgsn.h
+++ b/openbsc/include/openbsc/gprs_sgsn.h
@@ -125,6 +125,10 @@
 	PDP_STATE_NONE,
 	PDP_STATE_CR_REQ,
 	PDP_STATE_CR_CONF,
+
+	/* 04.08 / Figure 6.2 / 6.1.2.2 */
+	PDP_STATE_INACT_PEND,
+	PDP_STATE_INACTIVE = PDP_STATE_NONE,
 };
 
 enum pdp_type {
@@ -162,6 +166,10 @@
 	uint32_t		rx_gtp_snu;
 	//uint32_t		charging_id;
 	int			reordering_reqd;
+
+	struct timer_list	timer;
+	unsigned int		T;		/* Txxxx number */
+	unsigned int		num_T_exp;	/* number of consecutive T expirations */
 };
 
 
@@ -182,10 +190,12 @@
 	uint32_t id;
 	unsigned int gtp_version;
 	struct in_addr remote_addr;
+	int remote_restart_ctr;
 	struct gsn_t *gsn;
 };
 struct sgsn_ggsn_ctx *sgsn_ggsn_ctx_alloc(uint32_t id);
 struct sgsn_ggsn_ctx *sgsn_ggsn_ctx_by_id(uint32_t id);
+struct sgsn_ggsn_ctx *sgsn_ggsn_ctx_by_addr(struct in_addr *addr);
 struct sgsn_ggsn_ctx *sgsn_ggsn_ctx_find_alloc(uint32_t id);
 
 struct apn_ctx {
@@ -202,4 +212,8 @@
 
 uint32_t sgsn_alloc_ptmsi(void);
 
+/* High-level function to be called in case a GGSN has disappeared or
+ * ottherwise lost state (recovery procedure) */
+int drop_all_pdp_for_ggsn(struct sgsn_ggsn_ctx *ggsn);
+
 #endif /* _GPRS_SGSN_H */
diff --git a/openbsc/src/gprs/gprs_gmm.c b/openbsc/src/gprs/gprs_gmm.c
index 158f577..ee6e366 100644
--- a/openbsc/src/gprs/gprs_gmm.c
+++ b/openbsc/src/gprs/gprs_gmm.c
@@ -67,6 +67,12 @@
 #define GSM0408_T3314_SECS	44	/* force to STBY on expiry */
 #define GSM0408_T3316_SECS	44
 
+/* Section 11.3 / Table 11.2d Timers of Session Management - network side */
+#define GSM0408_T3385_SECS	8	/* wait for ACT PDP CTX REQ */
+#define GSM0408_T3386_SECS	8	/* wait for MODIFY PDP CTX ACK */
+#define GSM0408_T3395_SECS	8	/* wait for DEACT PDP CTX ACK */
+#define GSM0408_T3397_SECS	8	/* wait for DEACT AA PDP CTX ACK */
+
 extern struct sgsn_instance *sgsn;
 
 /* Protocol related stuff, should go into libosmocore */
@@ -1092,6 +1098,25 @@
 
 /* GPRS SESSION MANAGEMENT */
 
+static void pdpctx_timer_cb(void *_mm);
+
+static void pdpctx_timer_start(struct sgsn_pdp_ctx *pdp, unsigned int T,
+				unsigned int seconds)
+{
+	if (bsc_timer_pending(&pdp->timer))
+		LOGP(DMM, LOGL_ERROR, "Starting MM timer %u while old "
+			"timer %u pending\n", T, pdp->T);
+	pdp->T = T;
+	pdp->num_T_exp = 0;
+
+	/* FIXME: we should do this only once ? */
+	pdp->timer.data = pdp;
+	pdp->timer.cb = &pdpctx_timer_cb;
+
+	bsc_schedule_timer(&pdp->timer, seconds, 0);
+}
+
+
 static void msgb_put_pdp_addr_ipv4(struct msgb *msg, uint32_t ipaddr)
 {
 	uint8_t v[6];
@@ -1179,6 +1204,33 @@
 	return gsm48_gmm_sendmsg(msg, 0, mm);
 }
 
+/* Section 9.5.8: Deactivate PDP Context Request */
+static int _gsm48_tx_gsm_deact_pdp_req(struct sgsn_mm_ctx *mm, uint8_t tid,
+					uint8_t sm_cause)
+{
+	struct msgb *msg = gsm48_msgb_alloc();
+	struct gsm48_hdr *gh;
+	uint8_t transaction_id = tid ^ 0x8; /* flip */
+
+	DEBUGP(DMM, "<- DEACTIVATE PDP CONTEXT REQ\n");
+
+	mmctx2msgid(msg, mm);
+
+	gh = (struct gsm48_hdr *) msgb_put(msg, sizeof(*gh));
+	gh->proto_discr = GSM48_PDISC_SM_GPRS | (transaction_id << 4);
+	gh->msg_type = GSM48_MT_GSM_DEACT_PDP_REQ;
+
+	msgb_v_put(msg, sm_cause);
+
+	return gsm48_gmm_sendmsg(msg, 0, mm);
+}
+int gsm48_tx_gsm_deact_pdp_req(struct sgsn_pdp_ctx *pdp, uint8_t sm_cause)
+{
+	pdpctx_timer_start(pdp, 3395, GSM0408_T3395_SECS);
+
+	return _gsm48_tx_gsm_deact_pdp_req(pdp->mm, pdp->ti, sm_cause);
+}
+
 /* Section 9.5.9: Deactivate PDP Context Accept */
 static int _gsm48_tx_gsm_deact_pdp_acc(struct sgsn_mm_ctx *mm, uint8_t tid)
 {
@@ -1341,6 +1393,26 @@
 	return sgsn_delete_pdp_ctx(pdp);
 }
 
+/* Section 9.5.9: Deactivate PDP Context Accept */
+static int gsm48_rx_gsm_deact_pdp_ack(struct sgsn_mm_ctx *mm, struct msgb *msg)
+{
+	struct gsm48_hdr *gh = (struct gsm48_hdr *) msgb_gmmh(msg);
+	uint8_t transaction_id = (gh->proto_discr >> 4);
+	struct sgsn_pdp_ctx *pdp;
+
+	DEBUGP(DMM, "-> DEACTIVATE PDP CONTEXT ACK\n");
+
+	pdp = sgsn_pdp_ctx_by_tid(mm, transaction_id);
+	if (!pdp) {
+		LOGP(DMM, LOGL_NOTICE, "Deactivate PDP Context Accept for "
+			"non-existing PDP Context (IMSI=%s, TI=%u)\n",
+			mm->imsi, transaction_id);
+		return 0;
+	}
+
+	return sgsn_delete_pdp_ctx(pdp);
+}
+
 static int gsm48_rx_gsm_status(struct sgsn_mm_ctx *ctx, struct msgb *msg)
 {
 	struct gsm48_hdr *gh = msgb_l3(msg);
@@ -1351,6 +1423,30 @@
 	return 0;
 }
 
+static void pdpctx_timer_cb(void *_pdp)
+{
+	struct sgsn_pdp_ctx *pdp = _pdp;
+
+	pdp->num_T_exp++;
+
+	switch (pdp->T) {
+	case 3395:	/* waiting for PDP CTX DEACT ACK */
+		if (pdp->num_T_exp >= 4) {
+			LOGP(DMM, LOGL_NOTICE, "T3395 expired >= 5 times\n");
+			pdp->state = PDP_STATE_INACTIVE;
+			sgsn_delete_pdp_ctx(pdp);
+			break;
+		}
+		gsm48_tx_gsm_deact_pdp_req(pdp, GSM_CAUSE_NET_FAIL); 
+		bsc_schedule_timer(&pdp->timer, GSM0408_T3395_SECS, 0);
+		break;
+	default:
+		LOGP(DMM, LOGL_ERROR, "timer expired in unknown mode %u\n",
+			pdp->T);
+	}
+}
+
+
 /* GPRS Session Management */
 static int gsm0408_rcv_gsm(struct sgsn_mm_ctx *mmctx, struct msgb *msg,
 			   struct gprs_llc_llme *llme)
@@ -1373,6 +1469,9 @@
 	case GSM48_MT_GSM_DEACT_PDP_REQ:
 		rc = gsm48_rx_gsm_deact_pdp_req(mmctx, msg);
 		break;
+	case GSM48_MT_GSM_DEACT_PDP_ACK:
+		rc = gsm48_rx_gsm_deact_pdp_ack(mmctx, msg);
+		break;
 	case GSM48_MT_GSM_STATUS:
 		rc = gsm48_rx_gsm_status(mmctx, msg);
 		break;
diff --git a/openbsc/src/gprs/gprs_sgsn.c b/openbsc/src/gprs/gprs_sgsn.c
index 48a00b8..f558003 100644
--- a/openbsc/src/gprs/gprs_sgsn.c
+++ b/openbsc/src/gprs/gprs_sgsn.c
@@ -32,6 +32,8 @@
 #include <openbsc/gprs_ns.h>
 #include <openbsc/gprs_bssgp.h>
 #include <openbsc/sgsn.h>
+#include <openbsc/gsm_04_08_gprs.h>
+#include <openbsc/gprs_gmm.h>
 
 extern struct sgsn_instance *sgsn;
 
@@ -240,6 +242,7 @@
 
 	ggc->id = id;
 	ggc->gtp_version = 1;
+	ggc->remote_restart_ctr = -1;
 	/* if we are called from config file parse, this gsn doesn't exist yet */
 	ggc->gsn = sgsn->gsn;
 	llist_add(&ggc->list, &sgsn_ggsn_ctxts);
@@ -258,6 +261,18 @@
 	return NULL;
 }
 
+struct sgsn_ggsn_ctx *sgsn_ggsn_ctx_by_addr(struct in_addr *addr)
+{
+	struct sgsn_ggsn_ctx *ggc;
+
+	llist_for_each_entry(ggc, &sgsn_ggsn_ctxts, list) {
+		if (!memcmp(addr, &ggc->remote_addr, sizeof(*addr)))
+			return ggc;
+	}
+	return NULL;
+}
+
+
 struct sgsn_ggsn_ctx *sgsn_ggsn_ctx_find_alloc(uint32_t id)
 {
 	struct sgsn_ggsn_ctx *ggc;
@@ -320,3 +335,35 @@
 
 	return ptmsi;
 }
+
+static void drop_one_pdp(struct sgsn_pdp_ctx *pdp)
+{
+	if (pdp->mm->mm_state == GMM_REGISTERED_NORMAL)
+		gsm48_tx_gsm_deact_pdp_req(pdp, GSM_CAUSE_NET_FAIL);
+	else  {
+		/* FIXME: GPRS paging in case MS is SUSPENDED */
+		LOGP(DGPRS, LOGL_NOTICE, "Hard-dropping PDP ctx due to GGSN "
+			"recovery\n");
+		sgsn_pdp_ctx_free(pdp);
+	}
+}
+
+/* High-level function to be called in case a GGSN has disappeared or
+ * ottherwise lost state (recovery procedure) */
+int drop_all_pdp_for_ggsn(struct sgsn_ggsn_ctx *ggsn)
+{
+	struct sgsn_mm_ctx *mm;
+	int num = 0;
+
+	llist_for_each_entry(mm, &sgsn_mm_ctxts, list) {
+		struct sgsn_pdp_ctx *pdp;
+		llist_for_each_entry(pdp, &mm->pdp_list, list) {
+			if (pdp->ggsn == ggsn) {
+				drop_one_pdp(pdp);
+				num++;
+			}
+		}
+	}
+
+	return num;
+}
diff --git a/openbsc/src/gprs/sgsn_libgtp.c b/openbsc/src/gprs/sgsn_libgtp.c
index 5278e4d..e5defe7 100644
--- a/openbsc/src/gprs/sgsn_libgtp.c
+++ b/openbsc/src/gprs/sgsn_libgtp.c
@@ -325,15 +325,38 @@
 }
 
 /* Confirmation of an GTP ECHO request */
-static int echo_conf(int recovery)
+static int echo_conf(struct pdp_t *pdp, void *cbp, int recovery)
 {
 	if (recovery < 0) {
 		DEBUGP(DGPRS, "GTP Echo Request timed out\n");
 		/* FIXME: if version == 1, retry with version 0 */
 	} else {
 		DEBUGP(DGPRS, "GTP Rx Echo Response\n");
-		/* FIXME: check if recovery counter has incremented and
-		 * release all PDP context (if it has) */
+	}
+	return 0;
+}
+
+/* Any message received by GGSN contains a recovery IE */
+static int cb_recovery(struct sockaddr_in *peer, uint8_t recovery)
+{
+	struct sgsn_ggsn_ctx *ggsn;
+	
+	ggsn = sgsn_ggsn_ctx_by_addr(&peer->sin_addr);
+	if (!ggsn) {
+		DEBUGP(DGPRS, "Received Recovery IE for unknown GGSN\n");
+		return -EINVAL;
+	}
+
+	if (ggsn->remote_restart_ctr == -1) {
+		/* First received ECHO RESPONSE, note the restart ctr */
+		ggsn->remote_restart_ctr = recovery;
+	} else if (ggsn->remote_restart_ctr != recovery) {
+		/* counter has changed (GGSN restart): release all PDP */
+		LOGP(DGPRS, LOGL_NOTICE, "GGSN recovery (%u->%u), "
+		     "releasing all PDP contexts\n",
+		     ggsn->remote_restart_ctr, recovery);
+		ggsn->remote_restart_ctr = recovery;
+		drop_all_pdp_for_ggsn(ggsn);
 	}
 	return 0;
 }
@@ -351,7 +374,7 @@
 	switch (type) {
 	case GTP_ECHO_REQ:
 		/* libgtp hands us the RECOVERY number instead of a cause */
-		return echo_conf(cause);
+		return echo_conf(pdp, cbp, cause);
 	case GTP_CREATE_PDP_REQ:
 		return create_pdp_conf(pdp, cbp, cause);
 	case GTP_DELETE_PDP_REQ:
@@ -579,6 +602,7 @@
 	/* Register callbackcs with libgtp */
 	gtp_set_cb_delete_context(gsn, cb_delete_context);
 	gtp_set_cb_conf(gsn, cb_conf);
+	gtp_set_cb_recovery(gsn, cb_recovery);
 	gtp_set_cb_data_ind(gsn, cb_data_ind);
 	gtp_set_cb_unsup_ind(gsn, cb_unsup_ind);
 	gtp_set_cb_extheader_ind(gsn, cb_extheader_ind);