/* 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 * * All Rights Reserved * * Author: Neels Hofmeyr * * SPDX-License-Identifier: GPL-2.0+ * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include #include #include #include #include #include #include #include #include #include #include #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; }