Introduce a VTY, factually turning OpenGGSN into an Osmocom program

Change-Id: I9613ca3436e77ea132c47f0096df7c5050d7e826
diff --git a/ggsn/ggsn_vty.c b/ggsn/ggsn_vty.c
new file mode 100644
index 0000000..f23a298
--- /dev/null
+++ b/ggsn/ggsn_vty.c
@@ -0,0 +1,890 @@
+/*
+ * (C) 2017 by Harald Welte <laforge@gnumonks.org>
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <string.h>
+#include <stdint.h>
+#include <inttypes.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+#include <osmocom/core/talloc.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/core/rate_ctr.h>
+#include <osmocom/gsm/protocol/gsm_04_08_gprs.h>
+
+#include <osmocom/vty/command.h>
+#include <osmocom/vty/vty.h>
+#include <osmocom/vty/misc.h>
+
+#include "../gtp/gtp.h"
+#include "../gtp/pdp.h"
+
+#include "ggsn.h"
+
+#define PREFIX_STR	"Prefix (Network/Netmask)\n"
+#define IFCONFIG_STR	"GGSN-based interface configuration\n"
+#define GGSN_STR	"Gateway GPRS Support NODE (GGSN)\n"
+
+LLIST_HEAD(g_ggsn_list);
+
+enum ggsn_vty_node {
+	GGSN_NODE = _LAST_OSMOVTY_NODE + 1,
+	APN_NODE,
+};
+
+struct ggsn_ctx *ggsn_find(const char *name)
+{
+	struct ggsn_ctx *ggsn;
+
+	llist_for_each_entry(ggsn, &g_ggsn_list, list) {
+		if (!strcmp(ggsn->cfg.name, name))
+			return ggsn;
+	}
+	return NULL;
+}
+
+struct ggsn_ctx *ggsn_find_or_create(void *ctx, const char *name)
+{
+	struct ggsn_ctx *ggsn;
+
+	ggsn = ggsn_find(name);
+	if (ggsn)
+		return ggsn;
+
+	ggsn = talloc_zero(ctx, struct ggsn_ctx);
+	if (!ggsn)
+		return NULL;
+
+	ggsn->cfg.name = talloc_strdup(ggsn, name);
+	ggsn->cfg.state_dir = talloc_strdup(ggsn, "/tmp");
+	ggsn->cfg.shutdown = true;
+	INIT_LLIST_HEAD(&ggsn->apn_list);
+
+	llist_add_tail(&ggsn->list, &g_ggsn_list);
+	return ggsn;
+}
+
+struct apn_ctx *ggsn_find_apn(struct ggsn_ctx *ggsn, const char *name)
+{
+	struct apn_ctx *apn;
+
+	llist_for_each_entry(apn, &ggsn->apn_list, list) {
+		if (!strcmp(apn->cfg.name, name))
+			return apn;
+	}
+	return NULL;
+}
+
+struct apn_ctx *ggsn_find_or_create_apn(struct ggsn_ctx *ggsn, const char *name)
+{
+	struct apn_ctx *apn = ggsn_find_apn(ggsn, name);
+	if (apn)
+		return apn;
+
+	apn = talloc_zero(ggsn, struct apn_ctx);
+	if (!apn)
+		return NULL;
+	apn->ggsn = ggsn;
+	apn->cfg.name = talloc_strdup(apn, name);
+	apn->cfg.shutdown = true;
+	INIT_LLIST_HEAD(&apn->cfg.name_list);
+
+	llist_add_tail(&apn->list, &ggsn->apn_list);
+	return apn;
+}
+
+/* GGSN Node */
+
+static struct cmd_node ggsn_node = {
+	GGSN_NODE,
+	"%s(config-ggsn)# ",
+	1,
+};
+
+DEFUN(cfg_ggsn, cfg_ggsn_cmd,
+	"ggsn NAME",
+	"Configure the Gateway GPRS Support Node\n" "GGSN Name (has only local significance)\n")
+{
+	struct ggsn_ctx *ggsn;
+
+	ggsn = ggsn_find_or_create(tall_ggsn_ctx, argv[0]);
+	if (!ggsn)
+		return CMD_WARNING;
+
+	vty->node = GGSN_NODE;
+	vty->index = ggsn;
+	vty->index_sub = &ggsn->cfg.description;
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_no_ggsn, cfg_no_ggsn_cmd,
+	"no ggsn NAME",
+	NO_STR "Remove the named Gateway GPRS Support Node\n"
+	"GGSN Name (has only local significance)\n")
+{
+	struct ggsn_ctx *ggsn;
+
+	ggsn = ggsn_find(argv[0]);
+	if (!ggsn) {
+		vty_out(vty, "%% No such GGSN '%s'%s", argv[0], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	if (!ggsn->cfg.shutdown) {
+		vty_out(vty, "%% GGSN %s is still active, please shutdown first%s",
+			ggsn->cfg.name, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	if (!llist_empty(&ggsn->apn_list)) {
+		vty_out(vty, "%% GGSN %s still has APNs configured, please remove first%s",
+			ggsn->cfg.name, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	llist_del(&ggsn->list);
+	talloc_free(ggsn);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_ggsn_local_ip, cfg_ggsn_local_ip_cmd,
+	"gtp local-ip A.B.C.D",
+	"GTP Parameters\n"
+	"Set the IP address for the local GTP bind\n"
+	"IPv4 Address\n")
+{
+	struct ggsn_ctx *ggsn = (struct ggsn_ctx *) vty->index;
+	size_t t;
+
+	ippool_aton(&ggsn->cfg.listen_addr, &t, argv[0], 0);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_ggsn_state_dir, cfg_ggsn_state_dir_cmd,
+	"gtp state-dir PATH",
+	"GTP Parameters\n"
+	"Set the directory for the GTP State file\n"
+	"Local Directory\n")
+{
+	struct ggsn_ctx *ggsn = (struct ggsn_ctx *) vty->index;
+
+	osmo_talloc_replace_string(ggsn, &ggsn->cfg.state_dir, argv[0]);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_ggsn_apn, cfg_ggsn_apn_cmd,
+	"apn NAME", "APN Configuration\n" "APN Name\n")
+{
+	struct ggsn_ctx *ggsn = (struct ggsn_ctx *) vty->index;
+	struct apn_ctx *apn;
+
+	apn = ggsn_find_or_create_apn(ggsn, argv[0]);
+	if (!apn)
+		return CMD_WARNING;
+
+	vty->node = APN_NODE;
+	vty->index = apn;
+	vty->index_sub = &ggsn->cfg.description;
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_ggsn_no_apn, cfg_ggsn_no_apn_cmd,
+	"no apn NAME",
+	NO_STR "Remove APN Configuration\n" "APN Name\n")
+{
+	struct ggsn_ctx *ggsn = (struct ggsn_ctx *) vty->index;
+	struct apn_ctx *apn;
+
+	apn = ggsn_find_apn(ggsn, argv[0]);
+	if (!apn) {
+		vty_out(vty, "%% No such APN '%s'%s", argv[0], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	if (!apn->cfg.shutdown) {
+		vty_out(vty, "%% APN %s still active, please shutdown first%s",
+			apn->cfg.name, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	llist_del(&apn->list);
+	talloc_free(apn);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_ggsn_default_apn, cfg_ggsn_default_apn_cmd,
+	"default-apn NAME",
+	"Set a default-APN to be used if no other APN matches\n"
+	"APN Name\n")
+{
+	struct ggsn_ctx *ggsn = (struct ggsn_ctx *) vty->index;
+	struct apn_ctx *apn;
+
+	apn = ggsn_find_apn(ggsn, argv[0]);
+	if (!apn) {
+		vty_out(vty, "%% No APN of name '%s' found%s", argv[0], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	ggsn->cfg.default_apn = apn;
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_ggsn_no_default_apn, cfg_ggsn_no_default_apn_cmd,
+	"no default-apn",
+	NO_STR "Remove default-APN to be used if no other APN matches\n")
+{
+	struct ggsn_ctx *ggsn = (struct ggsn_ctx *) vty->index;
+	ggsn->cfg.default_apn = NULL;
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_ggsn_shutdown, cfg_ggsn_shutdown_cmd,
+	"shutdown ggsn",
+	"Put the GGSN in administrative shut-down\n" GGSN_STR)
+{
+	struct ggsn_ctx *ggsn = (struct ggsn_ctx *) vty->index;
+
+	if (!ggsn->cfg.shutdown) {
+		if (ggsn_stop(ggsn)) {
+			vty_out(vty, "%% Failed to shutdown GGSN%s", VTY_NEWLINE);
+			return CMD_WARNING;
+		}
+		ggsn->cfg.shutdown = true;
+	}
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_ggsn_no_shutdown, cfg_ggsn_no_shutdown_cmd,
+	"no shutdown ggsn",
+	NO_STR GGSN_STR "Remove the GGSN from administrative shut-down\n")
+{
+	struct ggsn_ctx *ggsn = (struct ggsn_ctx *) vty->index;
+
+	if (ggsn->cfg.shutdown) {
+		if (ggsn_start(ggsn) < 0) {
+			vty_out(vty, "%% Failed to start GGSN, check log for details%s", VTY_NEWLINE);
+			return CMD_WARNING;
+		}
+		ggsn->cfg.shutdown = false;
+	}
+
+	return CMD_SUCCESS;
+}
+
+/* APN Node */
+
+static struct cmd_node apn_node = {
+	APN_NODE,
+	"%s(config-ggsn-apn)# ",
+	1,
+};
+
+static const struct value_string pdp_type_names[] = {
+	{ APN_TYPE_IPv4, "v4" },
+	{ APN_TYPE_IPv6, "v6" },
+	{ APN_TYPE_IPv4v6, "v4v6" },
+	{ 0, NULL }
+};
+
+static const struct value_string apn_gtpu_mode_names[] = {
+	{ APN_GTPU_MODE_TUN,		"tun" },
+	{ APN_GTPU_MODE_KERNEL_GTP,	"kernel-gtp" },
+	{ 0, NULL }
+};
+
+
+#define V4V6V46_STRING	"IPv4(-only) PDP Type\n"	\
+			"IPv6(-only) PDP Type\n"	\
+			"IPv4v6 (dual-stack) PDP Type\n"
+
+DEFUN(cfg_apn_type_support, cfg_apn_type_support_cmd,
+	"type-support (v4|v6|v4v6)",
+	"Enable support for PDP Type\n"
+	V4V6V46_STRING)
+{
+	struct apn_ctx *apn = (struct apn_ctx *) vty->index;
+	uint32_t type = get_string_value(pdp_type_names, argv[0]);
+
+	apn->cfg.apn_type_mask |= type;
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_apn_no_type_support, cfg_apn_no_type_support_cmd,
+	"no type-support (v4|v6|v4v6)",
+	NO_STR "Disable support for PDP Type\n"
+	V4V6V46_STRING)
+{
+	struct apn_ctx *apn = (struct apn_ctx *) vty->index;
+	uint32_t type = get_string_value(pdp_type_names, argv[0]);
+
+	apn->cfg.apn_type_mask &= ~type;
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_apn_gtpu_mode, cfg_apn_gtpu_mode_cmd,
+	"gtpu-mode (tun|kernel-gtp)",
+	"Set the Mode for this APN (tun or Linux Kernel GTP)\n"
+	"GTP-U in userspace using TUN device\n"
+	"GTP-U in kernel using Linux Kernel GTP\n")
+{
+	struct apn_ctx *apn = (struct apn_ctx *) vty->index;
+
+	apn->cfg.gtpu_mode = get_string_value(apn_gtpu_mode_names, argv[0]);
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_apn_tun_dev_name, cfg_apn_tun_dev_name_cmd,
+	"tun-device NAME",
+	"Configure tun device name\n"
+	"TUN device name")
+{
+	struct apn_ctx *apn = (struct apn_ctx *) vty->index;
+	osmo_talloc_replace_string(apn, &apn->tun.cfg.dev_name, argv[0]);
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_apn_ipup_script, cfg_apn_ipup_script_cmd,
+	"ipup-script PATH",
+	"Configure name/path of ip-up script\n"
+	"File/Path name of ip-up script\n")
+{
+	struct apn_ctx *apn = (struct apn_ctx *) vty->index;
+	osmo_talloc_replace_string(apn, &apn->tun.cfg.ipup_script, argv[0]);
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_apn_no_ipup_script, cfg_apn_no_ipup_script_cmd,
+	"no ipup-script",
+	NO_STR "Disable ip-up script\n")
+{
+	struct apn_ctx *apn = (struct apn_ctx *) vty->index;
+	talloc_free(apn->tun.cfg.ipup_script);
+	apn->tun.cfg.ipup_script = NULL;
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_apn_ipdown_script, cfg_apn_ipdown_script_cmd,
+	"ipdown-script PATH",
+	"Configure name/path of ip-down script\n"
+	"File/Path name of ip-down script\n")
+{
+	struct apn_ctx *apn = (struct apn_ctx *) vty->index;
+	osmo_talloc_replace_string(apn, &apn->tun.cfg.ipdown_script, argv[0]);
+	return CMD_SUCCESS;
+}
+
+/* convert prefix from "A.B.C.D/M" notation to in46_prefix */
+static void str2prefix(struct in46_prefix *pfx, const char *in)
+{
+	size_t t;
+
+	ippool_aton(&pfx->addr, &t, in, 0);
+	pfx->prefixlen = t;
+}
+
+DEFUN(cfg_apn_no_ipdown_script, cfg_apn_no_ipdown_script_cmd,
+	"no ipdown-script",
+	NO_STR "Disable ip-down script\n")
+{
+	struct apn_ctx *apn = (struct apn_ctx *) vty->index;
+	talloc_free(apn->tun.cfg.ipdown_script);
+	apn->tun.cfg.ipdown_script = NULL;
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_apn_ip_prefix, cfg_apn_ip_prefix_cmd,
+	"ip prefix (static|dynamic) A.B.C.D/M",
+	IP_STR PREFIX_STR "IPv4 Adress/Prefix-Length\n")
+{
+	struct apn_ctx *apn = (struct apn_ctx *) vty->index;
+	struct in46_prefix *pfx;
+
+	/* first update our parsed prefix */
+	if (!strcmp(argv[0], "static"))
+		pfx = &apn->v4.cfg.static_prefix;
+	else
+		pfx = &apn->v4.cfg.dynamic_prefix;
+	str2prefix(pfx, argv[1]);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_apn_ip_ifconfig, cfg_apn_ip_ifconfig_cmd,
+	"ip ifconfig A.B.C.D/M",
+	IP_STR IFCONFIG_STR "IPv4 Adress/Prefix-Length\n")
+{
+	struct apn_ctx *apn = (struct apn_ctx *) vty->index;
+	str2prefix(&apn->v4.cfg.ifconfig_prefix, argv[0]);
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_apn_no_ip_ifconfig, cfg_apn_no_ip_ifconfig_cmd,
+	"no ip ifconfig",
+	NO_STR IP_STR IFCONFIG_STR)
+{
+	struct apn_ctx *apn = (struct apn_ctx *) vty->index;
+	memset(&apn->v4.cfg.ifconfig_prefix, 0, sizeof(apn->v4.cfg.ifconfig_prefix));
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_apn_ipv6_prefix, cfg_apn_ipv6_prefix_cmd,
+	"ipv6 prefix (static|dynamic) X:X::X:X/M",
+	IP6_STR PREFIX_STR "IPv6 Address/Prefix-Length\n")
+{
+	struct apn_ctx *apn = (struct apn_ctx *) vty->index;
+	struct in46_prefix *pfx;
+
+	if (!strcmp(argv[0], "static"))
+		pfx = &apn->v6.cfg.static_prefix;
+	else
+		pfx = &apn->v6.cfg.dynamic_prefix;
+	str2prefix(pfx, argv[1]);
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_apn_ipv6_ifconfig, cfg_apn_ipv6_ifconfig_cmd,
+	"ipv6 ifconfig X:X::X:X/M",
+	IP6_STR IFCONFIG_STR "IPv6 Adress/Prefix-Length\n")
+{
+	struct apn_ctx *apn = (struct apn_ctx *) vty->index;
+	str2prefix(&apn->v6.cfg.ifconfig_prefix, argv[0]);
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_apn_no_ipv6_ifconfig, cfg_apn_no_ipv6_ifconfig_cmd,
+	"no ipv6 ifconfig",
+	NO_STR IP6_STR IFCONFIG_STR)
+{
+	struct apn_ctx *apn = (struct apn_ctx *) vty->index;
+	memset(&apn->v6.cfg.ifconfig_prefix, 0, sizeof(apn->v6.cfg.ifconfig_prefix));
+	return CMD_SUCCESS;
+}
+
+#define DNS_STRINGS "Configure DNS Server\n" "primary/secondary DNS\n" "IP address of DNS Sever\n"
+
+DEFUN(cfg_apn_ip_dns, cfg_apn_ip_dns_cmd,
+	"ip dns <0-1> A.B.C.D",
+	IP_STR DNS_STRINGS)
+{
+	struct apn_ctx *apn = (struct apn_ctx *) vty->index;
+	int idx = atoi(argv[0]);
+	size_t dummy;
+
+	ippool_aton(&apn->v4.cfg.dns[idx], &dummy, argv[1], 0);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_apn_ipv6_dns, cfg_apn_ipv6_dns_cmd,
+	"ipv6 dns <0-1> X:X::X:X",
+	IP6_STR DNS_STRINGS)
+{
+	struct apn_ctx *apn = (struct apn_ctx *) vty->index;
+	int idx = atoi(argv[0]);
+	size_t dummy;
+
+	ippool_aton(&apn->v6.cfg.dns[idx], &dummy, argv[1], 0);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_apn_no_dns, cfg_apn_no_dns_cmd,
+	"no (ip|ipv6) dns <0-1>",
+	NO_STR IP_STR IP6_STR "Disable DNS Server\n" "primary/secondary DNS\n")
+{
+	struct apn_ctx *apn = (struct apn_ctx *) vty->index;
+	struct in46_addr *a;
+	int idx = atoi(argv[1]);
+
+	if (!strcmp(argv[0], "ip"))
+		a = &apn->v4.cfg.dns[idx];
+	else
+		a = &apn->v6.cfg.dns[idx];
+
+	memset(a, 0, sizeof(*a));
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_apn_shutdown, cfg_apn_shutdown_cmd,
+	"shutdown",
+	"Put the APN in administrative shut-down\n")
+{
+	struct apn_ctx *apn = (struct apn_ctx *) vty->index;
+
+	if (!apn->cfg.shutdown) {
+		if (apn_stop(apn, false)) {
+			vty_out(vty, "%% Failed to Stop APN%s", VTY_NEWLINE);
+			return CMD_WARNING;
+		}
+		apn->cfg.shutdown = true;
+	}
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_apn_no_shutdown, cfg_apn_no_shutdown_cmd,
+	"no shutdown",
+	NO_STR "Remove the APN from administrative shut-down\n")
+{
+	struct apn_ctx *apn = (struct apn_ctx *) vty->index;
+
+	if (apn->cfg.shutdown) {
+		if (apn_start(apn) < 0) {
+			vty_out(vty, "%% Failed to start APN, check log for details%s", VTY_NEWLINE);
+			return CMD_WARNING;
+		}
+		apn->cfg.shutdown = false;
+	}
+
+	return CMD_SUCCESS;
+}
+
+
+static void vty_dump_prefix(struct vty *vty, const char *pre, const struct in46_prefix *pfx)
+{
+	vty_out(vty, "%s %s%s", pre, in46p_ntoa(pfx), VTY_NEWLINE);
+}
+
+static void config_write_apn(struct vty *vty, struct apn_ctx *apn)
+{
+	unsigned int i;
+
+	vty_out(vty, " apn %s%s", apn->cfg.name, VTY_NEWLINE);
+	if (apn->cfg.description)
+		vty_out(vty, " description %s%s", apn->cfg.description, VTY_NEWLINE);
+	vty_out(vty, "  gtpu-mode %s%s", get_value_string(apn_gtpu_mode_names, apn->cfg.gtpu_mode),
+		VTY_NEWLINE);
+	if (apn->tun.cfg.dev_name)
+		vty_out(vty, "  tun-device %s%s", apn->tun.cfg.dev_name, VTY_NEWLINE);
+	if (apn->tun.cfg.ipup_script)
+		vty_out(vty, "  ipup-script %s%s", apn->tun.cfg.ipup_script, VTY_NEWLINE);
+	if (apn->tun.cfg.ipdown_script)
+		vty_out(vty, "  ipdown-script %s%s", apn->tun.cfg.ipdown_script, VTY_NEWLINE);
+
+	for (i = 0; i < 32; i++) {
+		if (!(apn->cfg.apn_type_mask & (1 << i)))
+			continue;
+		vty_out(vty, "  type-support %s%s", get_value_string(pdp_type_names, (1 << i)),
+			VTY_NEWLINE);
+	}
+
+	/* IPv4 prefixes + DNS */
+	if (apn->v4.cfg.static_prefix.addr.len)
+		vty_dump_prefix(vty, "  ip prefix static", &apn->v4.cfg.static_prefix);
+	if (apn->v4.cfg.dynamic_prefix.addr.len)
+		vty_dump_prefix(vty, "  ip prefix dynamic", &apn->v4.cfg.dynamic_prefix);
+	for (i = 0; i < ARRAY_SIZE(apn->v4.cfg.dns); i++) {
+		if (!apn->v4.cfg.dns[i].len)
+			continue;
+		vty_out(vty, "  ip dns %u %s%s", i, in46a_ntoa(&apn->v4.cfg.dns[i]), VTY_NEWLINE);
+	}
+	if (apn->v4.cfg.ifconfig_prefix.addr.len)
+		vty_dump_prefix(vty, "  ip ifconfig ", &apn->v4.cfg.ifconfig_prefix);
+
+	/* IPv6 prefixes + DNS */
+	if (apn->v6.cfg.static_prefix.addr.len)
+		vty_dump_prefix(vty, "  ipv6 prefix static", &apn->v6.cfg.static_prefix);
+	if (apn->v6.cfg.dynamic_prefix.addr.len)
+		vty_dump_prefix(vty, "  ipv6 prefix dynamic", &apn->v6.cfg.dynamic_prefix);
+	for (i = 0; i < ARRAY_SIZE(apn->v6.cfg.dns); i++) {
+		if (!apn->v6.cfg.dns[i].len)
+			continue;
+		vty_out(vty, "  ip dns %u %s%s", i, in46a_ntoa(&apn->v6.cfg.dns[i]), VTY_NEWLINE);
+	}
+	if (apn->v6.cfg.ifconfig_prefix.addr.len)
+		vty_dump_prefix(vty, "  ipv6 ifconfig ", &apn->v6.cfg.ifconfig_prefix);
+
+	/* must be last */
+	vty_out(vty, "  %sshutdown%s", apn->cfg.shutdown ? "" : "no ", VTY_NEWLINE);
+}
+
+static int config_write_ggsn(struct vty *vty)
+{
+	struct ggsn_ctx *ggsn;
+
+	llist_for_each_entry(ggsn, &g_ggsn_list, list) {
+		struct apn_ctx *apn;
+		vty_out(vty, "ggsn %s%s", ggsn->cfg.name, VTY_NEWLINE);
+		if (ggsn->cfg.description)
+			vty_out(vty, " description %s%s", ggsn->cfg.description, VTY_NEWLINE);
+		vty_out(vty, " gtp state-dir %s%s", ggsn->cfg.state_dir, VTY_NEWLINE);
+		vty_out(vty, " gtp local-ip %s%s", in46a_ntoa(&ggsn->cfg.listen_addr), VTY_NEWLINE);
+		llist_for_each_entry(apn, &ggsn->apn_list, list)
+			config_write_apn(vty, apn);
+		if (ggsn->cfg.default_apn)
+			vty_out(vty, " default-apn %s%s", ggsn->cfg.default_apn->cfg.name, VTY_NEWLINE);
+		/* must be last */
+		vty_out(vty, " %sshutdown ggsn%s", ggsn->cfg.shutdown ? "" : "no ", VTY_NEWLINE);
+	}
+
+	return 0;
+}
+
+static const char *print_gsnaddr(const struct ul16_t *in)
+{
+	struct in46_addr in46;
+
+	in46.len = in->l;
+	OSMO_ASSERT(in->l <= sizeof(in46.v6));
+	memcpy(&in46.v6, in->v, in->l);
+
+	return in46a_ntoa(&in46);
+}
+
+static void show_one_pdp(struct vty *vty, struct pdp_t *pdp)
+{
+	struct in46_addr eua46;
+
+	vty_out(vty, "IMSI: %s, NSAPI: %u, MSISDN: %s%s", imsi_gtp2str(&pdp->imsi), pdp->nsapi,
+		osmo_hexdump_nospc(pdp->msisdn.v, pdp->msisdn.l), VTY_NEWLINE);
+
+	vty_out(vty, " Control: %s:%08x ", print_gsnaddr(&pdp->gsnlc), pdp->teic_own);
+	vty_out(vty, "<-> %s:%08x%s", print_gsnaddr(&pdp->gsnrc), pdp->teic_gn, VTY_NEWLINE);
+
+	vty_out(vty, " Data: %s:%08x ", print_gsnaddr(&pdp->gsnlu), pdp->teid_own);
+	vty_out(vty, "<-> %s:%08x%s", print_gsnaddr(&pdp->gsnru), pdp->teid_gn, VTY_NEWLINE);
+
+	in46a_from_eua(&pdp->eua, &eua46);
+	vty_out(vty, " End-User Address: %s%s", in46a_ntoa(&eua46), VTY_NEWLINE);
+}
+
+DEFUN(show_pdpctx_imsi, show_pdpctx_imsi_cmd,
+	"show pdp-context imsi IMSI [<0-15>]",
+	SHOW_STR "Display information on PDP Context\n"
+	"PDP contexts for given IMSI\n"
+	"PDP context for given NSAPI\n")
+{
+	uint64_t imsi = strtoull(argv[0], NULL, 10);
+	unsigned int nsapi;
+	struct pdp_t *pdp;
+	int num_found = 0;
+
+	if (argc > 1) {
+		nsapi = atoi(argv[1]);
+		if (pdp_getimsi(&pdp, imsi, nsapi)) {
+			show_one_pdp(vty, pdp);
+			num_found++;
+		}
+	} else {
+		for (nsapi = 0; nsapi < PDP_MAXNSAPI; nsapi++) {
+			if (pdp_getimsi(&pdp, imsi, nsapi))
+				continue;
+			show_one_pdp(vty, pdp);
+			num_found++;
+		}
+	}
+	if (num_found == 0) {
+		vty_out(vty, "%% No such PDP context found%s", VTY_NEWLINE);
+		return CMD_WARNING;
+	} else
+		return CMD_SUCCESS;
+}
+
+/* show all (active) PDP contexts within a pool */
+static void ippool_show_pdp_contexts(struct vty *vty, struct ippool_t *pool)
+{
+	unsigned int i;
+
+	if (!pool)
+		return;
+
+	for (i = 0; i < pool->listsize; i++) {
+		struct ippoolm_t *member = &pool->member[i];
+		if (member->inuse == 0)
+			continue;
+		show_one_pdp(vty, member->peer);
+	}
+}
+
+/* show all (active) PDP contexts within an APN */
+static void apn_show_pdp_contexts(struct vty *vty, struct apn_ctx *apn)
+{
+	ippool_show_pdp_contexts(vty, apn->v4.pool);
+	ippool_show_pdp_contexts(vty, apn->v6.pool);
+}
+
+DEFUN(show_pdpctx, show_pdpctx_cmd,
+	"show pdp-context ggsn NAME [apn APN]",
+	SHOW_STR "Show PDP Context Information\n"
+	GGSN_STR "GGSN Name\n") // FIXME
+{
+	struct ggsn_ctx *ggsn;
+	struct apn_ctx *apn;
+
+	ggsn = ggsn_find(argv[0]);
+	if (!ggsn) {
+		vty_out(vty, "%% No such GGSN '%s'%s", argv[0], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	if (argc < 2) {
+		llist_for_each_entry(apn, &ggsn->apn_list, list)
+			apn_show_pdp_contexts(vty, apn);
+	} else {
+		apn = ggsn_find_apn(ggsn, argv[1]);
+		if (!apn) {
+			vty_out(vty, "%% No such APN '%s'%s", argv[1], VTY_NEWLINE);
+			return CMD_WARNING;
+		}
+		apn_show_pdp_contexts(vty, apn);
+	}
+
+	return CMD_SUCCESS;
+}
+
+static void show_apn(struct vty *vty, struct apn_ctx *apn)
+{
+	vty_out(vty, " APN: %s%s", apn->cfg.name, VTY_NEWLINE);
+	/* FIXME */
+}
+
+static void show_one_ggsn(struct vty *vty, struct ggsn_ctx *ggsn)
+{
+	struct apn_ctx *apn;
+	vty_out(vty, "GGSN %s: Bound to %s%s", ggsn->cfg.name, in46a_ntoa(&ggsn->cfg.listen_addr),
+		VTY_NEWLINE);
+	/* FIXME */
+
+	llist_for_each_entry(apn, &ggsn->apn_list, list)
+		show_apn(vty, apn);
+}
+
+DEFUN(show_ggsn, show_ggsn_cmd,
+	"show ggsn [NAME]",
+	SHOW_STR "Display information on the GGSN\n")
+{
+	struct ggsn_ctx *ggsn;
+
+	if (argc == 0) {
+		llist_for_each_entry(ggsn, &g_ggsn_list, list)
+			show_one_ggsn(vty, ggsn);
+	} else {
+		ggsn = ggsn_find(argv[0]);
+		if (!ggsn)
+			return CMD_WARNING;
+		show_one_ggsn(vty, ggsn);
+	}
+
+	return CMD_SUCCESS;
+}
+
+int ggsn_vty_init(void)
+{
+	install_element_ve(&show_pdpctx_cmd);
+	install_element_ve(&show_pdpctx_imsi_cmd);
+	install_element_ve(&show_ggsn_cmd);
+
+	install_element(CONFIG_NODE, &cfg_ggsn_cmd);
+	install_element(CONFIG_NODE, &cfg_no_ggsn_cmd);
+	install_node(&ggsn_node, config_write_ggsn);
+	vty_install_default(GGSN_NODE);
+	install_element(GGSN_NODE, &cfg_description_cmd);
+	install_element(GGSN_NODE, &cfg_no_description_cmd);
+	install_element(GGSN_NODE, &cfg_ggsn_shutdown_cmd);
+	install_element(GGSN_NODE, &cfg_ggsn_no_shutdown_cmd);
+	install_element(GGSN_NODE, &cfg_ggsn_local_ip_cmd);
+	install_element(GGSN_NODE, &cfg_ggsn_state_dir_cmd);
+	install_element(GGSN_NODE, &cfg_ggsn_apn_cmd);
+	install_element(GGSN_NODE, &cfg_ggsn_no_apn_cmd);
+	install_element(GGSN_NODE, &cfg_ggsn_default_apn_cmd);
+	install_element(GGSN_NODE, &cfg_ggsn_no_default_apn_cmd);
+
+	install_node(&apn_node, NULL);
+	vty_install_default(APN_NODE);
+	install_element(APN_NODE, &cfg_description_cmd);
+	install_element(APN_NODE, &cfg_no_description_cmd);
+	install_element(APN_NODE, &cfg_apn_shutdown_cmd);
+	install_element(APN_NODE, &cfg_apn_no_shutdown_cmd);
+	install_element(APN_NODE, &cfg_apn_gtpu_mode_cmd);
+	install_element(APN_NODE, &cfg_apn_type_support_cmd);
+	install_element(APN_NODE, &cfg_apn_no_type_support_cmd);
+	install_element(APN_NODE, &cfg_apn_tun_dev_name_cmd);
+	install_element(APN_NODE, &cfg_apn_ipup_script_cmd);
+	install_element(APN_NODE, &cfg_apn_no_ipup_script_cmd);
+	install_element(APN_NODE, &cfg_apn_ipdown_script_cmd);
+	install_element(APN_NODE, &cfg_apn_no_ipdown_script_cmd);
+	install_element(APN_NODE, &cfg_apn_ip_prefix_cmd);
+	install_element(APN_NODE, &cfg_apn_ipv6_prefix_cmd);
+	install_element(APN_NODE, &cfg_apn_ip_dns_cmd);
+	install_element(APN_NODE, &cfg_apn_ipv6_dns_cmd);
+	install_element(APN_NODE, &cfg_apn_no_dns_cmd);
+	install_element(APN_NODE, &cfg_apn_ip_ifconfig_cmd);
+	install_element(APN_NODE, &cfg_apn_no_ip_ifconfig_cmd);
+	install_element(APN_NODE, &cfg_apn_ipv6_ifconfig_cmd);
+	install_element(APN_NODE, &cfg_apn_no_ipv6_ifconfig_cmd);
+
+	return 0;
+}
+
+static int ggsn_vty_is_config_node(struct vty *vty, int node)
+{
+	switch (node) {
+	case GGSN_NODE:
+	case APN_NODE:
+		return 1;
+	default:
+		return 0;
+	}
+}
+
+static int ggsn_vty_go_parent(struct vty *vty)
+{
+	switch (vty->node) {
+	case GGSN_NODE:
+		vty->node = CONFIG_NODE;
+		vty->index = NULL;
+		vty->index_sub = NULL;
+		break;
+	case APN_NODE:
+		vty->node = GGSN_NODE;
+		{
+			struct apn_ctx *apn = vty->index;
+			vty->index = apn->ggsn;
+			vty->index_sub = &apn->ggsn->cfg.description;
+		}
+		break;
+	}
+
+	return vty->node;
+}
+
+static const char ggsn_copyright[] =
+	"Copyright (C) 2011-2017 Harald Welte <laforge@gnumonks.org>\r\n"
+	"Copyright (C) 2012-2017 Holger Hans Peter Freyther <holger@moiji-mobile.com>\r\n"
+	"Copyright (C) 2012-2017 sysmocom - s.f.m.c. GmbH\r\n"
+	"Copyright (C) 2002-2005 Mondru AB\r\n"
+	"License GPLv2: GNU GPL version 2 <http://gnu.org/licenses/gpl-2.0.html>\r\n"
+	"This is free software: you are free to change and redistribute it.\r\n"
+	"There is NO WARRANTY, to the extent permitted by law.\r\n";
+
+struct vty_app_info g_vty_info = {
+	.name		= "OpenGGSN",
+	.version	= PACKAGE_VERSION,
+	.copyright	= ggsn_copyright,
+	.go_parent_cb	= ggsn_vty_go_parent,
+	.is_config_node	= ggsn_vty_is_config_node,
+};