diff --git a/doc/examples/osmo-upf/osmo-upf-create-dev.cfg b/doc/examples/osmo-upf/osmo-upf-create-dev.cfg new file mode 100644 index 0000000..15b69e6 --- /dev/null +++ b/doc/examples/osmo-upf/osmo-upf-create-dev.cfg @@ -0,0 +1,17 @@ +log stderr + logging filter all 1 + logging color 1 + logging print level 1 + logging print category 1 + logging print category-hex 0 + logging print file basename last + logging print extended-timestamp 1 + logging level set-all notice + logging level set-all info +#logging level set-all debug + +timer pfcp x24 5000 +pfcp + local-addr 127.0.0.1 +gtp + dev create apn23 diff --git a/doc/examples/osmo-upf/osmo-upf.cfg b/doc/examples/osmo-upf/osmo-upf.cfg index 8a2a407..0f241a5 100644 --- a/doc/examples/osmo-upf/osmo-upf.cfg +++ b/doc/examples/osmo-upf/osmo-upf.cfg @@ -6,4 +6,10 @@ log stderr logging print category-hex 0 logging print file basename last logging print extended-timestamp 1 + logging level set-all debug logging level set-all notice + logging level set-all info + +timer pfcp x24 5000 +pfcp + local-addr 127.0.0.1 diff --git a/include/osmocom/upf/Makefile.am b/include/osmocom/upf/Makefile.am index 2608e8f..333132c 100644 --- a/include/osmocom/upf/Makefile.am +++ b/include/osmocom/upf/Makefile.am @@ -1,3 +1,9 @@ noinst_HEADERS = \ + up_endpoint.h \ + up_peer.h \ + up_session.h \ upf.h \ + upf_gtp.h \ + upf_nft.h \ + up_gtp_action.h \ $(NULL) diff --git a/include/osmocom/upf/up_endpoint.h b/include/osmocom/upf/up_endpoint.h new file mode 100644 index 0000000..e7639e3 --- /dev/null +++ b/include/osmocom/upf/up_endpoint.h @@ -0,0 +1,45 @@ +/* (C) 2021-2022 by sysmocom - s.f.m.c. GmbH + * All Rights Reserved + * + * Author: Neels Hofmeyr + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation; either version 3 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include + +struct osmo_pfcp_msg; +struct osmo_pfcp_endpoint; +struct osmo_sockaddr; + +#define UP_USE_MSG_RX "msg-rx" +#define UP_USE_MSG_TX "msg-tx" + +struct up_endpoint { + struct osmo_pfcp_endpoint *pfcp_ep; + + struct llist_head peers; + + uint64_t next_seid_state; + uint32_t next_teid_state; +}; + +struct up_endpoint *up_endpoint_init(void *ctx, const struct osmo_sockaddr *local_addr); +void up_endpoint_free(struct up_endpoint **ep); + +uint64_t up_endpoint_next_seid(struct up_endpoint *ep); +uint32_t up_endpoint_next_teid(struct up_endpoint *ep); diff --git a/include/osmocom/upf/up_gtp_action.h b/include/osmocom/upf/up_gtp_action.h new file mode 100644 index 0000000..c4ce32d --- /dev/null +++ b/include/osmocom/upf/up_gtp_action.h @@ -0,0 +1,50 @@ +#pragma once + +#include + +#include +#include +#include + +#include +#include +#include + +#define LOG_UP_GTP_ACTION(A, LEVEL, FMT, ARGS...) \ + LOGP(DGTP, LEVEL, "%s: " FMT, up_gtp_action_to_str_c(OTC_SELECT, A), ##ARGS) + +struct up_session; + +enum up_gtp_action_kind { + UP_GTP_DROP, + UP_GTP_U_ENDECAPS, + UP_GTP_U_FORW, +}; + +struct up_gtp_action { + struct llist_head entry; + struct up_session *session; + + uint16_t pdr_core; + uint16_t pdr_access; + + enum up_gtp_action_kind kind; + union { + /* En-/De-capsulate GTP: add/remove a GTP header and forward the GTP payload from/to plain IP. */ + struct upf_gtp_tun_desc endecaps; + + /* Forward GTP: translate from one TEID to another and forward */ + struct upf_nft_forw_desc forw; + }; + + /* volatile loop variable to match up wanted and actually present GTP actions */ + void *handle; +}; + +int up_gtp_action_cmp(const struct up_gtp_action *a, const struct up_gtp_action *b); + +int up_gtp_action_enable(struct up_gtp_action *a); +int up_gtp_action_disable(struct up_gtp_action *a); + +int up_gtp_action_to_str_buf(char *buf, size_t buflen, const struct up_gtp_action *a); +char *up_gtp_action_to_str_c(void *ctx, const struct up_gtp_action *a); diff --git a/include/osmocom/upf/up_peer.h b/include/osmocom/upf/up_peer.h new file mode 100644 index 0000000..605b809 --- /dev/null +++ b/include/osmocom/upf/up_peer.h @@ -0,0 +1,75 @@ +/* (C) 2021-2022 by sysmocom - s.f.m.c. GmbH + * All Rights Reserved + * + * Author: Neels Hofmeyr + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation; either version 3 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include +#include +#include +#include + +#include + +enum up_peer_event { + UP_PEER_EV_RX_ASSOC_SETUP_REQ, + UP_PEER_EV_RX_ASSOC_UPD_REQ, + UP_PEER_EV_RX_ASSOC_REL_REQ, + UP_PEER_EV_RX_SESSION_EST_REQ, + UP_PEER_EV_HEARTBEAT_FAILURE, + UP_PEER_EV_USE_COUNT_ZERO, + UP_PEER_EV_SESSION_TERM, +}; + +struct up_peer { + struct llist_head entry; + + struct osmo_fsm_inst *fi; + struct up_endpoint *up_endpoint; + + /* peer's remote address */ + struct osmo_sockaddr remote_addr; + struct osmo_pfcp_ie_node_id remote_node_id; + uint32_t remote_recovery_timestamp; + + struct osmo_pfcp_ie_up_function_features local_up_features; + struct osmo_pfcp_ie_cp_function_features peer_cp_features; + + uint32_t next_seq_nr; + + struct osmo_fsm_inst *heartbeat_fi; + + struct osmo_use_count use_count; + struct osmo_use_count_entry use_count_buf[5]; + + DECLARE_HASHTABLE(sessions_by_up_seid, 6); + DECLARE_HASHTABLE(sessions_by_cp_seid, 6); +}; + +struct up_peer *up_peer_find_or_add(struct up_endpoint *up_ep, const struct osmo_sockaddr *remote_addr); +struct up_peer *up_peer_find(struct up_endpoint *up_ep, const struct osmo_sockaddr *remote_addr); + +void up_peer_set_msg_ctx(struct up_peer *peer, struct osmo_pfcp_msg *m); + +char *up_peer_remote_addr_str(struct up_peer *peer); + +struct osmo_pfcp_msg *up_peer_init_tx(struct up_peer *peer, struct osmo_pfcp_msg *in_reply_to, + enum osmo_pfcp_message_type message_type); + +void up_peer_free(struct up_peer *peer); diff --git a/include/osmocom/upf/up_session.h b/include/osmocom/upf/up_session.h new file mode 100644 index 0000000..2dacf1b --- /dev/null +++ b/include/osmocom/upf/up_session.h @@ -0,0 +1,94 @@ +#pragma once + +#include + +#include +#include + +#include + +struct osmo_fsm_inst; +struct osmo_pfcp_msg; +struct up_peer; + +enum up_session_fsm_event { + UP_SESSION_EV_RX_SESSION_EST_REQ, + UP_SESSION_EV_RX_SESSION_MOD_REQ, + UP_SESSION_EV_RX_SESSION_DEL_REQ, + UP_SESSION_EV_USE_COUNT_ZERO, +}; + +enum up_session_kind { + UP_SESSION_DROP, + UP_SESSION_GTP_U_ENDECAPS, + UP_SESSION_GTP_U_FORW, +}; + +struct up_session { + struct hlist_node node_by_up_seid; + struct hlist_node node_by_cp_seid; + + struct osmo_fsm_inst *fi; + struct up_peer *up_peer; + + struct osmo_pfcp_ie_f_seid cp_f_seid; + uint64_t up_seid; + + struct osmo_use_count use_count; + struct osmo_use_count_entry use_count_buf[8]; + + struct llist_head pdrs; + struct llist_head fars; + struct llist_head chosen_f_teids; + + struct llist_head active_gtp_actions; +}; + +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 *up_session_find_by_up_seid(struct up_peer *peer, uint64_t up_seid); +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 *up_session_find_by_local_teid(struct up_peer *peer, uint32_t teid); + +void up_session_set_msg_ctx(struct up_session *session, struct osmo_pfcp_msg *m); + +char *up_session_gtp_status(struct up_session *session); +bool up_session_is_active(struct up_session *session); +bool up_session_is_fully_active(struct up_session *session, int *active_p, int *inactive_p); + +int up_session_discard(struct up_session *session); + +int up_session_to_str_buf(char *buf, size_t buflen, struct up_session *session); +char *up_session_to_str_c(void *ctx, struct up_session *session); + +struct pdr { + struct llist_head entry; + struct up_session *session; + + struct osmo_pfcp_ie_create_pdr desc; + struct osmo_pfcp_ie_f_teid *local_f_teid; + struct osmo_pfcp_ie_f_teid _local_f_teid_buf; + + struct far *far; + + bool rx_decaps; + bool forw_encaps; + bool forw_to_core; + bool forw_from_core; + + struct pdr *reverse_pdr; + bool active; + + char *inactive_reason; +}; + +int pdr_to_str_buf(char *buf, size_t buflen, const struct pdr *pdr); +char *pdr_to_str_c(void *ctx, const struct pdr *pdr); + +struct far { + struct llist_head entry; + struct up_session *session; + + struct osmo_pfcp_ie_create_far desc; + bool active; +}; diff --git a/include/osmocom/upf/up_session_to_gtp.c b/include/osmocom/upf/up_session_to_gtp.c new file mode 100644 index 0000000..e69de29 diff --git a/include/osmocom/upf/upf.h b/include/osmocom/upf/upf.h index 503c5a4..9cd4d0b 100644 --- a/include/osmocom/upf/upf.h +++ b/include/osmocom/upf/upf.h @@ -1,12 +1,65 @@ /* Global definitions for OsmoUPF */ #pragma once +#include +#include +#include +#include + +struct osmo_tdef; struct ctrl_handle; +struct upf_gtp_dev; + +#define UPF_PFCP_LISTEN_DEFAULT "0.0.0.0" + +extern struct osmo_tdef_group g_upf_tdef_groups[]; + +struct pfcp_vty_cfg { + char *local_addr; + uint16_t local_port; +}; + +struct gtp_vty_cfg_dev { + struct llist_head entry; + + bool create; + char *dev_name; + char *local_addr; +}; + +struct gtp_vty_cfg { + struct llist_head devs; +}; + struct g_upf { struct ctrl_handle *ctrl; + + struct { + struct pfcp_vty_cfg vty_cfg; + struct up_endpoint *ep; + } pfcp; + struct { + struct gtp_vty_cfg vty_cfg; + struct llist_head devs; + + struct mnl_socket *nl; + int32_t genl_id; + } gtp; }; extern struct g_upf *g_upf; +enum upf_log_subsys { + DREF, + DPEER, + DSESSION, + DGTP, +}; + void g_upf_alloc(void *ctx); +void upf_vty_init(); +int upf_pfcp_listen(); + +int upf_gtp_devs_open(); +void upf_gtp_devs_close(); diff --git a/include/osmocom/upf/upf_gtp.h b/include/osmocom/upf/upf_gtp.h new file mode 100644 index 0000000..e9e9589 --- /dev/null +++ b/include/osmocom/upf/upf_gtp.h @@ -0,0 +1,53 @@ +#pragma once + +#define PORT_GTP0_C 3386 +#define PORT_GTP0_U 3386 + +#define PORT_GTP1_C 2123 +#define PORT_GTP1_U 2152 + +struct upf_gtp_dev { + struct llist_head entry; + + bool created; + + char *name; + struct { + bool enabled; + struct osmo_sockaddr local_addr; + struct osmo_fd ofd; + } gtpv0; + struct { + struct osmo_sockaddr local_addr; + struct osmo_fd ofd; + } gtpv1; + bool sgsn_mode; + + uint32_t ifidx; + + struct llist_head tunnels; +}; + +struct upf_gtp_tun_desc { + uint32_t local_teid; + uint32_t remote_teid; + struct osmo_sockaddr ue_addr; + struct osmo_sockaddr gtp_remote_addr; +}; + +int upf_gtp_tun_desc_cmp(const struct upf_gtp_tun_desc *a, const struct upf_gtp_tun_desc *b); + +int upf_gtp_genl_open(); +void upf_gtp_genl_close(); + +struct upf_gtp_dev *upf_gtp_dev_create(const char *name, const char *local_addr, bool listen_for_gtpv0, bool sgsn_mode); +struct upf_gtp_dev *upf_gtp_dev_use(const char *name); +struct upf_gtp_dev *upf_gtp_dev_find_by_name(const char *name); +struct upf_gtp_dev *upf_gtp_dev_first(); + +int upf_gtp_dev_tunnel_add(struct upf_gtp_dev *dev, const struct upf_gtp_tun_desc *t); +bool upf_gtp_dev_is_tunnel_active(struct upf_gtp_dev *dev, const struct upf_gtp_tun_desc *t); +int upf_gtp_dev_tunnel_del(struct upf_gtp_dev *dev, const struct upf_gtp_tun_desc *t); + +int upf_gtp_dev_to_str_buf(char *buf, size_t buflen, const struct upf_gtp_dev *dev); +char *upf_gtp_dev_to_str_c(void *ctx, const struct upf_gtp_dev *dev); diff --git a/include/osmocom/upf/upf_nft.h b/include/osmocom/upf/upf_nft.h new file mode 100644 index 0000000..7a68d31 --- /dev/null +++ b/include/osmocom/upf/upf_nft.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +#include + +struct upf_nft_forw_desc { + struct { + struct osmo_sockaddr gtp_remote_addr; + uint32_t local_teid; + uint32_t remote_teid; + } access; + struct { + struct osmo_sockaddr gtp_remote_addr; + uint32_t local_teid; + uint32_t remote_teid; + } core; + uint32_t id; +}; + +int upf_nft_init(); +int upf_nft_free(); + +int upf_nft_forward_create(struct upf_nft_forw_desc *forw); +int upf_nft_forward_delete(struct upf_nft_forw_desc *forw); diff --git a/src/osmo-upf/Makefile.am b/src/osmo-upf/Makefile.am index ec04fc7..7618497 100644 --- a/src/osmo-upf/Makefile.am +++ b/src/osmo-upf/Makefile.am @@ -1,6 +1,7 @@ AM_CPPFLAGS = \ $(all_includes) \ -I$(top_srcdir)/include \ + -I$(top_builddir)/include \ -I$(top_builddir) \ $(NULL) @@ -9,10 +10,12 @@ AM_CFLAGS = \ $(LIBOSMOCORE_CFLAGS) \ $(LIBOSMOVTY_CFLAGS) \ $(LIBOSMOCTRL_CFLAGS) \ + $(LIBGTPNL_CFLAGS) \ $(COVERAGE_CFLAGS) \ $(NULL) AM_LDFLAGS = \ + $(LIBGTPNL_LDFLAGS) \ $(COVERAGE_LDFLAGS) \ $(NULL) @@ -22,12 +25,21 @@ bin_PROGRAMS = \ osmo_upf_SOURCES = \ osmo_upf_main.c \ + up_endpoint.c \ + up_gtp_action.c \ + up_peer.c \ + up_session.c \ upf.c \ + upf_gtp.c \ + upf_vty.c \ $(NULL) osmo_upf_LDADD = \ + $(top_builddir)/src/libosmo-pfcp/libosmo-pfcp.a \ + $(top_builddir)/src/libosmo-tlv/libosmo-tlv.a \ $(LIBOSMOCORE_LIBS) \ $(LIBOSMOVTY_LIBS) \ $(LIBOSMOCTRL_LIBS) \ + $(LIBGTPNL_LIBS) \ $(COVERAGE_LDFLAGS) \ $(NULL) diff --git a/src/osmo-upf/osmo_upf_main.c b/src/osmo-upf/osmo_upf_main.c index ddd1b44..6e3c802 100644 --- a/src/osmo-upf/osmo_upf_main.c +++ b/src/osmo-upf/osmo_upf_main.c @@ -31,11 +31,16 @@ #include #include #include +#include #include #include #include +#include + #include +#include +#include #define _GNU_SOURCE #include @@ -45,6 +50,7 @@ #include #include +#include extern void *tall_vty_ctx; @@ -175,7 +181,7 @@ static void signal_handler(int signum) * return, but program wouldn't exit if an external SIGABRT is * received. */ - talloc_report(tall_vty_ctx, stderr); + //talloc_report(tall_vty_ctx, stderr); talloc_report_full(tall_upf_ctx, stderr); signal(SIGABRT, SIG_DFL); raise(SIGABRT); @@ -204,6 +210,30 @@ static struct vty_app_info upf_vty_app_info = { }; static const struct log_info_cat upf_default_categories[] = { + [DREF] = { + .name = "DREF", + .description = "Reference Counting", + .enabled = 0, .loglevel = LOGL_NOTICE, + .color = OSMO_LOGCOLOR_DARKGREY, + }, + [DPEER] = { + .name = "DPEER", + .description = "PFCP peer association", + .enabled = 0, .loglevel = LOGL_NOTICE, + .color = OSMO_LOGCOLOR_YELLOW, + }, + [DSESSION] = { + .name = "DSESSION", + .description = "PFCP sessions", + .enabled = 0, .loglevel = LOGL_NOTICE, + .color = OSMO_LOGCOLOR_BLUE, + }, + [DGTP] = { + .name = "DGTP", + .description = "GTP tunneling", + .enabled = 0, .loglevel = LOGL_NOTICE, + .color = OSMO_LOGCOLOR_PURPLE, + }, }; const struct log_info log_info = { @@ -214,19 +244,27 @@ const struct log_info log_info = { int main(int argc, char **argv) { int rc; + void *tall_infra_ctx; /* Track the use of talloc NULL memory contexts */ talloc_enable_null_tracking(); osmo_fsm_set_dealloc_ctx(OTC_SELECT); - tall_upf_ctx = talloc_named_const(NULL, 1, "osmo-upf"); + tall_infra_ctx = talloc_named_const(NULL, 1, "osmo-upf"); + tall_upf_ctx = talloc_named_const(tall_infra_ctx, 1, "osmo-upf-main"); upf_vty_app_info.tall_ctx = tall_upf_ctx; msgb_talloc_ctx_init(tall_upf_ctx, 0); osmo_signal_talloc_ctx_init(tall_upf_ctx); - osmo_init_logging2(tall_upf_ctx, &log_info); + osmo_init_logging2(tall_infra_ctx, &log_info); + log_set_print_category_hex(osmo_stderr_target, 0); + log_set_print_category(osmo_stderr_target, 1); + log_set_print_level(osmo_stderr_target, 1); + log_set_print_filename2(osmo_stderr_target, LOG_FILENAME_BASENAME); + log_set_print_filename_pos(osmo_stderr_target, LOG_FILENAME_POS_LINE_END); + log_set_print_extended_timestamp(osmo_stderr_target, 1); osmo_fsm_log_timeouts(true); osmo_fsm_log_addr(true); @@ -243,6 +281,9 @@ int main(int argc, char **argv) osmo_talloc_vty_add_cmds(); osmo_cpu_sched_vty_init(tall_upf_ctx); + upf_vty_init(); + osmo_tdef_vty_groups_init(CONFIG_NODE, g_upf_tdef_groups); + /* Parse options */ handle_options(argc, argv); @@ -280,12 +321,21 @@ int main(int argc, char **argv) } } + if (upf_gtp_genl_open()) + return -1; + + if (upf_gtp_devs_open()) + return -1; + + if (upf_pfcp_listen()) + return -1; + do { log_reset_context(); osmo_select_main_ctx(0); /* If the user hits Ctrl-C the third time, just terminate immediately. */ - if (quit >= 3) + if (quit >= 1) //3) break; /* Has SIGTERM been received (and not yet been handled)? */ @@ -298,16 +348,24 @@ int main(int argc, char **argv) } } while (!osmo_select_shutdown_done()); - log_fini(); + up_endpoint_free(&g_upf->pfcp.ep); + upf_gtp_devs_close(); + + upf_gtp_genl_close(); /* Report the heap state of talloc contexts, then free, so both ASAN and Valgrind are happy... */ talloc_report_full(tall_upf_ctx, stderr); talloc_free(tall_upf_ctx); - talloc_report_full(tall_vty_ctx, stderr); + log_fini(); + + talloc_report_full(tall_infra_ctx, stderr); + talloc_free(tall_infra_ctx); + + //talloc_report_full(tall_vty_ctx, stderr); talloc_free(tall_vty_ctx); - talloc_report_full(NULL, stderr); + //talloc_report_full(NULL, stderr); talloc_disable_null_tracking(); return 0; } diff --git a/src/osmo-upf/up_endpoint.c b/src/osmo-upf/up_endpoint.c new file mode 100644 index 0000000..41ee46b --- /dev/null +++ b/src/osmo-upf/up_endpoint.c @@ -0,0 +1,353 @@ +/* (C) 2021-2022 by sysmocom - s.f.m.c. GmbH + * All Rights Reserved + * + * Author: Neels Hofmeyr + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation; either version 3 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +#include +#include +#include + +#include +#include +#include + +static void up_endpoint_set_msg_ctx(struct osmo_pfcp_endpoint *ep, struct osmo_pfcp_msg *m) +{ + struct up_endpoint *up_ep = ep->priv; + struct up_peer *peer; + + if (!m->ctx.peer_fi) { + peer = up_peer_find(up_ep, &m->remote_addr); + if (peer) { + up_peer_set_msg_ctx(peer, m); + } + } else { + peer = m->ctx.peer_fi->priv; + } + + /* Find a session, if the header is parsed yet and contains a SEID */ + if (peer && !m->ctx.session_fi && m->h.seid_present) { + struct up_session *session; + session = up_session_find_by_up_seid(peer, m->h.seid); + if (session) { + up_session_set_msg_ctx(session, m); + } + } +} + +static void up_ep_rx_not_impl_req(struct up_endpoint *up_ep, const struct osmo_pfcp_msg *m, + enum osmo_pfcp_message_type resp_msgt, + const struct osmo_pfcp_ie_node_id *node_id, enum osmo_pfcp_cause cause) +{ + struct osmo_pfcp_msg *tx; + enum osmo_pfcp_cause *tx_cause; + OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, "message type not implemented\n"); + tx = osmo_pfcp_msg_alloc_tx(OTC_SELECT, NULL, &up_ep->pfcp_ep->cfg.local_node_id, m, resp_msgt); + tx_cause = osmo_pfcp_msg_cause(tx); + if (tx_cause) + *tx_cause = cause; + osmo_pfcp_endpoint_tx(up_ep->pfcp_ep, tx); + return; +} + +static void up_ep_rx_heartbeat_req(struct up_endpoint *up_ep, const struct osmo_pfcp_msg *m) +{ + struct up_peer *peer = m->ctx.peer_fi ? m->ctx.peer_fi->priv : NULL; + /* osmo_pfcp_endpoint_handle_rx() has already taken care of the heartbeat response. Just dispatch the event + * here. */ + /* If the peer is not associated / not known, we don't care that a heartbeat happened. */ + if (!peer || !peer->heartbeat_fi) + return; + osmo_fsm_inst_dispatch(peer->heartbeat_fi, OSMO_PFCP_HEARTBEAT_EV_RX_REQ, (void*)m); +} + +static void up_ep_rx_heartbeat_resp(struct up_endpoint *up_ep, const struct osmo_pfcp_msg *m) +{ + struct up_peer *peer = m->ctx.peer_fi ? m->ctx.peer_fi->priv : NULL; + if (!peer) { + OSMO_LOG_PFCP_MSG(m, LOGL_INFO, "Heartbeat response from unknown peer %s\n", + osmo_sockaddr_to_str_c(OTC_SELECT, &m->remote_addr)); + return; + } + if (!peer->heartbeat_fi) { + OSMO_LOG_PFCP_MSG(m, LOGL_INFO, "Heartbeat response, but peer is not associated %s\n", + osmo_sockaddr_to_str_c(OTC_SELECT, &m->remote_addr)); + return; + } + osmo_fsm_inst_dispatch(peer->heartbeat_fi, OSMO_PFCP_HEARTBEAT_EV_RX_RESP, (void*)m); +} + +static void up_ep_rx_pfd_mgmt_req(struct up_endpoint *up_ep, const struct osmo_pfcp_msg *m) +{ + up_ep_rx_not_impl_req(up_ep, m, OSMO_PFCP_MSGT_PFD_MGMT_RESP, NULL, OSMO_PFCP_CAUSE_SERVICE_NOT_SUPPORTED); +} + +static void up_ep_rx_assoc_setup_req(struct up_endpoint *up_ep, const struct osmo_pfcp_msg *m) +{ + struct up_peer *peer = m->ctx.peer_fi ? m->ctx.peer_fi->priv : NULL; + if (!peer) { + peer = up_peer_find_or_add(up_ep, &m->remote_addr); + OSMO_ASSERT(peer); + } + osmo_fsm_inst_dispatch(peer->fi, UP_PEER_EV_RX_ASSOC_SETUP_REQ, (void*)m); +} + +static void up_ep_rx_assoc_upd_req(struct up_endpoint *up_ep, const struct osmo_pfcp_msg *m) +{ + if (!m->ctx.peer_fi) { + struct osmo_pfcp_msg *tx; + OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, "Peer is not associated, cannot update association\n"); + tx = osmo_pfcp_msg_alloc_tx(OTC_SELECT, NULL, &up_ep->pfcp_ep->cfg.local_node_id, m, OSMO_PFCP_MSGT_ASSOC_UPDATE_RESP); + /* FIXME set node_id, cause */ + osmo_pfcp_endpoint_tx(up_ep->pfcp_ep, tx); + return; + } + osmo_fsm_inst_dispatch(m->ctx.peer_fi, UP_PEER_EV_RX_ASSOC_UPD_REQ, (void*)m); +} + +static void up_ep_rx_assoc_rel_req(struct up_endpoint *up_ep, const struct osmo_pfcp_msg *m) +{ + if (!m->ctx.peer_fi) { + struct osmo_pfcp_msg *tx; + OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, "Peer is not associated. Sending ACK response anyway\n"); + tx = osmo_pfcp_msg_alloc_tx(OTC_SELECT, NULL, &up_ep->pfcp_ep->cfg.local_node_id, m, OSMO_PFCP_MSGT_ASSOC_RELEASE_RESP); + /* FIXME set node_id, cause */ + osmo_pfcp_endpoint_tx(up_ep->pfcp_ep, tx); + return; + } + osmo_fsm_inst_dispatch(m->ctx.peer_fi, UP_PEER_EV_RX_ASSOC_REL_REQ, (void*)m); +} + +static void up_ep_rx_node_report_req(struct up_endpoint *up_ep, const struct osmo_pfcp_msg *m) +{ + up_ep_rx_not_impl_req(up_ep, m, OSMO_PFCP_MSGT_NODE_REPORT_RESP, NULL /* FIXME? */, + OSMO_PFCP_CAUSE_SERVICE_NOT_SUPPORTED); +} + +static void up_ep_rx_session_set_del_req(struct up_endpoint *up_ep, const struct osmo_pfcp_msg *m) +{ + up_ep_rx_not_impl_req(up_ep, m, OSMO_PFCP_MSGT_SESSION_SET_DEL_RESP, + &up_ep->pfcp_ep->cfg.local_node_id, OSMO_PFCP_CAUSE_SERVICE_NOT_SUPPORTED); +} + +static void up_ep_rx_session_est_req(struct up_endpoint *up_ep, const struct osmo_pfcp_msg *m) +{ + if (!m->ctx.peer_fi) { + struct osmo_pfcp_msg *tx; + OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, "Peer is not associated, cannot establish session\n"); + tx = osmo_pfcp_msg_alloc_tx(OTC_SELECT, NULL, &up_ep->pfcp_ep->cfg.local_node_id, m, OSMO_PFCP_MSGT_SESSION_EST_RESP); + tx->ies.session_est_resp.cause = OSMO_PFCP_CAUSE_NO_ESTABLISHED_PFCP_ASSOC; + osmo_pfcp_endpoint_tx(up_ep->pfcp_ep, tx); + return; + } + osmo_fsm_inst_dispatch(m->ctx.peer_fi, UP_PEER_EV_RX_SESSION_EST_REQ, (void*)m); +} + +static void up_ep_rx_session_mod_req(struct up_endpoint *up_ep, const struct osmo_pfcp_msg *m) +{ + if (!m->ctx.session_fi) { + /* Session not found. */ + struct osmo_pfcp_msg *tx; + tx = osmo_pfcp_msg_alloc_tx(OTC_SELECT, NULL, &up_ep->pfcp_ep->cfg.local_node_id, m, + OSMO_PFCP_MSGT_SESSION_MOD_RESP); + if (!m->ctx.peer_fi) { + /* Not even the peer is associated. */ + OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, "Peer is not associated, cannot modify session\n"); + tx->ies.session_mod_resp.cause = OSMO_PFCP_CAUSE_NO_ESTABLISHED_PFCP_ASSOC; + } else { + OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, + "No established session with SEID=0x%"PRIx64", cannot modify\n", + m->h.seid); + tx->ies.session_mod_resp.cause = OSMO_PFCP_CAUSE_SESSION_CTX_NOT_FOUND; + } + osmo_pfcp_endpoint_tx(up_ep->pfcp_ep, tx); + return; + } + osmo_fsm_inst_dispatch(m->ctx.session_fi, UP_SESSION_EV_RX_SESSION_MOD_REQ, (void*)m); +} + +static void up_ep_rx_session_del_req(struct up_endpoint *up_ep, const struct osmo_pfcp_msg *m) +{ + if (!m->ctx.session_fi) { + /* Session not found. */ + struct osmo_pfcp_msg *tx; + tx = osmo_pfcp_msg_alloc_tx(OTC_SELECT, NULL, &up_ep->pfcp_ep->cfg.local_node_id, m, OSMO_PFCP_MSGT_SESSION_DEL_RESP); + if (!m->ctx.peer_fi) { + /* Not even the peer is associated. */ + OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, "Peer is not associated, cannot delete session\n"); + tx->ies.session_del_resp.cause = OSMO_PFCP_CAUSE_NO_ESTABLISHED_PFCP_ASSOC; + } else { + OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, + "No established session with SEID=0x%"PRIx64", cannot delete\n", + m->h.seid); + tx->ies.session_del_resp.cause = OSMO_PFCP_CAUSE_SESSION_CTX_NOT_FOUND; + } + osmo_pfcp_endpoint_tx(up_ep->pfcp_ep, tx); + return; + } + osmo_fsm_inst_dispatch(m->ctx.session_fi, UP_SESSION_EV_RX_SESSION_DEL_REQ, (void*)m); +} + +static void up_ep_rx_session_rep_req(struct up_endpoint *up_ep, const struct osmo_pfcp_msg *m) +{ + up_ep_rx_not_impl_req(up_ep, m, OSMO_PFCP_MSGT_SESSION_REP_RESP, NULL, OSMO_PFCP_CAUSE_SERVICE_NOT_SUPPORTED); +} + +static void up_endpoint_rx_cb(struct osmo_pfcp_endpoint *ep, struct osmo_pfcp_msg *m) +{ + struct up_endpoint *up_ep = ep->priv; + + switch (m->h.message_type) { + case OSMO_PFCP_MSGT_HEARTBEAT_REQ: + up_ep_rx_heartbeat_req(up_ep, m); + return; + case OSMO_PFCP_MSGT_HEARTBEAT_RESP: + up_ep_rx_heartbeat_resp(up_ep, m); + return; + case OSMO_PFCP_MSGT_PFD_MGMT_REQ: + up_ep_rx_pfd_mgmt_req(up_ep, m); + return; + case OSMO_PFCP_MSGT_ASSOC_SETUP_REQ: + up_ep_rx_assoc_setup_req(up_ep, m); + return; + case OSMO_PFCP_MSGT_ASSOC_UPDATE_REQ: + up_ep_rx_assoc_upd_req(up_ep, m); + return; + case OSMO_PFCP_MSGT_ASSOC_RELEASE_REQ: + up_ep_rx_assoc_rel_req(up_ep, m); + return; + case OSMO_PFCP_MSGT_NODE_REPORT_REQ: + up_ep_rx_node_report_req(up_ep, m); + return; + case OSMO_PFCP_MSGT_SESSION_SET_DEL_REQ: + up_ep_rx_session_set_del_req(up_ep, m); + return; + case OSMO_PFCP_MSGT_SESSION_EST_REQ: + up_ep_rx_session_est_req(up_ep, m); + return; + case OSMO_PFCP_MSGT_SESSION_MOD_REQ: + up_ep_rx_session_mod_req(up_ep, m); + return; + case OSMO_PFCP_MSGT_SESSION_DEL_REQ: + up_ep_rx_session_del_req(up_ep, m); + return; + case OSMO_PFCP_MSGT_SESSION_REP_REQ: + up_ep_rx_session_rep_req(up_ep, m); + return; + default: + OSMO_LOG_PFCP_MSG(m, LOGL_ERROR, "Unknown message type\n"); + return; + } +} + +struct up_endpoint *up_endpoint_init(void *ctx, const struct osmo_sockaddr *local_addr) +{ + int rc; + struct up_endpoint *up_ep; + up_ep = talloc_zero(ctx, struct up_endpoint); + INIT_LLIST_HEAD(&up_ep->peers); + + up_ep->pfcp_ep = osmo_pfcp_endpoint_create(up_ep, up_ep); + up_ep->pfcp_ep->cfg.local_addr = *local_addr; + + up_ep->pfcp_ep->set_msg_ctx = up_endpoint_set_msg_ctx; + up_ep->pfcp_ep->rx_msg = up_endpoint_rx_cb; + + osmo_pfcp_ie_node_id_from_osmo_sockaddr(&up_ep->pfcp_ep->cfg.local_node_id, local_addr); + + rc = osmo_pfcp_endpoint_bind(up_ep->pfcp_ep); + if (rc) { + talloc_free(up_ep); + return NULL; + } + return up_ep; +} + +static struct up_session *up_endpoint_find_session(struct up_endpoint *ep, uint64_t up_seid) +{ + struct up_peer *peer; + llist_for_each_entry(peer, &ep->peers, entry) { + struct up_session *session = up_session_find_by_up_seid(peer, up_seid); + if (session) + return session; + } + return NULL; +} + +static struct up_session *up_endpoint_find_session_by_local_teid(struct up_endpoint *ep, uint32_t teid) +{ + struct up_peer *peer; + llist_for_each_entry(peer, &ep->peers, entry) { + struct up_session *session = up_session_find_by_local_teid(peer, teid); + if (session) + return session; + } + return NULL; +} + +static uint64_t up_endpoint_inc_seid(struct up_endpoint *ep) +{ + ep->next_seid_state++; + if (!ep->next_seid_state) + ep->next_seid_state++; + return ep->next_seid_state; +} + +uint64_t up_endpoint_next_seid(struct up_endpoint *ep) +{ + uint64_t sanity; + for (sanity = 2342; sanity; sanity--) { + uint64_t next_seid = up_endpoint_inc_seid(ep); + if (up_endpoint_find_session(ep, next_seid)) + continue; + return next_seid; + } + return 0; +} + +static uint32_t up_endpoint_inc_teid(struct up_endpoint *ep) +{ + ep->next_teid_state++; + if (!ep->next_teid_state) + ep->next_teid_state++; + return ep->next_teid_state; +} + +uint32_t up_endpoint_next_teid(struct up_endpoint *ep) +{ + uint32_t sanity; + for (sanity = 2342; sanity; sanity--) { + uint32_t next_teid = up_endpoint_inc_teid(ep); + if (up_endpoint_find_session_by_local_teid(ep, next_teid)) + continue; + return next_teid; + } + return 0; +} + +void up_endpoint_free(struct up_endpoint **_ep) +{ + struct up_peer *peer; + struct up_endpoint *ep = *_ep; + + while ((peer = llist_first_entry_or_null(&ep->peers, struct up_peer, entry))) + up_peer_free(peer); + + osmo_pfcp_endpoint_free(&ep->pfcp_ep); + *_ep = NULL; +} diff --git a/src/osmo-upf/up_gtp_action.c b/src/osmo-upf/up_gtp_action.c new file mode 100644 index 0000000..2d6a419 --- /dev/null +++ b/src/osmo-upf/up_gtp_action.c @@ -0,0 +1,160 @@ +/* + * (C) 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +#include +#include + +#include + +#include +#include +#include +#include + +int up_gtp_action_cmp(const struct up_gtp_action *a, const struct up_gtp_action *b) +{ + int cmp; + if (a == b) + return 0; + if (!a) + return -1; + if (!b) + return 1; + +#define CMP_RET(MEMB) do { \ + int _cmp = OSMO_CMP(a->MEMB, b->MEMB); \ + if (_cmp) \ + return _cmp; \ + } while (0) + + CMP_RET(kind); + + switch (a->kind) { + case UP_GTP_U_ENDECAPS: + CMP_RET(endecaps.local_teid); + CMP_RET(endecaps.remote_teid); + cmp = osmo_sockaddr_cmp(&a->endecaps.gtp_remote_addr, &b->endecaps.gtp_remote_addr); + if (cmp) + return cmp; + cmp = osmo_sockaddr_cmp(&a->endecaps.ue_addr, &b->endecaps.ue_addr); + if (cmp) + return cmp; + break; + + case UP_GTP_U_FORW: + CMP_RET(forw.access.local_teid); + CMP_RET(forw.access.remote_teid); + CMP_RET(forw.core.local_teid); + CMP_RET(forw.core.remote_teid); + break; + default: + break; + } + return 0; +} + +static int up_gtp_action_enable_disable(struct up_gtp_action *a, bool enable) +{ + struct upf_gtp_dev *gtp_dev; + int rc; + + switch (a->kind) { + case UP_GTP_U_ENDECAPS: + /* use the first available GTP device. + * TODO: select by interface name? + */ + gtp_dev = upf_gtp_dev_first(); + if (!gtp_dev) { + LOG_UP_GTP_ACTION(a, LOGL_ERROR, "No GTP device open, cannot %s\n", enable ? "enable" : "disable"); + return -EIO; + } + + if (enable) + rc = upf_gtp_dev_tunnel_add(gtp_dev, &a->endecaps); + else + rc = upf_gtp_dev_tunnel_del(gtp_dev, &a->endecaps); + if (rc) { + LOG_UP_GTP_ACTION(a, LOGL_ERROR, "Failed to %s GTP tunnel: %d %s\n", + enable ? "enable" : "disable", rc, strerror(-rc)); + return rc; + } + LOG_UP_GTP_ACTION(a, LOGL_NOTICE, "%s GTP tunnel\n", enable ? "Enabled" : "Disabled"); + return 0; + case UP_GTP_U_FORW: + LOG_UP_GTP_ACTION(a, LOGL_ERROR, "TEID translation not yet implemented\n"); + return -ENOTSUP; + default: + LOG_UP_GTP_ACTION(a, LOGL_ERROR, "Invalid action\n"); + return -ENOTSUP; + } +} + +int up_gtp_action_enable(struct up_gtp_action *a) +{ + return up_gtp_action_enable_disable(a, true); +} + +int up_gtp_action_disable(struct up_gtp_action *a) +{ + return up_gtp_action_enable_disable(a, false); +} + +int up_gtp_action_to_str_buf(char *buf, size_t buflen, const struct up_gtp_action *a) +{ + struct osmo_strbuf sb = { .buf = buf, .len = buflen }; + switch (a->kind) { + case UP_GTP_U_ENDECAPS: + OSMO_STRBUF_PRINTF(sb, "GTP:endecaps GTP-access:"); + OSMO_STRBUF_APPEND(sb, osmo_sockaddr_to_str_buf2, &a->endecaps.gtp_remote_addr); + OSMO_STRBUF_PRINTF(sb, " TEID-r:0x%"PRIx32" TEID-l:0x%"PRIx32" IP-core:", + a->endecaps.remote_teid, a->endecaps.local_teid); + OSMO_STRBUF_APPEND(sb, osmo_sockaddr_to_str_buf2, &a->endecaps.ue_addr); + break; + case UP_GTP_U_FORW: + OSMO_STRBUF_PRINTF(sb, "GTP:forw GTP-access:"); + OSMO_STRBUF_APPEND(sb, osmo_sockaddr_to_str_buf2, &a->forw.access.gtp_remote_addr); + OSMO_STRBUF_PRINTF(sb, " TEID-access-r:0x%"PRIx32" TEID-access-l:0x%"PRIx32" GTP-core:", + a->forw.access.remote_teid, a->forw.access.local_teid); + OSMO_STRBUF_APPEND(sb, osmo_sockaddr_to_str_buf2, &a->forw.core.gtp_remote_addr); + OSMO_STRBUF_PRINTF(sb, " TEID-core-r:0x%"PRIx32" TEID-core-l:0x%"PRIx32, + a->forw.core.remote_teid, a->forw.core.local_teid); + break; + case UP_GTP_DROP: + OSMO_STRBUF_PRINTF(sb, "GTP:drop"); + break; + default: + OSMO_STRBUF_PRINTF(sb, "GTP:?"); + break; + } + if (a->session) + OSMO_STRBUF_PRINTF(sb, " PFCP-peer:%s SEID-l:0x%"PRIx64" PDR:%d,%d", + up_peer_remote_addr_str(a->session->up_peer), + a->session->up_seid, a->pdr_core, a->pdr_access); + return sb.chars_needed; +} + +char *up_gtp_action_to_str_c(void *ctx, const struct up_gtp_action *a) +{ + OSMO_NAME_C_IMPL(ctx, 128, "ERROR", up_gtp_action_to_str_buf, a) +} diff --git a/src/osmo-upf/up_peer.c b/src/osmo-upf/up_peer.c new file mode 100644 index 0000000..104607c --- /dev/null +++ b/src/osmo-upf/up_peer.c @@ -0,0 +1,550 @@ +/* (C) 2021-2022 by sysmocom - s.f.m.c. GmbH + * All Rights Reserved + * + * Author: Neels Hofmeyr + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation; either version 3 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +enum up_peer_fsm_state { + UP_PEER_ST_NOT_ASSOCIATED, + UP_PEER_ST_ASSOCIATED, + UP_PEER_ST_GRACEFUL_RELEASE, + UP_PEER_ST_WAIT_USE_COUNT, +}; + +static const struct value_string up_peer_fsm_event_names[] = { + OSMO_VALUE_STRING(UP_PEER_EV_RX_ASSOC_SETUP_REQ), + OSMO_VALUE_STRING(UP_PEER_EV_RX_ASSOC_UPD_REQ), + OSMO_VALUE_STRING(UP_PEER_EV_RX_ASSOC_REL_REQ), + OSMO_VALUE_STRING(UP_PEER_EV_RX_SESSION_EST_REQ), + OSMO_VALUE_STRING(UP_PEER_EV_HEARTBEAT_FAILURE), + OSMO_VALUE_STRING(UP_PEER_EV_USE_COUNT_ZERO), + OSMO_VALUE_STRING(UP_PEER_EV_SESSION_TERM), + {0} +}; + +static struct osmo_fsm up_peer_fsm; + +static const struct osmo_tdef_state_timeout up_peer_fsm_timeouts[32] = { + [UP_PEER_ST_GRACEFUL_RELEASE] = { .T = -21 }, +}; + +/* Transition to a state, using the T timer defined in up_peer_fsm_timeouts. + * Assumes local variable fi exists. */ +#define up_peer_fsm_state_chg(state) \ + osmo_tdef_fsm_inst_state_chg(fi, state, \ + up_peer_fsm_timeouts, \ + osmo_pfcp_tdefs, \ + 5) + +static int up_peer_use_cb(struct osmo_use_count_entry *e, int32_t old_use_count, const char *file, int line) +{ + struct up_peer *peer = e->use_count->talloc_object; + int32_t total; + int level; + + if (!e->use) + return -EINVAL; + + total = osmo_use_count_total(&peer->use_count); + + if (total == 0 + || (total == 1 && old_use_count == 0 && e->count == 1)) + level = LOGL_INFO; + else + level = LOGL_DEBUG; + + LOGPFSMSLSRC(peer->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, &peer->use_count)); + + if (e->count < 0) + return -ERANGE; + + if (total == 0) + osmo_fsm_inst_dispatch(peer->fi, UP_PEER_EV_USE_COUNT_ZERO, NULL); + return 0; +} + +char *up_peer_remote_addr_str(struct up_peer *peer) +{ + struct osmo_sockaddr remote_addr = peer->remote_addr; +#if 1 + /* Zero the port, it is not interesting information. The port for PFCP is defined fixed, and there is no use + * printing it in the logs */ + osmo_sockaddr_set_port(&remote_addr.u.sa, 0); +#endif + return osmo_sockaddr_to_str_c(OTC_SELECT, &remote_addr); +} + +static void up_peer_update_id(struct up_peer *peer) +{ + osmo_fsm_inst_update_id_f_sanitize(peer->fi, '-', "%s", up_peer_remote_addr_str(peer)); + LOGPFSML(peer->fi, LOGL_DEBUG, "Updated id\n"); +} + +static struct up_peer *up_peer_add(struct up_endpoint *up_endpoint, const struct osmo_sockaddr *remote_addr) +{ + struct up_peer *peer; + + struct osmo_fsm_inst *fi = osmo_fsm_inst_alloc(&up_peer_fsm, up_endpoint, NULL, LOGL_DEBUG, NULL); + OSMO_ASSERT(fi); + + peer = talloc(fi, struct up_peer); + OSMO_ASSERT(peer); + fi->priv = peer; + + *peer = (struct up_peer) { + .fi = fi, + .up_endpoint = up_endpoint, + .remote_addr = *remote_addr, + .heartbeat_fi = NULL /* FIXME */, + .use_count = { + .talloc_object = peer, + .use_cb = up_peer_use_cb, + }, + }; + osmo_use_count_make_static_entries(&peer->use_count, peer->use_count_buf, ARRAY_SIZE(peer->use_count_buf)); + hash_init(peer->sessions_by_up_seid); + hash_init(peer->sessions_by_cp_seid); + + osmo_pfcp_bits_set(peer->local_up_features.bits, OSMO_PFCP_UP_FEAT_BUNDL, true); + osmo_pfcp_bits_set(peer->local_up_features.bits, OSMO_PFCP_UP_FEAT_RTTL, true); + osmo_pfcp_bits_set(peer->local_up_features.bits, OSMO_PFCP_UP_FEAT_FTUP, true); + + up_peer_update_id(peer); + + llist_add(&peer->entry, &up_endpoint->peers); + return peer; +} + +struct up_peer *up_peer_find(struct up_endpoint *up_endpoint, const struct osmo_sockaddr *remote_addr) +{ + struct up_peer *peer; + llist_for_each_entry(peer, &up_endpoint->peers, entry) { + if (osmo_sockaddr_cmp(&peer->remote_addr, remote_addr)) + continue; + return peer; + } + return NULL; +} + +struct up_peer *up_peer_find_or_add(struct up_endpoint *up_endpoint, const struct osmo_sockaddr *remote_addr) +{ + struct up_peer *peer = up_peer_find(up_endpoint, remote_addr); + if (peer) + return peer; + return up_peer_add(up_endpoint, remote_addr); +} + +int up_peer_tx(struct up_peer *peer, struct osmo_pfcp_msg *m) +{ + return osmo_pfcp_endpoint_tx(peer->up_endpoint->pfcp_ep, m); +} + +static int up_peer_fsm_timer_cb(struct osmo_fsm_inst *fi) +{ + //struct up_peer *peer = fi->priv; + /* Return 1 to terminate FSM instance, 0 to keep running */ + return 1; +} + +void up_peer_set_msg_ctx(struct up_peer *peer, struct osmo_pfcp_msg *m) +{ + OSMO_ASSERT(!m->ctx.peer_fi); + + m->ctx.peer_fi = peer->fi; + m->ctx.peer_use_count = &peer->use_count; + m->ctx.peer_use_token = (m->rx ? UP_USE_MSG_RX : UP_USE_MSG_TX); + osmo_use_count_get_put(m->ctx.peer_use_count, m->ctx.peer_use_token, 1); +} + +struct osmo_pfcp_msg *up_peer_init_tx(struct up_peer *peer, struct osmo_pfcp_msg *in_reply_to, + enum osmo_pfcp_message_type message_type) +{ + struct osmo_pfcp_msg *tx = osmo_pfcp_msg_alloc_tx(OTC_SELECT, &peer->remote_addr, + &peer->up_endpoint->pfcp_ep->cfg.local_node_id, + in_reply_to, message_type); + up_peer_set_msg_ctx(peer, tx); + return tx; +} + +static int up_peer_tx_assoc_setup_resp(struct up_peer *peer, struct osmo_pfcp_msg *m, enum osmo_pfcp_cause cause) +{ + struct osmo_pfcp_msg *resp; + + resp = up_peer_init_tx(peer, m, OSMO_PFCP_MSGT_ASSOC_SETUP_RESP); + + resp->ies.assoc_setup_resp = (struct osmo_pfcp_msg_assoc_setup_resp) { + .cause = cause, + .recovery_time_stamp = g_upf->pfcp.ep->pfcp_ep->recovery_time_stamp, + .up_function_features_present = true, + .up_function_features = peer->local_up_features, + }; + resp->ies.assoc_setup_resp.recovery_time_stamp = g_upf->pfcp.ep->pfcp_ep->recovery_time_stamp; + + if (osmo_pfcp_endpoint_tx(peer->up_endpoint->pfcp_ep, resp)) { + OSMO_LOG_PFCP_MSG(resp, LOGL_ERROR, "Error sending response, cannot associate with peer\n"); + return -EIO; + } + return 0; +} + +static int up_peer_tx_assoc_rel_resp(struct up_peer *peer, struct osmo_pfcp_msg *m, enum osmo_pfcp_cause cause) +{ + struct osmo_pfcp_msg *resp; + + resp = up_peer_init_tx(peer, m, OSMO_PFCP_MSGT_ASSOC_RELEASE_RESP); + + resp->ies.assoc_release_resp = (struct osmo_pfcp_msg_assoc_release_resp) { + .cause = cause, + }; + + if (osmo_pfcp_endpoint_tx(peer->up_endpoint->pfcp_ep, resp)) { + OSMO_LOG_PFCP_MSG(resp, LOGL_ERROR, "Error sending response\n"); + return -EIO; + } + return 0; +} + +static void up_peer_clear_sessions(struct up_peer *peer) +{ + struct up_session *session; + int bkt; + struct hlist_node *tmp; + int count = 0; + hash_for_each_safe(peer->sessions_by_up_seid, bkt, tmp, session, node_by_up_seid) { + count += up_session_discard(session); + } + if (count) + LOGPFSML(peer->fi, LOGL_NOTICE, "terminated %d sessions\n", count); +} + +static void up_peer_rx_assoc_setup_req(struct up_peer *peer, struct osmo_pfcp_msg *m) +{ + struct osmo_fsm_inst *fi = peer->fi; + enum osmo_pfcp_cause cause = OSMO_PFCP_CAUSE_REQUEST_ACCEPTED; + + if (m->ies.assoc_setup_req.cp_function_features_present) + peer->peer_cp_features = m->ies.assoc_setup_req.cp_function_features; + + if (fi->state == UP_PEER_ST_ASSOCIATED) { + /* Retransmissions of the ACK response happen in pfcp_endpoint.c. So if we get this, it is a genuine + * duplicate association setup request. We could reject it. But why. Just "replace" with the new + * association. Continue. */ + /* If the peer has restarted, it has forgotten about all sessions. */ + if (peer->remote_recovery_timestamp != m->ies.assoc_setup_req.recovery_time_stamp) { + LOGPFSML(fi, LOGL_NOTICE, "another Association Setup Request, with different Recovery Timestamp." + " Clearing sessions, sending ACK.\n"); + up_peer_clear_sessions(peer); + } else { + LOGPFSML(fi, LOGL_NOTICE, "another Association Setup Request, with same Recovery Timestamp." + " Keeping sessions, sending ACK.\n"); + } + } + + peer->remote_recovery_timestamp = m->ies.assoc_setup_req.recovery_time_stamp; + + if (up_peer_fsm_state_chg(UP_PEER_ST_ASSOCIATED)) { + /* Not allowed to transition to ST_ASSOCIATED */ + cause = OSMO_PFCP_CAUSE_REQUEST_REJECTED; + } else { + /* Remember the Node ID that the peer sent */ + struct osmo_pfcp_ie_node_id *m_node_id = osmo_pfcp_msg_node_id(m); + OSMO_ASSERT(m_node_id); + peer->remote_node_id = *m_node_id; + } + + if (up_peer_tx_assoc_setup_resp(peer, m, cause) + || cause != OSMO_PFCP_CAUSE_REQUEST_ACCEPTED) + up_peer_fsm_state_chg(UP_PEER_ST_WAIT_USE_COUNT); +} + +static void up_peer_rx_assoc_rel_req(struct up_peer *peer, struct osmo_pfcp_msg *m) +{ + struct osmo_fsm_inst *fi = peer->fi; + up_peer_tx_assoc_rel_resp(peer, m, OSMO_PFCP_CAUSE_REQUEST_ACCEPTED); + up_peer_fsm_state_chg(UP_PEER_ST_WAIT_USE_COUNT); +} + +static void up_peer_rx_session_est_req(struct up_peer *peer, struct osmo_pfcp_msg *m) +{ + enum osmo_pfcp_cause cause = OSMO_PFCP_CAUSE_REQUEST_ACCEPTED; + struct osmo_pfcp_msg *resp; + struct up_session *session = up_session_find_or_add(peer, &m->ies.session_est_req.cp_f_seid, NULL); + + if (!session) { + cause = OSMO_PFCP_CAUSE_NO_RESOURCES_AVAILABLE; + goto nack_response; + } + + up_session_set_msg_ctx(session, m); + + if (osmo_fsm_inst_dispatch(session->fi, UP_SESSION_EV_RX_SESSION_EST_REQ, m)) { + cause = OSMO_PFCP_CAUSE_REQUEST_REJECTED; + goto nack_response; + } + return; + +nack_response: + resp = up_peer_init_tx(peer, m, OSMO_PFCP_MSGT_SESSION_EST_RESP); + resp->h.seid = m->ies.session_est_req.cp_f_seid.seid; + resp->h.seid_present = true; + resp->ies.session_est_resp = (struct osmo_pfcp_msg_session_est_resp){ + .cause = cause, + }; + osmo_pfcp_endpoint_tx(peer->up_endpoint->pfcp_ep, resp); +} + +static void up_peer_not_associated_action(struct osmo_fsm_inst *fi, uint32_t event, void *data) +{ + struct up_peer *peer = fi->priv; + + switch (event) { + + case UP_PEER_EV_RX_ASSOC_SETUP_REQ: + up_peer_rx_assoc_setup_req(peer, data); + break; + + case UP_PEER_EV_USE_COUNT_ZERO: + /* Not associated and no pending messages. discard peer. */ + up_peer_fsm_state_chg(UP_PEER_ST_WAIT_USE_COUNT); + return; + + default: + OSMO_ASSERT(false); + } +} + +static void up_peer_associated_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state) +{ + struct up_peer *peer = fi->priv; + LOGPFSML(fi, LOGL_NOTICE, "Peer associated. Local UP features: [%s]; Peer CP features: [%s]\n", + osmo_pfcp_bits_to_str_c(OTC_SELECT, peer->local_up_features.bits, osmo_pfcp_up_feature_strs), + osmo_pfcp_bits_to_str_c(OTC_SELECT, peer->peer_cp_features.bits, osmo_pfcp_cp_feature_strs)); +} + +static void up_peer_associated_action(struct osmo_fsm_inst *fi, uint32_t event, void *data) +{ + struct up_peer *peer = fi->priv; + + switch (event) { + + case UP_PEER_EV_RX_ASSOC_SETUP_REQ: + up_peer_rx_assoc_setup_req(peer, data); + break; + + case UP_PEER_EV_RX_ASSOC_UPD_REQ: + // FIXME + break; + + case UP_PEER_EV_RX_SESSION_EST_REQ: + up_peer_rx_session_est_req(peer, data); + break; + + case UP_PEER_EV_HEARTBEAT_FAILURE: + // FIXME + break; + + case UP_PEER_EV_USE_COUNT_ZERO: + /* Stay associated. */ + return; + + default: + OSMO_ASSERT(false); + } +} + +static void up_peer_associated_onleave(struct osmo_fsm_inst *fi, uint32_t next_state) +{ + if (next_state != UP_PEER_ST_ASSOCIATED) + LOGPFSML(fi, LOGL_NOTICE, "Peer released\n"); +} + +static void up_peer_graceful_release_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state) +{ + //struct up_peer *peer = fi->priv; + // FIXME +} + +static void up_peer_graceful_release_action(struct osmo_fsm_inst *fi, uint32_t event, void *data) +{ + struct up_peer *peer = fi->priv; + + switch (event) { + + case UP_PEER_EV_HEARTBEAT_FAILURE: + up_peer_fsm_state_chg(UP_PEER_ST_WAIT_USE_COUNT); + break; + + case UP_PEER_EV_USE_COUNT_ZERO: + /* When there are still sessions, stay around. */ + if (!hash_empty(peer->sessions_by_up_seid)) + return; + up_peer_fsm_state_chg(UP_PEER_ST_WAIT_USE_COUNT); + return; + + default: + OSMO_ASSERT(false); + } +} + +static void up_peer_wait_use_count_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state) +{ + struct up_peer *peer = fi->priv; + up_peer_clear_sessions(peer); + if (!osmo_use_count_total(&peer->use_count)) + osmo_fsm_inst_term(fi, OSMO_FSM_TERM_REGULAR, NULL); +} + +static void up_peer_wait_use_count_action(struct osmo_fsm_inst *fi, uint32_t event, void *data) +{ + struct up_peer *peer = fi->priv; + switch (event) { + + case UP_PEER_EV_USE_COUNT_ZERO: + osmo_fsm_inst_term(fi, OSMO_FSM_TERM_REGULAR, NULL); + return; + + case UP_PEER_EV_RX_ASSOC_SETUP_REQ: + up_peer_rx_assoc_setup_req(peer, data); + break; + + default: + OSMO_ASSERT(false); + } +} + +#define S(x) (1 << (x)) + +static const struct osmo_fsm_state up_peer_fsm_states[] = { + [UP_PEER_ST_NOT_ASSOCIATED] = { + .name = "NOT_ASSOCIATED", + .in_event_mask = 0 + | S(UP_PEER_EV_RX_ASSOC_SETUP_REQ) + | S(UP_PEER_EV_USE_COUNT_ZERO) + , + .out_state_mask = 0 + | S(UP_PEER_ST_ASSOCIATED) + | S(UP_PEER_ST_WAIT_USE_COUNT) + , + .action = up_peer_not_associated_action, + }, + [UP_PEER_ST_ASSOCIATED] = { + .name = "ASSOCIATED", + .in_event_mask = 0 + | S(UP_PEER_EV_RX_ASSOC_SETUP_REQ) + | S(UP_PEER_EV_RX_ASSOC_UPD_REQ) + | S(UP_PEER_EV_RX_SESSION_EST_REQ) + | S(UP_PEER_EV_HEARTBEAT_FAILURE) + | S(UP_PEER_EV_USE_COUNT_ZERO) + , + .out_state_mask = 0 + | S(UP_PEER_ST_ASSOCIATED) + | S(UP_PEER_ST_GRACEFUL_RELEASE) + | S(UP_PEER_ST_WAIT_USE_COUNT) + , + .onenter = up_peer_associated_onenter, + .action = up_peer_associated_action, + .onleave = up_peer_associated_onleave, + }, + [UP_PEER_ST_GRACEFUL_RELEASE] = { + .name = "GRACEFUL_RELEASE", + .in_event_mask = 0 + | S(UP_PEER_EV_HEARTBEAT_FAILURE) + | S(UP_PEER_EV_USE_COUNT_ZERO) + , + .out_state_mask = 0 + | S(UP_PEER_ST_WAIT_USE_COUNT) + , + .onenter = up_peer_graceful_release_onenter, + .action = up_peer_graceful_release_action, + }, + [UP_PEER_ST_WAIT_USE_COUNT] = { + .name = "WAIT_USE_COUNT", + .in_event_mask = 0 + | S(UP_PEER_EV_USE_COUNT_ZERO) + | S(UP_PEER_EV_RX_ASSOC_SETUP_REQ) + , + .out_state_mask = 0 + | S(UP_PEER_ST_ASSOCIATED) + , + .onenter = up_peer_wait_use_count_onenter, + .action = up_peer_wait_use_count_action, + }, +}; + +void up_peer_fsm_cleanup(struct osmo_fsm_inst *fi, enum osmo_fsm_term_cause cause) +{ + struct up_peer *peer = fi->priv; + LOGPFSML(fi, LOGL_NOTICE, "Peer removed\n"); + llist_del(&peer->entry); +} + +static void up_peer_allstate_action(struct osmo_fsm_inst *fi, uint32_t event, void *data) +{ + switch (event) { + case UP_PEER_EV_SESSION_TERM: + /* ignore */ + return; + case UP_PEER_EV_RX_ASSOC_REL_REQ: + up_peer_rx_assoc_rel_req(fi->priv, data); + return; + default: + OSMO_ASSERT(false); + } +} + +static struct osmo_fsm up_peer_fsm = { + .name = "up_peer", + .log_subsys = DPEER, + .states = up_peer_fsm_states, + .num_states = ARRAY_SIZE(up_peer_fsm_states), + .event_names = up_peer_fsm_event_names, + .timer_cb = up_peer_fsm_timer_cb, + .cleanup = up_peer_fsm_cleanup, + .allstate_event_mask = 0 + | S(UP_PEER_EV_RX_ASSOC_REL_REQ) + | S(UP_PEER_EV_SESSION_TERM) + , + .allstate_action = up_peer_allstate_action, +}; + +static __attribute__((constructor)) void up_peer_fsm_register(void) +{ + OSMO_ASSERT(osmo_fsm_register(&up_peer_fsm) == 0); +} + +void up_peer_free(struct up_peer *peer) +{ + osmo_fsm_inst_term(peer->fi, OSMO_FSM_TERM_REGULAR, NULL); +} diff --git a/src/osmo-upf/up_peer_fsm.c b/src/osmo-upf/up_peer_fsm.c new file mode 100644 index 0000000..9f2bc4d --- /dev/null +++ b/src/osmo-upf/up_peer_fsm.c @@ -0,0 +1,193 @@ +/* (C) 2021-2022 by sysmocom - s.f.m.c. GmbH + * All Rights Reserved + * + * Author: Neels Hofmeyr + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation; either version 3 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +#include +#include + +#include + +enum up_peer_fsm_state { + UP_PEER_ST_NOT_ASSOCIATED, + UP_PEER_ST_ASSOCIATED, + UP_PEER_ST_GRACEFUL_RELEASE, +}; + +static const struct value_string up_peer_fsm_event_names[] = { + OSMO_VALUE_STRING(UP_PEER_EV_RX_ASSOC_SETUP_REQ), + OSMO_VALUE_STRING(UP_PEER_EV_RX_ASSOC_UPD_REQ), + OSMO_VALUE_STRING(UP_PEER_EV_RX_SESSION_EST_REQ), + OSMO_VALUE_STRING(UP_PEER_EV_HEARTBEAT_FAILURE), + {0} +}; + +static struct osmo_fsm up_peer_fsm; + +static const struct osmo_tdef_state_timeout up_peer_fsm_timeouts[32] = { + [UP_PEER_ST_GRACEFUL_RELEASE] = { .T = -21 }, +}; + +/* Transition to a state, using the T timer defined in up_peer_fsm_timeouts. + * Assumes local variable fi exists. */ +#define up_peer_fsm_state_chg(state) \ + osmo_tdef_fsm_inst_state_chg(fi, state, \ + up_peer_fsm_timeouts, \ + g_upf_tdefs, \ + 5) + +struct up_peer *up_peer_alloc(struct osmo_fsm_inst *parent_fi, uint32_t parent_event_term) +{ + struct up_peer *up_peer; + + struct osmo_fsm_inst *fi = osmo_fsm_inst_alloc_child(&up_peer_fsm, parent_fi, parent_event_term); + OSMO_ASSERT(fi); + + up_peer = talloc(fi, struct up_peer); + OSMO_ASSERT(up_peer); + fi->priv = up_peer; + *up_peer = (struct up_peer){ + .fi = fi, + }; + + return up_peer; +} + +static int up_peer_fsm_timer_cb(struct osmo_fsm_inst *fi) +{ + //struct up_peer *up_peer = fi->priv; + /* Return 1 to terminate FSM instance, 0 to keep running */ + return 1; +} + +static void up_peer_not_associated_action(struct osmo_fsm_inst *fi, uint32_t event, void *data) +{ + //struct up_peer *up_peer = fi->priv; + + switch (event) { + + case UP_PEER_EV_RX_ASSOC_SETUP_REQ: + // FIXME + break; + + default: + OSMO_ASSERT(false); + } +} + +static void up_peer_associated_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state) +{ + //struct up_peer *up_peer = fi->priv; + // FIXME +} + +static void up_peer_associated_action(struct osmo_fsm_inst *fi, uint32_t event, void *data) +{ + //struct up_peer *up_peer = fi->priv; + + switch (event) { + + case UP_PEER_EV_RX_ASSOC_UPD_REQ: + // FIXME + break; + + case UP_PEER_EV_RX_SESSION_EST_REQ: + // FIXME + break; + + case UP_PEER_EV_HEARTBEAT_FAILURE: + // FIXME + break; + + default: + OSMO_ASSERT(false); + } +} + +static void up_peer_graceful_release_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state) +{ + //struct up_peer *up_peer = fi->priv; + // FIXME +} + +static void up_peer_graceful_release_action(struct osmo_fsm_inst *fi, uint32_t event, void *data) +{ + //struct up_peer *up_peer = fi->priv; + + switch (event) { + + case UP_PEER_EV_HEARTBEAT_FAILURE: + // FIXME + break; + + default: + OSMO_ASSERT(false); + } +} + +#define S(x) (1 << (x)) + +static const struct osmo_fsm_state up_peer_fsm_states[] = { + [UP_PEER_ST_NOT_ASSOCIATED] = { + .name = "not_associated", + .in_event_mask = 0 + | S(UP_PEER_EV_RX_ASSOC_SETUP_REQ) + , + .out_state_mask = 0 + | S(UP_PEER_ST_ASSOCIATED) + , + .action = up_peer_not_associated_action, + }, + [UP_PEER_ST_ASSOCIATED] = { + .name = "associated", + .in_event_mask = 0 + | S(UP_PEER_EV_RX_ASSOC_UPD_REQ) + | S(UP_PEER_EV_RX_SESSION_EST_REQ) + | S(UP_PEER_EV_HEARTBEAT_FAILURE) + , + .out_state_mask = 0 + | S(UP_PEER_ST_GRACEFUL_RELEASE) + , + .onenter = up_peer_associated_onenter, + .action = up_peer_associated_action, + }, + [UP_PEER_ST_GRACEFUL_RELEASE] = { + .name = "graceful_release", + .in_event_mask = 0 + | S(UP_PEER_EV_HEARTBEAT_FAILURE) + , + .out_state_mask = 0 + , + .onenter = up_peer_graceful_release_onenter, + .action = up_peer_graceful_release_action, + }, +}; + +static struct osmo_fsm up_peer_fsm = { + .name = "up_peer", + .states = up_peer_fsm_states, + .num_states = ARRAY_SIZE(up_peer_fsm_states), + .log_subsys = DSESSION, + .event_names = up_peer_fsm_event_names, + .timer_cb = up_peer_fsm_timer_cb, +}; + +static __attribute__((constructor)) void up_peer_fsm_register(void) +{ + OSMO_ASSERT(osmo_fsm_register(&up_peer_fsm) == 0); +} diff --git a/src/osmo-upf/up_session.c b/src/osmo-upf/up_session.c new file mode 100644 index 0000000..58188bf --- /dev/null +++ b/src/osmo-upf/up_session.c @@ -0,0 +1,1352 @@ +/* (C) 2021-2022 by sysmocom - s.f.m.c. GmbH + * All Rights Reserved + * + * Author: Neels Hofmeyr + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation; either version 3 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +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), + {0} +}; + +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) +{ + //struct up_session *up_session = fi->priv; + /* 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; +} + +/* 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. */ +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) +{ + 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 { + /* Choose a new F-TEID */ + *dst = (struct osmo_pfcp_ie_f_teid){ + .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 TEID 0x%x\n", dst->fixed.teid); + + if (osmo_pfcp_ip_addrs_set(&dst->fixed.ip_addr, &up_ep->pfcp_ep->cfg.local_addr)) { + LOGPFSML(session->fi, LOGL_ERROR, "Invalid local address in pfcp_endpoint cfg\n"); + return OSMO_PFCP_CAUSE_PFCP_ENTITY_IN_CONGESTION; + } + /* 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; + if (u->destination_iface_present) + p->destination_iface = u->destination_iface; + if (u->network_inst_present) { + p->network_inst = p->network_inst; + p->network_inst_present = true; + } + if (u->outer_header_creation_present) { + p->outer_header_creation = p->outer_header_creation; + p->outer_header_creation_present = true; + } + if (u->linked_te_id_present) { + p->linked_te_id = p->linked_te_id; + p->linked_te_id_present = true; + } + if (u->destination_iface_type_present) { + p->destination_iface_type = p->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, " encaps-"); + OSMO_STRBUF_APPEND(sb, osmo_pfcp_bits_to_str_buf, + f->forw_params.outer_header_creation.desc_bits, + osmo_pfcp_outer_header_creation_strs); + if (f->forw_params.outer_header_creation.teid_present) + OSMO_STRBUF_PRINTF(sb, " TEID-0x%x", f->forw_params.outer_header_creation.teid); + } + } + 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->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; +} + +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; + *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); + 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, &peer->up_endpoint->pfcp_ep->cfg.local_addr); + 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 up_session_fsm_cleanup(struct osmo_fsm_inst *fi, enum osmo_fsm_term_cause cause) +{ + struct up_session *session = fi->priv; + 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) + , + .out_state_mask = 0 + , + .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 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->forw_to_core = false; + pdr->forw_from_core = 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 (!is_forw(&pdr->far->desc.apply_action)) + return; + + pdr->forw_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->forw_from_core = (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; +} + +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. + */ +static void add_gtp_action_endecaps(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); + + /* To decaps, we need to have a local 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 TEID", pdr); + return; + } + + /* To encaps, we need to have a remote TEID assigned to send out in GTP packets, and we need to know where to + * send GTP to. */ + rpdr = pdr->reverse_pdr; + rfar = rpdr->far; + rfar_forw = &rfar->desc.forw_params; + 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 packets to be encapsulated, we need to know the assigned IP address for the UE, which receives the + * IP packets that should be placed into GTP. */ + 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_core = pdr->desc.pdr_id, + .pdr_access = rpdr->desc.pdr_id, + .kind = UP_GTP_U_ENDECAPS, + .endecaps = { + .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, + .ue_addr = rpdr->desc.pdi.ue_ip_address.ip_addr.v4, + }, + }; + + llist_add_tail(&a->entry, dst); +} + +static void add_gtp_action_forw(void *ctx, struct llist_head *dst, struct pdr *pdr) +{ + /* TODO implement GTP forwarding with TEID translation */ +} + +/* 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 forw_to_core directed PDRs, in the inner loop find the matching + * forw_from_core PDR. */ + if (!pdr->forw_to_core) + continue; + + /* If a required 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->forw_from_core) + 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 to-Core, where pdr->reverse_pdr will be the from-Core counterpart. */ + if (!pdr->forw_to_core) + continue; + + if (pdr->rx_decaps && !pdr->forw_encaps) + add_gtp_action_endecaps(ctx, dst, pdr); + else if (pdr->rx_decaps && pdr->forw_encaps) + add_gtp_action_forw(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); + setup_gtp_actions(session, &empty); +} + +/* Check whether the Packet Detection and Forwarding Action Rules amount to an encaps/decaps of GTP or a GTP forwarding, + * 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) +} diff --git a/src/osmo-upf/upf.c b/src/osmo-upf/upf.c index 8932d92..0a37a79 100644 --- a/src/osmo-upf/upf.c +++ b/src/osmo-upf/upf.c @@ -18,13 +18,74 @@ #include #include +#include + +#include #include +#include +#include struct g_upf *g_upf = NULL; +struct osmo_tdef_group g_upf_tdef_groups[] = { + { "pfcp", "PFCP endpoint timers", osmo_pfcp_tdefs, }, + {0} +}; + void g_upf_alloc(void *ctx) { OSMO_ASSERT(g_upf == NULL); g_upf = talloc_zero(ctx, struct g_upf); + + *g_upf = (struct g_upf){ + .pfcp = { + .vty_cfg = { + .local_addr = talloc_strdup(g_upf, UPF_PFCP_LISTEN_DEFAULT), + .local_port = OSMO_PFCP_PORT, + }, + }, + }; + + INIT_LLIST_HEAD(&g_upf->gtp.vty_cfg.devs); + INIT_LLIST_HEAD(&g_upf->gtp.devs); +} + +int upf_pfcp_listen() +{ + struct osmo_sockaddr_str local_addr_str; + struct osmo_sockaddr local_addr; + + OSMO_ASSERT(g_upf); + OSMO_ASSERT(g_upf->pfcp.ep == NULL); + + /* Translate address string from VTY config to osmo_sockaddr: first read into osmo_sockaddr_str, then write to + * osmo_sockaddr. */ + osmo_sockaddr_str_from_str(&local_addr_str, g_upf->pfcp.vty_cfg.local_addr, g_upf->pfcp.vty_cfg.local_port); + osmo_sockaddr_str_to_sockaddr(&local_addr_str, &local_addr.u.sas); + LOGP(DLPFCP, LOGL_NOTICE, "PFCP: Listening on %s\n", osmo_sockaddr_to_str_c(OTC_SELECT, &local_addr)); + + g_upf->pfcp.ep = up_endpoint_init(g_upf, &local_addr);; + if (!g_upf->pfcp.ep) { + fprintf(stderr, "Failed to allocate PFCP endpoint.\n"); + return -1; + } + return 0; +} + +int upf_gtp_devs_open() +{ + struct gtp_vty_cfg *c = &g_upf->gtp.vty_cfg; + struct gtp_vty_cfg_dev *d; + + llist_for_each_entry(d, &c->devs, entry) { + if (d->create) { + if (!upf_gtp_dev_create(d->dev_name, d->local_addr, false, false)) + return -1; + } else { + if (!upf_gtp_dev_use(d->dev_name)) + return -1; + } + } + return 0; } diff --git a/src/osmo-upf/upf_gtp.c b/src/osmo-upf/upf_gtp.c new file mode 100644 index 0000000..2c150d0 --- /dev/null +++ b/src/osmo-upf/upf_gtp.c @@ -0,0 +1,454 @@ +/* (C) 2021-2022 by sysmocom - s.f.m.c. GmbH + * All Rights Reserved + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation; either version 3 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include + +#define LOG_GTP_DEV(DEV, LEVEL, FMT, ARGS...) \ + LOGP(DGTP, LEVEL, "%s: " FMT, upf_gtp_dev_to_str_c(OTC_SELECT, (DEV)), ##ARGS) + +#define LOG_GTP_TUN(TUN, LEVEL, FMT, ARGS...) \ + LOGP(DGTP, LEVEL, "%s: " FMT, upf_gtp_tun_to_str_c(OTC_SELECT, (TUN)), ##ARGS) + +int upf_gtp_dev_to_str_buf(char *buf, size_t buflen, const struct upf_gtp_dev *dev) +{ + uint16_t v0_port; + struct osmo_strbuf sb = { .buf = buf, .len = buflen }; + OSMO_STRBUF_PRINTF(sb, "%s", dev->name ? : "null"); + if (dev->name && dev->ifidx) + OSMO_STRBUF_PRINTF(sb, " [%u]", dev->ifidx); + if (dev->sgsn_mode) + OSMO_STRBUF_PRINTF(sb, " (SGSN)"); + OSMO_STRBUF_PRINTF(sb, " "); + OSMO_STRBUF_APPEND(sb, osmo_sockaddr_to_str_buf2, &dev->gtpv1.local_addr); + v0_port = osmo_sockaddr_port(&dev->gtpv0.local_addr.u.sa); + if (dev->gtpv0.enabled && v0_port) + OSMO_STRBUF_PRINTF(sb, "/%u", v0_port); + return sb.chars_needed; +} + +char *upf_gtp_dev_to_str_c(void *ctx, const struct upf_gtp_dev *dev) +{ + OSMO_NAME_C_IMPL(ctx, 64, "ERROR", upf_gtp_dev_to_str_buf, dev) +} + +struct upf_gtp_dev *upf_gtp_dev_find_by_name(const char *name) +{ + struct upf_gtp_dev *dev; + llist_for_each_entry(dev, &g_upf->gtp.devs, entry) { + if (!strcmp(name, dev->name)) + return dev; + } + return NULL; +} + +struct upf_gtp_dev *upf_gtp_dev_first() +{ + return llist_first_entry_or_null(&g_upf->gtp.devs, struct upf_gtp_dev, entry); +} + +/* Tell the kernel to remove the GTP device. Called implicitly by talloc_free() (see upf_gtp_dev_destruct()). */ +static int upf_gtp_dev_delete(struct upf_gtp_dev *dev) +{ + int rc; + if (!dev->name) + return 0; + rc = gtp_dev_destroy(dev->name); + if (rc < 0) { + LOG_GTP_DEV(dev, LOGL_ERROR, "Error while deleting device: %s\n", strerror(errno)); + return rc; + } + LOG_GTP_DEV(dev, LOGL_NOTICE, "Deleted GTP device\n"); + dev->name = NULL; + return 0; +} + +static int upf_gtp_dev_destruct(struct upf_gtp_dev *dev); + +static struct upf_gtp_dev *upf_gtp_dev_alloc(const char *name, const char *local_addr) +{ + struct upf_gtp_dev *dev = upf_gtp_dev_find_by_name(name); + struct osmo_sockaddr_str addr_conv; + local_addr = local_addr ? : "0.0.0.0"; + if (dev) { + LOG_GTP_DEV(dev, LOGL_ERROR, "Device already exists. Cannot create %s %s\n", name, local_addr); + return NULL; + } + dev = talloc(g_upf, struct upf_gtp_dev); + *dev = (struct upf_gtp_dev){ + .name = talloc_strdup(dev, name), + .gtpv0.ofd.fd = -1, + .gtpv1.ofd.fd = -1, + }; + INIT_LLIST_HEAD(&dev->tunnels); + + osmo_sockaddr_str_from_str(&addr_conv, local_addr, PORT_GTP0_U); + + osmo_sockaddr_str_to_sockaddr(&addr_conv, &dev->gtpv0.local_addr.u.sas); + + addr_conv.port = PORT_GTP1_U; + osmo_sockaddr_str_to_sockaddr(&addr_conv, &dev->gtpv1.local_addr.u.sas); + + llist_add(&dev->entry, &g_upf->gtp.devs); + talloc_set_destructor(dev, upf_gtp_dev_destruct); + + return dev; +} + +static struct upf_gtp_dev *dev_resolve_ifidx(struct upf_gtp_dev *dev) +{ + int rc; + + dev->ifidx = if_nametoindex(dev->name); + if (dev->ifidx == 0) { + LOG_GTP_DEV(dev, LOGL_ERROR, "No such device: '%s'\n", dev->name); + talloc_free(dev); + return NULL; + } + /* Let's try something to see if talking to the device works. */ + errno = 0; + rc = gtp_list_tunnel(g_upf->gtp.genl_id, g_upf->gtp.nl); + if (errno) + rc = -errno; + else if (rc) + rc = -EINVAL; + if (rc) { + LOG_GTP_DEV(dev, LOGL_ERROR, "Failed to open GTP device: %s\n", strerror(-rc)); + talloc_free(dev); + return NULL; + } + + LOG_GTP_DEV(dev, LOGL_NOTICE, "GTP device ready (ifidx=%u)\n", dev->ifidx); + return dev; +} + +/* To clean up and deallocate, just call talloc_free() on the returned upf_gtp_dev*. */ +struct upf_gtp_dev *upf_gtp_dev_create(const char *name, const char *local_addr, bool listen_for_gtpv0, bool sgsn_mode) +{ + struct osmo_sockaddr any = { + .u.sin = { + .sin_family = AF_INET, + .sin_port = 0, + .sin_addr = { + .s_addr = INADDR_ANY, + }, + }, + }; + int gtp0_fd; + int gtp1_fd; + int rc; + struct upf_gtp_dev *dev = upf_gtp_dev_alloc(name, local_addr); + if (!dev) + return NULL; + dev->sgsn_mode = sgsn_mode; + + if (listen_for_gtpv0) { + dev->gtpv0.enabled = true; + rc = osmo_sock_init_osa_ofd(&dev->gtpv0.ofd, SOCK_DGRAM, 0, &dev->gtpv0.local_addr, &any, + OSMO_SOCK_F_BIND); + if (rc < 0) { + LOG_GTP_DEV(dev, LOGL_ERROR, "Cannot bind GTPv0 on %s (rc=%d)\n", + osmo_sockaddr_to_str_c(OTC_SELECT, &dev->gtpv0.local_addr), rc); + return NULL; + } + LOG_GTP_DEV(dev, LOGL_DEBUG, "GTPv0 bound\n"); + } + + /* GTPv1 */ + rc = osmo_sock_init_osa_ofd(&dev->gtpv1.ofd, SOCK_DGRAM, 0, &dev->gtpv1.local_addr, &any, + OSMO_SOCK_F_BIND); + if (rc < 0) { + LOG_GTP_DEV(dev, LOGL_ERROR, "Cannot bind GTPv1 (rc=%d)\n", rc); + return NULL; + } + LOG_GTP_DEV(dev, LOGL_DEBUG, "GTPv1 bound\n"); + + gtp0_fd = listen_for_gtpv0 ? dev->gtpv0.ofd.fd : -1; + gtp1_fd = dev->gtpv1.ofd.fd; + if (dev->sgsn_mode) + rc = gtp_dev_create_sgsn(-1, dev->name, gtp0_fd, gtp1_fd); + else + rc = gtp_dev_create(-1, dev->name, gtp0_fd, gtp1_fd); + if (rc < 0) { + LOG_GTP_DEV(dev, LOGL_ERROR, "Cannot create GTP device: rc=%d\n", rc); + /* name = NULL: signal to the destructor that it does not need to delete the device */ + dev->name = NULL; + talloc_free(dev); + return NULL; + } + + LOG_GTP_DEV(dev, LOGL_NOTICE, "created GTP device\n"); + dev->created = true; + + return dev_resolve_ifidx(dev); +} + +struct upf_gtp_dev *upf_gtp_dev_use(const char *name) +{ + struct upf_gtp_dev *dev = upf_gtp_dev_alloc(name, NULL); + if (!dev) + return NULL; + + return dev_resolve_ifidx(dev); +} + +void upf_gtp_devs_close() +{ + struct upf_gtp_dev *dev; + while ((dev = llist_first_entry_or_null(&g_upf->gtp.devs, struct upf_gtp_dev, entry))) + talloc_free(dev); +} + +void upf_gtp_genl_close() +{ + if (!g_upf->gtp.nl) + return; + genl_socket_close(g_upf->gtp.nl); + g_upf->gtp.nl = NULL; + g_upf->gtp.genl_id = -1; + + LOGP(DGTP, LOGL_NOTICE, "Closed mnl_socket\n"); +} + +/* Open an MNL socket which allows to create and remove GTP devices (requires CAP_NET_ADMIN). */ +int upf_gtp_genl_open() +{ + if (g_upf->gtp.nl && g_upf->gtp.genl_id >= 0) + return 0; + + if (g_upf->gtp.nl) + upf_gtp_genl_close(); + + g_upf->gtp.nl = genl_socket_open(); + if (!g_upf->gtp.nl) { + LOGP(DGTP, LOGL_ERROR, "Cannot open mnl_socket: %s\n", strerror(errno)); + return -EIO; + } + + g_upf->gtp.genl_id = genl_lookup_family(g_upf->gtp.nl, "gtp"); + if (g_upf->gtp.genl_id < 0) { + LOGP(DGTP, LOGL_ERROR, "genl family 'gtp' not found\n"); + return -ENOTSUP; + } + + LOGP(DGTP, LOGL_NOTICE, "Opened mnl_socket\n"); + return 0; +} + +struct upf_gtp_tun { + struct llist_head entry; + + struct upf_gtp_dev *dev; + struct upf_gtp_tun_desc desc; + bool active; +}; + +static int upf_gtp_tun_to_str_buf(char *buf, size_t buflen, const struct upf_gtp_tun *tun) +{ + struct osmo_strbuf sb = { .buf = buf, .len = buflen }; + OSMO_STRBUF_PRINTF(sb, "%s:tun{TEID=l:0x%x,r:0x%x UE=", tun->dev->name, tun->desc.local_teid, + tun->desc.remote_teid); + OSMO_STRBUF_APPEND(sb, osmo_sockaddr_to_str_buf2, &tun->desc.ue_addr); + OSMO_STRBUF_PRINTF(sb, " GTP-dst="); + OSMO_STRBUF_APPEND(sb, osmo_sockaddr_to_str_buf2, &tun->desc.gtp_remote_addr); + OSMO_STRBUF_PRINTF(sb, "}"); + return sb.chars_needed; +} + +static char *upf_gtp_tun_to_str_c(void *ctx, const struct upf_gtp_tun *tun) +{ + OSMO_NAME_C_IMPL(ctx, 64, "ERROR", upf_gtp_tun_to_str_buf, tun) +} + +static int upf_gtp_tun_deactivate(struct upf_gtp_tun *tun); + +static int upf_gtp_tun_destruct(struct upf_gtp_tun *tun) +{ + if (tun->active) + upf_gtp_tun_deactivate(tun); + llist_del(&tun->entry); + return 0; +} + +static struct upf_gtp_tun *upf_gtp_tun_alloc(struct upf_gtp_dev *dev, const struct upf_gtp_tun_desc *desc) +{ + struct upf_gtp_tun *tun = talloc(dev, struct upf_gtp_tun); + OSMO_ASSERT(tun); + *tun = (struct upf_gtp_tun){ + .dev = dev, + .desc = *desc, + }; + llist_add(&tun->entry, &dev->tunnels); + talloc_set_destructor(tun, upf_gtp_tun_destruct); + return tun; +} + +static struct gtp_tunnel *upf_gtp_tun_to_gtp_tunnel(struct upf_gtp_tun *tun) +{ + struct gtp_tunnel *t; + + if (tun->desc.ue_addr.u.sas.ss_family != AF_INET || tun->desc.gtp_remote_addr.u.sas.ss_family != AF_INET) { + LOG_GTP_TUN(tun, LOGL_ERROR, "Only capabale of IPv4\n"); + return NULL; + } + + t = gtp_tunnel_alloc(); + OSMO_ASSERT(t); + gtp_tunnel_set_ifidx(t, tun->dev->ifidx); + gtp_tunnel_set_version(t, GTP_V1); + gtp_tunnel_set_i_tei(t, tun->desc.local_teid); + gtp_tunnel_set_o_tei(t, tun->desc.remote_teid); + gtp_tunnel_set_ms_ip4(t, &tun->desc.ue_addr.u.sin.sin_addr); + gtp_tunnel_set_sgsn_ip4(t, &tun->desc.gtp_remote_addr.u.sin.sin_addr); + return t; +} + +int upf_gtp_tun_activate(struct upf_gtp_tun *tun) +{ + int rc; + struct gtp_tunnel *t; + + if (tun->active) + return -EALREADY; + + t = upf_gtp_tun_to_gtp_tunnel(tun); + if (!t) + return -ENOTSUP; + + errno = 0; + rc = gtp_add_tunnel(g_upf->gtp.genl_id, g_upf->gtp.nl, t); + if (errno) { + rc = -errno; + } else if (rc) { + rc = -EINVAL; + } else { + tun->active = true; + } + + gtp_tunnel_free(t); + return rc; +} + +static struct upf_gtp_tun *upf_gtp_dev_tunnel_find(struct upf_gtp_dev *dev, const struct upf_gtp_tun_desc *tun_desc) +{ + struct upf_gtp_tun *tun; + llist_for_each_entry(tun, &dev->tunnels, entry) { + if (upf_gtp_tun_desc_cmp(tun_desc, &tun->desc)) + continue; + return tun; + } + return NULL; +} + +int upf_gtp_dev_tunnel_add(struct upf_gtp_dev *dev, const struct upf_gtp_tun_desc *tun_desc) +{ + struct upf_gtp_tun *tun; + tun = upf_gtp_dev_tunnel_find(dev, tun_desc); + if (!tun) + tun = upf_gtp_tun_alloc(dev, tun_desc); + if (tun->active) + return 0; + return upf_gtp_tun_activate(tun); +} + +int upf_gtp_dev_tunnel_del(struct upf_gtp_dev *dev, const struct upf_gtp_tun_desc *tun_desc) +{ + struct upf_gtp_tun *tun; + int rc; + tun = upf_gtp_dev_tunnel_find(dev, tun_desc); + if (!tun) + return 0; + if (tun->active) { + rc = upf_gtp_tun_deactivate(tun); + if (rc) + return rc; + } + talloc_free(tun); + return 0; +} + +static int upf_gtp_tun_deactivate(struct upf_gtp_tun *tun) +{ + int rc; + struct gtp_tunnel *t; + + if (!tun->active) { + LOG_GTP_TUN(tun, LOGL_ERROR, "Cannot deactivate, not active\n"); + return -EINVAL; + } + + t = upf_gtp_tun_to_gtp_tunnel(tun); + if (!t) + return -EINVAL; + + rc = gtp_del_tunnel(g_upf->gtp.genl_id, g_upf->gtp.nl, t); + if (rc) + LOG_GTP_TUN(tun, LOGL_ERROR, "Failed to delete tunnel: %d %s\n", rc, strerror(rc)); + else + tun->active = false; + + gtp_tunnel_free(t); + return rc; +} + +static int upf_gtp_dev_destruct(struct upf_gtp_dev *dev) +{ + struct upf_gtp_tun *t; + /* Destruct and clean up all active tunnels before deleting the device */ + while ((t = llist_first_entry_or_null(&dev->tunnels, struct upf_gtp_tun, entry))) + talloc_free(t); + llist_del(&dev->entry); + osmo_fd_close(&dev->gtpv0.ofd); + osmo_fd_close(&dev->gtpv1.ofd); + if (dev->created) + upf_gtp_dev_delete(dev); + return 0; +} + +int upf_gtp_tun_desc_cmp(const struct upf_gtp_tun_desc *a, const struct upf_gtp_tun_desc *b) +{ + if (a == b) + return 0; + if (!a) + return -1; + if (!b) + return 1; + +#define CMP_RET(MEMB) do { \ + int _cmp = OSMO_CMP(a->MEMB, b->MEMB); \ + if (_cmp) \ + return _cmp; \ + } while (0) + + CMP_RET(local_teid); + CMP_RET(remote_teid); + return osmo_sockaddr_cmp(&a->gtp_remote_addr, &b->gtp_remote_addr); +} diff --git a/src/osmo-upf/upf_vty.c b/src/osmo-upf/upf_vty.c new file mode 100644 index 0000000..a724bee --- /dev/null +++ b/src/osmo-upf/upf_vty.c @@ -0,0 +1,281 @@ +/* OsmoUpf interface to quagga VTY */ +/* (C) 2021-2022 by sysmocom - s.f.m.c. GmbH + * All Rights Reserved + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation; either version 3 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +#include +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +enum upf_vty_node { + PFCP_NODE = _LAST_OSMOVTY_NODE + 1, + GTP_NODE, +}; + +static struct cmd_node cfg_pfcp_node = { + PFCP_NODE, + "%s(config-pfcp)# ", + 1, +}; + +#define pfcp_vty (g_upf->pfcp.vty_cfg) +#define gtp_vty (g_upf->gtp.vty_cfg) + +DEFUN(cfg_pfcp, cfg_pfcp_cmd, + "pfcp", + "Enter the PFCP configuration node\n") +{ + vty->node = PFCP_NODE; + return CMD_SUCCESS; +} + +static int config_write_pfcp(struct vty *vty) +{ + vty_out(vty, "pfcp%s", VTY_NEWLINE); + if (strcmp(UPF_PFCP_LISTEN_DEFAULT, pfcp_vty.local_addr)) + vty_out(vty, " local-addr %s%s", pfcp_vty.local_addr, VTY_NEWLINE); + return CMD_SUCCESS; +} + +DEFUN(cfg_pfcp_local_addr, cfg_pfcp_local_addr_cmd, + "local-addr IP_ADDR", + "Set the local IP address to bind on for PFCP\n" + "IP address\n") +{ + osmo_talloc_replace_string(g_upf, &pfcp_vty.local_addr, argv[0]); + return CMD_SUCCESS; +} + +static struct cmd_node cfg_gtp_node = { + GTP_NODE, + "%s(config-gtp)# ", + 1, +}; + +DEFUN(cfg_gtp, cfg_gtp_cmd, + "gtp", + "Enter the GTP configuration node\n") +{ + vty->node = GTP_NODE; + return CMD_SUCCESS; +} + +static int config_write_gtp(struct vty *vty) +{ + struct gtp_vty_cfg_dev *d; + vty_out(vty, "gtp%s", VTY_NEWLINE); + + llist_for_each_entry(d, >p_vty.devs, entry) { + if (d->create) { + vty_out(vty, " dev create %s", d->dev_name); + if (d->local_addr) + vty_out(vty, " %s", d->local_addr); + vty_out(vty, "%s", VTY_NEWLINE); + } else { + vty_out(vty, " dev use %s%s", d->dev_name, VTY_NEWLINE); + } + } + return CMD_SUCCESS; +} + +#define DEV_STR "Configure the GTP device to use for encaps/decaps.\n" + +DEFUN(cfg_gtp_dev_create, cfg_gtp_dev_create_cmd, + "dev create DEVNAME [LISTEN_ADDR]", + DEV_STR + "create a new GTP device. Will listen on GTPv1 port " OSMO_STRINGIFY_VAL(PORT_GTP1_U) + " and GTPv0 port " OSMO_STRINGIFY_VAL(PORT_GTP0_U) " on the specified interface, or on ANY if LISTEN_ADDR is" + " omitted.\n" + "device name, e.g. 'apn0'\n" + "IPv4 or IPv6 address to listen on, omit for any\n") +{ + struct gtp_vty_cfg_dev *d = talloc_zero(g_upf, struct gtp_vty_cfg_dev); + d->create = true; + d->dev_name = talloc_strdup(d, argv[0]); + if (argc > 1) + d->local_addr = talloc_strdup(d, argv[1]); + llist_add(&d->entry, >p_vty.devs); + vty_out(vty, "Added GTP device %s (create new)%s", d->dev_name, VTY_NEWLINE); + return CMD_SUCCESS; +} + +DEFUN(cfg_gtp_dev_use, cfg_gtp_dev_use_cmd, + "dev use DEVNAME", + DEV_STR + "use an existing GTP device, e.g. created by 'gtp-link'\n" + "device name, e.g. 'apn0'\n") +{ + struct gtp_vty_cfg_dev *d = talloc_zero(g_upf, struct gtp_vty_cfg_dev); + d->create = false; + d->dev_name = talloc_strdup(d, argv[0]); + llist_add(&d->entry, >p_vty.devs); + vty_out(vty, "Added GTP device %s (use existing)%s", d->dev_name, VTY_NEWLINE); + return CMD_SUCCESS; +} + +DEFUN(cfg_gtp_dev_del, cfg_gtp_dev_del_cmd, + "dev delete DEVNAME", + DEV_STR + "Remove a GTP device from the configuration, and delete the device if it was created here.\n" + "device name, e.g. 'apn0'\n") +{ + const char *dev_name = argv[0]; + struct gtp_vty_cfg_dev *d; + struct upf_gtp_dev *dev; + + /* remove from VTY cfg */ + llist_for_each_entry(d, >p_vty.devs, entry) { + if (strcmp(d->dev_name, dev_name)) + continue; + llist_del(&d->entry); + break; + } + + /* close device (and possibly delete from system, via talloc destructor) */ + dev = upf_gtp_dev_find_by_name(dev_name); + if (dev) + talloc_free(dev); + return CMD_SUCCESS; +} + +DEFUN(show_pdr, show_pdr_cmd, + "show pdr", + SHOW_STR + "List all sessions' PDR and FAR status\n") +{ + struct up_peer *peer; + int active_count = 0; + int inactive_count = 0; + llist_for_each_entry(peer, &g_upf->pfcp.ep->peers, entry) { + 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->active) { + vty_out(vty, "%s: inactive: %s%s%s%s", + session->fi->id, pdr_to_str_c(OTC_SELECT, pdr), + pdr->inactive_reason ? ": " : "", + pdr->inactive_reason ? : "", + VTY_NEWLINE); + inactive_count++; + } else { + vty_out(vty, "%s: active: %s%s", + session->fi->id, pdr_to_str_c(OTC_SELECT, pdr), + VTY_NEWLINE); + active_count++; + } + } + } + } + vty_out(vty, "(%d of %d active)%s", active_count, active_count + inactive_count, VTY_NEWLINE); + return CMD_SUCCESS; +} + +DEFUN(show_gtp, show_gtp_cmd, + "show gtp", + SHOW_STR + "Active GTP tunnels and forwardings\n") +{ + struct up_peer *peer; + int count = 0; + + if (!upf_gtp_dev_first()) { + vty_out(vty, "No GTP device open%s", VTY_NEWLINE); + return CMD_SUCCESS; + } + + llist_for_each_entry(peer, &g_upf->pfcp.ep->peers, entry) { + struct up_session *session; + int bkt; + hash_for_each(peer->sessions_by_up_seid, bkt, session, node_by_up_seid) { + struct up_gtp_action *a; + llist_for_each_entry(a, &session->active_gtp_actions, entry) { + vty_out(vty, "%s%s", up_gtp_action_to_str_c(OTC_SELECT, a), VTY_NEWLINE); + count++; + } + } + } + vty_out(vty, "(%d active)%s", count, VTY_NEWLINE); + return CMD_SUCCESS; +} + +DEFUN(show_session, show_session_cmd, + "show session", + SHOW_STR + "PFCP Session status\n") +{ + struct up_peer *peer; + int inactive_count = 0; + int active_count = 0; + int fully_active_count = 0; + + llist_for_each_entry(peer, &g_upf->pfcp.ep->peers, entry) { + struct up_session *session; + int bkt; + hash_for_each(peer->sessions_by_up_seid, bkt, session, node_by_up_seid) { + vty_out(vty, "%s %s%s", + up_session_to_str_c(OTC_SELECT, session), + up_session_gtp_status(session), VTY_NEWLINE); + if (up_session_is_active(session)) { + if (up_session_is_fully_active(session, NULL, NULL)) + fully_active_count++; + else + active_count++; + } else { + inactive_count++; + } + } + } + vty_out(vty, "(%d fully-active + %d partially active + %d inactive)%s", + fully_active_count, active_count, inactive_count, VTY_NEWLINE); + return CMD_SUCCESS; +} + +void upf_vty_init() +{ + OSMO_ASSERT(g_upf != NULL); + + install_element_ve(&show_pdr_cmd); + install_element_ve(&show_gtp_cmd); + install_element_ve(&show_session_cmd); + + install_node(&cfg_pfcp_node, config_write_pfcp); + install_element(CONFIG_NODE, &cfg_pfcp_cmd); + + install_element(PFCP_NODE, &cfg_pfcp_local_addr_cmd); + + install_node(&cfg_gtp_node, config_write_gtp); + install_element(CONFIG_NODE, &cfg_gtp_cmd); + + install_element(GTP_NODE, &cfg_gtp_dev_create_cmd); + install_element(GTP_NODE, &cfg_gtp_dev_use_cmd); + install_element(GTP_NODE, &cfg_gtp_dev_del_cmd); +} + diff --git a/tests/upf.vty b/tests/upf.vty index 02ea1af..ffaf969 100644 --- a/tests/upf.vty +++ b/tests/upf.vty @@ -2,3 +2,34 @@ OsmoUPF> enable OsmoUPF# configure terminal OsmoUPF(config)# show running-config ... + +OsmoUPF(config)# pfcp +OsmoUPF(config-pfcp)# list +... + local-addr IP_ADDR +OsmoUPF(config-pfcp)# local-addr? + local-addr Set the local IP address to bind on for PFCP +OsmoUPF(config-pfcp)# local-addr ? + IP_ADDR IP address +OsmoUPF(config-pfcp)# exit + +OsmoUPF(config)# gtp +OsmoUPF(config-gtp)# list +... + dev create DEVNAME [LISTEN_ADDR] + dev use DEVNAME + dev delete DEVNAME + +OsmoUPF(config-gtp)# dev? + dev Configure the GTP device to use for encaps/decaps. +OsmoUPF(config-gtp)# dev ? + create create a new GTP device. Will listen on GTPv1 port 2152 and GTPv0 port 3386 on the specified interface, or on ANY if LISTEN_ADDR is omitted. + use use an existing GTP device, e.g. created by 'gtp-link' + delete Remove a GTP device from the configuration, and delete the device if it was created here. +OsmoUPF(config-gtp)# dev create ? + DEVNAME device name, e.g. 'apn0' +OsmoUPF(config-gtp)# dev create foo ? + [LISTEN_ADDR] IPv4 or IPv6 address to listen on, omit for any +OsmoUPF(config-gtp)# dev delete ? + DEVNAME device name, e.g. 'apn0' +OsmoUPF(config-gtp)# exit