diff --git a/include/osmocom/pfcp/Makefile.am b/include/osmocom/pfcp/Makefile.am index 5d2db19..898113f 100644 --- a/include/osmocom/pfcp/Makefile.am +++ b/include/osmocom/pfcp/Makefile.am @@ -5,6 +5,7 @@ pfcp_HEADERS = \ pfcp_msg.h \ pfcp_proto.h \ pfcp_strs.h \ + pfcp_cp_peer.h \ $(NULL) pfcpdir = $(includedir)/osmocom/pfcp diff --git a/include/osmocom/pfcp/pfcp_cp_peer.h b/include/osmocom/pfcp/pfcp_cp_peer.h new file mode 100644 index 0000000..8c6e448 --- /dev/null +++ b/include/osmocom/pfcp/pfcp_cp_peer.h @@ -0,0 +1,63 @@ +/* + * (C) 2021-2022 by sysmocom - s.f.m.c. GmbH + * All Rights Reserved. + * + * Author: Neels Janosch Hofmeyr + * + * 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 . + * + */ + +#pragma once + +#include +#include + +#include +#include +#include + +struct osmo_fsm_inst; +struct osmo_pfcp_endpoint; +struct osmo_pfcp_cp_peer; + +typedef void (*osmo_pfcp_cp_peer_assoc_cb)(struct osmo_pfcp_cp_peer *cp_peer, bool associated); + +struct osmo_pfcp_cp_peer { + struct osmo_fsm_inst *fi; + struct osmo_pfcp_endpoint *ep; + struct osmo_sockaddr remote_addr; + uint64_t next_seid_state; + + /* If non-NULL, called whenever the peer completes a PFCP Association, and when it loses association. + * Argument associated == true means the peer has just associated; + * associated == false means the association has been lost. */ + osmo_pfcp_cp_peer_assoc_cb assoc_cb; + /* Application private data for assoc_cb, in case ep->priv does not suffice. */ + void *priv; + + struct osmo_use_count use_count; + struct osmo_use_count_entry use_count_buf[128]; +}; + +struct osmo_pfcp_cp_peer *osmo_pfcp_cp_peer_alloc(void *ctx, + struct osmo_pfcp_endpoint *ep, + const struct osmo_sockaddr *remote_addr); +int osmo_pfcp_cp_peer_associate(struct osmo_pfcp_cp_peer *cp_peer); +bool osmo_pfcp_cp_peer_is_associated(const struct osmo_pfcp_cp_peer *cp_peer); +struct osmo_pfcp_msg *osmo_pfcp_cp_peer_new_msg_tx(struct osmo_pfcp_cp_peer *cp_peer, + enum osmo_pfcp_message_type msg_type); +void osmo_pfcp_cp_peer_set_msg_ctx(struct osmo_pfcp_cp_peer *cp_peer, struct osmo_pfcp_msg *m); diff --git a/src/libosmo-pfcp/Makefile.am b/src/libosmo-pfcp/Makefile.am index 1f4b80f..a1e9904 100644 --- a/src/libosmo-pfcp/Makefile.am +++ b/src/libosmo-pfcp/Makefile.am @@ -28,6 +28,7 @@ libosmo_pfcp_la_SOURCES = \ pfcp_ies_custom.c \ pfcp_msg.c \ pfcp_strs.c \ + pfcp_cp_peer.c \ \ pfcp_ies_auto.c \ $(NULL) diff --git a/src/libosmo-pfcp/pfcp_cp_peer.c b/src/libosmo-pfcp/pfcp_cp_peer.c new file mode 100644 index 0000000..c47b440 --- /dev/null +++ b/src/libosmo-pfcp/pfcp_cp_peer.c @@ -0,0 +1,404 @@ +/* + * (C) 2021-2022 by sysmocom - s.f.m.c. GmbH + * All Rights Reserved. + * + * Author: Neels Janosch Hofmeyr + * + * 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 + +#define LOG_CP_PEER(CP_PEER, LOGLEVEL, FMT, ARGS...) \ + LOGPFSML((CP_PEER)->fi, LOGLEVEL, FMT, ##ARGS) + +enum pfcp_cp_peer_fsm_state { + PFCP_CP_PEER_ST_DISABLED, + PFCP_CP_PEER_ST_WAIT_ASSOC_SETUP_RESP, + PFCP_CP_PEER_ST_WAIT_RETRY, + PFCP_CP_PEER_ST_ASSOCIATED, + PFCP_CP_PEER_ST_GRACEFUL_RELEASE, + PFCP_CP_PEER_ST_WAIT_USE_COUNT, +}; + +enum pfcp_cp_peer_fsm_event { + PFCP_CP_PEER_EV_RX_ASSOC_SETUP_RESP, + PFCP_CP_PEER_EV_RX_ASSOC_UPDATE_REQ, + PFCP_CP_PEER_EV_USE_COUNT_ZERO, +}; + +static const struct value_string pfcp_cp_peer_fsm_event_names[] = { + OSMO_VALUE_STRING(PFCP_CP_PEER_EV_RX_ASSOC_SETUP_RESP), + OSMO_VALUE_STRING(PFCP_CP_PEER_EV_RX_ASSOC_UPDATE_REQ), + OSMO_VALUE_STRING(PFCP_CP_PEER_EV_USE_COUNT_ZERO), + {} +}; + +static struct osmo_fsm pfcp_cp_peer_fsm; + +static const struct osmo_tdef_state_timeout pfcp_cp_peer_fsm_timeouts[32] = { + [PFCP_CP_PEER_ST_WAIT_RETRY] = { .T = -26 }, + /* PFCP_CP_PEER_ST_WAIT_ASSOC_SETUP_RESP is terminated by the on_pfcp_assoc_resp() callback */ + [PFCP_CP_PEER_ST_GRACEFUL_RELEASE] = { .T = -21 }, +}; + +/* Transition to a state, using the T timer defined in pfcp_cp_peer_fsm_timeouts. + * Assumes local variable fi exists. */ +#define osmo_pfcp_cp_peer_fsm_state_chg(state) \ + osmo_tdef_fsm_inst_state_chg(fi, state, \ + pfcp_cp_peer_fsm_timeouts, \ + osmo_pfcp_tdefs, \ + 5) + +static int pfcp_cp_peer_use_cb(struct osmo_use_count_entry *e, int32_t old_use_count, const char *file, int line) +{ + struct osmo_pfcp_cp_peer *cp_peer = e->use_count->talloc_object; + + if (!e->use) + return -EINVAL; + + LOGPFSMSLSRC(cp_peer->fi, DLPFCP, LOGL_DEBUG, file, line, + "%s %s: now used by %s\n", + e->count > old_use_count ? "+" : "-", e->use, + osmo_use_count_to_str_c(OTC_SELECT, &cp_peer->use_count)); + + if (e->count < 0) + return -ERANGE; + + if (osmo_use_count_total(&cp_peer->use_count) == 0) + osmo_fsm_inst_dispatch(cp_peer->fi, PFCP_CP_PEER_EV_USE_COUNT_ZERO, NULL); + return 0; +} + +/* Allocate PFCP CP peer FSM and start sending PFCP Association Setup Request messages to remote_addr, using endpoint + * ep. As soon as a successful response is received, change to state PFCP_CP_PEER_ST_ASSOCIATED. + */ +struct osmo_pfcp_cp_peer *osmo_pfcp_cp_peer_alloc(void *ctx, + struct osmo_pfcp_endpoint *ep, + const struct osmo_sockaddr *remote_addr) +{ + struct osmo_pfcp_cp_peer *cp_peer; + struct osmo_fsm_inst *fi = osmo_fsm_inst_alloc(&pfcp_cp_peer_fsm, ctx, NULL, LOGL_DEBUG, NULL); + OSMO_ASSERT(fi); + + cp_peer = talloc(fi, struct osmo_pfcp_cp_peer); + OSMO_ASSERT(cp_peer); + fi->priv = cp_peer; + *cp_peer = (struct osmo_pfcp_cp_peer){ + .fi = fi, + .ep = ep, + .remote_addr = *remote_addr, + .use_count = { + .talloc_object = cp_peer, + .use_cb = pfcp_cp_peer_use_cb, + }, + }; + osmo_use_count_make_static_entries(&cp_peer->use_count, cp_peer->use_count_buf, ARRAY_SIZE(cp_peer->use_count_buf)); + + osmo_fsm_inst_update_id_f_sanitize(fi, '-', osmo_sockaddr_to_str_c(OTC_SELECT, &cp_peer->remote_addr)); + return cp_peer; +} + +int osmo_pfcp_cp_peer_associate(struct osmo_pfcp_cp_peer *cp_peer) +{ + struct osmo_fsm_inst *fi = cp_peer->fi; + + switch (fi->state) { + case PFCP_CP_PEER_ST_DISABLED: + case PFCP_CP_PEER_ST_WAIT_RETRY: + /* Idling. Send Association Setup Request now. */ + return osmo_pfcp_cp_peer_fsm_state_chg(PFCP_CP_PEER_ST_WAIT_ASSOC_SETUP_RESP); + default: + /* Already associated, or busy associating. */ + return 0; + case PFCP_CP_PEER_ST_WAIT_USE_COUNT: + /* Already asked to deallocate. */ + return -ENOLINK; + } + return 0; +} + +static int pfcp_cp_peer_fsm_timer_cb(struct osmo_fsm_inst *fi) +{ + switch (fi->state) { + + case PFCP_CP_PEER_ST_WAIT_RETRY: + osmo_pfcp_cp_peer_fsm_state_chg(PFCP_CP_PEER_ST_WAIT_ASSOC_SETUP_RESP); + return 0; + + default: + return 1; + } +} + +static int on_pfcp_assoc_resp(struct osmo_pfcp_msg *req, struct osmo_pfcp_msg *rx_resp, const char *errmsg); + +/* Send PFCP Association Setup Request */ +static void pfcp_cp_peer_wait_assoc_setup_resp_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state) +{ + struct osmo_pfcp_cp_peer *cp_peer = fi->priv; + struct osmo_pfcp_msg *m; + + m = osmo_pfcp_cp_peer_new_msg_tx(cp_peer, OSMO_PFCP_MSGT_ASSOC_SETUP_REQ); + m->ies.assoc_setup_req.recovery_time_stamp = cp_peer->ep->recovery_time_stamp; + + m->ies.assoc_setup_req.cp_function_features_present = true; + osmo_pfcp_bits_set(m->ies.assoc_setup_req.cp_function_features.bits, OSMO_PFCP_CP_FEAT_BUNDL, true); + + m->ctx.resp_cb = on_pfcp_assoc_resp; + + LOG_CP_PEER(cp_peer, LOGL_NOTICE, "Associating with %s...\n", + osmo_sockaddr_to_str_c(OTC_SELECT, &cp_peer->remote_addr)); + + if (osmo_pfcp_endpoint_tx(cp_peer->ep, m)) { + LOG_CP_PEER(cp_peer, LOGL_ERROR, "Failed to transmit PFCP Association Setup Request to UPF at %s\n", + osmo_sockaddr_to_str_c(OTC_SELECT, &cp_peer->remote_addr)); + osmo_pfcp_cp_peer_fsm_state_chg(PFCP_CP_PEER_ST_WAIT_RETRY); + } +} + +static int on_pfcp_assoc_resp(struct osmo_pfcp_msg *req, struct osmo_pfcp_msg *rx_resp, const char *errmsg) +{ + struct osmo_fsm_inst *fi = req->ctx.peer_fi; + struct osmo_pfcp_cp_peer *cp_peer = fi->priv; + enum osmo_pfcp_cause *cause; + + if (!rx_resp) { + LOG_CP_PEER(cp_peer, LOGL_ERROR, "Error: PFCP Association Setup Response: %s\n", + errmsg ? : "no response received"); + goto assoc_failed; + } + + cause = osmo_pfcp_msg_cause(rx_resp); + if (!cause) { + LOG_CP_PEER(cp_peer, LOGL_ERROR, "Invalid PFCP Association Setup Response: no Cause value\n"); + goto assoc_failed; + } + if (*cause != OSMO_PFCP_CAUSE_REQUEST_ACCEPTED) { + LOG_CP_PEER(cp_peer, LOGL_ERROR, "UPF rejected PFCP Association Setup Request with Cause: %s\n", + osmo_pfcp_cause_str(*cause)); + goto assoc_failed; + } + + osmo_fsm_inst_dispatch(fi, PFCP_CP_PEER_EV_RX_ASSOC_SETUP_RESP, rx_resp); + return 0; + +assoc_failed: + osmo_pfcp_cp_peer_fsm_state_chg(PFCP_CP_PEER_ST_WAIT_RETRY); + return 0; +} + +static void pfcp_cp_peer_wait_assoc_setup_resp_action(struct osmo_fsm_inst *fi, uint32_t event, void *data) +{ + switch (event) { + + case PFCP_CP_PEER_EV_RX_ASSOC_SETUP_RESP: + osmo_pfcp_cp_peer_fsm_state_chg(PFCP_CP_PEER_ST_ASSOCIATED); + break; + + default: + OSMO_ASSERT(false); + } +} + +static void pfcp_cp_peer_associated_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state) +{ + struct osmo_pfcp_cp_peer *cp_peer = fi->priv; + LOG_CP_PEER(cp_peer, LOGL_NOTICE, "Associated with UPF %s\n", + osmo_sockaddr_to_str_c(OTC_SELECT, &cp_peer->remote_addr)); + if (cp_peer->assoc_cb) + cp_peer->assoc_cb(cp_peer, true); +} + +static void pfcp_cp_peer_associated_action(struct osmo_fsm_inst *fi, uint32_t event, void *data) +{ + struct osmo_pfcp_cp_peer *cp_peer = fi->priv; + + switch (event) { + + case PFCP_CP_PEER_EV_RX_ASSOC_UPDATE_REQ: + LOG_CP_PEER(cp_peer, LOGL_ERROR, "PFCP Association Update Request is not implemented\n"); + break; + + default: + OSMO_ASSERT(false); + } +} + +static void pfcp_cp_peer_associated_onleave(struct osmo_fsm_inst *fi, uint32_t next_state) +{ + struct osmo_pfcp_cp_peer *cp_peer = fi->priv; + LOG_CP_PEER(cp_peer, LOGL_NOTICE, "Disassociating from UPF %s\n", + osmo_sockaddr_to_str_c(OTC_SELECT, &cp_peer->remote_addr)); + if (cp_peer->assoc_cb) + cp_peer->assoc_cb(cp_peer, false); +} + +static void pfcp_cp_peer_graceful_release_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state) +{ + struct osmo_pfcp_cp_peer *cp_peer = fi->priv; + LOG_CP_PEER(cp_peer, LOGL_ERROR, "PFCP graceful release is not implemented\n"); +} + +static void pfcp_cp_peer_graceful_release_action(struct osmo_fsm_inst *fi, uint32_t event, void *data) +{ + struct osmo_pfcp_cp_peer *cp_peer = fi->priv; + LOG_CP_PEER(cp_peer, LOGL_ERROR, "PFCP graceful release is not implemented\n"); +} + +static void pfcp_cp_peer_wait_use_count_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state) +{ + struct osmo_pfcp_cp_peer *cp_peer = fi->priv; + if (!osmo_use_count_total(&cp_peer->use_count)) + osmo_fsm_inst_term(fi, OSMO_FSM_TERM_REGULAR, NULL); +} + +static void pfcp_cp_peer_allstate_action(struct osmo_fsm_inst *fi, uint32_t event, void *data) +{ + switch (event) { + case PFCP_CP_PEER_EV_USE_COUNT_ZERO: + switch (fi->state) { + default: + /* still busy, ignore. */ + return; + case PFCP_CP_PEER_ST_WAIT_USE_COUNT: + /* Waiting for deallocation; now there are no more users, deallocate. */ + osmo_fsm_inst_term(fi, OSMO_FSM_TERM_REGULAR, NULL); + return; + } + default: + OSMO_ASSERT(false); + } +} + +#define S(x) (1 << (x)) + +static const struct osmo_fsm_state pfcp_cp_peer_fsm_states[] = { + [PFCP_CP_PEER_ST_DISABLED] = { + .name = "disabled", + .out_state_mask = 0 + | S(PFCP_CP_PEER_ST_WAIT_ASSOC_SETUP_RESP) + | S(PFCP_CP_PEER_ST_WAIT_USE_COUNT) + , + }, + [PFCP_CP_PEER_ST_WAIT_ASSOC_SETUP_RESP] = { + .name = "wait_assoc_setup_resp", + .in_event_mask = 0 + | S(PFCP_CP_PEER_EV_RX_ASSOC_SETUP_RESP) + , + .out_state_mask = 0 + | S(PFCP_CP_PEER_ST_ASSOCIATED) + | S(PFCP_CP_PEER_ST_WAIT_RETRY) + | S(PFCP_CP_PEER_ST_WAIT_USE_COUNT) + , + .onenter = pfcp_cp_peer_wait_assoc_setup_resp_onenter, + .action = pfcp_cp_peer_wait_assoc_setup_resp_action, + }, + [PFCP_CP_PEER_ST_WAIT_RETRY] = { + .name = "wait_retry", + .out_state_mask = 0 + | S(PFCP_CP_PEER_ST_WAIT_ASSOC_SETUP_RESP) + | S(PFCP_CP_PEER_ST_WAIT_USE_COUNT) + , + }, + [PFCP_CP_PEER_ST_ASSOCIATED] = { + .name = "associated", + .in_event_mask = 0 + | S(PFCP_CP_PEER_EV_RX_ASSOC_UPDATE_REQ) + , + .out_state_mask = 0 + | S(PFCP_CP_PEER_ST_WAIT_ASSOC_SETUP_RESP) + | S(PFCP_CP_PEER_ST_GRACEFUL_RELEASE) + | S(PFCP_CP_PEER_ST_WAIT_USE_COUNT) + , + .onenter = pfcp_cp_peer_associated_onenter, + .action = pfcp_cp_peer_associated_action, + .onleave = pfcp_cp_peer_associated_onleave, + }, + [PFCP_CP_PEER_ST_GRACEFUL_RELEASE] = { + .name = "graceful_release", + .in_event_mask = 0 + , + .out_state_mask = 0 + | S(PFCP_CP_PEER_ST_WAIT_RETRY) + | S(PFCP_CP_PEER_ST_WAIT_USE_COUNT) + , + .onenter = pfcp_cp_peer_graceful_release_onenter, + .action = pfcp_cp_peer_graceful_release_action, + }, + [PFCP_CP_PEER_ST_WAIT_USE_COUNT] = { + .name = "wait_use_count", + .in_event_mask = 0 + | S(PFCP_CP_PEER_EV_USE_COUNT_ZERO) + , + .onenter = pfcp_cp_peer_wait_use_count_onenter, + }, +}; + +static struct osmo_fsm pfcp_cp_peer_fsm = { + .name = "pfcp_cp_peer", + .states = pfcp_cp_peer_fsm_states, + .num_states = ARRAY_SIZE(pfcp_cp_peer_fsm_states), + .log_subsys = DLPFCP, + .event_names = pfcp_cp_peer_fsm_event_names, + .timer_cb = pfcp_cp_peer_fsm_timer_cb, + .allstate_action = pfcp_cp_peer_allstate_action, + .allstate_event_mask = 0 + | S(PFCP_CP_PEER_EV_USE_COUNT_ZERO) + , +}; + +static __attribute__((constructor)) void pfcp_cp_peer_fsm_register(void) +{ + OSMO_ASSERT(osmo_fsm_register(&pfcp_cp_peer_fsm) == 0); +} + +bool osmo_pfcp_cp_peer_is_associated(const struct osmo_pfcp_cp_peer *cp_peer) +{ + return cp_peer && cp_peer->fi->state == PFCP_CP_PEER_ST_ASSOCIATED; +} + +void osmo_pfcp_cp_peer_set_msg_ctx(struct osmo_pfcp_cp_peer *cp_peer, struct osmo_pfcp_msg *m) +{ + if (m->ctx.peer_fi) + return; + + m->ctx.peer_fi = cp_peer->fi; + + m->ctx.peer_use_count = &cp_peer->use_count; + m->ctx.peer_use_token = (m->rx ? "PFCPrx" : "PFCPtx"); + osmo_use_count_get_put(m->ctx.peer_use_count, m->ctx.peer_use_token, 1); +} + +struct osmo_pfcp_msg *osmo_pfcp_cp_peer_new_msg_tx(struct osmo_pfcp_cp_peer *cp_peer, + enum osmo_pfcp_message_type msg_type) +{ + struct osmo_pfcp_msg *m; + m = osmo_pfcp_msg_alloc_tx(cp_peer->ep, &cp_peer->remote_addr, &cp_peer->ep->cfg.local_node_id, NULL, + msg_type); + if (!m) + return m; + osmo_pfcp_cp_peer_set_msg_ctx(cp_peer, m); + return m; +}