/*! \file gprs_bssgp_bss.c * GPRS BSSGP protocol implementation as per 3GPP TS 08.18. */ /* * (C) 2009-2017 by Harald Welte * * 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 . * */ #include #include #include #include #include #include #include #include #include #include #include "gprs_bssgp_internal.h" #define GSM_IMSI_LENGTH 17 uint8_t *bssgp_msgb_tlli_put(struct msgb *msg, uint32_t tlli) { uint32_t _tlli = osmo_htonl(tlli); return msgb_tvlv_put(msg, BSSGP_IE_TLLI, 4, (uint8_t *) &_tlli); } uint8_t *bssgp_msgb_ra_put(struct msgb *msg, const struct gprs_ra_id *ra_id) { struct gsm48_ra_id ra; gsm48_encode_ra(&ra, ra_id); return msgb_tvlv_put(msg, BSSGP_IE_ROUTEING_AREA, sizeof(ra), (const uint8_t *)&ra); } /*! GMM-SUSPEND.req (Chapter 10.3.6) */ int bssgp_tx_suspend(uint16_t nsei, uint32_t tlli, const struct gprs_ra_id *ra_id) { struct msgb *msg = bssgp_msgb_alloc(); struct bssgp_normal_hdr *bgph = (struct bssgp_normal_hdr *) msgb_put(msg, sizeof(*bgph)); LOGP(DLBSSGP, LOGL_NOTICE, "BSSGP (BVCI=0) Tx SUSPEND (TLLI=0x%04x)\n", tlli); msgb_nsei(msg) = nsei; msgb_bvci(msg) = 0; /* Signalling */ bgph->pdu_type = BSSGP_PDUT_SUSPEND; bssgp_msgb_tlli_put(msg, tlli); bssgp_msgb_ra_put(msg, ra_id); return bssgp_ns_send(bssgp_ns_send_data, msg); } /*! GMM-RESUME.req (Chapter 10.3.9) */ int bssgp_tx_resume(uint16_t nsei, uint32_t tlli, const struct gprs_ra_id *ra_id, uint8_t suspend_ref) { struct msgb *msg = bssgp_msgb_alloc(); struct bssgp_normal_hdr *bgph = (struct bssgp_normal_hdr *) msgb_put(msg, sizeof(*bgph)); LOGP(DLBSSGP, LOGL_NOTICE, "BSSGP (BVCI=0) Tx RESUME (TLLI=0x%04x)\n", tlli); msgb_nsei(msg) = nsei; msgb_bvci(msg) = 0; /* Signalling */ bgph->pdu_type = BSSGP_PDUT_RESUME; bssgp_msgb_tlli_put(msg, tlli); bssgp_msgb_ra_put(msg, ra_id); msgb_tvlv_put(msg, BSSGP_IE_SUSPEND_REF_NR, 1, &suspend_ref); return bssgp_ns_send(bssgp_ns_send_data, msg); } /*! Transmit RA-CAPABILITY-UPDATE (10.3.3) */ int bssgp_tx_ra_capa_upd(struct bssgp_bvc_ctx *bctx, uint32_t tlli, uint8_t tag) { struct msgb *msg = bssgp_msgb_alloc(); struct bssgp_normal_hdr *bgph = (struct bssgp_normal_hdr *) msgb_put(msg, sizeof(*bgph)); LOGP(DLBSSGP, LOGL_NOTICE, "BSSGP (BVCI=%u) Tx RA-CAPA-UPD (TLLI=0x%04x)\n", bctx->bvci, tlli); /* set NSEI and BVCI in msgb cb */ msgb_nsei(msg) = bctx->nsei; msgb_bvci(msg) = bctx->bvci; bgph->pdu_type = BSSGP_PDUT_RA_CAPA_UDPATE; bssgp_msgb_tlli_put(msg, tlli); msgb_tvlv_put(msg, BSSGP_IE_TAG, 1, &tag); return bssgp_ns_send(bssgp_ns_send_data, msg); } /* first common part of RADIO-STATUS */ static struct msgb *common_tx_radio_status(struct bssgp_bvc_ctx *bctx) { struct msgb *msg = bssgp_msgb_alloc(); struct bssgp_normal_hdr *bgph = (struct bssgp_normal_hdr *) msgb_put(msg, sizeof(*bgph)); LOGP(DLBSSGP, LOGL_NOTICE, "BSSGP (BVCI=%u) Tx RADIO-STATUS ", bctx->bvci); /* set NSEI and BVCI in msgb cb */ msgb_nsei(msg) = bctx->nsei; msgb_bvci(msg) = bctx->bvci; bgph->pdu_type = BSSGP_PDUT_RADIO_STATUS; return msg; } /* second common part of RADIO-STATUS */ static int common_tx_radio_status2(struct msgb *msg, uint8_t cause) { msgb_tvlv_put(msg, BSSGP_IE_CAUSE, 1, &cause); LOGPC(DLBSSGP, LOGL_NOTICE, "CAUSE=%s\n", bssgp_cause_str(cause)); return bssgp_ns_send(bssgp_ns_send_data, msg); } /*! Transmit RADIO-STATUS for TLLI (10.3.5) */ int bssgp_tx_radio_status_tlli(struct bssgp_bvc_ctx *bctx, uint8_t cause, uint32_t tlli) { struct msgb *msg = common_tx_radio_status(bctx); if (!msg) return -ENOMEM; bssgp_msgb_tlli_put(msg, tlli); LOGPC(DLBSSGP, LOGL_NOTICE, "TLLI=0x%08x ", tlli); return common_tx_radio_status2(msg, cause); } /*! Transmit RADIO-STATUS for TMSI (10.3.5) */ int bssgp_tx_radio_status_tmsi(struct bssgp_bvc_ctx *bctx, uint8_t cause, uint32_t tmsi) { struct msgb *msg = common_tx_radio_status(bctx); uint32_t _tmsi = osmo_htonl(tmsi); if (!msg) return -ENOMEM; msgb_tvlv_put(msg, BSSGP_IE_TMSI, 4, (uint8_t *)&_tmsi); LOGPC(DLBSSGP, LOGL_NOTICE, "TMSI=0x%08x ", tmsi); return common_tx_radio_status2(msg, cause); } /*! Transmit RADIO-STATUS for IMSI (10.3.5) */ int bssgp_tx_radio_status_imsi(struct bssgp_bvc_ctx *bctx, uint8_t cause, const char *imsi) { struct msgb *msg = common_tx_radio_status(bctx); uint8_t mi[GSM48_MID_MAX_SIZE]; int imsi_len = gsm48_generate_mid_from_imsi(mi, imsi); if (!msg) return -ENOMEM; /* gsm48_generate_mid_from_imsi() is guaranteed to never return more than 11, * but somehow gcc (8.2) is not smart enough to figure this out and claims that * the memcpy in msgb_tvlv_put() below will cause and out-of-bounds access up to * mi[131], which is wrong */ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Warray-bounds" OSMO_ASSERT(imsi_len <= GSM48_MID_MAX_SIZE); /* strip the MI type and length values (2 bytes) */ if (imsi_len > 2) msgb_tvlv_put(msg, BSSGP_IE_IMSI, imsi_len-2, mi+2); #pragma GCC diagnostic pop LOGPC(DLBSSGP, LOGL_NOTICE, "IMSI=%s ", imsi); return common_tx_radio_status2(msg, cause); } /*! Transmit FLUSH-LL-ACK (Chapter 10.4.2) */ int bssgp_tx_flush_ll_ack(struct bssgp_bvc_ctx *bctx, uint32_t tlli, uint8_t action, uint16_t bvci_new, uint32_t num_octets) { struct msgb *msg = bssgp_msgb_alloc(); struct bssgp_normal_hdr *bgph = (struct bssgp_normal_hdr *) msgb_put(msg, sizeof(*bgph)); uint16_t _bvci_new = osmo_htons(bvci_new); uint32_t _oct_aff = osmo_htonl(num_octets & 0xFFFFFF); msgb_nsei(msg) = bctx->nsei; msgb_bvci(msg) = 0; /* Signalling */ bgph->pdu_type = BSSGP_PDUT_FLUSH_LL_ACK; bssgp_msgb_tlli_put(msg, tlli); msgb_tvlv_put(msg, BSSGP_IE_FLUSH_ACTION, 1, &action); if (action == 1) /* transferred */ msgb_tvlv_put(msg, BSSGP_IE_BVCI, 2, (uint8_t *) &_bvci_new); msgb_tvlv_put(msg, BSSGP_IE_NUM_OCT_AFF, 3, (uint8_t *) &_oct_aff); return bssgp_ns_send(bssgp_ns_send_data, msg); } /*! Transmit LLC-DISCARDED (Chapter 10.4.3) */ int bssgp_tx_llc_discarded(struct bssgp_bvc_ctx *bctx, uint32_t tlli, uint8_t num_frames, uint32_t num_octets) { struct msgb *msg = bssgp_msgb_alloc(); struct bssgp_normal_hdr *bgph = (struct bssgp_normal_hdr *) msgb_put(msg, sizeof(*bgph)); uint16_t _bvci = osmo_htons(bctx->bvci); uint32_t _oct_aff = osmo_htonl(num_octets & 0xFFFFFF); LOGP(DLBSSGP, LOGL_NOTICE, "BSSGP (BVCI=%u) Tx LLC-DISCARDED " "TLLI=0x%04x, FRAMES=%u, OCTETS=%u\n", bctx->bvci, tlli, num_frames, num_octets); msgb_nsei(msg) = bctx->nsei; msgb_bvci(msg) = 0; /* Signalling */ bgph->pdu_type = BSSGP_PDUT_LLC_DISCARD; bssgp_msgb_tlli_put(msg, tlli); msgb_tvlv_put(msg, BSSGP_IE_LLC_FRAMES_DISCARDED, 1, &num_frames); msgb_tvlv_put(msg, BSSGP_IE_BVCI, 2, (uint8_t *) &_bvci); msgb_tvlv_put(msg, BSSGP_IE_NUM_OCT_AFF, 3, ((uint8_t *) &_oct_aff) + 1); return bssgp_ns_send(bssgp_ns_send_data, msg); } /*! Transmit a BVC-BLOCK message (Chapter 10.4.8) */ int bssgp_tx_bvc_block(struct bssgp_bvc_ctx *bctx, uint8_t cause) { struct msgb *msg = bssgp_msgb_alloc(); struct bssgp_normal_hdr *bgph = (struct bssgp_normal_hdr *) msgb_put(msg, sizeof(*bgph)); uint16_t _bvci = osmo_htons(bctx->bvci); LOGP(DLBSSGP, LOGL_NOTICE, "BSSGP (BVCI=%u) Tx BVC-BLOCK " "CAUSE=%s\n", bctx->bvci, bssgp_cause_str(cause)); msgb_nsei(msg) = bctx->nsei; msgb_bvci(msg) = 0; /* Signalling */ bgph->pdu_type = BSSGP_PDUT_BVC_BLOCK; msgb_tvlv_put(msg, BSSGP_IE_BVCI, 2, (uint8_t *) &_bvci); msgb_tvlv_put(msg, BSSGP_IE_CAUSE, 1, &cause); return bssgp_ns_send(bssgp_ns_send_data, msg); } /*! Transmit a BVC-UNBLOCK message (Chapter 10.4.10) */ int bssgp_tx_bvc_unblock(struct bssgp_bvc_ctx *bctx) { struct msgb *msg = bssgp_msgb_alloc(); struct bssgp_normal_hdr *bgph = (struct bssgp_normal_hdr *) msgb_put(msg, sizeof(*bgph)); uint16_t _bvci = osmo_htons(bctx->bvci); LOGP(DLBSSGP, LOGL_NOTICE, "BSSGP (BVCI=%u) Tx BVC-UNBLOCK\n", bctx->bvci); msgb_nsei(msg) = bctx->nsei; msgb_bvci(msg) = 0; /* Signalling */ bgph->pdu_type = BSSGP_PDUT_BVC_UNBLOCK; msgb_tvlv_put(msg, BSSGP_IE_BVCI, 2, (uint8_t *) &_bvci); return bssgp_ns_send(bssgp_ns_send_data, msg); } /*! Transmit a BVC-RESET message (Chapter 10.4.12) */ int bssgp_tx_bvc_reset2(struct bssgp_bvc_ctx *bctx, uint16_t bvci, uint8_t cause, bool add_cell_id) { if (add_cell_id) return bssgp_tx_bvc_reset_nsei_bvci(bctx->nsei, bvci, cause, &bctx->ra_id, bctx->cell_id); else return bssgp_tx_bvc_reset_nsei_bvci(bctx->nsei, bvci, cause, NULL, 0); } int bssgp_tx_bvc_reset(struct bssgp_bvc_ctx *bctx, uint16_t bvci, uint8_t cause) { /* The Cell Identifier IE is mandatory in the BVC-RESET PDU sent from BSS to SGSN in order to reset a * BVC corresponding to a PTP functional entity. The Cell Identifier IE shall not be used in any other * BVC-RESET PDU. */ switch (bvci) { case BVCI_SIGNALLING: case BVCI_PTM: return bssgp_tx_bvc_reset2(bctx, bvci, cause, false); default: return bssgp_tx_bvc_reset2(bctx, bvci, cause, true); } } /*! Transmit a FLOW_CONTROL-BVC (Chapter 10.4.4) * \param[in] bctx BVC Context * \param[in] tag Additional tag to identify acknowledge * \param[in] bucket_size Maximum bucket size in octets * \param[in] bucket_leak_rate Bucket leak rate in octets/sec * \param[in] bmax_default_ms Maximum bucket size default for MS * \param[in] r_default_ms Bucket leak rate default for MS in octets/sec * \param[in] bucket_full_ratio Ratio (in percent) of queue filling * \param[in] queue_delay_ms Average queuing delay in milliseconds */ int bssgp_tx_fc_bvc(struct bssgp_bvc_ctx *bctx, uint8_t tag, uint32_t bucket_size, uint32_t bucket_leak_rate, uint32_t bmax_default_ms, uint32_t r_default_ms, uint8_t *bucket_full_ratio, uint32_t *queue_delay_ms) { struct msgb *msg; struct bssgp_normal_hdr *bgph; uint16_t e_bucket_size, e_leak_rate, e_bmax_default_ms, e_r_default_ms; uint16_t e_queue_delay = 0; /* to make gcc happy */ if ((bucket_size / 100) > 0xffff) return -EINVAL; e_bucket_size = osmo_htons(bucket_size / 100); if ((bucket_leak_rate * 8 / 100) > 0xffff) return -EINVAL; e_leak_rate = osmo_htons((bucket_leak_rate * 8) / 100); if ((bmax_default_ms / 100) > 0xffff) return -EINVAL; e_bmax_default_ms = osmo_htons(bmax_default_ms / 100); if ((r_default_ms * 8 / 100) > 0xffff) return -EINVAL; e_r_default_ms = osmo_htons((r_default_ms * 8) / 100); if (queue_delay_ms) { if ((*queue_delay_ms / 10) > 60000) return -EINVAL; else if (*queue_delay_ms == 0xFFFFFFFF) e_queue_delay = 0xFFFF; else e_queue_delay = osmo_htons(*queue_delay_ms / 10); } msg = bssgp_msgb_alloc(); bgph = (struct bssgp_normal_hdr *) msgb_put(msg, sizeof(*bgph)); msgb_nsei(msg) = bctx->nsei; msgb_bvci(msg) = bctx->bvci; bgph->pdu_type = BSSGP_PDUT_FLOW_CONTROL_BVC; msgb_tvlv_put(msg, BSSGP_IE_TAG, sizeof(tag), (uint8_t *)&tag); msgb_tvlv_put(msg, BSSGP_IE_BVC_BUCKET_SIZE, sizeof(e_bucket_size), (uint8_t *) &e_bucket_size); msgb_tvlv_put(msg, BSSGP_IE_BUCKET_LEAK_RATE, sizeof(e_leak_rate), (uint8_t *) &e_leak_rate); msgb_tvlv_put(msg, BSSGP_IE_BMAX_DEFAULT_MS, sizeof(e_bmax_default_ms), (uint8_t *) &e_bmax_default_ms); msgb_tvlv_put(msg, BSSGP_IE_R_DEFAULT_MS, sizeof(e_r_default_ms), (uint8_t *) &e_r_default_ms); if (bucket_full_ratio) msgb_tvlv_put(msg, BSSGP_IE_BUCKET_FULL_RATIO, 1, bucket_full_ratio); if (queue_delay_ms) msgb_tvlv_put(msg, BSSGP_IE_BVC_MEASUREMENT, sizeof(e_queue_delay), (uint8_t *) &e_queue_delay); return bssgp_ns_send(bssgp_ns_send_data, msg); } /*! Transmit a FLOW_CONTROL-MS (Chapter 10.4.6) * \param[in] bctx BVC Context * \param[in] tlli TLLI to identify MS * \param[in] tag Additional tag to identify acknowledge * \param[in] ms_bucket_size Maximum bucket size in octets * \param[in] bucket_leak_rate Bucket leak rate in octets/sec * \param[in] bucket_full_ratio Ratio (in percent) of queue filling */ int bssgp_tx_fc_ms(struct bssgp_bvc_ctx *bctx, uint32_t tlli, uint8_t tag, uint32_t ms_bucket_size, uint32_t bucket_leak_rate, uint8_t *bucket_full_ratio) { struct msgb *msg; struct bssgp_normal_hdr *bgph; uint16_t e_bucket_size, e_leak_rate; if ((ms_bucket_size / 100) > 0xffff) return -EINVAL; e_bucket_size = ms_bucket_size / 100; if ((bucket_leak_rate * 8 / 100) > 0xffff) return -EINVAL; e_leak_rate = (bucket_leak_rate * 8) / 100; msg = bssgp_msgb_alloc(); bgph = (struct bssgp_normal_hdr *) msgb_put(msg, sizeof(*bgph)); msgb_nsei(msg) = bctx->nsei; msgb_bvci(msg) = bctx->bvci; bgph->pdu_type = BSSGP_PDUT_FLOW_CONTROL_MS; bssgp_msgb_tlli_put(msg, tlli); msgb_tvlv_put(msg, BSSGP_IE_TAG, sizeof(tag), (uint8_t *)&tag); msgb_tvlv_put(msg, BSSGP_IE_MS_BUCKET_SIZE, sizeof(e_bucket_size), (uint8_t *) &e_bucket_size); msgb_tvlv_put(msg, BSSGP_IE_BUCKET_LEAK_RATE, sizeof(e_leak_rate), (uint8_t *) &e_leak_rate); if (bucket_full_ratio) msgb_tvlv_put(msg, BSSGP_IE_BUCKET_FULL_RATIO, 1, bucket_full_ratio); return bssgp_ns_send(bssgp_ns_send_data, msg); } /*! RL-UL-UNITDATA.req (Chapter 10.2.2) * \param[in] bctx BVC Context * \param[in] tlli TLLI to identify MS * \param[in] qos_profile Pointer to three octests of QoS profile * \param[in] llc_pdu msgb pointer containing UL Unitdata IE payload */ int bssgp_tx_ul_ud(struct bssgp_bvc_ctx *bctx, uint32_t tlli, const uint8_t *qos_profile, struct msgb *llc_pdu) { struct msgb *msg = llc_pdu; uint8_t bssgp_cid[8]; uint8_t bssgp_align[3] = {0, 0, 0}; struct bssgp_ud_hdr *budh; int align = sizeof(*budh); /* FIXME: Optional LSA Identifier List, PFI */ /* Cell Identifier */ bssgp_create_cell_id(bssgp_cid, &bctx->ra_id, bctx->cell_id); align += 2; /* add T+L */ align += sizeof(bssgp_cid); /* First push alignment IE */ align += 2; /* add T+L */ align = (4 - align) & 3; /* how many octest are required to align? */ msgb_tvlv_push(msg, BSSGP_IE_ALIGNMENT, align, bssgp_align); /* Push other IEs */ msgb_tvlv_push(msg, BSSGP_IE_CELL_ID, sizeof(bssgp_cid), bssgp_cid); /* User Data Header */ budh = (struct bssgp_ud_hdr *) msgb_push(msg, sizeof(*budh)); budh->tlli = osmo_htonl(tlli); memcpy(budh->qos_profile, qos_profile, 3); budh->pdu_type = BSSGP_PDUT_UL_UNITDATA; /* set NSEI and BVCI in msgb cb */ msgb_nsei(msg) = bctx->nsei; msgb_bvci(msg) = bctx->bvci; rate_ctr_inc(rate_ctr_group_get_ctr(bctx->ctrg, BSSGP_CTR_PKTS_OUT)); rate_ctr_add(rate_ctr_group_get_ctr(bctx->ctrg, BSSGP_CTR_BYTES_OUT), msg->len); return bssgp_ns_send(bssgp_ns_send_data, msg); } /* Parse a single GMM-PAGING.req to a given NSEI/NS-BVCI */ int bssgp_rx_paging(struct bssgp_paging_info *pinfo, struct msgb *msg) { struct bssgp_normal_hdr *bgph = (struct bssgp_normal_hdr *) msgb_bssgph(msg); struct tlv_parsed tp; uint8_t ra[6]; int rc, data_len; memset(ra, 0, sizeof(ra)); data_len = msgb_bssgp_len(msg) - sizeof(*bgph); rc = bssgp_tlv_parse(&tp, bgph->data, data_len); if (rc < 0) goto err_mand_ie; switch (bgph->pdu_type) { case BSSGP_PDUT_PAGING_PS: pinfo->mode = BSSGP_PAGING_PS; break; case BSSGP_PDUT_PAGING_CS: pinfo->mode = BSSGP_PAGING_CS; break; default: return -EINVAL; } /* IMSI */ if (!TLVP_PRESENT(&tp, BSSGP_IE_IMSI)) goto err_mand_ie; if (!pinfo->imsi) pinfo->imsi = talloc_zero_size(pinfo, GSM_IMSI_LENGTH); gsm48_mi_to_string(pinfo->imsi, GSM_IMSI_LENGTH, TLVP_VAL(&tp, BSSGP_IE_IMSI), TLVP_LEN(&tp, BSSGP_IE_IMSI)); /* DRX Parameters */ if (!TLVP_PRES_LEN(&tp, BSSGP_IE_DRX_PARAMS, 2)) goto err_mand_ie; pinfo->drx_params = tlvp_val16be(&tp, BSSGP_IE_DRX_PARAMS); /* Scope */ if (TLVP_PRES_LEN(&tp, BSSGP_IE_BSS_AREA_ID, 1)) { pinfo->scope = BSSGP_PAGING_BSS_AREA; } else if (TLVP_PRES_LEN(&tp, BSSGP_IE_LOCATION_AREA, 5)) { pinfo->scope = BSSGP_PAGING_LOCATION_AREA; memcpy(ra, TLVP_VAL(&tp, BSSGP_IE_LOCATION_AREA), TLVP_LEN(&tp, BSSGP_IE_LOCATION_AREA)); gsm48_parse_ra(&pinfo->raid, ra); } else if (TLVP_PRES_LEN(&tp, BSSGP_IE_ROUTEING_AREA, 6)) { pinfo->scope = BSSGP_PAGING_ROUTEING_AREA; memcpy(ra, TLVP_VAL(&tp, BSSGP_IE_ROUTEING_AREA), TLVP_LEN(&tp, BSSGP_IE_ROUTEING_AREA)); gsm48_parse_ra(&pinfo->raid, ra); } else if (TLVP_PRES_LEN(&tp, BSSGP_IE_BVCI, 2)) { pinfo->scope = BSSGP_PAGING_BVCI; pinfo->bvci = tlvp_val16be(&tp, BSSGP_IE_BVCI); } else return -EINVAL; /* QoS profile mandatory for PS */ if (pinfo->mode == BSSGP_PAGING_PS) { if (!TLVP_PRESENT(&tp, BSSGP_IE_QOS_PROFILE)) goto err_cond_ie; if (TLVP_LEN(&tp, BSSGP_IE_QOS_PROFILE) < 3) goto err; memcpy(&pinfo->qos, TLVP_VAL(&tp, BSSGP_IE_QOS_PROFILE), 3); } /* Optional (P-)TMSI */ if (TLVP_PRES_LEN(&tp, BSSGP_IE_TMSI, 4)) { if (!pinfo->ptmsi) pinfo->ptmsi = talloc_zero_size(pinfo, sizeof(uint32_t)); *(pinfo->ptmsi) = osmo_load32be(TLVP_VAL(&tp, BSSGP_IE_TMSI)); } return 0; err_mand_ie: err_cond_ie: err: /* FIXME */ return 0; }