/* TCAP transaction cache */ /* (C) 2025 by sysmocom s.f.m.c. GmbH * All Rights Reserved * Author: Alexander Couzens * * 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 . * */ #include #include #include #include #include "ss7_asp.h" #include "ss7_as.h" #include "tcap_transaction_tracking.h" static inline void entry_update_tstamp(struct tcap_trxn_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 trxn_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_trxn_track_entry *tcap_trxn_track_create_entry( 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_trxn_track_entry *entry; OSMO_ASSERT(own_addr); OSMO_ASSERT(peer_addr); OSMO_ASSERT(own_tid || peer_tid); entry = talloc_zero(as, struct tcap_trxn_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; trxn_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.trxn_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; trxn_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.trxn_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 %u/%u/%u -> %u/%u/%u\n", entry->own_addr->pc, entry->own_addr->ssn, entry->own_tid.tid, 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_trxn_track_free_entry(struct tcap_trxn_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_trxn_track_entry *tcap_trxn_track_find_entry( 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_trxn_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.trxn_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.trxn_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.trxn_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.trxn_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_trxn_track_entry *tcap_trxn_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_trxn_track_entry *entry = tcap_trxn_track_find_entry(as, own_addr, own_tid, peer_addr, peer_tid); if (entry) { entry_update_tstamp(entry); return entry; } return tcap_trxn_track_create_entry(as, asp, own_addr, own_tid, peer_addr, peer_tid); } struct osmo_ss7_asp *tcap_trxn_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_trxn_track_entry *entry = tcap_trxn_track_find_entry(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.trxn_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.trxn_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_trxn_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_trxn_track_entry *entry = tcap_trxn_track_find_entry(as, own_addr, own_tid, peer_addr, peer_tid); if (!entry) return NULL; asp = entry->asp; tcap_trxn_track_free_entry(entry); return asp; } int tcap_trxn_track_garbage_collect(struct osmo_ss7_as *as) { int i, count = 0; struct tcap_trxn_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.trxn_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_trxn_track_free_entry(entry); } } hash_for_each_safe(as->tcap.trxn_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_trxn_track_free_entry(entry); } } return count; } static void tcap_trxn_track_garbage_collect_cb(void *data) { struct osmo_ss7_as *as = data; int counts = tcap_trxn_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_trxn_track_garbage_collect_start(struct osmo_ss7_as *as) { osmo_timer_setup(&as->tcap.gc_timer, tcap_trxn_track_garbage_collect_cb, as); tcap_trxn_track_garbage_collect_cb(as); } void tcap_trxn_track_garbage_collect_stop(struct osmo_ss7_as *as) { if (osmo_timer_pending(&as->tcap.gc_timer)) osmo_timer_del(&as->tcap.gc_timer); } int tcap_trxn_track_free_asp_entries(struct osmo_ss7_as *as, struct osmo_ss7_asp *asp) { int i, count = 0; struct tcap_trxn_track_entry *entry; struct hlist_node *tmp; hash_for_each_safe(as->tcap.trxn_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_trxn_track_free_entry(entry); } } hash_for_each_safe(as->tcap.trxn_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_trxn_track_free_entry(entry); } } return count; } int tcap_trxn_track_free_all_entries(struct osmo_ss7_as *as) { int i, count = 0; struct tcap_trxn_track_entry *entry; struct hlist_node *tmp; hash_for_each_safe(as->tcap.trxn_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_trxn_track_free_entry(entry); } hash_for_each_safe(as->tcap.trxn_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_trxn_track_free_entry(entry); } return count; }