diff --git a/include/osmocom/upf/Makefile.am b/include/osmocom/upf/Makefile.am index 127e03e..f418cb1 100644 --- a/include/osmocom/upf/Makefile.am +++ b/include/osmocom/upf/Makefile.am @@ -1,4 +1,5 @@ noinst_HEADERS = \ + netinst.h \ up_endpoint.h \ up_peer.h \ up_session.h \ diff --git a/include/osmocom/upf/netinst.h b/include/osmocom/upf/netinst.h new file mode 100644 index 0000000..fb659b6 --- /dev/null +++ b/include/osmocom/upf/netinst.h @@ -0,0 +1,44 @@ +/* + * (C) 2022 by sysmocom - s.f.m.c. GmbH + * All Rights Reserved. + * + * Author: Neels Janosch Hofmeyr + * + * SPDX-License-Identifier: GPL-2.0+ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include +#include + +struct vty; + +struct network_instance { + struct llist_head entry; + + char *name; + struct osmo_sockaddr_str addr; +}; + +const struct network_instance *netinst_add(void *ctx, struct llist_head *list, const char *name, const char *addr, + const char **errmsg); +const struct network_instance *netinst_find(struct llist_head *list, const char *name); +const struct network_instance *netinst_first(struct llist_head *list); +int netinst_clear(struct llist_head *list); + +int netinst_vty_write(struct vty *vty, struct llist_head *list, const char *indent, const char *name_or_null); diff --git a/include/osmocom/upf/upf.h b/include/osmocom/upf/upf.h index 1597e2c..735a724 100644 --- a/include/osmocom/upf/upf.h +++ b/include/osmocom/upf/upf.h @@ -101,6 +101,8 @@ struct g_upf { int priority; uint32_t next_id_state; } nft; + + struct llist_head netinst; }; extern struct g_upf *g_upf; diff --git a/src/osmo-upf/Makefile.am b/src/osmo-upf/Makefile.am index 1d43ee8..403fb06 100644 --- a/src/osmo-upf/Makefile.am +++ b/src/osmo-upf/Makefile.am @@ -30,6 +30,7 @@ bin_PROGRAMS = \ $(NULL) osmo_upf_SOURCES = \ + netinst.c \ osmo_upf_main.c \ up_endpoint.c \ up_gtp_action.c \ diff --git a/src/osmo-upf/netinst.c b/src/osmo-upf/netinst.c new file mode 100644 index 0000000..8cead8e --- /dev/null +++ b/src/osmo-upf/netinst.c @@ -0,0 +1,124 @@ +/* + * (C) 2022 by sysmocom - s.f.m.c. GmbH + * All Rights Reserved. + * + * Author: Neels Janosch Hofmeyr + * + * SPDX-License-Identifier: GPL-2.0+ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include + +#include +#include +#include + +#include + +/* Add a new netinst entry to the given list. + * \param ctx talloc allocate new entry from ctx. + * \param list append to this list. + * \param name The Network Instance name as given in PFCP Network Instance IEs. + * \param addr IP address string of local interface to associate with the Network Instance. + * \param errmsg On error, an error description is returned in this out-argument. + * \return new network_instance entry, or NULL on error. + */ +const struct network_instance *netinst_add(void *ctx, struct llist_head *list, const char *name, const char *addr, + const char **errmsg) +{ + struct network_instance *netinst; + if (errmsg) + *errmsg = NULL; + + if (!name || !*name) { + if (errmsg) + *errmsg = "Network Instance name must not be empty"; + return NULL; + } + + if (netinst_find(list, name)) { + if (errmsg) + *errmsg = "Network Instance entry with this name already exists"; + return NULL; + } + + netinst = talloc(ctx, struct network_instance); + *netinst = (struct network_instance){ + .name = talloc_strdup(netinst, name), + }; + if (osmo_sockaddr_str_from_str(&netinst->addr, addr, 0)) { + if (errmsg) + *errmsg = "Network Instance address is not a valid IP address string"; + talloc_free(netinst); + return NULL; + } + + llist_add_tail(&netinst->entry, list); + + return netinst; +} + +const struct network_instance *netinst_find(struct llist_head *list, const char *name) +{ + const struct network_instance *netinst; + + if (!name) + return NULL; + + llist_for_each_entry(netinst, list, entry) + if (!strcmp(netinst->name, name)) + return netinst; + + return NULL; +} + +const struct network_instance *netinst_first(struct llist_head *list) +{ + return llist_first_entry_or_null(list, struct network_instance, entry); +} + +/* Clear the list of Network Instance entries, return the nr of entries that were removed. */ +int netinst_clear(struct llist_head *list) +{ + int count = 0; + while (1) { + struct network_instance *netinst = llist_first_entry_or_null(list, struct network_instance, entry); + if (!netinst) + break; + llist_del(&netinst->entry); + talloc_free(netinst); + count++; + } + return count; +} + +/* Write one or all netinst entries to the VTY output. + * If name_or_null is NULL, print all entries. Else, print only the entry matching that name. + * Return number of printed entries. */ +int netinst_vty_write(struct vty *vty, struct llist_head *list, const char *indent, const char *name_or_null) +{ + const struct network_instance *netinst; + int count = 0; + + llist_for_each_entry(netinst, list, entry) { + if (name_or_null && strcmp(netinst->name, name_or_null)) + continue; + vty_out(vty, "%sadd %s %s%s", indent, netinst->name, netinst->addr.ip, VTY_NEWLINE); + count++; + } + return count; +} diff --git a/src/osmo-upf/up_session.c b/src/osmo-upf/up_session.c index 76dc857..36d20f2 100644 --- a/src/osmo-upf/up_session.c +++ b/src/osmo-upf/up_session.c @@ -34,6 +34,7 @@ #include #include #include +#include static enum osmo_pfcp_cause up_session_setup_gtp(struct up_session *session); @@ -114,12 +115,75 @@ struct chosen_f_teid *chosen_f_teid_find(struct llist_head *list, uint8_t choose return NULL; } +/* Find local interface's IP address by Network Instance name. Return 0 on success, or an OSMO_PFCP_CAUSE_* value on + * failure. */ +static int up_session_choose_local_ip(struct up_session *session, struct osmo_pfcp_ip_addrs *local_addr, + const char *netinst_name) +{ + const struct network_instance *netinst; + struct osmo_sockaddr osa = {}; + + if (llist_empty(&g_upf->netinst)) { + /* No network instances are configured in osmo-upf.cfg. Instead use the local address configured for + * PFCP, assuming that in a simplistic setup the host has only one interface. It is unlikely to be + * useful for a production environment where the entire point is to hand packet data from one interface + * to another, and where PFCP most probably happens on an entirely different interface, but may make + * things simpler for lab testing. */ + if (osmo_pfcp_ip_addrs_set(local_addr, + osmo_pfcp_endpoint_get_local_addr(session->up_peer->up_endpoint->pfcp_ep))) { + LOGPFSML(session->fi, LOGL_ERROR, "Invalid local address in pfcp_endpoint cfg\n"); + return OSMO_PFCP_CAUSE_SYSTEM_FAILURE; + } + LOGPFSML(session->fi, LOGL_NOTICE, + "Cannot look up Network Instance %s: No 'netinst' is configured, setting up GTP on same local" + " interface as PFCP: %s (makes sense only for lab testing)\n", + osmo_quote_str_c(OTC_SELECT, netinst_name, -1), + osmo_pfcp_ip_addrs_to_str_c(OTC_SELECT, local_addr)); + return 0; + } + + if (!netinst_name || !*netinst_name) { + /* Empty or no Network Instance IE in incoming PFCP request. Pick the first network instance; makes + * sense only in a simplistic lab setup where packet data is forwarded to the same interface that it is + * received on, and where no Network Instance is indicated by the CPF. Warn if more than one network + * instance is configured to choose from. */ + if (llist_count(&g_upf->netinst) > 1) + LOGPFSML(session->fi, LOGL_NOTICE, + "Missing Network Instance in incoming request, using the first 'netinst' from cfg\n"); + netinst = netinst_first(&g_upf->netinst); + /* there has to be a first entry, because we handled the empty list above. */ + OSMO_ASSERT(netinst); + } else { + netinst = netinst_find(&g_upf->netinst, netinst_name); + if (!netinst) { + LOGPFSML(session->fi, LOGL_ERROR, "Network Instance from PFCP request not found: %s" + " -- ensure there is a 'netinst' / 'add %s ' entry in your config\n", + osmo_quote_str_c(OTC_SELECT, netinst_name, -1), + osmo_escape_str_c(OTC_SELECT, netinst_name, -1)); + return OSMO_PFCP_CAUSE_RULE_CREATION_MOD_FAILURE; + } + } + + /* Convert netinst IP address string first to osmo_sockaddr and then to osmo_pfcp_ip_addrs. */ + if (osmo_sockaddr_str_to_sockaddr(&netinst->addr, &osa.u.sas) + || osmo_pfcp_ip_addrs_set(local_addr, &osa)) { + LOGPFSML(session->fi, LOGL_ERROR, + "Network Instance %s from PFCP request yields no valid IP address: " + OSMO_SOCKADDR_STR_FMT "\n", + osmo_quote_str_c(OTC_SELECT, netinst_name, -1), + OSMO_SOCKADDR_STR_FMT_ARGS(&netinst->addr)); + return OSMO_PFCP_CAUSE_RULE_CREATION_MOD_FAILURE; + } + return 0; +} + /* Choose an F-TEID (when the peer has sent CHOOSE = 1). * If the peer also sent a CHOOSE_ID, then remember this F-TEID choice under the given ID, and re-use that choice when * the same ID re-appears. The chosen IDs are saved in session->chosen_f_teids. * Return 0 on success, or an OSMO_PFCP_CAUSE_* value on failure. */ static enum osmo_pfcp_cause up_session_choose_f_teid(struct up_session *session, struct osmo_pfcp_ie_f_teid *dst, - bool choose_id_present, uint8_t choose_id) + bool choose_id_present, uint8_t choose_id, + const char *netinst_name) { struct up_endpoint *up_ep = session->up_peer->up_endpoint; struct chosen_f_teid *chosen = NULL; @@ -130,23 +194,26 @@ static enum osmo_pfcp_cause up_session_choose_f_teid(struct up_session *session, /* Re-use a previous F-TEID */ *dst = chosen->f_teid; } else { - /* Choose a new F-TEID */ + int rc; + *dst = (struct osmo_pfcp_ie_f_teid){ - .fixed = { - .teid = up_endpoint_next_teid(up_ep), - }, + .choose_flag = false, }; + + /* Determine local IP address from Network Instance value received in PFCP request */ + rc = up_session_choose_local_ip(session, &dst->fixed.ip_addr, netinst_name); + if (rc) + return rc; + + /* Choose a new TEID */ + dst->fixed.teid = up_endpoint_next_teid(up_ep); if (dst->fixed.teid == 0) { LOGPFSML(session->fi, LOGL_ERROR, "Failed to allocate an unused TEID\n"); return OSMO_PFCP_CAUSE_PFCP_ENTITY_IN_CONGESTION; } - LOGPFSML(session->fi, LOGL_INFO, "Allocated new local TEID 0x%x\n", dst->fixed.teid); + LOGPFSML(session->fi, LOGL_INFO, "Allocated new local F-TEID %s\n", + osmo_pfcp_ie_f_teid_to_str_c(OTC_SELECT, dst)); - if (osmo_pfcp_ip_addrs_set(&dst->fixed.ip_addr, - osmo_pfcp_endpoint_get_local_addr(up_ep->pfcp_ep))) { - 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); @@ -270,6 +337,10 @@ int pdr_to_str_buf(char *buf, size_t buflen, const struct pdr *pdr) OSMO_STRBUF_APPEND(sb, osmo_sockaddr_to_str_buf2, &pdr->desc.pdi.ue_ip_address.ip_addr.v6); } } + if (pdr->desc.pdi.network_inst_present) { + OSMO_STRBUF_PRINTF(sb, " netinst:"); + OSMO_STRBUF_APPEND(sb, osmo_quote_str_buf3, pdr->desc.pdi.network_inst.str, -1); + } if (pdr->local_f_teid) { OSMO_STRBUF_PRINTF(sb, " "); OSMO_STRBUF_APPEND(sb, osmo_pfcp_ie_f_teid_to_str_buf, pdr->local_f_teid); @@ -370,9 +441,13 @@ static struct pdr *pdr_create(struct up_session *session, if (pdr->desc.pdi.local_f_teid.choose_flag) { /* CHOOSE = 1: we need to pick our own local F-TEID */ struct osmo_pfcp_ie_f_teid local_f_teid; + const char *netinst_name = NULL; + if (pdr->desc.pdi.network_inst_present) + netinst_name = pdr->desc.pdi.network_inst.str; *cause = up_session_choose_f_teid(session, &local_f_teid, pdr->desc.pdi.local_f_teid.choose.choose_id_present, - pdr->desc.pdi.local_f_teid.choose.choose_id); + pdr->desc.pdi.local_f_teid.choose.choose_id, + netinst_name); if (*cause != OSMO_PFCP_CAUSE_REQUEST_ACCEPTED) { *offending_ie = OSMO_PFCP_IEI_F_TEID; *offending_ie_present = true; diff --git a/src/osmo-upf/upf.c b/src/osmo-upf/upf.c index 3b8bfeb..0a84799 100644 --- a/src/osmo-upf/upf.c +++ b/src/osmo-upf/upf.c @@ -61,6 +61,7 @@ void g_upf_alloc(void *ctx) INIT_LLIST_HEAD(&g_upf->gtp.vty_cfg.devs); INIT_LLIST_HEAD(&g_upf->gtp.devs); + INIT_LLIST_HEAD(&g_upf->netinst); } int upf_pfcp_listen() diff --git a/src/osmo-upf/upf_vty.c b/src/osmo-upf/upf_vty.c index 45f6dec..aff7590 100644 --- a/src/osmo-upf/upf_vty.c +++ b/src/osmo-upf/upf_vty.c @@ -37,11 +37,13 @@ #include #include #include +#include enum upf_vty_node { PFCP_NODE = _LAST_OSMOVTY_NODE + 1, TUNEND_NODE, TUNMAP_NODE, + NETINST_NODE, }; static struct cmd_node cfg_pfcp_node = { @@ -252,6 +254,67 @@ DEFUN(cfg_tunmap_table_name, cfg_tunmap_table_name_cmd, return CMD_SUCCESS; } +static struct cmd_node cfg_netinst_node = { + NETINST_NODE, + "%s(config-netinst)# ", + 1, +}; + +DEFUN(cfg_netinst, cfg_netinst_cmd, + "netinst", + "Enter the Network Instance configuration node\n") +{ + vty->node = NETINST_NODE; + return CMD_SUCCESS; +} + +static int config_write_netinst(struct vty *vty) +{ + vty_out(vty, "netinst%s", VTY_NEWLINE); + netinst_vty_write(vty, &g_upf->netinst, " ", NULL); + return CMD_SUCCESS; +} + +DEFUN(cfg_netinst_add, cfg_netinst_add_cmd, + "add NAME ADDR", + "add Network Instance: associate a PFCP Network Instance name with a local IP address\n" + "Network Instance name as received in PFCP Network Instance IE\n" + "IP address of a local interface\n") +{ + const char *errmsg; + if (!netinst_add(g_upf, &g_upf->netinst, argv[0], argv[1], &errmsg)) { + vty_out(vty, "%% Error: netinst: cannot add %s %s: %s%s", argv[0], argv[1], + errmsg ? : "(unknown error)", VTY_NEWLINE); + return CMD_WARNING; + } + return CMD_SUCCESS; +} + +DEFUN(show_netinst, show_netinst_cmd, + "show netinst [NAME]", + SHOW_STR "List configured Network Instance entries\n" + "Show the Network Instance with this name (show all when omitted)\n") +{ + const char *name_or_null = argc > 0 ? argv[0] : NULL; + + if (!netinst_vty_write(vty, &g_upf->netinst, " ", name_or_null)) { + if (name_or_null) + vty_out(vty, "%% No such Network Instance entry%s", VTY_NEWLINE); + else + vty_out(vty, "%% No Network Instance entries configured%s", VTY_NEWLINE); + } + return CMD_SUCCESS; +} + +DEFUN(cfg_netinst_clear, cfg_netinst_clear_cmd, + "clear", + "Remove all Network Instance entries\n") +{ + int count = netinst_clear(&g_upf->netinst); + vty_out(vty, "netinst entries removed: %d%s", count, VTY_NEWLINE); + return CMD_SUCCESS; +} + DEFUN(show_pdr, show_pdr_cmd, "show pdr", SHOW_STR @@ -348,6 +411,7 @@ void upf_vty_init() install_element_ve(&show_pdr_cmd); install_element_ve(&show_gtp_cmd); install_element_ve(&show_session_cmd); + install_element_ve(&show_netinst_cmd); install_node(&cfg_pfcp_node, config_write_pfcp); install_element(CONFIG_NODE, &cfg_pfcp_cmd); @@ -371,4 +435,11 @@ void upf_vty_init() install_element(TUNMAP_NODE, &cfg_tunmap_mockup_cmd); install_element(TUNMAP_NODE, &cfg_tunmap_no_mockup_cmd); install_element(TUNMAP_NODE, &cfg_tunmap_table_name_cmd); + + install_node(&cfg_netinst_node, config_write_netinst); + install_element(CONFIG_NODE, &cfg_netinst_cmd); + + install_element(NETINST_NODE, &cfg_netinst_clear_cmd); + install_element(NETINST_NODE, &cfg_netinst_add_cmd); + install_element(NETINST_NODE, &show_netinst_cmd); } diff --git a/tests/netinst.vty b/tests/netinst.vty new file mode 100644 index 0000000..7b3c726 --- /dev/null +++ b/tests/netinst.vty @@ -0,0 +1,80 @@ +OsmoUPF> show ? +... + netinst List configured Network Instance entries +... +OsmoUPF> show netinst? + netinst List configured Network Instance entries +OsmoUPF> show netinst ? + [NAME] Show the Network Instance with this name (show all when omitted) + +OsmoUPF> show netinst +% No Network Instance entries configured +OsmoUPF> show netinst foo +% No such Network Instance entry + +OsmoUPF> enable + +OsmoUPF# show netinst +% No Network Instance entries configured + +OsmoUPF# configure terminal +OsmoUPF(config)# netinst + +OsmoUPF(config-netinst)# list +... + clear + add NAME ADDR + show netinst [NAME] + +OsmoUPF(config-netinst)# clear? + clear Remove all Network Instance entries +OsmoUPF(config-netinst)# clear ? + + +OsmoUPF(config-netinst)# add? + add add Network Instance: associate a PFCP Network Instance name with a local IP address +OsmoUPF(config-netinst)# add ? + NAME Network Instance name as received in PFCP Network Instance IE +OsmoUPF(config-netinst)# add foo ? + ADDR IP address of a local interface + +OsmoUPF(config-netinst)# add foo bar +% Error: netinst: cannot add foo bar: Network Instance address is not a valid IP address string +OsmoUPF(config-netinst)# add foo 1.2.3.4 +OsmoUPF(config-netinst)# add foo 2.3.4.5 +% Error: netinst: cannot add foo 2.3.4.5: Network Instance entry with this name already exists +OsmoUPF(config-netinst)# add bar 2.3.4.5 +OsmoUPF(config-netinst)# show netinst + add foo 1.2.3.4 + add bar 2.3.4.5 +OsmoUPF(config-netinst)# add baz 1:2:3:4::0 +OsmoUPF(config-netinst)# show netinst + add foo 1.2.3.4 + add bar 2.3.4.5 + add baz 1:2:3:4::0 +OsmoUPF(config-netinst)# show netinst foo + add foo 1.2.3.4 +OsmoUPF(config-netinst)# show netinst bar + add bar 2.3.4.5 +OsmoUPF(config-netinst)# show netinst baz + add baz 1:2:3:4::0 + +OsmoUPF(config-netinst)# show running-config +... +netinst + add foo 1.2.3.4 + add bar 2.3.4.5 + add baz 1:2:3:4::0 +... + +OsmoUPF(config-netinst)# clear +netinst entries removed: 3 +OsmoUPF(config-netinst)# show netinst +% No Network Instance entries configured +OsmoUPF(config-netinst)# clear +netinst entries removed: 0 + +OsmoUPF(config-netinst)# show netinst? + netinst List configured Network Instance entries +OsmoUPF(config-netinst)# show netinst ? + [NAME] Show the Network Instance with this name (show all when omitted)