Add initial remsim-bankd skeleton

This is not a complete program yet, but a rough initial skeleton with
the key data structures in place, as well as the thread / locking model
in place.

Change-Id: I5ad5a1a4918b8eacdaeb7e709ff05dc056346752
diff --git a/src/Makefile.am b/src/Makefile.am
index 05ac9ae..f1731fa 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -1,10 +1,24 @@
 SUBDIRS = rspro
 
 AM_CFLAGS = -Wall -I$(top_srcdir)/include -I$(top_builddir)/include \
-	    $(OSMOCORE_CFLAGS) $(ASN1C_CFLAGS)
+	    $(OSMOCORE_CFLAGS) $(OSMOGSM_CFLAGS) $(OSMOABIS_CFLAGS) \
+	    $(ASN1C_CFLAGS) $(PCSC_CFLAGS)
 
 RSPRO_LIBVERSION=0:0:0
 lib_LTLIBRARIES = libosmo-rspro.la
 libosmo_rspro_la_LDFLAGS = $(AM_LDFLAGS) -version-info $(RSPRO_LIBVERSION)
-libosmo_rspro_la_LIBADD = $(OSMOCORE_LIBS) $(ASN1C_LIBS) rspro/libosmo-asn1-rspro.la
-libosmo_rspro_la_SOURCES = rspro_util.c
+libosmo_rspro_la_LIBADD = $(OSMOCORE_LIBS) $(OSMOGSM_LIBS) $(OSMOABIS_LIBS) \
+			  $(ASN1C_LIBS) rspro/libosmo-asn1-rspro.la
+libosmo_rspro_la_SOURCES = rspro_util.c rspro_client.c
+
+noinst_HEADERS = bankd.h internal.h
+
+bin_PROGRAMS = pcsc_test remsim-bankd
+
+pcsc_test_SOURCES = driver_core.c driver_pcsc.c main.c
+pcsc_test_LDADD = $(OSMOCORE_LIBS) \
+		  $(ASN1C_LIBS) $(PCSC_LIBS) libosmo-rspro.la
+
+remsim_bankd_SOURCES = bankd_slotmap.c bankd_main.c
+remsim_bankd_LDADD = $(OSMOCORE_LIBS) \
+		     $(ASN1C_LIBS) $(PCSC_LIBS) libosmo-rspro.la
diff --git a/src/bankd.h b/src/bankd.h
new file mode 100644
index 0000000..eabf132
--- /dev/null
+++ b/src/bankd.h
@@ -0,0 +1,118 @@
+#pragma once
+
+#include <stdbool.h>
+#include <sys/socket.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+
+#include <pthread.h>
+
+#include <wintypes.h>
+#include <winscard.h>
+
+#include <osmocom/core/linuxlist.h>
+
+struct bankd;
+
+struct bank_slot {
+	uint16_t bank_id;
+	uint16_t slot_nr;
+};
+
+static inline bool bank_slot_equals(const struct bank_slot *a, const struct bank_slot *b)
+{
+	if (a->bank_id == b->bank_id && a->slot_nr == b->slot_nr)
+		return true;
+	else
+		return false;
+}
+
+struct client_slot {
+	uint16_t client_id;
+	uint16_t slot_nr;
+};
+
+static inline bool client_slot_equals(const struct client_slot *a, const struct client_slot *b)
+{
+	if (a->client_id == b->client_id && a->slot_nr == b->slot_nr)
+		return true;
+	else
+		return false;
+}
+
+/* slot mappings are created / removed by the server */
+struct bankd_slot_mapping {
+	/* global lits of bankd slot mappings */
+	struct llist_head list;
+	/* slot on bank side */
+	struct bank_slot bank;
+	/* slot on client side */
+	struct client_slot client;
+};
+
+/* thread-safe lookup of map by client:slot */
+struct bankd_slot_mapping *bankd_slotmap_by_client(struct bankd *bankd,
+						   const struct client_slot *client);
+
+/* thread-safe lookup of map by bank:slot */
+struct bankd_slot_mapping *bankd_slotmap_by_bank(struct bankd *bankd, const struct bank_slot *bank);
+
+/* thread-safe creating of a new bank<->client map */
+int bankd_slotmap_add(struct bankd *bankd, const struct bank_slot *bank,
+		      const struct client_slot *client);
+
+/* thread-safe removal of a bank<->client map */
+void bankd_slotmap_del(struct bankd *bankd, struct bankd_slot_mapping *map);
+
+
+/* bankd worker instance; one per card/slot, includes thread */
+struct bankd_worker {
+	/* global list of workers */
+	struct llist_head list;
+	/* back-pointer to bankd */
+	struct bankd *bankd;
+
+	/* slot number we are representing */
+	struct bank_slot slot;
+
+	/* thread of this worker. */
+	pthread_t thread;
+
+	/* File descriptor of the TCP connection to the remsim-client (modem) */
+	struct {
+		int fd;
+		struct sockaddr_storage peer_addr;
+		socklen_t peer_addr_len;
+	} client;
+
+	struct {
+		const char *name;
+		union {
+			struct {
+				/* PC/SC context / application handle */
+				SCARDCONTEXT hContext;
+				/* PC/SC card handle */
+				SCARDHANDLE hCard;
+			} pcsc;
+		};
+	} reader;
+};
+
+
+/* global bank deamon */
+struct bankd {
+	struct {
+		uint16_t bank_id;
+	} cfg;
+
+	/* TCP socket at which we are listening */
+	int accept_fd;
+
+	/* list of slit mappings. only ever modified in main thread! */
+	struct llist_head slot_mappings;
+	pthread_rwlock_t slot_mappings_rwlock;
+
+	/* list of bankd_workers. accessed/modified by multiple threads; protected by mutex */
+	struct llist_head workers;
+	pthread_mutex_t workers_mutex;
+};
diff --git a/src/bankd_main.c b/src/bankd_main.c
new file mode 100644
index 0000000..f6eb64f
--- /dev/null
+++ b/src/bankd_main.c
@@ -0,0 +1,285 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <unistd.h>
+
+#include <pthread.h>
+
+#include <wintypes.h>
+#include <winscard.h>
+#include <pcsclite.h>
+
+#include <osmocom/core/linuxlist.h>
+
+#include <osmocom/gsm/ipa.h>
+#include <osmocom/gsm/protocol/ipaccess.h>
+
+#include <asn1c/asn_application.h>
+#include <osmocom/rspro/RsproPDU.h>
+
+#include "bankd.h"
+
+__thread void *talloc_asn1_ctx;
+
+static void *worker_main(void *arg);
+
+/***********************************************************************
+* bankd core / main thread
+***********************************************************************/
+
+static void bankd_init(struct bankd *bankd)
+{
+	/* intialize members of 'bankd' */
+	INIT_LLIST_HEAD(&bankd->slot_mappings);
+	pthread_rwlock_init(&bankd->slot_mappings_rwlock, NULL);
+	INIT_LLIST_HEAD(&bankd->workers);
+	pthread_mutex_init(&bankd->workers_mutex, NULL);
+}
+
+/* create + start a new bankd_worker thread */
+static struct bankd_worker *bankd_create_worker(struct bankd *bankd)
+{
+	struct bankd_worker *worker;
+	int rc;
+
+	worker = talloc_zero(bankd, struct bankd_worker);
+	if (!worker)
+		return NULL;
+
+	worker->bankd = bankd;
+
+	/* in the initial state, the worker has no client.fd, bank_slot or pcsc handle yet */
+
+	rc = pthread_create(&worker->thread, NULL, worker_main, worker);
+	if (rc != 0) {
+		talloc_free(worker);
+		return NULL;
+	}
+
+	pthread_mutex_lock(&bankd->workers_mutex);
+	llist_add_tail(&worker->list, &bankd->workers);
+	pthread_mutex_unlock(&bankd->workers_mutex);
+
+	return worker;
+}
+
+static bool terminate = false;
+
+int main(int argc, char **argv)
+{
+	struct bankd *bankd = talloc_zero(NULL, struct bankd);
+	int i, rc;
+
+	OSMO_ASSERT(bankd);
+	bankd_init(bankd);
+
+	for (i = 0; i < 10; i++) {
+		struct bankd_worker *w;
+		w = bankd_create_worker(bankd);
+		if (!w)
+			exit(21);
+	}
+
+	while (1) {
+		if (terminate)
+			break;
+	}
+
+	talloc_free(bankd);
+	exit(0);
+}
+
+
+
+/***********************************************************************
+ * bankd worker thread
+ ***********************************************************************/
+
+#define PCSC_ERROR(rv, text) \
+if (rv != SCARD_S_SUCCESS) { \
+	fprintf(stderr, text ": %s (0x%lX)\n", pcsc_stringify_error(rv), rv); \
+	goto end; \
+} else { \
+        printf(text ": OK\n\n"); \
+}
+
+
+static void worker_cleanup(void *arg)
+{
+	struct bankd_worker *worker = (struct bankd_worker *) arg;
+	struct bankd *bankd = worker->bankd;
+
+	/* FIXME: should we still do this? in the thread ?!? */
+	pthread_mutex_lock(&bankd->workers_mutex);
+	llist_del(&worker->list);
+	talloc_free(worker);	/* FIXME: is this safe? */
+	pthread_mutex_unlock(&bankd->workers_mutex);
+}
+
+
+#if 0
+/* function running inside a worker thread; doing some initialization */
+static void worker_init(struct bankd_worker *worker)
+{
+	int rc;
+
+	/* push cleanup helper */
+	pthread_cleanup_push(&worker_cleanup, worker);
+
+	/* The PC/SC context must be created inside the thread where we'll later use it */
+	rc = SCardEstablishContext(SCARD_SCOPE_SYSTEM, NULL, NULL, &worker->reader.pcsc.hContext);
+	PCSC_ERROR(rc, "SCardEstablishContext")
+
+	rc = SCardConnect(worker->reader.pcsc.hContext, worker->reader.name, SCARD_SHARE_SHARED,
+			  SCARD_PROTOCOL_T0, &worker->reader.pcsc.hCard, NULL);
+	PCSC_ERROR(rc, "SCardConnect")
+
+	return;
+end:
+	pthread_exit(NULL);
+}
+#endif
+
+
+static int blocking_ipa_read(int fd, uint8_t *buf, unsigned int buf_size)
+{
+	struct ipaccess_head *hh;
+	uint16_t len;
+	int needed, rc;
+
+	if (buf_size < sizeof(*hh))
+		return -1;
+
+	hh = (struct ipaccess_head *) buf;
+
+	/* 1) blocking read from the socket (IPA header) */
+	rc = read(fd, buf, sizeof(*hh));
+	if (rc < sizeof(*hh))
+		return -2;
+
+	len = ntohs(hh->len);
+	needed = len; //- sizeof(*hh);
+
+	/* 2) blocking read from the socket (payload) */
+	rc = read(fd, buf+sizeof(*hh), needed);
+	if (rc < needed)
+		return -3;
+
+	return len;
+}
+
+/* handle one incoming RSPRO message from a client inside a worker thread */
+static int worker_handle_rspro(struct bankd_worker *worker, const RsproPDU_t *pdu)
+{
+	switch (pdu->msg.present) {
+	case RsproPDUchoice_PR_connectClientReq:
+		/* FIXME */
+		break;
+	case RsproPDUchoice_PR_tpduModemToCard:
+		/* FIXME */
+		break;
+	case RsproPDUchoice_PR_clientSlotStatusInd:
+		/* FIXME */
+		break;
+	default:
+		return -100;
+	}
+
+	return 0;
+}
+
+/* body of the main transceive loop */
+static int worker_transceive_loop(struct bankd_worker *worker)
+{
+	struct ipaccess_head *hh;
+	struct ipaccess_head_ext *hh_ext;
+	uint8_t buf[65536]; /* maximum length expressed in 16bit length field */
+	asn_dec_rval_t rval;
+	int data_len, rc;
+	RsproPDU_t *pdu;
+
+	/* 1) blocking read of entire IPA message from the socket */
+	rc = blocking_ipa_read(worker->client.fd, buf, sizeof(buf));
+	if (rc < 0)
+		return rc;
+	data_len = rc;
+
+	hh = (struct ipaccess_head *) buf;
+	if (hh->proto != IPAC_PROTO_OSMO)
+		return -4;
+
+	hh_ext = (struct ipaccess_head_ext *) buf + sizeof(*hh);
+	if (data_len < sizeof(*hh_ext))
+		return -5;
+	data_len -= sizeof(*hh_ext);
+	if (hh_ext->proto != IPAC_PROTO_EXT_RSPRO)
+		return -6;
+
+	/* 2) ASN1 BER decode of the message */
+	rval = ber_decode(NULL, &asn_DEF_RsproPDU, (void **) &pdu, hh_ext->data, data_len);
+	if (rval.code != RC_OK)
+		return -7;
+
+	/* 3) handling of the message, possibly resulting in PCSC commands */
+	rc = worker_handle_rspro(worker, pdu);
+	ASN_STRUCT_FREE(asn_DEF_RsproPDU, pdu);
+	if (rc < 0)
+		return rc;
+
+	/* everything OK if we reach here */
+	return 0;
+}
+
+/* worker thread main function */
+static void *worker_main(void *arg)
+{
+	struct bankd_worker *worker = (struct bankd_worker *) arg;
+	void *top_ctx;
+	int rc;
+
+	/* not permitted in multithreaded environment */
+	talloc_disable_null_tracking();
+	top_ctx = talloc_named_const(NULL, 0, "top");
+	talloc_asn1_ctx = talloc_named_const(top_ctx, 0, "asn1");
+
+	/* push cleanup helper */
+	pthread_cleanup_push(&worker_cleanup, worker);
+
+	/* we continuously perform the same loop here, recycling the worker thread
+	 * once the client connection is gone or we have some trouble with the card/reader */
+	while (1) {
+		worker->client.peer_addr_len = sizeof(worker->client.peer_addr);
+
+		/* first wait for an incoming TCP connection */
+		rc = accept(worker->bankd->accept_fd, (struct sockaddr *) &worker->client.peer_addr,
+			    &worker->client.peer_addr_len);
+		if (rc < 0) {
+			continue;
+		}
+		worker->client.fd = rc;
+
+		/* run the main worker transceive loop body until there was some error */
+		while (1) {
+			rc = worker_transceive_loop(worker);
+			if (rc < 0)
+				break;
+		}
+
+		/* clean-up: reset to sane state */
+		if (worker->reader.pcsc.hCard) {
+			SCardDisconnect(worker->reader.pcsc.hCard, SCARD_UNPOWER_CARD);
+			worker->reader.pcsc.hCard = 0;
+		}
+		if (worker->reader.pcsc.hContext) {
+			SCardReleaseContext(worker->reader.pcsc.hContext);
+			worker->reader.pcsc.hContext = 0;
+		}
+		if (worker->client.fd >= 0)
+			close(worker->client.fd);
+		worker->client.fd = -1;
+	}
+
+	pthread_cleanup_pop(1);
+	talloc_free(top_ctx);
+	pthread_exit(NULL);
+}
diff --git a/src/bankd_slotmap.c b/src/bankd_slotmap.c
new file mode 100644
index 0000000..373399b
--- /dev/null
+++ b/src/bankd_slotmap.c
@@ -0,0 +1,100 @@
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <errno.h>
+
+#include <pthread.h>
+
+#include <talloc.h>
+
+#include <osmocom/core/linuxlist.h>
+
+#include "bankd.h"
+
+/* thread-safe lookup of map by client:slot */
+struct bankd_slot_mapping *bankd_slotmap_by_client(struct bankd *bankd, const struct client_slot *client)
+{
+	struct bankd_slot_mapping *map;
+
+	pthread_rwlock_rdlock(&bankd->slot_mappings_rwlock);
+	llist_for_each_entry(map, &bankd->slot_mappings, list) {
+		if (client_slot_equals(&map->client, client)) {
+			pthread_rwlock_unlock(&bankd->slot_mappings_rwlock);
+			return map;
+		}
+	}
+	pthread_rwlock_unlock(&bankd->slot_mappings_rwlock);
+	return NULL;
+}
+
+/* thread-safe lookup of map by bank:slot */
+struct bankd_slot_mapping *bankd_slotmap_by_bank(struct bankd *bankd, const struct bank_slot *bank)
+{
+	struct bankd_slot_mapping *map;
+
+	pthread_rwlock_rdlock(&bankd->slot_mappings_rwlock);
+	llist_for_each_entry(map, &bankd->slot_mappings, list) {
+		if (bank_slot_equals(&map->bank, bank)) {
+			pthread_rwlock_unlock(&bankd->slot_mappings_rwlock);
+			return map;
+		}
+	}
+	pthread_rwlock_unlock(&bankd->slot_mappings_rwlock);
+	return NULL;
+
+}
+
+/* thread-safe creating of a new bank<->client map */
+int bankd_slotmap_add(struct bankd *bankd, const struct bank_slot *bank, const struct client_slot *client)
+{
+	struct bankd_slot_mapping *map;
+
+	/* We assume a single thread (main thread) will ever update the mappings,
+	 * and hence we don't have any races by first grabbing + releasing the read
+	 * lock twice before grabbing the writelock below */
+
+	map = bankd_slotmap_by_bank(bankd, bank);
+	if (map) {
+		fprintf(stderr, "BANKD %u:%u already in use, cannot add new map\n",
+			bank->bank_id, bank->slot_nr);
+		return -EBUSY;
+	}
+
+	map = bankd_slotmap_by_client(bankd, client);
+	if (map) {
+		fprintf(stderr, "CLIENT %u:%u already in use, cannot add new map\n",
+			client->client_id, client->slot_nr);
+		return -EBUSY;
+	}
+
+	/* allocate new mapping and add to list of mappings */
+	map = talloc_zero(bankd, struct bankd_slot_mapping);
+	if (!map)
+		return -ENOMEM;
+
+	map->bank = *bank;
+	map->client = *client;
+
+	pthread_rwlock_wrlock(&bankd->slot_mappings_rwlock);
+	llist_add_tail(&map->list, &bankd->slot_mappings);
+	pthread_rwlock_unlock(&bankd->slot_mappings_rwlock);
+
+	printf("Added Slot Map B(%u:%u) <-> C(%u:%u)\n",
+		map->client.client_id, map->client.slot_nr, map->bank.bank_id, map->bank.slot_nr);
+
+	return 0;
+}
+
+/* thread-safe removal of a bank<->client map */
+void bankd_slotmap_del(struct bankd *bankd, struct bankd_slot_mapping *map)
+{
+	printf("Deleting Slot Map B(%u:%u) <-> C(%u:%u)\n",
+		map->client.client_id, map->client.slot_nr, map->bank.bank_id, map->bank.slot_nr);
+
+	pthread_rwlock_wrlock(&bankd->slot_mappings_rwlock);
+	llist_del(&map->list);
+	pthread_rwlock_unlock(&bankd->slot_mappings_rwlock);
+
+	talloc_free(map);
+}
diff --git a/src/driver_pcsc.c b/src/driver_pcsc.c
index d028e55..5102512 100644
--- a/src/driver_pcsc.c
+++ b/src/driver_pcsc.c
@@ -83,6 +83,7 @@
 
 static int pcsc_reader_open_slot(struct card_reader_slot *slot)
 {
+#if 0
 	struct osim_card_hdl *card;
 	LONG rc;
 
@@ -101,7 +102,8 @@
 	rh->card = card;
 
 end:
-	return NULL;
+#endif
+	return -1;
 }
 
 
diff --git a/src/main.c b/src/main.c
index 977785c..67b7a41 100644
--- a/src/main.c
+++ b/src/main.c
@@ -36,7 +36,12 @@
 
 #include "internal.h"
 
+static void *g_ctx;
+__thread void *talloc_asn1_ctx;
+
 int main(int argc, char **argv)
 {
-	card_readers_probe(NULL);
+	g_ctx = talloc_named_const(NULL, 0, "main");
+	talloc_asn1_ctx = talloc_named_const(g_ctx, 0, "asn1_context");
+	card_readers_probe(g_ctx);
 }
diff --git a/src/rspro_client.c b/src/rspro_client.c
new file mode 100644
index 0000000..dcc9ed9
--- /dev/null
+++ b/src/rspro_client.c
@@ -0,0 +1,296 @@
+/* Generic Subscriber Update Protocol client */
+
+/* (C) 2018 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 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 <osmocom/rspro/rspro_client.h>
+
+#include <osmocom/abis/ipa.h>
+#include <osmocom/gsm/protocol/ipaccess.h>
+#include <osmocom/core/msgb.h>
+#include <osmocom/core/logging.h>
+
+#include <errno.h>
+#include <string.h>
+
+static void start_test_procedure(struct osmo_rspro_client *rsproc);
+
+static void rspro_client_send_ping(struct osmo_rspro_client *rsproc)
+{
+	struct msgb *msg = osmo_rspro_client_msgb_alloc();
+
+	msg->l2h = msgb_put(msg, 1);
+	msg->l2h[0] = IPAC_MSGT_PING;
+	ipa_msg_push_header(msg, IPAC_PROTO_IPACCESS);
+	ipa_client_conn_send(rsproc->link, msg);
+}
+
+static int rspro_client_connect(struct osmo_rspro_client *rsproc)
+{
+	int rc;
+
+	if (rsproc->is_connected)
+		return 0;
+
+	if (osmo_timer_pending(&rsproc->connect_timer)) {
+		LOGP(DLRSPRO, LOGL_DEBUG,
+		     "RSPRO connect: connect timer already running\n");
+		osmo_timer_del(&rsproc->connect_timer);
+	}
+
+	if (osmo_timer_pending(&rsproc->ping_timer)) {
+		LOGP(DLRSPRO, LOGL_DEBUG,
+		     "RSPRO connect: ping timer already running\n");
+		osmo_timer_del(&rsproc->ping_timer);
+	}
+
+	if (ipa_client_conn_clear_queue(rsproc->link) > 0)
+		LOGP(DLRSPRO, LOGL_DEBUG, "RSPRO connect: discarded stored messages\n");
+
+	rc = ipa_client_conn_open(rsproc->link);
+
+	if (rc >= 0) {
+		LOGP(DLRSPRO, LOGL_NOTICE, "RSPRO connecting to %s:%d\n",
+		     rsproc->link->addr, rsproc->link->port);
+		return 0;
+	}
+
+	LOGP(DLRSPRO, LOGL_ERROR, "RSPRO failed to connect to %s:%d: %s\n",
+	     rsproc->link->addr, rsproc->link->port, strerror(-rc));
+
+	if (rc == -EBADF || rc == -ENOTSOCK || rc == -EAFNOSUPPORT ||
+	    rc == -EINVAL)
+		return rc;
+
+	osmo_timer_schedule(&rsproc->connect_timer,
+			    OSMO_RSPRO_CLIENT_RECONNECT_INTERVAL, 0);
+
+	LOGP(DLRSPRO, LOGL_INFO, "Scheduled timer to retry RSPRO connect to %s:%d\n",
+	     rsproc->link->addr, rsproc->link->port);
+
+	return 0;
+}
+
+static void connect_timer_cb(void *rsproc_)
+{
+	struct osmo_rspro_client *rsproc = rsproc_;
+
+	if (rsproc->is_connected)
+		return;
+
+	rspro_client_connect(rsproc);
+}
+
+static void client_send(struct osmo_rspro_client *rsproc, int proto_ext,
+			struct msgb *msg_tx)
+{
+	ipa_prepend_header_ext(msg_tx, proto_ext);
+	ipa_msg_push_header(msg_tx, IPAC_PROTO_OSMO);
+	ipa_client_conn_send(rsproc->link, msg_tx);
+	/* msg_tx is now queued and will be freed. */
+}
+
+static void rspro_client_updown_cb(struct ipa_client_conn *link, int up)
+{
+	struct osmo_rspro_client *rsproc = link->data;
+
+	LOGP(DLRSPRO, LOGL_INFO, "RSPRO link to %s:%d %s\n",
+		     link->addr, link->port, up ? "UP" : "DOWN");
+
+	rsproc->is_connected = up;
+
+	if (up) {
+		start_test_procedure(rsproc);
+		osmo_timer_del(&rsproc->connect_timer);
+	} else {
+		osmo_timer_del(&rsproc->ping_timer);
+
+		osmo_timer_schedule(&rsproc->connect_timer,
+				    OSMO_RSPRO_CLIENT_RECONNECT_INTERVAL, 0);
+	}
+}
+
+static int rspro_client_read_cb(struct ipa_client_conn *link, struct msgb *msg)
+{
+	struct ipaccess_head *hh = (struct ipaccess_head *) msg->data;
+	struct ipaccess_head_ext *he = (struct ipaccess_head_ext *) msgb_l2(msg);
+	struct osmo_rspro_client *rsproc = (struct osmo_rspro_client *)link->data;
+	int rc;
+	struct ipaccess_unit ipa_dev = {
+		/* see rspro_client_create() on const vs non-const */
+		.unit_name = (char*)rsproc->unit_name,
+	};
+
+	OSMO_ASSERT(ipa_dev.unit_name);
+
+	msg->l2h = &hh->data[0];
+
+	rc = ipaccess_bts_handle_ccm(link, &ipa_dev, msg);
+
+	if (rc < 0) {
+		LOGP(DLRSPRO, LOGL_NOTICE,
+		     "RSPRO received an invalid IPA/CCM message from %s:%d\n",
+		     link->addr, link->port);
+		/* Link has been closed */
+		rsproc->is_connected = 0;
+		msgb_free(msg);
+		return -1;
+	}
+
+	if (rc == 1) {
+		uint8_t msg_type = *(msg->l2h);
+		/* CCM message */
+		if (msg_type == IPAC_MSGT_PONG) {
+			LOGP(DLRSPRO, LOGL_DEBUG, "RSPRO receiving PONG\n");
+			rsproc->got_ipa_pong = 1;
+		}
+
+		msgb_free(msg);
+		return 0;
+	}
+
+	if (hh->proto != IPAC_PROTO_OSMO)
+		goto invalid;
+
+	if (!he || msgb_l2len(msg) < sizeof(*he))
+		goto invalid;
+
+	msg->l2h = &he->data[0];
+
+	if (he->proto == IPAC_PROTO_EXT_RSPRO) {
+		OSMO_ASSERT(rsproc->read_cb != NULL);
+		rsproc->read_cb(rsproc, msg);
+		/* expecting read_cb() to free msg */
+	} else
+		goto invalid;
+
+	return 0;
+
+invalid:
+	LOGP(DLRSPRO, LOGL_NOTICE,
+	     "RSPRO received an invalid IPA message from %s:%d, size = %d\n",
+	     link->addr, link->port, msgb_length(msg));
+
+	msgb_free(msg);
+	return -1;
+}
+
+static void ping_timer_cb(void *rsproc_)
+{
+	struct osmo_rspro_client *rsproc = rsproc_;
+
+	LOGP(DLRSPRO, LOGL_INFO, "RSPRO ping callback (%s, %s PONG)\n",
+	     rsproc->is_connected ? "connected" : "not connected",
+	     rsproc->got_ipa_pong ? "got" : "didn't get");
+
+	if (rsproc->got_ipa_pong) {
+		start_test_procedure(rsproc);
+		return;
+	}
+
+	LOGP(DLRSPRO, LOGL_NOTICE, "RSPRO ping timed out, reconnecting\n");
+	ipa_client_conn_close(rsproc->link);
+	rsproc->is_connected = 0;
+
+	rspro_client_connect(rsproc);
+}
+
+static void start_test_procedure(struct osmo_rspro_client *rsproc)
+{
+	osmo_timer_setup(&rsproc->ping_timer, ping_timer_cb, rsproc);
+
+	rsproc->got_ipa_pong = 0;
+	osmo_timer_schedule(&rsproc->ping_timer, OSMO_RSPRO_CLIENT_PING_INTERVAL, 0);
+	LOGP(DLRSPRO, LOGL_DEBUG, "RSPRO sending PING\n");
+	rspro_client_send_ping(rsproc);
+}
+
+struct osmo_rspro_client *osmo_rspro_client_create(void *talloc_ctx,
+						 const char *unit_name,
+						 const char *ip_addr,
+						 unsigned int tcp_port,
+						 osmo_rspro_client_read_cb_t read_cb)
+{
+	struct osmo_rspro_client *rsproc;
+	int rc;
+
+	rsproc = talloc_zero(talloc_ctx, struct osmo_rspro_client);
+	OSMO_ASSERT(rsproc);
+
+	/* struct ipaccess_unit has a non-const unit_name, so let's copy to be
+	 * able to have a non-const unit_name here as well. To not taint the
+	 * public rspro_client API, let's store it in a const char* anyway. */
+	rsproc->unit_name = talloc_strdup(rsproc, unit_name);
+	OSMO_ASSERT(rsproc->unit_name);
+
+	rsproc->link = ipa_client_conn_create(rsproc,
+					     /* no e1inp */ NULL,
+					     0,
+					     ip_addr, tcp_port,
+					     rspro_client_updown_cb,
+					     rspro_client_read_cb,
+					     /* default write_cb */ NULL,
+					     rsproc);
+	if (!rsproc->link)
+		goto failed;
+
+	osmo_timer_setup(&rsproc->connect_timer, connect_timer_cb, rsproc);
+
+	rc = rspro_client_connect(rsproc);
+	if (rc < 0)
+		goto failed;
+
+	rsproc->read_cb = read_cb;
+
+	return rsproc;
+
+failed:
+	osmo_rspro_client_destroy(rsproc);
+	return NULL;
+}
+
+void osmo_rspro_client_destroy(struct osmo_rspro_client *rsproc)
+{
+	osmo_timer_del(&rsproc->connect_timer);
+	osmo_timer_del(&rsproc->ping_timer);
+
+	if (rsproc->link) {
+		ipa_client_conn_close(rsproc->link);
+		ipa_client_conn_destroy(rsproc->link);
+		rsproc->link = NULL;
+	}
+	talloc_free(rsproc);
+}
+
+int osmo_rspro_client_send(struct osmo_rspro_client *rsproc, struct msgb *msg)
+{
+	if (!rsproc || !rsproc->is_connected) {
+		LOGP(DLRSPRO, LOGL_ERROR, "RSPRO not connected, unable to send %s\n", msgb_hexdump(msg));
+		msgb_free(msg);
+		return -ENOTCONN;
+	}
+
+	client_send(rsproc, IPAC_PROTO_EXT_RSPRO, msg);
+
+	return 0;
+}
+
+struct msgb *osmo_rspro_client_msgb_alloc(void)
+{
+	return msgb_alloc_headroom(4000, 64, __func__);
+}
diff --git a/src/rspro_util.c b/src/rspro_util.c
index 7a53859..7ff93c3 100644
--- a/src/rspro_util.c
+++ b/src/rspro_util.c
@@ -152,5 +152,3 @@
 
 	return pdu;
 }
-
-