neels submitted this change.
add osmo-upf
Related: SYS#5599
Change-Id: I745bcbde6859004c41ddbfd2558036bf9a2d1de2
---
A doc/examples/osmo-upf/osmo-upf-create-dev.cfg
M doc/examples/osmo-upf/osmo-upf.cfg
M include/osmocom/upf/Makefile.am
A include/osmocom/upf/up_endpoint.h
A include/osmocom/upf/up_gtp_action.h
A include/osmocom/upf/up_peer.h
A include/osmocom/upf/up_session.h
A include/osmocom/upf/up_session_to_gtp.c
M include/osmocom/upf/upf.h
A include/osmocom/upf/upf_gtp.h
A include/osmocom/upf/upf_nft.h
M src/osmo-upf/Makefile.am
M src/osmo-upf/osmo_upf_main.c
A src/osmo-upf/up_endpoint.c
A src/osmo-upf/up_gtp_action.c
A src/osmo-upf/up_peer.c
A src/osmo-upf/up_peer_fsm.c
A src/osmo-upf/up_session.c
M src/osmo-upf/upf.c
A src/osmo-upf/upf_gtp.c
A src/osmo-upf/upf_vty.c
M tests/upf.vty
22 files changed, 4,054 insertions(+), 3 deletions(-)
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..54ca643
--- /dev/null
+++ b/doc/examples/osmo-upf/osmo-upf-create-dev.cfg
@@ -0,0 +1,16 @@
+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 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 @@
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..3bb14ec
--- /dev/null
+++ b/include/osmocom/upf/up_endpoint.h
@@ -0,0 +1,48 @@
+/*
+ * (C) 2021-2022 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved.
+ *
+ * Author: Neels Janosch Hofmeyr <nhofmeyr@sysmocom.de>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#pragma once
+
+#include <osmocom/core/linuxlist.h>
+
+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..1907ed1
--- /dev/null
+++ b/include/osmocom/upf/up_gtp_action.h
@@ -0,0 +1,73 @@
+/*
+ * (C) 2021-2022 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved.
+ *
+ * Author: Neels Janosch Hofmeyr <nhofmeyr@sysmocom.de>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/core/socket.h>
+#include <osmocom/core/fsm.h>
+
+#include <osmocom/upf/up_session.h>
+#include <osmocom/upf/upf_gtp.h>
+#include <osmocom/upf/upf_nft.h>
+
+#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_TUNMAP,
+};
+
+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;
+
+ /* Tunnel-map GTP: translate from one TEID to another and forward */
+ struct upf_nft_tunmap_desc tunmap;
+ };
+
+ /* 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..0c168cf
--- /dev/null
+++ b/include/osmocom/upf/up_peer.h
@@ -0,0 +1,78 @@
+/*
+ * (C) 2021-2022 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved.
+ *
+ * Author: Neels Janosch Hofmeyr <nhofmeyr@sysmocom.de>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#pragma once
+
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/core/hashtable.h>
+#include <osmocom/core/socket.h>
+#include <osmocom/core/use_count.h>
+
+#include <osmocom/pfcp/pfcp_msg.h>
+
+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..5b514a7
--- /dev/null
+++ b/include/osmocom/upf/up_session.h
@@ -0,0 +1,117 @@
+/*
+ * (C) 2021-2022 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved.
+ *
+ * Author: Neels Janosch Hofmeyr <nhofmeyr@sysmocom.de>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+#include <osmocom/core/hashtable.h>
+#include <osmocom/core/use_count.h>
+
+#include <osmocom/pfcp/pfcp_msg.h>
+
+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..161ba80
--- /dev/null
+++ b/include/osmocom/upf/up_session_to_gtp.c
@@ -0,0 +1,23 @@
+/*
+ * (C) 2021-2022 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved.
+ *
+ * Author: Neels Janosch Hofmeyr <nhofmeyr@sysmocom.de>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
diff --git a/include/osmocom/upf/upf.h b/include/osmocom/upf/upf.h
index c3c7849..b8065dd 100644
--- a/include/osmocom/upf/upf.h
+++ b/include/osmocom/upf/upf.h
@@ -24,12 +24,78 @@
#pragma once
+#include <osmocom/core/tdef.h>
+#include <osmocom/core/socket.h>
+#include <osmocom/core/select.h>
+#include <osmocom/core/linuxlist.h>
+
+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;
+
+ /* If true, osmo-upf creates the GTP device on startup. If false, the GTP device was created by the user, and we
+ * just plug into it. */
+ bool create;
+
+ /* GTP device name as shown by 'ip link', e.g. 'apn0' */
+ char *dev_name;
+
+ /* Which address the GTP device should listen on.
+ * When create == true, the GTP kernel module is created to listen on this address.
+ * Also used to listen for GTP ECHO requests using libgtp. */
+ char *local_addr;
+};
+
+struct gtp_vty_cfg {
+ /* list of struct gtp_vty_cfg_dev, GTP devices as in the config file. The actual GTP devices in use are in
+ * g_upf->gtp.devs. */
+ struct llist_head devs;
+};
+
struct g_upf {
struct ctrl_handle *ctrl;
+
+ struct {
+ struct pfcp_vty_cfg vty_cfg;
+ struct up_endpoint *ep;
+ } pfcp;
+ struct {
+ /* GTP devices as in osmo-upf.cfg */
+ struct gtp_vty_cfg vty_cfg;
+
+ /* GTP devices actually in use, list of struct upf_gtp_dev. */
+ 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..a67c850
--- /dev/null
+++ b/include/osmocom/upf/upf_gtp.h
@@ -0,0 +1,85 @@
+/*
+ * (C) 2021-2022 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved.
+ *
+ * Author: Neels Janosch Hofmeyr <nhofmeyr@sysmocom.de>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#pragma once
+
+#include <osmocom/core/linuxlist.h>
+#include <osmocom/core/select.h>
+#include <osmocom/core/logging.h>
+
+#define LOG_GTP_DEV(DEV, LEVEL, FMT, ARGS...) \
+ LOGP(DGTP, LEVEL, "%s: " FMT, upf_gtp_dev_to_str_c(OTC_SELECT, (DEV)), ##ARGS)
+
+#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;
+
+ /* If true, osmo-upf created this GTP device on startup and will destroy it on program exit. If false, the user
+ * has created the device and osmo-upf will not destroy it. */
+ 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();
+
+int upf_gtp_dev_open(const char *name, bool create_gtp_dev, const char *local_addr, bool listen_for_gtpv0,
+ bool sgsn_mode);
+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..569eadb
--- /dev/null
+++ b/include/osmocom/upf/upf_nft.h
@@ -0,0 +1,48 @@
+/*
+ * (C) 2021-2022 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved.
+ *
+ * Author: Neels Janosch Hofmeyr <nhofmeyr@sysmocom.de>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+#include <osmocom/core/socket.h>
+
+struct upf_nft_tunmap_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_tunmap_create(struct upf_nft_tunmap_desc *tunmap);
+int upf_nft_tunmap_delete(struct upf_nft_tunmap_desc *tunmap);
diff --git a/src/osmo-upf/Makefile.am b/src/osmo-upf/Makefile.am
index ec04fc7..d8aa5a0 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,14 @@
$(LIBOSMOCORE_CFLAGS) \
$(LIBOSMOVTY_CFLAGS) \
$(LIBOSMOCTRL_CFLAGS) \
+ $(LIBOSMOGTLV_CFLAGS) \
+ $(LIBOSMOPFCP_CFLAGS) \
+ $(LIBGTPNL_CFLAGS) \
$(COVERAGE_CFLAGS) \
$(NULL)
AM_LDFLAGS = \
+ $(LIBGTPNL_LDFLAGS) \
$(COVERAGE_LDFLAGS) \
$(NULL)
@@ -22,12 +27,21 @@
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 = \
$(LIBOSMOCORE_LIBS) \
$(LIBOSMOVTY_LIBS) \
$(LIBOSMOCTRL_LIBS) \
+ $(LIBOSMOGTLV_LIBS) \
+ $(LIBOSMOPFCP_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 f81eab1..302d957 100644
--- a/src/osmo-upf/osmo_upf_main.c
+++ b/src/osmo-upf/osmo_upf_main.c
@@ -34,11 +34,16 @@
#include <osmocom/vty/cpu_sched_vty.h>
#include <osmocom/vty/telnet_interface.h>
#include <osmocom/vty/ports.h>
+#include <osmocom/vty/tdef_vty.h>
#include <osmocom/ctrl/control_if.h>
#include <osmocom/ctrl/control_vty.h>
#include <osmocom/ctrl/ports.h>
+#include <osmocom/pfcp/pfcp_endpoint.h>
+
#include <osmocom/upf/upf.h>
+#include <osmocom/upf/up_endpoint.h>
+#include <osmocom/upf/upf_gtp.h>
#define _GNU_SOURCE
#include <getopt.h>
@@ -48,6 +53,7 @@
#include <signal.h>
#include <stdio.h>
+#include <string.h>
extern void *tall_vty_ctx;
@@ -207,6 +213,30 @@
};
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 = {
@@ -217,19 +247,27 @@
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);
@@ -246,6 +284,9 @@
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);
@@ -283,6 +324,15 @@
}
}
+ 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);
@@ -301,12 +351,20 @@
}
} 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);
+ 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);
diff --git a/src/osmo-upf/up_endpoint.c b/src/osmo-upf/up_endpoint.c
new file mode 100644
index 0000000..b33e112
--- /dev/null
+++ b/src/osmo-upf/up_endpoint.c
@@ -0,0 +1,322 @@
+/*
+ * (C) 2021-2022 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved.
+ *
+ * Author: Neels Janosch Hofmeyr <nhofmeyr@sysmocom.de>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <osmocom/pfcp/pfcp_endpoint.h>
+#include <osmocom/pfcp/pfcp_msg.h>
+
+#include <osmocom/upf/up_endpoint.h>
+#include <osmocom/upf/up_peer.h>
+#include <osmocom/upf/up_session.h>
+
+static void up_endpoint_set_msg_ctx(struct osmo_pfcp_endpoint *ep, struct osmo_pfcp_msg *m, struct osmo_pfcp_msg *req)
+{
+ struct up_endpoint *up_ep = ep->priv;
+ struct up_peer *peer;
+
+ /* If this is a response to an earlier request, just take the msg context from the request message. */
+ if (req) {
+ if (!m->ctx.peer_fi && req->ctx.peer_fi)
+ up_peer_set_msg_ctx(req->ctx.peer_fi->priv, m);
+ if (!m->ctx.session_fi && req->ctx.session_fi)
+ up_session_set_msg_ctx(req->ctx.peer_fi->priv, m);
+ }
+
+ /* From the remote address, find the matching peer instance */
+ 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);
+}
+
+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 osmo_pfcp_msg *req)
+{
+ struct up_endpoint *up_ep = ep->priv;
+
+ switch (m->h.message_type) {
+ 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;
+}
+
+uint64_t up_endpoint_next_seid(struct up_endpoint *ep)
+{
+ uint64_t sanity;
+ for (sanity = 2342; sanity; sanity--) {
+ uint64_t next_seid = osmo_pfcp_next_seid(&ep->next_seid_state);
+ 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..47aa3ac
--- /dev/null
+++ b/src/osmo-upf/up_gtp_action.c
@@ -0,0 +1,162 @@
+/*
+ * (C) 2021-2022 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved.
+ *
+ * Author: Neels Janosch Hofmeyr <nhofmeyr@sysmocom.de>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <errno.h>
+#include <string.h>
+
+#include <osmocom/core/utils.h>
+
+#include <osmocom/upf/upf.h>
+#include <osmocom/upf/up_gtp_action.h>
+#include <osmocom/upf/up_peer.h>
+#include <osmocom/upf/up_session.h>
+
+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_MEMB(MEMB) OSMO_CMP(a->MEMB, b->MEMB)
+
+ if ((cmp = CMP_MEMB(kind)))
+ return cmp;
+
+ switch (a->kind) {
+ case UP_GTP_U_ENDECAPS:
+ if ((cmp = CMP_MEMB(endecaps.local_teid)))
+ return cmp;
+ if ((cmp = CMP_MEMB(endecaps.remote_teid)))
+ return cmp;
+ 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_TUNMAP:
+ if ((cmp = CMP_MEMB(tunmap.access.local_teid)))
+ return cmp;
+ if ((cmp = CMP_MEMB(tunmap.access.remote_teid)))
+ return cmp;
+ if ((cmp = CMP_MEMB(tunmap.core.local_teid)))
+ return cmp;
+ if ((cmp = CMP_MEMB(tunmap.core.remote_teid)))
+ return cmp;
+ 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_TUNMAP:
+ 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_TUNMAP:
+ OSMO_STRBUF_PRINTF(sb, "GTP:tunmap GTP-access:");
+ OSMO_STRBUF_APPEND(sb, osmo_sockaddr_to_str_buf2, &a->tunmap.access.gtp_remote_addr);
+ OSMO_STRBUF_PRINTF(sb, " TEID-access-r:0x%"PRIx32" TEID-access-l:0x%"PRIx32" GTP-core:",
+ a->tunmap.access.remote_teid, a->tunmap.access.local_teid);
+ OSMO_STRBUF_APPEND(sb, osmo_sockaddr_to_str_buf2, &a->tunmap.core.gtp_remote_addr);
+ OSMO_STRBUF_PRINTF(sb, " TEID-core-r:0x%"PRIx32" TEID-core-l:0x%"PRIx32,
+ a->tunmap.core.remote_teid, a->tunmap.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..d0d7dbd
--- /dev/null
+++ b/src/osmo-upf/up_peer.c
@@ -0,0 +1,545 @@
+/*
+ * (C) 2021-2022 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved.
+ *
+ * Author: Neels Janosch Hofmeyr <nhofmeyr@sysmocom.de>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <errno.h>
+
+#include <osmocom/core/fsm.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/core/tdef.h>
+#include <osmocom/core/utils.h>
+
+#include <osmocom/pfcp/pfcp_msg.h>
+#include <osmocom/pfcp/pfcp_endpoint.h>
+
+#include <osmocom/upf/upf.h>
+#include <osmocom/upf/up_peer.h>
+#include <osmocom/upf/up_endpoint.h>
+#include <osmocom/upf/up_session.h>
+
+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),
+ {}
+};
+
+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;
+
+ /* 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);
+
+ 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 (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");
+ }
+ } else if (up_peer_fsm_state_chg(UP_PEER_ST_ASSOCIATED)) {
+ /* Not allowed to transition to ST_ASSOCIATED */
+ cause = OSMO_PFCP_CAUSE_REQUEST_REJECTED;
+ } else {
+ /* Successfully transitioned to ST_ASSOCIATED */
+ peer->remote_recovery_timestamp = m->ies.assoc_setup_req.recovery_time_stamp;
+ peer->remote_node_id = m->ies.assoc_setup_req.node_id;
+ if (m->ies.assoc_setup_req.cp_function_features_present)
+ peer->peer_cp_features = m->ies.assoc_setup_req.cp_function_features;
+ }
+
+ 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);
+
+ LOGPFSML(fi, LOGL_NOTICE, "Peer associated, Node-Id=%s. Local UP features: [%s]; Peer CP features: [%s]\n",
+ osmo_pfcp_ie_node_id_to_str_c(OTC_SELECT, &peer->remote_node_id),
+ 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_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_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)
+{
+ struct up_peer *peer = fi->priv;
+ if (next_state != UP_PEER_ST_ASSOCIATED)
+ LOGPFSML(fi, LOGL_NOTICE, "Peer %s released\n",
+ osmo_pfcp_ie_node_id_to_str_c(OTC_SELECT, &peer->remote_node_id));
+}
+
+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)
+ ,
+ .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..ec37c0b
--- /dev/null
+++ b/src/osmo-upf/up_peer_fsm.c
@@ -0,0 +1,196 @@
+/*
+ * (C) 2021-2022 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved.
+ *
+ * Author: Neels Janosch Hofmeyr <nhofmeyr@sysmocom.de>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <osmocom/core/utils.h>
+#include <osmocom/core/fsm.h>
+
+#include <osmocom/upf/up_peer.h>
+
+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),
+ {}
+};
+
+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..1cb65d7
--- /dev/null
+++ b/src/osmo-upf/up_session.c
@@ -0,0 +1,1358 @@
+/*
+ * (C) 2021-2022 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved.
+ *
+ * Author: Neels Janosch Hofmeyr <nhofmeyr@sysmocom.de>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <errno.h>
+
+#include <osmocom/core/utils.h>
+#include <osmocom/core/fsm.h>
+
+#include <osmocom/pfcp/pfcp_msg.h>
+#include <osmocom/pfcp/pfcp_endpoint.h>
+
+#include <osmocom/upf/upf.h>
+#include <osmocom/upf/up_endpoint.h>
+#include <osmocom/upf/up_peer.h>
+#include <osmocom/upf/up_session.h>
+#include <osmocom/upf/up_gtp_action.h>
+
+static enum osmo_pfcp_cause up_session_setup_gtp(struct up_session *session);
+
+void up_session_set_msg_ctx(struct up_session *session, struct osmo_pfcp_msg *m)
+{
+ if (!m->ctx.peer_fi)
+ up_peer_set_msg_ctx(session->up_peer, m);
+
+ OSMO_ASSERT(!m->ctx.session_fi);
+
+ m->ctx.session_fi = session->fi;
+ m->ctx.session_use_count = &session->use_count;
+ m->ctx.session_use_token = (m->rx ? UP_USE_MSG_RX : UP_USE_MSG_TX);
+ osmo_use_count_get_put(m->ctx.session_use_count, m->ctx.session_use_token, 1);
+}
+
+enum up_session_fsm_state {
+ UP_SESSION_ST_INIT,
+ UP_SESSION_ST_ESTABLISHED,
+ UP_SESSION_ST_WAIT_USE_COUNT,
+};
+
+static const struct value_string up_session_fsm_event_names[] = {
+ OSMO_VALUE_STRING(UP_SESSION_EV_RX_SESSION_EST_REQ),
+ OSMO_VALUE_STRING(UP_SESSION_EV_RX_SESSION_MOD_REQ),
+ OSMO_VALUE_STRING(UP_SESSION_EV_RX_SESSION_DEL_REQ),
+ OSMO_VALUE_STRING(UP_SESSION_EV_USE_COUNT_ZERO),
+ {}
+};
+
+static struct osmo_fsm up_session_fsm;
+
+static const struct osmo_tdef_state_timeout up_session_fsm_timeouts[32] = {
+ [UP_SESSION_ST_INIT] = { .T = 0 },
+ [UP_SESSION_ST_ESTABLISHED] = { .T = 0 },
+ [UP_SESSION_ST_WAIT_USE_COUNT] = { .T = 0 },
+};
+
+/* Transition to a state, using the T timer defined in up_session_fsm_timeouts.
+ * Assumes local variable fi exists. */
+#define up_session_fsm_state_chg(STATE) do { \
+ if (fi->state != STATE) \
+ osmo_tdef_fsm_inst_state_chg(fi, STATE, \
+ up_session_fsm_timeouts, \
+ osmo_pfcp_tdefs, \
+ 5); \
+ } while (0)
+
+static int up_session_fsm_timer_cb(struct osmo_fsm_inst *fi)
+{
+ /* Return 1 to terminate FSM instance, 0 to keep running */
+ return 1;
+}
+
+struct osmo_pfcp_msg *up_session_init_tx(struct up_session *session, struct osmo_pfcp_msg *in_reply_to,
+ enum osmo_pfcp_message_type message_type)
+{
+ struct osmo_pfcp_msg *tx = up_peer_init_tx(session->up_peer, in_reply_to, message_type);
+ tx->h.seid = session->cp_f_seid.seid;
+ tx->h.seid_present = true;
+ up_session_set_msg_ctx(session, tx);
+ return tx;
+}
+
+struct chosen_f_teid {
+ struct llist_head entry;
+ uint8_t choose_id;
+ struct osmo_pfcp_ie_f_teid f_teid;
+};
+
+struct chosen_f_teid *chosen_f_teid_find(struct llist_head *list, uint8_t choose_id)
+{
+ struct chosen_f_teid *chosen;
+ llist_for_each_entry(chosen, list, entry) {
+ if (chosen->choose_id == choose_id)
+ return chosen;
+ }
+ return NULL;
+}
+
+/* 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 drop_gtp_actions(struct up_session *session);
+
+static void up_session_fsm_cleanup(struct osmo_fsm_inst *fi, enum osmo_fsm_term_cause cause)
+{
+ struct up_session *session = fi->priv;
+
+ drop_gtp_actions(session);
+
+ up_session_clear_pdr_far(session);
+
+ hash_del(&session->node_by_up_seid);
+ hash_del(&session->node_by_cp_seid);
+}
+
+#define S(x) (1 << (x))
+
+static const struct osmo_fsm_state up_session_fsm_states[] = {
+ [UP_SESSION_ST_INIT] = {
+ .name = "INIT",
+ .in_event_mask = 0
+ | S(UP_SESSION_EV_RX_SESSION_EST_REQ)
+ | S(UP_SESSION_EV_RX_SESSION_DEL_REQ)
+ | S(UP_SESSION_EV_USE_COUNT_ZERO)
+ ,
+ .out_state_mask = 0
+ | S(UP_SESSION_ST_ESTABLISHED)
+ | S(UP_SESSION_ST_WAIT_USE_COUNT)
+ ,
+ .action = up_session_init_action,
+ },
+ [UP_SESSION_ST_ESTABLISHED] = {
+ .name = "ESTABLISHED",
+ .in_event_mask = 0
+ | S(UP_SESSION_EV_RX_SESSION_MOD_REQ)
+ | S(UP_SESSION_EV_RX_SESSION_DEL_REQ)
+ | S(UP_SESSION_EV_USE_COUNT_ZERO)
+ ,
+ .out_state_mask = 0
+ | S(UP_SESSION_ST_WAIT_USE_COUNT)
+ ,
+ .onenter = up_session_established_onenter,
+ .action = up_session_established_action,
+ .onleave = up_session_established_onleave,
+ },
+ [UP_SESSION_ST_WAIT_USE_COUNT] = {
+ .name = "WAIT_USE_COUNT",
+ .in_event_mask = 0
+ | S(UP_SESSION_EV_USE_COUNT_ZERO)
+ ,
+ .onenter = up_session_wait_use_count_onenter,
+ .action = up_session_wait_use_count_action,
+ },
+};
+
+static struct osmo_fsm up_session_fsm = {
+ .name = "up_session",
+ .states = up_session_fsm_states,
+ .num_states = ARRAY_SIZE(up_session_fsm_states),
+ .log_subsys = DSESSION,
+ .event_names = up_session_fsm_event_names,
+ .timer_cb = up_session_fsm_timer_cb,
+ .cleanup = up_session_fsm_cleanup,
+};
+
+static __attribute__((constructor)) void up_session_fsm_register(void)
+{
+ OSMO_ASSERT(osmo_fsm_register(&up_session_fsm) == 0);
+}
+
+static int up_session_use_cb(struct osmo_use_count_entry *e, int32_t old_use_count, const char *file, int line)
+{
+ struct up_session *session = e->use_count->talloc_object;
+ int32_t total;
+ int level;
+
+ if (!e->use)
+ return -EINVAL;
+
+ total = osmo_use_count_total(&session->use_count);
+
+ if (total == 0
+ || (total == 1 && old_use_count == 0 && e->count == 1))
+ level = LOGL_INFO;
+ else
+ level = LOGL_DEBUG;
+
+ LOGPFSMSLSRC(session->fi, DREF, level, file, line,
+ "%s %s: now used by %s\n",
+ (e->count - old_use_count) > 0 ? "+" : "-", e->use,
+ osmo_use_count_to_str_c(OTC_SELECT, &session->use_count));
+
+ if (e->count < 0)
+ return -ERANGE;
+
+ if (total == 0)
+ osmo_fsm_inst_dispatch(session->fi, UP_SESSION_EV_USE_COUNT_ZERO, NULL);
+ return 0;
+}
+
+static void up_session_update_id(struct up_session *session)
+{
+ osmo_fsm_inst_update_id_f_sanitize(session->fi, '-', "%s-0x%" PRIx64,
+ up_peer_remote_addr_str(session->up_peer),
+ session->up_seid);
+ LOGPFSML(session->fi, LOGL_DEBUG, "Updated id\n");
+}
+
+static inline uint64_t up_session_key(uint64_t cp_seid, uint64_t up_seid)
+{
+ return cp_seid + up_seid;
+}
+
+static struct up_session *up_session_add(struct up_peer *peer, const struct osmo_pfcp_ie_f_seid *cp_f_seid)
+{
+ struct up_session *session;
+ uint64_t up_seid = up_endpoint_next_seid(peer->up_endpoint);
+
+ if (!up_seid)
+ return NULL;
+
+ struct osmo_fsm_inst *fi = osmo_fsm_inst_alloc_child(&up_session_fsm, peer->fi, UP_PEER_EV_SESSION_TERM);
+ OSMO_ASSERT(fi);
+
+ session = talloc(fi, struct up_session);
+ OSMO_ASSERT(session);
+ fi->priv = session;
+
+ *session = (struct up_session) {
+ .fi = fi,
+ .up_peer = peer,
+ .cp_f_seid = *cp_f_seid,
+ .up_seid = up_seid,
+ .use_count = {
+ .talloc_object = session,
+ .use_cb = up_session_use_cb,
+ },
+ };
+ INIT_LLIST_HEAD(&session->pdrs);
+ INIT_LLIST_HEAD(&session->fars);
+ INIT_LLIST_HEAD(&session->chosen_f_teids);
+ INIT_LLIST_HEAD(&session->active_gtp_actions);
+ osmo_use_count_make_static_entries(&session->use_count, session->use_count_buf, ARRAY_SIZE(session->use_count_buf));
+ LOGPFSML(session->fi, LOGL_INFO, "Allocated new UP-SEID: 0x%" PRIx64 "\n", session->up_seid);
+ up_session_update_id(session);
+
+ hash_add(peer->sessions_by_up_seid, &session->node_by_up_seid, session->up_seid);
+ hash_add(peer->sessions_by_cp_seid, &session->node_by_cp_seid, session->cp_f_seid.seid);
+ return session;
+}
+
+struct up_session *up_session_find_or_add(struct up_peer *peer, const struct osmo_pfcp_ie_f_seid *cp_f_seid,
+ const struct osmo_pfcp_ie_f_seid *up_f_seid)
+{
+ struct up_session *session;
+ if (cp_f_seid)
+ session = up_session_find_by_cp_f_seid(peer, cp_f_seid);
+ else if (up_f_seid)
+ session = up_session_find_by_up_seid(peer, up_f_seid->seid);
+ else
+ return NULL;
+ if (session)
+ return session;
+
+ return up_session_add(peer, cp_f_seid);
+}
+
+struct up_session *up_session_find_by_up_seid(struct up_peer *peer, uint64_t up_seid)
+{
+ struct up_session *session;
+ hash_for_each_possible(peer->sessions_by_up_seid, session, node_by_up_seid, up_seid) {
+ if (up_seid == session->up_seid)
+ return session;
+ }
+ return NULL;
+}
+
+struct up_session *up_session_find_by_cp_f_seid(struct up_peer *peer, const struct osmo_pfcp_ie_f_seid *cp_f_seid)
+{
+ struct up_session *session;
+ hash_for_each_possible(peer->sessions_by_cp_seid, session, node_by_cp_seid, cp_f_seid->seid) {
+ if (osmo_pfcp_ie_f_seid_cmp(&session->cp_f_seid, cp_f_seid) == 0)
+ return session;
+ }
+ return NULL;
+}
+
+struct up_session *up_session_find_by_local_teid(struct up_peer *peer, uint32_t teid)
+{
+ struct up_session *session;
+ int bkt;
+ hash_for_each(peer->sessions_by_up_seid, bkt, session, node_by_up_seid) {
+ struct pdr *pdr;
+ llist_for_each_entry(pdr, &session->pdrs, entry) {
+ if (!pdr->local_f_teid)
+ continue;
+ if (pdr->local_f_teid->fixed.teid == teid)
+ return session;
+ }
+ }
+ return NULL;
+}
+
+static bool action_is_forw(const struct osmo_pfcp_ie_apply_action *aa)
+{
+ return osmo_pfcp_bits_get(aa->bits, OSMO_PFCP_APPLY_ACTION_FORW)
+ && !osmo_pfcp_bits_get(aa->bits, OSMO_PFCP_APPLY_ACTION_DROP);
+}
+
+static void pdr_classify(struct pdr *pdr)
+{
+ pdr->rx_decaps = false;
+ pdr->forw_encaps = false;
+ pdr->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 (!action_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 6463e32..936307f 100644
--- a/src/osmo-upf/upf.c
+++ b/src/osmo-upf/upf.c
@@ -23,13 +23,69 @@
#include <osmocom/core/utils.h>
#include <osmocom/core/talloc.h>
+#include <osmocom/core/sockaddr_str.h>
+
+#include <osmocom/pfcp/pfcp_endpoint.h>
#include <osmocom/upf/upf.h>
+#include <osmocom/upf/up_endpoint.h>
+#include <osmocom/upf/upf_gtp.h>
struct g_upf *g_upf = NULL;
+struct osmo_tdef_group g_upf_tdef_groups[] = {
+ { "pfcp", "PFCP endpoint timers", osmo_pfcp_tdefs, },
+ {}
+};
+
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 (upf_gtp_dev_open(d->dev_name, d->create, d->local_addr, false, false))
+ 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..e3435a9
--- /dev/null
+++ b/src/osmo-upf/upf_gtp.c
@@ -0,0 +1,457 @@
+/*
+ * (C) 2021-2022 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved.
+ *
+ * Author: Neels Janosch Hofmeyr <nhofmeyr@sysmocom.de>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdbool.h>
+#include <errno.h>
+#include <string.h>
+#include <net/if.h>
+#include <linux/gtp.h>
+
+#include <libgtpnl/gtpnl.h>
+#include <libgtpnl/gtp.h>
+
+#include <osmocom/core/socket.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/core/sockaddr_str.h>
+
+#include <osmocom/upf/upf.h>
+#include <osmocom/upf/upf_gtp.h>
+
+#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);
+
+ /* Need to add to list before setting up the destructor. A talloc_free() does automagically remove from the
+ * list. */
+ llist_add(&dev->entry, &g_upf->gtp.devs);
+
+ talloc_set_destructor(dev, upf_gtp_dev_destruct);
+
+ return dev;
+}
+
+static int 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 -EIO;
+ }
+ /* 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 rc;
+ }
+
+ LOG_GTP_DEV(dev, LOGL_NOTICE, "GTP device ready (ifidx=%u)\n", dev->ifidx);
+ return 0;
+}
+
+int upf_gtp_dev_open(const char *name, bool create_gtp_dev, 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 rc;
+ struct upf_gtp_dev *dev = upf_gtp_dev_alloc(name, local_addr);
+ if (!dev)
+ return -EIO;
+
+ dev->sgsn_mode = sgsn_mode;
+
+ if (listen_for_gtpv0) {
+ 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);
+ talloc_free(dev);
+ return -EIO;
+ }
+ dev->gtpv0.enabled = true;
+ 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);
+ talloc_free(dev);
+ return -EIO;
+ }
+ LOG_GTP_DEV(dev, LOGL_DEBUG, "GTPv1 bound\n");
+
+ if (create_gtp_dev) {
+ int gtp0_fd = listen_for_gtpv0 ? dev->gtpv0.ofd.fd : -1;
+ int 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 -EIO;
+ }
+
+ LOG_GTP_DEV(dev, LOGL_NOTICE, "created GTP device\n");
+ dev->created = true;
+ }
+
+ rc = dev_resolve_ifidx(dev);
+ if (rc) {
+ talloc_free(dev);
+ return -EIO;
+ }
+
+ return 0;
+}
+
+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)
+{
+ int r;
+
+ if (a == b)
+ return 0;
+ if (!a)
+ return -1;
+ if (!b)
+ return 1;
+
+#define CMP_MEMB(MEMB) OSMO_CMP(a->MEMB, b->MEMB)
+ if ((r = CMP_MEMB(local_teid)))
+ return r;
+ if ((r = CMP_MEMB(remote_teid)))
+ return r;
+ 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..73d86d1
--- /dev/null
+++ b/src/osmo-upf/upf_vty.c
@@ -0,0 +1,286 @@
+/* OsmoUpf interface to quagga VTY */
+/*
+ * (C) 2021-2022 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ * All Rights Reserved.
+ *
+ * Author: Neels Janosch Hofmeyr <nhofmeyr@sysmocom.de>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <osmocom/core/sockaddr_str.h>
+#include <osmocom/core/socket.h>
+
+#include <osmocom/pfcp/pfcp_endpoint.h>
+#include <osmocom/pfcp/pfcp_msg.h>
+
+#include <osmocom/vty/vty.h>
+#include <osmocom/vty/command.h>
+
+#include <osmocom/upf/upf.h>
+#include <osmocom/upf/upf_gtp.h>
+#include <osmocom/upf/up_endpoint.h>
+#include <osmocom/upf/up_peer.h>
+#include <osmocom/upf/up_session.h>
+#include <osmocom/upf/up_gtp_action.h>
+
+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..107909b 100644
--- a/tests/upf.vty
+++ b/tests/upf.vty
@@ -2,3 +2,34 @@
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
To view, visit change 28308. To unsubscribe, or for help writing mail filters, visit settings.