gprs: Track IMSI/TLLI to control APN patching

This patch adds IMSI/TLLI connection tracking and uses it to control
APN patching based on the IMSI. TLLI entries can expire based on age
and/or by limiting the TLLI list size.

VTY config-gbproxy:
  no core-access-point-name                   disable APN patching
  core-access-point-name none                 remove APN if present
  core-access-point-name APN                  replace APN if present
  core-access-point-name none match-imsi RE   remove if IMSI matches
  core-access-point-name APN match-imsi RE    replace if IMSI matches
  tlli-list max-age SECONDS                   expire after SECONDS
  no tlli-list max-age                        don't expire by age
  tlli-list max-length N                      keep N entries only
  no tlli-list max-length                     don't limit list length

RE is an extended regular expression, e.g. ^12345|^23456

Ticket: OW#1192
Sponsored-by: On-Waves ehf
diff --git a/openbsc/include/openbsc/gb_proxy.h b/openbsc/include/openbsc/gb_proxy.h
index 9610ca6..25e7265 100644
--- a/openbsc/include/openbsc/gb_proxy.h
+++ b/openbsc/include/openbsc/gb_proxy.h
@@ -29,7 +29,10 @@
 	int core_mcc;
 	uint8_t* core_apn;
 	size_t core_apn_size;
+	char * match_re;
 	enum gbproxy_patch_mode patch_mode;
+	int tlli_max_age;
+	int tlli_max_len;
 };
 
 extern struct gbproxy_config gbcfg;
@@ -60,4 +63,6 @@
 
 char *gbprox_apn_to_str(char *str, const uint8_t *apn_enc, size_t max_chars);
 int gbprox_str_to_apn(uint8_t *apn_enc, const char *str, size_t max_chars);
+
+int gbprox_set_patch_filter(const char *filter, const char **err_msg);
 #endif
diff --git a/openbsc/src/gprs/gb_proxy.c b/openbsc/src/gprs/gb_proxy.c
index 4f6ac24..8fee4ac 100644
--- a/openbsc/src/gprs/gb_proxy.c
+++ b/openbsc/src/gprs/gb_proxy.c
@@ -29,6 +29,7 @@
 #include <sys/fcntl.h>
 #include <sys/stat.h>
 #include <arpa/inet.h>
+#include <time.h>
 
 #include <osmocom/core/talloc.h>
 #include <osmocom/core/select.h>
@@ -39,6 +40,8 @@
 #include <osmocom/gprs/gprs_ns.h>
 #include <osmocom/gprs/gprs_bssgp.h>
 
+#include <osmocom/gsm/gsm_utils.h>
+
 #include <openbsc/signal.h>
 #include <openbsc/debug.h>
 #include <openbsc/gb_proxy.h>
@@ -128,9 +131,18 @@
 	.ctr_desc = peer_ctr_description,
 };
 
+struct {
+	int check_imsi;
+	regex_t imsi_re_comp;
+} gbprox_global_patch_state = {0,};
+
 struct gbprox_patch_state {
 	int local_mnc;
 	int local_mcc;
+
+	/* List of TLLIs for which patching is enabled */
+	struct llist_head enabled_tllis;
+	int enabled_tllis_count;
 };
 
 struct gbprox_peer {
@@ -155,6 +167,8 @@
 /* Linked list of all Gb peers (except SGSN) */
 static LLIST_HEAD(gbprox_bts_peers);
 
+static void gbprox_delete_tllis(struct gbprox_peer *peer);
+
 /* Find the gbprox_peer by its BVCI */
 static struct gbprox_peer *peer_by_bvci(uint16_t bvci)
 {
@@ -237,6 +251,8 @@
 
 	llist_add(&peer->list, &gbprox_bts_peers);
 
+	INIT_LLIST_HEAD(&peer->patch_state.enabled_tllis);
+
 	return peer;
 }
 
@@ -244,6 +260,9 @@
 {
 	rate_ctr_group_free(peer->ctrg);
 	llist_del(&peer->list);
+
+	gbprox_delete_tllis(peer);
+
 	talloc_free(peer);
 }
 
@@ -378,6 +397,275 @@
 	return len;
 }
 
+struct gbprox_tlli_info {
+	struct llist_head list;
+
+	uint32_t tlli;
+	time_t timestamp;
+	uint8_t *mi_data;
+	size_t mi_data_len;
+};
+
+static struct gbprox_tlli_info *gbprox_find_tlli(struct gbprox_peer *peer,
+						 uint32_t tlli)
+{
+	struct gbprox_tlli_info *tlli_info;
+	struct gbprox_patch_state *state = &peer->patch_state;
+
+	llist_for_each_entry(tlli_info, &state->enabled_tllis, list)
+		if (tlli_info->tlli == tlli)
+			return tlli_info;
+
+	return NULL;
+}
+
+static struct gbprox_tlli_info *gbprox_find_tlli_by_mi(
+	struct gbprox_peer *peer,
+	const uint8_t *mi_data,
+	size_t mi_data_len)
+{
+	struct gbprox_tlli_info *tlli_info;
+	struct gbprox_patch_state *state = &peer->patch_state;
+
+	llist_for_each_entry(tlli_info, &state->enabled_tllis, list) {
+		if (tlli_info->mi_data_len != mi_data_len)
+			continue;
+		if (memcmp(tlli_info->mi_data, mi_data, mi_data_len) != 0)
+			continue;
+
+		return tlli_info;
+	}
+
+	return NULL;
+}
+
+static void gbprox_delete_tlli(struct gbprox_peer *peer,
+			       struct gbprox_tlli_info *tlli_info)
+{
+	struct gbprox_patch_state *state = &peer->patch_state;
+
+	llist_del(&tlli_info->list);
+	talloc_free(tlli_info);
+	state->enabled_tllis_count -= 1;
+}
+
+static void gbprox_delete_tllis(struct gbprox_peer *peer)
+{
+	struct gbprox_tlli_info *tlli_info, *nxt;
+	struct gbprox_patch_state *state = &peer->patch_state;
+
+	llist_for_each_entry_safe(tlli_info, nxt, &state->enabled_tllis, list) {
+		llist_del(&tlli_info->list);
+		talloc_free(tlli_info);
+	}
+
+	OSMO_ASSERT(llist_empty(&state->enabled_tllis));
+}
+
+int gbprox_set_patch_filter(const char *filter, const char **err_msg)
+{
+	static char err_buf[300];
+	int rc;
+
+	if (gbprox_global_patch_state.check_imsi) {
+		regfree(&gbprox_global_patch_state.imsi_re_comp);
+		gbprox_global_patch_state.check_imsi = 0;
+	}
+
+	if (!filter)
+		return 0;
+
+	rc = regcomp(&gbprox_global_patch_state.imsi_re_comp, filter,
+		     REG_EXTENDED | REG_NOSUB | REG_ICASE);
+
+	if (rc == 0) {
+		gbprox_global_patch_state.check_imsi = 1;
+		return 0;
+	}
+
+	if (err_msg) {
+		regerror(rc, &gbprox_global_patch_state.imsi_re_comp,
+			 err_buf, sizeof(err_buf));
+		*err_msg = err_buf;
+	}
+
+	return -1;
+}
+
+static int gbprox_check_imsi(struct gbprox_peer *peer,
+			     const uint8_t *imsi, size_t imsi_len)
+{
+	char mi_buf[200];
+	int rc;
+
+	if (!gbprox_global_patch_state.check_imsi)
+		return 1;
+
+	rc = gsm48_mi_to_string(mi_buf, sizeof(mi_buf), imsi, imsi_len);
+	if (rc < 1) {
+		LOGP(DGPRS, LOGL_NOTICE, "Invalid IMSI %s\n",
+		     osmo_hexdump(imsi, imsi_len));
+		return -1;
+	}
+
+	LOGP(DGPRS, LOGL_DEBUG, "Checking IMSI '%s' (%d)\n", mi_buf, rc);
+
+	rc = regexec(&gbprox_global_patch_state.imsi_re_comp, mi_buf, 0, NULL, 0);
+	if (rc == REG_NOMATCH) {
+		LOGP(DGPRS, LOGL_INFO,
+		       "IMSI '%s' doesn't match pattern '%s'\n",
+		       mi_buf, gbcfg.match_re);
+		return 0;
+	}
+
+	return 1;
+}
+
+static int gbprox_remove_stale_ttlis(struct gbprox_peer *peer, time_t now)
+{
+	struct gbprox_patch_state *state = &peer->patch_state;
+	struct gbprox_tlli_info *tlli_info = NULL, *nxt;
+	int count = 0;
+	int deleted_count = 0;
+
+	llist_for_each_entry_safe(tlli_info, nxt, &state->enabled_tllis, list) {
+		int is_stale = 0;
+		time_t age = now - tlli_info->timestamp;
+
+		count += 1;
+
+		if (gbcfg.tlli_max_len > 0)
+			is_stale = is_stale || count > gbcfg.tlli_max_len;
+
+		if (gbcfg.tlli_max_age > 0)
+			is_stale = is_stale || age > gbcfg.tlli_max_age;
+
+		if (!is_stale)
+			continue;
+
+		LOGP(DGPRS, LOGL_INFO,
+		     "Removing TLLI %08x from list (stale)\n",
+		     tlli_info->tlli);
+
+		gbprox_delete_tlli(peer, tlli_info);
+		tlli_info = NULL;
+
+		deleted_count += 1;
+	}
+
+	return deleted_count;
+}
+
+static void gbprox_register_tlli(struct gbprox_peer *peer, uint32_t tlli,
+				 const uint8_t *imsi, size_t imsi_len)
+{
+	struct gbprox_patch_state *state = &peer->patch_state;
+	struct gbprox_tlli_info *tlli_info;
+	int enable_patching;
+	time_t now = 0;
+
+	if (gprs_tlli_type(tlli) != TLLI_LOCAL)
+		return;
+
+	if (!imsi || (imsi[0] & GSM_MI_TYPE_MASK) != GSM_MI_TYPE_IMSI)
+		return;
+
+	if (!gbprox_global_patch_state.check_imsi)
+		return;
+
+	tlli_info = gbprox_find_tlli(peer, tlli);
+
+	/* Check, whether the IMSI matches */
+	enable_patching = gbprox_check_imsi(peer, imsi, imsi_len);
+
+	if (enable_patching < 0)
+		return;
+
+	if (!tlli_info) {
+		tlli_info = gbprox_find_tlli_by_mi(peer, imsi, imsi_len);
+
+		if (tlli_info) {
+			/* TLLI has changed somehow, adjust it */
+			LOGP(DGPRS, LOGL_INFO,
+			     "The TLLI has changed from %08x to %08x\n",
+			     tlli_info->tlli, tlli);
+			tlli_info->tlli = tlli;
+		}
+	}
+
+	if (!tlli_info) {
+		if (!enable_patching)
+			return;
+
+		LOGP(DGPRS, LOGL_INFO, "Adding TLLI %08x to list\n", tlli);
+		tlli_info = talloc_zero(peer, struct gbprox_tlli_info);
+		tlli_info->tlli = tlli;
+	} else {
+		llist_del(&tlli_info->list);
+		OSMO_ASSERT(state->enabled_tllis_count > 0);
+		state->enabled_tllis_count -= 1;
+	}
+
+	OSMO_ASSERT(tlli_info != NULL);
+
+	if (enable_patching) {
+		now = time(NULL);
+
+		tlli_info->timestamp = now;
+		llist_add(&tlli_info->list, &state->enabled_tllis);
+		state->enabled_tllis_count += 1;
+
+		gbprox_remove_stale_ttlis(peer, now);
+
+		if (tlli_info != llist_entry(state->enabled_tllis.next,
+					     struct gbprox_tlli_info, list)) {
+			LOGP(DGPRS, LOGL_ERROR,
+			     "Unexpectedly removed new TLLI entry as stale, "
+			     "TLLI %08x\n", tlli);
+			tlli_info = NULL;
+		}
+	} else {
+		LOGP(DGPRS, LOGL_INFO,
+		     "Removing TLLI %08x from list (patching no longer enabled)\n",
+		     tlli);
+		talloc_free(tlli_info);
+		tlli_info = NULL;
+	}
+
+	if (tlli_info) {
+		tlli_info->mi_data_len = imsi_len;
+		tlli_info->mi_data =
+			talloc_realloc_size(tlli_info, tlli_info->mi_data, imsi_len);
+		OSMO_ASSERT(tlli_info->mi_data != NULL);
+		memcpy(tlli_info->mi_data, imsi, imsi_len);
+	}
+}
+
+static void gbprox_unregister_tlli(struct gbprox_peer *peer, uint32_t tlli)
+{
+	struct gbprox_tlli_info *tlli_info;
+
+	tlli_info = gbprox_find_tlli(peer, tlli);
+	if (tlli_info) {
+		LOGP(DGPRS, LOGL_INFO,
+		     "Removing TLLI %08x from list\n",
+		     tlli);
+		llist_del(&tlli_info->list);
+		talloc_free(tlli_info);
+	}
+}
+
+static int gbprox_check_tlli(struct gbprox_peer *peer, uint32_t tlli)
+{
+	LOGP(DGPRS, LOGL_INFO, "Checking TLLI %08x, class: %d\n",
+	     tlli, gprs_tlli_type(tlli));
+	if (gprs_tlli_type(tlli) != TLLI_LOCAL)
+		return 0;
+
+	return !gbprox_global_patch_state.check_imsi ||
+		gbprox_find_tlli(peer, tlli) != NULL;
+}
+
 /* check whether patching is enabled at this level */
 static int patching_is_enabled(enum gbproxy_patch_mode need_at_least)
 {
@@ -715,7 +1003,7 @@
 
 static int gbprox_patch_dtap(struct msgb *msg, uint8_t *data, size_t data_len,
 			     struct gbprox_peer *peer, int to_bss,
-			     int *len_change)
+			     uint32_t tlli, int *len_change)
 {
 	struct gsm48_hdr *g48h;
 
@@ -767,8 +1055,16 @@
 			break;
 		if (gbcfg.core_apn == NULL)
 			break;
+		if (!gbprox_check_tlli(peer, tlli))
+			break;
 		return gbprox_patch_gsm_act_pdp_req(msg, data, data_len,
 						    peer, to_bss, len_change);
+
+	case GSM48_MT_GMM_DETACH_ACK:
+	case GSM48_MT_GMM_DETACH_REQ:
+		gbprox_unregister_tlli(peer, tlli);
+		break;
+
 	default:
 		break;
 	};
@@ -777,7 +1073,9 @@
 }
 
 static void gbprox_patch_llc(struct msgb *msg, uint8_t *llc, size_t llc_len,
-			     struct gbprox_peer *peer, int to_bss)
+			     struct gbprox_peer *peer, int to_bss,
+			     struct bssgp_ud_hdr *budh,
+			     struct tlv_parsed *bssgp_tp)
 {
 	struct gprs_llc_hdr_parsed ghp = {0};
 	int rc;
@@ -787,6 +1085,7 @@
 	int len_change = 0;
 	const char *err_info = NULL;
 	int err_ctr = -1;
+	uint32_t tlli = budh ? ntohl(budh->tlli) : 0;
 
 	/* parse LLC */
 	rc = gprs_llc_hdr_parse(&ghp, llc, llc_len);
@@ -806,6 +1105,12 @@
 	if (ghp.sapi != GPRS_SAPI_GMM)
 		return;
 
+	if (gbcfg.core_apn && to_bss && tlli &&
+	    TLVP_PRESENT(bssgp_tp, BSSGP_IE_IMSI))
+		gbprox_register_tlli(peer, tlli,
+				     TLVP_VAL(bssgp_tp, BSSGP_IE_IMSI),
+				     TLVP_LEN(bssgp_tp, BSSGP_IE_IMSI));
+
 	if (ghp.cmd != GPRS_LLC_UI)
 		return;
 
@@ -829,7 +1134,7 @@
 	data_len = ghp.data_len;
 
 	rc = gbprox_patch_dtap(msg, data, data_len, peer, to_bss,
-			       &len_change);
+			       tlli, &len_change);
 
 	if (rc > 0) {
 		llc_len += len_change;
@@ -875,10 +1180,9 @@
 				       struct gbprox_peer *peer, int to_bss)
 {
 	struct bssgp_normal_hdr *bgph;
-	struct bssgp_ud_hdr *budh;
+	struct bssgp_ud_hdr *budh = NULL;
 	struct tlv_parsed tp;
 	uint8_t pdu_type;
-	struct gbprox_patch_state *state = NULL;
 	uint8_t *data;
 	size_t data_len;
 
@@ -886,11 +1190,12 @@
 		return;
 
 	bgph = (struct bssgp_normal_hdr *) msgb_bssgph(msg);
-	budh = (struct bssgp_ud_hdr *) msgb_bssgph(msg);
 	pdu_type = bgph->pdu_type;
 
 	if (pdu_type == BSSGP_PDUT_UL_UNITDATA ||
 	    pdu_type == BSSGP_PDUT_DL_UNITDATA) {
+		budh = (struct bssgp_ud_hdr *) msgb_bssgph(msg);
+		bgph = NULL;
 		data = budh->data;
 		data_len = msgb_bssgp_len(msg) - sizeof(*budh);
 	} else {
@@ -919,11 +1224,6 @@
 		return;
 	}
 
-	state = &peer->patch_state;
-
-	if (to_bss && !state->local_mcc && !state->local_mnc)
-		return;
-
 	if (TLVP_PRESENT(&tp, BSSGP_IE_ROUTEING_AREA)) {
 		gbprox_patch_raid((uint8_t *)TLVP_VAL(&tp, BSSGP_IE_ROUTEING_AREA),
 				  peer, to_bss, "ROUTING_AREA");
@@ -937,7 +1237,7 @@
 	    patching_is_enabled(GBPROX_PATCH_LLC_ATTACH_REQ)) {
 		uint8_t *llc = (uint8_t *)TLVP_VAL(&tp, BSSGP_IE_LLC_PDU);
 		size_t llc_len = TLVP_LEN(&tp, BSSGP_IE_LLC_PDU);
-		gbprox_patch_llc(msg, llc, llc_len, peer, to_bss);
+		gbprox_patch_llc(msg, llc, llc_len, peer, to_bss, budh, &tp);
 		/* Note that the tp struct might contain invalid pointers here
 		 * if the LLC field has changed its size */
 	}
diff --git a/openbsc/src/gprs/gb_proxy_vty.c b/openbsc/src/gprs/gb_proxy_vty.c
index 6d24165..36d959a 100644
--- a/openbsc/src/gprs/gb_proxy_vty.c
+++ b/openbsc/src/gprs/gb_proxy_vty.c
@@ -72,15 +72,24 @@
 	if (g_cfg->core_apn != NULL) {
 	       if (g_cfg->core_apn_size > 0) {
 		       char str[500] = {0};
-		       vty_out(vty, " core-access-point-name %s%s",
+		       vty_out(vty, " core-access-point-name %s",
 			       gbprox_apn_to_str(str, g_cfg->core_apn,
-						 g_cfg->core_apn_size),
-			       VTY_NEWLINE);
+						 g_cfg->core_apn_size));
 	       } else {
-		       vty_out(vty, " core-access-point-name%s",
-			       VTY_NEWLINE);
+		       vty_out(vty, " core-access-point-name none");
 	       }
+	       if (g_cfg->match_re)
+		       vty_out(vty, " match-imsi %s%s",
+			       g_cfg->match_re, VTY_NEWLINE);
+	       else
+		       vty_out(vty, "%s", VTY_NEWLINE);
 	}
+	if (g_cfg->tlli_max_age > 0)
+		vty_out(vty, " tlli-list max-age %d%s",
+			g_cfg->tlli_max_age, VTY_NEWLINE);
+	if (g_cfg->tlli_max_len > 0)
+		vty_out(vty, " tlli-list max-length %d%s",
+			g_cfg->tlli_max_len, VTY_NEWLINE);
 
 	if (g_cfg->patch_mode != GBPROX_PATCH_DEFAULT)
 		vty_out(vty, " patch-mode %s%s",
@@ -153,50 +162,140 @@
 }
 
 #define GBPROXY_CORE_APN_STR "Use this access point name (APN) for the backbone\n"
+#define GBPROXY_CORE_APN_ARG_STR "Replace APN by this string\n" "Remove APN\n"
 
-DEFUN(cfg_gbproxy_core_apn_remove,
-      cfg_gbproxy_core_apn_remove_cmd,
-      "core-access-point-name",
-      GBPROXY_CORE_APN_STR)
+static int set_core_apn(struct vty *vty, const char *apn, const char *filter)
 {
-	talloc_free(g_cfg->core_apn);
-	/* TODO: replace NULL */
-	g_cfg->core_apn = talloc_zero_size(NULL, 2);
-	g_cfg->core_apn_size = 0;
-	return CMD_SUCCESS;
-}
+	const char *err_msg = NULL;
+	int apn_len;
 
-DEFUN(cfg_gbproxy_core_apn,
-      cfg_gbproxy_core_apn_cmd,
-      "core-access-point-name APN",
-      GBPROXY_CORE_APN_STR "Replacement APN\n")
-{
-	int apn_len = strlen(argv[0]) + 1;
+	if (!apn) {
+		talloc_free(g_cfg->core_apn);
+		g_cfg->core_apn = NULL;
+		g_cfg->core_apn_size = 0;
+		gbprox_set_patch_filter(NULL, NULL);
+		return CMD_SUCCESS;
+	}
 
-	if (apn_len > 100) {
+	apn_len = strlen(apn);
+
+	if (apn_len >= 100) {
 		vty_out(vty, "APN string too long (max 99 chars)%s",
 			VTY_NEWLINE);
 		return CMD_WARNING;
 	}
 
-	/* TODO: replace NULL */
-	g_cfg->core_apn = talloc_realloc_size(NULL, g_cfg->core_apn, apn_len);
-	g_cfg->core_apn_size = gbprox_str_to_apn(g_cfg->core_apn, argv[0], apn_len);
+	if (!filter) {
+		gbprox_set_patch_filter(NULL, NULL);
+	} else if (gbprox_set_patch_filter(filter, &err_msg) != 0) {
+		vty_out(vty, "Match expression invalid: %s%s",
+			err_msg, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	talloc_free(g_cfg->match_re);
+	if (filter)
+		/* TODO: replace NULL */
+		g_cfg->match_re = talloc_strdup(NULL, filter);
+	else
+		g_cfg->match_re = NULL;
+
+	if (apn_len == 0) {
+		talloc_free(g_cfg->core_apn);
+		/* TODO: replace NULL */
+		g_cfg->core_apn = talloc_zero_size(NULL, 2);
+		g_cfg->core_apn_size = 0;
+	} else {
+		/* TODO: replace NULL */
+		g_cfg->core_apn =
+			talloc_realloc_size(NULL, g_cfg->core_apn, apn_len + 1);
+		g_cfg->core_apn_size =
+			gbprox_str_to_apn(g_cfg->core_apn, apn, apn_len + 1);
+	}
 
 	return CMD_SUCCESS;
 }
 
+DEFUN(cfg_gbproxy_core_apn,
+      cfg_gbproxy_core_apn_cmd,
+      "core-access-point-name (APN|none)",
+      GBPROXY_CORE_APN_STR GBPROXY_CORE_APN_ARG_STR)
+{
+	if (strcmp(argv[0], "none") == 0)
+		return set_core_apn(vty, "", NULL);
+	else
+		return set_core_apn(vty, argv[0], NULL);
+}
+
+DEFUN(cfg_gbproxy_core_apn_match,
+      cfg_gbproxy_core_apn_match_cmd,
+      "core-access-point-name (APN|none) match-imsi .REGEXP",
+      GBPROXY_CORE_APN_STR GBPROXY_CORE_APN_ARG_STR
+      "Only modify if the IMSI matches\n"
+      "Regular expression for the match\n")
+{
+	if (strcmp(argv[0], "none") == 0)
+		return set_core_apn(vty, "", argv[1]);
+	else
+		return set_core_apn(vty, argv[0], argv[1]);
+}
+
 DEFUN(cfg_gbproxy_no_core_apn,
       cfg_gbproxy_no_core_apn_cmd,
       "no core-access-point-name",
       NO_STR GBPROXY_CORE_APN_STR)
 {
-	talloc_free(g_cfg->core_apn);
-	g_cfg->core_apn = NULL;
-	g_cfg->core_apn_size = 0;
+	return set_core_apn(vty, NULL, NULL);
+}
+
+#define GBPROXY_TLLI_LIST_STR "Set TLLI list parameters\n"
+#define GBPROXY_MAX_AGE_STR "Limit maximum age\n"
+
+DEFUN(cfg_gbproxy_tlli_list_max_age,
+      cfg_gbproxy_tlli_list_max_age_cmd,
+      "tlli-list max-age <1-999999>",
+      GBPROXY_TLLI_LIST_STR GBPROXY_MAX_AGE_STR
+      "Maximum age in seconds\n")
+{
+	g_cfg->tlli_max_age = atoi(argv[0]);
+
 	return CMD_SUCCESS;
 }
 
+DEFUN(cfg_gbproxy_tlli_list_no_max_age,
+      cfg_gbproxy_tlli_list_no_max_age_cmd,
+      "no tlli-list max-age",
+      NO_STR GBPROXY_TLLI_LIST_STR GBPROXY_MAX_AGE_STR)
+{
+	g_cfg->tlli_max_age = 0;
+
+	return CMD_SUCCESS;
+}
+
+#define GBPROXY_MAX_LEN_STR "Limit list length\n"
+
+DEFUN(cfg_gbproxy_tlli_list_max_len,
+      cfg_gbproxy_tlli_list_max_len_cmd,
+      "tlli-list max-length <1-99999>",
+      GBPROXY_TLLI_LIST_STR GBPROXY_MAX_LEN_STR
+      "Maximum number of TLLIs in the list\n")
+{
+	g_cfg->tlli_max_len = atoi(argv[0]);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_gbproxy_tlli_list_no_max_len,
+      cfg_gbproxy_tlli_list_no_max_len_cmd,
+      "no tlli-list max-length",
+      NO_STR GBPROXY_TLLI_LIST_STR GBPROXY_MAX_LEN_STR)
+{
+	g_cfg->tlli_max_len = 0;
+
+	return CMD_SUCCESS;
+}
+
+
 DEFUN(cfg_gbproxy_patch_mode,
       cfg_gbproxy_patch_mode_cmd,
       "patch-mode (default|bssgp|llc-attach-req|llc-attach|llc-gmm|llc-gsm|llc)",
@@ -231,11 +330,15 @@
 	install_element(GBPROXY_NODE, &cfg_nsip_sgsn_nsei_cmd);
 	install_element(GBPROXY_NODE, &cfg_gbproxy_core_mcc_cmd);
 	install_element(GBPROXY_NODE, &cfg_gbproxy_core_mnc_cmd);
-	install_element(GBPROXY_NODE, &cfg_gbproxy_core_apn_remove_cmd);
 	install_element(GBPROXY_NODE, &cfg_gbproxy_core_apn_cmd);
+	install_element(GBPROXY_NODE, &cfg_gbproxy_core_apn_match_cmd);
+	install_element(GBPROXY_NODE, &cfg_gbproxy_tlli_list_max_age_cmd);
+	install_element(GBPROXY_NODE, &cfg_gbproxy_tlli_list_max_len_cmd);
 	install_element(GBPROXY_NODE, &cfg_gbproxy_no_core_mcc_cmd);
 	install_element(GBPROXY_NODE, &cfg_gbproxy_no_core_mnc_cmd);
 	install_element(GBPROXY_NODE, &cfg_gbproxy_no_core_apn_cmd);
+	install_element(GBPROXY_NODE, &cfg_gbproxy_tlli_list_no_max_age_cmd);
+	install_element(GBPROXY_NODE, &cfg_gbproxy_tlli_list_no_max_len_cmd);
 	install_element(GBPROXY_NODE, &cfg_gbproxy_patch_mode_cmd);
 
 	return 0;
diff --git a/openbsc/tests/gbproxy/gbproxy_test.c b/openbsc/tests/gbproxy/gbproxy_test.c
index fa7e0bb..a9d0655 100644
--- a/openbsc/tests/gbproxy/gbproxy_test.c
+++ b/openbsc/tests/gbproxy/gbproxy_test.c
@@ -842,6 +842,7 @@
 		{.mcc = 123, .mnc = 456, .lac = 16464, .rac = 96};
 	struct  gprs_ra_id rai_unknown =
 		{.mcc = 1, .mnc = 99, .lac = 99, .rac = 96};
+	const char *err_msg = NULL;
 
 	bssgp_nsi = nsi;
 	gbcfg.nsi = bssgp_nsi;
@@ -854,6 +855,14 @@
 	configure_sgsn_peer(&sgsn_peer);
 	configure_bss_peers(bss_peer, ARRAY_SIZE(bss_peer));
 
+	gbcfg.match_re = talloc_strdup(NULL, "^9898|^121314");
+	if (gbprox_set_patch_filter(gbcfg.match_re, &err_msg) != 0) {
+		fprintf(stderr, "Failed to compile RE '%s': %s\n",
+			gbcfg.match_re, err_msg);
+		exit(1);
+	}
+
+
 	printf("=== %s ===\n", __func__);
 	printf("--- Initialise SGSN ---\n\n");
 
@@ -917,7 +926,7 @@
 
 	printf("--- Bad cases ---\n\n");
 
-	printf("Invalid BVCI, shouldn't patch\n");
+	printf("TLLI is already detached, shouldn't patch\n");
 	send_ns_unitdata(nsi, "ACT PDP CTX REQ", &bss_peer[0], 0x1002,
 			 bssgp_act_pdp_ctx_req, sizeof(bssgp_act_pdp_ctx_req));
 
diff --git a/openbsc/tests/gbproxy/gbproxy_test.ok b/openbsc/tests/gbproxy/gbproxy_test.ok
index da5cd41..fb8991c 100644
--- a/openbsc/tests/gbproxy/gbproxy_test.ok
+++ b/openbsc/tests/gbproxy/gbproxy_test.ok
@@ -1680,18 +1680,18 @@
     APN patched                     : 2
 --- Bad cases ---
 
-Invalid BVCI, shouldn't patch
+TLLI is already detached, shouldn't patch
 PROCESSING ACT PDP CTX REQ from 0x01020304:1111
 00 00 10 02 01 ef e2 b7 00 00 00 04 08 88 11 22 33 40 50 60 75 30 00 80 0e 00 35 01 c0 0d 0a 41 05 03 0c 00 00 1f 10 00 00 00 00 00 00 00 00 02 01 21 28 03 02 61 62 27 14 80 80 21 10 01 00 00 10 81 06 00 00 00 00 83 06 00 00 00 00 5a ff 02 
 
 CALLBACK, event 0, msg length 76, bvci 0x1002
 00 00 10 02 01 ef e2 b7 00 00 00 04 08 88 11 22 33 40 50 60 75 30 00 80 0e 00 35 01 c0 0d 0a 41 05 03 0c 00 00 1f 10 00 00 00 00 00 00 00 00 02 01 21 28 03 02 61 62 27 14 80 80 21 10 01 00 00 10 81 06 00 00 00 00 83 06 00 00 00 00 5a ff 02 
 
-NS UNITDATA MESSAGE to SGSN, BVCI 0x1002, msg length 71 (gprs_ns_sendmsg)
-MESSAGE to SGSN at 0x05060708:32000, msg length 75
-00 00 10 02 01 ef e2 b7 00 00 00 04 08 88 21 63 54 40 50 60 75 30 00 80 0e 00 30 01 c0 0d 0a 41 05 03 0c 00 00 1f 10 00 00 00 00 00 00 00 00 02 01 21 27 14 80 80 21 10 01 00 00 10 81 06 00 00 00 00 83 06 00 00 00 00 85 fa 60 
+NS UNITDATA MESSAGE to SGSN, BVCI 0x1002, msg length 76 (gprs_ns_sendmsg)
+MESSAGE to SGSN at 0x05060708:32000, msg length 80
+00 00 10 02 01 ef e2 b7 00 00 00 04 08 88 21 63 54 40 50 60 75 30 00 80 0e 00 35 01 c0 0d 0a 41 05 03 0c 00 00 1f 10 00 00 00 00 00 00 00 00 02 01 21 28 03 02 61 62 27 14 80 80 21 10 01 00 00 10 81 06 00 00 00 00 83 06 00 00 00 00 5a ff 02 
 
-result (ACT PDP CTX REQ) = 75
+result (ACT PDP CTX REQ) = 80
 
 Invalid RAI, shouldn't patch
 PROCESSING BVC_SUSPEND_ACK from 0x05060708:32000
@@ -1713,6 +1713,6 @@
   NSEI 4096, BVCI 4098, not blocked, RAI 112-332-16464-96
     RAID patched              (BSS ): 10
     RAID patched              (SGSN): 3
-    APN patched                     : 3
+    APN patched                     : 2
 ===== GbProxy test END