daniel submitted this change.
Add TCAP based loadsharing/routing
TCAP based loadsharing will share the load based on the TCAP oTID and
dTID.
Because TCAP are session based, a TCAP session based tracking is implemented,
to allow following traffic to forwarded to the same ASP.
This TCAP session tracking is similar to IP connection tracking.
ASPs within an AS can use the new IPA TCAP ROUTING protocol to register
for specific TCAP ranges.
TCAP sessions initiated by a peer (traffic towards such loadsharing AS/ASP),
will use the oTID of the `TCAP Begin` to loadshare and select a ASP.
Further if the TCAP session was initiated by a `loadshared` ASP, the oTID
will be added to the session tracking.
Co-authored-by: Harald Welte <laforge@osmocom.org>
Co-authored-by: Alexander Couzens <lynxis@fe80.eu>
Co-authored-by: Pau Espin Pedrol <pespin@sysmocom.de>
Related: SYS#5423
Change-Id: Ibcb48aa0e515ad346f59ddd84b24c6e2c026144d
---
M configure.ac
M src/Makefile.am
M src/ipa.c
M src/ss7_as.c
M src/ss7_as.h
M src/ss7_as_vty.c
M src/ss7_asp.h
A src/tcap_as_loadshare.c
A src/tcap_as_loadshare.h
A src/tcap_trans_tracking.c
A src/tcap_trans_tracking.h
M src/xua_asp_fsm.c
M tests/Makefile.am
A tests/tcap/Makefile.am
A tests/tcap/tcap_transaction_tracking_test.c
A tests/tcap/tcap_transaction_tracking_test.ok
M tests/testsuite.at
M tests/vty/Makefile.am
M tests/vty/osmo_stp_test.vty
A tests/vty/osmo_stp_test_tcap.vty
20 files changed, 1,985 insertions(+), 4 deletions(-)
diff --git a/configure.ac b/configure.ac
index c26ad5d..c8f3c91 100644
--- a/configure.ac
+++ b/configure.ac
@@ -204,6 +204,7 @@
tests/m2ua/Makefile
tests/xua/Makefile
tests/ss7/Makefile
+ tests/tcap/Makefile
tests/vty/Makefile
examples/Makefile
stp/Makefile
diff --git a/src/Makefile.am b/src/Makefile.am
index f51819e..c57b3d0 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -25,6 +25,8 @@
ss7_user.h \
ss7_vty.h \
ss7_xua_srv.h \
+ tcap_as_loadshare.h \
+ tcap_trans_tracking.h \
xua_asp_fsm.h \
xua_as_fsm.h \
xua_internal.h \
@@ -105,6 +107,11 @@
$(NULL)
if BUILD_WITH_TCAP_LOADSHARING
+libosmo_sigtran_la_SOURCES += \
+ tcap_trans_tracking.c \
+ tcap_as_loadshare.c \
+ $(NULL)
+
AM_CFLAGS += \
$(LIBOSMOASN1TCAP_CFLAGS)
$(NULL)
diff --git a/src/ipa.c b/src/ipa.c
index 2083d09..1f4b808 100644
--- a/src/ipa.c
+++ b/src/ipa.c
@@ -47,6 +47,9 @@
#include "mtp3_hmdc.h"
#include "ss7_as.h"
+#ifdef WITH_TCAP_LOADSHARING
+#include "tcap_as_loadshare.h"
+#endif /* WITH_TCAP_LOADSHARING */
#include "ss7_asp.h"
#include "ss7_internal.h"
#include "xua_asp_fsm.h"
@@ -319,6 +322,10 @@
case IPAC_PROTO_SCCP:
/* Third, patch this into the SCCP message and create M3UA message in XUA structure */
data_hdr.si = MTP_SI_SCCP;
+#ifdef WITH_TCAP_LOADSHARING
+ /* update TCAP Transaction Tracking (Rx) */
+ tcap_as_rx_sccp_asp(as, asp, opc, dpc, msg);
+#endif /* WITH_TCAP_LOADSHARING */
if (as->cfg.pc_override.sccp_mode == OSMO_SS7_PATCH_BOTH) {
struct msgb *msg_patched = patch_sccp_with_pc(asp, msg, opc, dpc);
if (!msg_patched) {
@@ -368,6 +375,14 @@
case IPAC_PROTO_IPACCESS:
rc = ipa_rx_msg_ccm(asp, msg);
break;
+#ifdef WITH_TCAP_LOADSHARING
+ case IPAC_PROTO_OSMO:
+ if (osmo_ipa_msgb_cb_proto_ext(msg) == IPAC_PROTO_EXT_TCAP_ROUTING) {
+ rc = ipa_rx_msg_osmo_ext_tcap_routing(asp, msg);
+ break;
+ }
+ /* fall-through */
+#endif /* WITH_TCAP_LOADSHARING */
default:
rc = ipa_rx_msg_up(asp, msg, sls);
break;
diff --git a/src/ss7_as.c b/src/ss7_as.c
index 440385a..98a9057 100644
--- a/src/ss7_as.c
+++ b/src/ss7_as.c
@@ -26,6 +26,7 @@
#include <unistd.h>
#include <inttypes.h>
+#include <osmocom/core/hashtable.h>
#include <osmocom/core/linuxlist.h>
#include <osmocom/core/utils.h>
#include <osmocom/core/talloc.h>
@@ -36,6 +37,9 @@
#include <osmocom/sigtran/mtp_sap.h>
#include "ss7_as.h"
+#ifdef WITH_TCAP_LOADSHARING
+#include "tcap_as_loadshare.h"
+#endif /* WITH_TCAP_LOADSHARING */
#include "ss7_asp.h"
#include "ss7_route.h"
#include "ss7_route_table.h"
@@ -133,6 +137,16 @@
/* Pick 1st ASP upon 1st roundrobin assignment: */
as->cfg.last_asp_idx_assigned = ARRAY_SIZE(as->cfg.asps) - 1;
+#ifdef WITH_TCAP_LOADSHARING
+ /* loadshare-tcap based id sharing */
+ hash_init(as->tcap.tid_ranges);
+ hash_init(as->tcap.trans_track_own);
+ hash_init(as->tcap.trans_track_peer);
+
+ /* TODO: use Tdef */
+ as->cfg.loadshare.tcap.timeout_s = 30;
+#endif /* WITH_TCAP_LOADSHARING */
+
as->fi = xua_as_fsm_start(as, LOGL_DEBUG);
llist_add_tail(&as->list, &inst->as_list);
@@ -211,6 +225,10 @@
as->aesls_table[i].alt_asp = NULL;
}
+#ifdef WITH_TCAP_LOADSHARING
+ tcap_as_del_asp(as, asp);
+#endif /* WITH_TCAP_LOADSHARING */
+
for (i = 0; i < ARRAY_SIZE(as->cfg.asps); i++) {
if (as->cfg.asps[i] == asp) {
as->cfg.asps[i] = NULL;
@@ -253,6 +271,10 @@
OSMO_ASSERT(ss7_initialized);
LOGPAS(as, DLSS7, LOGL_INFO, "Destroying AS\n");
+#ifdef WITH_TCAP_LOADSHARING
+ tcap_disable(as);
+#endif /* WITH_TCAP_LOADSHARING */
+
if (as->fi)
osmo_fsm_inst_term(as->fi, OSMO_FSM_TERM_REQUEST, NULL);
@@ -550,7 +572,17 @@
asp = ss7_as_select_asp_override(as);
break;
case OSMO_SS7_AS_TMOD_LOADSHARE:
- asp = ss7_as_select_asp_loadshare(as, mtp);
+#ifdef WITH_TCAP_LOADSHARING
+ if (as->cfg.loadshare.tcap.enabled) {
+ int rc = tcap_as_select_asp_loadshare(&asp, as, xua);
+ if (rc == -EPROTONOSUPPORT) /* fallback to non-tcap loadsharing */
+ asp = ss7_as_select_asp_loadshare(as, mtp);
+ /* for all other cases, the asp has been either set to NULL or the corresponding asp */
+ } else
+#endif
+ {
+ asp = ss7_as_select_asp_loadshare(as, mtp);
+ }
break;
case OSMO_SS7_AS_TMOD_ROUNDROBIN:
asp = ss7_as_select_asp_roundrobin(as);
diff --git a/src/ss7_as.h b/src/ss7_as.h
index bb7b1b2..18210ec 100644
--- a/src/ss7_as.h
+++ b/src/ss7_as.h
@@ -3,6 +3,7 @@
#include <stdint.h>
#include <osmocom/core/linuxlist.h>
#include <osmocom/core/fsm.h>
+#include <osmocom/core/hashtable.h>
#include <osmocom/core/msgb.h>
#include <osmocom/core/tdef.h>
#include <osmocom/netif/stream.h>
@@ -19,6 +20,7 @@
struct osmo_ss7_instance;
struct osmo_ss7_asp;
struct osmo_mtp_transfer_param;
+struct xua_msg;
enum osmo_ss7_as_patch_sccp_mode {
OSMO_SS7_PATCH_NONE, /* no patching of SCCP */
@@ -92,6 +94,21 @@
/* ASP loadshare: */
struct osmo_ss7_as_esls_entry aesls_table[NUM_AS_EXT_SLS];
+#ifdef WITH_TCAP_LOADSHARING
+ struct {
+ /* optimisation: true if tid_ranges contains PCs (not only wildcards) */
+ bool contains_pc;
+ /* optimisation: true if tid_ranges contains SSNs (not only wildcards (0)) */
+ bool contains_ssn;
+ DECLARE_HASHTABLE(tid_ranges, 10);
+ /* gargabe collector timer */
+ struct osmo_timer_list gc_timer;
+ /* TODO: the hash tables size might not be optimal */
+ DECLARE_HASHTABLE(trans_track_own, 10);
+ DECLARE_HASHTABLE(trans_track_peer, 10);
+ } tcap;
+#endif /* WITH_TCAP_LOADSHARING */
+
struct {
char *name;
char *description;
@@ -128,6 +145,13 @@
* to skip for routing decisions (always takes 12 bits).
* range 0-2, defaults to 0, which means take least significant 12 bits. */
uint8_t opc_shift;
+#ifdef WITH_TCAP_LOADSHARING
+ /* Should we do load-sharing based on tcap ids? */
+ struct {
+ bool enabled;
+ unsigned int timeout_s;
+ } tcap;
+#endif /* WITH_TCAP_LOADSHARING */
} loadshare;
} cfg;
};
diff --git a/src/ss7_as_vty.c b/src/ss7_as_vty.c
index d88c9b4..7878825 100644
--- a/src/ss7_as_vty.c
+++ b/src/ss7_as_vty.c
@@ -36,6 +36,9 @@
#include <osmocom/sigtran/protocol/mtp.h>
#include "ss7_as.h"
+#ifdef WITH_TCAP_LOADSHARING
+#include "tcap_as_loadshare.h"
+#endif /* WITH_TCAP_LOADSHARING */
#include "ss7_asp.h"
#include "ss7_route.h"
#include "ss7_route_table.h"
@@ -217,6 +220,30 @@
return CMD_SUCCESS;
}
+#ifdef WITH_TCAP_LOADSHARING
+DEFUN_USRATTR(as_tcap_routing, as_tcap_routing_cmd,
+ OSMO_SCCP_LIB_ATTR_RSTRT_ASP,
+ "tcap-routing",
+ "Enable TCAP-based routing when in traffic-mode loadshare\n")
+{
+ struct osmo_ss7_as *as = vty->index;
+ tcap_enable(as);
+
+ return CMD_SUCCESS;
+}
+
+DEFUN_USRATTR(as_no_tcap_routing, as_no_tcap_routing_cmd,
+ OSMO_SCCP_LIB_ATTR_RSTRT_ASP,
+ "no tcap-routing",
+ NO_STR "Disable TCAP-based routing when in traffic-mode loadshare\n")
+{
+ struct osmo_ss7_as *as = vty->index;
+ tcap_disable(as);
+
+ return CMD_SUCCESS;
+}
+#endif /* WITH_TCAP_LOADSHARING */
+
DEFUN_ATTR(as_bindingtable_reset, as_bindingtable_reset_cmd,
"binding-table reset",
"AS Loadshare binding table operations\n"
@@ -474,6 +501,10 @@
if (as->cfg.loadshare.sls_shift != 0)
vty_out(vty, " sls-shift %u%s", as->cfg.loadshare.sls_shift, VTY_NEWLINE);
}
+#ifdef WITH_TCAP_LOADSHARING
+ if (as->cfg.loadshare.tcap.enabled)
+ vty_out(vty, "tcap-routing");
+#endif /* WITH_TCAP_LOADSHARING */
if (as->cfg.recovery_timeout_msec != 2000) {
vty_out(vty, " recovery-timeout %u%s",
@@ -651,6 +682,11 @@
"'point-code override dpc PC' configured in its routing-key. Fix your config!%s",
as->cfg.name, VTY_NEWLINE);
}
+#ifdef WITH_TCAP_LOADSHARING
+ if (as->cfg.loadshare.tcap.enabled && as->cfg.mode != OSMO_SS7_AS_TMOD_LOADSHARE)
+ vty_out(vty, "%% AS '%s' TCAP routing is enabled, but only works in traffic-mode loadshare!%s",
+ as->cfg.name, VTY_NEWLINE);
+#endif /* WITH_TCAP_LOADSHARING */
}
return 0;
}
@@ -670,6 +706,10 @@
install_lib_element(L_CS7_AS_NODE, &as_traf_mode_loadshare_cmd);
install_lib_element(L_CS7_AS_NODE, &as_no_traf_mode_cmd);
install_lib_element(L_CS7_AS_NODE, &as_sls_shift_cmd);
+#ifdef WITH_TCAP_LOADSHARING
+ install_lib_element(L_CS7_AS_NODE, &as_tcap_routing_cmd);
+ install_lib_element(L_CS7_AS_NODE, &as_no_tcap_routing_cmd);
+#endif /* WITH_TCAP_LOADSHARING */
install_lib_element(L_CS7_AS_NODE, &as_bindingtable_reset_cmd);
install_lib_element(L_CS7_AS_NODE, &as_recov_tout_cmd);
install_lib_element(L_CS7_AS_NODE, &as_qos_class_cmd);
diff --git a/src/ss7_asp.h b/src/ss7_asp.h
index 62378a3..5d8002c 100644
--- a/src/ss7_asp.h
+++ b/src/ss7_asp.h
@@ -93,6 +93,12 @@
bool sls_assigned;
} ipa;
+#ifdef WITH_TCAP_LOADSHARING
+ struct {
+ bool enabled;
+ } tcap;
+#endif /* WITH_TCAP_LOADSHARING */
+
struct {
char *name;
char *description;
diff --git a/src/tcap_as_loadshare.c b/src/tcap_as_loadshare.c
new file mode 100644
index 0000000..157685e
--- /dev/null
+++ b/src/tcap_as_loadshare.c
@@ -0,0 +1,934 @@
+/* TCAP ID based ASP Load-Sharing */
+
+/* (C) 2025 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 <errno.h>
+
+#include <osmocom/core/bit32gen.h>
+#include <osmocom/core/byteswap.h>
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/core/talloc.h>
+
+#include <osmocom/netif/ipa.h>
+
+#include <osmocom/sccp/sccp_types.h>
+#include <osmocom/sigtran/osmo_ss7.h>
+#include <osmocom/sigtran/protocol/m3ua.h>
+#include <osmocom/sigtran/protocol/sua.h>
+#include <osmocom/sigtran/protocol/mtp.h>
+#include <osmocom/sigtran/sccp_sap.h>
+#include <osmocom/tcap/OCTET_STRING.h>
+#include <osmocom/tcap/TCAP_TCMessage.h>
+
+#include "mtp3_hmrt.h"
+#include "ss7_as.h"
+#include "sccp_internal.h"
+#include "ss7_asp.h"
+#include "ss7_internal.h"
+#include "tcap_as_loadshare.h"
+#include "tcap_trans_tracking.h"
+#include "xua_internal.h"
+
+#define OTID_SET 1 << 0
+#define DTID_SET 1 << 1
+
+struct tcap_parsed {
+ TCAP_TCMessage_PR present;
+ uint32_t otid;
+ uint32_t dtid;
+};
+
+static inline uint32_t tcap_id_from_octet_str(const OCTET_STRING_t *src)
+{
+ OSMO_ASSERT(src->size == 4);
+
+ return osmo_load32be(src->buf);
+}
+
+/* returns negative on error, mask with any/both OTID_SET|DTID_SET on success */
+static int parse_tcap(struct osmo_ss7_as *as, const uint8_t *data, size_t len, struct tcap_parsed *ids)
+{
+ int rc = 0;
+ asn_dec_rval_t asn_rc;
+ struct TCAP_TCMessage tcap = { 0 };
+ struct TCAP_TCMessage *tcapmsg = &tcap;
+
+ OSMO_ASSERT(ids);
+
+ asn_rc = ber_decode(0, &asn_DEF_TCAP_TCMessage, (void **)&tcapmsg, data, len);
+ if (asn_rc.code != RC_OK) {
+ LOGPAS(as, DLSS7, LOGL_DEBUG, "Error decoding TCAP message rc: %d, message: %s\n",
+ asn_rc.code, osmo_hexdump(data, len));
+ rc = -EINVAL;
+ goto free_asn;
+ }
+
+ ids->present = tcapmsg->present;
+ switch (tcapmsg->present) {
+ case TCAP_TCMessage_PR_begin:
+ {
+ TCAP_Begin_t part = tcapmsg->choice.begin;
+ ids->otid = tcap_id_from_octet_str(&part.otid);
+ rc = OTID_SET;
+ break;
+ }
+ case TCAP_TCMessage_PR_continue:
+ {
+ TCAP_Continue_t part = tcapmsg->choice.Continue;
+ ids->otid = tcap_id_from_octet_str(&part.otid);
+ ids->dtid = tcap_id_from_octet_str(&part.dtid);
+ rc = OTID_SET | DTID_SET;
+ break;
+ }
+ case TCAP_TCMessage_PR_end:
+ {
+ TCAP_End_t part = tcapmsg->choice.end;
+ ids->dtid = tcap_id_from_octet_str(&part.dtid);
+ rc = DTID_SET;
+ break;
+ }
+ case TCAP_TCMessage_PR_abort:
+ {
+ TCAP_Abort_t part = tcapmsg->choice.abort;
+ ids->dtid = tcap_id_from_octet_str(&part.dtid);
+ rc = DTID_SET;
+ break;
+ }
+
+ /* No TID present */
+ case TCAP_TCMessage_PR_unidirectional:
+ rc = 0;
+ break;
+ default:
+ rc = -EINVAL;
+ break;
+ }
+
+ /* Only asn_fprint is available, but no asn_sprint:
+ * asn_fprint(stdout, &asn_DEF_TCAP_TCMessage, tcapmsg);
+ */
+
+free_asn:
+ ASN_STRUCT_FREE_CONTENTS_ONLY(asn_DEF_TCAP_TCMessage, tcapmsg);
+
+ return rc;
+}
+
+static inline uint32_t tcap_gen_hash(uint32_t pc, uint8_t ssn)
+{
+ ssn ^= ((pc >> 24) & 0xff);
+ return ((uint32_t)ssn << 24) | (pc & 0xffffff);
+}
+
+static inline uint64_t tcap_gen_hash_addr(const struct osmo_sccp_addr *addr)
+{
+ uint8_t ssn = 0;
+ uint32_t pc = 0xffffffff;
+
+ if (addr->presence & OSMO_SCCP_ADDR_T_PC)
+ pc = addr->pc;
+
+ if (addr->presence & OSMO_SCCP_ADDR_T_SSN)
+ ssn = addr->ssn;
+
+ return tcap_gen_hash(pc, ssn);
+}
+
+/* TODO: potential optimisation:
+ * Use a sorted list under the hash (tid_range{hash(pc, ssn, tcrng_entry)} -> (struct tcrng_entry {hlist, tid_start, tid_end})
+ */
+static struct osmo_ss7_asp *tcap_hlist_get(const struct osmo_ss7_as *as, uint32_t pc, uint8_t ssn, uint32_t tid)
+{
+ struct tcap_range *tcrng;
+ struct osmo_ss7_asp *asp = NULL;
+
+ hash_for_each_possible(as->tcap.tid_ranges, tcrng, list, tcap_gen_hash(pc, ssn)) {
+ if (tcrng->pc != pc || tcrng->ssn != ssn)
+ continue;
+
+ if (tcap_range_matches(tcrng, tid)) {
+ asp = tcrng->asp;
+ break;
+ }
+ }
+
+ return asp;
+}
+
+struct osmo_ss7_asp *tcap_as_asp_find_by_tcap_id(
+ struct osmo_ss7_as *as,
+ struct osmo_sccp_addr *calling_addr,
+ struct osmo_sccp_addr *called_addr,
+ uint32_t otid)
+{
+ struct osmo_ss7_asp *asp = NULL;
+
+ uint8_t ssn = 0;
+ uint32_t pc = TCAP_PC_WILDCARD;
+
+ if (called_addr->presence & OSMO_SCCP_ADDR_T_PC)
+ pc = called_addr->pc;
+
+ if (called_addr->presence & OSMO_SCCP_ADDR_T_SSN)
+ ssn = called_addr->ssn;
+
+ /* check full range of PC/SSN */
+ if (as->tcap.contains_pc && as->tcap.contains_ssn) {
+ asp = tcap_hlist_get(as, pc, ssn, otid);
+ if (asp)
+ return asp;
+ }
+
+ /* check with PC wildcard */
+ if (as->tcap.contains_ssn) {
+ asp = tcap_hlist_get(as, TCAP_PC_WILDCARD, ssn, otid);
+ if (asp)
+ return asp;
+ }
+
+ /* check with SSN wildcard */
+ if (as->tcap.contains_pc) {
+ asp = tcap_hlist_get(as, pc, TCAP_SSN_WILDCARD, otid);
+ if (asp)
+ return asp;
+ }
+
+ /* check with PC/SSN wildcard */
+ return tcap_hlist_get(as, TCAP_PC_WILDCARD, TCAP_SSN_WILDCARD, otid);
+}
+
+static struct tcap_range *tcap_overlap_tid(struct osmo_ss7_as *as, uint32_t pc, uint8_t ssn,
+ uint32_t tid_start, uint32_t tid_end)
+{
+ struct tcap_range *tcrng;
+
+ hash_for_each_possible(as->tcap.tid_ranges, tcrng, list, tcap_gen_hash(pc, ssn)) {
+ if (tcrng->pc != pc || tcrng->ssn != ssn)
+ continue;
+
+ if (tcap_range_overlaps(tcrng, tid_start, tid_end))
+ return tcrng;
+ }
+
+ return NULL;
+}
+
+static struct osmo_ss7_asp *find_asp_no_tcap_range(struct osmo_ss7_as *as)
+{
+ struct osmo_ss7_asp *asp = NULL;
+
+ for (int i = 0; i < ARRAY_SIZE(as->cfg.asps); i++) {
+ asp = as->cfg.asps[i];
+ if (!asp)
+ continue;
+ if (!asp->tcap.enabled)
+ return asp;
+ }
+
+ return asp;
+}
+
+static bool ssn_contains_tcap(uint8_t ssn)
+{
+ switch (ssn) {
+ case OSMO_SCCP_SSN_MAP:
+ case OSMO_SCCP_SSN_HLR:
+ case OSMO_SCCP_SSN_VLR:
+ case OSMO_SCCP_SSN_MSC:
+ case OSMO_SCCP_SSN_EIR:
+ case OSMO_SCCP_SSN_AUC:
+ case OSMO_SCCP_SSN_TC_TEST:
+ case OSMO_SCCP_SSN_GMLC_MAP:
+ case OSMO_SCCP_SSN_CAP:
+ case OSMO_SCCP_SSN_gsmSCF_MAP:
+ case OSMO_SCCP_SSN_SIWF_MAP:
+ case OSMO_SCCP_SSN_SGSN_MAP:
+ case OSMO_SCCP_SSN_GGSN_MAP:
+ /* SSNs known to use TCAP */
+ return true;
+ default:
+ return false;
+ }
+}
+
+/** Traffic from the TCAP ASP -> AS -> osmo-stp, only used to update transaction tracking
+ *
+ * @param as
+ * @param asp asp sent the \ref sccp_msg message towards osmo-stp
+ * @param opc M3UA opc
+ * @param dpc M3UA DPC
+ * @param sccp_msg pointer to a msg.
+ * @return 0 on successful handling, < 0 on error cases (missing IE, decoding errors)
+ */
+int tcap_as_rx_sccp_asp(struct osmo_ss7_as *as, struct osmo_ss7_asp *asp, uint32_t opc, uint32_t dpc, struct msgb *sccp_msg)
+{
+ struct tcap_parsed parsed = {};
+ struct xua_msg_part *ie_data;
+ struct osmo_sccp_addr calling_addr, called_addr;
+ int rc;
+ struct xua_msg *sua = osmo_sccp_to_xua(sccp_msg);
+ if (!sua) {
+ LOGPAS(as, DLSS7, LOGL_ERROR, "Unable to parse SCCP message\n");
+ return -1;
+ }
+
+ /* TCAP uses only connectionless SCCP messages */
+ if (sua->hdr.msg_class != SUA_MSGC_CL && sua->hdr.msg_class != SUA_CL_CLDT)
+ return -2;
+
+ rc = sua_addr_parse(&calling_addr, sua, SUA_IEI_SRC_ADDR);
+ if (rc < 0) {
+ LOGPAS(as, DLSS7, LOGL_ERROR, "Unable to parse SCCP Destination Address\n");
+ return -3;
+ }
+
+ /* retrieve + decode destination address */
+ rc = sua_addr_parse(&called_addr, sua, SUA_IEI_DEST_ADDR);
+ if (rc < 0) {
+ LOGPAS(as, DLSS7, LOGL_ERROR, "Unable to parse SCCP Destination Address\n");
+ return -4;
+ }
+
+ if (!ssn_contains_tcap(called_addr.ssn)) {
+ /* No TCAP */
+ return 0;
+ }
+
+ /* TCAP transaction tracking requires point codes */
+ if (!(calling_addr.presence & OSMO_SCCP_ADDR_T_PC)) {
+ /* use M3UA OPC instead */
+ calling_addr.pc = opc;
+ calling_addr.presence |= OSMO_SCCP_ADDR_T_PC;
+ }
+ if (!(called_addr.presence & OSMO_SCCP_ADDR_T_PC)) {
+ /* use M3UA DPC instead */
+ called_addr.pc = dpc;
+ called_addr.presence |= OSMO_SCCP_ADDR_T_PC;
+ }
+
+ /* retrieve the SCCP payload (actual encoded TCAP data) */
+ ie_data = xua_msg_find_tag(sua, SUA_IEI_DATA);
+ if (!ie_data)
+ return -6;
+
+ rc = parse_tcap(as, ie_data->dat, ie_data->len, &parsed);
+ if (rc <= 0) {
+ LOGPAS(as, DLSS7, LOGL_ERROR, "TCAP: failed get otid/dtid.\n");
+ return -7;
+ }
+
+ LOGPAS(as, DLSS7, LOGL_INFO, "TCAP: Looking up transaction for type 0x%02x, otid=%u dtid=%u\n", parsed.present, parsed.otid, parsed.dtid);
+ /* TCAP messages towards the IPAC nodes */
+ switch (parsed.present) {
+ case TCAP_TCMessage_PR_begin:
+ if (!(rc & OTID_SET)) {
+ /* FIXME: failure case */
+ return -8;
+ }
+
+ tcap_trans_track_entry_create(as, asp, &calling_addr, &parsed.otid, &called_addr, NULL);
+ break;
+ case TCAP_TCMessage_PR_continue:
+ if (!((rc & OTID_SET) && (rc & DTID_SET))) {
+ /* FIXME: failure case */
+ return -8;
+ }
+
+ /* only hit/update the transaction tracking */
+ tcap_trans_track_continue(as, &calling_addr, &parsed.otid, &called_addr, &parsed.dtid);
+ break;
+ case TCAP_TCMessage_PR_abort:
+ case TCAP_TCMessage_PR_end:
+ if (!(rc & DTID_SET)) {
+ /* FIXME: failure case */
+ return -8;
+ }
+
+ /* only hit/update the transaction tracking */
+ tcap_trans_track_end(as, &calling_addr, NULL, &called_addr, &parsed.dtid);
+ break;
+ case TCAP_TCMessage_PR_unidirectional:
+ case TCAP_TCMessage_PR_NOTHING:
+ default:
+ /* TODO: what to do with those messages? */
+ return -9;
+ }
+
+ return 0;
+}
+
+/** Send UDTS to indicate that the originating UDT could not be delivered to its destination
+ * @param as
+ * @param orig_mtp MTP routing information of the originating message (message that could not be delivered)
+ * @param orig_sua Originating message that could not be delivered
+ * @param cause_code the return cause of the UDTS
+ * @return 0 on success, negative on error
+*/
+static int send_back_udts(struct osmo_ss7_as *as,
+ const struct osmo_mtp_transfer_param *orig_mtp,
+ const struct xua_msg *orig_sua,
+ uint8_t cause_code)
+{
+ struct msgb *msg;
+ struct xua_msg *sua;
+ int rc = -EINVAL;
+ uint32_t spare_proto = 0;
+ struct osmo_mtp_transfer_param new_mtp;
+ uint32_t rctx;
+
+ OSMO_ASSERT(orig_sua->hdr.msg_class == SUA_MSGC_CL && orig_sua->hdr.msg_type == SUA_CL_CLDT);
+
+ if (!xua_msg_get_u32p(orig_sua, SUA_IEI_PROTO_CLASS, &spare_proto))
+ return -EINVAL;
+
+ /* Check if Return on Error is set */
+ if ((spare_proto & 0xf0) != 0x80)
+ return 0;
+
+ struct xua_msg_part *rctx_ie = xua_msg_find_tag(orig_sua, SUA_IEI_ROUTE_CTX);
+ if (rctx_ie)
+ rctx = xua_msg_part_get_u32(rctx_ie);
+ else
+ rctx = 0; /* Routing Context should be there as per proto... */
+
+ sua = sua_gen_cldr(orig_sua, rctx, cause_code);
+ if (!sua)
+ return -ENOMEM;
+
+ LOGPAS(as, DLSS7, LOGL_INFO, "TCAP: Tx UDTS: %s\n", xua_msg_dump(sua, &xua_dialect_sua));
+
+ msg = osmo_sua_to_sccp(sua);
+ if (!msg) {
+ rc = -ENOMEM;
+ goto free_sua;
+ }
+
+ new_mtp = *orig_mtp;
+ new_mtp.opc = orig_mtp->dpc;
+ new_mtp.dpc = orig_mtp->opc;
+ mtp3_hmrt_mtp_xfer_request_l4_to_l3(as->inst, &new_mtp, msgb_data(msg), msgb_length(msg));
+ msgb_free(msg);
+free_sua:
+ xua_msg_free(sua);
+ return rc;
+}
+
+/*! Traffic STP -> AS -> ASP (Tx path) Loadshare towards the TCAP routing AS
+ *
+ * \param[out] rasp the selected ASP if any, can be NULL
+ * \param[in] as
+ * \param[in] opc the OPC from MTP
+ * \param[in] dpc the DPC from MTP
+ * \param[in] mtp MTP routing information
+ * \param[in] sccp_msg the SCCP message. Callee takes ownership.
+ * \return 0: on succcess (msg handled by the callee),
+ * -EPROTONOSUPPORT: let caller (regular loadsharing) handle those.
+ */
+static int asp_loadshare_tcap_sccp(struct osmo_ss7_asp **rasp, struct osmo_ss7_as *as,
+ const struct osmo_mtp_transfer_param *mtp, struct msgb *sccp_msg)
+{
+ struct tcap_parsed parsed = {};
+ struct xua_msg *sua;
+ struct xua_msg_part *ie_data;
+ struct osmo_sccp_addr calling_addr, called_addr;
+ struct osmo_ss7_asp *asp = NULL;
+ int rc;
+
+ OSMO_ASSERT(rasp);
+
+ /* decode SCCP and convert to a SUA/xUA representation */
+ sua = osmo_sccp_to_xua(sccp_msg);
+ if (!sua) {
+ LOGPAS(as, DLSS7, LOGL_ERROR, "Unable to parse SCCP message\n");
+ rc = -EPROTONOSUPPORT;
+ goto out_free_msgb;
+ }
+
+ /* TCAP uses only connectionless SCCP messages */
+ if (sua->hdr.msg_class != SUA_MSGC_CL && sua->hdr.msg_class != SUA_CL_CLDT) {
+ /* ignoring packets */
+ rc = -EPROTONOSUPPORT;
+ goto out_free_sua;
+ }
+
+ rc = sua_addr_parse(&calling_addr, sua, SUA_IEI_SRC_ADDR);
+ if (rc < 0) {
+ LOGPAS(as, DLSS7, LOGL_ERROR, "Unable to parse SCCP Source Address\n");
+ goto out_free_sua;
+ }
+
+ rc = sua_addr_parse(&called_addr, sua, SUA_IEI_DEST_ADDR);
+ if (rc < 0) {
+ LOGPAS(as, DLSS7, LOGL_ERROR, "Unable to parse SCCP Destination Address\n");
+ goto out_free_sua;
+ }
+
+ if (!(called_addr.presence & OSMO_SCCP_ADDR_T_SSN) || !ssn_contains_tcap(called_addr.ssn)) {
+ /* No tcap, return NULL */
+ rc = -EPROTONOSUPPORT;
+ goto out_free_sua;
+ }
+
+ /* TCAP transaction tracking requires point codes */
+ if (!(calling_addr.presence & OSMO_SCCP_ADDR_T_PC)) {
+ /* use M3UA OPC instead */
+ calling_addr.pc = mtp->opc;
+ calling_addr.presence |= OSMO_SCCP_ADDR_T_PC;
+ }
+ if (!(called_addr.presence & OSMO_SCCP_ADDR_T_PC)) {
+ /* use M3UA DPC instead */
+ called_addr.pc = mtp->dpc;
+ called_addr.presence |= OSMO_SCCP_ADDR_T_PC;
+ }
+
+ /* retrieve the SCCP payload (TCAP data) */
+ ie_data = xua_msg_find_tag(sua, SUA_IEI_DATA);
+ if (!ie_data) {
+ rc = -ENODATA;
+ goto out_free_sua;
+ }
+
+ rc = parse_tcap(as, ie_data->dat, ie_data->len, &parsed);
+ LOGPAS(as, DLSS7, LOGL_DEBUG, "TCAP: decoded rc=%d otid=%u dtid=%u\n", rc, parsed.otid, parsed.dtid);
+
+ if (rc <= 0) {
+ LOGPAS(as, DLSS7, LOGL_ERROR, "TCAP: failed get otid/dtid.\n");
+ rc = -EINVAL;
+ goto out_free_sua;
+ }
+
+ /* TCAP messages towards the IPA nodes */
+ switch (parsed.present) {
+ case TCAP_TCMessage_PR_begin:
+ if (!(rc & OTID_SET)) {
+ rc = -EINVAL;
+ goto out_free_sua;
+ }
+
+ /* lookup a new ASP */
+ asp = tcap_as_asp_find_by_tcap_id(as, &calling_addr, &called_addr, parsed.otid);
+
+ /* if no ASP found for this TCAP, try to find a non-tcap-range ASP as fallback*/
+ if (!asp) {
+ asp = find_asp_no_tcap_range(as);
+ if (!asp) {
+ /* couldn't find a suitable canditate for OTID */
+ LOGPAS(as, DLSS7, LOGL_DEBUG, "TCAP: couldn't find a suitable canditate for otid %u\n", parsed.otid);
+ rc = -ENOKEY;
+ goto out_free_sua;
+ }
+ }
+
+ tcap_trans_track_begin(as, asp, &called_addr, NULL, &calling_addr, &parsed.otid);
+ rc = 0;
+ break;
+ case TCAP_TCMessage_PR_continue:
+ if (!((rc & OTID_SET) && (rc & DTID_SET))) {
+ rc = -EINVAL;
+ goto out_free_sua;
+ }
+
+ asp = tcap_trans_track_continue(as, &called_addr, &parsed.dtid, &calling_addr, &parsed.otid);
+ rc = asp ? 0 : -ENOKEY;
+ break;
+ case TCAP_TCMessage_PR_abort:
+ case TCAP_TCMessage_PR_end:
+ if (!(rc & DTID_SET)) {
+ /* FIXME: failure case */
+ rc = -EINVAL;
+ goto out_free_sua;
+ }
+
+ asp = tcap_trans_track_end(as, &called_addr, &parsed.dtid, &calling_addr, NULL);
+ rc = asp ? 0 : -ENOKEY;
+ break;
+ case TCAP_TCMessage_PR_unidirectional:
+ case TCAP_TCMessage_PR_NOTHING:
+ default:
+ /* Ignore, let regular loadsharing handle those */
+ rc = -EPROTONOSUPPORT;
+ break;
+ }
+out_free_sua:
+ /* RFC3868 4.7.3: "If an ASP is not available, the SG may generate (X)UDTS "routing failure",
+ * if the return option is used."
+ * See also ITU Q.714 4.2 */
+ if (rc < 0 && rc != -EPROTONOSUPPORT) {
+ send_back_udts(as, mtp, sua, SCCP_RETURN_CAUSE_SUBSYSTEM_FAILURE);
+ rc = 0;
+ }
+ xua_msg_free(sua);
+out_free_msgb:
+ msgb_free(sccp_msg);
+ *rasp = asp;
+ return rc;
+}
+
+/*! Entrypoint for M3UA messages towards the TCAP nodes
+ *
+ * @param[out] asp Result pointer of the selected asp. Set to NULL if return code is != 0
+ * @param[in] as
+ * @param[in] xua
+ * @return 0: on succcess (msg handled by the callee),
+ * -EPROTONOSUPPORT: let caller (regular loadsharing) handle those.
+ */
+/* return 0 and asp is set */
+int tcap_as_select_asp_loadshare(struct osmo_ss7_asp **asp, struct osmo_ss7_as *as, const struct xua_msg *xua)
+{
+ uint8_t service_ind = xua->mtp.sio & 0xF;
+ struct xua_msg_part *m3ua_data_ie;
+ struct msgb *sccp_msg;
+ uint8_t *cur;
+
+ OSMO_ASSERT(asp);
+ *asp = NULL;
+
+ if (service_ind != MTP_SI_SCCP)
+ return -EPROTONOSUPPORT;
+
+ /* we only care about actual M3UA data transfer messages */
+ if (xua->hdr.msg_class != M3UA_MSGC_XFER || xua->hdr.msg_type != M3UA_XFER_DATA)
+ return -EPROTONOSUPPORT;
+
+ /* we only care about SCCP as higher layer protocol.
+ * extract the SCCP payload and convert to a msgb */
+ m3ua_data_ie = xua_msg_find_tag(xua, M3UA_IEI_PROT_DATA);
+ if (!m3ua_data_ie) {
+ LOGPAS(as, DLSS7, LOGL_ERROR, "Couldn't find M3UA protocol data\n");
+ return -EPROTONOSUPPORT;
+ }
+
+ sccp_msg = msgb_alloc(m3ua_data_ie->len, "loadshare_tcap");
+ if (!sccp_msg) {
+ LOGPAS(as, DLSS7, LOGL_ERROR, "Unable to allocate SCCP message buffer\n");
+ return -ENOMEM;
+ }
+ cur = msgb_put(sccp_msg, m3ua_data_ie->len);
+ memcpy(cur, m3ua_data_ie->dat, m3ua_data_ie->len);
+ sccp_msg->l2h = cur + sizeof(struct m3ua_data_hdr);
+
+ return asp_loadshare_tcap_sccp(asp, as, &xua->mtp, sccp_msg);
+}
+
+enum ipa_tcap_routing_msg_types {
+ MT_TID_ADD_RANGE = 0x01,
+ MT_TID_ACK = 0x02,
+ MT_TID_NACK = 0x03,
+};
+
+enum ipa_tcap_routing_nack_error {
+ NACK_ERR_SYS_FAILURE = 0x01, /* system failure */
+ NACK_ERR_EALREADY = 0x72, /* already in use */
+};
+
+struct ipa_tcap_routing_hdr {
+ uint8_t mt;
+ uint32_t seq;
+ uint8_t data[0];
+} __attribute__((packed));
+
+struct ipa_tcap_routing_add_range {
+ uint32_t tid_start;
+ uint32_t tid_end;
+ uint32_t pc;
+ uint8_t ssn;
+} __attribute__((packed));
+
+struct ipa_tcap_routing_nack {
+ uint8_t err;
+} __attribute__((packed));
+
+static struct msgb *ipa_tcap_routing_alloc(uint32_t seq_nr, uint8_t mt)
+{
+ struct ipa_tcap_routing_hdr hdr = {
+ .mt = mt,
+ .seq = osmo_htonl(seq_nr),
+ };
+
+ struct msgb *msg = osmo_ipa_msg_alloc(16);
+ if (!msg)
+ return NULL;
+
+ void *dst = msgb_put(msg, sizeof(hdr));
+ memcpy(dst, &hdr, sizeof(hdr));
+
+ return msg;
+}
+
+static int ipa_tx_tcap_routing_ack(struct osmo_ss7_asp *asp, uint32_t seq_nr)
+{
+ struct msgb *msg = ipa_tcap_routing_alloc(seq_nr, MT_TID_ACK);
+ if (!msg)
+ return -ENOMEM;
+
+ osmo_ipa_msg_push_headers(msg, IPAC_PROTO_OSMO, IPAC_PROTO_EXT_TCAP_ROUTING);
+ return osmo_ss7_asp_send(asp, msg);
+}
+
+static int ipa_tx_tcap_routing_nack(struct osmo_ss7_asp *asp, uint32_t seq_nr, uint8_t err_code)
+{
+ struct msgb *msg = ipa_tcap_routing_alloc(seq_nr, MT_TID_NACK);
+ if (!msg)
+ return -ENOMEM;
+
+ msgb_put_u8(msg, err_code);
+
+ osmo_ipa_msg_push_headers(msg, IPAC_PROTO_OSMO, IPAC_PROTO_EXT_TCAP_ROUTING);
+ return osmo_ss7_asp_send(asp, msg);
+}
+
+/** Entrypoint for IPA TCAP Routing messages, parses and handles those
+ *
+ * @param asp
+ * @param msg the message buffer. Callee takes ownership!
+ * @return 0 on success
+ */
+int ipa_rx_msg_osmo_ext_tcap_routing(struct osmo_ss7_asp *asp, struct msgb *msg)
+{
+ int rc = 0;
+ struct osmo_ss7_as *as = ipa_find_as_for_asp(asp);
+ struct ipa_tcap_routing_hdr *hdr;
+ enum ipa_tcap_routing_msg_types routing_msg;
+
+ if (!as) {
+ LOGPASP(asp, DLSS7, LOGL_ERROR, "Rx message for IPA ASP without AS?!\n");
+ rc = -ENOENT;
+ goto out;
+ }
+
+ /* pull the IPA and OSMO_EXT header */
+ hdr = (struct ipa_tcap_routing_hdr *) msgb_data(msg);
+ if (msgb_length(msg) < sizeof(struct ipa_tcap_routing_hdr)) {
+ LOGPASP(asp, DLSS7, LOGL_ERROR, "TCAP routing message too short\n");
+ rc = -EINVAL;
+ goto out;
+ }
+
+ routing_msg = (enum ipa_tcap_routing_msg_types) hdr->mt;
+ switch (routing_msg) {
+ case MT_TID_ADD_RANGE: {
+ struct tcap_range *tcrng;
+ struct ipa_tcap_routing_add_range tcar = {};
+
+ if (!as->cfg.loadshare.tcap.enabled || as->cfg.mode != OSMO_SS7_AS_TMOD_LOADSHARE)
+ LOGPASP(asp, DLSS7, LOGL_NOTICE, "Wrong traffic mode %s on AS %s will not use TCAP Ranges\n", osmo_ss7_as_traffic_mode_name(as->cfg.mode), as->cfg.name);
+
+ if (msgb_length(msg) < sizeof(*hdr) + sizeof(struct ipa_tcap_routing_add_range)) {
+ LOGPASP(asp, DLSS7, LOGL_ERROR, "TCAP routing message is too small\n");
+ rc = -EINVAL;
+ goto out;
+ }
+
+ msgb_pull(msg, sizeof(*hdr));
+
+ tcar.tid_start = msgb_pull_u32(msg);
+ tcar.tid_end = msgb_pull_u32(msg);
+ tcar.pc = msgb_pull_u32(msg);
+ tcar.ssn = msgb_pull_u8(msg);
+
+ LOGPASP(asp, DLSS7, LOGL_INFO, "Rx: TCAP Add Range command: seq: %u pc: %u ssn: %u [%u-%u]\n", osmo_ntohl(hdr->seq), tcar.pc, tcar.ssn, tcar.tid_start, tcar.tid_end);
+
+ tcrng = tcap_overlap_tid(as, tcar.pc, tcar.ssn, tcar.tid_start, tcar.tid_end);
+ if (tcrng) {
+ LOGPASP(asp, DLSS7, LOGL_ERROR, "New TCAP Range overlaps with existing range to ASP %s [%u-%u]. Rejecting Add Range Command seq: %u pc: %u ssn: %u [%u-%u]\n",
+ tcrng->asp->cfg.name, tcrng->tid_start, tcrng->tid_end, osmo_ntohl(hdr->seq), tcar.pc, tcar.ssn, tcar.tid_start, tcar.tid_end);
+ rc = ipa_tx_tcap_routing_nack(asp, osmo_ntohl(hdr->seq), NACK_ERR_EALREADY);
+ goto out;
+ }
+
+ tcrng = tcap_range_alloc(as, asp, tcar.tid_start, tcar.tid_end, tcar.pc, tcar.ssn);
+ if (!tcrng) {
+ LOGPASP(asp, DLSS7, LOGL_ERROR, "TCAP Add Range: failed to allocate memory\n");
+ rc = ipa_tx_tcap_routing_nack(asp, osmo_ntohl(hdr->seq), NACK_ERR_SYS_FAILURE);
+ goto out;
+ }
+
+ if (tcar.pc != TCAP_PC_WILDCARD)
+ as->tcap.contains_pc = true;
+
+ if (tcar.ssn != TCAP_SSN_WILDCARD)
+ as->tcap.contains_ssn = true;
+
+ asp->tcap.enabled = true;
+ rc = ipa_tx_tcap_routing_ack(asp, osmo_ntohl(hdr->seq));
+ break;
+ }
+ case MT_TID_ACK: /* shouldn't received from other end */
+ case MT_TID_NACK: /* shouldn't received from other end */
+ default:
+ rc = -EINVAL;
+ break;
+ }
+
+out:
+ return rc;
+}
+
+/* update the short cuts contains_pc & contains_ssn */
+static void tcap_range_as_update_pc_ssn(struct osmo_ss7_as *as)
+{
+ int i;
+ struct tcap_range *tcrng;
+ struct hlist_node *tmp;
+
+ bool check_pc = as->tcap.contains_pc;
+ bool found_pc = false;
+ bool check_ssn = as->tcap.contains_ssn;
+ bool found_ssn = false;
+
+ hash_for_each_safe(as->tcap.tid_ranges, i, tmp, tcrng, list) {
+ if (!check_pc && !check_ssn)
+ break;
+
+ if (check_pc && tcrng->pc != TCAP_PC_WILDCARD) {
+ check_pc = false;
+ found_pc = true;
+ }
+
+ if (check_ssn && tcrng->ssn != TCAP_SSN_WILDCARD) {
+ check_ssn = false;
+ found_ssn = true;
+ }
+ }
+
+ if (as->tcap.contains_pc)
+ as->tcap.contains_pc = found_pc;
+
+ if (as->tcap.contains_ssn)
+ as->tcap.contains_ssn = found_ssn;
+}
+
+/** Create and alloc a new TCAP range entry
+ *
+ * @param[in] as
+ * @param[in] asp
+ * @param[in] tid_start
+ * @param[in] tid_end
+ * @param[in] pc
+ * @param[in] ssn
+ * @return the TCAP range entry or NULL
+ */
+struct tcap_range *tcap_range_alloc(struct osmo_ss7_as *as,
+ struct osmo_ss7_asp *asp,
+ uint32_t tid_start, uint32_t tid_end,
+ uint32_t pc,
+ uint8_t ssn)
+{
+ struct tcap_range *tcrng = talloc_zero(asp, struct tcap_range);
+
+ if (!tcrng)
+ return NULL;
+
+ tcrng->asp = asp;
+ tcrng->pc = pc;
+ tcrng->ssn = ssn;
+ tcrng->tid_start = tid_start;
+ tcrng->tid_end = tid_end;
+
+ hash_add(as->tcap.tid_ranges, &tcrng->list, tcap_gen_hash(pc, ssn));
+
+ return tcrng;
+}
+
+/** Remove and free a single TCAP range entry
+ *
+ * @param[in] tcrng
+ */
+void tcap_range_free(struct tcap_range *tcrng)
+{
+ hash_del(&tcrng->list);
+ talloc_free(tcrng);
+}
+
+/** Checks if a tid matches to a specific range
+ *
+ * @param tcrng
+ * @param tid
+ * @return true if tid is included in the range
+ */
+bool tcap_range_matches(const struct tcap_range *tcrng, uint32_t tid)
+{
+ return (tid >= tcrng->tid_start) && (tid <= tcrng->tid_end);
+}
+
+/** Checks if a tid rnage overlaps with another range
+ *
+ * @param a
+ * @param tid_start
+ * @param tid_end
+ * @return
+ */
+bool tcap_range_overlaps(const struct tcap_range *a, uint32_t tid_start, uint32_t tid_end)
+{
+ struct tcap_range b = {
+ .tid_start = tid_start,
+ .tid_end = tid_end
+ };
+
+ return tcap_range_matches(&b, a->tid_start) || tcap_range_matches(&b, a->tid_end)
+ || tcap_range_matches(a, tid_start) || tcap_range_matches(a, tid_end);
+}
+
+
+static void _tcap_range_asp_down(struct osmo_ss7_as *as, struct osmo_ss7_asp *asp)
+{
+ int i;
+ struct tcap_range *tcrng;
+ struct hlist_node *tmp;
+
+ tcap_trans_track_entries_free_by_asp(as, asp);
+ hash_for_each_safe(as->tcap.tid_ranges, i, tmp, tcrng, list) {
+ if (tcrng->asp == asp)
+ tcap_range_free(tcrng);
+ }
+}
+
+void tcap_as_del_asp(struct osmo_ss7_as *as, struct osmo_ss7_asp *asp)
+{
+ if (!asp->tcap.enabled)
+ return;
+
+ _tcap_range_asp_down(as, asp);
+ if (as->tcap.contains_pc || as->tcap.contains_ssn)
+ tcap_range_as_update_pc_ssn(as);
+}
+
+void tcap_enable(struct osmo_ss7_as *as)
+{
+ if (as->cfg.loadshare.tcap.enabled)
+ return;
+
+ as->cfg.loadshare.tcap.enabled = true;
+ tcap_trans_track_garbage_collect_start(as);
+}
+
+void tcap_disable(struct osmo_ss7_as *as)
+{
+ if (!as->cfg.loadshare.tcap.enabled)
+ return;
+
+ as->cfg.loadshare.tcap.enabled = false;
+ as->tcap.contains_pc = false;
+ as->tcap.contains_ssn = false;
+ tcap_trans_track_garbage_collect_stop(as);
+ tcap_trans_track_entries_free_all(as);
+}
diff --git a/src/tcap_as_loadshare.h b/src/tcap_as_loadshare.h
new file mode 100644
index 0000000..a361061
--- /dev/null
+++ b/src/tcap_as_loadshare.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <stdint.h>
+
+#include <osmocom/core/msgb.h>
+#include <osmocom/sigtran/osmo_ss7.h>
+
+struct xua_msg;
+
+#define TCAP_PC_WILDCARD 0xffffffff
+#define TCAP_SSN_WILDCARD 0
+
+struct tcap_range {
+ struct hlist_node list;
+ struct osmo_ss7_asp *asp;
+ uint32_t tid_start;
+ uint32_t tid_end;
+ uint32_t pc;
+ uint8_t ssn;
+};
+
+/* IPA entry point */
+int ipa_rx_msg_osmo_ext_tcap_routing(struct osmo_ss7_asp *asp, struct msgb *msg);
+
+struct tcap_range *tcap_range_alloc(struct osmo_ss7_as *as, struct osmo_ss7_asp *asp,
+ uint32_t tid_start, uint32_t tid_end, uint32_t pc, uint8_t ssn);
+void tcap_range_free(struct tcap_range *tcrng);
+
+bool tcap_range_matches(const struct tcap_range *tcrng, uint32_t tid);
+bool tcap_range_overlaps(const struct tcap_range *a, uint32_t tid_min, uint32_t tid_max);
+
+/* Traffic ASP -> AS -> STP (Rx path) From TCAP Routing AS, only used for connection tracking */
+int tcap_as_rx_sccp_asp(struct osmo_ss7_as *as, struct osmo_ss7_asp *asp, uint32_t opc, uint32_t dpc, struct msgb *sccp_msg);
+
+/* Traffic STP -> AS -> ASP (Tx path) Loadshare towards the TCAP routing AS */
+int tcap_as_select_asp_loadshare(struct osmo_ss7_asp **asp, struct osmo_ss7_as *as, const struct xua_msg *xua);
+
+/* When the ASP got removed */
+void tcap_as_del_asp(struct osmo_ss7_as *as, struct osmo_ss7_asp *asp);
+
+void tcap_enable(struct osmo_ss7_as *as);
+void tcap_disable(struct osmo_ss7_as *as);
diff --git a/src/tcap_trans_tracking.c b/src/tcap_trans_tracking.c
new file mode 100644
index 0000000..2123b34
--- /dev/null
+++ b/src/tcap_trans_tracking.c
@@ -0,0 +1,405 @@
+/* TCAP transaction cache */
+
+/* (C) 2025 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved
+ * Author: Alexander Couzens <lynxis@fe80.eu>
+ *
+ * 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 <stdint.h>
+
+#include <osmocom/core/msgb.h>
+#include <osmocom/sigtran/osmo_ss7.h>
+#include <osmocom/sigtran/sccp_sap.h>
+
+#include "ss7_asp.h"
+#include "ss7_as.h"
+#include "tcap_trans_tracking.h"
+
+
+static inline void entry_update_tstamp(struct tcap_trans_track_entry *entry)
+{
+ struct timespec now;
+ int rc;
+
+ rc = osmo_clock_gettime(CLOCK_MONOTONIC, &now);
+ OSMO_ASSERT(rc >= 0);
+
+ entry->tstamp = now.tv_sec;
+}
+
+static inline uint64_t gen_hash(uint32_t tid, uint8_t ssn, uint32_t pc)
+{
+ return ((uint64_t)tid << 32) | ssn | (pc & (0xffffff));
+}
+
+static inline uint64_t gen_hash_addr(uint32_t tid, const struct osmo_sccp_addr *addr)
+{
+ uint8_t ssn = 0;
+ uint32_t pc = 0;
+
+ if (addr->presence & OSMO_SCCP_ADDR_T_PC)
+ pc = addr->pc;
+
+ if (addr->presence & OSMO_SCCP_ADDR_T_SSN)
+ ssn = addr->ssn;
+
+ return gen_hash(tid, ssn, pc);
+}
+
+static void trans_sccp_addr_cpy(struct osmo_sccp_addr *dst, const struct osmo_sccp_addr *src)
+{
+ /* FIXME: reduce the sccp_addr in the txact tracking? */
+ memset(dst, 0, sizeof(*dst));
+ dst->presence = src->presence & (OSMO_SCCP_ADDR_T_SSN | OSMO_SCCP_ADDR_T_PC);
+
+ if (src->presence & OSMO_SCCP_ADDR_T_SSN)
+ dst->ssn = src->ssn;
+
+ if (src->presence & OSMO_SCCP_ADDR_T_PC)
+ dst->pc = src->pc;
+}
+
+struct tcap_trans_track_entry *tcap_trans_track_entry_create(
+ struct osmo_ss7_as *as,
+ struct osmo_ss7_asp *asp,
+ const struct osmo_sccp_addr *own_addr,
+ const uint32_t *own_tid,
+ const struct osmo_sccp_addr *peer_addr,
+ const uint32_t *peer_tid)
+{
+ char own_pc[MAX_PC_STR_LEN], peer_pc[MAX_PC_STR_LEN];
+ struct tcap_trans_track_entry *entry;
+
+ OSMO_ASSERT(own_addr);
+ OSMO_ASSERT(peer_addr);
+ OSMO_ASSERT(own_tid || peer_tid);
+ entry = talloc_zero(as, struct tcap_trans_track_entry);
+ if (!entry)
+ return NULL;
+
+ entry->asp = asp;
+
+ entry->own_addr = talloc_zero(entry, struct osmo_sccp_addr);
+ if (!entry->own_addr)
+ goto err;
+
+ trans_sccp_addr_cpy(entry->own_addr, own_addr);
+ if (own_tid) {
+ entry->own_tid.tid_valid = true;
+ entry->own_tid.tid = *own_tid;
+ hash_add(as->tcap.trans_track_own, &entry->own_tid.list, gen_hash_addr(*own_tid, own_addr));
+ }
+
+ entry->peer_addr = talloc_zero(entry, struct osmo_sccp_addr);
+ if (!entry->peer_addr)
+ goto err_own;
+
+ trans_sccp_addr_cpy(entry->peer_addr, peer_addr);
+ if (peer_tid) {
+ entry->peer_tid.tid_valid = true;
+ entry->peer_tid.tid = *peer_tid;
+ hash_add(as->tcap.trans_track_peer, &entry->peer_tid.list, gen_hash_addr(*peer_tid, peer_addr));
+ }
+
+ entry_update_tstamp(entry);
+ /* TODO: optimisation: add a llist to asp to allow cleaning it up easier */
+
+
+ LOGPASP(entry->asp, DLSS7, LOGL_DEBUG, "Creating tcap cache, entry (own) pc/ssn/tid %s/%u/%u -> %s/%u/%u\n",
+ osmo_ss7_pointcode_print_buf(own_pc, sizeof(own_pc), as->inst, entry->own_addr->pc),
+ entry->own_addr->ssn, entry->own_tid.tid,
+ osmo_ss7_pointcode_print_buf(peer_pc, sizeof(peer_pc), as->inst, entry->peer_addr->pc),
+ entry->peer_addr->ssn, entry->peer_tid.tid);
+
+ return entry;
+
+err_own:
+ if (entry->own_tid.tid_valid)
+ hash_del(&entry->own_tid.list);
+err:
+ talloc_free(entry);
+ return NULL;
+}
+
+void tcap_trans_track_entry_free(struct tcap_trans_track_entry *entry)
+{
+ if (!entry)
+ return;
+
+ if (entry->own_tid.tid_valid)
+ hash_del(&entry->own_tid.list);
+
+ if (entry->peer_tid.tid_valid)
+ hash_del(&entry->peer_tid.list);
+
+ talloc_free(entry);
+}
+
+struct tcap_trans_track_entry *tcap_trans_track_entry_find(
+ struct osmo_ss7_as *as,
+ const struct osmo_sccp_addr *own_addr,
+ const uint32_t *own_tid,
+ const struct osmo_sccp_addr *peer_addr,
+ const uint32_t *peer_tid)
+{
+ struct tcap_trans_track_entry *entry = NULL;
+ OSMO_ASSERT(own_tid || peer_tid);
+
+ /* TODO: possible optimisation: deref own_tid / peer_tid once here?
+ * or does the compiler figure this out on its own?
+ */
+
+ if (own_tid && !peer_tid) {
+ hash_for_each_possible(as->tcap.trans_track_own, entry, own_tid.list, gen_hash_addr(*own_tid, own_addr)) {
+ if (entry->own_tid.tid != *own_tid)
+ continue;
+
+ if (osmo_sccp_addr_cmp(entry->own_addr, own_addr, OSMO_SCCP_ADDR_T_SSN | OSMO_SCCP_ADDR_T_PC) ||
+ osmo_sccp_addr_cmp(entry->peer_addr, peer_addr, OSMO_SCCP_ADDR_T_SSN | OSMO_SCCP_ADDR_T_PC))
+ continue;
+
+ return entry;
+ }
+
+ return NULL;
+ }
+
+ if (!own_tid && peer_tid) {
+ hash_for_each_possible(as->tcap.trans_track_peer, entry, peer_tid.list, gen_hash_addr(*peer_tid, peer_addr)) {
+ if (entry->peer_tid.tid != *peer_tid)
+ continue;
+
+ if (osmo_sccp_addr_cmp(entry->own_addr, own_addr, OSMO_SCCP_ADDR_T_SSN | OSMO_SCCP_ADDR_T_PC) ||
+ osmo_sccp_addr_cmp(entry->peer_addr, peer_addr, OSMO_SCCP_ADDR_T_SSN | OSMO_SCCP_ADDR_T_PC))
+ continue;
+
+ return entry;
+ }
+
+ return NULL;
+ }
+
+ /* if (own_tid && peer_tid) */
+ hash_for_each_possible(as->tcap.trans_track_own, entry, own_tid.list, gen_hash_addr(*own_tid, own_addr)) {
+ if (entry->own_tid.tid != *own_tid)
+ continue;
+
+ if (entry->peer_tid.tid_valid && (entry->peer_tid.tid != *peer_tid))
+ continue;
+
+ if (osmo_sccp_addr_cmp(entry->own_addr, own_addr, OSMO_SCCP_ADDR_T_SSN | OSMO_SCCP_ADDR_T_PC) ||
+ osmo_sccp_addr_cmp(entry->peer_addr, peer_addr, OSMO_SCCP_ADDR_T_SSN | OSMO_SCCP_ADDR_T_PC))
+ continue;
+
+ /* tid is either not set in the entry or equals */
+ return entry;
+ }
+
+ /* only check for remaining entries which don't have an own_tid */
+ hash_for_each_possible(as->tcap.trans_track_peer, entry, peer_tid.list, gen_hash_addr(*peer_tid, peer_addr)) {
+ if (entry->peer_tid.tid != *peer_tid)
+ continue;
+
+ /* can't be a match, otherwise already found by own_tid hash_for_each */
+ if (entry->own_tid.tid_valid)
+ continue;
+
+ if (osmo_sccp_addr_cmp(entry->own_addr, own_addr, OSMO_SCCP_ADDR_T_SSN | OSMO_SCCP_ADDR_T_PC) ||
+ osmo_sccp_addr_cmp(entry->peer_addr, peer_addr, OSMO_SCCP_ADDR_T_SSN | OSMO_SCCP_ADDR_T_PC))
+ continue;
+
+ return entry;
+ }
+
+ return NULL;
+}
+
+struct tcap_trans_track_entry *tcap_trans_track_begin(
+ struct osmo_ss7_as *as,
+ struct osmo_ss7_asp *asp,
+ const struct osmo_sccp_addr *own_addr,
+ const uint32_t *own_tid,
+ const struct osmo_sccp_addr *peer_addr,
+ const uint32_t *peer_tid)
+{
+ struct tcap_trans_track_entry *entry = tcap_trans_track_entry_find(as,
+ own_addr, own_tid,
+ peer_addr, peer_tid);
+ if (entry) {
+ entry_update_tstamp(entry);
+ return entry;
+ }
+
+ return tcap_trans_track_entry_create(as, asp, own_addr, own_tid, peer_addr, peer_tid);
+}
+
+struct osmo_ss7_asp *tcap_trans_track_continue(
+ struct osmo_ss7_as *as,
+ const struct osmo_sccp_addr *own_addr,
+ const uint32_t *own_tid,
+ const struct osmo_sccp_addr *peer_addr,
+ const uint32_t *peer_tid)
+{
+ struct tcap_trans_track_entry *entry = tcap_trans_track_entry_find(as,
+ own_addr, own_tid,
+ peer_addr, peer_tid);
+ if (!entry)
+ return NULL;
+
+ /* ensure half complete entries are updated. A TCAP Begin only contains
+ * the oTID, the following Continue will contain dTID.
+ */
+ if (!entry->own_tid.tid_valid && own_tid) {
+ entry->own_tid.tid_valid = true;
+ entry->own_tid.tid = *own_tid;
+ hash_add(as->tcap.trans_track_own, &entry->own_tid.list, gen_hash_addr(*own_tid, own_addr));
+ }
+
+ if (!entry->peer_tid.tid_valid && peer_tid) {
+ entry->peer_tid.tid_valid = true;
+ entry->peer_tid.tid = *peer_tid;
+ hash_add(as->tcap.trans_track_peer, &entry->peer_tid.list, gen_hash_addr(*peer_tid, peer_addr));
+ }
+
+ entry_update_tstamp(entry);
+ return entry->asp;
+}
+
+/* find an entry, if entry exists, free it and return the associated asp */
+struct osmo_ss7_asp *tcap_trans_track_end(
+ struct osmo_ss7_as *as,
+ const struct osmo_sccp_addr *own_addr,
+ const uint32_t *own_tid,
+ const struct osmo_sccp_addr *peer_addr,
+ const uint32_t *peer_tid)
+{
+ struct osmo_ss7_asp *asp;
+ struct tcap_trans_track_entry *entry = tcap_trans_track_entry_find(as, own_addr, own_tid, peer_addr, peer_tid);
+ if (!entry)
+ return NULL;
+
+ asp = entry->asp;
+ tcap_trans_track_entry_free(entry);
+
+ return asp;
+}
+
+
+int tcap_trans_track_garbage_collect(struct osmo_ss7_as *as)
+{
+ int i, count = 0;
+ struct tcap_trans_track_entry *entry;
+ struct hlist_node *tmp;
+ struct timespec now;
+ time_t expiry;
+
+ if (!as->cfg.loadshare.tcap.timeout_s)
+ return 0;
+
+ osmo_clock_gettime(CLOCK_MONOTONIC, &now);
+ expiry = now.tv_sec - as->cfg.loadshare.tcap.timeout_s;
+
+ hash_for_each_safe(as->tcap.trans_track_own, i, tmp, entry, own_tid.list) {
+ if (entry->tstamp < expiry) {
+ count++;
+ LOGPASP(entry->asp, DLSS7, LOGL_DEBUG, "Remove Cache entry for tcap own tid %u (expired)\n", entry->own_tid.tid);
+ tcap_trans_track_entry_free(entry);
+ }
+ }
+
+ hash_for_each_safe(as->tcap.trans_track_peer, i, tmp, entry, peer_tid.list) {
+ if (entry->tstamp < expiry) {
+ count++;
+ LOGPASP(entry->asp, DLSS7, LOGL_DEBUG, "Remove Cache entry for tcap peer tid %u (expired)\n", entry->peer_tid.tid);
+ tcap_trans_track_entry_free(entry);
+ }
+ }
+
+ return count;
+}
+
+static void tcap_trans_track_garbage_collect_cb(void *data)
+{
+ struct osmo_ss7_as *as = data;
+ int counts = tcap_trans_track_garbage_collect(as);
+
+ if (counts)
+ LOGPAS(as, DLSS7, LOGL_DEBUG, "Removed %d cache entry (expired)", counts);
+
+ osmo_timer_schedule(&as->tcap.gc_timer, as->cfg.loadshare.tcap.timeout_s, 0);
+}
+
+
+void tcap_trans_track_garbage_collect_start(struct osmo_ss7_as *as)
+{
+ osmo_timer_setup(&as->tcap.gc_timer, tcap_trans_track_garbage_collect_cb, as);
+ tcap_trans_track_garbage_collect_cb(as);
+}
+
+void tcap_trans_track_garbage_collect_stop(struct osmo_ss7_as *as)
+{
+ osmo_timer_del(&as->tcap.gc_timer);
+}
+
+int tcap_trans_track_entries_free_by_asp(struct osmo_ss7_as *as, struct osmo_ss7_asp *asp)
+{
+ int i, count = 0;
+ struct tcap_trans_track_entry *entry;
+ struct hlist_node *tmp;
+
+ hash_for_each_safe(as->tcap.trans_track_own, i, tmp, entry, own_tid.list) {
+ if (entry->asp == asp) {
+ count++;
+ LOGPASP(entry->asp, DLSS7, LOGL_DEBUG, "Remove Cache entry for tcap own tid %u (asp removed)", entry->own_tid.tid);
+ tcap_trans_track_entry_free(entry);
+ }
+ }
+
+ hash_for_each_safe(as->tcap.trans_track_peer, i, tmp, entry, peer_tid.list) {
+ if (entry->asp == asp) {
+ count++;
+ LOGPASP(entry->asp, DLSS7, LOGL_DEBUG, "Remove Cache entry for tcap own tid %u (asp removed)", entry->peer_tid.tid);
+ tcap_trans_track_entry_free(entry);
+ }
+ }
+
+ return count;
+}
+
+int tcap_trans_track_entries_free_all(struct osmo_ss7_as *as)
+{
+ int i, count = 0;
+ struct tcap_trans_track_entry *entry;
+ struct hlist_node *tmp;
+
+ hash_for_each_safe(as->tcap.trans_track_own, i, tmp, entry, own_tid.list) {
+ count++;
+ LOGPASP(entry->asp, DLSS7, LOGL_DEBUG, "Remove Cache entry for tcap own tid %u (as removed)", entry->own_tid.tid);
+ tcap_trans_track_entry_free(entry);
+ }
+
+ hash_for_each_safe(as->tcap.trans_track_peer, i, tmp, entry, peer_tid.list) {
+ count++;
+ LOGPASP(entry->asp, DLSS7, LOGL_DEBUG, "Remove Cache entry for tcap own tid %u (as removed)", entry->peer_tid.tid);
+ tcap_trans_track_entry_free(entry);
+ }
+
+ return count;
+}
+
diff --git a/src/tcap_trans_tracking.h b/src/tcap_trans_tracking.h
new file mode 100644
index 0000000..c0c1661
--- /dev/null
+++ b/src/tcap_trans_tracking.h
@@ -0,0 +1,86 @@
+#pragma once
+
+/* TCAP transaction tracking */
+
+#include <stdint.h>
+
+#include <osmocom/core/hashtable.h>
+#include <osmocom/core/msgb.h>
+
+#include <osmocom/sigtran/sccp_sap.h>
+
+struct osmo_ss7_as;
+struct osmo_ss7_asp;
+
+struct tcap_trans_track_tid_entry {
+ struct hlist_node list;
+ bool tid_valid;
+ uint32_t tid;
+};
+
+struct tcap_trans_track_entry {
+ struct tcap_trans_track_tid_entry peer_tid; /* of the peer. If peer initiate transaction, this is otid */
+ struct tcap_trans_track_tid_entry own_tid; /* assigned by this asp */
+
+ struct osmo_sccp_addr *own_addr;
+ struct osmo_sccp_addr *peer_addr;
+
+ time_t tstamp; /* last time this cache was used */
+
+ struct osmo_ss7_asp *asp;
+};
+
+/* Entry centric API
+ * Managing entries without management (e.g. update entries when used)
+ */
+struct tcap_trans_track_entry *tcap_trans_track_entry_create(
+ struct osmo_ss7_as *as,
+ struct osmo_ss7_asp *asp,
+ const struct osmo_sccp_addr *own_addr,
+ const uint32_t *own_tid,
+ const struct osmo_sccp_addr *peer_addr,
+ const uint32_t *peer_tid);
+
+struct tcap_trans_track_entry *tcap_trans_track_entry_find(
+ struct osmo_ss7_as *as,
+ const struct osmo_sccp_addr *own_addr,
+ const uint32_t *own_tid,
+ const struct osmo_sccp_addr *peer_addr,
+ const uint32_t *peer_tid);
+
+void tcap_trans_track_entry_free(struct tcap_trans_track_entry *entry);
+
+int tcap_trans_track_entries_free_all(struct osmo_ss7_as *as);
+int tcap_trans_track_entries_free_by_asp(struct osmo_ss7_as *as, struct osmo_ss7_asp *asp);
+
+/* Transaction centric API
+ * It will update timestamp and used out of a TCAP transaction context
+ */
+struct tcap_trans_track_entry *tcap_trans_track_begin(
+ struct osmo_ss7_as *as,
+ struct osmo_ss7_asp *asp,
+ const struct osmo_sccp_addr *own_addr,
+ const uint32_t *own_tid,
+ const struct osmo_sccp_addr *peer_addr,
+ const uint32_t *peer_tid);
+
+struct osmo_ss7_asp *tcap_trans_track_continue(
+ struct osmo_ss7_as *as,
+ const struct osmo_sccp_addr *own_addr,
+ const uint32_t *own_tid,
+ const struct osmo_sccp_addr *peer_addr,
+ const uint32_t *peer_tid);
+
+struct osmo_ss7_asp *tcap_trans_track_end(
+ struct osmo_ss7_as *as,
+ const struct osmo_sccp_addr *own_addr,
+ const uint32_t *own_tid,
+ const struct osmo_sccp_addr *peer_addr,
+ const uint32_t *peer_tid);
+
+/* Garbage collection */
+int tcap_trans_track_garbage_collect(struct osmo_ss7_as *as);
+
+void tcap_trans_track_garbage_collect_start(struct osmo_ss7_as *as);
+void tcap_trans_track_garbage_collect_stop(struct osmo_ss7_as *as);
+
diff --git a/src/xua_asp_fsm.c b/src/xua_asp_fsm.c
index 7a6979d..b0f26fb 100644
--- a/src/xua_asp_fsm.c
+++ b/src/xua_asp_fsm.c
@@ -36,6 +36,10 @@
#include "xua_as_fsm.h"
#include "xua_internal.h"
+#ifdef WITH_TCAP_LOADSHARING
+#include "tcap_as_loadshare.h"
+#endif /* WITH_TCAP_LOADSHARING */
+
#define S(x) (1 << (x))
/* The general idea is:
@@ -489,6 +493,11 @@
llist_for_each_entry_safe(as, as2, &inst->as_list, list) {
if (!osmo_ss7_as_has_asp(as, asp))
continue;
+
+#ifdef WITH_TCAP_LOADSHARING
+ tcap_as_del_asp(as, asp);
+#endif
+
if (as->rkm_dyn_allocated) {
/* RFC 4666 4.4.2: "An ASP SHOULD deregister from all Application Servers
* of which it is a member before attempting to move to the ASP-Down state [...]
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 6a150e1..7927bbd 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -1,5 +1,9 @@
SUBDIRS = xua m2ua ss7 vty
+if BUILD_WITH_TCAP_LOADSHARING
+SUBDIRS += tcap
+endif
+
# The `:;' works around a Bash 3.2 bug when the output is not writeable.
$(srcdir)/package.m4: $(top_srcdir)/configure.ac
:;{ \
diff --git a/tests/tcap/Makefile.am b/tests/tcap/Makefile.am
new file mode 100644
index 0000000..f53d74b
--- /dev/null
+++ b/tests/tcap/Makefile.am
@@ -0,0 +1,25 @@
+AM_CPPFLAGS = $(all_includes) -I$(top_srcdir)/include -I$(top_srcdir)/src
+AM_CFLAGS = -Wall \
+ $(LIBOSMOCORE_CFLAGS) $(LIBOSMOVTY_CFLAGS) $(LIBOSMONETIF_CFLAGS) $(LIBOSMOASN1TCAP_CFLAGS) $(LIBSCTP_CFLAGS)
+
+AM_LDFLAGS = -static -no-install
+
+LDADD = $(top_builddir)/src/libosmo-sigtran.la \
+ $(LIBOSMOCORE_LIBS) $(LIBOSMOVTY_LIBS) $(LIBOSMONETIF_LIBS) $(LIBOSMOASN1TCAP_LIBS) $(LIBSCTP_LIBS)
+
+check_PROGRAMS = \
+ tcap_transaction_tracking_test \
+ $(NULL)
+
+EXTRA_DIST = \
+ tcap_transaction_tracking_test.ok \
+ $(NULL)
+
+
+tcap_transaction_tracking_test_SOURCES = \
+ tcap_transaction_tracking_test.c \
+ $(NULL)
+
+tcap_transaction_tracking_test_LDADD = \
+ $(top_builddir)/src/tcap_trans_tracking.o \
+ $(LDADD)
diff --git a/tests/tcap/tcap_transaction_tracking_test.c b/tests/tcap/tcap_transaction_tracking_test.c
new file mode 100644
index 0000000..7ad52a9
--- /dev/null
+++ b/tests/tcap/tcap_transaction_tracking_test.c
@@ -0,0 +1,285 @@
+/* TCAP parsing tests */
+#include <complex.h>
+#include <stdio.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+#include <osmocom/core/msgb.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/core/talloc.h>
+
+#include <osmocom/sigtran/osmo_ss7.h>
+#include <osmocom/sigtran/sccp_sap.h>
+
+#include "ss7_asp.h"
+#include "ss7_as.h"
+#include "tcap_trans_tracking.h"
+
+typedef void (*tcap_trans_track_test_func_t)(void);
+tcap_trans_track_test_func_t tcap_trans_track_tests[];
+
+static struct osmo_sccp_addr gvlr = {
+ .presence = OSMO_SCCP_ADDR_T_GT | OSMO_SCCP_ADDR_T_SSN,
+ .ri = OSMO_SCCP_RI_GT,
+ .gt = {
+ .gti = OSMO_SCCP_GTI_TT_NPL_ENC_NAI,
+ .tt = 0,
+ .npi = OSMO_SCCP_NPI_E164_ISDN,
+ .nai = OSMO_SCCP_NAI_INTL,
+ .digits = "919969679389",
+ },
+ .ssn = OSMO_SCCP_SSN_VLR,
+};
+
+static struct osmo_sccp_addr ghlr = {
+ .presence = OSMO_SCCP_ADDR_T_GT | OSMO_SCCP_ADDR_T_SSN,
+ .ri = OSMO_SCCP_RI_GT,
+ .gt = {
+ .gti = OSMO_SCCP_GTI_TT_NPL_ENC_NAI,
+ .tt = 0,
+ .npi = OSMO_SCCP_NPI_E164_ISDN,
+ .nai = OSMO_SCCP_NAI_INTL,
+ .digits = "919911111111",
+ },
+ .ssn = OSMO_SCCP_SSN_VLR,
+};
+
+static void init_as(struct osmo_ss7_as *as)
+{
+ as->cfg.loadshare.tcap.enabled = true;
+ as->cfg.loadshare.tcap.timeout_s = 10;
+
+ hash_init(as->tcap.tid_ranges);
+ hash_init(as->tcap.trans_track_own);
+ hash_init(as->tcap.trans_track_peer);
+}
+
+static void init_asp(struct osmo_ss7_asp *asp)
+{
+ asp->tcap.enabled = true;
+}
+
+static size_t count_unique_hash_entries(struct osmo_ss7_as *as)
+{
+ size_t counter = 0;
+ struct tcap_trans_track_entry *entry;
+ struct hlist_node *tmp;
+ int i;
+
+ /* count entries with only own_tid and own_tid && peer_tid */
+ hash_for_each_safe(as->tcap.trans_track_own, i, tmp, entry, own_tid.list) {
+ counter++;
+ }
+
+ /* only count those who doesn't have a valid own tid */
+ hash_for_each_safe(as->tcap.trans_track_peer, i, tmp, entry, peer_tid.list) {
+ if (!entry->own_tid.tid_valid)
+ counter++;
+ }
+
+ return counter;
+}
+
+void tcap_trans_track_test_create_find_free(void)
+{
+ void *ctx = talloc_new(NULL);
+ struct osmo_ss7_as *as = talloc_zero(ctx, struct osmo_ss7_as);
+ init_as(as);
+
+ struct osmo_ss7_asp *asp = talloc_zero(ctx, struct osmo_ss7_asp);
+ init_asp(asp);
+
+ size_t before_test = talloc_total_size(ctx), after_test;
+ uint32_t hlr_tid = 23;
+ uint32_t vlr_tid = 42;
+
+ struct tcap_trans_track_entry *entry1, *entry2, *entry3, *search;
+
+ printf("Create/Find/Free test\n");
+
+ /* create an entry */
+ entry1 = tcap_trans_track_entry_create(as, asp,
+ &ghlr, NULL,
+ &gvlr, &vlr_tid);
+ OSMO_ASSERT(entry1);
+
+ /* should find it regardless of the missing own tid in the entry */
+ search = tcap_trans_track_entry_find(as,
+ &ghlr, &hlr_tid,
+ &gvlr, &vlr_tid);
+ OSMO_ASSERT(search == entry1);
+
+
+ /* should not find it (entry still without own tid) */
+search = tcap_trans_track_entry_find(as,
+ &ghlr, &hlr_tid,
+ &gvlr, NULL);
+ OSMO_ASSERT(!search);
+
+ search = tcap_trans_track_entry_find(as,
+ &ghlr, NULL,
+ &gvlr, &vlr_tid);
+ OSMO_ASSERT(search == entry1);
+ tcap_trans_track_entry_free(search);
+
+ OSMO_ASSERT(hash_empty(as->tcap.trans_track_own));
+ OSMO_ASSERT(hash_empty(as->tcap.trans_track_peer));
+
+
+ /* create an entries */
+ entry1 = tcap_trans_track_entry_create(as, asp,
+ &ghlr, &hlr_tid,
+ &gvlr, NULL);
+ OSMO_ASSERT(entry1);
+
+ hlr_tid = 24;
+ entry2 = tcap_trans_track_entry_create(as, asp,
+ &ghlr, &hlr_tid,
+ &gvlr, NULL);
+ OSMO_ASSERT(entry2);
+
+ hlr_tid = 25;
+ entry3 = tcap_trans_track_entry_create(as, asp,
+ &ghlr, &hlr_tid,
+ &gvlr, NULL);
+ OSMO_ASSERT(entry3);
+
+ search = tcap_trans_track_entry_find(as,
+ &ghlr, &hlr_tid,
+ &gvlr, NULL);
+ OSMO_ASSERT(search == entry3);
+ tcap_trans_track_entries_free_by_asp(as, asp);
+
+ OSMO_ASSERT(hash_empty(as->tcap.trans_track_own));
+ OSMO_ASSERT(hash_empty(as->tcap.trans_track_peer));
+
+ after_test = talloc_total_size(ctx);
+ fprintf(stderr, "Consuming %lu bytes after test. Failing if not %lu.\n",
+ after_test, before_test);
+ OSMO_ASSERT(after_test == before_test);
+ talloc_free(ctx);
+}
+
+void tcap_trans_track_test_transaction(void)
+{
+ void *ctx = talloc_new(NULL);
+ struct osmo_ss7_as *as = talloc_zero(ctx, struct osmo_ss7_as);
+ init_as(as);
+
+ struct osmo_ss7_asp *asp = talloc_zero(ctx, struct osmo_ss7_asp);
+ init_asp(asp);
+
+ size_t before_test = talloc_total_size(ctx), after_test;
+ uint32_t hlr_tid = 23;
+ uint32_t vlr_tid = 42;
+
+ struct tcap_trans_track_entry *entry;
+ struct osmo_ss7_asp *search_asp;
+
+
+ printf("Full transaction test\n");
+
+ /* create an entry (VLR -> HLR (TCAP Begin otid 23) */
+ entry = tcap_trans_track_begin(as, asp,
+ &ghlr, NULL,
+ &gvlr, &vlr_tid);
+ OSMO_ASSERT(entry);
+ OSMO_ASSERT(count_unique_hash_entries(as) == 1);
+
+ /* should find it regardless of the missing own tid and update the missing tid */
+ search_asp = tcap_trans_track_continue(as,
+ &ghlr, &hlr_tid,
+ &gvlr, &vlr_tid);
+ OSMO_ASSERT(search_asp == asp);
+ OSMO_ASSERT(count_unique_hash_entries(as) == 1);
+
+ /* update entry by use tcap_trans_track_connection_get() */
+ search_asp = tcap_trans_track_continue(as,
+ &ghlr, &hlr_tid,
+ &gvlr, NULL);
+ OSMO_ASSERT(search_asp == asp);
+
+ search_asp = tcap_trans_track_end(as,
+ &gvlr, NULL,
+ &gvlr, &vlr_tid);
+ OSMO_ASSERT(search_asp);
+ OSMO_ASSERT(search_asp == asp);
+
+ OSMO_ASSERT(hash_empty(as->tcap.trans_track_own));
+ OSMO_ASSERT(hash_empty(as->tcap.trans_track_peer));
+ after_test = talloc_total_size(ctx);
+ fprintf(stderr, "Consuming %lu bytes after test. Failing if not %lu.\n",
+ after_test, before_test);
+ OSMO_ASSERT(after_test == before_test);
+ talloc_free(ctx);
+}
+
+static void tcap_trans_track_test_gc(void)
+{
+ void *ctx = talloc_new(NULL);
+ struct osmo_ss7_as *as = talloc_zero(ctx, struct osmo_ss7_as);
+ init_as(as);
+
+ struct osmo_ss7_asp *asp = talloc_zero(ctx, struct osmo_ss7_asp);
+ init_asp(asp);
+
+ size_t before_test = talloc_total_size(ctx), after_test;
+ uint32_t hlr_tids[] = { 24, 25, 26};
+ uint32_t vlr_tids[] = { 44, 45, 46};
+
+ struct tcap_trans_track_entry *entry;
+
+ printf("GC test\n");
+
+ /* create 3 entries */
+ for (int i = 0; i < ARRAY_SIZE(hlr_tids); i++) {
+ entry = tcap_trans_track_entry_create(as, asp,
+ &ghlr, &hlr_tids[i],
+ &gvlr, &vlr_tids[i]);
+ OSMO_ASSERT(entry);
+ }
+
+ OSMO_ASSERT(count_unique_hash_entries(as) == 3);
+ /* No entries should be GC'ed, because all entries should be within 10 secs */
+ OSMO_ASSERT(tcap_trans_track_garbage_collect(as) == 0);
+ OSMO_ASSERT(count_unique_hash_entries(as) == 3);
+
+ entry->tstamp = 1;
+ OSMO_ASSERT(tcap_trans_track_garbage_collect(as) == 1);
+ OSMO_ASSERT(count_unique_hash_entries(as) == 2);
+
+ tcap_trans_track_entries_free_all(as);
+ OSMO_ASSERT(count_unique_hash_entries(as) == 0);
+ OSMO_ASSERT(hash_empty(as->tcap.trans_track_own));
+ OSMO_ASSERT(hash_empty(as->tcap.trans_track_peer));
+ after_test = talloc_total_size(ctx);
+ fprintf(stderr, "Consuming %lu bytes after test. Failing if not %lu.\n",
+ after_test, before_test);
+ OSMO_ASSERT(after_test == before_test);
+ talloc_free(ctx);
+}
+
+int main(int argc, char **argv)
+{
+ printf("Start running tests.\n");
+
+ tcap_trans_track_test_func_t iter = NULL;
+ for (int i = 0; ((iter = tcap_trans_track_tests[i])); i++) {
+ printf("Starting test %d\n", i);
+ iter();
+ printf("Finished test %d\n", i);
+ }
+
+ printf("All tests passed.\n");
+ return 0;
+}
+
+
+tcap_trans_track_test_func_t tcap_trans_track_tests[] = {
+ tcap_trans_track_test_create_find_free,
+ tcap_trans_track_test_transaction,
+ tcap_trans_track_test_gc,
+ NULL
+};
+
diff --git a/tests/tcap/tcap_transaction_tracking_test.ok b/tests/tcap/tcap_transaction_tracking_test.ok
new file mode 100644
index 0000000..c683cf7
--- /dev/null
+++ b/tests/tcap/tcap_transaction_tracking_test.ok
@@ -0,0 +1,11 @@
+Start running tests.
+Starting test 0
+Create/Find/Free test
+Finished test 0
+Starting test 1
+Full transaction test
+Finished test 1
+Starting test 2
+GC test
+Finished test 2
+All tests passed.
diff --git a/tests/testsuite.at b/tests/testsuite.at
index ce1fd87..910fc0b 100644
--- a/tests/testsuite.at
+++ b/tests/testsuite.at
@@ -19,3 +19,10 @@
cat $abs_srcdir/ss7/ss7_test.ok > expout
AT_CHECK([$abs_top_builddir/tests/ss7/ss7_test], [], [expout], [ignore])
AT_CLEANUP
+
+AT_SETUP([tcap_transaction_tracking])
+AT_KEYWORDS([tcap_transaction_tracking])
+AT_SKIP_IF([test ! -e $abs_srcdir/tcap/tcap_transaction_tracking_test])
+cat $abs_srcdir/tcap/tcap_transaction_tracking_test.ok > expout
+AT_CHECK([$abs_top_builddir/tests/tcap/tcap_transaction_tracking_test], [], [expout], [ignore])
+AT_CLEANUP
diff --git a/tests/vty/Makefile.am b/tests/vty/Makefile.am
index 4b09f6d..707fbd4 100644
--- a/tests/vty/Makefile.am
+++ b/tests/vty/Makefile.am
@@ -29,14 +29,24 @@
vty-python-test: $(top_builddir)/stp/osmo-stp
$(srcdir)/vty_test_runner.py -w $(abs_top_builddir) -v
+VTY_TEST_STP_DEFAULT = \
+ osmo_stp_test.vty \
+ osmo_stp_route_prio.vty \
+ $(NULL)
+
+if BUILD_WITH_TCAP_LOADSHARING
+VTY_TEST_STP_DEFAULT += osmo_stp_test_tcap.vty
+EXTRA_DIST += osmo_stp_test_tcap.vty
+endif
+
# Run a specific test with: 'make vty-test VTY_TEST_STP=osmo_stp_test.vty'
-VTY_TEST_STP ?= osmo_stp_*.vty
+VTY_TEST_STP ?= $(VTY_TEST_STP_DEFAULT)
vty-transcript-test-stp: $(top_builddir)/stp/osmo-stp
osmo_verify_transcript_vty.py -v \
-n OsmoSTP -p 4239 \
-r "$(top_builddir)/stp/osmo-stp -c $(top_srcdir)/doc/examples/osmo-stp-multihome.cfg" \
- $(U) $(srcdir)/$(VTY_TEST_STP)
+ $(U) $(patsubst %,$(srcdir)/%,$(VTY_TEST_STP))
# Run a specific test with: 'make vty-test VTY_TEST_ASP=ss7_asp_test.vty'
VTY_TEST_ASP ?= ss7_asp_*.vty
@@ -45,7 +55,7 @@
osmo_verify_transcript_vty.py -v \
-p 42043 \
-r "$(builddir)/ss7_asp_vty_test" \
- $(U) $(srcdir)/$(VTY_TEST_ASP)
+ $(U) $(patsubst %,$(srcdir)/%,$(VTY_TEST_ASP))
# To update the VTY script from current application behavior,
# pass -u to osmo_verify_transcript_vty.py by doing:
diff --git a/tests/vty/osmo_stp_test.vty b/tests/vty/osmo_stp_test.vty
index 08d0092..47742f2 100644
--- a/tests/vty/osmo_stp_test.vty
+++ b/tests/vty/osmo_stp_test.vty
@@ -387,6 +387,7 @@
traffic-mode loadshare [bindings] [sls] [opc-sls] [opc-shift] [<0-2>]
no traffic-mode
sls-shift <0-3>
+...
binding-table reset
recovery-timeout <1-2000>
qos-class <0-7>
@@ -405,6 +406,7 @@
no Negate a command or set its defaults
traffic-mode Specifies traffic mode of operation of the ASP within the AS
sls-shift Shift SLS bits used during routing decision
+...
binding-table AS Loadshare binding table operations
recovery-timeout Specifies RFC4666 recovery timer T(r) timeout
qos-class Specity QoS Class of AS
@@ -414,6 +416,7 @@
OsmoSTP(config-cs7-as)# no ?
asp Specify ASP to be removed from this AS
traffic-mode Remove explicit traffic mode of operation of this AS
+...
point-code Point Code Specific Features
OsmoSTP(config-cs7-as)# do show cs7 instance 0 as all
diff --git a/tests/vty/osmo_stp_test_tcap.vty b/tests/vty/osmo_stp_test_tcap.vty
new file mode 100644
index 0000000..665945f
--- /dev/null
+++ b/tests/vty/osmo_stp_test_tcap.vty
@@ -0,0 +1,35 @@
+OsmoSTP> enable
+OsmoSTP# configure terminal
+OsmoSTP(config)# cs7 instance 0
+
+OsmoSTP(config-cs7)# as my-ass m3ua
+OsmoSTP(config-cs7-as)# list
+...
+ tcap-routing
+ no tcap-routing
+...
+
+OsmoSTP(config-cs7-as)# ?
+ help Description of the interactive help system
+ list Print command list
+ show Show running system information
+ write Write running configuration to memory, network, or terminal
+ exit Exit current mode and down to previous mode
+ end End current mode and change to enable mode.
+ description Save human-readable description of the object
+ asp Specify that a given ASP is part of this AS
+ no Negate a command or set its defaults
+ traffic-mode Specifies traffic mode of operation of the ASP within the AS
+ sls-shift Shift SLS bits used during routing decision
+ tcap-routing Enable TCAP-based routing when in traffic-mode loadshare
+ binding-table AS Loadshare binding table operations
+ recovery-timeout Specifies RFC4666 recovery timer T(r) timeout
+ qos-class Specity QoS Class of AS
+ routing-key Define a routing key
+ point-code Point Code Specific Features
+
+OsmoSTP(config-cs7-as)# no ?
+ asp Specify ASP to be removed from this AS
+ traffic-mode Remove explicit traffic mode of operation of this AS
+ tcap-routing Disable TCAP-based routing when in traffic-mode loadshare
+ point-code Point Code Specific Features
To view, visit change 41309. To unsubscribe, or for help writing mail filters, visit settings.