/*! \file lapdm.c * GSM LAPDm (TS 04.06) implementation. */ /* * (C) 2010-2017 by Harald Welte * (C) 2010-2011 by Andreas Eversberg * (C) 2014-2016 by sysmocom - s.f.m.c GmbH * * 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, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * */ /*! \addtogroup lapdm * @{ * \file lapdm.c */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define LAPD_U_SABM 0x7 /* TS 04.06 Figure 4 / Section 3.2 */ #define LAPDm_LPD_NORMAL 0 #define LAPDm_LPD_SMSCB 1 #define LAPDm_SAPI_NORMAL 0 #define LAPDm_SAPI_SMS 3 #define LAPDm_ADDR(lpd, sapi, cr) ((((lpd) & 0x3) << 5) | (((sapi) & 0x7) << 2) | (((cr) & 0x1) << 1) | 0x1) #define LAPDm_ADDR_LPD(addr) (((addr) >> 5) & 0x3) #define LAPDm_ADDR_SAPI(addr) (((addr) >> 2) & 0x7) #define LAPDm_ADDR_CR(addr) (((addr) >> 1) & 0x1) #define LAPDm_ADDR_EA(addr) ((addr) & 0x1) /* TS 04.06 Table 3 / Section 3.4.3 */ #define LAPDm_CTRL_I(nr, ns, p) ((((nr) & 0x7) << 5) | (((p) & 0x1) << 4) | (((ns) & 0x7) << 1)) #define LAPDm_CTRL_S(nr, s, p) ((((nr) & 0x7) << 5) | (((p) & 0x1) << 4) | (((s) & 0x3) << 2) | 0x1) #define LAPDm_CTRL_U(u, p) ((((u) & 0x1c) << (5-2)) | (((p) & 0x1) << 4) | (((u) & 0x3) << 2) | 0x3) #define LAPDm_CTRL_is_I(ctrl) (((ctrl) & 0x1) == 0) #define LAPDm_CTRL_is_S(ctrl) (((ctrl) & 0x3) == 1) #define LAPDm_CTRL_is_U(ctrl) (((ctrl) & 0x3) == 3) #define LAPDm_CTRL_U_BITS(ctrl) ((((ctrl) & 0xC) >> 2) | ((ctrl) & 0xE0) >> 3) #define LAPDm_CTRL_PF_BIT(ctrl) (((ctrl) >> 4) & 0x1) #define LAPDm_CTRL_S_BITS(ctrl) (((ctrl) & 0xC) >> 2) #define LAPDm_CTRL_I_Ns(ctrl) (((ctrl) & 0xE) >> 1) #define LAPDm_CTRL_Nr(ctrl) (((ctrl) & 0xE0) >> 5) #define LAPDm_LEN(len) ((len << 2) | 0x1) #define LAPDm_MORE 0x2 #define LAPDm_EL 0x1 #define LAPDm_U_UI 0x0 /* TS 04.06 Section 5.8.3 */ #define N201_AB_SACCH 18 #define N201_AB_SDCCH 20 #define N201_AB_FACCH 20 #define N201_Bbis 23 #define N201_Bter_SACCH 21 #define N201_Bter_SDCCH 23 #define N201_Bter_FACCH 23 #define N201_B4 19 /* 5.8.2.1 N200 during establish and release */ #define N200_EST_REL 5 /* 5.8.2.1 N200 during timer recovery state */ #define N200_TR_SACCH 5 #define N200_TR_SDCCH 23 #define N200_TR_FACCH_FR 34 #define N200_TR_EFACCH_FR 48 #define N200_TR_FACCH_HR 29 /* FIXME: set N200 depending on chan_nr */ #define N200 N200_TR_SDCCH enum lapdm_format { LAPDm_FMT_A, LAPDm_FMT_B, LAPDm_FMT_Bbis, LAPDm_FMT_Bter, LAPDm_FMT_B4, }; const struct value_string osmo_ph_prim_names[] = { { PRIM_PH_DATA, "PH-DATA" }, { PRIM_PH_RACH, "PH-RANDOM_ACCESS" }, { PRIM_PH_CONN, "PH-CONNECT" }, { PRIM_PH_EMPTY_FRAME, "PH-EMPTY_FRAME" }, { PRIM_PH_RTS, "PH-RTS" }, { PRIM_MPH_INFO, "MPH-INFO" }, { PRIM_TCH, "TCH" }, { PRIM_TCH_RTS, "TCH-RTS" }, { 0, NULL } }; static int lapdm_send_ph_data_req(struct lapd_msg_ctx *lctx, struct msgb *msg); static int send_rslms_dlsap(struct osmo_dlsap_prim *dp, struct lapd_msg_ctx *lctx); static int update_pending_frames(struct lapd_msg_ctx *lctx); static void lapdm_dl_init(struct lapdm_datalink *dl, struct lapdm_entity *entity, int t200) { memset(dl, 0, sizeof(*dl)); dl->entity = entity; lapd_dl_init(&dl->dl, 1, 8, 200); dl->dl.reestablish = 0; /* GSM uses no reestablish */ dl->dl.send_ph_data_req = lapdm_send_ph_data_req; dl->dl.send_dlsap = send_rslms_dlsap; dl->dl.update_pending_frames = update_pending_frames; dl->dl.n200_est_rel = N200_EST_REL; dl->dl.n200 = N200; dl->dl.t203_sec = 0; dl->dl.t203_usec = 0; dl->dl.t200_sec = t200; dl->dl.t200_usec = 0; } /*! initialize a LAPDm entity and all datalinks inside * \param[in] le LAPDm entity * \param[in] mode \ref lapdm_mode (BTS/MS) */ void lapdm_entity_init(struct lapdm_entity *le, enum lapdm_mode mode, int t200) { unsigned int i; for (i = 0; i < ARRAY_SIZE(le->datalink); i++) lapdm_dl_init(&le->datalink[i], le, t200); lapdm_entity_set_mode(le, mode); } /*! initialize a LAPDm channel and all its channels * \param[in] lc \ref lapdm_channel to be initialized * \param[in] mode \ref lapdm_mode (BTS/MS) * * This really is a convenience wrapper around calling \ref * lapdm_entity_init twice. */ void lapdm_channel_init(struct lapdm_channel *lc, enum lapdm_mode mode) { lapdm_entity_init(&lc->lapdm_acch, mode, 2); lc->lapdm_acch.lapdm_ch = lc; /* FIXME: this depends on chan type */ lapdm_entity_init(&lc->lapdm_dcch, mode, 1); lc->lapdm_dcch.lapdm_ch = lc; } /*! flush and release all resoures in LAPDm entity */ void lapdm_entity_exit(struct lapdm_entity *le) { unsigned int i; struct lapdm_datalink *dl; for (i = 0; i < ARRAY_SIZE(le->datalink); i++) { dl = &le->datalink[i]; lapd_dl_exit(&dl->dl); } } /* lfush and release all resources in LAPDm channel * * A convenience wrapper calling \ref lapdm_entity_exit on both * entities inside the \ref lapdm_channel */ void lapdm_channel_exit(struct lapdm_channel *lc) { lapdm_entity_exit(&lc->lapdm_acch); lapdm_entity_exit(&lc->lapdm_dcch); } struct lapdm_datalink *lapdm_datalink_for_sapi(struct lapdm_entity *le, uint8_t sapi) { switch (sapi) { case LAPDm_SAPI_NORMAL: return &le->datalink[0]; case LAPDm_SAPI_SMS: return &le->datalink[1]; default: return NULL; } } /* Append padding (if required) */ static void lapdm_pad_msgb(struct msgb *msg, uint8_t n201) { int pad_len = n201 - msgb_l2len(msg); uint8_t *data; if (pad_len < 0) { LOGP(DLLAPD, LOGL_ERROR, "cannot pad message that is already too big!\n"); return; } data = msgb_put(msg, pad_len); memset(data, 0x2B, pad_len); } /* input function that L2 calls when sending messages up to L3 */ static int rslms_sendmsg(struct msgb *msg, struct lapdm_entity *le) { if (!le->l3_cb) { msgb_free(msg); return -EIO; } /* call the layer2 message handler that is registered */ return le->l3_cb(msg, le, le->l3_ctx); } /* write a frame into the tx queue */ static int tx_ph_data_enqueue(struct lapdm_datalink *dl, struct msgb *msg, uint8_t chan_nr, uint8_t link_id, uint8_t pad) { struct lapdm_entity *le = dl->entity; struct osmo_phsap_prim pp; /* if there is a pending message, queue it */ if (le->tx_pending || le->flags & LAPDM_ENT_F_POLLING_ONLY) { *msgb_push(msg, 1) = pad; *msgb_push(msg, 1) = link_id; *msgb_push(msg, 1) = chan_nr; msgb_enqueue(&dl->dl.tx_queue, msg); return -EBUSY; } osmo_prim_init(&pp.oph, SAP_GSM_PH, PRIM_PH_DATA, PRIM_OP_REQUEST, msg); pp.u.data.chan_nr = chan_nr; pp.u.data.link_id = link_id; /* send the frame now */ le->tx_pending = 0; /* disabled flow control */ lapdm_pad_msgb(msg, pad); return le->l1_prim_cb(&pp.oph, le->l1_ctx); } static struct msgb *tx_dequeue_msgb(struct lapdm_entity *le) { struct lapdm_datalink *dl; int last = le->last_tx_dequeue; int i = last, n = ARRAY_SIZE(le->datalink); struct msgb *msg = NULL; /* round-robin dequeue */ do { /* next */ i = (i + 1) % n; dl = &le->datalink[i]; if ((msg = msgb_dequeue(&dl->dl.tx_queue))) break; } while (i != last); if (msg) { /* Set last dequeue position */ le->last_tx_dequeue = i; } return msg; } /*! dequeue a msg that's pending transmission via L1 and wrap it into * a osmo_phsap_prim */ int lapdm_phsap_dequeue_prim(struct lapdm_entity *le, struct osmo_phsap_prim *pp) { struct msgb *msg; uint8_t pad; msg = tx_dequeue_msgb(le); if (!msg) return -ENODEV; /* if we have a message, send PH-DATA.req */ osmo_prim_init(&pp->oph, SAP_GSM_PH, PRIM_PH_DATA, PRIM_OP_REQUEST, msg); /* Pull chan_nr and link_id */ pp->u.data.chan_nr = *msg->data; msgb_pull(msg, 1); pp->u.data.link_id = *msg->data; msgb_pull(msg, 1); pad = *msg->data; msgb_pull(msg, 1); /* Pad the frame, we can transmit now */ lapdm_pad_msgb(msg, pad); return 0; } /* get next frame from the tx queue. because the ms has multiple datalinks, * each datalink's queue is read round-robin. */ static int l2_ph_data_conf(struct msgb *msg, struct lapdm_entity *le) { struct osmo_phsap_prim pp; /* we may send again */ le->tx_pending = 0; /* free confirm message */ if (msg) msgb_free(msg); if (lapdm_phsap_dequeue_prim(le, &pp) < 0) { /* no message in all queues */ /* If user didn't request PH-EMPTY_FRAME.req, abort */ if (!(le->flags & LAPDM_ENT_F_EMPTY_FRAME)) return 0; /* otherwise, send PH-EMPTY_FRAME.req */ osmo_prim_init(&pp.oph, SAP_GSM_PH, PRIM_PH_EMPTY_FRAME, PRIM_OP_REQUEST, NULL); } else { le->tx_pending = 1; } return le->l1_prim_cb(&pp.oph, le->l1_ctx); } /* Is a given msg_type "transparent" as per TS 48.058 Section 8.1 */ static int rsl_is_transparent(uint8_t msg_type) { switch (msg_type) { case RSL_MT_DATA_IND: case RSL_MT_UNIT_DATA_IND: return 1; case RSL_MT_DATA_REQ: case RSL_MT_UNIT_DATA_REQ: return 1; default: return 0; } } /* Create RSLms various RSLms messages */ static int send_rslms_rll_l3(uint8_t msg_type, struct lapdm_msg_ctx *mctx, struct msgb *msg) { int transparent = rsl_is_transparent(msg_type); /* Add the RSL + RLL header */ rsl_rll_push_l3(msg, msg_type, mctx->chan_nr, mctx->link_id, transparent); /* send off the RSLms message to L3 */ return rslms_sendmsg(msg, mctx->dl->entity); } /* Take a B4 format message from L1 and create RSLms UNIT DATA IND */ static int send_rslms_rll_l3_ui(struct lapdm_msg_ctx *mctx, struct msgb *msg) { uint8_t l3_len = msg->tail - (uint8_t *)msgb_l3(msg); /* Add the RSL + RLL header */ msgb_tv16_push(msg, RSL_IE_L3_INFO, l3_len); /* Add two non-standard IEs carrying MS power and TA values for B4 (SACCH) */ if (mctx->lapdm_fmt == LAPDm_FMT_B4) { msgb_tv_push(msg, RSL_IE_MS_POWER, mctx->tx_power_ind); msgb_tv_push(msg, RSL_IE_TIMING_ADVANCE, mctx->ta_ind); } rsl_rll_push_hdr(msg, RSL_MT_UNIT_DATA_IND, mctx->chan_nr, mctx->link_id, 1); return rslms_sendmsg(msg, mctx->dl->entity); } static int send_rll_simple(uint8_t msg_type, struct lapdm_msg_ctx *mctx) { struct msgb *msg; int transparent = rsl_is_transparent(msg_type); msg = rsl_rll_simple(msg_type, mctx->chan_nr, mctx->link_id, transparent); /* send off the RSLms message to L3 */ return rslms_sendmsg(msg, mctx->dl->entity); } static int rsl_rll_error(uint8_t cause, struct lapdm_msg_ctx *mctx) { struct msgb *msg; LOGP(DLLAPD, LOGL_NOTICE, "sending MDL-ERROR-IND %d\n", cause); msg = rsl_rll_simple(RSL_MT_ERROR_IND, mctx->chan_nr, mctx->link_id, 0); msgb_tlv_put(msg, RSL_IE_RLM_CAUSE, 1, &cause); return rslms_sendmsg(msg, mctx->dl->entity); } /* DLSAP L2 -> L3 (RSLms) */ static int send_rslms_dlsap(struct osmo_dlsap_prim *dp, struct lapd_msg_ctx *lctx) { struct lapd_datalink *dl = lctx->dl; struct lapdm_datalink *mdl = container_of(dl, struct lapdm_datalink, dl); struct lapdm_msg_ctx *mctx = &mdl->mctx; uint8_t rll_msg = 0; switch (OSMO_PRIM_HDR(&dp->oph)) { case OSMO_PRIM(PRIM_DL_EST, PRIM_OP_INDICATION): rll_msg = RSL_MT_EST_IND; break; case OSMO_PRIM(PRIM_DL_EST, PRIM_OP_CONFIRM): rll_msg = RSL_MT_EST_CONF; break; case OSMO_PRIM(PRIM_DL_DATA, PRIM_OP_INDICATION): rll_msg = RSL_MT_DATA_IND; break; case OSMO_PRIM(PRIM_DL_UNIT_DATA, PRIM_OP_INDICATION): return send_rslms_rll_l3_ui(mctx, dp->oph.msg); case OSMO_PRIM(PRIM_DL_REL, PRIM_OP_INDICATION): rll_msg = RSL_MT_REL_IND; break; case OSMO_PRIM(PRIM_DL_REL, PRIM_OP_CONFIRM): rll_msg = RSL_MT_REL_CONF; break; case OSMO_PRIM(PRIM_DL_SUSP, PRIM_OP_CONFIRM): rll_msg = RSL_MT_SUSP_CONF; break; case OSMO_PRIM(PRIM_MDL_ERROR, PRIM_OP_INDICATION): rsl_rll_error(dp->u.error_ind.cause, mctx); if (dp->oph.msg) msgb_free(dp->oph.msg); return 0; } if (!rll_msg) { LOGP(DLLAPD, LOGL_ERROR, "Unsupported op %d, prim %d. Please " "fix!\n", dp->oph.primitive, dp->oph.operation); return -EINVAL; } if (!dp->oph.msg) return send_rll_simple(rll_msg, mctx); return send_rslms_rll_l3(rll_msg, mctx, dp->oph.msg); } /* send a data frame to layer 1 */ static int lapdm_send_ph_data_req(struct lapd_msg_ctx *lctx, struct msgb *msg) { uint8_t l3_len = msg->tail - msg->data; struct lapd_datalink *dl = lctx->dl; struct lapdm_datalink *mdl = container_of(dl, struct lapdm_datalink, dl); struct lapdm_msg_ctx *mctx = &mdl->mctx; int format = lctx->format; /* prepend l2 header */ msg->l2h = msgb_push(msg, 3); msg->l2h[0] = LAPDm_ADDR(lctx->lpd, lctx->sapi, lctx->cr); /* EA is set here too */ switch (format) { case LAPD_FORM_I: msg->l2h[1] = LAPDm_CTRL_I(lctx->n_recv, lctx->n_send, lctx->p_f); break; case LAPD_FORM_S: msg->l2h[1] = LAPDm_CTRL_S(lctx->n_recv, lctx->s_u, lctx->p_f); break; case LAPD_FORM_U: msg->l2h[1] = LAPDm_CTRL_U(lctx->s_u, lctx->p_f); break; default: msgb_free(msg); return -EINVAL; } msg->l2h[2] = LAPDm_LEN(l3_len); /* EL is set here too */ if (lctx->more) msg->l2h[2] |= LAPDm_MORE; /* add ACCH header with last indicated tx-power and TA */ if ((mctx->link_id & 0x40)) { struct lapdm_entity *le = mdl->entity; msg->l2h = msgb_push(msg, 2); msg->l2h[0] = le->tx_power; msg->l2h[1] = le->ta; } return tx_ph_data_enqueue(mctx->dl, msg, mctx->chan_nr, mctx->link_id, 23); } static int update_pending_frames(struct lapd_msg_ctx *lctx) { struct lapd_datalink *dl = lctx->dl; struct msgb *msg; int rc = -1; llist_for_each_entry(msg, &dl->tx_queue, list) { if (LAPDm_CTRL_is_I(msg->l2h[1])) { msg->l2h[1] = LAPDm_CTRL_I(dl->v_recv, LAPDm_CTRL_I_Ns(msg->l2h[1]), LAPDm_CTRL_PF_BIT(msg->l2h[1])); rc = 0; } else if (LAPDm_CTRL_is_S(msg->l2h[1])) { LOGP(DLLAPD, LOGL_ERROR, "Supervisory frame in queue, this shouldn't happen\n"); } } return rc; } /* determine if receiving a given LAPDm message is not permitted */ static int lapdm_rx_not_permitted(const struct lapdm_entity *le, const struct lapd_msg_ctx *lctx) { /* we currently only implement SABM related checks here */ if (lctx->format != LAPD_FORM_U || lctx->s_u != LAPD_U_SABM) return 0; if (le->mode == LAPDM_MODE_BTS) { if (le == &le->lapdm_ch->lapdm_acch) { /* no contention resolution on SACCH */ if (lctx->length > 0) return RLL_CAUSE_SABM_INFO_NOTALL; } else { switch (lctx->sapi) { case 3: /* SAPI3 doesn't support contention resolution */ if (lctx->length > 0) return RLL_CAUSE_SABM_INFO_NOTALL; break; default: break; } } } else if (le->mode == LAPDM_MODE_MS) { /* contention resolution (L3 present) is only sent by MS, but * never received by it */ if (lctx->length > 0) return RLL_CAUSE_SABM_INFO_NOTALL; } return 0; } /* input into layer2 (from layer 1) */ static int l2_ph_data_ind(struct msgb *msg, struct lapdm_entity *le, uint8_t chan_nr, uint8_t link_id) { uint8_t cbits = chan_nr >> 3; uint8_t sapi; /* we cannot take SAPI from link_id, as L1 has no clue */ struct lapdm_msg_ctx mctx; struct lapd_msg_ctx lctx; int rc = 0; int n201; /* when we reach here, we have a msgb with l2h pointing to the raw * 23byte mac block. The l1h has already been purged. */ memset(&mctx, 0, sizeof(mctx)); mctx.chan_nr = chan_nr; mctx.link_id = link_id; /* check for L1 chan_nr/link_id and determine LAPDm hdr format */ if (cbits == 0x10 || cbits == 0x12) { /* Format Bbis is used on BCCH and CCCH(PCH, NCH and AGCH) */ mctx.lapdm_fmt = LAPDm_FMT_Bbis; n201 = N201_Bbis; sapi = 0; } else { if (mctx.link_id & 0x40) { /* It was received from network on SACCH */ /* If UI on SACCH sent by BTS, lapdm_fmt must be B4 */ if (le->mode == LAPDM_MODE_MS && LAPDm_CTRL_is_U(msg->l2h[3]) && LAPDm_CTRL_U_BITS(msg->l2h[3]) == 0) { mctx.lapdm_fmt = LAPDm_FMT_B4; n201 = N201_B4; LOGP(DLLAPD, LOGL_INFO, "fmt=B4\n"); } else { mctx.lapdm_fmt = LAPDm_FMT_B; n201 = N201_AB_SACCH; LOGP(DLLAPD, LOGL_INFO, "fmt=B\n"); } /* SACCH frames have a two-byte L1 header that * OsmocomBB L1 doesn't strip */ mctx.tx_power_ind = msg->l2h[0] & 0x1f; mctx.ta_ind = msg->l2h[1]; msgb_pull(msg, 2); msg->l2h += 2; sapi = (msg->l2h[0] >> 2) & 7; } else { mctx.lapdm_fmt = LAPDm_FMT_B; LOGP(DLLAPD, LOGL_INFO, "fmt=B\n"); n201 = N201_AB_SDCCH; sapi = (msg->l2h[0] >> 2) & 7; } } mctx.dl = lapdm_datalink_for_sapi(le, sapi); /* G.2.1 No action on frames containing an unallocated SAPI. */ if (!mctx.dl) { LOGP(DLLAPD, LOGL_NOTICE, "Received frame for unsupported " "SAPI %d!\n", sapi); msgb_free(msg); return -EIO; } switch (mctx.lapdm_fmt) { case LAPDm_FMT_A: case LAPDm_FMT_B: case LAPDm_FMT_B4: lctx.dl = &mctx.dl->dl; /* obtain SAPI from address field */ mctx.link_id |= LAPDm_ADDR_SAPI(msg->l2h[0]); /* G.2.3 EA bit set to "0" is not allowed in GSM */ if (!LAPDm_ADDR_EA(msg->l2h[0])) { LOGP(DLLAPD, LOGL_NOTICE, "EA bit 0 is not allowed in " "GSM\n"); msgb_free(msg); rsl_rll_error(RLL_CAUSE_FRM_UNIMPL, &mctx); return -EINVAL; } /* adress field */ lctx.lpd = LAPDm_ADDR_LPD(msg->l2h[0]); lctx.sapi = LAPDm_ADDR_SAPI(msg->l2h[0]); lctx.cr = LAPDm_ADDR_CR(msg->l2h[0]); /* command field */ if (LAPDm_CTRL_is_I(msg->l2h[1])) { lctx.format = LAPD_FORM_I; lctx.n_send = LAPDm_CTRL_I_Ns(msg->l2h[1]); lctx.n_recv = LAPDm_CTRL_Nr(msg->l2h[1]); } else if (LAPDm_CTRL_is_S(msg->l2h[1])) { lctx.format = LAPD_FORM_S; lctx.n_recv = LAPDm_CTRL_Nr(msg->l2h[1]); lctx.s_u = LAPDm_CTRL_S_BITS(msg->l2h[1]); } else if (LAPDm_CTRL_is_U(msg->l2h[1])) { lctx.format = LAPD_FORM_U; lctx.s_u = LAPDm_CTRL_U_BITS(msg->l2h[1]); } else lctx.format = LAPD_FORM_UKN; lctx.p_f = LAPDm_CTRL_PF_BIT(msg->l2h[1]); if (lctx.sapi != LAPDm_SAPI_NORMAL && lctx.sapi != LAPDm_SAPI_SMS && lctx.format == LAPD_FORM_U && lctx.s_u == LAPDm_U_UI) { /* 5.3.3 UI frames with invalid SAPI values shall be * discarded */ LOGP(DLLAPD, LOGL_INFO, "sapi=%u (discarding)\n", lctx.sapi); msgb_free(msg); return 0; } if (mctx.lapdm_fmt == LAPDm_FMT_B4) { lctx.n201 = n201; lctx.length = n201; lctx.more = 0; msg->l3h = msg->l2h + 2; msgb_pull_to_l3(msg); } else { /* length field */ if (!(msg->l2h[2] & LAPDm_EL)) { /* G.4.1 If the EL bit is set to "0", an * MDL-ERROR-INDICATION primitive with cause * "frame not implemented" is sent to the * mobile management entity. */ LOGP(DLLAPD, LOGL_NOTICE, "we don't support " "multi-octet length\n"); msgb_free(msg); rsl_rll_error(RLL_CAUSE_FRM_UNIMPL, &mctx); return -EINVAL; } lctx.n201 = n201; lctx.length = msg->l2h[2] >> 2; lctx.more = !!(msg->l2h[2] & LAPDm_MORE); msg->l3h = msg->l2h + 3; msgb_pull_to_l3(msg); } /* store context for messages from lapd */ memcpy(&mctx.dl->mctx, &mctx, sizeof(mctx.dl->mctx)); rc =lapdm_rx_not_permitted(le, &lctx); if (rc > 0) { LOGP(DLLAPD, LOGL_NOTICE, "received message not permitted\n"); msgb_free(msg); rsl_rll_error(rc, &mctx); return -EINVAL; } /* send to LAPD */ rc = lapd_ph_data_ind(msg, &lctx); break; case LAPDm_FMT_Bter: /* FIXME */ msgb_free(msg); break; case LAPDm_FMT_Bbis: /* directly pass up to layer3 */ LOGP(DLLAPD, LOGL_INFO, "fmt=Bbis UI\n"); msg->l3h = msg->l2h; msgb_pull_to_l3(msg); rc = send_rslms_rll_l3(RSL_MT_UNIT_DATA_IND, &mctx, msg); break; default: msgb_free(msg); } return rc; } /* input into layer2 (from layer 1) */ static int l2_ph_rach_ind(struct lapdm_entity *le, uint8_t ra, uint32_t fn, uint8_t acc_delay) { struct abis_rsl_cchan_hdr *ch; struct gsm48_req_ref req_ref; struct gsm_time gt; struct msgb *msg = msgb_alloc_headroom(512, 64, "RSL CHAN RQD"); if (!msg) return -ENOMEM; msg->l2h = msgb_push(msg, sizeof(*ch)); ch = (struct abis_rsl_cchan_hdr *)msg->l2h; rsl_init_cchan_hdr(ch, RSL_MT_CHAN_RQD); ch->chan_nr = RSL_CHAN_RACH; /* generate a RSL CHANNEL REQUIRED message */ gsm_fn2gsmtime(>, fn); req_ref.ra = ra; req_ref.t1 = gt.t1; /* FIXME: modulo? */ req_ref.t2 = gt.t2; req_ref.t3_low = gt.t3 & 7; req_ref.t3_high = gt.t3 >> 3; msgb_tv_fixed_put(msg, RSL_IE_REQ_REFERENCE, 3, (uint8_t *) &req_ref); msgb_tv_put(msg, RSL_IE_ACCESS_DELAY, acc_delay); return rslms_sendmsg(msg, le); } static int l2_ph_chan_conf(struct msgb *msg, struct lapdm_entity *le, uint32_t frame_nr); /*! Receive a PH-SAP primitive from L1 */ int lapdm_phsap_up(struct osmo_prim_hdr *oph, struct lapdm_entity *le) { struct osmo_phsap_prim *pp = (struct osmo_phsap_prim *) oph; int rc = 0; if (oph->sap != SAP_GSM_PH) { LOGP(DLLAPD, LOGL_ERROR, "primitive for unknown SAP %u\n", oph->sap); rc = -ENODEV; goto free; } switch (oph->primitive) { case PRIM_PH_DATA: if (oph->operation != PRIM_OP_INDICATION) { LOGP(DLLAPD, LOGL_ERROR, "PH_DATA is not INDICATION %u\n", oph->operation); rc = -ENODEV; goto free; } rc = l2_ph_data_ind(oph->msg, le, pp->u.data.chan_nr, pp->u.data.link_id); break; case PRIM_PH_RTS: if (oph->operation != PRIM_OP_INDICATION) { LOGP(DLLAPD, LOGL_ERROR, "PH_RTS is not INDICATION %u\n", oph->operation); rc = -ENODEV; goto free; } rc = l2_ph_data_conf(oph->msg, le); break; case PRIM_PH_RACH: switch (oph->operation) { case PRIM_OP_INDICATION: rc = l2_ph_rach_ind(le, pp->u.rach_ind.ra, pp->u.rach_ind.fn, pp->u.rach_ind.acc_delay); break; case PRIM_OP_CONFIRM: rc = l2_ph_chan_conf(oph->msg, le, pp->u.rach_ind.fn); break; default: rc = -EIO; goto free; } break; default: LOGP(DLLAPD, LOGL_ERROR, "Unknown primitive %u\n", oph->primitive); rc = -EINVAL; goto free; } return rc; free: msgb_free(oph->msg); return rc; } /* L3 -> L2 / RSLMS -> LAPDm */ /* Set LAPDm context for established connection */ static int set_lapdm_context(struct lapdm_datalink *dl, uint8_t chan_nr, uint8_t link_id, int n201, uint8_t sapi) { memset(&dl->mctx, 0, sizeof(dl->mctx)); dl->mctx.dl = dl; dl->mctx.chan_nr = chan_nr; dl->mctx.link_id = link_id; dl->dl.lctx.dl = &dl->dl; dl->dl.lctx.n201 = n201; dl->dl.lctx.sapi = sapi; return 0; } /* L3 requests establishment of data link */ static int rslms_rx_rll_est_req(struct msgb *msg, struct lapdm_datalink *dl) { struct abis_rsl_rll_hdr *rllh = msgb_l2(msg); uint8_t chan_nr = rllh->chan_nr; uint8_t link_id = rllh->link_id; uint8_t sapi = rllh->link_id & 7; struct tlv_parsed tv; uint8_t length; uint8_t n201 = (rllh->link_id & 0x40) ? N201_AB_SACCH : N201_AB_SDCCH; struct osmo_dlsap_prim dp; /* Set LAPDm context for established connection */ set_lapdm_context(dl, chan_nr, link_id, n201, sapi); rsl_tlv_parse(&tv, rllh->data, msgb_l2len(msg) - sizeof(*rllh)); if (TLVP_PRESENT(&tv, RSL_IE_L3_INFO)) { msg->l3h = (uint8_t *) TLVP_VAL(&tv, RSL_IE_L3_INFO); /* contention resolution establishment procedure */ if (sapi != 0) { /* According to clause 6, the contention resolution * procedure is only permitted with SAPI value 0 */ LOGP(DLLAPD, LOGL_ERROR, "SAPI != 0 but contention" "resolution (discarding)\n"); msgb_free(msg); return send_rll_simple(RSL_MT_REL_IND, &dl->mctx); } /* transmit a SABM command with the P bit set to "1". The SABM * command shall contain the layer 3 message unit */ length = TLVP_LEN(&tv, RSL_IE_L3_INFO); } else { /* normal establishment procedure */ msg->l3h = msg->l2h + sizeof(*rllh); length = 0; } /* check if the layer3 message length exceeds N201 */ if (length > n201) { LOGP(DLLAPD, LOGL_ERROR, "frame too large: %d > N201(%d) " "(discarding)\n", length, n201); msgb_free(msg); return send_rll_simple(RSL_MT_REL_IND, &dl->mctx); } /* Remove RLL header from msgb and set length to L3-info */ msgb_pull_to_l3(msg); msgb_trim(msg, length); /* prepare prim */ osmo_prim_init(&dp.oph, 0, PRIM_DL_EST, PRIM_OP_REQUEST, msg); /* send to L2 */ return lapd_recv_dlsap(&dp, &dl->dl.lctx); } /* L3 requests transfer of unnumbered information */ static int rslms_rx_rll_udata_req(struct msgb *msg, struct lapdm_datalink *dl) { struct lapdm_entity *le = dl->entity; struct abis_rsl_rll_hdr *rllh = msgb_l2(msg); uint8_t chan_nr = rllh->chan_nr; uint8_t link_id = rllh->link_id; uint8_t sapi = link_id & 7; struct tlv_parsed tv; int length, ui_bts; if (!le) { LOGP(DLLAPD, LOGL_ERROR, "lapdm_datalink without entity error\n"); msgb_free(msg); return -EMLINK; } ui_bts = (le->mode == LAPDM_MODE_BTS && (link_id & 0x40)); /* check if the layer3 message length exceeds N201 */ rsl_tlv_parse(&tv, rllh->data, msgb_l2len(msg)-sizeof(*rllh)); if (TLVP_PRESENT(&tv, RSL_IE_TIMING_ADVANCE)) { le->ta = *TLVP_VAL(&tv, RSL_IE_TIMING_ADVANCE); } if (TLVP_PRESENT(&tv, RSL_IE_MS_POWER)) { le->tx_power = *TLVP_VAL(&tv, RSL_IE_MS_POWER); } if (!TLVP_PRESENT(&tv, RSL_IE_L3_INFO)) { LOGP(DLLAPD, LOGL_ERROR, "unit data request without message " "error\n"); msgb_free(msg); return -EINVAL; } msg->l3h = (uint8_t *) TLVP_VAL(&tv, RSL_IE_L3_INFO); length = TLVP_LEN(&tv, RSL_IE_L3_INFO); /* check if the layer3 message length exceeds N201 */ if (length + ((link_id & 0x40) ? 4 : 2) + !ui_bts > 23) { LOGP(DLLAPD, LOGL_ERROR, "frame too large: %d > N201(%d) " "(discarding)\n", length, ((link_id & 0x40) ? 18 : 20) + ui_bts); msgb_free(msg); return -EIO; } LOGP(DLLAPD, LOGL_INFO, "sending unit data (tx_power=%d, ta=%d)\n", le->tx_power, le->ta); /* Remove RLL header from msgb and set length to L3-info */ msgb_pull_to_l3(msg); msgb_trim(msg, length); /* Push L1 + LAPDm header on msgb */ msg->l2h = msgb_push(msg, 2 + !ui_bts); msg->l2h[0] = LAPDm_ADDR(LAPDm_LPD_NORMAL, sapi, dl->dl.cr.loc2rem.cmd); msg->l2h[1] = LAPDm_CTRL_U(LAPDm_U_UI, 0); if (!ui_bts) msg->l2h[2] = LAPDm_LEN(length); if (link_id & 0x40) { msg->l2h = msgb_push(msg, 2); msg->l2h[0] = le->tx_power; msg->l2h[1] = le->ta; } /* Tramsmit */ return tx_ph_data_enqueue(dl, msg, chan_nr, link_id, 23); } /* L3 requests transfer of acknowledged information */ static int rslms_rx_rll_data_req(struct msgb *msg, struct lapdm_datalink *dl) { struct abis_rsl_rll_hdr *rllh = msgb_l2(msg); struct tlv_parsed tv; int length; struct osmo_dlsap_prim dp; rsl_tlv_parse(&tv, rllh->data, msgb_l2len(msg)-sizeof(*rllh)); if (!TLVP_PRESENT(&tv, RSL_IE_L3_INFO)) { LOGP(DLLAPD, LOGL_ERROR, "data request without message " "error\n"); msgb_free(msg); return -EINVAL; } msg->l3h = (uint8_t *) TLVP_VAL(&tv, RSL_IE_L3_INFO); length = TLVP_LEN(&tv, RSL_IE_L3_INFO); /* Remove RLL header from msgb and set length to L3-info */ msgb_pull_to_l3(msg); msgb_trim(msg, length); /* prepare prim */ osmo_prim_init(&dp.oph, 0, PRIM_DL_DATA, PRIM_OP_REQUEST, msg); /* send to L2 */ return lapd_recv_dlsap(&dp, &dl->dl.lctx); } /* L3 requests suspension of data link */ static int rslms_rx_rll_susp_req(struct msgb *msg, struct lapdm_datalink *dl) { struct abis_rsl_rll_hdr *rllh = msgb_l2(msg); uint8_t sapi = rllh->link_id & 7; struct osmo_dlsap_prim dp; if (sapi != 0) { LOGP(DLLAPD, LOGL_ERROR, "SAPI != 0 while suspending\n"); msgb_free(msg); return -EINVAL; } /* prepare prim */ osmo_prim_init(&dp.oph, 0, PRIM_DL_SUSP, PRIM_OP_REQUEST, msg); /* send to L2 */ return lapd_recv_dlsap(&dp, &dl->dl.lctx); } /* L3 requests resume of data link */ static int rslms_rx_rll_res_req(struct msgb *msg, struct lapdm_datalink *dl) { struct abis_rsl_rll_hdr *rllh = msgb_l2(msg); int msg_type = rllh->c.msg_type; uint8_t chan_nr = rllh->chan_nr; uint8_t link_id = rllh->link_id; uint8_t sapi = rllh->link_id & 7; struct tlv_parsed tv; uint8_t length; uint8_t n201 = (rllh->link_id & 0x40) ? N201_AB_SACCH : N201_AB_SDCCH; struct osmo_dlsap_prim dp; /* Set LAPDm context for established connection */ set_lapdm_context(dl, chan_nr, link_id, n201, sapi); rsl_tlv_parse(&tv, rllh->data, msgb_l2len(msg)-sizeof(*rllh)); if (!TLVP_PRESENT(&tv, RSL_IE_L3_INFO)) { LOGP(DLLAPD, LOGL_ERROR, "resume without message error\n"); msgb_free(msg); return send_rll_simple(RSL_MT_REL_IND, &dl->mctx); } msg->l3h = (uint8_t *) TLVP_VAL(&tv, RSL_IE_L3_INFO); length = TLVP_LEN(&tv, RSL_IE_L3_INFO); /* Remove RLL header from msgb and set length to L3-info */ msgb_pull_to_l3(msg); msgb_trim(msg, length); /* prepare prim */ osmo_prim_init(&dp.oph, 0, (msg_type == RSL_MT_RES_REQ) ? PRIM_DL_RES : PRIM_DL_RECON, PRIM_OP_REQUEST, msg); /* send to L2 */ return lapd_recv_dlsap(&dp, &dl->dl.lctx); } /* L3 requests release of data link */ static int rslms_rx_rll_rel_req(struct msgb *msg, struct lapdm_datalink *dl) { struct abis_rsl_rll_hdr *rllh = msgb_l2(msg); uint8_t mode = 0; struct osmo_dlsap_prim dp; /* get release mode */ if (rllh->data[0] == RSL_IE_RELEASE_MODE) mode = rllh->data[1] & 1; /* Pull rllh */ msgb_pull_to_l3(msg); /* 04.06 3.8.3: No information field is permitted with the DISC * command. */ msgb_trim(msg, 0); /* prepare prim */ osmo_prim_init(&dp.oph, 0, PRIM_DL_REL, PRIM_OP_REQUEST, msg); dp.u.rel_req.mode = mode; /* send to L2 */ return lapd_recv_dlsap(&dp, &dl->dl.lctx); } /* L3 requests channel in idle state */ static int rslms_rx_chan_rqd(struct lapdm_channel *lc, struct msgb *msg) { struct abis_rsl_cchan_hdr *cch = msgb_l2(msg); void *l1ctx = lc->lapdm_dcch.l1_ctx; struct osmo_phsap_prim pp; osmo_prim_init(&pp.oph, SAP_GSM_PH, PRIM_PH_RACH, PRIM_OP_REQUEST, NULL); if (msgb_l2len(msg) < sizeof(*cch) + 4 + 2 + 2) { LOGP(DLLAPD, LOGL_ERROR, "Message too short for CHAN RQD!\n"); return -EINVAL; } if (cch->data[0] != RSL_IE_REQ_REFERENCE) { LOGP(DLLAPD, LOGL_ERROR, "Missing REQ REFERENCE IE\n"); return -EINVAL; } pp.u.rach_req.ra = cch->data[1]; pp.u.rach_req.offset = ((cch->data[2] & 0x7f) << 8) | cch->data[3]; pp.u.rach_req.is_combined_ccch = cch->data[2] >> 7; if (cch->data[4] != RSL_IE_ACCESS_DELAY) { LOGP(DLLAPD, LOGL_ERROR, "Missing ACCESS_DELAY IE\n"); return -EINVAL; } /* TA = 0 - delay */ pp.u.rach_req.ta = 0 - cch->data[5]; if (cch->data[6] != RSL_IE_MS_POWER) { LOGP(DLLAPD, LOGL_ERROR, "Missing MS POWER IE\n"); return -EINVAL; } pp.u.rach_req.tx_power = cch->data[7]; msgb_free(msg); return lc->lapdm_dcch.l1_prim_cb(&pp.oph, l1ctx); } /* L1 confirms channel request */ static int l2_ph_chan_conf(struct msgb *msg, struct lapdm_entity *le, uint32_t frame_nr) { struct abis_rsl_cchan_hdr *ch; struct gsm_time tm; struct gsm48_req_ref *ref; gsm_fn2gsmtime(&tm, frame_nr); msgb_pull_to_l3(msg); msg->l2h = msgb_push(msg, sizeof(*ch) + sizeof(*ref)); ch = (struct abis_rsl_cchan_hdr *)msg->l2h; rsl_init_cchan_hdr(ch, RSL_MT_CHAN_CONF); ch->chan_nr = RSL_CHAN_RACH; ch->data[0] = RSL_IE_REQ_REFERENCE; ref = (struct gsm48_req_ref *) (ch->data + 1); ref->t1 = tm.t1; ref->t2 = tm.t2; ref->t3_low = tm.t3 & 0x7; ref->t3_high = tm.t3 >> 3; return rslms_sendmsg(msg, le); } /* incoming RSLms RLL message from L3 */ static int rslms_rx_rll(struct msgb *msg, struct lapdm_channel *lc) { struct abis_rsl_rll_hdr *rllh = msgb_l2(msg); int msg_type = rllh->c.msg_type; uint8_t sapi = rllh->link_id & 7; struct lapdm_entity *le; struct lapdm_datalink *dl; int rc = 0; if (msgb_l2len(msg) < sizeof(*rllh)) { LOGP(DLLAPD, LOGL_ERROR, "Message too short for RLL hdr!\n"); msgb_free(msg); return -EINVAL; } if (rllh->link_id & 0x40) le = &lc->lapdm_acch; else le = &lc->lapdm_dcch; /* 4.1.1.5 / 4.1.1.6 / 4.1.1.7 all only exist on MS side, not * BTS side */ if (le->mode == LAPDM_MODE_BTS) { switch (msg_type) { case RSL_MT_SUSP_REQ: case RSL_MT_RES_REQ: case RSL_MT_RECON_REQ: LOGP(DLLAPD, LOGL_NOTICE, "(%p) RLL Message '%s' unsupported in BTS side LAPDm\n", lc->name, rsl_msg_name(msg_type)); msgb_free(msg); return -EINVAL; break; default: break; } } /* G.2.1 No action shall be taken on frames containing an unallocated * SAPI. */ dl = lapdm_datalink_for_sapi(le, sapi); if (!dl) { LOGP(DLLAPD, LOGL_ERROR, "No instance for SAPI %d!\n", sapi); msgb_free(msg); return -EINVAL; } switch (msg_type) { case RSL_MT_DATA_REQ: case RSL_MT_SUSP_REQ: case RSL_MT_REL_REQ: /* This is triggered in abnormal error conditions where * set_lapdm_context() was not called for the channel earlier. */ if (!dl->dl.lctx.dl) { LOGP(DLLAPD, LOGL_NOTICE, "(%p) RLL Message '%s' received without LAPDm context. (sapi %d)\n", lc->name, rsl_msg_name(msg_type), sapi); msgb_free(msg); return -EINVAL; } break; default: LOGP(DLLAPD, LOGL_INFO, "(%p) RLL Message '%s' received. (sapi %d)\n", lc->name, rsl_msg_name(msg_type), sapi); } switch (msg_type) { case RSL_MT_UNIT_DATA_REQ: rc = rslms_rx_rll_udata_req(msg, dl); break; case RSL_MT_EST_REQ: rc = rslms_rx_rll_est_req(msg, dl); break; case RSL_MT_DATA_REQ: rc = rslms_rx_rll_data_req(msg, dl); break; case RSL_MT_SUSP_REQ: rc = rslms_rx_rll_susp_req(msg, dl); break; case RSL_MT_RES_REQ: rc = rslms_rx_rll_res_req(msg, dl); break; case RSL_MT_RECON_REQ: rc = rslms_rx_rll_res_req(msg, dl); break; case RSL_MT_REL_REQ: rc = rslms_rx_rll_rel_req(msg, dl); break; default: LOGP(DLLAPD, LOGL_NOTICE, "Message unsupported.\n"); msgb_free(msg); rc = -EINVAL; } return rc; } /* incoming RSLms COMMON CHANNEL message from L3 */ static int rslms_rx_com_chan(struct msgb *msg, struct lapdm_channel *lc) { struct abis_rsl_cchan_hdr *cch = msgb_l2(msg); int msg_type = cch->c.msg_type; int rc = 0; if (msgb_l2len(msg) < sizeof(*cch)) { LOGP(DLLAPD, LOGL_ERROR, "Message too short for COM CHAN hdr!\n"); return -EINVAL; } switch (msg_type) { case RSL_MT_CHAN_RQD: /* create and send RACH request */ rc = rslms_rx_chan_rqd(lc, msg); break; default: LOGP(DLLAPD, LOGL_NOTICE, "Unknown COMMON CHANNEL msg %d!\n", msg_type); msgb_free(msg); return 0; } return rc; } /*! Receive a RSLms \ref msgb from Layer 3. 'msg' ownership is transferred, * i.e. caller must not free it */ int lapdm_rslms_recvmsg(struct msgb *msg, struct lapdm_channel *lc) { struct abis_rsl_common_hdr *rslh = msgb_l2(msg); int rc = 0; if (msgb_l2len(msg) < sizeof(*rslh)) { LOGP(DLLAPD, LOGL_ERROR, "Message too short RSL hdr!\n"); msgb_free(msg); return -EINVAL; } switch (rslh->msg_discr & 0xfe) { case ABIS_RSL_MDISC_RLL: rc = rslms_rx_rll(msg, lc); break; case ABIS_RSL_MDISC_COM_CHAN: rc = rslms_rx_com_chan(msg, lc); break; default: LOGP(DLLAPD, LOGL_ERROR, "unknown RSLms message " "discriminator 0x%02x", rslh->msg_discr); msgb_free(msg); return -EINVAL; } return rc; } /*! Set the \ref lapdm_mode of a LAPDm entity */ int lapdm_entity_set_mode(struct lapdm_entity *le, enum lapdm_mode mode) { int i; enum lapd_mode lm; switch (mode) { case LAPDM_MODE_MS: lm = LAPD_MODE_USER; break; case LAPDM_MODE_BTS: lm = LAPD_MODE_NETWORK; break; default: return -EINVAL; } for (i = 0; i < ARRAY_SIZE(le->datalink); i++) { lapd_set_mode(&le->datalink[i].dl, lm); } le->mode = mode; return 0; } /*! Set the \ref lapdm_mode of a LAPDm channel*/ int lapdm_channel_set_mode(struct lapdm_channel *lc, enum lapdm_mode mode) { int rc; rc = lapdm_entity_set_mode(&lc->lapdm_dcch, mode); if (rc < 0) return rc; return lapdm_entity_set_mode(&lc->lapdm_acch, mode); } /*! Set the L1 callback and context of a LAPDm channel */ void lapdm_channel_set_l1(struct lapdm_channel *lc, osmo_prim_cb cb, void *ctx) { lc->lapdm_dcch.l1_prim_cb = cb; lc->lapdm_acch.l1_prim_cb = cb; lc->lapdm_dcch.l1_ctx = ctx; lc->lapdm_acch.l1_ctx = ctx; } /*! Set the L3 callback and context of a LAPDm channel */ void lapdm_channel_set_l3(struct lapdm_channel *lc, lapdm_cb_t cb, void *ctx) { lc->lapdm_dcch.l3_cb = cb; lc->lapdm_acch.l3_cb = cb; lc->lapdm_dcch.l3_ctx = ctx; lc->lapdm_acch.l3_ctx = ctx; } /*! Reset an entire LAPDm entity and all its datalinks */ void lapdm_entity_reset(struct lapdm_entity *le) { struct lapdm_datalink *dl; int i; for (i = 0; i < ARRAY_SIZE(le->datalink); i++) { dl = &le->datalink[i]; lapd_dl_reset(&dl->dl); } } /*! Reset a LAPDm channel with all its entities */ void lapdm_channel_reset(struct lapdm_channel *lc) { lapdm_entity_reset(&lc->lapdm_dcch); lapdm_entity_reset(&lc->lapdm_acch); } /*! Set the flags of a LAPDm entity */ void lapdm_entity_set_flags(struct lapdm_entity *le, unsigned int flags) { le->flags = flags; } /*! Set the flags of all LAPDm entities in a LAPDm channel */ void lapdm_channel_set_flags(struct lapdm_channel *lc, unsigned int flags) { lapdm_entity_set_flags(&lc->lapdm_dcch, flags); lapdm_entity_set_flags(&lc->lapdm_acch, flags); } /*! @} */