implement GTP tunnel mapping via netfilter

Implement support for PFCP rulesets that ask for mapping a GTP tunnel:
forwarding GTP payload between two GTP tunnels.

For a GTP tunnel mapping, dispatch netfilter rules that detect GTP
packets with a given source address and TEID, and replace the TEID and
destination address according to the PFCP ruleset.

The netfilter implementation is chosen to effect the packet rewriting
and forwarding to take place directly in the kernel, for high throughput
of GTP packets.

Related: SYS#5599
Change-Id: Ic0d319eb4f98cd51a5999c804c4203ab0bdda650
This commit is contained in:
Neels Hofmeyr 2022-02-25 01:25:21 +01:00
parent bee02fc34f
commit 06482c6554
10 changed files with 455 additions and 7 deletions

View File

@ -15,3 +15,5 @@ pfcp
local-addr 127.0.0.1
gtp
mockup
nft
mockup

View File

@ -33,6 +33,7 @@ struct osmo_tdef;
struct ctrl_handle;
struct upf_gtp_dev;
struct nft_ctx;
#define UPF_PFCP_LISTEN_DEFAULT "0.0.0.0"
@ -72,6 +73,8 @@ struct g_upf {
struct pfcp_vty_cfg vty_cfg;
struct up_endpoint *ep;
} pfcp;
/* Tunnel encaps decaps via GTP kernel module */
struct {
/* if true, don't actually send commands to the GTP kernel module, just return success. */
bool mockup;
@ -85,6 +88,17 @@ struct g_upf {
struct mnl_socket *nl;
int32_t genl_id;
} gtp;
/* Tunnel forwarding via linux netfilter */
struct {
/* if true, don't actually send commands to nftables, just return success. */
bool mockup;
struct nft_ctx *nft_ctx;
char *table_name;
int priority;
uint32_t next_id_state;
} nft;
};
extern struct g_upf *g_upf;
@ -94,6 +108,7 @@ enum upf_log_subsys {
DPEER,
DSESSION,
DGTP,
DNFT,
};
void g_upf_alloc(void *ctx);

View File

@ -27,6 +27,8 @@
#include <osmocom/core/socket.h>
#define NFT_CHAIN_NAME_PREFIX_TUNMAP "tunmap"
struct upf_nft_tunmap_desc {
struct {
struct osmo_sockaddr gtp_remote_addr;

View File

@ -37,6 +37,7 @@ osmo_upf_SOURCES = \
up_session.c \
upf.c \
upf_gtp.c \
upf_nft.c \
upf_vty.c \
$(NULL)

View File

@ -44,6 +44,7 @@
#include <osmocom/upf/upf.h>
#include <osmocom/upf/up_endpoint.h>
#include <osmocom/upf/upf_gtp.h>
#include <osmocom/upf/upf_nft.h>
#define _GNU_SOURCE
#include <getopt.h>
@ -237,6 +238,12 @@ static const struct log_info_cat upf_default_categories[] = {
.enabled = 0, .loglevel = LOGL_NOTICE,
.color = OSMO_LOGCOLOR_PURPLE,
},
[DNFT] = {
.name = "DNFT",
.description = "GTP forwarding rules via linux netfilter",
.enabled = 0, .loglevel = LOGL_NOTICE,
.color = OSMO_LOGCOLOR_PURPLE,
},
};
const struct log_info log_info = {
@ -330,6 +337,9 @@ int main(int argc, char **argv)
if (upf_gtp_devs_open())
return -1;
if (upf_nft_init())
return -1;
if (upf_pfcp_listen())
return -1;
@ -356,6 +366,8 @@ int main(int argc, char **argv)
upf_gtp_genl_close();
upf_nft_free();
/* 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);

View File

@ -109,9 +109,42 @@ static int up_gtp_action_enable_disable(struct up_gtp_action *a, bool enable)
}
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;
if (g_upf->nft.mockup) {
LOG_UP_GTP_ACTION(a, LOGL_NOTICE, "nft/mockup active, skipping nftables ruleset %s\n",
enable ? "enable" : "disable");
return 0;
}
if (enable && a->tunmap.id != 0) {
LOG_UP_GTP_ACTION(a, LOGL_ERROR,
"Cannot enable: nft GTP tunnel mapping rule has been enabled before"
" as " NFT_CHAIN_NAME_PREFIX_TUNMAP "%u\n", a->tunmap.id);
return -EALREADY;
}
if (!enable && a->tunmap.id == 0) {
LOG_UP_GTP_ACTION(a, LOGL_ERROR,
"Cannot disable: nft GTP tunnel mapping rule has not been enabled"
" (no " NFT_CHAIN_NAME_PREFIX_TUNMAP " id)\n");
return -ENOENT;
}
if (enable)
rc = upf_nft_tunmap_create(&a->tunmap);
else
rc = upf_nft_tunmap_delete(&a->tunmap);
if (rc) {
LOG_UP_GTP_ACTION(a, LOGL_ERROR,
"Failed to %s nft GTP tunnel mapping " NFT_CHAIN_NAME_PREFIX_TUNMAP "%u:"
" %d %s\n", enable ? "enable" : "disable", a->tunmap.id, rc, strerror(-rc));
return rc;
}
LOG_UP_GTP_ACTION(a, LOGL_NOTICE, "%s nft GTP tunnel mapping " NFT_CHAIN_NAME_PREFIX_TUNMAP "%u\n",
enable ? "Enabled" : "Disabled", a->tunmap.id);
if (!enable)
a->tunmap.id = 0;
return 0;
default:
LOG_UP_GTP_ACTION(a, LOGL_ERROR, "Invalid action\n");
return -ENOTSUP;

View File

@ -1141,7 +1141,107 @@ static void add_gtp_action_endecaps(void *ctx, struct llist_head *dst, struct pd
static void add_gtp_action_forw(void *ctx, struct llist_head *dst, struct pdr *pdr)
{
/* TODO implement GTP forwarding with TEID translation */
struct up_session *session = pdr->session;
struct up_gtp_action *a;
struct pdr *rpdr;
struct far *far;
struct osmo_pfcp_ie_forw_params *far_forw;
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);
far = pdr->far;
far_forw = &far->desc.forw_params;
rpdr = pdr->reverse_pdr;
rfar = rpdr->far;
rfar_forw = &rfar->desc.forw_params;
/* To decaps, we need to have a local TEID assigned for which to receive GTP packets. */
/* decaps from CORE */
if (!pdr->local_f_teid || pdr->local_f_teid->choose_flag) {
log_inactive_pdr_set(pdr, "missing local TEID (CORE side)", pdr);
return;
}
/* decaps from ACCESS */
if (!rpdr->local_f_teid || rpdr->local_f_teid->choose_flag) {
log_inactive_pdr_set(pdr, "missing local TEID (ACCESS side)", 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. */
/* encaps towards ACCESS */
if (!far->desc.forw_params_present) {
log_inactive_pdr_set(pdr, "missing FAR Forwarding Parameters", pdr);
return;
}
if (!far_forw->outer_header_creation_present) {
log_inactive_pdr_set(pdr, "missing FAR Outer Header Creation", pdr);
return;
}
if (!far_forw->outer_header_creation.teid_present) {
log_inactive_pdr_set(pdr, "missing TEID in FAR Outer Header Creation", pdr);
return;
}
if (!far_forw->outer_header_creation.ip_addr.v4_present) {
log_inactive_pdr_set(pdr, "missing IPv4 in FAR Outer Header Creation", pdr);
return;
}
/* encaps towards CORE */
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;
}
pdr->active = true;
far->active = true;
rpdr->active = true;
rfar->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_TUNMAP,
.tunmap = {
.core = {
.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,
},
.access = {
.local_teid = rpdr->local_f_teid->fixed.teid,
.remote_teid = far_forw->outer_header_creation.teid,
.gtp_remote_addr = far_forw->outer_header_creation.ip_addr.v4,
},
},
};
llist_add_tail(&a->entry, dst);
}
/* Analyse all PDRs and FARs and find configurations that match either a GTP encaps/decaps or a GTP forward rule. Add to
@ -1294,11 +1394,12 @@ static void drop_gtp_actions(struct up_session *session)
{
struct llist_head empty;
INIT_LLIST_HEAD(&empty);
/* Call setup_gtp_actions() with an empty list, to clean up all active GTP actions */
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. */
/* Check whether the Packet Detection and Forwarding Action Rules amount to an encaps/decaps of GTP or a GTP tunnel
* mapping, or none of the two. */
static enum osmo_pfcp_cause up_session_setup_gtp(struct up_session *session)
{
enum osmo_pfcp_cause cause;

View File

@ -50,6 +50,9 @@ void g_upf_alloc(void *ctx)
.local_port = OSMO_PFCP_PORT,
},
},
.nft = {
.priority = -300,
},
};
INIT_LLIST_HEAD(&g_upf->gtp.vty_cfg.devs);

218
src/osmo-upf/upf_nft.c Normal file
View File

@ -0,0 +1,218 @@
/*
* (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 <nftables/libnftables.h>
#include <osmocom/core/talloc.h>
#include <osmocom/core/logging.h>
#include <osmocom/upf/upf.h>
#include <osmocom/upf/upf_nft.h>
static char *upf_nft_ruleset_table_create(void *ctx, const char *table_name)
{
return talloc_asprintf(ctx, "add table inet %s\n", table_name);
}
static int upf_nft_run(const char *ruleset)
{
int rc;
if (g_upf->nft.mockup) {
LOGP(DNFT, LOGL_NOTICE, "--mockup-gtp active, not running nft ruleset: '%s'\n", ruleset);
return 0;
}
if (!g_upf->nft.nft_ctx) {
rc = upf_nft_init();
if (rc)
return rc;
}
rc = nft_run_cmd_from_buffer(g_upf->nft.nft_ctx, ruleset);
if (rc < 0) {
LOGP(DNFT, LOGL_ERROR, "error running nft ruleset: rc=%d ruleset=%s\n",
rc, osmo_quote_str_c(OTC_SELECT, ruleset, -1));
return -EIO;
}
return 0;
}
int upf_nft_init()
{
int rc;
if (g_upf->nft.mockup) {
LOGP(DNFT, LOGL_NOTICE, "--mockup-gtp active, not allocating libnftables nft_ctx\n");
return 0;
}
g_upf->nft.nft_ctx = nft_ctx_new(NFT_CTX_DEFAULT);
if (!g_upf->nft.nft_ctx) {
LOGP(DNFT, LOGL_ERROR, "cannot allocate libnftables nft_ctx\n");
return -EIO;
}
if (!g_upf->nft.table_name)
g_upf->nft.table_name = talloc_strdup(g_upf, "osmo-upf");
rc = upf_nft_run(upf_nft_ruleset_table_create(OTC_SELECT, g_upf->nft.table_name));
if (rc) {
LOGP(DNFT, LOGL_ERROR, "Failed to create nft table %s\n",
osmo_quote_str_c(OTC_SELECT, g_upf->nft.table_name, -1));
return rc;
}
LOGP(DNFT, LOGL_NOTICE, "Created nft table %s\n", osmo_quote_str_c(OTC_SELECT, g_upf->nft.table_name, -1));
return 0;
}
int upf_nft_free()
{
if (!g_upf->nft.nft_ctx)
return 0;
nft_ctx_free(g_upf->nft.nft_ctx);
g_upf->nft.nft_ctx = NULL;
return 0;
}
struct upf_nft_args_peer {
/* The source IP address in packets received from this peer */
const struct osmo_sockaddr *addr;
/* The TEID that we send to the peer in GTP packets. */
uint32_t teid_remote;
/* The TEID that the peer sends to us in GTP packets. */
uint32_t teid_local;
};
struct upf_nft_args {
/* global table name */
const char *table_name;
/* chain name for this specific tunnel mapping */
uint32_t chain_id;
int priority;
struct upf_nft_args_peer peer_a;
struct upf_nft_args_peer peer_b;
};
static int tunmap_single_direction(char *buf, size_t buflen,
const struct upf_nft_args *args,
const struct upf_nft_args_peer *from_peer,
const struct upf_nft_args_peer *to_peer)
{
struct osmo_strbuf sb = { .buf = buf, .len = buflen };
OSMO_STRBUF_PRINTF(sb, "add rule inet %s " NFT_CHAIN_NAME_PREFIX_TUNMAP "%u", args->table_name, args->chain_id);
/* Match only UDP packets */
OSMO_STRBUF_PRINTF(sb, " meta l4proto udp");
/* Match on packets coming in from from_peer */
OSMO_STRBUF_PRINTF(sb, " ip saddr ");
OSMO_STRBUF_APPEND(sb, osmo_sockaddr_to_str_buf2, from_peer->addr);
/* Match on the TEID in the header */
OSMO_STRBUF_PRINTF(sb, " @ih,32,32 0x%08x", from_peer->teid_local);
/* Change destination address to to_peer */
OSMO_STRBUF_PRINTF(sb, " ip daddr set ");
OSMO_STRBUF_APPEND(sb, osmo_sockaddr_to_str_buf2, to_peer->addr);
/* Change the TEID in the header to the one to_peer expects */
OSMO_STRBUF_PRINTF(sb, " @ih,32,32 set 0x%08x", to_peer->teid_remote);
OSMO_STRBUF_PRINTF(sb, " counter\n");
return sb.chars_needed;
}
static int upf_nft_ruleset_tunmap_create_buf(char *buf, size_t buflen, const struct upf_nft_args *args)
{
struct osmo_strbuf sb = { .buf = buf, .len = buflen };
/* Add a chain for this tunnel mapping */
OSMO_STRBUF_PRINTF(sb, "add chain inet %s " NFT_CHAIN_NAME_PREFIX_TUNMAP "%u { type filter hook prerouting priority %d; }\n",
args->table_name, args->chain_id, args->priority);
/* Forwarding from peer_a to peer_b */
OSMO_STRBUF_APPEND(sb, tunmap_single_direction, args, &args->peer_a, &args->peer_b);
/* And from peer_b to peer_a */
OSMO_STRBUF_APPEND(sb, tunmap_single_direction, args, &args->peer_b, &args->peer_a);
return sb.chars_needed;
}
static char *upf_nft_ruleset_tunmap_create_c(void *ctx, const struct upf_nft_args *args)
{
OSMO_NAME_C_IMPL(ctx, 512, "ERROR", upf_nft_ruleset_tunmap_create_buf, args)
}
static int upf_nft_ruleset_tunmap_delete_buf(char *buf, size_t buflen, const struct upf_nft_args *args)
{
struct osmo_strbuf sb = { .buf = buf, .len = buflen };
OSMO_STRBUF_PRINTF(sb, "delete chain inet %s " NFT_CHAIN_NAME_PREFIX_TUNMAP "%u\n",
args->table_name, args->chain_id);
return sb.chars_needed;
}
static char *upf_nft_ruleset_tunmap_delete_c(void *ctx, const struct upf_nft_args *args)
{
OSMO_NAME_C_IMPL(ctx, 64, "ERROR", upf_nft_ruleset_tunmap_delete_buf, args)
}
static void upf_nft_args_from_tunmap_desc(struct upf_nft_args *args, const struct upf_nft_tunmap_desc *tunmap)
{
*args = (struct upf_nft_args){
.table_name = g_upf->nft.table_name,
.chain_id = tunmap->id,
.priority = g_upf->nft.priority,
.peer_a = {
.addr = &tunmap->access.gtp_remote_addr,
.teid_remote = tunmap->access.remote_teid,
.teid_local = tunmap->access.local_teid,
},
.peer_b = {
.addr = &tunmap->core.gtp_remote_addr,
.teid_remote = tunmap->core.remote_teid,
.teid_local = tunmap->core.local_teid,
},
};
}
int upf_nft_tunmap_create(struct upf_nft_tunmap_desc *tunmap)
{
struct upf_nft_args args;
/* Give this tunnel mapping a new id, returned to the caller so that the tunnel mapping can be deleted later */
g_upf->nft.next_id_state++;
tunmap->id = g_upf->nft.next_id_state;
upf_nft_args_from_tunmap_desc(&args, tunmap);
return upf_nft_run(upf_nft_ruleset_tunmap_create_c(OTC_SELECT, &args));
}
int upf_nft_tunmap_delete(struct upf_nft_tunmap_desc *tunmap)
{
struct upf_nft_args args;
upf_nft_args_from_tunmap_desc(&args, tunmap);
return upf_nft_run(upf_nft_ruleset_tunmap_delete_c(OTC_SELECT, &args));
}

View File

@ -41,6 +41,7 @@
enum upf_vty_node {
PFCP_NODE = _LAST_OSMOVTY_NODE + 1,
GTP_NODE,
NFT_NODE,
};
static struct cmd_node cfg_pfcp_node = {
@ -85,7 +86,7 @@ static struct cmd_node cfg_gtp_node = {
DEFUN(cfg_gtp, cfg_gtp_cmd,
"gtp",
"Enter the GTP configuration node\n")
"Enter the 'gtp' node to configure GTP kernel module usage\n")
{
vty->node = GTP_NODE;
return CMD_SUCCESS;
@ -189,6 +190,60 @@ DEFUN(cfg_gtp_dev_del, cfg_gtp_dev_del_cmd,
return CMD_SUCCESS;
}
static struct cmd_node cfg_nft_node = {
NFT_NODE,
"%s(config-nft)# ",
1,
};
DEFUN(cfg_nft, cfg_nft_cmd,
"nft",
"Enter the 'nft' node to configure nftables usage\n")
{
vty->node = NFT_NODE;
return CMD_SUCCESS;
}
static int config_write_nft(struct vty *vty)
{
vty_out(vty, "nft%s", VTY_NEWLINE);
if (g_upf->nft.mockup)
vty_out(vty, " mockup%s", VTY_NEWLINE);
if (g_upf->nft.table_name && strcmp(g_upf->nft.table_name, "osmo-upf"))
vty_out(vty, " table-name %s%s", g_upf->nft.table_name, VTY_NEWLINE);
return CMD_SUCCESS;
}
DEFUN(cfg_nft_mockup, cfg_nft_mockup_cmd,
"mockup",
"don't actually send rulesets to nftables, just return success\n")
{
g_upf->nft.mockup = true;
return CMD_SUCCESS;
}
DEFUN(cfg_nft_no_mockup, cfg_nft_no_mockup_cmd,
"no mockup",
NO_STR
"operate nftables rulesets normally\n")
{
g_upf->nft.mockup = false;
return CMD_SUCCESS;
}
DEFUN(cfg_nft_table_name, cfg_nft_table_name_cmd,
"table-name TABLE_NAME",
"Set the nft inet table name to create and place GTP tunnel forwarding chains in"
" (as in 'nft add table inet foo'). If multiple instances of osmo-upf are running on the same system, each"
" osmo-upf must have its own table name. Otherwise the names of created forwarding chains will collide.\n"
"nft inet table name\n")
{
osmo_talloc_replace_string(g_upf, &g_upf->nft.table_name, argv[0]);
return CMD_SUCCESS;
}
DEFUN(show_pdr, show_pdr_cmd,
"show pdr",
SHOW_STR
@ -304,5 +359,11 @@ void upf_vty_init()
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);
}
install_node(&cfg_nft_node, config_write_nft);
install_element(CONFIG_NODE, &cfg_nft_cmd);
install_element(NFT_NODE, &cfg_nft_mockup_cmd);
install_element(NFT_NODE, &cfg_nft_no_mockup_cmd);
install_element(NFT_NODE, &cfg_nft_table_name_cmd);
}