mgw: Allow auditing speciall 'null' endpoint

This is a special endpoint which can always be audited. This is useful
for clients who wish to submit requests to osmo-mgw periodically to find
out whether the MGW is still reachable. This endpoint will be used by
libomso-mgcp-client as default target endpoint to implement such
feature.
This "null" Endpoint is osmo-mgw specific, not described in MGCP specs.

Related: SYS#6481
Change-Id: Ia409b16e9211e6261e2e0f21288544289d6f3733
diff --git a/doc/manuals/chapters/mgcp_endpoints.adoc b/doc/manuals/chapters/mgcp_endpoints.adoc
index 797fdea..2fb6e16 100644
--- a/doc/manuals/chapters/mgcp_endpoints.adoc
+++ b/doc/manuals/chapters/mgcp_endpoints.adoc
@@ -91,4 +91,10 @@
 not yet useable.
 
 NOTE: the VTY command "show mgcp" can be used to get a list of all available
-endpoints (including identifiers)
\ No newline at end of file
+endpoints (including identifiers)
+
+=== The `null` endpoint
+
+OsmoMGW offers a special `null@<domain>` endpoint which can be audited at all times.
+This is useful for MGCP clients who wish to submit requests to OsmoMGW
+periodically to find out whether it is still reachable and in a working state.
\ No newline at end of file
diff --git a/include/osmocom/mgcp/mgcp_endp.h b/include/osmocom/mgcp/mgcp_endp.h
index 08b966f..7bf034b 100644
--- a/include/osmocom/mgcp/mgcp_endp.h
+++ b/include/osmocom/mgcp/mgcp_endp.h
@@ -133,6 +133,7 @@
 int mgcp_endp_claim(struct mgcp_endpoint *endp, const char *callid);
 void mgcp_endp_update(struct mgcp_endpoint *endp);
 bool mgcp_endp_is_wildcarded(const char *epname);
+bool mgcp_endp_is_null(const char *epname);
 struct mgcp_endpoint *mgcp_endp_by_name_trunk(int *cause, const char *epname,
 					      const struct mgcp_trunk *trunk);
 struct mgcp_endpoint *mgcp_endp_by_name(int *cause, const char *epname,
diff --git a/src/libosmo-mgcp/mgcp_endp.c b/src/libosmo-mgcp/mgcp_endp.c
index 2bf15e0..20088b7 100644
--- a/src/libosmo-mgcp/mgcp_endp.c
+++ b/src/libosmo-mgcp/mgcp_endp.c
@@ -229,6 +229,17 @@
 	return false;
 }
 
+/*! Check if the given epname refers to a "null" endpoint.
+ *  \param[in] epname endpoint name to check
+ *  \returns true if epname refers to "null"" endpoint, else false. */
+bool mgcp_endp_is_null(const char *epname)
+{
+	if (strncasecmp(epname, "null@", 5) == 0)
+		return true;
+
+	return false;
+}
+
 /*! Find an endpoint by its name on a specified trunk.
  *  \param[out] cause pointer to store cause code, can be NULL.
  *  \param[in] epname endpoint name to lookup.
diff --git a/src/libosmo-mgcp/mgcp_protocol.c b/src/libosmo-mgcp/mgcp_protocol.c
index 1fa345b..80e0f8a 100644
--- a/src/libosmo-mgcp/mgcp_protocol.c
+++ b/src/libosmo-mgcp/mgcp_protocol.c
@@ -86,6 +86,9 @@
 	/* set to true when the request has been classified as wildcarded */
 	bool wildcarded;
 
+	/* Set to true when the request is targeted at the "null" endpoint */
+	bool null_endp;
+
 	/* contains cause code in case of problems during endp/trunk resolution */
 	int mgcp_cause;
 };
@@ -390,7 +393,10 @@
 	/* Locate endpoint and trunk, if no endpoint can be located try at least to identify the trunk. */
 	rq.pdata = &pdata;
 	rq.wildcarded = mgcp_endp_is_wildcarded(pdata.epname);
-	rq.endp = mgcp_endp_by_name(&rc, pdata.epname, pdata.cfg);
+	if (!rq.wildcarded)
+		rq.null_endp = mgcp_endp_is_null(pdata.epname);
+	if (!rq.null_endp)
+		rq.endp = mgcp_endp_by_name(&rc, pdata.epname, pdata.cfg);
 	rq.mgcp_cause = rc;
 	if (!rq.endp) {
 		rate_ctr_inc(rate_ctr_group_get_ctr(rate_ctrs, MGCP_GENERAL_RX_FAIL_NO_ENDPOINT));
@@ -407,14 +413,14 @@
 				     rq.name, pdata.epname);
 				return create_err_response(cfg, NULL, -rq.mgcp_cause, rq.name, pdata.trans);
 			}
-		} else {
+		} else if (!rq.null_endp) {
 			/* If the endpoint name suggests that the request refers to a specific endpoint, then the
 			 * request cannot be handled and we must stop early. */
 			LOGP(DLMGCP, LOGL_NOTICE,
 			     "%s: cannot find endpoint \"%s\", cause=%d -- abort\n", rq.name,
 			     pdata.epname, -rq.mgcp_cause);
 			return create_err_response(cfg, NULL, -rq.mgcp_cause, rq.name, pdata.trans);
-		}
+		} /* else: Handle special "null" endpoint below (with rq.endp=NULL, rq.trunk=NULL) */
 	} else {
 		osmo_strlcpy(debug_last_endpoint_name, rq.endp->name, sizeof(debug_last_endpoint_name));
 		rq.trunk = rq.endp->trunk;
@@ -460,6 +466,11 @@
 static struct msgb *handle_audit_endpoint(struct mgcp_request_data *rq)
 {
 	LOGPENDP(rq->endp, DLMGCP, LOGL_NOTICE, "AUEP: auditing endpoint ...\n");
+
+	/* Auditing "null" endpoint is allowed for keepalive purposes. There's no rq->endp nor rq->trunk in this case. */
+	if (rq->null_endp)
+		return create_ok_response(rq->pdata->cfg, NULL, 200, "AUEP", rq->pdata->trans);
+
 	if (!rq->endp || !mgcp_endp_avail(rq->endp)) {
 		LOGPENDP(rq->endp, DLMGCP, LOGL_ERROR, "AUEP: selected endpoint not available!\n");
 		return create_err_response(rq->trunk, NULL, 501, "AUEP", rq->pdata->trans);
@@ -855,7 +866,7 @@
 	struct mgcp_parse_data *pdata = rq->pdata;
 	struct mgcp_trunk *trunk = rq->trunk;
 	struct mgcp_endpoint *endp = rq->endp;
-	struct rate_ctr_group *rate_ctrs = trunk->ratectr.mgcp_crcx_ctr_group;
+	struct rate_ctr_group *rate_ctrs;
 	int error_code = 400;
 	const char *local_options = NULL;
 	const char *callid = NULL;
@@ -869,6 +880,14 @@
 
 	LOGPENDP(endp, DLMGCP, LOGL_NOTICE, "CRCX: creating new connection ...\n");
 
+	if (rq->null_endp) {
+		/* trunk not available so rate_ctr aren't available either. */
+		LOGP(DLMGCP, LOGL_ERROR, "CRCX: Not allowed in 'null' endpoint!\n");
+		return create_err_response(pdata->cfg, NULL, 502, "CRCX", pdata->trans);
+	}
+
+	rate_ctrs = trunk->ratectr.mgcp_crcx_ctr_group;
+
 	/* we must have a free ep */
 	if (!endp) {
 		rate_ctr_inc(rate_ctr_group_get_ctr(rate_ctrs, MGCP_CRCX_FAIL_AVAIL));
@@ -1134,7 +1153,7 @@
 	struct mgcp_parse_data *pdata = rq->pdata;
 	struct mgcp_trunk *trunk = rq->trunk;
 	struct mgcp_endpoint *endp = rq->endp;
-	struct rate_ctr_group *rate_ctrs = trunk->ratectr.mgcp_mdcx_ctr_group;
+	struct rate_ctr_group *rate_ctrs;
 	char new_local_addr[INET6_ADDRSTRLEN];
 	int error_code = 500;
 	int silent = 0;
@@ -1149,6 +1168,14 @@
 
 	LOGPENDP(endp, DLMGCP, LOGL_NOTICE, "MDCX: modifying existing connection ...\n");
 
+	if (rq->null_endp) {
+		/* trunk not available so rate_ctr aren't available either. */
+		LOGP(DLMGCP, LOGL_ERROR, "MDCX: Not allowed in 'null' endpoint!\n");
+		return create_err_response(pdata->cfg, NULL, 502, "MDCX", pdata->trans);
+	}
+
+	rate_ctrs = trunk->ratectr.mgcp_mdcx_ctr_group;
+
 	/* Prohibit wildcarded requests */
 	if (rq->wildcarded) {
 		LOGPENDP(endp, DLMGCP, LOGL_ERROR,
@@ -1360,7 +1387,7 @@
 	struct mgcp_parse_data *pdata = rq->pdata;
 	struct mgcp_trunk *trunk = rq->trunk;
 	struct mgcp_endpoint *endp = rq->endp;
-	struct rate_ctr_group *rate_ctrs = trunk->ratectr.mgcp_dlcx_ctr_group;
+	struct rate_ctr_group *rate_ctrs;
 	int error_code = 400;
 	int silent = 0;
 	char *line;
@@ -1370,10 +1397,18 @@
 	unsigned int i;
 
 	/* NOTE: In this handler we can not take it for granted that the endp
-	 * pointer will be populated, however a trunk is always guaranteed. */
+	 * pointer will be populated, however a trunk is always guaranteed (except for 'null' endp).
+	 */
 
 	LOGPEPTR(endp, trunk, DLMGCP, LOGL_NOTICE, "DLCX: deleting connection(s) ...\n");
 
+	if (rq->null_endp) {
+		/* trunk not available so rate_ctr aren't available either. */
+		LOGP(DLMGCP, LOGL_ERROR, "DLCX: Not allowed in 'null' endpoint!\n");
+		return create_err_response(pdata->cfg, NULL, 502, "DLCX", pdata->trans);
+	}
+
+	rate_ctrs = trunk->ratectr.mgcp_dlcx_ctr_group;
 	if (endp && !mgcp_endp_avail(endp)) {
 		rate_ctr_inc(rate_ctr_group_get_ctr(rate_ctrs, MGCP_DLCX_FAIL_AVAIL));
 		LOGPENDP(endp, DLMGCP, LOGL_ERROR,
@@ -1524,6 +1559,12 @@
 
 	LOGP(DLMGCP, LOGL_NOTICE, "RSIP: resetting all endpoints ...\n");
 
+	if (rq->null_endp) {
+		/* trunk not available so rate_ctr aren't available either. */
+		LOGP(DLMGCP, LOGL_ERROR, "RSIP: Not allowed in 'null' endpoint!\n");
+		return create_err_response(rq->pdata->cfg, NULL, 502, "RSIP", rq->pdata->trans);
+	}
+
 	if (rq->pdata->cfg->reset_cb)
 		rq->pdata->cfg->reset_cb(rq->endp->trunk);
 	return NULL;
@@ -1549,6 +1590,12 @@
 
 	LOGP(DLMGCP, LOGL_NOTICE, "RQNT: processing request for notification ...\n");
 
+	if (rq->null_endp) {
+		/* trunk not available so rate_ctr aren't available either. */
+		LOGP(DLMGCP, LOGL_ERROR, "RQNT: Not allowed in 'null' endpoint!\n");
+		return create_err_response(rq->pdata->cfg, NULL, 502, "RQNT", rq->pdata->trans);
+	}
+
 	for_each_line(line, rq->pdata->save) {
 		switch (toupper(line[0])) {
 		case 'S':
diff --git a/tests/mgcp/mgcp_test.c b/tests/mgcp/mgcp_test.c
index 2374e91..e37bb57 100644
--- a/tests/mgcp/mgcp_test.c
+++ b/tests/mgcp/mgcp_test.c
@@ -77,6 +77,8 @@
 #define AUEP1_RET "500 158663169 FAIL\r\n"
 #define AUEP2	"AUEP 18983213 ds/e1-2/1@mgw MGCP 1.0\r\n"
 #define AUEP2_RET "500 18983213 FAIL\r\n"
+#define AUEP_NULL "AUEP 18983215 null@mgw MGCP 1.0\r\n"
+#define AUEP_NULL_RET "200 18983215 OK\r\n"
 #define EMPTY	"\r\n"
 #define EMPTY_RET NULL
 #define SHORT	"CRCX \r\n"
@@ -274,6 +276,12 @@
 
 #define MDCX_TOO_LONG_CI_RET "510 18983224 FAIL\r\n"
 
+#define MDCX_NULL \
+	"MDCX 9 null@mgw MGCP 1.0\r\n" \
+	"I: %s\n"
+
+#define MDCX_NULL_RET "502 9 FAIL\r\n"
+
 #define SHORT2	"CRCX 1"
 #define SHORT2_RET "510 000000 FAIL\r\n"
 #define SHORT3	"CRCX 1 1@mgw"
@@ -392,6 +400,13 @@
  #define DLCX_RET_OSMUX DLCX_RET \
 	"X-Osmo-CP: EC TI=0, TO=0\r\n"
 
+#define DLCX_NULL \
+	"DLCX 8 null@mgw MGCP 1.0\r\n" \
+	"I: %s\r\n" \
+	"C: 2\r\n"
+
+#define DLCX_NULL_RET "502 8 FAIL\r\n"
+
 #define RQNT \
 	"RQNT 186908780 1@mgw MGCP 1.0\r\n" \
 	"X: B244F267488\r\n" \
@@ -405,6 +420,13 @@
 #define RQNT1_RET "200 186908780 OK\r\n"
 #define RQNT2_RET "200 186908781 OK\r\n"
 
+#define RQNT_NULL \
+	"RQNT 186908782 null@mgw MGCP 1.0\r\n" \
+	"X: B244F267488\r\n" \
+	"S: D/9\r\n"
+
+#define RQNT_NULL_RET "502 186908782 FAIL\r\n"
+
 #define PTYPE_IGNORE 0		/* == default initializer */
 #define PTYPE_NONE 128
 #define PTYPE_NYI  PTYPE_NONE
@@ -545,6 +567,20 @@
 	"m=audio 16008 RTP/AVP 0\r\n" \
 	"a=ptime:20\r\n"
 
+#define CRCX_NULL \
+	"CRCX 2 null@mgw MGCP 1.0\r\n" \
+	"m: recvonly\r\n" \
+	"C: 2\r\n" \
+	"L: p:20\r\n" \
+	"\r\n" \
+	"v=0\r\n" \
+	"c=IN IP4 123.12.12.123\r\n" \
+	"m=audio 5904 RTP/AVP 97\r\n" \
+	"a=rtpmap:97 GSM-EFR/8000\r\n" \
+	"a=ptime:40\r\n"
+
+#define CRCX_NULL_RET "502 2 FAIL\r\n"
+
 struct mgcp_test {
 	const char *name;
 	const char *req;
@@ -586,6 +622,11 @@
 	{"CRCX", CRCX_X_OSMO_IGN, CRCX_X_OSMO_IGN_RET, 97},
 	{"MDCX_TOO_LONG_CI", MDCX_TOO_LONG_CI, MDCX_TOO_LONG_CI_RET},
 	{"CRCX", CRCX_AMR_WITH_FMTP, CRCX_AMR_WITH_FMTP_RET},
+	{"AUEP_NULL", AUEP_NULL, AUEP_NULL_RET},
+	{"CRCX_NULL", CRCX_NULL, CRCX_NULL_RET},
+	{"MDCX_NULL", MDCX_NULL, MDCX_NULL_RET},
+	{"DLCX_NULL", DLCX_NULL, DLCX_NULL_RET},
+	{"RQNT_NULL", RQNT_NULL, RQNT_NULL_RET},
 };
 
 static const struct mgcp_test retransmit[] = {
diff --git a/tests/mgcp/mgcp_test.ok b/tests/mgcp/mgcp_test.ok
index 08df5d4..d76e455 100644
--- a/tests/mgcp/mgcp_test.ok
+++ b/tests/mgcp/mgcp_test.ok
Binary files differ