/* OpenBSC Abis/IP proxy ip.access nanoBTS */

/* (C) 2009 by Harald Welte <laforge@gnumonks.org>
 * (C) 2010 by On-Waves
 * (C) 2010 by Holger Hans Peter Freyther <zecke@selfish.org>
 *
 * 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 <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <time.h>
#include <sys/fcntl.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define _GNU_SOURCE
#include <getopt.h>

#include <osmocom/core/application.h>
#include <osmocom/core/select.h>
#include <osmocom/core/msgb.h>
#include <osmocom/core/socket.h>
#include <osmocom/core/talloc.h>
#include <osmocom/gsm/tlv.h>
#include <osmocom/gsm/protocol/ipaccess.h>
#include <osmocom/gsm/ipa.h>
#include <osmocom/bsc/gsm_data.h>
#include <osmocom/bsc/debug.h>
#include <osmocom/bsc/ipaccess.h>

/* one instance of an ip.access protocol proxy */
struct ipa_proxy {
	/* socket where we listen for incoming OML from BTS */
	struct osmo_fd oml_listen_fd;
	/* socket where we listen for incoming RSL from BTS */
	struct osmo_fd rsl_listen_fd;
	/* list of BTS's (struct ipa_bts_conn */
	struct llist_head bts_list;
	/* the BSC reconnect timer */
	struct osmo_timer_list reconn_timer;
	/* global GPRS NS data */
	struct in_addr gprs_addr;
	struct in_addr listen_addr;
};

/* global pointer to the proxy structure */
static struct ipa_proxy *ipp;

struct ipa_proxy_conn {
	struct osmo_fd fd;
	struct llist_head tx_queue;
	struct ipa_bts_conn *bts_conn;
};
#define MAX_TRX 4

/* represents a particular BTS in our proxy */
struct ipa_bts_conn {
	/* list of BTS's (ipa_proxy->bts_list) */
	struct llist_head list;
	/* back pointer to the proxy which we belong to */
	struct ipa_proxy *ipp;
	/* the unit ID as determined by CCM */
	struct {
		uint16_t site_id;
		uint16_t bts_id;
	} unit_id;

	/* incoming connections from BTS */
	struct ipa_proxy_conn *oml_conn;
	struct ipa_proxy_conn *rsl_conn[MAX_TRX];

	/* outgoing connections to BSC */
	struct ipa_proxy_conn *bsc_oml_conn;
	struct ipa_proxy_conn *bsc_rsl_conn[MAX_TRX];

	/* UDP sockets for BTS and BSC injection */
	struct osmo_fd udp_bts_fd;
	struct osmo_fd udp_bsc_fd;

	/* NS data */
	struct in_addr bts_addr;
	struct osmo_fd gprs_ns_fd;
	int gprs_local_port;
	uint16_t gprs_orig_port;
	uint32_t gprs_orig_ip;

	char *id_tags[256];
	uint8_t *id_resp;
	unsigned int id_resp_len;
};

enum ipp_fd_type {
	OML_FROM_BTS = 1,
	RSL_FROM_BTS = 2,
	OML_TO_BSC = 3,
	RSL_TO_BSC = 4,
	UDP_TO_BTS = 5,
	UDP_TO_BSC = 6,
};

extern void *tall_bsc_ctx;

static char *listen_ipaddr;
static char *bsc_ipaddr;
static char *gprs_ns_ipaddr;

static int gprs_ns_cb(struct osmo_fd *bfd, unsigned int what);

#define PROXY_ALLOC_SIZE	1200

static struct ipa_bts_conn *find_bts_by_unitid(struct ipa_proxy *ipp,
						uint16_t site_id,
						uint16_t bts_id)
{
	struct ipa_bts_conn *ipbc;

	llist_for_each_entry(ipbc, &ipp->bts_list, list) {
		if (ipbc->unit_id.site_id == site_id &&
		    ipbc->unit_id.bts_id == bts_id)
			return ipbc;
	}

	return NULL;
}

struct ipa_proxy_conn *alloc_conn(void)
{
	struct ipa_proxy_conn *ipc;

	ipc = talloc_zero(tall_bsc_ctx, struct ipa_proxy_conn);
	if (!ipc)
		return NULL;

	INIT_LLIST_HEAD(&ipc->tx_queue);

	return ipc;
}

static int store_idtags(struct ipa_bts_conn *ipbc, struct tlv_parsed *tlvp)
{
	unsigned int i, len;

	for (i = 0; i <= 0xff; i++) {
		if (!TLVP_PRESENT(tlvp, i))
			continue;

		len = TLVP_LEN(tlvp, i);
#if 0
		if (!ipbc->id_tags[i])
			ipbc->id_tags[i] = talloc_size(tall_bsc_ctx, len);
		else
#endif
			ipbc->id_tags[i] = talloc_realloc_size(ipbc,
							  ipbc->id_tags[i], len);
		if (!ipbc->id_tags[i])
			return -ENOMEM;

		memset(ipbc->id_tags[i], 0, len);
		//memcpy(ipbc->id_tags[i], TLVP_VAL(tlvp, i), len);
	}
	return 0;
}


static struct ipa_proxy_conn *connect_bsc(struct sockaddr_in *sa, int priv_nr, void *data);

#define logp_ipbc_uid(ss, lvl, ipbc, trx_id) _logp_ipbc_uid(ss, lvl, __FILE__, __LINE__, ipbc, trx_id)

static void _logp_ipbc_uid(unsigned int ss, unsigned int lvl, char *file, int line,
			   struct ipa_bts_conn *ipbc, uint8_t trx_id)
{
	if (ipbc)
		logp2(ss, lvl, file, line, 0, "(%u/%u/%u) ", ipbc->unit_id.site_id,
		     ipbc->unit_id.bts_id, trx_id);
	else
		logp2(ss, lvl, file, line, 0, "unknown ");
}

static int handle_udp_read(struct osmo_fd *bfd)
{
	struct ipa_bts_conn *ipbc = bfd->data;
	struct ipa_proxy_conn *other_conn = NULL;
	struct msgb *msg = msgb_alloc(PROXY_ALLOC_SIZE, "Abis/IP UDP");
	struct ipaccess_head *hh;
	int ret;

	/* with UDP sockets, we cannot read partial packets but have to read
	 * all of it in one go */
	hh = (struct ipaccess_head *) msg->data;
	ret = recv(bfd->fd, msg->data, msg->data_len, 0);
	if (ret < 0) {
		if (errno != EAGAIN)
			LOGP(DLINP, LOGL_ERROR, "recv error  %s\n", strerror(errno));
		msgb_free(msg);
		return ret;
	}
	if (ret == 0) {
		DEBUGP(DLINP, "UDP peer disappeared, dead socket\n");
		osmo_fd_unregister(bfd);
		close(bfd->fd);
		bfd->fd = -1;
		msgb_free(msg);
		return -EIO;
	}
	if (ret < sizeof(*hh)) {
		DEBUGP(DLINP, "could not even read header!?!\n");
		msgb_free(msg);
		return -EIO;
	}
	msgb_put(msg, ret);
	msg->l2h = msg->data + sizeof(*hh);
	DEBUGP(DLMI, "UDP RX: %s\n", osmo_hexdump(msg->data, msg->len));

	if (hh->len != msg->len - sizeof(*hh)) {
		DEBUGP(DLINP, "length (%u/%u) disagrees with header(%u)\n",
			msg->len, msg->len - 3, hh->len);
		msgb_free(msg);
		return -EIO;
	}

	switch (bfd->priv_nr & 0xff) {
	case UDP_TO_BTS:
		/* injection towards BTS */
		switch (hh->proto) {
		case IPAC_PROTO_RSL:
			/* FIXME: what to do about TRX > 0 */
			other_conn = ipbc->rsl_conn[0];
			break;
		default:
			DEBUGP(DLINP, "Unknown protocol 0x%02x, sending to "
				"OML FD\n", hh->proto);
			/* fall through */
		case IPAC_PROTO_IPACCESS:
		case IPAC_PROTO_OML:
			other_conn = ipbc->oml_conn;
			break;
		}
		break;
	case UDP_TO_BSC:
		/* injection towards BSC */
		switch (hh->proto) {
		case IPAC_PROTO_RSL:
			/* FIXME: what to do about TRX > 0 */
			other_conn = ipbc->bsc_rsl_conn[0];
			break;
		default:
			DEBUGP(DLINP, "Unknown protocol 0x%02x, sending to "
				"OML FD\n", hh->proto);
			/* fall through */
		case IPAC_PROTO_IPACCESS:
		case IPAC_PROTO_OML:
			other_conn = ipbc->bsc_oml_conn;
			break;
		}
		break;
	default:
		DEBUGP(DLINP, "Unknown filedescriptor priv_nr=%04x\n", bfd->priv_nr);
		break;
	}

	if (other_conn) {
		/* enqueue the message for TX on the respective FD */
		msgb_enqueue(&other_conn->tx_queue, msg);
		osmo_fd_write_enable(&other_conn->fd);
	} else
		msgb_free(msg);

	return 0;
}

static int handle_udp_write(struct osmo_fd *bfd)
{
	/* not implemented yet */
	osmo_fd_write_disable(bfd);

	return -EIO;
}

/* callback from select.c in case one of the fd's can be read/written */
static int udp_fd_cb(struct osmo_fd *bfd, unsigned int what)
{
	int rc = 0;

	if (what & OSMO_FD_READ)
		rc = handle_udp_read(bfd);
	if (what & OSMO_FD_WRITE)
		rc = handle_udp_write(bfd);

	return rc;
}


static int ipbc_alloc_connect(struct ipa_proxy_conn *ipc, struct osmo_fd *bfd,
			      uint16_t site_id, uint16_t bts_id,
			      uint16_t trx_id, struct tlv_parsed *tlvp,
			      struct msgb *msg)
{
	struct ipa_bts_conn *ipbc;
	uint16_t udp_port;
	int ret = 0;
	struct sockaddr_in sin;

	memset(&sin, 0, sizeof(sin));
	sin.sin_family = AF_INET;
	inet_aton(bsc_ipaddr, &sin.sin_addr);

	DEBUGP(DLINP, "(%u/%u/%u) New BTS connection: ",
		site_id, bts_id, trx_id);

	/* OML needs to be established before RSL */
	if ((bfd->priv_nr & 0xff) != OML_FROM_BTS) {
		DEBUGPC(DLINP, "Not a OML connection ?!?\n");
		return -EIO;
	}

	/* allocate new BTS connection data structure */
	ipbc = talloc_zero(tall_bsc_ctx, struct ipa_bts_conn);
	if (!ipbc) {
		ret = -ENOMEM;
		goto err_out;
	}

	DEBUGPC(DLINP, "Created BTS Conn data structure\n");
	ipbc->ipp = ipp;
	ipbc->unit_id.site_id = site_id;
	ipbc->unit_id.bts_id = bts_id;
	ipbc->oml_conn = ipc;
	ipc->bts_conn = ipbc;

	/* store the content of the ID TAGS for later reference */
	store_idtags(ipbc, tlvp);
	ipbc->id_resp_len = msg->len;
	ipbc->id_resp = talloc_size(tall_bsc_ctx, ipbc->id_resp_len);
	memcpy(ipbc->id_resp, msg->data, ipbc->id_resp_len);

	/* Create OML TCP connection towards BSC */
	sin.sin_port = htons(IPA_TCP_PORT_OML);
	ipbc->bsc_oml_conn = connect_bsc(&sin, OML_TO_BSC, ipbc);
	if (!ipbc->bsc_oml_conn) {
		ret = -EIO;
		goto err_bsc_conn;
	}

	DEBUGP(DLINP, "(%u/%u/%u) OML Connected to BSC\n",
		site_id, bts_id, trx_id);

	/* Create UDP socket for BTS packet injection */
	udp_port = 10000 + (site_id % 1000)*100 + (bts_id % 100);
	ipbc->udp_bts_fd.priv_nr = UDP_TO_BTS;
	ipbc->udp_bts_fd.cb = udp_fd_cb;
	ipbc->udp_bts_fd.data = ipbc;
	ret = osmo_sock_init_ofd(&ipbc->udp_bts_fd, AF_INET, SOCK_DGRAM, IPPROTO_UDP,
				 NULL, udp_port, OSMO_SOCK_F_BIND);
	if (ret < 0)
		goto err_udp_bts;
	DEBUGP(DLINP, "(%u/%u/%u) Created UDP socket for injection "
		"towards BTS at port %u\n", site_id, bts_id, trx_id, udp_port);

	/* Create UDP socket for BSC packet injection */
	udp_port = 20000 + (site_id % 1000)*100 + (bts_id % 100);
	ipbc->udp_bsc_fd.priv_nr = UDP_TO_BSC;
	ipbc->udp_bsc_fd.cb = udp_fd_cb;
	ipbc->udp_bsc_fd.data = ipbc;
	ret = osmo_sock_init_ofd(&ipbc->udp_bsc_fd, AF_INET, SOCK_DGRAM, IPPROTO_UDP,
				 NULL, udp_port, OSMO_SOCK_F_BIND);
	if (ret < 0)
		goto err_udp_bsc;
	DEBUGP(DLINP, "(%u/%u/%u) Created UDP socket for injection "
		"towards BSC at port %u\n", site_id, bts_id, trx_id, udp_port);


	/* GPRS NS related code */
	if (gprs_ns_ipaddr) {
		struct sockaddr_in sock;
		socklen_t len = sizeof(sock);

		ipbc->gprs_ns_fd.priv_nr = 0;
		ipbc->gprs_ns_fd.cb = gprs_ns_cb;
		ipbc->gprs_ns_fd.data = ipbc;
		ret = osmo_sock_init_ofd(&ipbc->gprs_ns_fd, AF_INET, SOCK_DGRAM, IPPROTO_UDP,
					 listen_ipaddr, 0, OSMO_SOCK_F_BIND);
		if (ret < 0) {
			LOGP(DLINP, LOGL_ERROR, "Creating the GPRS socket failed.\n");
			goto err_udp_bsc;
		}

		ret = getsockname(ipbc->gprs_ns_fd.fd, (struct sockaddr* ) &sock, &len);
		ipbc->gprs_local_port = ntohs(sock.sin_port);
		LOGP(DLINP, LOGL_NOTICE,
			"Created GPRS NS Socket. Listening on: %s:%d\n",
			inet_ntoa(sock.sin_addr), ipbc->gprs_local_port);

		ret = getpeername(bfd->fd, (struct sockaddr* ) &sock, &len);
		ipbc->bts_addr = sock.sin_addr;
	}

	llist_add(&ipbc->list, &ipp->bts_list);

	return 0;

err_udp_bsc:
	osmo_fd_unregister(&ipbc->udp_bts_fd);
err_udp_bts:
	osmo_fd_unregister(&ipbc->bsc_oml_conn->fd);
	close(ipbc->bsc_oml_conn->fd.fd);
	talloc_free(ipbc->bsc_oml_conn);
	ipbc->bsc_oml_conn = NULL;
err_bsc_conn:
	talloc_free(ipbc->id_resp);
	talloc_free(ipbc);
#if 0
	osmo_fd_unregister(bfd);
	close(bfd->fd);
	talloc_free(bfd);
#endif
err_out:
	return ret;
}

static int ipaccess_rcvmsg(struct ipa_proxy_conn *ipc, struct msgb *msg,
			   struct osmo_fd *bfd)
{
	struct tlv_parsed tlvp;
	uint8_t msg_type = *(msg->l2h);
	struct ipaccess_unit unit_data;
	struct ipa_bts_conn *ipbc;
	int ret = 0;

	switch (msg_type) {
	case IPAC_MSGT_PING:
		ret = ipa_ccm_send_pong(bfd->fd);
		break;
	case IPAC_MSGT_PONG:
		DEBUGP(DLMI, "PONG!\n");
		break;
	case IPAC_MSGT_ID_RESP:
		DEBUGP(DLMI, "ID_RESP ");
		/* parse tags, search for Unit ID */
		ret = ipa_ccm_id_resp_parse(&tlvp, (uint8_t *)msg->l2h+1, msgb_l2len(msg)-1);
		if (ret < 0) {
			LOGP(DLINP, LOGL_ERROR, "Error parsing CCM ID RESP !?!\n");
			return -EIO;
		}
		DEBUGP(DLMI, "\n");

		if (!TLVP_PRESENT(&tlvp, IPAC_IDTAG_UNIT)) {
			LOGP(DLINP, LOGL_ERROR, "No Unit ID in ID RESPONSE !?!\n");
			return -EIO;
		}

		/* lookup BTS, create sign_link, ... */
		memset(&unit_data, 0, sizeof(unit_data));
		ipa_parse_unitid((char *)TLVP_VAL(&tlvp, IPAC_IDTAG_UNIT),
				      &unit_data);
		ipbc = find_bts_by_unitid(ipp, unit_data.site_id, unit_data.bts_id);
		if (!ipbc) {
			/* We have not found an ipbc (per-bts proxy instance)
			 * for this BTS yet.  The first connection of a new BTS must
			 * be a OML connection.  We allocate the associated data structures,
			 * and try to connect to the remote end */

			return ipbc_alloc_connect(ipc, bfd, unit_data.site_id,
						  unit_data.bts_id,
						  unit_data.trx_id, &tlvp, msg);
			/* if this fails, the caller will clean up bfd */
		} else {
			struct sockaddr_in sin;
			memset(&sin, 0, sizeof(sin));
			sin.sin_family = AF_INET;
			inet_aton(bsc_ipaddr, &sin.sin_addr);

			DEBUGP(DLINP, "Identified BTS %u/%u/%u\n",
				unit_data.site_id, unit_data.bts_id, unit_data.trx_id);

			if ((bfd->priv_nr & 0xff) != RSL_FROM_BTS) {
				LOGP(DLINP, LOGL_ERROR, "Second OML connection from "
				     "same BTS ?!?\n");
				return 0;
			}

			if (unit_data.trx_id >= MAX_TRX) {
				LOGP(DLINP, LOGL_ERROR, "We don't support more "
				     "than %u TRX\n", MAX_TRX);
				return -EINVAL;
			}

			ipc->bts_conn = ipbc;
			/* store TRX number in higher 8 bit of the bfd private number */
			bfd->priv_nr |= unit_data.trx_id << 8;
			ipbc->rsl_conn[unit_data.trx_id] = ipc;

			/* Create RSL TCP connection towards BSC */
			sin.sin_port = htons(IPA_TCP_PORT_RSL);
			ipbc->bsc_rsl_conn[unit_data.trx_id] =
				connect_bsc(&sin, RSL_TO_BSC | (unit_data.trx_id << 8), ipbc);
			if (!ipbc->bsc_oml_conn)
				return -EIO;
			DEBUGP(DLINP, "(%u/%u/%u) Connected RSL to BSC\n",
				unit_data.site_id, unit_data.bts_id, unit_data.trx_id);
		}
		break;
	case IPAC_MSGT_ID_GET:
		DEBUGP(DLMI, "ID_GET\n");
		if ((bfd->priv_nr & 0xff) != OML_TO_BSC &&
		    (bfd->priv_nr & 0xff) != RSL_TO_BSC) {
			DEBUGP(DLINP, "IDentity REQuest from BTS ?!?\n");
			return -EIO;
		}
		ipbc = ipc->bts_conn;
		if (!ipbc) {
			DEBUGP(DLINP, "ID_GET from BSC before we have ID_RESP from BTS\n");
			return -EIO;
		}
		ret = write(bfd->fd, ipbc->id_resp, ipbc->id_resp_len);
		if (ret != ipbc->id_resp_len) {
			LOGP(DLINP, LOGL_ERROR, "Partial write: %d of %d\n",
			     ret, ipbc->id_resp_len);
			return -EIO;
		}
		ret = 0;
		break;
	case IPAC_MSGT_ID_ACK:
		DEBUGP(DLMI, "ID_ACK? -> ACK!\n");
		ret = ipa_ccm_send_id_ack(bfd->fd);
		break;
	default:
		LOGP(DLMI, LOGL_ERROR, "Unhandled IPA type; %d\n", msg_type);
		return 1;
		break;
	}
	return ret;
}

struct msgb *ipaccess_proxy_read_msg(struct osmo_fd *bfd, int *error)
{
	struct msgb *msg = msgb_alloc(PROXY_ALLOC_SIZE, "Abis/IP");
	struct ipaccess_head *hh;
	int len, ret = 0;

	if (!msg) {
		*error = -ENOMEM;
		return NULL;
	}

	/* first read our 3-byte header */
	hh = (struct ipaccess_head *) msg->data;
	ret = recv(bfd->fd, msg->data, 3, 0);
	if (ret < 0) {
		if (errno != EAGAIN)
			LOGP(DLINP, LOGL_ERROR, "recv error: %s\n", strerror(errno));
		msgb_free(msg);
		*error = ret;
		return NULL;
	} else if (ret == 0) {
		msgb_free(msg);
		*error = ret;
		return NULL;
	}

	msgb_put(msg, ret);

	/* then read the length as specified in header */
	msg->l2h = msg->data + sizeof(*hh);
	len = ntohs(hh->len);
	ret = recv(bfd->fd, msg->l2h, len, 0);
	if (ret < len) {
		LOGP(DLINP, LOGL_ERROR, "short read!\n");
		msgb_free(msg);
		*error = -EIO;
		return NULL;
	}
	msgb_put(msg, ret);

	return msg;
}

static struct ipa_proxy_conn *ipc_by_priv_nr(struct ipa_bts_conn *ipbc,
					     unsigned int priv_nr)
{
	struct ipa_proxy_conn *bsc_conn;
	unsigned int trx_id = priv_nr >> 8;

	switch (priv_nr & 0xff) {
	case OML_FROM_BTS: /* incoming OML data from BTS, forward to BSC OML */
		bsc_conn = ipbc->bsc_oml_conn;
		break;
	case RSL_FROM_BTS: /* incoming RSL data from BTS, forward to BSC RSL */
		bsc_conn = ipbc->bsc_rsl_conn[trx_id];
		break;
	case OML_TO_BSC: /* incoming OML data from BSC, forward to BTS OML */
		bsc_conn = ipbc->oml_conn;
		break;
	case RSL_TO_BSC: /* incoming RSL data from BSC, forward to BTS RSL */
		bsc_conn = ipbc->rsl_conn[trx_id];
		break;
	default:
		bsc_conn = NULL;
		break;
	}
	return bsc_conn;
}

static void reconn_tmr_cb(void *data)
{
	struct ipa_proxy *ipp = data;
	struct ipa_bts_conn *ipbc;
	struct sockaddr_in sin;
	int i;

	DEBUGP(DLINP, "Running reconnect timer\n");

	memset(&sin, 0, sizeof(sin));
	sin.sin_family = AF_INET;
	inet_aton(bsc_ipaddr, &sin.sin_addr);

	llist_for_each_entry(ipbc, &ipp->bts_list, list) {
		/* if OML to BSC is dead, try to restore it */
		if (ipbc->oml_conn && !ipbc->bsc_oml_conn) {
			sin.sin_port = htons(IPA_TCP_PORT_OML);
			logp_ipbc_uid(DLINP, LOGL_NOTICE, ipbc, 0);
			LOGPC(DLINP, LOGL_NOTICE, "OML Trying to reconnect\n");
			ipbc->bsc_oml_conn = connect_bsc(&sin, OML_TO_BSC, ipbc);
			if (!ipbc->bsc_oml_conn)
				goto reschedule;
			logp_ipbc_uid(DLINP, LOGL_NOTICE, ipbc, 0);
			LOGPC(DLINP, LOGL_NOTICE, "OML Reconnected\n");
		}
		/* if we (still) don't have a OML connection, skip RSL */
		if (!ipbc->oml_conn || !ipbc->bsc_oml_conn)
			continue;

		for (i = 0; i < ARRAY_SIZE(ipbc->rsl_conn); i++) {
			unsigned int priv_nr;
			/* don't establish RSL links which we don't have */
			if (!ipbc->rsl_conn[i])
				continue;
			if (ipbc->bsc_rsl_conn[i])
				continue;
			priv_nr = ipbc->rsl_conn[i]->fd.priv_nr;
			priv_nr &= ~0xff;
			priv_nr |= RSL_TO_BSC;
			sin.sin_port = htons(IPA_TCP_PORT_RSL);
			logp_ipbc_uid(DLINP, LOGL_NOTICE, ipbc, priv_nr >> 8);
			LOGPC(DLINP, LOGL_NOTICE, "RSL Trying to reconnect\n");
			ipbc->bsc_rsl_conn[i] = connect_bsc(&sin, priv_nr, ipbc);
			if (!ipbc->bsc_rsl_conn[i])
				goto reschedule;
			logp_ipbc_uid(DLINP, LOGL_NOTICE, ipbc, priv_nr >> 8);
			LOGPC(DLINP, LOGL_NOTICE, "RSL Reconnected\n");
		}
	}
	return;

reschedule:
	osmo_timer_schedule(&ipp->reconn_timer, 5, 0);
}

static void handle_dead_socket(struct osmo_fd *bfd)
{
	struct ipa_proxy_conn *ipc = bfd->data;		/* local conn */
	struct ipa_proxy_conn *bsc_conn;		/* remote conn */
	struct ipa_bts_conn *ipbc = ipc->bts_conn;
	unsigned int trx_id = bfd->priv_nr >> 8;
	struct msgb *msg, *msg2;

	osmo_fd_unregister(bfd);
	close(bfd->fd);
	bfd->fd = -1;

	/* FIXME: clear tx_queue, remove all references, etc. */
	llist_for_each_entry_safe(msg, msg2, &ipc->tx_queue, list)
		msgb_free(msg);

	switch (bfd->priv_nr & 0xff) {
	case OML_FROM_BTS: /* incoming OML data from BTS, forward to BSC OML */
		/* The BTS started a connection with us but we got no
		 * IPAC_MSGT_ID_RESP message yet, in that scenario we did not
		 * allocate the ipa_bts_conn structure. */
		if (ipbc == NULL)
			break;
		ipbc->oml_conn = NULL;
		bsc_conn = ipbc->bsc_oml_conn;
		/* close the connection to the BSC */
		osmo_fd_unregister(&bsc_conn->fd);
		close(bsc_conn->fd.fd);
		llist_for_each_entry_safe(msg, msg2, &bsc_conn->tx_queue, list)
			msgb_free(msg);
		talloc_free(bsc_conn);
		ipbc->bsc_oml_conn = NULL;
		/* FIXME: do we need to delete the entire ipbc ? */
		break;
	case RSL_FROM_BTS: /* incoming RSL data from BTS, forward to BSC RSL */
		ipbc->rsl_conn[trx_id] = NULL;
		bsc_conn = ipbc->bsc_rsl_conn[trx_id];
		/* close the connection to the BSC */
		osmo_fd_unregister(&bsc_conn->fd);
		close(bsc_conn->fd.fd);
		llist_for_each_entry_safe(msg, msg2, &bsc_conn->tx_queue, list)
			msgb_free(msg);
		talloc_free(bsc_conn);
		ipbc->bsc_rsl_conn[trx_id] = NULL;
		break;
	case OML_TO_BSC: /* incoming OML data from BSC, forward to BTS OML */
		ipbc->bsc_oml_conn = NULL;
		bsc_conn = ipbc->oml_conn;
		/* start reconnect timer */
		osmo_timer_schedule(&ipp->reconn_timer, 5, 0);
		break;
	case RSL_TO_BSC: /* incoming RSL data from BSC, forward to BTS RSL */
		ipbc->bsc_rsl_conn[trx_id] = NULL;
		bsc_conn = ipbc->rsl_conn[trx_id];
		/* start reconnect timer */
		osmo_timer_schedule(&ipp->reconn_timer, 5, 0);
		break;
	default:
		bsc_conn = NULL;
		break;
	}

	talloc_free(ipc);
}

static void patch_gprs_msg(struct ipa_bts_conn *ipbc, int priv_nr, struct msgb *msg)
{
	uint8_t *nsvci;

	if ((priv_nr & 0xff) != OML_FROM_BTS && (priv_nr & 0xff) != OML_TO_BSC)
		return;

	if (msgb_l2len(msg) != 39)
		return;

	/*
	 * Check if this is a IPA Set Attribute or IPA Set Attribute ACK
	 * and if the FOM Class is GPRS NSVC0 and then we will patch it.
	 *
	 * The patch assumes the message looks like the one from the trace
	 * but we only match messages with a specific size anyway... So
	 * this hack should work just fine.
	 */

	if (msg->l2h[0] == 0x10 && msg->l2h[1] == 0x80 &&
	    msg->l2h[2] == 0x00 && msg->l2h[3] == 0x15 &&
	    msg->l2h[18] == 0xf5 && msg->l2h[19] == 0xf2) {
		nsvci = &msg->l2h[23];
		ipbc->gprs_orig_port =  *(uint16_t *)(nsvci+8);
		ipbc->gprs_orig_ip = *(uint32_t *)(nsvci+10);
		*(uint16_t *)(nsvci+8) = htons(ipbc->gprs_local_port);
		*(uint32_t *)(nsvci+10) = ipbc->ipp->listen_addr.s_addr;
	} else if (msg->l2h[0] == 0x10 && msg->l2h[1] == 0x80 &&
	    msg->l2h[2] == 0x00 && msg->l2h[3] == 0x15 &&
	    msg->l2h[18] == 0xf6 && msg->l2h[19] == 0xf2) {
		nsvci = &msg->l2h[23];
		*(uint16_t *)(nsvci+8) = ipbc->gprs_orig_port;
		*(uint32_t *)(nsvci+10) = ipbc->gprs_orig_ip;
	}
}

static int handle_tcp_read(struct osmo_fd *bfd)
{
	struct ipa_proxy_conn *ipc = bfd->data;
	struct ipa_bts_conn *ipbc = ipc->bts_conn;
	struct ipa_proxy_conn *bsc_conn;
	struct msgb *msg;
	struct ipaccess_head *hh;
	int ret = 0;
	char *btsbsc;

	if ((bfd->priv_nr & 0xff) <= 2)
		btsbsc = "BTS";
	else
		btsbsc = "BSC";

	msg = ipaccess_proxy_read_msg(bfd, &ret);
	if (!msg) {
		if (ret == 0) {
			logp_ipbc_uid(DLINP, LOGL_NOTICE, ipbc, bfd->priv_nr >> 8);
			LOGPC(DLINP, LOGL_NOTICE, "%s disappeared, "
			     "dead socket\n", btsbsc);
			handle_dead_socket(bfd);
		}
		return ret;
	}

	msgb_put(msg, ret);
	logp_ipbc_uid(DLMI, LOGL_DEBUG, ipbc, bfd->priv_nr >> 8);
	DEBUGPC(DLMI, "RX<-%s: %s\n", btsbsc, osmo_hexdump(msg->data, msg->len));

	hh = (struct ipaccess_head *) msg->data;
	if (hh->proto == IPAC_PROTO_IPACCESS) {
		ret = ipaccess_rcvmsg(ipc, msg, bfd);
		if (ret < 0) {
			osmo_fd_unregister(bfd);
			close(bfd->fd);
			bfd->fd = -1;
			talloc_free(bfd);
			msgb_free(msg);
			return ret;
		} else if (ret == 0) {
			/* we do not forward parts of the CCM protocol
			 * through the proxy but rather terminate it ourselves. */
			msgb_free(msg);
			return ret;
		}
	}

	if (!ipbc) {
		LOGP(DLINP, LOGL_ERROR,
		     "received %s packet but no ipc->bts_conn?!?\n", btsbsc);
		msgb_free(msg);
		return -EIO;
	}

	bsc_conn = ipc_by_priv_nr(ipbc, bfd->priv_nr);
	if (bsc_conn) {
		if (gprs_ns_ipaddr)
			patch_gprs_msg(ipbc, bfd->priv_nr, msg);
		/* enqueue packet towards BSC */
		msgb_enqueue(&bsc_conn->tx_queue, msg);
		/* mark respective filedescriptor as 'we want to write' */
		osmo_fd_write_enable(&bsc_conn->fd);
	} else {
		logp_ipbc_uid(DLINP, LOGL_INFO, ipbc, bfd->priv_nr >> 8);
		LOGPC(DLINP, LOGL_INFO, "Dropping packet from %s, "
		     "since remote connection is dead\n", btsbsc);
		msgb_free(msg);
	}

	return ret;
}

/* a TCP socket is ready to be written to */
static int handle_tcp_write(struct osmo_fd *bfd)
{
	struct ipa_proxy_conn *ipc = bfd->data;
	struct ipa_bts_conn *ipbc = ipc->bts_conn;
	struct llist_head *lh;
	struct msgb *msg;
	char *btsbsc;
	int ret;

	if ((bfd->priv_nr & 0xff) <= 2)
		btsbsc = "BTS";
	else
		btsbsc = "BSC";


	/* get the next msg for this timeslot */
	if (llist_empty(&ipc->tx_queue)) {
		osmo_fd_write_disable(bfd);
		return 0;
	}
	lh = ipc->tx_queue.next;
	llist_del(lh);
	msg = llist_entry(lh, struct msgb, list);

	logp_ipbc_uid(DLMI, LOGL_DEBUG, ipbc, bfd->priv_nr >> 8);
	DEBUGPC(DLMI, "TX %04x: %s\n", bfd->priv_nr,
		osmo_hexdump(msg->data, msg->len));

	ret = send(bfd->fd, msg->data, msg->len, 0);
	msgb_free(msg);

	if (ret == 0) {
		logp_ipbc_uid(DLINP, LOGL_NOTICE, ipbc, bfd->priv_nr >> 8);
		LOGP(DLINP, LOGL_NOTICE, "%s disappeared, dead socket\n", btsbsc);
		handle_dead_socket(bfd);
	}

	return ret;
}

/* callback from select.c in case one of the fd's can be read/written */
static int proxy_ipaccess_fd_cb(struct osmo_fd *bfd, unsigned int what)
{
	int rc = 0;

	if (what & OSMO_FD_READ) {
		rc = handle_tcp_read(bfd);
		if (rc < 0)
			return rc;
	}
	if (what & OSMO_FD_WRITE)
		rc = handle_tcp_write(bfd);

	return rc;
}

/* callback of the listening filedescriptor */
static int listen_fd_cb(struct osmo_fd *listen_bfd, unsigned int what)
{
	int ret;
	struct ipa_proxy_conn *ipc;
	struct osmo_fd *bfd;
	struct sockaddr_in sa;
	socklen_t sa_len = sizeof(sa);

	if (!(what & OSMO_FD_READ))
		return 0;

	ret = accept(listen_bfd->fd, (struct sockaddr *) &sa, &sa_len);
	if (ret < 0) {
		perror("accept");
		return ret;
	}
	DEBUGP(DLINP, "accept()ed new %s link from %s\n",
		(listen_bfd->priv_nr & 0xff) == OML_FROM_BTS ? "OML" : "RSL",
		inet_ntoa(sa.sin_addr));

	ipc = alloc_conn();
	if (!ipc) {
		close(ret);
		return -ENOMEM;
	}

	bfd = &ipc->fd;
	osmo_fd_setup(bfd, ret, OSMO_FD_READ, proxy_ipaccess_fd_cb, ipc, listen_bfd->priv_nr);
	ret = osmo_fd_register(bfd);
	if (ret < 0) {
		LOGP(DLINP, LOGL_ERROR, "could not register FD\n");
		close(bfd->fd);
		talloc_free(ipc);
		return ret;
	}

	/* Request ID. FIXME: request LOCATION, HW/SW VErsion, Unit Name, Serno */
	ret = ipa_ccm_send_id_req(bfd->fd);

	return 0;
}

static void send_ns(int fd, const char *buf, int size, struct in_addr ip, int port)
{
	int ret;
	struct sockaddr_in addr;
	socklen_t len = sizeof(addr);
	memset(&addr, 0, sizeof(addr));

	addr.sin_family = AF_INET;
	addr.sin_port = htons(port);
	addr.sin_addr = ip;

	ret = sendto(fd, buf, size, 0, (struct sockaddr *) &addr, len);
	if (ret < 0) {
		LOGP(DLINP, LOGL_ERROR, "Failed to forward GPRS message.\n");
	}
}

static int gprs_ns_cb(struct osmo_fd *bfd, unsigned int what)
{
	struct ipa_bts_conn *bts;
	char buf[4096];
	int ret;
	struct sockaddr_in sock;
	socklen_t len = sizeof(sock);

	/* 1. get the data... */
	ret = recvfrom(bfd->fd, buf, sizeof(buf), 0, (struct sockaddr *) &sock, &len);
	if (ret < 0) {
		LOGP(DLINP, LOGL_ERROR, "Failed to recv GPRS NS msg: %s.\n", strerror(errno));
		return -1;
	}

	bts = bfd->data;

	/* 2. figure out where to send it to */
	if (memcmp(&sock.sin_addr, &ipp->gprs_addr, sizeof(sock.sin_addr)) == 0) {
		LOGP(DLINP, LOGL_DEBUG, "GPRS NS msg from network.\n");
		send_ns(bfd->fd, buf, ret, bts->bts_addr, 23000);
	} else if (memcmp(&sock.sin_addr, &bts->bts_addr, sizeof(sock.sin_addr)) == 0) {
		LOGP(DLINP, LOGL_DEBUG, "GPRS NS msg from BTS.\n");
		send_ns(bfd->fd, buf, ret, ipp->gprs_addr, 23000);
	} else {
		LOGP(DLINP, LOGL_ERROR, "Unknown GPRS source: %s\n", inet_ntoa(sock.sin_addr));
	}

	return 0;
}

/* Actively connect to a BSC.  */
static struct ipa_proxy_conn *connect_bsc(struct sockaddr_in *sa, int priv_nr, void *data)
{
	struct ipa_proxy_conn *ipc;
	struct osmo_fd *bfd;
	int ret, on = 1;

	ipc = alloc_conn();
	if (!ipc)
		return NULL;

	ipc->bts_conn = data;

	ret = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (ret < 0) {
		LOGP(DLINP, LOGL_ERROR, "Could not create socket: %s\n",
			strerror(errno));
		talloc_free(ipc);
		return NULL;
	}

	bfd = &ipc->fd;
	osmo_fd_setup(bfd, ret, OSMO_FD_READ | OSMO_FD_WRITE, proxy_ipaccess_fd_cb, ipc, priv_nr);

	ret = setsockopt(bfd->fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
	if (ret < 0) {
		LOGP(DLINP, LOGL_ERROR, "Could not set socket option\n");
		close(bfd->fd);
		talloc_free(ipc);
		return NULL;
	}

	ret = connect(bfd->fd, (struct sockaddr *) sa, sizeof(*sa));
	if (ret < 0) {
		LOGP(DLINP, LOGL_ERROR, "Could not connect socket: %s\n",
		     inet_ntoa(sa->sin_addr));
		close(bfd->fd);
		talloc_free(ipc);
		return NULL;
	}

	/* pre-fill tx_queue with identity request */
	ret = osmo_fd_register(bfd);
	if (ret < 0) {
		close(bfd->fd);
		talloc_free(ipc);
		return NULL;
	}

	return ipc;
}

static int ipaccess_proxy_setup(void)
{
	int ret;

	ipp = talloc_zero(tall_bsc_ctx, struct ipa_proxy);
	if (!ipp)
		return -ENOMEM;
	INIT_LLIST_HEAD(&ipp->bts_list);
	osmo_timer_setup(&ipp->reconn_timer, reconn_tmr_cb, ipp);

	/* Listen for OML connections */
	ipp->oml_listen_fd.priv_nr = OML_FROM_BTS;
	ipp->oml_listen_fd.cb = listen_fd_cb;
	ret = osmo_sock_init_ofd(&ipp->oml_listen_fd, AF_INET, SOCK_STREAM, IPPROTO_TCP,
				 NULL, IPA_TCP_PORT_OML, OSMO_SOCK_F_BIND);
	if (ret < 0)
		return ret;

	/* Listen for RSL connections */
	ipp->rsl_listen_fd.priv_nr = RSL_FROM_BTS;
	ipp->rsl_listen_fd.cb = listen_fd_cb;
	ret = osmo_sock_init_ofd(&ipp->rsl_listen_fd, AF_INET, SOCK_STREAM, IPPROTO_TCP,
				 NULL, IPA_TCP_PORT_RSL, OSMO_SOCK_F_BIND);
	if (ret < 0)
		return ret;

	/* Connect the GPRS NS Socket */
	if (gprs_ns_ipaddr) {
		inet_aton(gprs_ns_ipaddr, &ipp->gprs_addr);
		inet_aton(listen_ipaddr, &ipp->listen_addr);
	}

	return ret;
}

static void signal_handler(int signum)
{
	fprintf(stdout, "signal %u received\n", signum);

	switch (signum) {
	case SIGABRT:
		/* in case of abort, we want to obtain a talloc report and
		 * then run default SIGABRT handler, who will generate coredump
		 * and abort the process. abort() should do this for us after we
		 * return, but program wouldn't exit if an external SIGABRT is
		 * received.
		 */
		talloc_report_full(tall_bsc_ctx, stderr);
		signal(SIGABRT, SIG_DFL);
		raise(SIGABRT);
		break;
	case SIGUSR1:
		talloc_report_full(tall_bsc_ctx, stderr);
		break;
	default:
		break;
	}
}

static void print_help(void)
{
	printf(" ipaccess-proxy is a proxy BTS.\n");
	printf(" -h --help. This help text.\n");
	printf(" -l --listen IP. The ip to listen to.\n");
	printf(" -b --bsc IP. The BSC IP address.\n");
	printf(" -g --gprs IP. Take GPRS NS from that IP.\n");
	printf("\n");
	printf(" -s --disable-color. Disable the color inside the logging message.\n");
	printf(" -e --log-level number. Set the global loglevel.\n");
	printf(" -T --timestamp. Prefix every log message with a timestamp.\n");
	printf(" -V --version. Print the version of OpenBSC.\n");
}

static void print_usage(void)
{
	printf("Usage: ipaccess-proxy [options]\n");
}

enum {
	IPA_PROXY_OPT_LISTEN_NONE	= 0,
	IPA_PROXY_OPT_LISTEN_IP		= (1 << 0),
	IPA_PROXY_OPT_BSC_IP		= (1 << 1),
};

static void handle_options(int argc, char** argv)
{
	int options_mask = 0;

	/* disable explicit missing arguments error output from getopt_long */
	opterr = 0;

	while (1) {
		int option_index = 0, c;
		static struct option long_options[] = {
			{"help", 0, 0, 'h'},
			{"disable-color", 0, 0, 's'},
			{"timestamp", 0, 0, 'T'},
			{"log-level", 1, 0, 'e'},
			{"listen", 1, 0, 'l'},
			{"bsc", 1, 0, 'b'},
			{0, 0, 0, 0}
		};

		c = getopt_long(argc, argv, "hsTe:l:b:g:",
				long_options, &option_index);
		if (c == -1)
			break;

		switch (c) {
		case 'h':
			print_usage();
			print_help();
			exit(0);
		case 'l':
			listen_ipaddr = optarg;
			options_mask |= IPA_PROXY_OPT_LISTEN_IP;
			break;
		case 'b':
			bsc_ipaddr = optarg;
			options_mask |= IPA_PROXY_OPT_BSC_IP;
			break;
		case 'g':
			gprs_ns_ipaddr = optarg;
			break;
		case 's':
			log_set_use_color(osmo_stderr_target, 0);
			break;
		case 'T':
			log_set_print_timestamp(osmo_stderr_target, 1);
			break;
		case 'e':
			log_set_log_level(osmo_stderr_target, atoi(optarg));
			break;
		case '?':
			if (optopt) {
				printf("ERROR: missing mandatory argument "
				       "for `%s' option\n", argv[optind-1]);
			} else {
				printf("ERROR: unknown option `%s'\n",
					argv[optind-1]);
			}
			print_usage();
			print_help();
			exit(EXIT_FAILURE);
			break;
		default:
			/* ignore */
			break;
		}
	}
	if (argc > optind) {
		fprintf(stderr, "Unsupported positional arguments on command line\n");
		exit(2);
	}
	if ((options_mask & (IPA_PROXY_OPT_LISTEN_IP | IPA_PROXY_OPT_BSC_IP))
		 != (IPA_PROXY_OPT_LISTEN_IP | IPA_PROXY_OPT_BSC_IP)) {
		printf("ERROR: You have to specify `--listen' and `--bsc' "
		       "options at least.\n");
		print_usage();
		print_help();
		exit(EXIT_FAILURE);
	}
}

static const struct log_info_cat log_categories[] = {
	[DMM] = {
		.name = "DMM",
		.description = "Layer3 Mobility Management (MM)",
		.color = "\033[1;33m",
		.enabled = 1, .loglevel = LOGL_NOTICE,
	},
};

const struct log_info log_info = {
	.cat = log_categories,
	.num_cat = ARRAY_SIZE(log_categories),
};

int main(int argc, char **argv)
{
	int rc;

	tall_bsc_ctx = talloc_named_const(NULL, 1, "ipaccess-proxy");
	msgb_talloc_ctx_init(tall_bsc_ctx, 0);

	osmo_init_logging2(tall_bsc_ctx, &log_info);
	log_parse_category_mask(osmo_stderr_target, "DLINP:DLMI");

	handle_options(argc, argv);

	rc = ipaccess_proxy_setup();
	if (rc < 0)
		exit(1);

	signal(SIGUSR1, &signal_handler);
	signal(SIGABRT, &signal_handler);
	osmo_init_ignore_signals();

	while (1) {
		osmo_select_main(0);
	}
}
