osmo-upf/tests/unique_ids/unique_ids_test.c

576 lines
15 KiB
C

/* OsmoUPF: Verify that skipping used ids works for: UP-SEID, GTP local TEID, nft ruleset chain_id. */
/* (C) 2023 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
*
* All Rights Reserved
*
* Author: Neels 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 <getopt.h>
#include <nftables/libnftables.h>
#include <osmocom/core/sockaddr_str.h>
#include <osmocom/core/application.h>
#include <osmocom/pfcp/pfcp_endpoint.h>
#include <osmocom/upf/upf.h>
#include <osmocom/upf/netinst.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>
#define log(FMT, ARGS...) fprintf(stderr, FMT, ##ARGS)
#define log_assert(COND) do { \
log("assert(" #COND ")\n"); \
OSMO_ASSERT(COND); \
} while (0)
#define log_assert_expect_failure(COND) do { \
log("assert(" #COND ") <-- EXPECTED TO FAIL (known error)\n"); \
OSMO_ASSERT(!(COND)); \
} while (0)
void *main_ctx;
void *ctx;
/* The override of osmo_pfcp_endpoint_tx() stores any Session Establishment Response's UP-SEID here, so that this test
* can reference specific sessions later.
*/
uint64_t last_up_seid = 0;
void select_poll(void)
{
while (osmo_select_main_ctx(1));
}
static void setup(const char *name)
{
log("\n===== START of %s\n", name);
ctx = talloc_named_const(main_ctx, 0, name);
g_upf_alloc(ctx);
osmo_talloc_replace_string(g_upf, &g_upf->pfcp.vty_cfg.local_addr, "1.1.1.1");
OSMO_ASSERT(netinst_add(g_upf, &g_upf->netinst, "default", "1.1.1.1", NULL));
/* PFCP endpoint recovery timestamp overridden by time() below */
upf_pfcp_init();
/* but do not upf_pfcp_listen() */
upf_nft_init();
select_poll();
log("\n");
}
static void cleanup(void)
{
up_endpoint_free(&g_upf->pfcp.ep);
upf_gtp_devs_close();
upf_gtp_genl_close();
upf_nft_free();
log("\n===== END of %s\n", talloc_get_name(ctx));
talloc_free(ctx);
}
static struct osmo_sockaddr *str2addr(const char *addr, uint16_t port)
{
static struct osmo_sockaddr osa;
struct osmo_sockaddr_str str;
osmo_sockaddr_str_from_str(&str, addr, port);
osmo_sockaddr_str_to_sockaddr(&str, &osa.u.sas);
return &osa;
}
static struct up_peer *have_peer(const char *remote_addr, uint16_t port)
{
return up_peer_find_or_add(g_upf->pfcp.ep, str2addr(remote_addr, port));
}
static struct osmo_pfcp_msg *new_pfcp_msg_for_osmo_upf_rx(struct up_peer *from_peer, enum osmo_pfcp_message_type msg_type)
{
/* pfcp_endpoint discards received messages immediately after dispatching; in this test, allocate them in
* OTC_SELECT so they get discarded on the next select_poll().
* osmo_pfcp_msg_alloc_rx() is not useful here, it creates a blank struct to be decoded from raw data; instead,
* use osmo_pfcp_msg_alloc_tx_req() which properly sets up the internal structures to match the given msg_type,
* and when that is done set m->rx = true to indicate it is a message received by osmo-upf. */
struct osmo_pfcp_msg *m = osmo_pfcp_msg_alloc_tx_req(OTC_SELECT, &from_peer->remote_addr, msg_type);
m->rx = true;
return m;
}
static void peer_assoc(struct up_peer *peer)
{
struct osmo_pfcp_msg *m = new_pfcp_msg_for_osmo_upf_rx(peer, OSMO_PFCP_MSGT_ASSOC_SETUP_REQ);
m->ies.assoc_setup_req.recovery_time_stamp = 1234;
osmo_fsm_inst_dispatch(peer->fi, UP_PEER_EV_RX_ASSOC_SETUP_REQ, m);
select_poll();
}
static int next_teid = 0x100;
static int next_cp_seid = 0x100;
/* Send a PFCP Session Establishment Request, and return the created session */
static struct up_session *session_est_tunmap(struct up_peer *peer)
{
struct osmo_pfcp_msg *m;
struct osmo_pfcp_ie_f_seid cp_f_seid;
struct osmo_pfcp_ie_f_teid f_teid_access_local;
struct osmo_pfcp_ie_outer_header_creation ohc_access;
struct osmo_pfcp_ie_f_teid f_teid_core_local;
struct osmo_pfcp_ie_outer_header_creation ohc_core;
struct osmo_pfcp_ie_apply_action aa = {};
osmo_pfcp_bits_set(aa.bits, OSMO_PFCP_APPLY_ACTION_FORW, true);
f_teid_access_local = (struct osmo_pfcp_ie_f_teid){
.choose_flag = true,
.choose = {
.ipv4_addr = true,
},
};
ohc_access = (struct osmo_pfcp_ie_outer_header_creation){
.teid_present = true,
.teid = next_teid++,
.ip_addr = {
.v4_present = true,
.v4 = *str2addr("5.6.7.8", 0),
},
};
osmo_pfcp_bits_set(ohc_access.desc_bits, OSMO_PFCP_OUTER_HEADER_CREATION_GTP_U_UDP_IPV4, true);
f_teid_core_local = (struct osmo_pfcp_ie_f_teid){
.choose_flag = true,
.choose = {
.ipv4_addr = true,
},
};
ohc_core = (struct osmo_pfcp_ie_outer_header_creation){
.teid_present = true,
.teid = next_teid++,
.ip_addr = {
.v4_present = true,
.v4 = *str2addr("13.14.15.16", 0),
},
};
osmo_pfcp_bits_set(ohc_core.desc_bits, OSMO_PFCP_OUTER_HEADER_CREATION_GTP_U_UDP_IPV4, true);
cp_f_seid = (struct osmo_pfcp_ie_f_seid){
.seid = next_cp_seid++,
};
osmo_pfcp_ip_addrs_set(&cp_f_seid.ip_addr, osmo_pfcp_endpoint_get_local_addr(g_upf->pfcp.ep->pfcp_ep));
m = new_pfcp_msg_for_osmo_upf_rx(peer, OSMO_PFCP_MSGT_SESSION_EST_REQ);
m->h.seid_present = true;
m->h.seid = 0;
/* GTP tunmap: remove header from both directions, and add header in both directions */
m->ies.session_est_req = (struct osmo_pfcp_msg_session_est_req){
.node_id = m->ies.session_est_req.node_id,
.cp_f_seid_present = true,
.cp_f_seid = cp_f_seid,
.create_pdr_count = 2,
.create_pdr = {
{
.pdr_id = 1,
.precedence = 255,
.pdi = {
.source_iface = OSMO_PFCP_SOURCE_IFACE_CORE,
.local_f_teid_present = true,
.local_f_teid = f_teid_core_local,
},
.outer_header_removal_present = true,
.outer_header_removal = {
.desc = OSMO_PFCP_OUTER_HEADER_REMOVAL_GTP_U_UDP_IPV4,
},
.far_id_present = true,
.far_id = 1,
},
{
.pdr_id = 2,
.precedence = 255,
.pdi = {
.source_iface = OSMO_PFCP_SOURCE_IFACE_ACCESS,
.local_f_teid_present = true,
.local_f_teid = f_teid_access_local,
},
.outer_header_removal_present = true,
.outer_header_removal = {
.desc = OSMO_PFCP_OUTER_HEADER_REMOVAL_GTP_U_UDP_IPV4,
},
.far_id_present = true,
.far_id = 2,
},
},
.create_far_count = 2,
.create_far = {
{
.far_id = 1,
.forw_params_present = true,
.forw_params = {
.destination_iface = OSMO_PFCP_DEST_IFACE_ACCESS,
.outer_header_creation_present = true,
.outer_header_creation = ohc_access,
},
.apply_action = aa,
},
{
.far_id = 2,
.forw_params_present = true,
.forw_params = {
.destination_iface = OSMO_PFCP_DEST_IFACE_CORE,
.outer_header_creation_present = true,
.outer_header_creation = ohc_core,
},
.apply_action = aa,
},
},
};
osmo_fsm_inst_dispatch(peer->fi, UP_PEER_EV_RX_SESSION_EST_REQ, m);
select_poll();
return up_session_find_by_up_seid(peer, last_up_seid);
}
static void session_del(struct up_session *session)
{
struct osmo_pfcp_msg *m;
log_assert(session);
m = new_pfcp_msg_for_osmo_upf_rx(session->up_peer, OSMO_PFCP_MSGT_SESSION_DEL_REQ);
m->h.seid_present = true;
m->h.seid = session->up_seid;
osmo_fsm_inst_dispatch(session->fi, UP_SESSION_EV_RX_SESSION_DEL_REQ, m);
select_poll();
}
static void dump_state(void)
{
struct up_peer *peer;
log("\n state:\n");
llist_for_each_entry(peer, &g_upf->pfcp.ep->peers, entry) {
struct up_session *session;
int bkt;
log(" | peer %s %s\n", peer->fi->name, osmo_fsm_inst_state_name(peer->fi));
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) {
if (a->kind != UP_GTP_U_TUNMAP)
continue;
log(" | session[%s]: UP-SEID 0x%"PRIx64"; chain_id access=%u core=%u;"
" local TEID access=0x%x core=0x%x\n",
osmo_fsm_inst_state_name(session->fi),
session->up_seid,
a->tunmap.access.chain_id, a->tunmap.core.chain_id,
a->tunmap.access.tun.local.teid, a->tunmap.core.tun.local.teid);
}
}
}
log("\n");
}
static void test_skip_used_id(void)
{
struct up_peer *peer;
struct up_session *s1;
uint64_t s1_up_seid;
struct up_session *s2;
struct up_session *s3;
struct up_session *s4;
struct up_gtp_action *a;
setup(__func__);
log("PFCP Associate peer\n");
peer = have_peer("1.2.3.4", 1234);
peer_assoc(peer);
dump_state();
/* Make sure to start out all IDs with 1 */
g_upf->pfcp.ep->next_up_seid_state = 0;
g_upf->gtp.next_local_teid_state = 0;
g_upf->tunmap.next_chain_id_state = 0;
log("set up tunmap, which assigns first UP-SEID 0x1, local-TEID 0x1 and 0x2, chain_ids 1 and 2\n");
s1 = session_est_tunmap(peer);
dump_state();
log_assert(s1->up_seid == 1);
a = llist_first_entry_or_null(&s1->active_gtp_actions, struct up_gtp_action, entry);
log_assert(a);
log_assert(a->kind == UP_GTP_U_TUNMAP);
log_assert(a->tunmap.core.tun.local.teid == 1);
log_assert(a->tunmap.access.tun.local.teid == 2);
log_assert(a->tunmap.access.chain_id == 1);
log_assert(a->tunmap.core.chain_id == 2);
log("\n");
log("simulate wrapping of IDs back to 1\n");
g_upf->pfcp.ep->next_up_seid_state = 0;
g_upf->gtp.next_local_teid_state = 0;
g_upf->tunmap.next_chain_id_state = 0;
log("set up second tunmap, should use distinct IDs\n");
s2 = session_est_tunmap(peer);
dump_state();
log_assert(s2->up_seid == 2);
a = llist_first_entry_or_null(&s2->active_gtp_actions, struct up_gtp_action, entry);
log_assert(a);
log_assert(a->kind == UP_GTP_U_TUNMAP);
log_assert(a->tunmap.core.tun.local.teid == 3);
log_assert(a->tunmap.access.tun.local.teid == 4);
log_assert(a->tunmap.access.chain_id == 3);
log_assert(a->tunmap.core.chain_id == 4);
log("\n");
log("drop first tunmap (%s)\n", s1->fi->name);
s1_up_seid = s1->up_seid;
session_del(s1);
dump_state();
log_assert(up_session_find_by_up_seid(peer, s1_up_seid) == NULL);
log("\n");
log("again wrap all ID state back to 1\n");
g_upf->pfcp.ep->next_up_seid_state = 0;
g_upf->gtp.next_local_teid_state = 0;
g_upf->tunmap.next_chain_id_state = 0;
log("set up third tunmap, should now re-use same IDs as the first session\n");
s3 = session_est_tunmap(peer);
dump_state();
log_assert(s3->up_seid == 1);
a = llist_first_entry_or_null(&s3->active_gtp_actions, struct up_gtp_action, entry);
log_assert(a);
log_assert(a->kind == UP_GTP_U_TUNMAP);
log_assert(a->tunmap.core.tun.local.teid == 1);
log_assert(a->tunmap.access.tun.local.teid == 2);
log_assert(a->tunmap.access.chain_id == 1);
log_assert(a->tunmap.core.chain_id == 2);
log("\n");
log("set up 4th tunmap; chain_id state would use 3 and 4, but they are in use, so should assign 5 and 6\n");
s4 = session_est_tunmap(peer);
dump_state();
log_assert(s4->up_seid == 3);
a = llist_first_entry_or_null(&s4->active_gtp_actions, struct up_gtp_action, entry);
log_assert(a);
log_assert(a->kind == UP_GTP_U_TUNMAP);
log_assert(a->tunmap.core.tun.local.teid == 5);
log_assert(a->tunmap.access.tun.local.teid == 6);
log_assert(a->tunmap.access.chain_id == 5);
log_assert(a->tunmap.core.chain_id == 6);
log("\n");
cleanup();
}
static const struct log_info_cat test_default_categories[] = {
[DREF] = {
.name = "DREF",
.description = "Reference Counting",
.enabled = 1, .loglevel = LOGL_DEBUG,
.color = OSMO_LOGCOLOR_DARKGREY,
},
[DPEER] = {
.name = "DPEER",
.description = "PFCP peer association",
.enabled = 1, .loglevel = LOGL_DEBUG,
.color = OSMO_LOGCOLOR_YELLOW,
},
[DSESSION] = {
.name = "DSESSION",
.description = "PFCP sessions",
.enabled = 1, .loglevel = LOGL_DEBUG,
.color = OSMO_LOGCOLOR_BLUE,
},
[DGTP] = {
.name = "DGTP",
.description = "GTP tunneling",
.enabled = 1, .loglevel = LOGL_DEBUG,
.color = OSMO_LOGCOLOR_PURPLE,
},
[DNFT] = {
.name = "DNFT",
.description = "GTP forwarding rules via linux netfilter",
.enabled = 1, .loglevel = LOGL_DEBUG,
.color = OSMO_LOGCOLOR_PURPLE,
},
};
const struct log_info log_info = {
.cat = test_default_categories,
.num_cat = ARRAY_SIZE(test_default_categories),
};
static struct {
bool verbose;
} cmdline_opts = {
.verbose = false,
};
static void print_help(const char *program)
{
printf("Usage:\n"
" %s [-v]\n"
"Options:\n"
" -h --help show this text.\n"
" -v --verbose print source file and line numbers\n",
program
);
}
static void handle_options(int argc, char **argv)
{
while (1) {
int option_index = 0, c;
static struct option long_options[] = {
{"help", 0, 0, 'h'},
{"verbose", 1, 0, 'v'},
{0, 0, 0, 0}
};
c = getopt_long(argc, argv, "hv",
long_options, &option_index);
if (c == -1)
break;
switch (c) {
case 'h':
print_help(argv[0]);
exit(0);
case 'v':
cmdline_opts.verbose = true;
break;
default:
/* catch unknown options *as well as* missing arguments. */
fprintf(stderr, "Error in command line options. Exiting.\n");
exit(-1);
break;
}
}
}
int main(int argc, char **argv)
{
handle_options(argc, argv);
main_ctx = talloc_named_const(NULL, 0, "main");
msgb_talloc_ctx_init(main_ctx, 0);
osmo_fsm_set_dealloc_ctx(OTC_SELECT);
osmo_init_logging2(main_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_timestamp(osmo_stderr_target, 0);
log_set_print_extended_timestamp(osmo_stderr_target, 0);
log_set_all_filter(osmo_stderr_target, 1);
if (cmdline_opts.verbose) {
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_use_color(osmo_stderr_target, 1);
} else {
log_set_print_filename2(osmo_stderr_target, LOG_FILENAME_NONE);
log_set_use_color(osmo_stderr_target, 0);
}
osmo_fsm_log_timeouts(true);
osmo_fsm_log_addr(false);
/* actual tests */
test_skip_used_id();
log_fini();
talloc_free(main_ctx);
return 0;
}
/* overrides */
int osmo_pfcp_endpoint_tx(struct osmo_pfcp_endpoint *ep, struct osmo_pfcp_msg *m)
{
enum osmo_pfcp_cause *cause;
log("\n[test override] PFCP tx:\n%s\n\n", osmo_pfcp_msg_to_str_c(OTC_SELECT, m));
last_up_seid = 0;
cause = osmo_pfcp_msg_cause(m);
switch (m->h.message_type) {
case OSMO_PFCP_MSGT_SESSION_EST_RESP:
if (*cause == OSMO_PFCP_CAUSE_REQUEST_ACCEPTED) {
last_up_seid = m->ies.session_est_resp.up_f_seid.seid;
log("osmo-upf created session 0x%"PRIx64"\n\n", last_up_seid);
}
break;
default:
break;
};
osmo_pfcp_msg_free(m);
return 0;
}
static void *fake_nft_ctx = (void *)0x1;
struct nft_ctx *nft_ctx_new(uint32_t flags)
{
log("[test override] %s()\n", __func__);
return fake_nft_ctx;
}
void nft_ctx_free(struct nft_ctx *ctx)
{
log("[test override] %s()\n", __func__);
log_assert(ctx == fake_nft_ctx);
}
int nft_run_cmd_from_buffer(struct nft_ctx *nft, const char *buf)
{
log("\n[test override] %s():\n%s\n", __func__, buf);
return 0;
}
/* for deterministic recovery_time_stamp */
time_t time(time_t *tloc)
{
log("[test override] %s()\n", __func__);
return 0;
}