summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorNeels Hofmeyr <neels@hofmeyr.de>2017-10-06 02:59:54 +0200
committerNeels Hofmeyr <neels@hofmeyr.de>2017-10-17 00:59:00 +0200
commit183e7009afc8577f0d89c99f92a5720697040494 (patch)
tree3c774fa05e32f09d3805845751b48d9554177153 /src
parentb6837e36a34a1e9e7fafea822516e61285e3c09c (diff)
implement subscriber vty interface, tests
Implement VTY commands for subscriber manipulation: - create / delete subscriber - modify MSISDN - add/edit/remove 2G and 3G authentication data - show by IMSI, MSISDN or DB ID. (enable/disable CS/PS and purge/unpurge to follow later.) Implement VTY unit tests for the new commands using new osmo_verify_transcript_vty.py from osmo-python-tests. Depends: libosmocore I1e94f5b0717b947d2a7a7d36bacdf04a75cb3522 osmo-python-tests Id47331009910e651372b9c9c76e12f2e8964cc2c Change-Id: I42b3b70a0439a8f2e4964d7cc31e593c1f0d7537
Diffstat (limited to 'src')
-rw-r--r--src/Makefile.am2
-rw-r--r--src/hlr_vty.c3
-rw-r--r--src/hlr_vty_subscr.c484
-rw-r--r--src/hlr_vty_subscr.h3
4 files changed, 492 insertions, 0 deletions
diff --git a/src/Makefile.am b/src/Makefile.am
index b410ff3..fc7c653 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -23,6 +23,7 @@ noinst_HEADERS = \
rand.h \
ctrl.h \
hlr_vty.h \
+ hlr_vty_subscr.h \
$(NULL)
bin_PROGRAMS = \
@@ -46,6 +47,7 @@ osmo_hlr_SOURCES = \
logging.c \
rand_urandom.c \
hlr_vty.c \
+ hlr_vty_subscr.c \
$(NULL)
osmo_hlr_LDADD = \
diff --git a/src/hlr_vty.c b/src/hlr_vty.c
index 946117e..a5eb26f 100644
--- a/src/hlr_vty.c
+++ b/src/hlr_vty.c
@@ -26,6 +26,7 @@
#include <osmocom/vty/logging.h>
#include "hlr_vty.h"
+#include "hlr_vty_subscr.h"
static struct hlr *g_hlr = NULL;
@@ -135,4 +136,6 @@ void hlr_vty_init(struct hlr *hlr, const struct log_info *cat)
install_default(GSUP_NODE);
install_element(GSUP_NODE, &cfg_hlr_gsup_bind_ip_cmd);
+
+ hlr_vty_subscriber_init(hlr);
}
diff --git a/src/hlr_vty_subscr.c b/src/hlr_vty_subscr.c
new file mode 100644
index 0000000..5704922
--- /dev/null
+++ b/src/hlr_vty_subscr.c
@@ -0,0 +1,484 @@
+/* OsmoHLR subscriber management VTY implementation */
+/* (C) 2017 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <inttypes.h>
+#include <string.h>
+#include <errno.h>
+
+#include <osmocom/gsm/gsm23003.h>
+#include <osmocom/vty/vty.h>
+#include <osmocom/vty/command.h>
+#include <osmocom/core/utils.h>
+
+#include "hlr.h"
+#include "db.h"
+
+struct vty;
+
+#define hexdump_buf(buf) osmo_hexdump_nospc((void*)buf, sizeof(buf))
+
+static struct hlr *g_hlr = NULL;
+
+static void subscr_dump_full_vty(struct vty *vty, struct hlr_subscriber *subscr)
+{
+ int rc;
+ struct osmo_sub_auth_data aud2g;
+ struct osmo_sub_auth_data aud3g;
+
+ vty_out(vty, " ID: %"PRIu64"%s", subscr->id, VTY_NEWLINE);
+
+ vty_out(vty, " IMSI: %s%s", subscr->imsi ? subscr->imsi : "none", VTY_NEWLINE);
+ vty_out(vty, " MSISDN: %s%s", *subscr->msisdn ? subscr->msisdn : "none", VTY_NEWLINE);
+ if (*subscr->vlr_number)
+ vty_out(vty, " VLR number: %s%s", subscr->vlr_number, VTY_NEWLINE);
+ if (*subscr->sgsn_number)
+ vty_out(vty, " SGSN number: %s%s", subscr->sgsn_number, VTY_NEWLINE);
+ if (*subscr->sgsn_address)
+ vty_out(vty, " SGSN address: %s%s", subscr->sgsn_address, VTY_NEWLINE);
+ if (subscr->periodic_lu_timer)
+ vty_out(vty, " Periodic LU timer: %u%s", subscr->periodic_lu_timer, VTY_NEWLINE);
+ if (subscr->periodic_rau_tau_timer)
+ vty_out(vty, " Periodic RAU/TAU timer: %u%s", subscr->periodic_rau_tau_timer, VTY_NEWLINE);
+ if (subscr->lmsi)
+ vty_out(vty, " LMSI: %x%s", subscr->lmsi, VTY_NEWLINE);
+ if (!subscr->nam_cs)
+ vty_out(vty, " CS disabled%s", VTY_NEWLINE);
+ if (subscr->ms_purged_cs)
+ vty_out(vty, " CS purged%s", VTY_NEWLINE);
+ if (!subscr->nam_ps)
+ vty_out(vty, " PS disabled%s", VTY_NEWLINE);
+ if (subscr->ms_purged_ps)
+ vty_out(vty, " PS purged%s", VTY_NEWLINE);
+
+ if (!*subscr->imsi)
+ return;
+
+ OSMO_ASSERT(g_hlr);
+ rc = db_get_auth_data(g_hlr->dbc, subscr->imsi, &aud2g, &aud3g, NULL);
+
+ if (rc) {
+ if (rc == -ENOENT) {
+ aud2g.algo = OSMO_AUTH_ALG_NONE;
+ aud3g.algo = OSMO_AUTH_ALG_NONE;
+ } else {
+ vty_out(vty, "%% Error retrieving data from database (%d)%s", rc, VTY_NEWLINE);
+ return;
+ }
+ }
+
+ if (aud2g.type != OSMO_AUTH_TYPE_NONE && aud2g.type != OSMO_AUTH_TYPE_GSM) {
+ vty_out(vty, "%% Error: 2G auth data is not of type 'GSM'%s", VTY_NEWLINE);
+ aud2g = (struct osmo_sub_auth_data){};
+ }
+
+ if (aud3g.type != OSMO_AUTH_TYPE_NONE && aud3g.type != OSMO_AUTH_TYPE_UMTS) {
+ vty_out(vty, "%% Error: 3G auth data is not of type 'UMTS'%s", VTY_NEWLINE);
+ aud3g = (struct osmo_sub_auth_data){};
+ }
+
+ if (aud2g.algo != OSMO_AUTH_ALG_NONE && aud2g.type != OSMO_AUTH_TYPE_NONE) {
+ vty_out(vty, " 2G auth: %s%s",
+ osmo_auth_alg_name(aud2g.algo), VTY_NEWLINE);
+ vty_out(vty, " KI=%s%s",
+ hexdump_buf(aud2g.u.gsm.ki), VTY_NEWLINE);
+ }
+
+ if (aud3g.algo != OSMO_AUTH_ALG_NONE && aud3g.type != OSMO_AUTH_TYPE_NONE) {
+ vty_out(vty, " 3G auth: %s%s", osmo_auth_alg_name(aud3g.algo), VTY_NEWLINE);
+ vty_out(vty, " K=%s%s", hexdump_buf(aud3g.u.umts.k), VTY_NEWLINE);
+ vty_out(vty, " %s=%s%s", aud3g.u.umts.opc_is_op? "OP" : "OPC",
+ hexdump_buf(aud3g.u.umts.opc), VTY_NEWLINE);
+ vty_out(vty, " IND-bitlen=%u", aud3g.u.umts.ind_bitlen);
+ if (aud3g.u.umts.sqn)
+ vty_out(vty, " last-SQN=%"PRIu64, aud3g.u.umts.sqn);
+ vty_out(vty, VTY_NEWLINE);
+ }
+}
+
+static int get_subscr_by_argv(struct vty *vty, const char *type, const char *id, struct hlr_subscriber *subscr)
+{
+ int rc = -1;
+ if (strcmp(type, "imsi") == 0)
+ rc = db_subscr_get_by_imsi(g_hlr->dbc, id, subscr);
+ else if (strcmp(type, "msisdn") == 0)
+ rc = db_subscr_get_by_msisdn(g_hlr->dbc, id, subscr);
+ else if (strcmp(type, "id") == 0)
+ rc = db_subscr_get_by_id(g_hlr->dbc, atoll(id), subscr);
+ if (rc)
+ vty_out(vty, "%% No subscriber for %s = '%s'%s",
+ type, id, VTY_NEWLINE);
+ return rc;
+}
+
+#define SUBSCR_CMD "subscriber "
+#define SUBSCR_CMD_HELP "Subscriber management commands\n"
+
+#define SUBSCR_ID "(imsi|msisdn|id) IDENT "
+#define SUBSCR_ID_HELP \
+ "Identify subscriber by IMSI\n" \
+ "Identify subscriber by MSISDN (phone number)\n" \
+ "Identify subscriber by database ID\n" \
+ "IMSI/MSISDN/ID of the subscriber\n"
+
+#define SUBSCR SUBSCR_CMD SUBSCR_ID
+#define SUBSCR_HELP SUBSCR_CMD_HELP SUBSCR_ID_HELP
+
+#define SUBSCR_UPDATE SUBSCR "update "
+#define SUBSCR_UPDATE_HELP SUBSCR_HELP "Set or update subscriber data\n"
+
+DEFUN(subscriber_show,
+ subscriber_show_cmd,
+ SUBSCR "show",
+ SUBSCR_HELP "Show subscriber information\n")
+{
+ struct hlr_subscriber subscr;
+ const char *id_type = argv[0];
+ const char *id = argv[1];
+
+ if (get_subscr_by_argv(vty, id_type, id, &subscr))
+ return CMD_WARNING;
+
+ subscr_dump_full_vty(vty, &subscr);
+ return CMD_SUCCESS;
+}
+
+DEFUN(subscriber_create,
+ subscriber_create_cmd,
+ SUBSCR_CMD "imsi IDENT create",
+ SUBSCR_CMD_HELP
+ "Create subscriber by IMSI\n"
+ "IMSI/MSISDN/ID of the subscriber\n")
+{
+ int rc;
+ struct hlr_subscriber subscr;
+ const char *imsi = argv[0];
+
+ if (!osmo_imsi_str_valid(imsi)) {
+ vty_out(vty, "%% Not a valid IMSI: %s%s", imsi, VTY_NEWLINE);
+ return CMD_WARNING;
+ }
+
+ rc = db_subscr_create(g_hlr->dbc, imsi);
+
+ if (rc) {
+ if (rc == -EEXIST)
+ vty_out(vty, "%% Subscriber already exists for IMSI = %s%s",
+ imsi, VTY_NEWLINE);
+ else
+ vty_out(vty, "%% Error (rc=%d): cannot create subscriber for IMSI = %s%s",
+ rc, imsi, VTY_NEWLINE);
+ return CMD_WARNING;
+ }
+
+ rc = db_subscr_get_by_imsi(g_hlr->dbc, imsi, &subscr);
+ vty_out(vty, "%% Created subscriber %s%s", imsi, VTY_NEWLINE);
+
+ subscr_dump_full_vty(vty, &subscr);
+
+ return CMD_SUCCESS;
+}
+
+DEFUN(subscriber_delete,
+ subscriber_delete_cmd,
+ SUBSCR "delete",
+ SUBSCR_HELP "Delete subscriber from database\n")
+{
+ struct hlr_subscriber subscr;
+ int rc;
+ const char *id_type = argv[0];
+ const char *id = argv[1];
+
+ /* Find out the IMSI regardless of which way the caller decided to
+ * identify the subscriber by. */
+ if (get_subscr_by_argv(vty, id_type, id, &subscr))
+ return CMD_WARNING;
+
+ rc = db_subscr_delete_by_id(g_hlr->dbc, subscr.id);
+ if (rc) {
+ vty_out(vty, "%% Error: Failed to remove subscriber for IMSI '%s'%s",
+ subscr.imsi, VTY_NEWLINE);
+ return CMD_WARNING;
+ }
+
+ vty_out(vty, "%% Deleted subscriber for IMSI '%s'%s", subscr.imsi, VTY_NEWLINE);
+ return CMD_SUCCESS;
+}
+
+DEFUN(subscriber_msisdn,
+ subscriber_msisdn_cmd,
+ SUBSCR_UPDATE "msisdn MSISDN",
+ SUBSCR_UPDATE_HELP
+ "Set MSISDN (phone number) of the subscriber\n"
+ "New MSISDN (phone number)\n")
+{
+ struct hlr_subscriber subscr;
+ const char *id_type = argv[0];
+ const char *id = argv[1];
+ const char *msisdn = argv[2];
+
+ if (strlen(msisdn) > sizeof(subscr.msisdn) - 1) {
+ vty_out(vty, "%% MSISDN is too long, max. %zu characters are allowed%s",
+ sizeof(subscr.msisdn)-1, VTY_NEWLINE);
+ return CMD_WARNING;
+ }
+
+ if (!osmo_msisdn_str_valid(msisdn)) {
+ vty_out(vty, "%% MSISDN invalid: '%s'%s", msisdn, VTY_NEWLINE);
+ return CMD_WARNING;
+ }
+
+ if (get_subscr_by_argv(vty, id_type, id, &subscr))
+ return CMD_WARNING;
+
+ if (db_subscr_update_msisdn_by_imsi(g_hlr->dbc, subscr.imsi, msisdn)) {
+ vty_out(vty, "%% Error: cannot update MSISDN for subscriber IMSI='%s'%s",
+ subscr.imsi, VTY_NEWLINE);
+ return CMD_WARNING;
+ }
+
+ vty_out(vty, "%% Updated subscriber IMSI='%s' to MSISDN='%s'%s",
+ subscr.imsi, msisdn, VTY_NEWLINE);
+ return CMD_SUCCESS;
+}
+
+static bool is_hexkey_valid(struct vty *vty, const char *label,
+ const char *hex_str, int minlen, int maxlen)
+{
+ if (osmo_is_hexstr(hex_str, minlen * 2, maxlen * 2, true))
+ return true;
+ vty_out(vty, "%% Invalid value for %s: '%s'%s", label, hex_str, VTY_NEWLINE);
+ return false;
+}
+
+#define AUTH_ALG_TYPES_2G "(comp128v1|comp128v2|comp128v3|xor)"
+#define AUTH_ALG_TYPES_2G_HELP \
+ "Use COMP128v1 algorithm\n" \
+ "Use COMP128v2 algorithm\n" \
+ "Use COMP128v3 algorithm\n" \
+ "Use XOR algorithm\n"
+
+#define AUTH_ALG_TYPES_3G "milenage"
+#define AUTH_ALG_TYPES_3G_HELP \
+ "Use Milenage algorithm\n"
+
+#define A38_XOR_MIN_KEY_LEN 12
+#define A38_XOR_MAX_KEY_LEN 16
+#define A38_COMP128_KEY_LEN 16
+
+#define MILENAGE_KEY_LEN 16
+
+static bool auth_algo_parse(const char *alg_str, enum osmo_auth_algo *algo,
+ int *minlen, int *maxlen)
+{
+ if (!strcasecmp(alg_str, "none")) {
+ *algo = OSMO_AUTH_ALG_NONE;
+ *minlen = *maxlen = 0;
+ } else if (!strcasecmp(alg_str, "comp128v1")) {
+ *algo = OSMO_AUTH_ALG_COMP128v1;
+ *minlen = *maxlen = A38_COMP128_KEY_LEN;
+ } else if (!strcasecmp(alg_str, "comp128v2")) {
+ *algo = OSMO_AUTH_ALG_COMP128v2;
+ *minlen = *maxlen = A38_COMP128_KEY_LEN;
+ } else if (!strcasecmp(alg_str, "comp128v3")) {
+ *algo = OSMO_AUTH_ALG_COMP128v3;
+ *minlen = *maxlen = A38_COMP128_KEY_LEN;
+ } else if (!strcasecmp(alg_str, "xor")) {
+ *algo = OSMO_AUTH_ALG_XOR;
+ *minlen = A38_XOR_MIN_KEY_LEN;
+ *maxlen = A38_XOR_MAX_KEY_LEN;
+ } else if (!strcasecmp(alg_str, "milenage")) {
+ *algo = OSMO_AUTH_ALG_MILENAGE;
+ *minlen = *maxlen = MILENAGE_KEY_LEN;
+ } else
+ return false;
+ return true;
+}
+
+DEFUN(subscriber_no_aud2g,
+ subscriber_no_aud2g_cmd,
+ SUBSCR_UPDATE "aud2g none",
+ SUBSCR_UPDATE_HELP
+ "Set 2G authentication data\n"
+ "Delete 2G authentication data\n")
+{
+ struct hlr_subscriber subscr;
+ int rc;
+ const char *id_type = argv[0];
+ const char *id = argv[1];
+ struct sub_auth_data_str aud = {
+ .type = OSMO_AUTH_TYPE_GSM,
+ .algo = OSMO_AUTH_ALG_NONE,
+ };
+
+ if (get_subscr_by_argv(vty, id_type, id, &subscr))
+ return CMD_WARNING;
+
+ rc = db_subscr_update_aud_by_id(g_hlr->dbc, subscr.id, &aud);
+
+ if (rc) {
+ vty_out(vty, "%% Error: cannot disable 2G auth data for IMSI='%s'%s",
+ subscr.imsi, VTY_NEWLINE);
+ return CMD_WARNING;
+ }
+ return CMD_SUCCESS;
+}
+
+DEFUN(subscriber_aud2g,
+ subscriber_aud2g_cmd,
+ SUBSCR_UPDATE "aud2g " AUTH_ALG_TYPES_2G " ki KI",
+ SUBSCR_UPDATE_HELP
+ "Set 2G authentication data\n"
+ AUTH_ALG_TYPES_2G_HELP
+ "Set Ki Encryption Key\n" "Ki as 32 hexadecimal characters\n")
+{
+ struct hlr_subscriber subscr;
+ int rc;
+ int minlen = 0;
+ int maxlen = 0;
+ const char *id_type = argv[0];
+ const char *id = argv[1];
+ const char *alg_type = argv[2];
+ const char *ki = argv[3];
+ struct sub_auth_data_str aud2g = {
+ .type = OSMO_AUTH_TYPE_GSM,
+ .u.gsm.ki = ki,
+ };
+
+ if (!auth_algo_parse(alg_type, &aud2g.algo, &minlen, &maxlen)) {
+ vty_out(vty, "%% Unknown auth algorithm: '%s'%s", alg_type, VTY_NEWLINE);
+ return CMD_WARNING;
+ }
+
+ if (!is_hexkey_valid(vty, "KI", aud2g.u.gsm.ki, minlen, maxlen))
+ return CMD_WARNING;
+
+ if (get_subscr_by_argv(vty, id_type, id, &subscr))
+ return CMD_WARNING;
+
+ rc = db_subscr_update_aud_by_id(g_hlr->dbc, subscr.id, &aud2g);
+
+ if (rc) {
+ vty_out(vty, "%% Error: cannot set 2G auth data for IMSI='%s'%s",
+ subscr.imsi, VTY_NEWLINE);
+ return CMD_WARNING;
+ }
+ return CMD_SUCCESS;
+}
+
+DEFUN(subscriber_no_aud3g,
+ subscriber_no_aud3g_cmd,
+ SUBSCR_UPDATE "aud3g none",
+ SUBSCR_UPDATE_HELP
+ "Set UMTS authentication data (3G, and 2G with UMTS AKA)\n"
+ "Delete 3G authentication data\n")
+{
+ struct hlr_subscriber subscr;
+ int rc;
+ const char *id_type = argv[0];
+ const char *id = argv[1];
+ struct sub_auth_data_str aud = {
+ .type = OSMO_AUTH_TYPE_UMTS,
+ .algo = OSMO_AUTH_ALG_NONE,
+ };
+
+ if (get_subscr_by_argv(vty, id_type, id, &subscr))
+ return CMD_WARNING;
+
+ rc = db_subscr_update_aud_by_id(g_hlr->dbc, subscr.id, &aud);
+
+ if (rc) {
+ vty_out(vty, "%% Error: cannot disable 3G auth data for IMSI='%s'%s",
+ subscr.imsi, VTY_NEWLINE);
+ return CMD_WARNING;
+ }
+ return CMD_SUCCESS;
+}
+
+DEFUN(subscriber_aud3g,
+ subscriber_aud3g_cmd,
+ SUBSCR_UPDATE "aud3g " AUTH_ALG_TYPES_3G
+ " k K"
+ " (op|opc) OP_C"
+ " [ind-bitlen] [<0-28>]",
+ SUBSCR_UPDATE_HELP
+ "Set UMTS authentication data (3G, and 2G with UMTS AKA)\n"
+ AUTH_ALG_TYPES_3G_HELP
+ "Set Encryption Key K\n" "K as 32 hexadecimal characters\n"
+ "Set OP key\n" "Set OPC key\n" "OP or OPC as 32 hexadecimal characters\n"
+ "Set IND bit length\n" "IND bit length value (default: 5)\n")
+{
+ struct hlr_subscriber subscr;
+ int minlen = 0;
+ int maxlen = 0;
+ int rc;
+ const char *id_type = argv[0];
+ const char *id = argv[1];
+ const char *alg_type = AUTH_ALG_TYPES_3G;
+ const char *k = argv[2];
+ bool opc_is_op = (strcasecmp("op", argv[3]) == 0);
+ const char *op_opc = argv[4];
+ int ind_bitlen = argc > 6? atoi(argv[6]) : 5;
+ struct sub_auth_data_str aud3g = {
+ .type = OSMO_AUTH_TYPE_UMTS,
+ .u.umts = {
+ .k = k,
+ .opc_is_op = opc_is_op,
+ .opc = op_opc,
+ .ind_bitlen = ind_bitlen,
+ },
+ };
+
+ if (!auth_algo_parse(alg_type, &aud3g.algo, &minlen, &maxlen)) {
+ vty_out(vty, "%% Unknown auth algorithm: '%s'%s", alg_type, VTY_NEWLINE);
+ return CMD_WARNING;
+ }
+
+ if (!is_hexkey_valid(vty, "K", aud3g.u.umts.k, minlen, maxlen))
+ return CMD_WARNING;
+
+ if (!is_hexkey_valid(vty, opc_is_op ? "OP" : "OPC", aud3g.u.umts.opc,
+ MILENAGE_KEY_LEN, MILENAGE_KEY_LEN))
+ return CMD_WARNING;
+
+ if (get_subscr_by_argv(vty, id_type, id, &subscr))
+ return CMD_WARNING;
+
+ rc = db_subscr_update_aud_by_id(g_hlr->dbc, subscr.id, &aud3g);
+
+ if (rc) {
+ vty_out(vty, "%% Error: cannot set 3G auth data for IMSI='%s'%s",
+ subscr.imsi, VTY_NEWLINE);
+ return CMD_WARNING;
+ }
+ return CMD_SUCCESS;
+}
+
+void hlr_vty_subscriber_init(struct hlr *hlr)
+{
+ g_hlr = hlr;
+
+ install_element_ve(&subscriber_show_cmd);
+ install_element(ENABLE_NODE, &subscriber_create_cmd);
+ install_element(ENABLE_NODE, &subscriber_delete_cmd);
+ install_element(ENABLE_NODE, &subscriber_msisdn_cmd);
+ install_element(ENABLE_NODE, &subscriber_no_aud2g_cmd);
+ install_element(ENABLE_NODE, &subscriber_aud2g_cmd);
+ install_element(ENABLE_NODE, &subscriber_no_aud3g_cmd);
+ install_element(ENABLE_NODE, &subscriber_aud3g_cmd);
+}
diff --git a/src/hlr_vty_subscr.h b/src/hlr_vty_subscr.h
new file mode 100644
index 0000000..841db5a
--- /dev/null
+++ b/src/hlr_vty_subscr.h
@@ -0,0 +1,3 @@
+#pragma once
+
+void hlr_vty_subscriber_init(struct hlr *hlr);