/* Core SS7 ASP Handling */

/* (C) 2015-2017 by Harald Welte <laforge@gnumonks.org>
 * (C) 2023 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
 * All Rights Reserved
 *
 * SPDX-License-Identifier: GPL-2.0+
 *
 * 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 "config.h"

#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <inttypes.h>
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/sctp.h>

#include <osmocom/sigtran/osmo_ss7.h>
#include <osmocom/sigtran/mtp_sap.h>
#include <osmocom/sigtran/protocol/mtp.h>
#include <osmocom/sigtran/protocol/sua.h>
#include <osmocom/sigtran/protocol/m3ua.h>

#include <osmocom/core/linuxlist.h>
#include <osmocom/core/select.h>
#include <osmocom/core/utils.h>
#include <osmocom/core/talloc.h>
#include <osmocom/core/logging.h>
#include <osmocom/core/msgb.h>
#include <osmocom/core/socket.h>
#include <osmocom/core/sockaddr_str.h>
#include <osmocom/core/osmo_io.h>

#include <osmocom/netif/stream.h>
#include <osmocom/netif/ipa.h>
#include <osmocom/netif/sctp.h>

#include "sccp_internal.h"
#include "xua_internal.h"
#include "ss7_as.h"
#include "ss7_asp.h"
#include "ss7_internal.h"
#include "ss7_xua_srv.h"
#include "xua_asp_fsm.h"
#include "xua_as_fsm.h"

static int _setsockopt_peer_primary_addr(int fd, const struct osmo_sockaddr *saddr)
{
	int rc;

	struct sctp_setpeerprim so_sctp_setpeerprim = {0};

	/* rfc6458 sec 8: "For the one-to-one style sockets and branched-off one-to-many
	 * style sockets (see Section 9.2), this association ID parameter is ignored"
	 */

	/* NOTE: Requires setting:
	 * - sysctl net.sctp.addip_enable = 1, otherwise EPERM is returned
	 * - sysctl net.sctp.auth_enable = 1, RFC 5061 4.2.7 "An implementation supporting this
	 *   extension MUST list the ASCONF,the ASCONF-ACK, and the AUTH chunks in
	 *   its INIT and INIT-ACK parameters."
	 */

	so_sctp_setpeerprim.sspp_addr = saddr->u.sas;
	rc = setsockopt(fd, IPPROTO_SCTP, SCTP_SET_PEER_PRIMARY_ADDR,
			&so_sctp_setpeerprim, sizeof(so_sctp_setpeerprim));
	if (rc < 0) {
		char buf[128];
		int err = errno;
		strerror_r(err, (char *)buf, sizeof(buf));
		LOGP(DLSS7, LOGL_ERROR, "setsockopt(SCTP_SET_PEER_PRIMARY_ADDR, %s) failed: %s%s\n",
		     osmo_sockaddr_to_str(saddr), buf,
		     err == EPERM ? " (EPERM: Make sure you have sysctl 'net.sctp.auth_enable' "
				    "and 'net.sctp.addip_enable' set to 1)" : "");
	}
	return rc;
}

static int _setsockopt_primary_addr(int fd, const struct osmo_sockaddr *saddr)
{
	int rc;

	struct sctp_prim so_sctp_prim = {0};

	/* rfc6458 sec 8: "For the one-to-one style sockets and branched-off one-to-many
	 * style sockets (see Section 9.2), this association ID parameter is ignored"
	 */

	so_sctp_prim.ssp_addr = saddr->u.sas;
	rc = setsockopt(fd, IPPROTO_SCTP, SCTP_PRIMARY_ADDR,
			&so_sctp_prim, sizeof(so_sctp_prim));
	if (rc < 0) {
		char buf[128];
		strerror_r(errno, (char *)buf, sizeof(buf));
		LOGP(DLSS7, LOGL_ERROR, "setsockopt(SCTP_PRIMARY_ADDR, %s) failed: %s\n",
		     osmo_sockaddr_to_str(saddr), buf);
	}
	return rc;
}

/***********************************************************************
 * Timer Handling
 ***********************************************************************/

const struct osmo_tdef ss7_asp_xua_timer_defaults[SS7_ASP_XUA_TIMERS_LEN] = {
	{ .T = SS7_ASP_XUA_T_ACK,	.default_val = SS7_ASP_XUA_DEFAULT_T_ACK_SEC,	.unit = OSMO_TDEF_S,
	  .desc = "Resend ASP Up/Down/Active/Inactive after timeout waiting for ASP Up/Down/Active/Inactive ACK (ASP role)(s)" },
	{ .T = SS7_ASP_XUA_T_BEAT,	.default_val = SS7_ASP_XUA_DEFAULT_T_BEAT_SEC,	.unit = OSMO_TDEF_S,
	  .desc = "Heartbeat Timer (0 = disabled) (s)" },
	{}
};

/* Appendix C.4 of ITU-T Q.714 */
const struct value_string ss7_asp_xua_timer_names[] = {
	{ SS7_ASP_XUA_T_ACK, "ack" },
	{ SS7_ASP_XUA_T_BEAT, "beat" },
	{}
};

osmo_static_assert(ARRAY_SIZE(ss7_asp_xua_timer_defaults) == (SS7_ASP_XUA_TIMERS_LEN) &&
		   ARRAY_SIZE(ss7_asp_xua_timer_names) == (SS7_ASP_XUA_TIMERS_LEN),
		   assert_ss7_asp_xua_timer_count);

/***********************************************************************
 * SS7 Application Server Process
 ***********************************************************************/

struct value_string osmo_ss7_asp_protocol_vals[] = {
	{ OSMO_SS7_ASP_PROT_NONE,	"none" },
	{ OSMO_SS7_ASP_PROT_SUA,	"sua" },
	{ OSMO_SS7_ASP_PROT_M3UA,	"m3ua" },
	{ OSMO_SS7_ASP_PROT_IPA,	"ipa" },
	{ 0, NULL }
};

const struct value_string osmo_ss7_asp_admin_state_names[] = {
	{ OSMO_SS7_ASP_ADM_S_SHUTDOWN,	"SHUTDOWN" },
	{ OSMO_SS7_ASP_ADM_S_BLOCKED,	"BLOCKED" },
	{ OSMO_SS7_ASP_ADM_S_ENABLED,	"ENABLED" },
	{ 0, NULL }
};

const struct value_string osmo_ss7_asp_role_names[] = {
	{ OSMO_SS7_ASP_ROLE_ASP,	"ASP" },
	{ OSMO_SS7_ASP_ROLE_SG,		"SG" },
	{ OSMO_SS7_ASP_ROLE_IPSP,	"IPSP" },
	{ 0, NULL }
};

/* check if the given transport and ASP protocols are compatible (and implemented) */
bool ss7_asp_protocol_check_trans_proto(enum osmo_ss7_asp_protocol proto, int trans_proto)
{
	switch (proto) {
	case OSMO_SS7_ASP_PROT_IPA:
		if (trans_proto == IPPROTO_TCP)
			return true;
		return false;
	case OSMO_SS7_ASP_PROT_SUA:
		if (trans_proto == IPPROTO_SCTP)
			return true;
		return false;
	case OSMO_SS7_ASP_PROT_M3UA:
		if (trans_proto == IPPROTO_SCTP)
			return true;
		if (trans_proto == IPPROTO_TCP)
			return true;
		return false;
	case OSMO_SS7_ASP_PROT_NONE:
	default:
		return false;
	}
}

/* get _default_ transport protocol for the given ASP protocol */
int ss7_default_trans_proto_for_asp_proto(enum osmo_ss7_asp_protocol proto)
{
	switch (proto) {
	case OSMO_SS7_ASP_PROT_IPA:
		return IPPROTO_TCP;
	case OSMO_SS7_ASP_PROT_SUA:
	case OSMO_SS7_ASP_PROT_M3UA:
	default:
		return IPPROTO_SCTP;
	}
}

static const uint16_t prot2port[] = {
	[OSMO_SS7_ASP_PROT_NONE] = 0,
	[OSMO_SS7_ASP_PROT_SUA] = SUA_PORT,
	[OSMO_SS7_ASP_PROT_M3UA] = M3UA_PORT,
	[OSMO_SS7_ASP_PROT_IPA] = 5000,
};

int osmo_ss7_asp_protocol_port(enum osmo_ss7_asp_protocol prot)
{
	if (prot >= ARRAY_SIZE(prot2port))
		return -EINVAL;
	else
		return prot2port[prot];
}

static const struct rate_ctr_desc ss7_asp_rcd[] = {
	[SS7_ASP_CTR_PKT_RX_TOTAL] = { "rx:packets:total", "Total number of packets received" },
	[SS7_ASP_CTR_PKT_RX_UNKNOWN] = { "rx:packets:unknown", "Number of packets received for unknown PPID" },
	[SS7_ASP_CTR_PKT_RX_NI_MISMATCH] = { "rx:packets:ni_mismatch", "Number of packets received and dropped due to Network Indicator mismatch" },
	[SS7_ASP_CTR_PKT_TX_TOTAL] = { "tx:packets:total", "Total number of packets transmitted" },
};

static const struct rate_ctr_group_desc ss7_asp_rcgd = {
	.group_name_prefix = "sigtran_asp",
	.group_description = "SIGTRAN Application Server Process",
	.num_ctr = ARRAY_SIZE(ss7_asp_rcd),
	.ctr_desc = ss7_asp_rcd,
};
static unsigned int g_ss7_asp_rcg_idx;

static int ss7_asp_apply_tcp_keepalive(const struct osmo_ss7_asp *asp)
{
	uint8_t byte = 1;
	int val;
	int rc;

	if (!asp->cfg.tcp.keepalive_enable)
		return 0;

	LOGPASP(asp, DLSS7, LOGL_DEBUG, "Enabling TCP keep-alive time=%d intvl=%d probes=%d\n",
		asp->cfg.tcp.keepalive_time_present ? asp->cfg.tcp.keepalive_time_value : -1,
		asp->cfg.tcp.keepalive_intvl_present ? asp->cfg.tcp.keepalive_intvl_value : -1,
		asp->cfg.tcp.keepalive_probes_present ? asp->cfg.tcp.keepalive_probes_value : -1);

	if (asp->server) {
		rc = osmo_stream_srv_set_param(asp->server, OSMO_STREAM_SRV_PAR_TCP_SOCKOPT_KEEPALIVE,
					  &byte, sizeof(byte));
		if (rc < 0)
			return rc;

		if (asp->cfg.tcp.keepalive_time_present) {
			val = asp->cfg.tcp.keepalive_time_value;
			rc = osmo_stream_srv_set_param(asp->server, OSMO_STREAM_SRV_PAR_TCP_SOCKOPT_KEEPIDLE,
						       &val, sizeof(val));
			if (rc < 0)
				return rc;
		}
		if (asp->cfg.tcp.keepalive_intvl_present) {
			val = asp->cfg.tcp.keepalive_intvl_value;
			rc = osmo_stream_srv_set_param(asp->server, OSMO_STREAM_SRV_PAR_TCP_SOCKOPT_KEEPINTVL,
						       &val, sizeof(val));
			if (rc < 0)
				return rc;
		}
		if (asp->cfg.tcp.keepalive_probes_present) {
			val = asp->cfg.tcp.keepalive_probes_value;
			rc = osmo_stream_srv_set_param(asp->server, OSMO_STREAM_SRV_PAR_TCP_SOCKOPT_KEEPCNT,
						       &val, sizeof(val));
			if (rc < 0)
				return rc;
		}

	} else if (asp->client) {
		rc = osmo_stream_cli_set_param(asp->client, OSMO_STREAM_CLI_PAR_TCP_SOCKOPT_KEEPALIVE,
					  &byte, sizeof(byte));
		if (rc < 0)
			return rc;

		if (asp->cfg.tcp.keepalive_time_present) {
			val = asp->cfg.tcp.keepalive_time_value;
			rc = osmo_stream_cli_set_param(asp->client, OSMO_STREAM_CLI_PAR_TCP_SOCKOPT_KEEPIDLE,
						       &val, sizeof(val));
			if (rc < 0)
				return rc;
		}
		if (asp->cfg.tcp.keepalive_intvl_present) {
			val = asp->cfg.tcp.keepalive_intvl_value;
			rc = osmo_stream_cli_set_param(asp->client, OSMO_STREAM_CLI_PAR_TCP_SOCKOPT_KEEPINTVL,
						       &val, sizeof(val));
			if (rc < 0)
				return rc;
		}
		if (asp->cfg.tcp.keepalive_probes_present) {
			val = asp->cfg.tcp.keepalive_probes_value;
			rc = osmo_stream_cli_set_param(asp->client, OSMO_STREAM_CLI_PAR_TCP_SOCKOPT_KEEPCNT,
						       &val, sizeof(val));
			if (rc < 0)
				return rc;
		}
	}

	return 0;
}

static int ss7_asp_apply_tcp_user_timeout(const struct osmo_ss7_asp *asp)
{
	int rc = 0;
	unsigned int val;

	if (!asp->cfg.tcp.user_timeout_present)
		return 0;

	LOGPASP(asp, DLSS7, LOGL_DEBUG, "Setting TCP user-timeout %u ms\n",
		asp->cfg.tcp.user_timeout_value);

	val = asp->cfg.tcp.user_timeout_value;
	if (asp->server) {
		rc = osmo_stream_srv_set_param(asp->server, OSMO_STREAM_SRV_PAR_TCP_SOCKOPT_USER_TIMEOUT,
					       &val, sizeof(val));
	} else if (asp->client) {
		rc = osmo_stream_cli_set_param(asp->client, OSMO_STREAM_CLI_PAR_TCP_SOCKOPT_USER_TIMEOUT,
					       &val, sizeof(val));
	}

	return rc;
}

int ss7_asp_apply_tcp_pars(const struct osmo_ss7_asp *asp)
{
	int rc;
	rc = ss7_asp_apply_tcp_keepalive(asp);
	if (rc < 0)
		return rc;
	return ss7_asp_apply_tcp_user_timeout(asp);
}

int ss7_asp_apply_new_local_address(const struct osmo_ss7_asp *asp, unsigned int loc_idx)
{
	const char *new_loc_addr;
	int fd;

	OSMO_ASSERT(loc_idx < asp->cfg.local.host_cnt);
	new_loc_addr = asp->cfg.local.host[loc_idx];

	LOGPASP(asp, DLSS7, LOGL_INFO, "Add local address %s\n",
		new_loc_addr);

	if (asp->cfg.is_server)
		fd = osmo_stream_srv_get_fd(asp->server);
	else
		fd = osmo_stream_cli_get_fd(asp->client);

	if (fd < 0)
		return fd;

	return osmo_sock_multiaddr_add_local_addr(fd, &new_loc_addr, 1);
}

int ss7_asp_apply_drop_local_address(const struct osmo_ss7_asp *asp, unsigned int loc_idx)
{
	const char *new_loc_addr;
	int fd;

	OSMO_ASSERT(loc_idx < asp->cfg.local.host_cnt);
	new_loc_addr = asp->cfg.local.host[loc_idx];

	LOGPASP(asp, DLSS7, LOGL_INFO, "Remove local address %s\n",
		new_loc_addr);

	if (asp->cfg.is_server)
		fd = osmo_stream_srv_get_fd(asp->server);
	else
		fd = osmo_stream_cli_get_fd(asp->client);

	if (fd < 0)
		return fd;

	return osmo_sock_multiaddr_del_local_addr(fd, &new_loc_addr, 1);
}

int ss7_asp_apply_peer_primary_address(const struct osmo_ss7_asp *asp)
{
	struct osmo_sockaddr_str addr_str;
	struct osmo_sockaddr addr;
	uint16_t local_port;
	int fd, rc;

	/* No SCTP Peer Primary Address explicitly configured, do nothing. */
	if (asp->cfg.local.idx_primary == -1)
		return 0;
	OSMO_ASSERT(asp->cfg.local.idx_primary < asp->cfg.local.host_cnt);

	if (asp->cfg.is_server)
		fd = osmo_stream_srv_get_fd(asp->server);
	else
		fd = osmo_stream_cli_get_fd(asp->client);

	if (fd < 0)
		return fd;

	if (asp->cfg.local.port == 0) {
		char port_buf[16];
		osmo_sock_get_local_ip_port(fd, port_buf, sizeof(port_buf));
		local_port = atoi(port_buf);
	} else {
		local_port = asp->cfg.local.port;
	}
	rc = osmo_sockaddr_str_from_str(&addr_str,
					asp->cfg.local.host[asp->cfg.local.idx_primary],
					local_port);
	if (rc < 0)
		return rc;
	rc = osmo_sockaddr_str_to_sockaddr(&addr_str, &addr.u.sas);
	if (rc < 0)
		return rc;
	LOGPASP(asp, DLSS7, LOGL_INFO, "Set Peer's Primary Address %s\n",
		osmo_sockaddr_to_str(&addr));
	rc = _setsockopt_peer_primary_addr(fd, &addr);

	return rc;
}

int ss7_asp_apply_primary_address(const struct osmo_ss7_asp *asp)
{
	struct osmo_sockaddr_str addr_str;
	struct osmo_sockaddr addr;
	int fd, rc;

	/* No SCTP Primary Address explicitly configured, do nothing. */
	if (asp->cfg.remote.idx_primary == -1)
		return 0;
	OSMO_ASSERT(asp->cfg.remote.idx_primary < asp->cfg.remote.host_cnt);

	if (asp->cfg.is_server)
		fd = osmo_stream_srv_get_fd(asp->server);
	else
		fd = osmo_stream_cli_get_fd(asp->client);

	if (fd < 0)
		return fd;

	rc = osmo_sockaddr_str_from_str(&addr_str,
					asp->cfg.remote.host[asp->cfg.remote.idx_primary],
					asp->cfg.remote.port);
	if (rc < 0)
		return rc;
	rc = osmo_sockaddr_str_to_sockaddr(&addr_str, &addr.u.sas);
	if (rc < 0)
		return rc;
	LOGPASP(asp, DLSS7, LOGL_INFO, "Set Primary Address %s\n",
		osmo_sockaddr_to_str(&addr));
	rc = _setsockopt_primary_addr(fd, &addr);
	return rc;
}

/* Returns whether the address signalled in the SCTP_PEER_ADDR_CHANGE matches
 * the user-configured Primary Address. */
static bool sctp_peer_addr_change_ev_addr_matches_our_primary(const struct osmo_ss7_asp *asp,
							      const union sctp_notification *notif)
{
	const char *primary_str;
	int primary_str_type;
	struct osmo_sockaddr ev_addr, primary;

	OSMO_ASSERT(asp->cfg.remote.idx_primary >= 0);

	primary_str = asp->cfg.remote.host[asp->cfg.remote.idx_primary];
	primary_str_type = osmo_ip_str_type(primary_str);
	memcpy(&ev_addr.u.sas, &notif->sn_paddr_change.spc_aaddr, sizeof(ev_addr.u.sas));

	/* This whole switch is to properly compare addresses (take into account v6mapped IPv4 addresses): */
	switch (ev_addr.u.sa.sa_family) {
	case AF_INET:
		switch (primary_str_type) {
		case AF_INET:
			primary = ev_addr; /* Copy AF + port */
			inet_pton(AF_INET, primary_str, &primary.u.sin.sin_addr);
			return (osmo_sockaddr_cmp(&primary, &ev_addr) == 0);
		case AF_INET6:
			/* for sure not the same */
			return false;
		}
		return false;
	case AF_INET6:
		/* "ev_addr" can either be a IPv6 addr or a v6-mapped IPv4
		 * address. Compare both as IPv6 (or v6-mapped IPv4) addresses. */
		primary = ev_addr; /* Copy AF + port */
		inet_pton(AF_INET6, primary_str, &primary.u.sin6.sin6_addr);
		return (osmo_sockaddr_cmp(&primary, &ev_addr) == 0);
	default:
		return false;
	}
}

/* Simple SCTP Path-manager tracking/driving the VTY-user-configured primary
 * address against the kernel when assoc state changes: */
static void asp_handle_sctp_notif_monitor_primary_address(const struct osmo_ss7_asp *asp,
							  const union sctp_notification *notif)
{
	bool match;

	if (asp->cfg.remote.idx_primary == -1)
		return;
	if (notif->sn_header.sn_type != SCTP_PEER_ADDR_CHANGE)
		return;

	switch (notif->sn_paddr_change.spc_state) {
	case SCTP_ADDR_AVAILABLE:
	case SCTP_ADDR_ADDED:
	case SCTP_ADDR_CONFIRMED:
		/* If our primary addr became available/added/confirmed, set it */
		match = sctp_peer_addr_change_ev_addr_matches_our_primary(asp, notif);
		if (match)
			ss7_asp_apply_primary_address(asp);
		break;
	case SCTP_ADDR_MADE_PRIM:
		/* If another primary addr was made primary, overwrite it by setting it again */
		match = sctp_peer_addr_change_ev_addr_matches_our_primary(asp, notif);
		if (!match)
			ss7_asp_apply_primary_address(asp);
	default:
		break;
	}
}

static int asp_client_apply_sctp_init_pars(struct osmo_ss7_asp *asp)
{
	uint8_t byte;

	OSMO_ASSERT(asp->client);
	byte = 1; /*AUTH is needed by ASCONF. enable, don't abort socket creation if AUTH can't be enabled */
	osmo_stream_cli_set_param(asp->client, OSMO_STREAM_CLI_PAR_SCTP_SOCKOPT_AUTH_SUPPORTED, &byte, sizeof(byte));
	byte = 1; /* enable, don't abort socket creation if ASCONF can't be enabled */
	osmo_stream_cli_set_param(asp->client, OSMO_STREAM_CLI_PAR_SCTP_SOCKOPT_ASCONF_SUPPORTED, &byte, sizeof(byte));
	if (asp->cfg.sctp_init.num_ostreams_present)
		osmo_stream_cli_set_param(asp->client, OSMO_STREAM_CLI_PAR_SCTP_INIT_NUM_OSTREAMS,
					&asp->cfg.sctp_init.num_ostreams_value,
					sizeof(asp->cfg.sctp_init.num_ostreams_value));
	if (asp->cfg.sctp_init.max_instreams_present)
		osmo_stream_cli_set_param(asp->client, OSMO_STREAM_CLI_PAR_SCTP_INIT_MAX_INSTREAMS,
					&asp->cfg.sctp_init.max_instreams_value,
					sizeof(asp->cfg.sctp_init.max_instreams_value));
	if (asp->cfg.sctp_init.max_attempts_present)
		osmo_stream_cli_set_param(asp->client, OSMO_STREAM_CLI_PAR_SCTP_INIT_MAX_ATTEMPTS,
					&asp->cfg.sctp_init.max_attempts_value,
					sizeof(asp->cfg.sctp_init.max_attempts_value));
	if (asp->cfg.sctp_init.max_init_timeo_present)
		osmo_stream_cli_set_param(asp->client, OSMO_STREAM_CLI_PAR_SCTP_INIT_TIMEOUT,
					&asp->cfg.sctp_init.max_init_timeo_value,
					sizeof(asp->cfg.sctp_init.max_init_timeo_value));
	return 0;
}

/* Set default values for local and remote peer hosts if they are not yet set.
 *  \param[in] asp ASP for which to set default hosts.
 *  \returns true if values where changed, false otherwise.
 *
 * If the ASP is already started, osmo_ss7_asp_restart() must be called
 * afterwards in order to apply the new settings.
 * This API is internal, hence doesn't appear in osmo_ss7.h
 */
bool ss7_asp_set_default_peer_hosts(struct osmo_ss7_asp *asp)
{
	bool changed = false;
	/* If no local addr was set */
	if (!asp->cfg.local.host_cnt) {
		bool rem_has_v4 = false, rem_has_v6 = false;
		int i;
		for (i = 0; i < asp->cfg.remote.host_cnt; i++) {
			if (osmo_ip_str_type(asp->cfg.remote.host[i]) == AF_INET6)
				rem_has_v6 = true;
			else
				rem_has_v4 = true;
		}
		/* "::" Covers both IPv4 and IPv6, but if only IPv4
		 * address are set on the remote side, IPv4 on the local
		 * side must be set too */
		if (ss7_ipv6_sctp_supported("::", true) && !(rem_has_v4 && !rem_has_v6))
			ss7_asp_peer_add_host(&asp->cfg.local, asp, "::");
		else
			ss7_asp_peer_add_host(&asp->cfg.local, asp, "0.0.0.0");
		changed = true;
	}
	/* If no remote addr was set */
	if (!asp->cfg.remote.host_cnt) {
		ss7_asp_peer_add_host(&asp->cfg.remote, asp, "127.0.0.1");
		if (ss7_ipv6_sctp_supported("::1", false))
			ss7_asp_peer_add_host(&asp->cfg.remote, asp, "::1");
		changed = true;
	}
	return changed;
}

static uint16_t get_in_port(struct sockaddr *sa)
{
	switch (sa->sa_family) {
	case AF_INET:
		return (((struct sockaddr_in *)sa)->sin_port);
	case AF_INET6:
		return (((struct sockaddr_in6 *)sa)->sin6_port);
	default:
		return 0;
	}
}

/* Converts string representation of v4-mappend-on-v6 IP addr to a pure IPv4 address.
 * Example: ::ffff:172.18.19.200 => 172.18.19.200
 */
static void chop_v4_mapped_on_v6_prefix(char *buf)
{
	char *last_colon;
	size_t len;
	char *first_dot = strchr(buf, '.');

	if (!first_dot)
		return; /* Not an IPv4-mappend-on-v6 string representation, nothing to do */
	last_colon = strrchr(buf, ':');
	if (!last_colon)
		return; /* pure IPv4 address, nothing to do */

	len = strlen(last_colon + 1);
	memmove(buf, last_colon + 1, len);
	buf[len] = '\0';
}

/*! \brief Find an ASP definition matching the local+remote IP/PORT of given fd
 *  \param[in] fd socket descriptor of given socket
 *  \returns SS7 ASP in case a matching one is found; NULL otherwise */
struct osmo_ss7_asp *
ss7_asp_find_by_socket_addr(int fd, int trans_proto)
{
	struct osmo_ss7_instance *inst;
	struct sockaddr_storage sa_l, sa_r;
	socklen_t sa_len_l = sizeof(sa_l);
	socklen_t sa_len_r = sizeof(sa_r);
	char hostbuf_l[64], hostbuf_r[64];
	uint16_t local_port, remote_port;
	bool loc_is_v6, rem_is_v6;
	int rc;

	OSMO_ASSERT(ss7_initialized);
	/* convert local and remote IP to string */
	rc = getsockname(fd, (struct sockaddr *)&sa_l, &sa_len_l);
	if (rc < 0)
		return NULL;
	rc = getnameinfo((struct sockaddr *)&sa_l, sa_len_l,
			 hostbuf_l, sizeof(hostbuf_l),
			 NULL, 0, NI_NUMERICHOST);
	if (rc < 0)
		return NULL;
	local_port = ntohs(get_in_port((struct sockaddr *)&sa_l));

	rc = getpeername(fd, (struct sockaddr *)&sa_r, &sa_len_r);
	if (rc < 0)
		return NULL;
	rc = getnameinfo((struct sockaddr *)&sa_r, sa_len_r,
			 hostbuf_r, sizeof(hostbuf_r),
			 NULL, 0, NI_NUMERICHOST);
	if (rc < 0)
		return NULL;
	remote_port = ntohs(get_in_port((struct sockaddr *)&sa_r));

	/* If multi-home is used with both IPv4 and IPv6, then the socket is
	 * AF_INET6, and then returned IPv4 addresses are actually v6mapped ones.
	 * We need to convert them to IPv4 before matching.
	 */
	chop_v4_mapped_on_v6_prefix(hostbuf_l);
	chop_v4_mapped_on_v6_prefix(hostbuf_r);
	loc_is_v6 = osmo_ip_str_type(hostbuf_l) == AF_INET6;
	rem_is_v6 = osmo_ip_str_type(hostbuf_r) == AF_INET6;

	/* check all instances for any ASP definition matching the
	 * address combination of local/remote ip/port */
	llist_for_each_entry(inst, &osmo_ss7_instances, list) {
		struct osmo_ss7_asp *asp;
		llist_for_each_entry(asp, &inst->asp_list, list) {
			if (asp->cfg.trans_proto != trans_proto)
				continue;
			if (asp->cfg.local.port != local_port)
				continue;
			if (asp->cfg.remote.port && asp->cfg.remote.port != remote_port)
				continue;

			if (!ss7_asp_peer_match_host(&asp->cfg.local, hostbuf_l, loc_is_v6))
				continue; /* didn't match any local.host */

			/* If no remote host was set, it's probably a server and hence we match any cli src */
			if (asp->cfg.remote.host_cnt) {
				if (!ss7_asp_peer_match_host(&asp->cfg.remote, hostbuf_r, rem_is_v6))
					continue; /* didn't match any remote.host */
			}

			return asp;
		}
	}

	return NULL;
}

struct osmo_ss7_asp *ss7_asp_alloc(struct osmo_ss7_instance *inst, const char *name,
				   uint16_t remote_port, uint16_t local_port,
				   int trans_proto, enum osmo_ss7_asp_protocol proto)
{
	struct osmo_ss7_asp *asp;

	if (!ss7_asp_protocol_check_trans_proto(proto, trans_proto)) {
		LOGP(DLSCCP, LOGL_ERROR,
		     "ASP protocol '%s' with transport protocol %d is not supported",
		     osmo_ss7_asp_protocol_name(proto), trans_proto);
		return NULL;
	}

	asp = talloc_zero(inst, struct osmo_ss7_asp);
	asp->ctrg = rate_ctr_group_alloc(asp, &ss7_asp_rcgd, g_ss7_asp_rcg_idx++);
	if (!asp->ctrg) {
		talloc_free(asp);
		return NULL;
	}
	rate_ctr_group_set_name(asp->ctrg, name);
	asp->inst = inst;
	/* ASP in "no shutdown" state by default: */
	asp->cfg.adm_state = OSMO_SS7_ASP_ADM_S_ENABLED;
	ss7_asp_peer_init(&asp->cfg.remote);
	asp->cfg.remote.port = remote_port;
	ss7_asp_peer_init(&asp->cfg.local);
	asp->cfg.local.port = local_port;
	asp->cfg.trans_proto = trans_proto;
	asp->cfg.proto = proto;
	asp->cfg.name = talloc_strdup(asp, name);

	asp->cfg.T_defs_xua = talloc_memdup(asp, ss7_asp_xua_timer_defaults,
					    sizeof(ss7_asp_xua_timer_defaults));
	osmo_tdefs_reset(asp->cfg.T_defs_xua);
	asp->cfg.T_defs_lm = talloc_memdup(asp, ss7_asp_lm_timer_defaults,
					   sizeof(ss7_asp_lm_timer_defaults));
	osmo_tdefs_reset(asp->cfg.T_defs_lm);

	llist_add_tail(&asp->list, &inst->asp_list);

	/* The SUA code internally needs SCCP to work */
	if (proto == OSMO_SS7_ASP_PROT_SUA)
		osmo_ss7_ensure_sccp(inst);
	return asp;
}

void osmo_ss7_asp_destroy(struct osmo_ss7_asp *asp)
{
	struct osmo_ss7_as *as, *as2;

	OSMO_ASSERT(ss7_initialized);
	LOGPASP(asp, DLSS7, LOGL_INFO, "Destroying ASP\n");

	if (asp->server)
		osmo_stream_srv_destroy(asp->server);
	if (asp->client)
		osmo_stream_cli_destroy(asp->client);
	if (asp->fi)
		osmo_fsm_inst_term(asp->fi, OSMO_FSM_TERM_REQUEST, NULL);

	/* Unlink from all ASs we are part of.
	 * Some RKM-dynamically allocated ASs may be freed as a result from this: */
	llist_for_each_entry_safe(as, as2, &asp->inst->as_list, list)
		ss7_as_del_asp(as, asp);

	/* unlink from ss7_instance */
	asp->inst = NULL;
	llist_del(&asp->list);
	rate_ctr_group_free(asp->ctrg);
	/* release memory */
	talloc_free(asp);
}

static int xua_cli_read_cb(struct osmo_stream_cli *conn, int res, struct msgb *msg);
static int ipa_cli_read_cb(struct osmo_stream_cli *conn, int res, struct msgb *msg);
static int m3ua_tcp_cli_read_cb(struct osmo_stream_cli *conn, int res, struct msgb *msg);
static int xua_cli_connect_cb(struct osmo_stream_cli *cli);
static int xua_cli_disconnect_cb(struct osmo_stream_cli *cli);
static int xua_cli_close_and_reconnect(struct osmo_stream_cli *cli);


static int ss7_asp_start_client(struct osmo_ss7_asp *asp)
{
	int rc;

	if (!asp->client)
		asp->client = osmo_stream_cli_create(asp);
	if (!asp->client) {
		LOGPASP(asp, DLSS7, LOGL_ERROR, "Unable to create stream"
			" client for ASP %s\n", asp->cfg.name);
		return -1;
	}
	osmo_stream_cli_set_name(asp->client, asp->cfg.name);
	osmo_stream_cli_set_nodelay(asp->client, true);
	osmo_stream_cli_set_addrs(asp->client, (const char **)asp->cfg.remote.host, asp->cfg.remote.host_cnt);
	osmo_stream_cli_set_port(asp->client, asp->cfg.remote.port);
	osmo_stream_cli_set_local_addrs(asp->client, (const char **)asp->cfg.local.host, asp->cfg.local.host_cnt);
	osmo_stream_cli_set_local_port(asp->client, asp->cfg.local.port);
	osmo_stream_cli_set_proto(asp->client, asp->cfg.trans_proto);
	osmo_stream_cli_set_reconnect_timeout(asp->client, 5);
	osmo_stream_cli_set_connect_cb(asp->client, xua_cli_connect_cb);
	osmo_stream_cli_set_disconnect_cb(asp->client, xua_cli_disconnect_cb);
	switch (asp->cfg.proto) {
	case OSMO_SS7_ASP_PROT_IPA:
		OSMO_ASSERT(asp->cfg.trans_proto == IPPROTO_TCP);
		osmo_stream_cli_set_read_cb2(asp->client, ipa_cli_read_cb);
		osmo_stream_cli_set_segmentation_cb(asp->client, osmo_ipa_segmentation_cb);
		break;
	case OSMO_SS7_ASP_PROT_M3UA:
		if (asp->cfg.trans_proto == IPPROTO_SCTP) {
			osmo_stream_cli_set_read_cb2(asp->client, xua_cli_read_cb);
			osmo_stream_cli_set_segmentation_cb(asp->client, NULL);
		} else if (asp->cfg.trans_proto == IPPROTO_TCP) {
			osmo_stream_cli_set_read_cb2(asp->client, m3ua_tcp_cli_read_cb);
			osmo_stream_cli_set_segmentation_cb(asp->client, xua_tcp_segmentation_cb);
		} else
			OSMO_ASSERT(0);
		break;
	default:
		OSMO_ASSERT(asp->cfg.trans_proto == IPPROTO_SCTP);
		osmo_stream_cli_set_read_cb2(asp->client, xua_cli_read_cb);
		osmo_stream_cli_set_segmentation_cb(asp->client, NULL);
		break;
	}
	osmo_stream_cli_set_data(asp->client, asp);

	switch (asp->cfg.trans_proto) {
	case IPPROTO_TCP:
		(void)ss7_asp_apply_tcp_pars(asp);
		break;
	case IPPROTO_SCTP:
		(void)asp_client_apply_sctp_init_pars(asp);
		break;
	default:
		OSMO_ASSERT(0);
	}

	rc = osmo_stream_cli_open(asp->client);
	return rc;
}

/* Force stream disconnection, which should trigger disconnect_cb/close_cb() and
 * announce disconnection to upper layers. */
int ss7_asp_disconnect_stream(struct osmo_ss7_asp *asp)
{
	struct osmo_stream_cli *cli;
	struct osmo_stream_srv *srv;

	/* First tear down previous state if existing: */
	if (asp->cfg.is_server) {
		/* We are in server mode now */
		if (asp->client) {
			/* if we previously were in client mode,
			 * destroy it */
			cli = asp->client;
			asp->client = NULL;
			osmo_stream_cli_destroy(cli);
		}
		if (asp->server) {
			/* Make sure we close the previous stream right now: */
			srv = asp->server;
			asp->server = NULL;
			osmo_stream_srv_destroy(srv);
		}
	} else {
		/* We are in client mode now */
		if (asp->server) {
			/* if we previously were in server mode,
			 * destroy it */
			srv = asp->server;
			asp->server = NULL;
			osmo_stream_srv_destroy(srv);
		}
		if (asp->client) {
			/* Make sure we close the previous stream before starting a new one: */
			osmo_stream_cli_close(asp->client);
		}
	}
	return 0;
}

int osmo_ss7_asp_restart(struct osmo_ss7_asp *asp)
{
	int rc;
	char bufloc[512], bufrem[512];
	bool destroyed = asp->dyn_allocated && asp->cfg.is_server;

	OSMO_ASSERT(ss7_initialized);
	ss7_asp_peer_snprintf(bufloc, sizeof(bufloc), &asp->cfg.local);
	ss7_asp_peer_snprintf(bufrem, sizeof(bufrem), &asp->cfg.remote);
	LOGPASP(asp, DLSS7, LOGL_INFO, "Restarting ASP %s, r=%s<->l=%s\n",
		asp->cfg.name, bufrem, bufloc);

	/* First tear down previous state if existing: */
	ss7_asp_disconnect_stream(asp);

	/* Dynamic ASPs in SCTP=server are destroyed when connection is closed. */
	if (destroyed)
		return 0;

	/* The ASP FSM must be terminated *after* tearing down the conn, so that
	 * DISCONNECT events go up the stack */
	if (asp->fi) {
		osmo_fsm_inst_term(asp->fi, OSMO_FSM_TERM_REQUEST, NULL);
		OSMO_ASSERT(!asp->fi);
	}

	/* Apply default LM FSM for client ASP */
	if (asp->cfg.proto != OSMO_SS7_ASP_PROT_IPA &&
	    asp->cfg.role == OSMO_SS7_ASP_ROLE_ASP &&
	    !asp->cfg.is_server) {
		osmo_ss7_asp_use_default_lm(asp, LOGL_DEBUG);
	} else {
		osmo_ss7_asp_remove_default_lm(asp);
	}

	if ((rc = xua_asp_fsm_start(asp, asp->cfg.role, LOGL_DEBUG)) < 0)
		return rc;
	OSMO_ASSERT(asp->fi);

	if (asp->cfg.adm_state == OSMO_SS7_ASP_ADM_S_SHUTDOWN) {
		LOGPASP(asp, DLSS7, LOGL_NOTICE, "Skipping start for ASP in administrative state %s\n",
			get_value_string(osmo_ss7_asp_admin_state_names, OSMO_SS7_ASP_ADM_S_SHUTDOWN));
		return 0;
	}

	/* Now start the new stream: */

	if (asp->cfg.is_server) {
		/* We are in server mode now */
		/* FIXME: ensure we have a SCTP server */
		LOGPASP(asp, DLSS7, LOGL_NOTICE, "ASP Restart for server "
			"not implemented yet!\n");
	} else {
		/* We are in client mode now */
		rc = ss7_asp_start_client(asp);
		if (rc < 0) {
			LOGPASP(asp, DLSS7, LOGL_ERROR, "Unable to open stream"
				" client for ASP %s, %s ==> %s\n", asp->cfg.name, bufloc, bufrem);
			/* we don't return error in here because osmo_stream_cli_open()
			will continue to retry (due to timeout being explicitly set with
			osmo_stream_cli_set_reconnect_timeout() above) to connect so the error is transient */
		}
	}
	return 0;
}

bool osmo_ss7_asp_active(const struct osmo_ss7_asp *asp)
{
	if (!asp->fi)
		return false;
	return asp->fi->state == XUA_ASP_S_ACTIVE;
}

bool ss7_asp_is_started(const struct osmo_ss7_asp *asp)
{
	if (asp->cfg.is_server)
		return !!asp->server;
	else
		return !!asp->client;
}

/***********************************************************************
 * libosmo-netif integration for SCTP stream server/client
 ***********************************************************************/

static int get_logevel_by_sn_type(int sn_type)
{
	switch (sn_type) {
	case SCTP_ADAPTATION_INDICATION:
	case SCTP_PEER_ADDR_CHANGE:
#ifdef SCTP_AUTHENTICATION_INDICATION
	case SCTP_AUTHENTICATION_INDICATION:
#endif
#ifdef SCTP_SENDER_DRY_EVENT
	case SCTP_SENDER_DRY_EVENT:
#endif
		return LOGL_INFO;
	case SCTP_ASSOC_CHANGE:
		return LOGL_NOTICE;
	case SCTP_SHUTDOWN_EVENT:
	case SCTP_PARTIAL_DELIVERY_EVENT:
		return LOGL_NOTICE;
	case SCTP_SEND_FAILED:
	case SCTP_REMOTE_ERROR:
		return LOGL_ERROR;
	default:
		return LOGL_NOTICE;
	}
}

static void log_sctp_notification(struct osmo_ss7_asp *asp, const char *pfx,
				  union sctp_notification *notif)
{
	int log_level;

	LOGPASP(asp, DLSS7, LOGL_INFO, "%s SCTP NOTIFICATION %u flags=0x%0x\n",
		pfx, notif->sn_header.sn_type,
		notif->sn_header.sn_flags);

	log_level = get_logevel_by_sn_type(notif->sn_header.sn_type);

	switch (notif->sn_header.sn_type) {
	case SCTP_ASSOC_CHANGE:
		LOGPASP(asp, DLSS7, log_level, "%s SCTP_ASSOC_CHANGE: %s\n",
			pfx, osmo_sctp_assoc_chg_str(notif->sn_assoc_change.sac_state));
		break;
	case SCTP_PEER_ADDR_CHANGE:
		{
		char addr_str[INET6_ADDRSTRLEN + 10];
		struct sockaddr_storage sa = notif->sn_paddr_change.spc_aaddr;
		osmo_sockaddr_to_str_buf(addr_str, sizeof(addr_str), (const struct osmo_sockaddr *)&sa);
		LOGPASP(asp, DLSS7, log_level, "%s SCTP_PEER_ADDR_CHANGE: %s %s err=%s\n",
			pfx, osmo_sctp_paddr_chg_str(notif->sn_paddr_change.spc_state), addr_str,
			(notif->sn_paddr_change.spc_state == SCTP_ADDR_UNREACHABLE) ?
			osmo_sctp_sn_error_str(notif->sn_paddr_change.spc_error) : "None");
		}
		break;
	default:
		LOGPASP(asp, DLSS7, log_level, "%s %s\n",
			pfx, osmo_sctp_sn_type_str(notif->sn_header.sn_type));
		break;
	}
}

/* netif code tells us we can read something from the socket */
int ss7_asp_ipa_srv_conn_rx_cb(struct osmo_stream_srv *conn, int res, struct msgb *msg)
{
	struct osmo_ss7_asp *asp = osmo_stream_srv_get_data(conn);
	struct osmo_stream_srv_link *link = osmo_stream_srv_get_master(conn);
	int rc;

	/* Reparent msg to srv_link, to avoid "msg" being automatically freed if
	 * "conn" is teared down during msg handling (or if its associated
	 * dynamic ASP becomes unused), which would then result in a double-free
	 * if same code path then explicitly frees the msgb through msgb_free().
	 */
	talloc_steal(link, msg);

	if (res <= 0) {
		if (res == -EAGAIN) {
			msgb_free(msg);
			return 0;
		}
		msgb_free(msg);
		osmo_stream_srv_destroy(conn);
		return res;
	}

	msg->dst = asp;
	rate_ctr_inc2(asp->inst->ctrg, SS7_INST_CTR_PKT_RX_TOTAL);
	rate_ctr_inc2(asp->ctrg, SS7_ASP_CTR_PKT_RX_TOTAL);
	rc = ipa_rx_msg(asp, msg, asp->ipa.sls);
	msgb_free(msg);
	return rc;
}

static int xua_rx_msg(struct osmo_ss7_asp *asp, struct msgb *msg)
{
	unsigned int ppid = msgb_sctp_ppid(msg);
	int rc;

	if (ppid == SUA_PPID && asp->cfg.proto == OSMO_SS7_ASP_PROT_SUA)
		rc = sua_rx_msg(asp, msg);
	else if (ppid == M3UA_PPID && asp->cfg.proto == OSMO_SS7_ASP_PROT_M3UA)
		rc = m3ua_rx_msg(asp, msg);
	else
		rc = ss7_asp_rx_unknown(asp, ppid, msg);
	return rc;
}

static int m3ua_tcp_rx_msg(struct osmo_ss7_asp *asp, struct msgb *msg)
{
	const struct xua_common_hdr *hdr = (const struct xua_common_hdr *) msg->data;

	/* spoof SCTP PPID */
	msgb_sctp_ppid(msg) = M3UA_PPID;

	/* spoof SCTP Stream ID */
	if (hdr->msg_class == M3UA_MSGC_XFER)
		msgb_sctp_stream(msg) = 1;
	else
		msgb_sctp_stream(msg) = 0;

	return m3ua_rx_msg(asp, msg);
}

/* netif code tells us we can read something from the socket */
int ss7_asp_xua_srv_conn_rx_cb(struct osmo_stream_srv *conn, int res, struct msgb *msg)
{
	struct osmo_ss7_asp *asp = osmo_stream_srv_get_data(conn);
	struct osmo_stream_srv_link *link = osmo_stream_srv_get_master(conn);
	int flags;
	int rc = 0;

	/* Reparent msg to srv_link, to avoid "msg" being automatically freed if
	 * "conn" is teared down during msg handling (or if its associated
	 * dynamic ASP becomes unused), which would then result in a double-free
	 * if same code path then explicitly frees the msgb through msgb_free().
	 */
	talloc_steal(link, msg);

	/* process the received xUA message */
	flags = msgb_sctp_msg_flags(msg);

	LOGPASP(asp, DLSS7, LOGL_DEBUG, "%s(): sctp_recvmsg() returned %d (flags=0x%x)\n",
		__func__, rc, flags);

	if (flags & OSMO_STREAM_SCTP_MSG_FLAGS_NOTIFICATION) {
		union sctp_notification *notif = (union sctp_notification *) msgb_data(msg);
		log_sctp_notification(asp, "xUA SRV", notif);
		asp_handle_sctp_notif_monitor_primary_address(asp, notif);

		switch (notif->sn_header.sn_type) {
		case SCTP_ASSOC_CHANGE:
			if (notif->sn_assoc_change.sac_state == SCTP_RESTART)
				xua_asp_send_xlm_prim_simple(asp, OSMO_XLM_PRIM_M_SCTP_RESTART,
							     PRIM_OP_INDICATION);
		default:
			break;
		}
		msgb_free(msg);
		return 0;
	}
	if (res <= 0) {
		msgb_free(msg);
		osmo_stream_srv_destroy(conn);
		return rc;
	}

	msg->dst = asp;
	rate_ctr_inc2(asp->inst->ctrg, SS7_INST_CTR_PKT_RX_TOTAL);
	rate_ctr_inc2(asp->ctrg, SS7_ASP_CTR_PKT_RX_TOTAL);
	rc = xua_rx_msg(asp, msg);
	msgb_free(msg);
	return rc;
}

int xua_tcp_segmentation_cb(struct msgb *msg)
{
	const struct xua_common_hdr *hdr;
	size_t msg_length;

	if (msgb_length(msg) < sizeof(*hdr))
		return -EAGAIN;

	hdr = (const struct xua_common_hdr *) msg->data;
	msg_length = ntohl(hdr->msg_length); /* includes sizeof(*hdr) */

	return msg_length;
}

/* netif code tells us we can read something from the socket */
int ss7_asp_m3ua_tcp_srv_conn_rx_cb(struct osmo_stream_srv *conn, int res, struct msgb *msg)
{
	struct osmo_ss7_asp *asp = osmo_stream_srv_get_data(conn);
	struct osmo_stream_srv_link *link = osmo_stream_srv_get_master(conn);
	int rc;

	/* Reparent msg to srv_link, to avoid "msg" being automatically freed if
	 * "conn" is teared down during msg handling (or if its associated
	 * dynamic ASP becomes unused), which would then result in a double-free
	 * if same code path then explicitly frees the msgb through msgb_free().
	 */
	talloc_steal(link, msg);

	if (res <= 0) {
		if (res == -EAGAIN) {
			msgb_free(msg);
			return 0;
		}
		msgb_free(msg);
		osmo_stream_srv_destroy(conn);
		return res;
	}

	msg->dst = asp;
	rate_ctr_inc2(asp->inst->ctrg, SS7_INST_CTR_PKT_RX_TOTAL);
	rate_ctr_inc2(asp->ctrg, SS7_ASP_CTR_PKT_RX_TOTAL);
	rc = m3ua_tcp_rx_msg(asp, msg);
	msgb_free(msg);
	return rc;
}

/* client has established SCTP connection to server */
static int xua_cli_connect_cb(struct osmo_stream_cli *cli)
{
	int fd = osmo_stream_cli_get_fd(cli);
	struct osmo_ss7_asp *asp = osmo_stream_cli_get_data(cli);
	int rc = 0;

	if (fd < 0)
		return fd;

	osmo_iofd_set_alloc_info(osmo_stream_cli_get_iofd(cli), M3UA_MSG_SIZE, M3UA_MSG_HEADROOM);

	/* update the socket name */
	talloc_free(asp->sock_name);
	asp->sock_name = osmo_sock_get_name(asp, fd);

	LOGPASP(asp, DLSS7, LOGL_INFO, "Client connected %s\n", asp->sock_name);

	/* Now that we have the conn in place, the local/remote addresses are
	 * fed and the local port is known for sure. Apply SCTP Primary addresses
	 * if needed:
	 */
	if (asp->cfg.trans_proto == IPPROTO_SCTP) {
		rc = ss7_asp_apply_peer_primary_address(asp);
		rc = ss7_asp_apply_primary_address(asp);
	}

	if (asp->lm && asp->lm->prim_cb) {
		/* Notify layer manager that a connection has been
		 * established */
		xua_asp_send_xlm_prim_simple(asp, OSMO_XLM_PRIM_M_SCTP_ESTABLISH, PRIM_OP_INDICATION);
	} else {
		/* directly ask the ASP FSM to start by sending an ASP-UP ... */
		osmo_fsm_inst_dispatch(asp->fi, XUA_ASP_E_M_ASP_UP_REQ, NULL);
	}

	return rc;
}

static void xua_cli_close(struct osmo_stream_cli *cli)
{
	struct osmo_ss7_asp *asp = osmo_stream_cli_get_data(cli);

	osmo_stream_cli_close(cli);
	osmo_fsm_inst_dispatch(asp->fi, XUA_ASP_E_SCTP_COMM_DOWN_IND, asp);
	/* send M-SCTP_RELEASE.ind to XUA Layer Manager */
	xua_asp_send_xlm_prim_simple(asp, OSMO_XLM_PRIM_M_SCTP_RELEASE, PRIM_OP_INDICATION);
}

static int xua_cli_close_and_reconnect(struct osmo_stream_cli *cli)
{
	struct osmo_ss7_asp *asp = osmo_stream_cli_get_data(cli);
	LOGPASP(asp, DLSS7, LOGL_NOTICE, "Closing and reconnecting ASP\n");
	xua_cli_close(cli);
	if (asp->cfg.adm_state != OSMO_SS7_ASP_ADM_S_SHUTDOWN)
		osmo_stream_cli_reconnect(cli);
	return 0;
}

static int xua_cli_disconnect_cb(struct osmo_stream_cli *cli)
{
	struct osmo_ss7_asp *asp = osmo_stream_cli_get_data(cli);
	LOGPASP(asp, DLSS7, LOGL_NOTICE, "disconnect_cb() from lower layers, reconnecting\n");
	xua_cli_close_and_reconnect(cli);
	return 0;
}

/* read call-back for IPA/SCCPlite socket */
static int ipa_cli_read_cb(struct osmo_stream_cli *conn, int res, struct msgb *msg)
{
	struct osmo_ss7_asp *asp = osmo_stream_cli_get_data(conn);
	int rc;

	if (res <= 0) {
		if (res == -EAGAIN) {
			msgb_free(msg);
			return 0;
		}
		msgb_free(msg);
		xua_cli_close_and_reconnect(conn);
		return res;
	}

	msg->dst = asp;
	rate_ctr_inc2(asp->inst->ctrg, SS7_INST_CTR_PKT_RX_TOTAL);
	rate_ctr_inc2(asp->ctrg, SS7_ASP_CTR_PKT_RX_TOTAL);
	rc = ipa_rx_msg(asp, msg, asp->ipa.sls);
	msgb_free(msg);
	return rc;
}

/* read call-back for M3UA-over-TCP socket */
static int m3ua_tcp_cli_read_cb(struct osmo_stream_cli *conn, int res, struct msgb *msg)
{
	struct osmo_ss7_asp *asp = osmo_stream_cli_get_data(conn);
	int rc = 0;

	if (res < 0) {
		xua_cli_close_and_reconnect(conn);
		goto out;
	} else if (res == 0) {
		xua_cli_close_and_reconnect(conn);
		goto out;
	}

	msg->dst = asp;
	rate_ctr_inc2(asp->inst->ctrg, SS7_INST_CTR_PKT_RX_TOTAL);
	rate_ctr_inc2(asp->ctrg, SS7_ASP_CTR_PKT_RX_TOTAL);
	rc = m3ua_tcp_rx_msg(asp, msg);
out:
	msgb_free(msg);
	return rc;
}

static int xua_cli_read_cb(struct osmo_stream_cli *conn, int res, struct msgb *msg)
{
	struct osmo_ss7_asp *asp = osmo_stream_cli_get_data(conn);
	int flags;
	int rc = 0;

	flags = msgb_sctp_msg_flags(msg);

	LOGPASP(asp, DLSS7, LOGL_DEBUG, "%s(): sctp_recvmsg() returned %d (flags=0x%x)\n",
		__func__, msgb_length(msg), flags);

	if (flags & OSMO_STREAM_SCTP_MSG_FLAGS_NOTIFICATION) {
		union sctp_notification *notif = (union sctp_notification *) msgb_data(msg);
		log_sctp_notification(asp, "xUA CLNT", notif);
		asp_handle_sctp_notif_monitor_primary_address(asp, notif);

		switch (notif->sn_header.sn_type) {
		case SCTP_ASSOC_CHANGE:
			if (notif->sn_assoc_change.sac_state == SCTP_RESTART)
				xua_asp_send_xlm_prim_simple(asp, OSMO_XLM_PRIM_M_SCTP_RESTART,
							     PRIM_OP_INDICATION);
		default:
			break;
		}
		if (res == 0)
			xua_cli_close_and_reconnect(conn);
		goto out;
	}
	if (res < 0) {
		xua_cli_close_and_reconnect(conn);
		goto out;
	} else if (res == 0) {
		xua_cli_close_and_reconnect(conn);
		goto out;
	}

	msg->dst = asp;
	rate_ctr_inc2(asp->inst->ctrg, SS7_INST_CTR_PKT_RX_TOTAL);
	rate_ctr_inc2(asp->ctrg, SS7_ASP_CTR_PKT_RX_TOTAL);
	rc = xua_rx_msg(asp, msg);
out:
	msgb_free(msg);
	return rc;
}

int ss7_asp_xua_srv_conn_closed_cb(struct osmo_stream_srv *srv)
{
	struct osmo_ss7_asp *asp = osmo_stream_srv_get_data(srv);

	LOGP(DLSS7, LOGL_INFO, "%s: connection closed\n", asp ? asp->cfg.name : "?");

	if (!asp)
		return 0;

	/* notify ASP FSM and everyone else */
	osmo_fsm_inst_dispatch(asp->fi, XUA_ASP_E_SCTP_COMM_DOWN_IND, NULL);

	/* send M-SCTP_RELEASE.ind to Layer Manager */
	xua_asp_send_xlm_prim_simple(asp, OSMO_XLM_PRIM_M_SCTP_RELEASE, PRIM_OP_INDICATION);

	asp->server = NULL;
	if (asp->xua_server) {
		llist_del(&asp->siblings);
		asp->xua_server = NULL;
	}

	/* if we were dynamically allocated at accept_cb() time, let's
	 * self-destruct now.  A new connection will re-create the ASP. */
	if (asp->dyn_allocated) {
		/* avoid re-entrance via osmo_stream_srv_destroy() which
		 * called us */
		osmo_ss7_asp_destroy(asp);
	}

	return 0;
}

/*! \brief send a fully encoded msgb via a given ASP
 *  \param[in] asp Application Server Process through which to send
 *  \param[in] msg message buffer to transmit. Ownership transferred.
 *  \returns 0 on success; negative in case of error */
int osmo_ss7_asp_send(struct osmo_ss7_asp *asp, struct msgb *msg)
{
	OSMO_ASSERT(ss7_initialized);

	switch (asp->cfg.proto) {
	case OSMO_SS7_ASP_PROT_SUA:
		msgb_sctp_ppid(msg) = SUA_PPID;
		break;
	case OSMO_SS7_ASP_PROT_M3UA:
		msgb_sctp_ppid(msg) = M3UA_PPID;
		break;
	case OSMO_SS7_ASP_PROT_IPA:
		break;
	default:
		OSMO_ASSERT(0);
	}

	rate_ctr_inc2(asp->inst->ctrg, SS7_INST_CTR_PKT_TX_TOTAL);
	rate_ctr_inc2(asp->ctrg, SS7_ASP_CTR_PKT_TX_TOTAL);
	LOGPASP(asp, DLSS7, LOGL_DEBUG, "Tx %d bytes: %s\n", msg->len, msgb_hexdump(msg));

	if (asp->cfg.is_server) {
		if (!asp->server) {
			LOGPASP(asp, DLSS7, LOGL_ERROR, "Cannot transmit, no asp->server\n");
			/* FIXME: what to do here? delete the route? send DUNA? */
			msgb_free(msg);
			return -EIO;
		}
		osmo_stream_srv_send(asp->server, msg);
	} else {
		if (!asp->client) {
			LOGPASP(asp, DLSS7, LOGL_ERROR, "Cannot transmit, no asp->client\n");
			/* FIXME: what to do here? delete the route? send DUNA? */
			msgb_free(msg);
			return -EIO;
		}
		if (!osmo_stream_cli_is_connected(asp->client)) {
			LOGPASP(asp, DLSS7, LOGL_ERROR, "Cannot transmit, asp->client not connected\n");
			msgb_free(msg);
			return -EIO;
		}
		osmo_stream_cli_send(asp->client, msg);
	}

	return 0;
}

void osmo_ss7_asp_disconnect(struct osmo_ss7_asp *asp)
{
	if (asp->server)
		osmo_stream_srv_destroy(asp->server);
		/* the close_cb() will handle the remaining cleanup here */
	else if (asp->client)
		xua_cli_close_and_reconnect(asp->client);
}

static osmo_ss7_asp_rx_unknown_cb *g_osmo_ss7_asp_rx_unknown_cb;

/*! Register a call-back function for unknown SCTP PPID / IPA Stream ID */
void osmo_ss7_register_rx_unknown_cb(osmo_ss7_asp_rx_unknown_cb *cb)
{
	g_osmo_ss7_asp_rx_unknown_cb = cb;
}

int ss7_asp_rx_unknown(struct osmo_ss7_asp *asp, int ppid_mux, struct msgb *msg)
{
	rate_ctr_inc2(asp->inst->ctrg, SS7_INST_CTR_PKT_RX_UNKNOWN);
	rate_ctr_inc2(asp->ctrg, SS7_ASP_CTR_PKT_RX_UNKNOWN);

	if (g_osmo_ss7_asp_rx_unknown_cb)
		return (*g_osmo_ss7_asp_rx_unknown_cb)(asp, ppid_mux, msg);

	switch (asp->cfg.proto) {
	case OSMO_SS7_ASP_PROT_IPA:
		LOGPASP(asp, DLSS7, LOGL_NOTICE, "Rx IPA for unknown Stream ID 0x%02x: %s\n",
			ppid_mux, msgb_hexdump(msg));
		break;
	default:
		LOGPASP(asp, DLSS7, LOGL_NOTICE, "Rx SCTP chunk for unknown PPID %u: %s\n",
			ppid_mux, msgb_hexdump(msg));
		break;
	}
	return 0;
}

/*! Get the logging subsystem for a given ASP. Used by generic code. */
int osmo_ss7_asp_get_log_subsys(const struct osmo_ss7_asp *asp)
{
	switch (asp->cfg.proto) {
	case OSMO_SS7_ASP_PROT_M3UA:
		return DLM3UA;
	case OSMO_SS7_ASP_PROT_SUA:
		return DLSUA;
	default:
		return DLSS7;
	}
}

/*! \brief Get the name of a given ASP
 *  \param[in] asp The ASP for which the name is requested
 *  \returns The name of the ASP, or NULL if not set
 */
const char *osmo_ss7_asp_get_name(const struct osmo_ss7_asp *asp)
{
	return asp->cfg.name;
}

/*! \brief Get the proto of a given ASP
 *  \param[in] asp The ASP for which the proto is requested
 *  \returns The proto of the ASP
 */
enum osmo_ss7_asp_protocol osmo_ss7_asp_get_proto(const struct osmo_ss7_asp *asp)
{
	return asp->cfg.proto;
}

/*! \brief Get the transport proto of a given ASP
 *  \param[in] asp The ASP for which the transport proto is requested
 *  \returns The transport proto of the ASP (one of IPPROTO_*)
 */
int osmo_ss7_asp_get_trans_proto(const struct osmo_ss7_asp *asp)
{
	return asp->cfg.trans_proto;
}

/*! \brief Get the fd of a given ASP
 *  \param[in] asp The ASP for which the fd is requested
 *  \returns The fd of the ASP if acailable, negative otherwise
 */
int ss7_asp_get_fd(const struct osmo_ss7_asp *asp)
{
	if (asp->cfg.is_server) {
		if (asp->server)
			return osmo_stream_srv_get_fd(asp->server);
	} else {
		if (asp->client)
			return osmo_stream_cli_get_fd(asp->client);
	}
	return -1;
}

/* Apply sane configs for unconfigured options and restart the ASP.  */
void ss7_asp_restart_after_reconfigure(struct osmo_ss7_asp *asp)
{
	/* Make sure proper defaults values are applied if user didn't provide
	* specific default values */
	ss7_asp_set_default_peer_hosts(asp);

	osmo_ss7_asp_restart(asp);
}

/* Obtain all routing contexts (in network byte order) that exist within the given ASP */
static unsigned int _ss7_asp_get_all_rctx(const struct osmo_ss7_asp *asp, uint32_t *rctx, unsigned int rctx_size,
					  const struct osmo_ss7_as *excl_as, bool network_byte_order)
{
	unsigned int count = 0;
	struct osmo_ss7_as *as;

	llist_for_each_entry(as, &asp->inst->as_list, list) {
		if (as == excl_as)
			continue;
		if (!osmo_ss7_as_has_asp(as, asp))
			continue;
		if (as->cfg.routing_key.context == 0)
			continue;
		if (count >= rctx_size)
			break;
		if (network_byte_order)
			rctx[count] = htonl(as->cfg.routing_key.context);
		else
			rctx[count] = as->cfg.routing_key.context;
		count++;
	}
	return count;
}

/* Obtain all routing contexts (in network byte order) that exist within the given ASP */
unsigned int ss7_asp_get_all_rctx_be(const struct osmo_ss7_asp *asp, uint32_t *rctx, unsigned int rctx_size,
				     const struct osmo_ss7_as *excl_as)
{
	return _ss7_asp_get_all_rctx(asp, rctx, rctx_size, excl_as, true);
}

/* Obtain all routing contexts (in host byte order) that exist within the given ASP */
unsigned int ss7_asp_get_all_rctx(const struct osmo_ss7_asp *asp, uint32_t *rctx, unsigned int rctx_size,
				     const struct osmo_ss7_as *excl_as)
{
	return _ss7_asp_get_all_rctx(asp, rctx, rctx_size, excl_as, false);
}

/* determine the osmo_ss7_as_traffic_mode to be used by this ASP; will
 * iterate over all AS configured for this ASP.  If they're compatible,
 * a single traffic mode is returned as enum osmo_ss7_as_traffic_mode.
 * If they're incompatible, -EINVAL is returned.  If there is none
 * configured, -1 is returned */
int ss7_asp_determine_traf_mode(const struct osmo_ss7_asp *asp)
{
	const struct osmo_ss7_as *as;
	int tmode = -1;

	llist_for_each_entry(as, &asp->inst->as_list, list) {
		if (!osmo_ss7_as_has_asp(as, asp))
			continue;
		/* we only care about traffic modes explicitly set */
		if (!as->cfg.mode_set_by_vty)
			continue;
		if (tmode == -1) {
			/* this is the first AS; we use this traffic mode */
			tmode = as->cfg.mode;
		} else {
			if (tmode != as->cfg.mode)
				return -EINVAL;
		}
	}
	return tmode;
}
