/* * (C) 2012-2017 by Pablo Neira Ayuso * (C) 2012 by On Waves ehf * (C) 2015-2022 by sysmocom - s.f.m.c. GmbH * * 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. */ #include #include #include #include #include #include #include #include #include #include #include #include /*! \addtogroup osmux Osmocom Multiplex Protocol * @{ * * This code implements a variety of utility functions related to the * OSMUX user-plane multiplexing protocol, an efficient alternative to * plain UDP/RTP streams for voice transport in back-haul of cellular * networks. * * For information about the OSMUX protocol design, please see the * OSMUX reference manual at * http://ftp.osmocom.org/docs/latest/osmux-reference.pdf */ /*! \file osmux_input.c * \brief Osmocom multiplex protocol helpers (input) */ /* This allows you to debug osmux message transformations (spamming) */ #if 0 #define DEBUG_MSG 0 #endif /* delta time between two RTP messages (in microseconds) */ #define DELTA_RTP_MSG 20000 /* delta time between two RTP messages (in samples, 8kHz) */ #define DELTA_RTP_TIMESTAMP 160 static void *osmux_ctx; static uint32_t osmux_ft_dummy_size(uint8_t amr_ft, uint8_t batch_factor) { return sizeof(struct osmux_hdr) + (osmo_amr_bytes(amr_ft) * batch_factor); } struct osmux_batch { struct osmo_timer_list timer; struct osmux_hdr *osmuxh; struct llist_head circuit_list; unsigned int remaining_bytes; uint32_t nmsgs; int ndummy; }; struct osmux_circuit { struct llist_head head; int ccid; struct llist_head msg_list; int nmsgs; int dummy; uint8_t seq; }; /* returns: 1 if batch is full, 0 if batch still not full, negative on error. */ static int osmux_batch_enqueue(struct msgb *msg, struct osmux_circuit *circuit, uint8_t batch_factor) { /* Validate amount of messages per batch. The counter field of the * osmux header is just 3 bits long, so make sure it doesn't overflow. */ OSMO_ASSERT(batch_factor <= 8); if (circuit->nmsgs >= batch_factor) { struct rtp_hdr *rtph; rtph = osmo_rtp_get_hdr(msg); if (rtph == NULL) return -1; LOGP(DLMUX, LOGL_DEBUG, "Batch is full for RTP sssrc=%u\n", rtph->ssrc); return 1; } llist_add_tail(&msg->list, &circuit->msg_list); circuit->nmsgs++; return 0; } static void osmux_batch_dequeue(struct msgb *msg, struct osmux_circuit *circuit) { llist_del(&msg->list); circuit->nmsgs--; } static void osmux_circuit_del_msgs(struct osmux_batch *batch, struct osmux_circuit *circuit) { struct msgb *cur, *tmp; llist_for_each_entry_safe(cur, tmp, &circuit->msg_list, list) { osmux_batch_dequeue(cur, circuit); msgb_free(cur); batch->nmsgs--; } } struct osmux_input_state { struct msgb *out_msg; struct msgb *msg; struct rtp_hdr *rtph; struct amr_hdr *amrh; uint32_t amr_payload_len; struct osmux_circuit *circuit; int add_osmux_hdr; }; static int osmux_batch_put(struct osmux_batch *batch, struct osmux_input_state *state) { if (state->add_osmux_hdr) { struct osmux_hdr *osmuxh; osmuxh = (struct osmux_hdr *)msgb_put(state->out_msg, sizeof(struct osmux_hdr)); osmuxh->ft = OSMUX_FT_VOICE_AMR; osmuxh->ctr = 0; osmuxh->rtp_m = osmuxh->rtp_m || state->rtph->marker; osmuxh->seq = state->circuit->seq++; osmuxh->circuit_id = state->circuit->ccid; osmuxh->amr_ft = state->amrh->ft; /* annotate current osmux header */ batch->osmuxh = osmuxh; } else { if (batch->osmuxh->ctr == 0x7) { LOGP(DLMUX, LOGL_ERROR, "cannot add msg=%p, " "too many messages for this RTP ssrc=%u\n", state->msg, state->rtph->ssrc); return 0; } batch->osmuxh->ctr++; } /* For fields below, we only use the last included in batch and ignore any previous: */ batch->osmuxh->amr_cmr = state->amrh->cmr; batch->osmuxh->amr_f = state->amrh->f; batch->osmuxh->amr_q = state->amrh->q; memcpy(state->out_msg->tail, osmo_amr_get_payload(state->amrh), state->amr_payload_len); msgb_put(state->out_msg, state->amr_payload_len); return 0; } static void osmux_encode_dummy(struct osmux_batch *batch, uint8_t batch_factor, struct osmux_input_state *state) { struct osmux_hdr *osmuxh; /* TODO: This should be configurable at some point. */ uint32_t payload_size = osmux_ft_dummy_size(AMR_FT_3, batch_factor) - sizeof(struct osmux_hdr); osmuxh = (struct osmux_hdr *)state->out_msg->tail; osmuxh->ft = OSMUX_FT_DUMMY; osmuxh->ctr = batch_factor - 1; osmuxh->amr_f = 0; osmuxh->amr_q = 0; osmuxh->seq = 0; osmuxh->circuit_id = state->circuit->ccid; osmuxh->amr_cmr = 0; osmuxh->amr_ft = AMR_FT_3; msgb_put(state->out_msg, sizeof(struct osmux_hdr)); memset(state->out_msg->tail, 0xff, payload_size); msgb_put(state->out_msg, payload_size); } static struct msgb *osmux_build_batch(struct osmux_batch *batch, uint32_t batch_size, uint8_t batch_factor) { struct msgb *batch_msg; struct osmux_circuit *circuit; #ifdef DEBUG_MSG LOGP(DLMUX, LOGL_DEBUG, "Now building batch\n"); #endif batch_msg = msgb_alloc(batch_size, "osmux"); if (batch_msg == NULL) { LOGP(DLMUX, LOGL_ERROR, "Not enough memory\n"); return NULL; } llist_for_each_entry(circuit, &batch->circuit_list, head) { struct msgb *cur, *tmp; int ctr = 0; int prev_amr_ft; if (circuit->dummy) { struct osmux_input_state state = { .out_msg = batch_msg, .circuit = circuit, }; osmux_encode_dummy(batch, batch_factor, &state); continue; } llist_for_each_entry_safe(cur, tmp, &circuit->msg_list, list) { struct osmux_input_state state = { .msg = cur, .out_msg = batch_msg, .circuit = circuit, }; uint32_t amr_len; #ifdef DEBUG_MSG char buf[4096]; osmo_rtp_snprintf(buf, sizeof(buf), cur); buf[sizeof(buf)-1] = '\0'; LOGP(DLMUX, LOGL_DEBUG, "to BSC-NAT: %s\n", buf); #endif state.rtph = osmo_rtp_get_hdr(cur); if (!state.rtph) return NULL; state.amrh = osmo_rtp_get_payload(state.rtph, state.msg, &amr_len); if (!state.amrh) return NULL; state.amr_payload_len = amr_len - sizeof(struct amr_hdr); if (ctr == 0) { #ifdef DEBUG_MSG LOGP(DLMUX, LOGL_DEBUG, "Add osmux header (First in batch)\n"); #endif state.add_osmux_hdr = 1; } else if (prev_amr_ft != state.amrh->ft) { /* If AMR FT changed, we have to generate an extra batch osmux header: */ #ifdef DEBUG_MSG LOGP(DLMUX, LOGL_DEBUG, "Add osmux header (New AMR FT)\n"); #endif state.add_osmux_hdr = 1; } osmux_batch_put(batch, &state); osmux_batch_dequeue(cur, circuit); prev_amr_ft = state.amrh->ft; ctr++; msgb_free(cur); batch->nmsgs--; } } return batch_msg; } void osmux_xfrm_input_deliver(struct osmux_in_handle *h) { struct msgb *batch_msg; struct osmux_batch *batch = (struct osmux_batch *)h->internal_data; #ifdef DEBUG_MSG LOGP(DLMUX, LOGL_DEBUG, "invoking delivery function\n"); #endif batch_msg = osmux_build_batch(batch, h->batch_size, h->batch_factor); if (!batch_msg) return; h->stats.output_osmux_msgs++; h->stats.output_osmux_bytes += batch_msg->len; h->deliver(batch_msg, h->data); osmo_timer_del(&batch->timer); batch->remaining_bytes = h->batch_size; if (batch->ndummy) { osmo_timer_schedule(&batch->timer, 0, h->batch_factor * DELTA_RTP_MSG); } } static void osmux_batch_timer_expired(void *data) { struct osmux_in_handle *h = data; #ifdef DEBUG_MSG LOGP(DLMUX, LOGL_DEBUG, "osmux_batch_timer_expired\n"); #endif osmux_xfrm_input_deliver(h); } static int osmux_rtp_amr_payload_len(struct amr_hdr *amrh, uint32_t amr_len) { int amr_payload_len; if (!osmo_amr_ft_valid(amrh->ft)) return -1; amr_payload_len = amr_len - sizeof(struct amr_hdr); /* The AMR payload does not fit with what we expect */ if (osmo_amr_bytes(amrh->ft) != amr_payload_len) { LOGP(DLMUX, LOGL_ERROR, "Bad AMR frame, expected %zd bytes, got %d bytes\n", osmo_amr_bytes(amrh->ft), amr_len); return -1; } return amr_payload_len; } /* Last stored AMR FT to be added in the current osmux batch. -1 if none stored yet */ static int osmux_circuit_get_last_stored_amr_ft(struct osmux_circuit *circuit) { struct msgb *last; struct rtp_hdr *rtph; struct amr_hdr *amrh; uint32_t amr_len; /* Have we seen any RTP packet in this batch before? */ if (llist_empty(&circuit->msg_list)) return -1; OSMO_ASSERT(circuit->nmsgs > 0); /* Get last RTP packet seen in this batch */ last = llist_entry(circuit->msg_list.prev, struct msgb, list); rtph = osmo_rtp_get_hdr(last); amrh = osmo_rtp_get_payload(rtph, last, &amr_len); if (amrh == NULL) return -1; return amrh->ft; } /* returns: 1 if batch is full, 0 if batch still not full, negative on error. */ static int osmux_replay_lost_packets(struct osmux_circuit *circuit, struct rtp_hdr *cur_rtph, int batch_factor) { int16_t diff; struct msgb *last; struct rtp_hdr *rtph; int i, rc; /* Have we seen any RTP packet in this batch before? */ if (llist_empty(&circuit->msg_list)) return 0; /* Get last RTP packet seen in this batch */ last = llist_entry(circuit->msg_list.prev, struct msgb, list); rtph = osmo_rtp_get_hdr(last); if (rtph == NULL) return -1; diff = ntohs(cur_rtph->sequence) - ntohs(rtph->sequence); /* Lifesaver: make sure bugs don't spawn lots of clones */ if (diff > 16) diff = 16; rc = 0; /* If diff between last RTP packet seen and this one is > 1, * then we lost several RTP packets, let's replay them. */ for (i = 1; i < diff; i++) { struct msgb *clone; /* Clone last RTP packet seen */ clone = msgb_alloc(last->data_len, "RTP clone"); if (!clone) continue; memcpy(clone->data, last->data, last->len); msgb_put(clone, last->len); /* The original RTP message has been already sanity checked. */ rtph = osmo_rtp_get_hdr(clone); /* Adjust sequence number and timestamp */ rtph->sequence = htons(ntohs(rtph->sequence) + i); rtph->timestamp = htonl(ntohl(rtph->timestamp) + DELTA_RTP_TIMESTAMP); /* No more room in this batch, skip padding with more clones */ rc = osmux_batch_enqueue(clone, circuit, batch_factor); if (rc != 0) { msgb_free(clone); return rc; } LOGP(DLMUX, LOGL_ERROR, "adding cloned RTP\n"); } return rc; } static struct osmux_circuit * osmux_batch_find_circuit(struct osmux_batch *batch, int ccid) { struct osmux_circuit *circuit; llist_for_each_entry(circuit, &batch->circuit_list, head) { if (circuit->ccid == ccid) return circuit; } return NULL; } static void osmux_batch_del_circuit(struct osmux_batch *batch, struct osmux_circuit *circuit) { if (circuit->dummy) batch->ndummy--; llist_del(&circuit->head); osmux_circuit_del_msgs(batch, circuit); talloc_free(circuit); } /* returns: 1 if batch is full, 0 if batch still not full, negative on error. */ static int osmux_batch_add(struct osmux_batch *batch, uint32_t batch_factor, struct msgb *msg, struct rtp_hdr *rtph, int ccid) { int bytes = 0, amr_payload_len; struct osmux_circuit *circuit; struct msgb *cur; int rc; struct amr_hdr *amrh; uint32_t amr_len; circuit = osmux_batch_find_circuit(batch, ccid); if (!circuit) return -1; /* We've seen the first RTP message, disable dummy padding */ if (circuit->dummy) { circuit->dummy = 0; batch->ndummy--; } amrh = osmo_rtp_get_payload(rtph, msg, &amr_len); if (amrh == NULL) return -1; amr_payload_len = osmux_rtp_amr_payload_len(amrh, amr_len); if (amr_payload_len < 0) { LOGP(DLMUX, LOGL_NOTICE, "AMR payload invalid\n"); return -1; } /* Init of talkspurt (RTP M marker bit) needs to be in the first AMR slot * of the OSMUX packet, enforce sending previous batch if required: */ if (rtph->marker && circuit->nmsgs != 0) return 1; /* Extra validation: check if this message already exists, should not * happen but make sure we don't propagate duplicated messages. */ llist_for_each_entry(cur, &circuit->msg_list, list) { struct rtp_hdr *rtph2 = osmo_rtp_get_hdr(cur); if (rtph2 == NULL) return -1; /* Already exists message with this sequence, skip */ if (rtph2->sequence == rtph->sequence) { LOGP(DLMUX, LOGL_ERROR, "already exists " "message with seq=%u, skip it\n", rtph->sequence); return -1; } } /* First check if there is room for this message in the batch */ /* First in batch comes after the batch header: */ if (circuit->nmsgs == 0) bytes += sizeof(struct osmux_hdr); /* If AMR FT changes in the middle of current batch a new header is * required to adapt to size change: */ else if (osmux_circuit_get_last_stored_amr_ft(circuit) != amrh->ft) bytes += sizeof(struct osmux_hdr); bytes += amr_payload_len; /* No room, sorry. You'll have to retry */ if (bytes > batch->remaining_bytes) return 1; /* Handle RTP packet loss scenario */ rc = osmux_replay_lost_packets(circuit, rtph, batch_factor); if (rc != 0) return rc; /* This batch is full, force batch delivery */ rc = osmux_batch_enqueue(msg, circuit, batch_factor); if (rc != 0) return rc; #ifdef DEBUG_MSG LOGP(DLMUX, LOGL_DEBUG, "adding msg with ssrc=%u to batch\n", rtph->ssrc); #endif /* Update remaining room in this batch */ batch->remaining_bytes -= bytes; if (batch->nmsgs == 0) { #ifdef DEBUG_MSG LOGP(DLMUX, LOGL_DEBUG, "osmux start timer batch\n"); #endif osmo_timer_schedule(&batch->timer, 0, batch_factor * DELTA_RTP_MSG); } batch->nmsgs++; return 0; } /** * osmux_xfrm_input - add RTP message to OSmux batch * \param msg: RTP message that you want to batch into one OSmux message * * If 0 is returned, this indicates that the message has been batched and the * msgb is now owned by the osmux layer. * If negative value is returned, an error occurred and the message has been * dropped (and freed). * If 1 is returned, you have to invoke osmux_xfrm_input_deliver and try again. * * The function takes care of releasing the messages in case of error and * when building the batch. */ int osmux_xfrm_input(struct osmux_in_handle *h, struct msgb *msg, int ccid) { int ret; struct rtp_hdr *rtph; struct osmux_batch *batch = (struct osmux_batch *)h->internal_data; /* Ignore too big RTP/RTCP messages, most likely forged. Sanity check * to avoid a possible forever loop in the caller. */ if (msg->len > h->batch_size - sizeof(struct osmux_hdr)) { LOGP(DLMUX, LOGL_NOTICE, "RTP payload too big (%u) for configured batch size (%u)\n", msg->len, h->batch_size); msgb_free(msg); return -1; } rtph = osmo_rtp_get_hdr(msg); if (rtph == NULL) { LOGP(DLMUX, LOGL_NOTICE, "msg not containing an RTP header\n"); msgb_free(msg); return -1; } switch (rtph->payload_type) { case RTP_PT_RTCP: LOGP(DLMUX, LOGL_INFO, "Dropping RTCP packet\n"); msgb_free(msg); return 0; default: /* The RTP payload type is dynamically allocated, * although we use static ones. Assume that we always * receive AMR traffic. */ /* Add this RTP to the OSMUX batch */ ret = osmux_batch_add(batch, h->batch_factor, msg, rtph, ccid); if (ret < 0) { /* Cannot put this message into the batch. * Malformed, duplicated, OOM. Drop it and tell * the upper layer that we have digest it. */ LOGP(DLMUX, LOGL_DEBUG, "Dropping RTP packet instead of adding to batch\n"); msgb_free(msg); return ret; } h->stats.input_rtp_msgs++; h->stats.input_rtp_bytes += msg->len; break; } return ret; } static int osmux_xfrm_input_talloc_destructor(struct osmux_in_handle *h) { struct osmux_batch *batch = (struct osmux_batch *)h->internal_data; struct osmux_circuit *circuit, *next; llist_for_each_entry_safe(circuit, next, &batch->circuit_list, head) osmux_batch_del_circuit(batch, circuit); osmo_timer_del(&batch->timer); talloc_free(batch); return 0; } /*! \brief Allocate a new osmux in handle (osmux source, tx side) * \param[in] ctx talloc context to use when allocating the returned struct * \return Allocated osmux in handle * * This object contains configuration and state to handle a group of circuits (trunk), * receiving RTP packets from the upper layer (API user) and sending batched & * trunked Osmux messages containing all the data of those circuits down the * stack outgoing network Osmux messages. * Returned pointer can be freed with regular talloc_free, all pending messages * in queue and all internal data will be freed. */ struct osmux_in_handle *osmux_xfrm_input_alloc(void *ctx) { struct osmux_in_handle *h; h = talloc_zero(ctx, struct osmux_in_handle); OSMO_ASSERT(h); struct osmux_batch *batch; h->batch_size = OSMUX_BATCH_DEFAULT_MAX; batch = talloc_zero(h, struct osmux_batch); OSMO_ASSERT(batch); INIT_LLIST_HEAD(&batch->circuit_list); batch->remaining_bytes = h->batch_size; osmo_timer_setup(&batch->timer, osmux_batch_timer_expired, h); h->internal_data = (void *)batch; LOGP(DLMUX, LOGL_DEBUG, "initialized osmux input converter\n"); talloc_set_destructor(h, osmux_xfrm_input_talloc_destructor); return h; } /* DEPRECATED: Use osmux_xfrm_input_alloc() instead */ void osmux_xfrm_input_init(struct osmux_in_handle *h) { struct osmux_batch *batch; /* Default to osmux packet size if not specified */ if (h->batch_size == 0) h->batch_size = OSMUX_BATCH_DEFAULT_MAX; batch = talloc_zero(osmux_ctx, struct osmux_batch); if (batch == NULL) return; INIT_LLIST_HEAD(&batch->circuit_list); batch->remaining_bytes = h->batch_size; osmo_timer_setup(&batch->timer, osmux_batch_timer_expired, h); h->internal_data = (void *)batch; LOGP(DLMUX, LOGL_DEBUG, "initialized osmux input converter\n"); } int osmux_xfrm_input_set_batch_factor(struct osmux_in_handle *h, uint8_t batch_factor) { if (batch_factor > 8) return -1; h->batch_factor = batch_factor; return 0; } void osmux_xfrm_input_set_batch_size(struct osmux_in_handle *h, uint16_t batch_size) { if (batch_size == 0) h->batch_size = OSMUX_BATCH_DEFAULT_MAX; else h->batch_size = batch_size; } void osmux_xfrm_input_set_initial_seqnum(struct osmux_in_handle *h, uint8_t osmux_seqnum) { h->osmux_seq = osmux_seqnum; } void osmux_xfrm_input_set_deliver_cb(struct osmux_in_handle *h, void (*deliver_cb)(struct msgb *msg, void *data), void *data) { h->deliver = deliver_cb; h->data = data; } void *osmux_xfrm_input_get_deliver_cb_data(struct osmux_in_handle *h) { return h->data; } int osmux_xfrm_input_open_circuit(struct osmux_in_handle *h, int ccid, int dummy) { struct osmux_batch *batch = (struct osmux_batch *)h->internal_data; struct osmux_circuit *circuit; circuit = osmux_batch_find_circuit(batch, ccid); if (circuit != NULL) { LOGP(DLMUX, LOGL_ERROR, "circuit %u already exists!\n", ccid); return -1; } circuit = talloc_zero(osmux_ctx, struct osmux_circuit); if (circuit == NULL) { LOGP(DLMUX, LOGL_ERROR, "OOM on circuit %u\n", ccid); return -1; } circuit->ccid = ccid; circuit->seq = h->osmux_seq; INIT_LLIST_HEAD(&circuit->msg_list); llist_add_tail(&circuit->head, &batch->circuit_list); if (dummy) { circuit->dummy = dummy; batch->ndummy++; if (!osmo_timer_pending(&batch->timer)) osmo_timer_schedule(&batch->timer, 0, h->batch_factor * DELTA_RTP_MSG); } return 0; } void osmux_xfrm_input_close_circuit(struct osmux_in_handle *h, int ccid) { struct osmux_batch *batch = (struct osmux_batch *)h->internal_data; struct osmux_circuit *circuit; circuit = osmux_batch_find_circuit(batch, ccid); if (circuit == NULL) { LOGP(DLMUX, LOGL_NOTICE, "Unable to close circuit %d: Not found\n", ccid); return; } osmux_batch_del_circuit(batch, circuit); } /* DEPRECATED: Use talloc_free() instead (will call osmux_xfrm_input_talloc_destructor()) */ void osmux_xfrm_input_fini(struct osmux_in_handle *h) { (void)osmux_xfrm_input_talloc_destructor(h); } /*! @} */