blob: fe3d359060ce7cc578aedbc3af23bd244b2f63aa [file] [log] [blame]
/*
* (C) 2021-2022 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
* All Rights Reserved.
*
* Author: Neels Janosch Hofmeyr <nhofmeyr@sysmocom.de>
*
* 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/utils.h>
#include <osmocom/core/fsm.h>
#include <osmocom/pfcp/pfcp_msg.h>
#include <osmocom/pfcp/pfcp_endpoint.h>
#include <osmocom/upf/upf.h>
#include <osmocom/upf/up_endpoint.h>
#include <osmocom/upf/up_peer.h>
#include <osmocom/upf/up_session.h>
#include <osmocom/upf/up_gtp_action.h>
#include <osmocom/upf/netinst.h>
static enum osmo_pfcp_cause up_session_setup_gtp(struct up_session *session);
void up_session_set_msg_ctx(struct up_session *session, struct osmo_pfcp_msg *m)
{
if (!m->ctx.peer_fi)
up_peer_set_msg_ctx(session->up_peer, m);
OSMO_ASSERT(!m->ctx.session_fi);
m->ctx.session_fi = session->fi;
m->ctx.session_use_count = &session->use_count;
m->ctx.session_use_token = (m->rx ? UP_USE_MSG_RX : UP_USE_MSG_TX);
osmo_use_count_get_put(m->ctx.session_use_count, m->ctx.session_use_token, 1);
}
enum up_session_fsm_state {
UP_SESSION_ST_INIT,
UP_SESSION_ST_ESTABLISHED,
UP_SESSION_ST_WAIT_USE_COUNT,
};
static const struct value_string up_session_fsm_event_names[] = {
OSMO_VALUE_STRING(UP_SESSION_EV_RX_SESSION_EST_REQ),
OSMO_VALUE_STRING(UP_SESSION_EV_RX_SESSION_MOD_REQ),
OSMO_VALUE_STRING(UP_SESSION_EV_RX_SESSION_DEL_REQ),
OSMO_VALUE_STRING(UP_SESSION_EV_USE_COUNT_ZERO),
{}
};
static struct osmo_fsm up_session_fsm;
static const struct osmo_tdef_state_timeout up_session_fsm_timeouts[32] = {
[UP_SESSION_ST_INIT] = { .T = 0 },
[UP_SESSION_ST_ESTABLISHED] = { .T = 0 },
[UP_SESSION_ST_WAIT_USE_COUNT] = { .T = 0 },
};
/* Transition to a state, using the T timer defined in up_session_fsm_timeouts.
* Assumes local variable fi exists. */
#define up_session_fsm_state_chg(STATE) do { \
if (fi->state != STATE) \
osmo_tdef_fsm_inst_state_chg(fi, STATE, \
up_session_fsm_timeouts, \
osmo_pfcp_tdefs, \
5); \
} while (0)
static int up_session_fsm_timer_cb(struct osmo_fsm_inst *fi)
{
/* Return 1 to terminate FSM instance, 0 to keep running */
return 1;
}
struct osmo_pfcp_msg *up_session_init_tx(struct up_session *session, struct osmo_pfcp_msg *in_reply_to,
enum osmo_pfcp_message_type message_type)
{
struct osmo_pfcp_msg *tx = up_peer_init_tx(session->up_peer, in_reply_to, message_type);
tx->h.seid = session->cp_f_seid.seid;
tx->h.seid_present = true;
up_session_set_msg_ctx(session, tx);
return tx;
}
struct chosen_f_teid {
struct llist_head entry;
uint8_t choose_id;
struct osmo_pfcp_ie_f_teid f_teid;
};
struct chosen_f_teid *chosen_f_teid_find(struct llist_head *list, uint8_t choose_id)
{
struct chosen_f_teid *chosen;
llist_for_each_entry(chosen, list, entry) {
if (chosen->choose_id == choose_id)
return chosen;
}
return NULL;
}
/* Find local interface's IP address by Network Instance name. Return 0 on success, or an OSMO_PFCP_CAUSE_* value on
* failure. */
static int up_session_choose_local_ip(struct up_session *session, struct osmo_pfcp_ip_addrs *local_addr,
const char *netinst_name)
{
const struct network_instance *netinst;
struct osmo_sockaddr osa = {};
if (llist_empty(&g_upf->netinst)) {
/* No network instances are configured in osmo-upf.cfg. Instead use the local address configured for
* PFCP, assuming that in a simplistic setup the host has only one interface. It is unlikely to be
* useful for a production environment where the entire point is to hand packet data from one interface
* to another, and where PFCP most probably happens on an entirely different interface, but may make
* things simpler for lab testing. */
if (osmo_pfcp_ip_addrs_set(local_addr,
osmo_pfcp_endpoint_get_local_addr(session->up_peer->up_endpoint->pfcp_ep))) {
LOGPFSML(session->fi, LOGL_ERROR, "Invalid local address in pfcp_endpoint cfg\n");
return OSMO_PFCP_CAUSE_SYSTEM_FAILURE;
}
LOGPFSML(session->fi, LOGL_NOTICE,
"Cannot look up Network Instance %s: No 'netinst' is configured, setting up GTP on same local"
" interface as PFCP: %s (makes sense only for lab testing)\n",
osmo_quote_str_c(OTC_SELECT, netinst_name, -1),
osmo_pfcp_ip_addrs_to_str_c(OTC_SELECT, local_addr));
return 0;
}
if (!netinst_name || !*netinst_name) {
/* Empty or no Network Instance IE in incoming PFCP request. Pick the first network instance; makes
* sense only in a simplistic lab setup where packet data is forwarded to the same interface that it is
* received on, and where no Network Instance is indicated by the CPF. Warn if more than one network
* instance is configured to choose from. */
if (llist_count(&g_upf->netinst) > 1)
LOGPFSML(session->fi, LOGL_NOTICE,
"Missing Network Instance in incoming request, using the first 'netinst' from cfg\n");
netinst = netinst_first(&g_upf->netinst);
/* there has to be a first entry, because we handled the empty list above. */
OSMO_ASSERT(netinst);
} else {
netinst = netinst_find(&g_upf->netinst, netinst_name);
if (!netinst) {
LOGPFSML(session->fi, LOGL_ERROR, "Network Instance from PFCP request not found: %s"
" -- ensure there is a 'netinst' / 'add %s <ip-addr>' entry in your config\n",
osmo_quote_str_c(OTC_SELECT, netinst_name, -1),
osmo_escape_str_c(OTC_SELECT, netinst_name, -1));
return OSMO_PFCP_CAUSE_RULE_CREATION_MOD_FAILURE;
}
}
/* Convert netinst IP address string first to osmo_sockaddr and then to osmo_pfcp_ip_addrs. */
if (osmo_sockaddr_str_to_sockaddr(&netinst->addr, &osa.u.sas)
|| osmo_pfcp_ip_addrs_set(local_addr, &osa)) {
LOGPFSML(session->fi, LOGL_ERROR,
"Network Instance %s from PFCP request yields no valid IP address: "
OSMO_SOCKADDR_STR_FMT "\n",
osmo_quote_str_c(OTC_SELECT, netinst_name, -1),
OSMO_SOCKADDR_STR_FMT_ARGS(&netinst->addr));
return OSMO_PFCP_CAUSE_RULE_CREATION_MOD_FAILURE;
}
return 0;
}
/* Choose an F-TEID (when the peer has sent CHOOSE = 1).
* If the peer also sent a CHOOSE_ID, then remember this F-TEID choice under the given ID, and re-use that choice when
* the same ID re-appears. The chosen IDs are saved in session->chosen_f_teids.
* Return 0 on success, or an OSMO_PFCP_CAUSE_* value on failure. */
static enum osmo_pfcp_cause up_session_choose_f_teid(struct up_session *session, struct osmo_pfcp_ie_f_teid *dst,
bool choose_id_present, uint8_t choose_id,
const char *netinst_name)
{
struct up_endpoint *up_ep = session->up_peer->up_endpoint;
struct chosen_f_teid *chosen = NULL;
if (choose_id_present)
chosen = chosen_f_teid_find(&session->chosen_f_teids, choose_id);
if (chosen) {
/* Re-use a previous F-TEID */
*dst = chosen->f_teid;
} else {
int rc;
*dst = (struct osmo_pfcp_ie_f_teid){
.choose_flag = false,
};
/* Determine local IP address from Network Instance value received in PFCP request */
rc = up_session_choose_local_ip(session, &dst->fixed.ip_addr, netinst_name);
if (rc)
return rc;
/* Choose a new TEID */
dst->fixed.teid = up_endpoint_next_teid(up_ep);
if (dst->fixed.teid == 0) {
LOGPFSML(session->fi, LOGL_ERROR, "Failed to allocate an unused TEID\n");
return OSMO_PFCP_CAUSE_PFCP_ENTITY_IN_CONGESTION;
}
LOGPFSML(session->fi, LOGL_INFO, "Allocated new local F-TEID %s\n",
osmo_pfcp_ie_f_teid_to_str_c(OTC_SELECT, dst));
/* Save this choice */
if (choose_id_present) {
chosen = talloc(session, struct chosen_f_teid);
*chosen = (struct chosen_f_teid){
.f_teid = *dst,
.choose_id = choose_id,
};
llist_add_tail(&chosen->entry, &session->chosen_f_teids);
}
}
return OSMO_PFCP_CAUSE_REQUEST_ACCEPTED;
}
static struct far *far_create(struct up_session *session,
const struct osmo_pfcp_ie_create_far *create_far)
{
struct far *far = talloc(session, struct far);
*far = (struct far){
.session = session,
.desc = *create_far,
};
llist_add_tail(&far->entry, &session->fars);
return far;
}
static struct far *far_find(struct up_session *session, uint32_t far_id)
{
struct far *far;
llist_for_each_entry(far, &session->fars, entry) {
if (far->desc.far_id == far_id)
return far;
}
return NULL;
}
static void far_upd(struct far *far, const struct osmo_pfcp_ie_upd_far *upd)
{
if (upd->apply_action_present)
far->desc.apply_action = upd->apply_action;
if (upd->upd_forw_params_present) {
const struct osmo_pfcp_ie_upd_forw_params *u = &upd->upd_forw_params;
struct osmo_pfcp_ie_forw_params *p = &far->desc.forw_params;
far->desc.forw_params_present = true;
if (u->destination_iface_present)
p->destination_iface = u->destination_iface;
if (u->network_inst_present) {
p->network_inst = u->network_inst;
p->network_inst_present = true;
}
if (u->outer_header_creation_present) {
p->outer_header_creation = u->outer_header_creation;
p->outer_header_creation_present = true;
}
if (u->linked_te_id_present) {
p->linked_te_id = u->linked_te_id;
p->linked_te_id_present = true;
}
if (u->destination_iface_type_present) {
p->destination_iface_type = u->destination_iface_type;
p->destination_iface_type_present = true;
}
}
}
struct pdr;
static void pdr_classify(struct pdr *pdr);
static void far_del(struct far *far)
{
struct pdr *pdr;
llist_for_each_entry(pdr, &far->session->pdrs, entry) {
if (pdr->far == far) {
pdr->far = NULL;
pdr_classify(pdr);
}
}
llist_del(&far->entry);
talloc_free(far);
}
static int far_to_str_buf(char *buf, size_t len, const struct far *far)
{
struct osmo_strbuf sb = { .buf = buf, .len = len };
const struct osmo_pfcp_ie_create_far *f = &far->desc;
OSMO_STRBUF_PRINTF(sb, "FAR-%u{", f->far_id);
OSMO_STRBUF_APPEND(sb, osmo_pfcp_bits_to_str_buf, f->apply_action.bits, osmo_pfcp_apply_action_strs);
if (f->forw_params_present) {
OSMO_STRBUF_PRINTF(sb, " dst:%s", osmo_pfcp_dest_iface_str(f->forw_params.destination_iface));
if (f->forw_params.outer_header_creation_present) {
OSMO_STRBUF_PRINTF(sb, ",");
OSMO_STRBUF_APPEND(sb, osmo_pfcp_ie_outer_header_creation_to_str_buf,
&f->forw_params.outer_header_creation);
}
}
OSMO_STRBUF_PRINTF(sb, "}");
return sb.chars_needed;
}
#if 0
static char *far_to_str_c(void *ctx, const struct far *far)
{
OSMO_NAME_C_IMPL(ctx, 64, "ERROR", far_to_str_buf, far)
}
#endif
int pdr_to_str_buf(char *buf, size_t buflen, const struct pdr *pdr)
{
struct osmo_strbuf sb = { .buf = buf, .len = buflen };
const struct osmo_pfcp_ie_create_pdr *d = &pdr->desc;
OSMO_STRBUF_PRINTF(sb, "PDR-%u{src:%s", d->pdr_id, osmo_pfcp_source_iface_str(d->pdi.source_iface));
if (pdr->desc.pdi.ue_ip_address_present) {
if (pdr->desc.pdi.ue_ip_address.ip_addr.v4_present) {
OSMO_STRBUF_PRINTF(sb, " %s",
pdr->desc.pdi.ue_ip_address.ip_is_destination ? "dst:" : "src:");
OSMO_STRBUF_APPEND(sb, osmo_sockaddr_to_str_buf2, &pdr->desc.pdi.ue_ip_address.ip_addr.v4);
}
if (pdr->desc.pdi.ue_ip_address.ip_addr.v6_present) {
OSMO_STRBUF_PRINTF(sb, " ");
OSMO_STRBUF_APPEND(sb, osmo_sockaddr_to_str_buf2, &pdr->desc.pdi.ue_ip_address.ip_addr.v6);
}
}
if (pdr->desc.pdi.network_inst_present) {
OSMO_STRBUF_PRINTF(sb, " netinst:");
OSMO_STRBUF_APPEND(sb, osmo_quote_str_buf3, pdr->desc.pdi.network_inst.str, -1);
}
if (pdr->local_f_teid) {
OSMO_STRBUF_PRINTF(sb, " ");
OSMO_STRBUF_APPEND(sb, osmo_pfcp_ie_f_teid_to_str_buf, pdr->local_f_teid);
}
if (d->outer_header_removal_present)
OSMO_STRBUF_PRINTF(sb, " decaps-%s",
osmo_pfcp_outer_header_removal_desc_str(d->outer_header_removal.desc));
OSMO_STRBUF_PRINTF(sb, "}");
if (pdr->far) {
OSMO_STRBUF_PRINTF(sb, " --> ");
OSMO_STRBUF_APPEND(sb, far_to_str_buf, pdr->far);
}
return sb.chars_needed;
}
char *pdr_to_str_c(void *ctx, const struct pdr *pdr)
{
OSMO_NAME_C_IMPL(ctx, 128, "ERROR", pdr_to_str_buf, pdr)
}
static struct pdr *pdr_find(struct up_session *session, uint16_t pdr_id)
{
struct pdr *pdr;
llist_for_each_entry(pdr, &session->pdrs, entry) {
if (pdr->desc.pdr_id == pdr_id)
return pdr;
}
return NULL;
}
static void pdr_del(struct pdr *pdr)
{
llist_del(&pdr->entry);
talloc_free(pdr);
}
static void pdr_set_far(struct pdr *pdr, struct far *far)
{
pdr->far = far;
}
/* Set up a new Packet Detection Rule, append the response to the end of the created_pdr/created_pdr_count array. */
static struct pdr *pdr_create(struct up_session *session,
const struct osmo_pfcp_ie_create_pdr *create_pdr,
enum osmo_pfcp_cause *cause,
bool *offending_ie_present,
enum osmo_pfcp_iei *offending_ie,
struct osmo_pfcp_ie_created_pdr created_pdr[],
unsigned int *created_pdr_count,
size_t created_pdr_maxcount)
{
struct pdr *pdr = NULL;
/* Is there still room in the response for 'Created PDR' IEs? */
if (*created_pdr_count >= created_pdr_maxcount) {
*cause = OSMO_PFCP_CAUSE_NO_RESOURCES_AVAILABLE;
goto nack_resp;
}
pdr = talloc(session, struct pdr);
*pdr = (struct pdr){
.session = session,
.desc = *create_pdr,
};
llist_add_tail(&pdr->entry, &session->pdrs);
if (pdr->desc.far_id_present) {
struct far *far = far_find(session, pdr->desc.far_id);
if (!far) {
LOGPFSML(session->fi, LOGL_ERROR, "PDR-%u requests FAR-%u, but there is no such FAR\n",
pdr->desc.pdr_id, pdr->desc.far_id);
*cause = OSMO_PFCP_CAUSE_REQUEST_REJECTED;
*offending_ie_present = true;
*offending_ie = OSMO_PFCP_IEI_FAR_ID;
goto nack_resp;
}
pdr_set_far(pdr, far);
} else if (pdr->desc.activate_predefined_rules_present) {
LOGPFSML(session->fi, LOGL_ERROR,
"Predefined Rules feature not implemented: PDR-%u requests predefined rule '%s'\n",
pdr->desc.pdr_id, pdr->desc.activate_predefined_rules.str);
*cause = OSMO_PFCP_CAUSE_RULE_CREATION_MOD_FAILURE;
*offending_ie_present = true;
*offending_ie = OSMO_PFCP_IEI_ACTIVATE_PREDEFINED_RULES;
goto nack_resp;
} else {
LOGPFSML(session->fi, LOGL_ERROR, "No FAR defined for PDR-%u\n", pdr->desc.pdr_id);
*cause = OSMO_PFCP_CAUSE_CONDITIONAL_IE_MISSING;
*offending_ie_present = true;
*offending_ie = OSMO_PFCP_IEI_FAR_ID;
goto nack_resp;
}
/* Figure out the TEID and IP address for this PDR */
if (pdr->desc.pdi.local_f_teid_present) {
if (pdr->desc.pdi.local_f_teid.choose_flag) {
/* CHOOSE = 1: we need to pick our own local F-TEID */
struct osmo_pfcp_ie_f_teid local_f_teid;
const char *netinst_name = NULL;
if (pdr->desc.pdi.network_inst_present)
netinst_name = pdr->desc.pdi.network_inst.str;
*cause = up_session_choose_f_teid(session, &local_f_teid,
pdr->desc.pdi.local_f_teid.choose.choose_id_present,
pdr->desc.pdi.local_f_teid.choose.choose_id,
netinst_name);
if (*cause != OSMO_PFCP_CAUSE_REQUEST_ACCEPTED) {
*offending_ie = OSMO_PFCP_IEI_F_TEID;
*offending_ie_present = true;
goto nack_resp;
}
pdr->_local_f_teid_buf = local_f_teid;
pdr->local_f_teid = &pdr->_local_f_teid_buf;
} else {
/* CHOOSE = 0: just use the provided TEID and IP addr */
pdr->local_f_teid = &pdr->desc.pdi.local_f_teid;
if (!(pdr->local_f_teid->fixed.ip_addr.v4_present
|| pdr->local_f_teid->fixed.ip_addr.v6_present)) {
LOGPFSML(session->fi, LOGL_ERROR,
"peer requested to choose an F-TEID, but neither IPv4 nor IPv6 is selected"
" in the incoming Create PDR IE for PDR-%u.\n",
pdr->desc.pdr_id);
*cause = OSMO_PFCP_CAUSE_RULE_CREATION_MOD_FAILURE;
*offending_ie = OSMO_PFCP_IEI_F_TEID;
*offending_ie_present = true;
goto nack_resp;
}
}
}
if (pdr->local_f_teid) {
created_pdr[*created_pdr_count] = (struct osmo_pfcp_ie_created_pdr){
.pdr_id = pdr->desc.pdr_id,
.local_f_teid_present = true,
.local_f_teid = *pdr->local_f_teid,
};
} else {
created_pdr[*created_pdr_count] = (struct osmo_pfcp_ie_created_pdr){
.pdr_id = pdr->desc.pdr_id,
};
}
(*created_pdr_count)++;
LOGPFSML(session->fi, LOGL_INFO, "New %s\n", pdr_to_str_c(OTC_SELECT, pdr));
return pdr;
nack_resp:
if (pdr)
pdr_del(pdr);
if (!*offending_ie_present) {
*offending_ie = OSMO_PFCP_IEI_CREATE_PDR;
*offending_ie_present = true;
}
return NULL;
}
static struct pdr *pdr_upd(struct pdr *pdr,
const struct osmo_pfcp_ie_upd_pdr *update_pdr,
enum osmo_pfcp_cause *cause,
bool *offending_ie_present,
enum osmo_pfcp_iei *offending_ie,
struct osmo_pfcp_ie_updated_pdr updated_pdr[],
unsigned int *updated_pdr_count,
size_t updated_pdr_maxcount)
{
struct up_session *session = pdr->session;
/* Is there still room in the response for 'Updated PDR' IEs? */
if (*updated_pdr_count >= updated_pdr_maxcount) {
*cause = OSMO_PFCP_CAUSE_NO_RESOURCES_AVAILABLE;
goto nack_resp;
}
if (update_pdr->outer_header_removal_present) {
pdr->desc.outer_header_removal = update_pdr->outer_header_removal;
pdr->desc.outer_header_removal_present = true;
}
if (update_pdr->pdi_present)
pdr->desc.pdi = update_pdr->pdi;
if (update_pdr->far_id_present) {
struct far *far = far_find(session, update_pdr->far_id);
if (!far) {
LOGPFSML(session->fi, LOGL_ERROR, "PDR-%u requests FAR-%u, but there is no such FAR\n",
pdr->desc.pdr_id, pdr->desc.far_id);
*cause = OSMO_PFCP_CAUSE_REQUEST_REJECTED;
*offending_ie_present = true;
*offending_ie = OSMO_PFCP_IEI_FAR_ID;
goto nack_resp;
}
pdr_set_far(pdr, far);
}
if (pdr->local_f_teid) {
updated_pdr[*updated_pdr_count] = (struct osmo_pfcp_ie_updated_pdr){
.pdr_id = pdr->desc.pdr_id,
.local_f_teid_present = true,
.local_f_teid = *pdr->local_f_teid,
};
} else {
updated_pdr[*updated_pdr_count] = (struct osmo_pfcp_ie_updated_pdr){
.pdr_id = pdr->desc.pdr_id,
};
}
(*updated_pdr_count)++;
pdr_classify(pdr);
LOGPFSML(session->fi, LOGL_INFO, "Updated %s\n", pdr_to_str_c(OTC_SELECT, pdr));
return pdr;
nack_resp:
if (pdr)
pdr_del(pdr);
if (!*offending_ie_present) {
*offending_ie = OSMO_PFCP_IEI_UPD_PDR;
*offending_ie_present = true;
}
return NULL;
}
char *up_session_gtp_status(struct up_session *session)
{
struct pdr *pdr;
struct far *far;
int active_pdrs = 0;
int total_pdrs = 0;
int active_fars = 0;
int total_fars = 0;
llist_for_each_entry(pdr, &session->pdrs, entry) {
if (pdr->active)
active_pdrs++;
total_pdrs++;
}
llist_for_each_entry(far, &session->fars, entry) {
if (far->active)
active_fars++;
total_fars++;
}
return talloc_asprintf(OTC_SELECT, "PDR-active:%d/%d FAR-active:%d/%d GTP-active:%u",
active_pdrs, total_pdrs, active_fars, total_fars,
llist_count(&session->active_gtp_actions));
}
static void up_session_est(struct up_session *session, struct osmo_pfcp_msg *m)
{
struct osmo_fsm_inst *fi = session->fi;
struct up_peer *peer = session->up_peer;
struct osmo_pfcp_msg_session_est_req *req = &m->ies.session_est_req;
struct osmo_pfcp_msg_session_est_resp *resp;
struct osmo_pfcp_msg *tx;
int i;
int rc;
tx = up_session_init_tx(session, m, OSMO_PFCP_MSGT_SESSION_EST_RESP);
resp = &tx->ies.session_est_resp;
for (i = 0; i < req->create_far_count; i++) {
struct far *far = far_create(session, &req->create_far[i]);
if (!far)
goto nack_response;
}
for (i = 0; i < req->create_pdr_count; i++) {
struct pdr *pdr = pdr_create(session, &req->create_pdr[i],
&resp->cause,
&resp->offending_ie_present, &resp->offending_ie,
resp->created_pdr, &resp->created_pdr_count,
ARRAY_SIZE(resp->created_pdr));
if (!pdr)
goto nack_response;
}
resp->cause = up_session_setup_gtp(session);
if (resp->cause != OSMO_PFCP_CAUSE_REQUEST_ACCEPTED)
goto nack_response;
/* Success, send ACK */
osmo_pfcp_ie_f_seid_set(&resp->up_f_seid, session->up_seid,
osmo_pfcp_endpoint_get_local_addr(peer->up_endpoint->pfcp_ep));
resp->up_f_seid_present = true;
rc = osmo_pfcp_endpoint_tx(peer->up_endpoint->pfcp_ep, tx);
if (rc)
up_session_fsm_state_chg(UP_SESSION_ST_WAIT_USE_COUNT);
up_session_fsm_state_chg(UP_SESSION_ST_ESTABLISHED);
return;
nack_response:
resp->created_pdr_count = 0;
osmo_pfcp_endpoint_tx(peer->up_endpoint->pfcp_ep, tx);
up_session_fsm_state_chg(UP_SESSION_ST_WAIT_USE_COUNT);
}
static void up_session_mod(struct up_session *session, struct osmo_pfcp_msg *m)
{
struct osmo_fsm_inst *fi = session->fi;
struct up_peer *peer = session->up_peer;
struct osmo_pfcp_msg_session_mod_req *req = &m->ies.session_mod_req;
struct osmo_pfcp_msg_session_mod_resp *resp;
struct osmo_pfcp_msg *tx;
int i;
tx = up_session_init_tx(session, m, OSMO_PFCP_MSGT_SESSION_MOD_RESP);
resp = &tx->ies.session_mod_resp;
for (i = 0; i < req->remove_far_count; i++) {
uint32_t far_id = req->remove_far[i].far_id;
struct far *far = far_find(session, far_id);
if (!far) {
OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, "Cannot remove, does not exist: FAR-%u\n", far_id);
continue;
}
far_del(far);
}
for (i = 0; i < req->remove_pdr_count; i++) {
uint16_t pdr_id = req->remove_pdr[i].pdr_id;
struct pdr *pdr = pdr_find(session, pdr_id);
if (!pdr) {
OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, "Cannot remove, does not exist: PDR-%u\n", pdr_id);
continue;
}
pdr_del(pdr);
}
for (i = 0; i < req->create_far_count; i++) {
struct far *far = far_create(session, &req->create_far[i]);
if (!far)
goto nack_response;
}
for (i = 0; i < req->create_pdr_count; i++) {
struct pdr *pdr = pdr_create(session, &req->create_pdr[i],
&resp->cause,
&resp->offending_ie_present, &resp->offending_ie,
resp->created_pdr, &resp->created_pdr_count,
ARRAY_SIZE(resp->created_pdr));
if (!pdr)
goto nack_response;
}
for (i = 0; i < req->upd_far_count; i++) {
uint32_t far_id = req->upd_far[i].far_id;
struct far *far = far_find(session, req->upd_far[i].far_id);
if (!far) {
OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, "Cannot update, does not exist: FAR-%u\n", far_id);
goto nack_response;
}
far_upd(far, &req->upd_far[i]);
}
for (i = 0; i < req->upd_pdr_count; i++) {
uint16_t pdr_id = req->upd_pdr[i].pdr_id;
struct pdr *pdr = pdr_find(session, req->upd_pdr[i].pdr_id);
if (!pdr) {
OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, "Cannot update, does not exist: PDR-%u\n", pdr_id);
goto nack_response;
}
pdr_upd(pdr, &req->upd_pdr[i],
&resp->cause,
&resp->offending_ie_present, &resp->offending_ie,
resp->updated_pdr, &resp->updated_pdr_count,
ARRAY_SIZE(resp->updated_pdr));
}
resp->cause = up_session_setup_gtp(session);
if (resp->cause != OSMO_PFCP_CAUSE_REQUEST_ACCEPTED)
goto nack_response;
/* Success, send ACK */
if (osmo_pfcp_endpoint_tx(peer->up_endpoint->pfcp_ep, tx))
up_session_fsm_state_chg(UP_SESSION_ST_WAIT_USE_COUNT);
LOGPFSML(fi, LOGL_NOTICE, "Session modified: %s\n", up_session_gtp_status(session));
return;
nack_response:
resp->created_pdr_count = 0;
osmo_pfcp_endpoint_tx(peer->up_endpoint->pfcp_ep, tx);
up_session_fsm_state_chg(UP_SESSION_ST_WAIT_USE_COUNT);
}
static void up_session_del(struct up_session *session, struct osmo_pfcp_msg *m)
{
struct osmo_fsm_inst *fi = session->fi;
struct up_peer *peer = session->up_peer;
struct osmo_pfcp_msg *tx;
tx = up_session_init_tx(session, m, OSMO_PFCP_MSGT_SESSION_DEL_RESP);
tx->ies.session_del_resp = (struct osmo_pfcp_msg_session_del_resp){
.cause = OSMO_PFCP_CAUSE_REQUEST_ACCEPTED
};
osmo_pfcp_endpoint_tx(peer->up_endpoint->pfcp_ep, tx);
up_session_fsm_state_chg(UP_SESSION_ST_WAIT_USE_COUNT);
}
static void up_session_init_action(struct osmo_fsm_inst *fi, uint32_t event, void *data)
{
struct up_session *up_session = fi->priv;
switch (event) {
case UP_SESSION_EV_RX_SESSION_EST_REQ:
up_session_est(up_session, data);
break;
case UP_SESSION_EV_RX_SESSION_DEL_REQ:
up_session_del(up_session, data);
break;
case UP_SESSION_EV_USE_COUNT_ZERO:
/* ignore */
break;
default:
OSMO_ASSERT(false);
}
}
static void up_session_established_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state)
{
struct up_session *session = fi->priv;
LOGPFSML(fi, LOGL_NOTICE, "Session established: %s %s\n", up_session_to_str_c(OTC_SELECT, session),
up_session_gtp_status(session));
}
static void up_session_established_action(struct osmo_fsm_inst *fi, uint32_t event, void *data)
{
struct up_session *up_session = fi->priv;
switch (event) {
case UP_SESSION_EV_RX_SESSION_MOD_REQ:
up_session_mod(up_session, data);
break;
case UP_SESSION_EV_RX_SESSION_DEL_REQ:
up_session_del(up_session, data);
break;
case UP_SESSION_EV_USE_COUNT_ZERO:
/* ignore */
break;
default:
OSMO_ASSERT(false);
}
}
static void up_session_established_onleave(struct osmo_fsm_inst *fi, uint32_t next_state)
{
struct up_session *session = fi->priv;
struct up_gtp_action *a;
LOGPFSML(fi, LOGL_NOTICE, "Session releasing: %s %s\n", up_session_to_str_c(OTC_SELECT, session), up_session_gtp_status(session));
/* Shut down all active GTP rules */
while ((a = llist_first_entry_or_null(&session->active_gtp_actions, struct up_gtp_action, entry))) {
up_gtp_action_disable(a);
llist_del(&a->entry);
talloc_free(a);
}
}
static void up_session_clear_pdr_far(struct up_session *session);
static void drop_gtp_actions(struct up_session *session);
int up_session_discard(struct up_session *session)
{
struct osmo_fsm_inst *fi = session->fi;
if (fi->state == UP_SESSION_ST_WAIT_USE_COUNT)
return 0;
up_session_fsm_state_chg(UP_SESSION_ST_WAIT_USE_COUNT);
return 1;
}
static void up_session_wait_use_count_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state)
{
struct up_session *session = fi->priv;
drop_gtp_actions(session);
up_session_clear_pdr_far(session);
if (!osmo_use_count_total(&session->use_count))
osmo_fsm_inst_term(fi, OSMO_FSM_TERM_REGULAR, NULL);
}
static void up_session_wait_use_count_action(struct osmo_fsm_inst *fi, uint32_t event, void *data)
{
switch (event) {
case UP_SESSION_EV_USE_COUNT_ZERO:
osmo_fsm_inst_term(fi, OSMO_FSM_TERM_REGULAR, NULL);
return;
default:
OSMO_ASSERT(false);
}
}
static void up_session_clear_pdr_far(struct up_session *session)
{
struct pdr *pdr;
struct far *far;
while ((pdr = llist_first_entry_or_null(&session->pdrs, struct pdr, entry)))
pdr_del(pdr);
while ((far = llist_first_entry_or_null(&session->fars, struct far, entry)))
far_del(far);
}
static void drop_gtp_actions(struct up_session *session);
static void up_session_fsm_cleanup(struct osmo_fsm_inst *fi, enum osmo_fsm_term_cause cause)
{
struct up_session *session = fi->priv;
drop_gtp_actions(session);
up_session_clear_pdr_far(session);
hash_del(&session->node_by_up_seid);
hash_del(&session->node_by_cp_seid);
}
#define S(x) (1 << (x))
static const struct osmo_fsm_state up_session_fsm_states[] = {
[UP_SESSION_ST_INIT] = {
.name = "INIT",
.in_event_mask = 0
| S(UP_SESSION_EV_RX_SESSION_EST_REQ)
| S(UP_SESSION_EV_RX_SESSION_DEL_REQ)
| S(UP_SESSION_EV_USE_COUNT_ZERO)
,
.out_state_mask = 0
| S(UP_SESSION_ST_ESTABLISHED)
| S(UP_SESSION_ST_WAIT_USE_COUNT)
,
.action = up_session_init_action,
},
[UP_SESSION_ST_ESTABLISHED] = {
.name = "ESTABLISHED",
.in_event_mask = 0
| S(UP_SESSION_EV_RX_SESSION_MOD_REQ)
| S(UP_SESSION_EV_RX_SESSION_DEL_REQ)
| S(UP_SESSION_EV_USE_COUNT_ZERO)
,
.out_state_mask = 0
| S(UP_SESSION_ST_WAIT_USE_COUNT)
,
.onenter = up_session_established_onenter,
.action = up_session_established_action,
.onleave = up_session_established_onleave,
},
[UP_SESSION_ST_WAIT_USE_COUNT] = {
.name = "WAIT_USE_COUNT",
.in_event_mask = 0
| S(UP_SESSION_EV_USE_COUNT_ZERO)
,
.onenter = up_session_wait_use_count_onenter,
.action = up_session_wait_use_count_action,
},
};
static struct osmo_fsm up_session_fsm = {
.name = "up_session",
.states = up_session_fsm_states,
.num_states = ARRAY_SIZE(up_session_fsm_states),
.log_subsys = DSESSION,
.event_names = up_session_fsm_event_names,
.timer_cb = up_session_fsm_timer_cb,
.cleanup = up_session_fsm_cleanup,
};
static __attribute__((constructor)) void up_session_fsm_register(void)
{
OSMO_ASSERT(osmo_fsm_register(&up_session_fsm) == 0);
}
static int up_session_use_cb(struct osmo_use_count_entry *e, int32_t old_use_count, const char *file, int line)
{
struct up_session *session = e->use_count->talloc_object;
int32_t total;
int level;
if (!e->use)
return -EINVAL;
total = osmo_use_count_total(&session->use_count);
if (total == 0
|| (total == 1 && old_use_count == 0 && e->count == 1))
level = LOGL_INFO;
else
level = LOGL_DEBUG;
LOGPFSMSLSRC(session->fi, DREF, level, file, line,
"%s %s: now used by %s\n",
(e->count - old_use_count) > 0 ? "+" : "-", e->use,
osmo_use_count_to_str_c(OTC_SELECT, &session->use_count));
if (e->count < 0)
return -ERANGE;
if (total == 0)
osmo_fsm_inst_dispatch(session->fi, UP_SESSION_EV_USE_COUNT_ZERO, NULL);
return 0;
}
static void up_session_update_id(struct up_session *session)
{
osmo_fsm_inst_update_id_f_sanitize(session->fi, '-', "%s-0x%" PRIx64,
up_peer_remote_addr_str(session->up_peer),
session->up_seid);
LOGPFSML(session->fi, LOGL_DEBUG, "Updated id\n");
}
static inline uint64_t up_session_key(uint64_t cp_seid, uint64_t up_seid)
{
return cp_seid + up_seid;
}
static struct up_session *up_session_add(struct up_peer *peer, const struct osmo_pfcp_ie_f_seid *cp_f_seid)
{
struct up_session *session;
uint64_t up_seid = up_endpoint_next_seid(peer->up_endpoint);
if (!up_seid)
return NULL;
struct osmo_fsm_inst *fi = osmo_fsm_inst_alloc_child(&up_session_fsm, peer->fi, UP_PEER_EV_SESSION_TERM);
OSMO_ASSERT(fi);
session = talloc(fi, struct up_session);
OSMO_ASSERT(session);
fi->priv = session;
*session = (struct up_session) {
.fi = fi,
.up_peer = peer,
.cp_f_seid = *cp_f_seid,
.up_seid = up_seid,
.use_count = {
.talloc_object = session,
.use_cb = up_session_use_cb,
},
};
INIT_LLIST_HEAD(&session->pdrs);
INIT_LLIST_HEAD(&session->fars);
INIT_LLIST_HEAD(&session->chosen_f_teids);
INIT_LLIST_HEAD(&session->active_gtp_actions);
osmo_use_count_make_static_entries(&session->use_count, session->use_count_buf, ARRAY_SIZE(session->use_count_buf));
LOGPFSML(session->fi, LOGL_INFO, "Allocated new UP-SEID: 0x%" PRIx64 "\n", session->up_seid);
up_session_update_id(session);
hash_add(peer->sessions_by_up_seid, &session->node_by_up_seid, session->up_seid);
hash_add(peer->sessions_by_cp_seid, &session->node_by_cp_seid, session->cp_f_seid.seid);
return session;
}
struct up_session *up_session_find_or_add(struct up_peer *peer, const struct osmo_pfcp_ie_f_seid *cp_f_seid,
const struct osmo_pfcp_ie_f_seid *up_f_seid)
{
struct up_session *session;
if (cp_f_seid)
session = up_session_find_by_cp_f_seid(peer, cp_f_seid);
else if (up_f_seid)
session = up_session_find_by_up_seid(peer, up_f_seid->seid);
else
return NULL;
if (session)
return session;
return up_session_add(peer, cp_f_seid);
}
struct up_session *up_session_find_by_up_seid(struct up_peer *peer, uint64_t up_seid)
{
struct up_session *session;
hash_for_each_possible(peer->sessions_by_up_seid, session, node_by_up_seid, up_seid) {
if (up_seid == session->up_seid)
return session;
}
return NULL;
}
struct up_session *up_session_find_by_cp_f_seid(struct up_peer *peer, const struct osmo_pfcp_ie_f_seid *cp_f_seid)
{
struct up_session *session;
hash_for_each_possible(peer->sessions_by_cp_seid, session, node_by_cp_seid, cp_f_seid->seid) {
if (osmo_pfcp_ie_f_seid_cmp(&session->cp_f_seid, cp_f_seid) == 0)
return session;
}
return NULL;
}
struct up_session *up_session_find_by_local_teid(struct up_peer *peer, uint32_t teid)
{
struct up_session *session;
int bkt;
hash_for_each(peer->sessions_by_up_seid, bkt, session, node_by_up_seid) {
struct pdr *pdr;
llist_for_each_entry(pdr, &session->pdrs, entry) {
if (!pdr->local_f_teid)
continue;
if (pdr->local_f_teid->fixed.teid == teid)
return session;
}
}
return NULL;
}
static bool action_is_forw(const struct osmo_pfcp_ie_apply_action *aa)
{
return osmo_pfcp_bits_get(aa->bits, OSMO_PFCP_APPLY_ACTION_FORW)
&& !osmo_pfcp_bits_get(aa->bits, OSMO_PFCP_APPLY_ACTION_DROP);
}
static void pdr_classify(struct pdr *pdr)
{
pdr->rx_decaps = false;
pdr->forw_encaps = false;
pdr->access_to_core = false;
pdr->core_to_access = false;
if (!pdr->far)
return;
pdr->rx_decaps = (pdr->desc.outer_header_removal_present
&& pdr->desc.outer_header_removal.desc == OSMO_PFCP_OUTER_HEADER_REMOVAL_GTP_U_UDP_IPV4);
pdr->forw_encaps = (pdr->far->desc.forw_params_present
&& pdr->far->desc.forw_params.outer_header_creation_present);
if (!action_is_forw(&pdr->far->desc.apply_action))
return;
pdr->access_to_core = (pdr->desc.pdi.source_iface == OSMO_PFCP_SOURCE_IFACE_ACCESS
&& pdr->far->desc.forw_params.destination_iface == OSMO_PFCP_DEST_IFACE_CORE);
pdr->core_to_access = (pdr->desc.pdi.source_iface == OSMO_PFCP_SOURCE_IFACE_CORE
&& pdr->far->desc.forw_params.destination_iface == OSMO_PFCP_DEST_IFACE_ACCESS);
}
void pdr_reverse_set(struct pdr *a, struct pdr *b)
{
if (a)
a->reverse_pdr = b;
if (b)
b->reverse_pdr = a;
}
void pdr_reverse_unset(struct pdr *pdr)
{
if (!pdr->reverse_pdr)
return;
pdr->reverse_pdr->reverse_pdr = NULL;
pdr->reverse_pdr = NULL;
}
/* Log that a PDR (and its reverse-PDR) is inactive.
* \param pdr The Access-to-Core PDR.
* \param desc Why it is inactive.
* \param pdr_to_str The PDR that desc describes, can be pdr or the reverse Core-to-Access PDR.
*/
static void log_inactive_pdr_set(struct pdr *pdr, const char *desc, const struct pdr *pdr_to_str)
{
struct pdr *rpdr = pdr->reverse_pdr;
pdr_to_str = pdr_to_str ? : pdr;
osmo_talloc_replace_string_fmt(pdr, &pdr->inactive_reason, "%s (in PDR-%u %s)", desc,
pdr_to_str->desc.pdr_id,
osmo_pfcp_source_iface_str(pdr_to_str->desc.pdi.source_iface));
if (rpdr)
osmo_talloc_replace_string_fmt(rpdr, &rpdr->inactive_reason, "%s (in PDR-%u %s)", desc,
pdr_to_str->desc.pdr_id,
osmo_pfcp_source_iface_str(pdr_to_str->desc.pdi.source_iface));
if (rpdr)
LOGPFSML(pdr->session->fi, LOGL_INFO, "Inactive set: PDR-%u + PDR-%u: %s: %s\n",
pdr->desc.pdr_id, rpdr->desc.pdr_id, desc, pdr_to_str_c(OTC_SELECT, pdr_to_str));
else
LOGPFSML(pdr->session->fi, LOGL_INFO, "Inactive: PDR-%u: %s: %s\n",
pdr->desc.pdr_id, desc, pdr_to_str_c(OTC_SELECT, pdr_to_str));
}
/* A GTP tunnel on Access side, plain IP on Core side.
* The given PDR must have an outer-header-removal and a local F-TEID.
* Its reverse-PDR must have a UE address flagged as "Destination" IP addr.
* Its reverse-PDR's FAR must have an outer-header creation with a remote TEID.
* \param pdr A rule detecting packets on Access, where pdr->reverse_pdr detects packets on Core.
*/
static void add_gtp_action_tunend(void *ctx, struct llist_head *dst, struct pdr *pdr)
{
struct up_session *session = pdr->session;
struct up_gtp_action *a;
struct pdr *rpdr;
struct far *rfar;
struct osmo_pfcp_ie_forw_params *rfar_forw;
OSMO_ASSERT(pdr->far);
OSMO_ASSERT(pdr->reverse_pdr);
OSMO_ASSERT(pdr->reverse_pdr->far);
rpdr = pdr->reverse_pdr;
rfar = rpdr->far;
rfar_forw = &rfar->desc.forw_params;
OSMO_ASSERT(pdr->access_to_core);
OSMO_ASSERT(rpdr->core_to_access);
/* To decaps incoming on Access, we need to have a local F-TEID assigned for which to receive GTP packets. */
if (!pdr->local_f_teid || pdr->local_f_teid->choose_flag) {
log_inactive_pdr_set(pdr, "missing local F-TEID", pdr);
return;
}
/* To encaps outgoing on Access, we need to have a remote F-TEID assigned to send out in GTP packets */
if (!rfar->desc.forw_params_present) {
log_inactive_pdr_set(pdr, "missing FAR Forwarding Parameters", rpdr);
return;
}
if (!rfar_forw->outer_header_creation_present) {
log_inactive_pdr_set(pdr, "missing FAR Outer Header Creation", rpdr);
return;
}
if (!rfar_forw->outer_header_creation.teid_present) {
log_inactive_pdr_set(pdr, "missing TEID in FAR Outer Header Creation", rpdr);
return;
}
if (!rfar_forw->outer_header_creation.ip_addr.v4_present) {
log_inactive_pdr_set(pdr, "missing IPv4 in FAR Outer Header Creation", rpdr);
return;
}
/* To receive IP packets incoming on Core, we need to know the assigned IP address for the UE */
if (!rpdr->desc.pdi.ue_ip_address_present) {
log_inactive_pdr_set(pdr, "missing UE IP Address in PDI", rpdr);
return;
}
if (!rpdr->desc.pdi.ue_ip_address.ip_addr.v4_present) {
log_inactive_pdr_set(pdr, "UE IP Address in PDI is not IPv4", rpdr);
return;
}
if (!rpdr->desc.pdi.ue_ip_address.ip_is_destination) {
log_inactive_pdr_set(pdr, "UE IP Address in PDI is not flagged as destination", rpdr);
return;
}
pdr->active = true;
pdr->far->active = true;
rpdr->active = true;
rpdr->far->active = true;
LOGPFSML(session->fi, LOGL_DEBUG, "Active PDR set: %s\n", pdr_to_str_c(OTC_SELECT, pdr));
LOGPFSML(session->fi, LOGL_DEBUG, "Active PDR set: + %s\n", pdr_to_str_c(OTC_SELECT, rpdr));
talloc_free(pdr->inactive_reason);
pdr->inactive_reason = NULL;
talloc_free(rpdr->inactive_reason);
rpdr->inactive_reason = NULL;
a = talloc(ctx, struct up_gtp_action);
OSMO_ASSERT(a);
*a = (struct up_gtp_action){
.session = session,
.pdr_access = pdr->desc.pdr_id,
.pdr_core = rpdr->desc.pdr_id,
.kind = UP_GTP_U_TUNEND,
.tunend = {
.access = {
.local_teid = pdr->local_f_teid->fixed.teid,
.remote_teid = rfar_forw->outer_header_creation.teid,
.gtp_remote_addr = rfar_forw->outer_header_creation.ip_addr.v4,
},
.core = {
.ue_local_addr = rpdr->desc.pdi.ue_ip_address.ip_addr.v4,
},
},
};
llist_add_tail(&a->entry, dst);
}
/* A GTP tunnel on Access side, mapping to another GTP tunnel on Core side and vice versa.
* The PDR and its reverse PDR must both have an outer-header-removal and a local F-TEID.
* Both FARs must have an outer-header creation with a remote F-TEID.
* \param pdr A rule detecting packets on Access, where pdr->reverse_pdr detects packets on Core.
*/
static void add_gtp_action_tunmap(void *ctx, struct llist_head *dst, struct pdr *pdr)
{
struct up_session *session = pdr->session;
struct up_gtp_action *a;
struct pdr *rpdr;
struct far *far;
struct osmo_pfcp_ie_forw_params *far_forw;
struct far *rfar;
struct osmo_pfcp_ie_forw_params *rfar_forw;
OSMO_ASSERT(pdr->far);
OSMO_ASSERT(pdr->reverse_pdr);
OSMO_ASSERT(pdr->reverse_pdr->far);
far = pdr->far;
far_forw = &far->desc.forw_params;
rpdr = pdr->reverse_pdr;
rfar = rpdr->far;
rfar_forw = &rfar->desc.forw_params;
OSMO_ASSERT(pdr->access_to_core);
OSMO_ASSERT(rpdr->core_to_access);
/* To decaps incoming on Access, we need to have a local F-TEID assigned for which to receive GTP packets. */
if (!pdr->local_f_teid || pdr->local_f_teid->choose_flag) {
log_inactive_pdr_set(pdr, "missing local F-TEID (Access side)", pdr);
return;
}
/* To decaps incoming on Core, we need to have a local F-TEID assigned for which to receive GTP packets. */
if (!rpdr->local_f_teid || rpdr->local_f_teid->choose_flag) {
log_inactive_pdr_set(pdr, "missing local F-TEID (Core side)", pdr);
return;
}
/* To encaps outgoing on Core, we need to have a remote F-TEID assigned to send out in GTP packets */
if (!far->desc.forw_params_present) {
log_inactive_pdr_set(pdr, "missing FAR Forwarding Parameters (Access side)", pdr);
return;
}
if (!far_forw->outer_header_creation_present) {
log_inactive_pdr_set(pdr, "missing FAR Outer Header Creation (Access side)", pdr);
return;
}
if (!far_forw->outer_header_creation.teid_present) {
log_inactive_pdr_set(pdr, "missing TEID in FAR Outer Header Creation (Access side)", pdr);
return;
}
if (!far_forw->outer_header_creation.ip_addr.v4_present) {
log_inactive_pdr_set(pdr, "missing IPv4 in FAR Outer Header Creation (Access side)", pdr);
return;
}
/* To encaps outgoing on Access, we need to have a remote F-TEID assigned to send out in GTP packets */
if (!rfar->desc.forw_params_present) {
log_inactive_pdr_set(pdr, "missing FAR Forwarding Parameters (Access side)", rpdr);
return;
}
if (!rfar_forw->outer_header_creation_present) {
log_inactive_pdr_set(pdr, "missing FAR Outer Header Creation (Access side)", rpdr);
return;
}
if (!rfar_forw->outer_header_creation.teid_present) {
log_inactive_pdr_set(pdr, "missing TEID in FAR Outer Header Creation (Access side)", rpdr);
return;
}
if (!rfar_forw->outer_header_creation.ip_addr.v4_present) {
log_inactive_pdr_set(pdr, "missing IPv4 in FAR Outer Header Creation (Access side)", rpdr);
return;
}
pdr->active = true;
far->active = true;
rpdr->active = true;
rfar->active = true;
LOGPFSML(session->fi, LOGL_DEBUG, "Active PDR set: %s\n", pdr_to_str_c(OTC_SELECT, pdr));
LOGPFSML(session->fi, LOGL_DEBUG, "Active PDR set: + %s\n", pdr_to_str_c(OTC_SELECT, rpdr));
talloc_free(pdr->inactive_reason);
pdr->inactive_reason = NULL;
talloc_free(rpdr->inactive_reason);
rpdr->inactive_reason = NULL;
a = talloc(ctx, struct up_gtp_action);
OSMO_ASSERT(a);
*a = (struct up_gtp_action){
.session = session,
.pdr_access = pdr->desc.pdr_id,
.pdr_core = rpdr->desc.pdr_id,
.kind = UP_GTP_U_TUNMAP,
.tunmap = {
.access = {
.local_teid = pdr->local_f_teid->fixed.teid,
.remote_teid = rfar_forw->outer_header_creation.teid,
.gtp_remote_addr = rfar_forw->outer_header_creation.ip_addr.v4,
},
.core = {
.local_teid = rpdr->local_f_teid->fixed.teid,
.remote_teid = far_forw->outer_header_creation.teid,
.gtp_remote_addr = far_forw->outer_header_creation.ip_addr.v4,
},
},
};
llist_add_tail(&a->entry, dst);
}
/* Analyse all PDRs and FARs and find configurations that match either a GTP encaps/decaps or a GTP forward rule. Add to
* list dst, containing struct up_gtp_action instances allocated from ctx. */
static enum osmo_pfcp_cause find_gtp_actions(void *ctx, struct llist_head *dst, struct up_session *session)
{
struct far *far;
struct pdr *pdr;
llist_for_each_entry(far, &session->fars, entry) {
far->active = false;
}
llist_for_each_entry(pdr, &session->pdrs, entry) {
pdr->reverse_pdr = NULL;
pdr_classify(pdr);
pdr->active = false;
}
llist_for_each_entry(pdr, &session->pdrs, entry) {
struct pdr *other;
/* Already paired up in an earlier iteration? */
if (pdr->reverse_pdr)
continue;
/* In this outer loop, only follow the access_to_core directed PDRs, in the inner loop find the matching
* core_to_access PDR. i.e. we are looking only at PDRs detecting packets on the Access side, pairing up
* with "reverse PDRs" detecting packets on the Core side. */
if (!pdr->access_to_core)
continue;
/* If a required local addr + TEID is not known, we cannot pair this PDR up */
if (pdr->rx_decaps && !pdr->local_f_teid)
continue;
/* Try to find a matching PDR+FAR that points in the reverse direction. */
llist_for_each_entry(other, &session->pdrs, entry) {
/* Already paired up in an earlier iteration? */
if (other->reverse_pdr)
continue;
/* Looking for a PDR facing the other way */
if (!other->core_to_access)
continue;
/* GTP header-ness must match, in reverse. */
if (pdr->rx_decaps != other->forw_encaps
|| pdr->forw_encaps != other->rx_decaps)
continue;
/* TEID: when adding a GTP header, we only know the remote side TEID sent out.
* When removing a GTP header, we only know the local TEID that the remote side sends here.
* So can't match up TEIDs. */
/* Just match these up, simply based on direction and encaps/decaps match */
pdr_reverse_set(pdr, other);
}
}
/* Iterate again to create the GTP actions (and debug log what is going on) */
llist_for_each_entry(pdr, &session->pdrs, entry) {
if (!pdr->reverse_pdr) {
LOGPFSML(session->fi, LOGL_INFO, "Inactive PDR: no matching reverse PDR for: %s\n", pdr_to_str_c(OTC_SELECT, pdr));
continue;
}
/* Iterate in direction Access-to-Core, where pdr->reverse_pdr will be the Core-to-Access counterpart. */
if (!pdr->access_to_core)
continue;
if (pdr->rx_decaps && !pdr->forw_encaps)
add_gtp_action_tunend(ctx, dst, pdr);
else if (pdr->rx_decaps && pdr->forw_encaps)
add_gtp_action_tunmap(ctx, dst, pdr);
else {
/* log the details of both PDRs in two separate log lines */
log_inactive_pdr_set(pdr, "not implemented", pdr);
log_inactive_pdr_set(pdr, "not implemented", pdr->reverse_pdr);
}
}
return OSMO_PFCP_CAUSE_REQUEST_ACCEPTED;
}
static enum osmo_pfcp_cause setup_gtp_actions(struct up_session *session, struct llist_head *want)
{
struct up_gtp_action *a, *a_safe;
struct up_gtp_action *w, *w_safe;
LOGPFSML(session->fi, LOGL_DEBUG, "GTP actions: %u previously active; want active: %u\n",
llist_count(&session->active_gtp_actions), llist_count(want));
llist_for_each_entry(w, want, entry) {
LOGPFSML(session->fi, LOGL_DEBUG, "want: %s\n", up_gtp_action_to_str_c(OTC_SELECT, w));
w->handle = NULL;
}
/* Match up the wanted GTP tunnels with the already active ones */
llist_for_each_entry(a, &session->active_gtp_actions, entry) {
LOGPFSML(session->fi, LOGL_DEBUG, "active: %s\n", up_gtp_action_to_str_c(OTC_SELECT, a));
a->handle = NULL;
llist_for_each_entry(w, want, entry) {
/* Already matched up? */
if (w->handle)
continue;
if (up_gtp_action_cmp(a, w))
continue;
/* Found a match, mark. */
a->handle = w;
w->handle = a;
}
}
/* At this point, all matching entries in session->active_gtp_actions and the 'want' list have a handle != NULL.
* If handle == NULL in active_gtp_actions, it means it has no match in the wanted list and is to be torn down.
* If handle == NULL in 'want', it means it has no match in the active list and should be created. */
/* Shut down all active GTP rules that no longer appear in the session setup. */
llist_for_each_entry_safe(a, a_safe, &session->active_gtp_actions, entry) {
if (a->handle)
continue;
LOGPFSML(session->fi, LOGL_DEBUG, "disabling: %s\n", up_gtp_action_to_str_c(OTC_SELECT, a));
up_gtp_action_disable(a);
llist_del(&a->entry);
talloc_free(a);
}
/* Set up all GTP tunnels requested in the session setup, but not active yet */
llist_for_each_entry_safe(w, w_safe, want, entry) {
if (w->handle)
continue;
LOGPFSML(session->fi, LOGL_DEBUG, "enabling: %s\n", up_gtp_action_to_str_c(OTC_SELECT, w));
/* If enabling fails, don't add to the active list. Error logging is done in up_gtp_action_enable(). */
if (up_gtp_action_enable(w))
continue;
/* Successfully activated, move the entry from 'want' to 'active_gtp_actions' */
llist_del(&w->entry);
talloc_steal(session, w);
llist_add_tail(&w->entry, &session->active_gtp_actions);
}
return OSMO_PFCP_CAUSE_REQUEST_ACCEPTED;
}
static void drop_gtp_actions(struct up_session *session)
{
struct llist_head empty;
INIT_LLIST_HEAD(&empty);
/* Call setup_gtp_actions() with an empty list, to clean up all active GTP actions */
setup_gtp_actions(session, &empty);
}
/* Check whether the Packet Detection and Forwarding Action Rules amount to an encaps/decaps of GTP or a GTP tunnel
* mapping, or none of the two. */
static enum osmo_pfcp_cause up_session_setup_gtp(struct up_session *session)
{
enum osmo_pfcp_cause cause;
struct llist_head want_gtp_actions;
INIT_LLIST_HEAD(&want_gtp_actions);
cause = find_gtp_actions(OTC_SELECT, &want_gtp_actions, session);
if (cause != OSMO_PFCP_CAUSE_REQUEST_ACCEPTED)
return cause;
cause = setup_gtp_actions(session, &want_gtp_actions);
return cause;
}
bool up_session_is_active(struct up_session *session)
{
return session && (session->fi->state == UP_SESSION_ST_ESTABLISHED) && !llist_empty(&session->active_gtp_actions);
}
bool up_session_is_fully_active(struct up_session *session, int *active_p, int *inactive_p)
{
struct pdr *pdr;
int inactive = 0;
int active = 0;
llist_for_each_entry(pdr, &session->pdrs, entry) {
if (pdr->active)
active++;
else
inactive++;
}
if (!up_session_is_active(session)) {
inactive += active;
active = 0;
}
if (active_p)
*active_p = active;
if (inactive_p)
*inactive_p = inactive;
return active && !inactive;
}
int up_session_to_str_buf(char *buf, size_t buflen, struct up_session *session)
{
struct osmo_strbuf sb = { .buf = buf, .len = buflen };
if (!session) {
OSMO_STRBUF_PRINTF(sb, "session=null");
return sb.chars_needed;
}
OSMO_STRBUF_PRINTF(sb, "peer:%s SEID-r:0x%"PRIx64" SEID-l:0x%"PRIx64" state:%s",
up_peer_remote_addr_str(session->up_peer),
session->cp_f_seid.seid, session->up_seid,
osmo_fsm_inst_state_name(session->fi));
return sb.chars_needed;
}
char *up_session_to_str_c(void *ctx, struct up_session *session)
{
OSMO_NAME_C_IMPL(ctx, 128, "ERROR", up_session_to_str_buf, session)
}