ipa-proxy: add A-bis over IP generic proxy commands for VTY

This patch adds VTY commands to route IPA flows. The following
example allows to add a new route:

$ tests/./ipa_proxy_test &
<0000> ipa_proxy_test.c:74 entering main loop
$ telnet localhost 4260
ipa-proxy-test> enable
ipa-proxy-test# ipa instance input-oml bind 127.0.0.1 tcp port 8888
ipa-proxy-test# ipa instance output-oml connect 127.0.0.1 tcp port 3002
ipa-proxy-test# ipa route instance input-oml streamid 0xfe instance output-oml streamid 0xfe
ipa-proxy-test# ipa instance input-rsl bind 127.0.0.1 tcp port 8889
ipa-proxy-test# ipa instance output-rsl connect 127.0.0.1 tcp port 3003
ipa-proxy-test# ipa route instance input-rsl streamid 0xfe instance output-rsl streamid 0xfe

I'm using this to initially test this code [*].

[*] note that this requires a minor hackish patch for the
src/input/ipaccess.c driver which changes the default OML and RSL
ports to listen in 8888 and 8889 instead of the default ports,
thus, I can initially test everything from the localhost.
diff --git a/include/internal.h b/include/internal.h
index 9d16722..f19f006 100644
--- a/include/internal.h
+++ b/include/internal.h
@@ -1,6 +1,9 @@
 #ifndef _INTERNAL_H_
 #define _INTERNAL_H_
 
+/* XXX: fix this in libosmocore, we need some reserved range */
+#define IPA_NODE _LAST_OSMOVTY_NODE + 100
+
 /* talloc context for libosmo-abis. */
 extern void *libosmo_abis_ctx;
 
diff --git a/include/osmocom/abis/Makefile.am b/include/osmocom/abis/Makefile.am
index 3093ee5..6cf3510 100644
--- a/include/osmocom/abis/Makefile.am
+++ b/include/osmocom/abis/Makefile.am
@@ -1,3 +1,8 @@
-osmoabis_HEADERS = abis.h e1_input.h subchan_demux.h ipaccess.h trau_frame.h
+osmoabis_HEADERS = abis.h		\
+		   e1_input.h		\
+		   subchan_demux.h	\
+		   ipa_proxy.h		\
+		   ipaccess.h		\
+		   trau_frame.h
 
 osmoabisdir = $(includedir)/osmocom/gsm/abis
diff --git a/include/osmocom/abis/ipa_proxy.h b/include/osmocom/abis/ipa_proxy.h
new file mode 100644
index 0000000..9b4efa9
--- /dev/null
+++ b/include/osmocom/abis/ipa_proxy.h
@@ -0,0 +1,6 @@
+#ifndef _IPA_PROXY_H_
+#define _IPA_PROXY_H_
+
+void ipa_proxy_vty_init(void);
+
+#endif
diff --git a/src/Makefile.am b/src/Makefile.am
index d6f8340..d81a6c3 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -6,12 +6,13 @@
 
 INCLUDES = $(all_includes) -I$(top_srcdir)/include -I$(top_builddir)
 AM_CFLAGS= -fPIC -Wall $(LIBOSMOCORE_CFLAGS) $(LIBOSMOGSM_CFLAGS) $(LIBOSMOVTY_CFLAGS) $(COVERAGE_CFLAGS)
-AM_LDFLAGS = $(LIBOSMOCORE_LIBS) $(LIBOSMOGSM_LIBS) $(COVERAGE_LDFLAGS)
+AM_LDFLAGS = $(LIBOSMOCORE_LIBS) $(LIBOSMOGSM_LIBS) $(LIBOSMOVTY_LIBS) $(COVERAGE_LDFLAGS)
 
 lib_LTLIBRARIES = libosmoabis.la
 
 libosmoabis_la_LIBADD = input/libosmoabis-input.la
 libosmoabis_la_SOURCES = init.c \
 			 e1_input.c \
+			 ipa_proxy.c \
 			 subchan_demux.c \
 			 trau_frame.c
diff --git a/src/ipa_proxy.c b/src/ipa_proxy.c
new file mode 100644
index 0000000..980e3c7
--- /dev/null
+++ b/src/ipa_proxy.c
@@ -0,0 +1,688 @@
+#include "internal.h"
+
+#include <stdio.h>
+#include <stdbool.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+#include <time.h>
+#include <sys/fcntl.h>
+#include <sys/socket.h>
+#include <sys/ioctl.h>
+#include <arpa/inet.h>
+
+#include <osmocom/core/select.h>
+#include <osmocom/core/bitvec.h>
+#include <osmocom/gsm/tlv.h>
+#include <osmocom/core/msgb.h>
+#include <osmocom/core/logging.h>
+#include <talloc.h>
+#include <osmocom/abis/e1_input.h>
+#include <osmocom/abis/ipaccess.h>
+#include <osmocom/core/socket.h>
+#include <osmocom/abis/logging.h>
+
+#include <osmocom/abis/ipa.h>
+#include <osmocom/vty/vty.h>
+#include <osmocom/vty/command.h>
+
+static void *tall_ipa_proxy_ctx;
+
+/*
+ * data structures used by the IPA VTY commands
+ */
+static LLIST_HEAD(ipa_instance_list);
+
+enum ipa_proxy_instance_net_type {
+	IPA_INSTANCE_T_NONE,
+	IPA_INSTANCE_T_BIND,
+	IPA_INSTANCE_T_CONNECT,
+	IPA_INSTANCE_T_MAX
+};
+
+struct ipa_proxy_instance_net {
+	char					*addr;
+	uint16_t				port;
+	enum ipa_proxy_instance_net_type	type;
+};
+
+struct ipa_proxy_instance {
+	struct llist_head		head;
+#define IPA_INSTANCE_NAME		16
+	char				name[IPA_INSTANCE_NAME];
+	struct ipa_proxy_instance_net	net;
+	int				refcnt;
+};
+
+static LLIST_HEAD(ipa_proxy_route_list);
+
+/* Several routes pointing to the same instances share this. */
+struct ipa_proxy_route_shared {
+	int					refcnt;
+
+	/* this file descriptor is used to accept() new connections. */
+	struct osmo_fd				bfd;
+
+	struct {
+		struct ipa_proxy_instance	*inst;
+		struct bitvec			streamid_map;
+		uint8_t				streamid_map_data[(0xff+1)/8];
+		uint8_t				streamid[0xff];
+	} src;
+	struct {
+		struct ipa_proxy_instance	*inst;
+		struct bitvec			streamid_map;
+		uint8_t				streamid_map_data[(0xff+1)/8];
+		uint8_t				streamid[0xff];
+	} dst;
+
+	struct llist_head			conn_list;
+};
+
+/* One route is composed of two instances. */
+struct ipa_proxy_route {
+	struct llist_head			head;
+
+	struct {
+		uint8_t				streamid;
+	} src;
+	struct {
+		uint8_t				streamid;
+	} dst;
+
+	struct ipa_proxy_route_shared		*shared;
+};
+
+enum ipa_conn_state {
+	IPA_CONN_S_NONE,
+	IPA_CONN_S_CONNECTING,
+	IPA_CONN_S_CONNECTED,
+	IPA_CONN_S_MAX
+};
+
+/* One route may forward more than one connection. */
+struct ipa_proxy_conn {
+	struct llist_head		head;
+
+	struct ipa_server_peer		*src;
+	struct ipa_client_link		*dst;
+	struct ipa_proxy_route		*route;
+};
+
+/*
+ * socket callbacks used by IPA VTY commands
+ */
+static int ipa_sock_dst_cb(struct ipa_client_link *link, struct msgb *msg)
+{
+	struct ipaccess_head *hh;
+	struct ipa_proxy_conn *conn = link->data;
+
+	LOGP(DINP, LOGL_NOTICE, "received message from client side\n");
+
+	hh = (struct ipaccess_head *)msg->data;
+	/* check if we have a route for this message. */
+	if (bitvec_get_bit_pos(
+	     &conn->route->shared->dst.streamid_map,
+	     hh->proto) != ONE) {
+		LOGP(DINP, LOGL_NOTICE, "we don't have a "
+			"route for streamid 0x%x\n", hh->proto);
+		msgb_free(msg);
+		return 0;
+	}
+	/* mangle message, if required. */
+	hh->proto = conn->route->shared->src.streamid[hh->proto];
+
+	ipa_server_peer_send(conn->src, msg);
+	return 0;
+}
+
+static int ipa_sock_src_cb(struct ipa_server_peer *peer, struct msgb *msg)
+{
+	struct ipaccess_head *hh;
+	struct ipa_proxy_conn *conn = peer->data;
+
+	LOGP(DINP, LOGL_NOTICE, "received message from server side\n");
+
+	hh = (struct ipaccess_head *)msg->data;
+	/* check if we have a route for this message. */
+	if (bitvec_get_bit_pos(&conn->route->shared->src.streamid_map,
+			hh->proto) != ONE) {
+		LOGP(DINP, LOGL_NOTICE, "we don't have a "
+			"route for streamid 0x%x\n", hh->proto);
+		msgb_free(msg);
+		return 0;
+	}
+	/* mangle message, if required. */
+	hh->proto = conn->route->shared->dst.streamid[hh->proto];
+
+	ipa_client_link_send(conn->dst, msg);
+	return 0;
+}
+
+static int
+ipa_sock_src_accept_cb(struct ipa_server_link *link, int fd)
+{
+	int ret;
+	struct ipa_proxy_route *route = link->data;
+	struct ipa_proxy_conn *conn;
+
+	conn = talloc_zero(tall_ipa_proxy_ctx, struct ipa_proxy_conn);
+	if (conn == NULL) {
+		LOGP(DINP, LOGL_ERROR, "cannot allocate memory for "
+				       "origin IPA\n");
+		close(fd);
+		return ret;
+	}
+	conn->route = route;
+
+	conn->src = ipa_server_peer_create(tall_ipa_proxy_ctx, link, fd,
+					   ipa_sock_src_cb, conn);
+	if (conn->src == NULL) {
+		LOGP(DINP, LOGL_ERROR, "could not create server peer: %s\n",
+			strerror(errno));
+		return -ENOMEM;
+	}
+
+	LOGP(DINP, LOGL_NOTICE, "now trying to connect to destination\n");
+
+	conn->dst = ipa_client_link_create(NULL, NULL,
+					   route->shared->dst.inst->net.addr,
+					   route->shared->dst.inst->net.port,
+					   ipa_sock_dst_cb, conn);
+	if (conn->dst == NULL) {
+		LOGP(DINP, LOGL_ERROR, "could not create client: %s\n",
+			strerror(errno));
+		return -ENOMEM;
+	}
+	if (ipa_client_link_open(conn->dst) < 0) {
+		LOGP(DINP, LOGL_ERROR, "could not start client: %s\n",
+			strerror(errno));
+		return -ENOMEM;
+	}
+	llist_add(&conn->head, &route->shared->conn_list);
+	return ret;
+}
+
+/*
+ * VTY commands for IPA
+ */
+DEFUN(ipa_proxy, ipa_cmd, "ipa", "Configure the ipaccess proxy")
+{
+	vty->index = NULL;
+	vty->node = IPA_NODE;
+	return CMD_SUCCESS;
+}
+
+static int __ipa_instance_add(struct vty *vty, int argc, const char *argv[])
+{
+	struct ipa_proxy_instance *ipi;
+	enum ipa_proxy_instance_net_type type;
+	struct in_addr addr;
+	uint16_t port;
+
+	if (argc < 4)
+		return CMD_ERR_INCOMPLETE;
+
+	llist_for_each_entry(ipi, &ipa_instance_list, head) {
+		if (strncmp(ipi->name, argv[0], IPA_INSTANCE_NAME) != 0)
+			continue;
+
+		vty_out(vty, "%% instance `%s' already exists%s",
+			ipi->name, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	if (strncmp(argv[1], "bind", IPA_INSTANCE_NAME) == 0)
+		type = IPA_INSTANCE_T_BIND;
+	else if (strncmp(argv[1], "connect", IPA_INSTANCE_NAME) == 0)
+		type = IPA_INSTANCE_T_CONNECT;
+	else
+		return CMD_ERR_INCOMPLETE;
+
+	if (inet_aton(argv[2], &addr) < 0) {
+		vty_out(vty, "%% invalid address %s%s", argv[1], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	port = atoi(argv[3]);
+
+	ipi = talloc_zero(tall_ipa_proxy_ctx, struct ipa_proxy_instance);
+	if (ipi == NULL) {
+		vty_out(vty, "%% can't allocate memory for new instance%s",
+			VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	strncpy(ipi->name, argv[0], IPA_INSTANCE_NAME);
+	ipi->net.type = type;
+	ipi->net.addr = talloc_strdup(tall_ipa_proxy_ctx, argv[2]);
+	ipi->net.port = port;
+	llist_add_tail(&ipi->head, &ipa_instance_list);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(ipa_instance_add, ipa_instance_add_cmd,
+      "ipa instance NAME (bind|connect) IP tcp port PORT",
+      "Bind or connect instance to address and port")
+{
+	return __ipa_instance_add(vty, argc, argv);
+}
+
+DEFUN(ipa_instance_del, ipa_instance_del_cmd,
+      "no ipa instance NAME",
+      "Delete instance to address and port")
+{
+	struct ipa_proxy_instance *ipi;
+
+	if (argc < 1)
+		return CMD_ERR_INCOMPLETE;
+
+	llist_for_each_entry(ipi, &ipa_instance_list, head) {
+		if (strncmp(ipi->name, argv[0], IPA_INSTANCE_NAME) != 0)
+			continue;
+
+		if (ipi->refcnt > 0) {
+			vty_out(vty, "%% instance `%s' is in use%s",
+			ipi->name, VTY_NEWLINE);
+			return CMD_WARNING;
+		}
+		llist_del(&ipi->head);
+		talloc_free(ipi);
+		return CMD_SUCCESS;
+	}
+	vty_out(vty, "%% instance `%s' does not exist%s",
+		ipi->name, VTY_NEWLINE);
+
+	return CMD_WARNING;
+}
+
+DEFUN(ipa_instance_show, ipa_instance_show_cmd,
+      "ipa instance show", "Show existing ipaccess proxy instances")
+{
+	struct ipa_proxy_instance *this;
+
+	llist_for_each_entry(this, &ipa_instance_list, head) {
+		vty_out(vty, "instance %s %s %s tcp port %u%s",
+			this->name, this->net.addr,
+			this->net.type == IPA_INSTANCE_T_BIND ?
+				"bind" : "connect",
+			this->net.port, VTY_NEWLINE);
+	}
+	return CMD_SUCCESS;
+}
+
+static int __ipa_route_add(struct vty *vty, int argc, const char *argv[])
+{
+	struct ipa_proxy_instance *ipi = vty->index;
+	struct ipa_proxy_instance *src = NULL, *dst = NULL;
+	uint32_t src_streamid, dst_streamid;
+	struct ipa_proxy_route *route, *matching_route = NULL;
+	struct ipa_proxy_route_shared *shared = NULL;
+	int ret;
+
+	if (argc < 4)
+		return CMD_ERR_INCOMPLETE;
+
+	llist_for_each_entry(ipi, &ipa_instance_list, head) {
+		if (strncmp(ipi->name, argv[0], IPA_INSTANCE_NAME) == 0) {
+			src = ipi;
+			continue;
+		}
+		if (strncmp(ipi->name, argv[2], IPA_INSTANCE_NAME) == 0) {
+			dst = ipi;
+			continue;
+		}
+	}
+	if (src == NULL) {
+		vty_out(vty, "%% instance `%s' does not exists%s",
+			argv[0], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	if (dst == NULL) {
+		vty_out(vty, "%% instance `%s' does not exists%s",
+			argv[2], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	if (src->net.type != IPA_INSTANCE_T_BIND) {
+		vty_out(vty, "%% instance `%s' is not of bind type%s",
+			argv[0], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	if (dst->net.type != IPA_INSTANCE_T_CONNECT) {
+		vty_out(vty, "%% instance `%s' is not of connect type%s",
+			argv[2], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	src_streamid = strtoul(argv[1], NULL, 16);
+	if (src_streamid > 0xff) {
+		vty_out(vty, "%% source streamid must be "
+			     ">= 0x00 and <= 0xff%s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	dst_streamid = strtoul(argv[3], NULL, 16);
+	if (dst_streamid > 0xff) {
+		vty_out(vty, "%% destination streamid must be "
+			     ">= 0x00 and <= 0xff%s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	llist_for_each_entry(route, &ipa_proxy_route_list, head) {
+		if (route->shared->src.inst == src &&
+		    route->shared->dst.inst == dst) {
+			if (route->src.streamid == src_streamid &&
+			    route->dst.streamid == dst_streamid) {
+				vty_out(vty, "%% this route already exists%s",
+					VTY_NEWLINE);
+				return CMD_WARNING;
+			}
+			matching_route = route;
+			break;
+		}
+	}
+	/* new route for this configuration. */
+	route = talloc_zero(tall_ipa_proxy_ctx, struct ipa_proxy_route);
+	if (route == NULL) {
+		vty_out(vty, "%% can't allocate memory for new route%s",
+			VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	route->src.streamid = src_streamid;
+	route->dst.streamid = dst_streamid;
+
+	if (matching_route != NULL) {
+		/* there's already a master route for these configuration. */
+		if (matching_route->shared->src.inst != src) {
+			vty_out(vty, "%% route does not contain "
+				     "source instance `%s'%s",
+				     argv[0], VTY_NEWLINE);
+			return CMD_WARNING;
+		}
+		if (matching_route->shared->dst.inst != dst) {
+			vty_out(vty, "%% route does not contain "
+				     "destination instance `%s'%s",
+				     argv[2], VTY_NEWLINE);
+			return CMD_WARNING;
+		}
+		/* use already existing shared routing information. */
+		shared = matching_route->shared;
+	} else {
+		struct ipa_server_link *link;
+
+		/* this is a brand new route, allocate shared routing info. */
+		shared = talloc_zero(tall_ipa_proxy_ctx, struct ipa_proxy_route_shared);
+		if (shared == NULL) {
+			vty_out(vty, "%% can't allocate memory for "
+				"new route shared%s", VTY_NEWLINE);
+			return CMD_WARNING;
+		}
+		shared->src.streamid_map.data_len =
+			sizeof(shared->src.streamid_map_data);
+		shared->src.streamid_map.data =
+			shared->src.streamid_map_data;
+		shared->dst.streamid_map.data_len =
+			sizeof(shared->dst.streamid_map_data);
+		shared->dst.streamid_map.data =
+			shared->dst.streamid_map_data;
+
+		link = ipa_server_link_create(tall_ipa_proxy_ctx, NULL,
+						"0.0.0.0",
+						src->net.port,
+						ipa_sock_src_accept_cb, route);
+		if (link == NULL) {
+			vty_out(vty, "%% can't bind instance `%s' to port%s",
+				src->name, VTY_NEWLINE);
+			return CMD_WARNING;
+		}
+		if (ipa_server_link_open(link) < 0) {
+			vty_out(vty, "%% can't bind instance `%s' to port%s",
+				src->name, VTY_NEWLINE);
+			return CMD_WARNING;
+		}
+		INIT_LLIST_HEAD(&shared->conn_list);
+	}
+	route->shared = shared;
+	src->refcnt++;
+	route->shared->src.inst = src;
+	dst->refcnt++;
+	route->shared->dst.inst = dst;
+	shared->src.streamid[src_streamid] = dst_streamid;
+	shared->dst.streamid[dst_streamid] = src_streamid;
+	ret = bitvec_set_bit_pos(&shared->src.streamid_map, src_streamid, ONE);
+	if (ret < 0) {
+		vty_out(vty, "%% bad bitmask (?)%s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	ret = bitvec_set_bit_pos(&shared->dst.streamid_map, dst_streamid, ONE);
+	if (ret < 0) {
+		vty_out(vty, "%% bad bitmask (?)%s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	shared->refcnt++;
+
+	llist_add_tail(&route->head, &ipa_proxy_route_list);
+	return CMD_SUCCESS;
+}
+
+DEFUN(ipa_route_add, ipa_route_add_cmd,
+      "ipa route instance NAME streamid HEXNUM "
+		"instance NAME streamid HEXNUM", "Add IPA route")
+{
+	return __ipa_route_add(vty, argc, argv);
+}
+
+DEFUN(ipa_route_del, ipa_route_del_cmd,
+      "no ipa route instance NAME streamid HEXNUM "
+		   "instance NAME streamid HEXNUM", "Delete IPA route")
+{
+	struct ipa_proxy_instance *ipi = vty->index;
+	struct ipa_proxy_instance *src = NULL, *dst = NULL;
+	uint32_t src_streamid, dst_streamid;
+	struct ipa_proxy_route *route, *matching_route = NULL;
+	struct ipa_proxy_conn *conn, *tmp;
+
+	if (argc < 4)
+		return CMD_ERR_INCOMPLETE;
+
+	llist_for_each_entry(ipi, &ipa_instance_list, head) {
+		if (strncmp(ipi->name, argv[0], IPA_INSTANCE_NAME) == 0) {
+			src = ipi;
+			continue;
+		}
+		if (strncmp(ipi->name, argv[2], IPA_INSTANCE_NAME) == 0) {
+			dst = ipi;
+			continue;
+		}
+	}
+	if (src == NULL) {
+		vty_out(vty, "%% instance `%s' does not exists%s",
+			argv[0], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	if (dst == NULL) {
+		vty_out(vty, "%% instance `%s' does not exists%s",
+			argv[2], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	if (src->net.type != IPA_INSTANCE_T_BIND) {
+		vty_out(vty, "%% instance `%s' is not of bind type%s",
+			argv[0], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	if (dst->net.type != IPA_INSTANCE_T_CONNECT) {
+		vty_out(vty, "%% instance `%s' is not of connect type%s",
+			argv[2], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	src_streamid = strtoul(argv[1], NULL, 16);
+	if (src_streamid > 0xff) {
+		vty_out(vty, "%% source streamid must be "
+			     ">= 0x00 and <= 0xff%s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	dst_streamid = strtoul(argv[3], NULL, 16);
+	if (dst_streamid > 0xff) {
+		vty_out(vty, "%% destination streamid must be "
+			     ">= 0x00 and <= 0xff%s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	llist_for_each_entry(route, &ipa_proxy_route_list, head) {
+		if (route->shared->src.inst == src &&
+		    route->shared->dst.inst == dst &&
+		    route->src.streamid == src_streamid &&
+		    route->dst.streamid == dst_streamid) {
+			matching_route = route;
+			break;
+		}
+	}
+	if (matching_route == NULL) {
+		vty_out(vty, "%% no route with that configuration%s",
+			VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	/* delete this route from list. */
+	llist_del(&matching_route->head);
+
+	if (--matching_route->shared->refcnt == 0) {
+		/* nobody else using this route, release all resources. */
+		llist_for_each_entry_safe(conn, tmp,
+				&matching_route->shared->conn_list, head) {
+			ipa_server_peer_destroy(conn->src);
+			llist_del(&conn->head);
+			talloc_free(conn);
+		}
+		osmo_fd_unregister(&route->shared->bfd);
+		close(route->shared->bfd.fd);
+		route->shared->bfd.fd = -1;
+
+		talloc_free(route->shared);
+	} else {
+		/* otherwise, revert the mapping that this route applies. */
+		bitvec_set_bit_pos(&matching_route->shared->src.streamid_map,
+				   src_streamid, ZERO);
+		bitvec_set_bit_pos(&matching_route->shared->dst.streamid_map,
+				   dst_streamid, ZERO);
+		matching_route->shared->src.streamid[src_streamid] = 0x00;
+		matching_route->shared->dst.streamid[dst_streamid] = 0x00;
+	}
+	matching_route->shared->src.inst->refcnt--;
+	matching_route->shared->dst.inst->refcnt--;
+	talloc_free(matching_route);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(ipa_route_show, ipa_route_show_cmd,
+      "ipa route show", "Show existing ipaccess proxy routes")
+{
+	struct ipa_proxy_route *this;
+
+	llist_for_each_entry(this, &ipa_proxy_route_list, head) {
+		vty_out(vty, "route instance %s streamid 0x%.2x "
+			           "instance %s streamid 0x%.2x%s",
+			this->shared->src.inst->name, this->src.streamid,
+			this->shared->dst.inst->name, this->dst.streamid,
+			VTY_NEWLINE);
+	}
+	return CMD_SUCCESS;
+}
+
+/*
+ * Config for ipaccess-proxy
+ */
+DEFUN(ipa_cfg, ipa_cfg_cmd, "ipa", "Configure the ipaccess proxy")
+{
+	vty->index = NULL;
+	vty->node = IPA_NODE;
+	return CMD_SUCCESS;
+}
+
+/* all these below look like enable commands, but without the ipa prefix. */
+DEFUN(ipa_route_cfg_add, ipa_route_cfg_add_cmd,
+      "route instance NAME streamid HEXNUM "
+	    "instance NAME streamid HEXNUM", "Add IPA route")
+{
+	return __ipa_route_add(vty, argc, argv);
+}
+
+DEFUN(ipa_instance_cfg_add, ipa_instance_cfg_add_cmd,
+      "instance NAME (bind|connect) IP tcp port PORT",
+      "Bind or connect instance to address and port")
+{
+	return __ipa_instance_add(vty, argc, argv);
+}
+
+struct cmd_node ipa_node = {
+	IPA_NODE,
+	"%s(ipa)#",
+	1,
+};
+
+static int ipa_cfg_write(struct vty *vty)
+{
+	bool heading = false;
+	struct ipa_proxy_instance *inst;
+	struct ipa_proxy_route *route;
+
+	llist_for_each_entry(inst, &ipa_instance_list, head) {
+		if (!heading) {
+			vty_out(vty, "ipa%s", VTY_NEWLINE);
+			heading = true;
+		}
+		vty_out(vty, " instance %s %s %s tcp port %u%s",
+			inst->name,
+			inst->net.type == IPA_INSTANCE_T_BIND ?
+				"bind" : "connect",
+			inst->net.addr,
+			inst->net.port, VTY_NEWLINE);
+	}
+	llist_for_each_entry(route, &ipa_proxy_route_list, head) {
+		vty_out(vty, " route instance %s streamid 0x%.2x "
+				    "instance %s streamid 0x%.2x%s",
+			route->shared->src.inst->name, route->src.streamid,
+			route->shared->dst.inst->name, route->dst.streamid,
+			VTY_NEWLINE);
+	}
+	return CMD_SUCCESS;
+}
+
+DEFUN(ournode_exit,
+      ournode_exit_cmd, "exit", "Exit current mode and down to previous mode\n")
+{
+        switch (vty->node) {
+        case IPA_NODE:
+                vty->node = CONFIG_NODE;
+                vty->index = NULL;
+                break;
+	}
+	return CMD_SUCCESS;
+}
+
+DEFUN(ournode_end,
+      ournode_end_cmd, "end", "End current mode and change to enable mode.\n")
+{
+	switch (vty->node) {
+	case IPA_NODE:
+		break;
+	}
+	return CMD_SUCCESS;
+}
+
+void ipa_proxy_vty_init(void)
+{
+	tall_ipa_proxy_ctx =
+		talloc_named_const(libosmo_abis_ctx, 1, "ipa_proxy");
+
+	install_element(ENABLE_NODE, &ipa_cmd);
+	install_element(ENABLE_NODE, &ipa_instance_add_cmd);
+	install_element(ENABLE_NODE, &ipa_instance_del_cmd);
+	install_element(ENABLE_NODE, &ipa_instance_show_cmd);
+	install_element(ENABLE_NODE, &ipa_route_add_cmd);
+	install_element(ENABLE_NODE, &ipa_route_del_cmd);
+	install_element(ENABLE_NODE, &ipa_route_show_cmd);
+
+	install_element(CONFIG_NODE, &ipa_cfg_cmd);
+	install_node(&ipa_node, ipa_cfg_write);
+	install_default(IPA_NODE);
+	install_element(IPA_NODE, &ournode_exit_cmd);
+	install_element(IPA_NODE, &ournode_end_cmd);
+	install_element(IPA_NODE, &ipa_instance_cfg_add_cmd);
+	install_element(IPA_NODE, &ipa_route_cfg_add_cmd);
+}
diff --git a/tests/Makefile.am b/tests/Makefile.am
index c27342d..5cb6447 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -2,7 +2,9 @@
 AM_CFLAGS=-Wall -g $(LIBOSMOCORE_CFLAGS) $(LIBOSMOGSM_CFLAGS) $(COVERAGE_CFLAGS)
 AM_LDFLAGS = $(COVERAGE_LDFLAGS)
 
-noinst_PROGRAMS = e1inp_ipa_bsc_test e1inp_ipa_bts_test
+noinst_PROGRAMS = e1inp_ipa_bsc_test	\
+		  e1inp_ipa_bts_test	\
+		  ipa_proxy_test
 
 e1inp_ipa_bsc_test_SOURCES = e1inp_ipa_bsc_test.c
 e1inp_ipa_bsc_test_LDADD = $(top_builddir)/src/libosmoabis.la \
@@ -11,3 +13,7 @@
 e1inp_ipa_bts_test_SOURCES = e1inp_ipa_bts_test.c
 e1inp_ipa_bts_test_LDADD = $(top_builddir)/src/libosmoabis.la \
 			$(LIBOSMOCORE_LIBS) $(LIBOSMOGSM_LIBS)
+
+ipa_proxy_test_SOURCES = ipa_proxy_test.c
+ipa_proxy_test_LDADD = $(top_builddir)/src/libosmoabis.la \
+			$(LIBOSMOCORE_LIBS) $(LIBOSMOGSM_LIBS)
diff --git a/tests/ipa_proxy_test.c b/tests/ipa_proxy_test.c
new file mode 100644
index 0000000..cea8e2b
--- /dev/null
+++ b/tests/ipa_proxy_test.c
@@ -0,0 +1,79 @@
+#include <stdio.h>
+#include <talloc.h>
+#include <osmocom/abis/abis.h>
+#include <osmocom/abis/e1_input.h>
+#include <osmocom/abis/ipa_proxy.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/core/application.h>
+#include <osmocom/vty/vty.h>
+#include <osmocom/vty/command.h>
+#include <osmocom/vty/telnet_interface.h>
+#include "internal.h"
+#include "config.h"
+
+static void *tall_test;
+
+#define DIPA_PROXY_TEST OSMO_LOG_SS_APPS
+
+struct log_info_cat ipa_proxy_test_cat[] = {
+	[DIPA_PROXY_TEST] = {
+		.name = "DINP_IPA_PROXY_TEST",
+		.description = "IPA proxy test",
+		.color = "\033[1;35m",
+		.enabled = 1, .loglevel = LOGL_NOTICE,
+	},
+};
+
+const struct log_info ipa_proxy_test_log_info = {
+	.filter_fn = NULL,
+	.cat = ipa_proxy_test_cat,
+	.num_cat = ARRAY_SIZE(ipa_proxy_test_cat),
+};
+
+static int bsc_vty_is_config_node(struct vty *vty, int node)
+{
+	switch(node) {
+	case IPA_NODE:
+		return 1;
+		break;
+	}
+	return 0;
+}
+
+static enum node_type bsc_vty_go_parent(struct vty *vty)
+{
+	switch (vty->node) {
+	case IPA_NODE:
+		vty->node = VIEW_NODE;
+		break;
+	}
+	return vty->node;
+}
+
+static struct vty_app_info vty_info = {
+	.name		= "ipa-proxy-test",
+	.version	= PACKAGE_VERSION,
+	.go_parent_cb	= bsc_vty_go_parent,
+	.is_config_node	= bsc_vty_is_config_node,
+};
+
+#define IPA_PROXY_TEST_TELNET_PORT	4260
+
+int main(void)
+{
+	tall_test = talloc_named_const(NULL, 1, "ipa proxy test");
+	libosmo_abis_init(tall_test);
+
+	osmo_init_logging(&ipa_proxy_test_log_info);
+
+	vty_init(&vty_info);
+	ipa_proxy_vty_init();
+
+	telnet_init(tall_test, NULL, IPA_PROXY_TEST_TELNET_PORT);
+
+	LOGP(DIPA_PROXY_TEST, LOGL_NOTICE, "entering main loop\n");
+
+	while (1) {
+		osmo_select_main(0);
+	}
+}