12 Commits

Author SHA1 Message Date
Neels Janosch Hofmeyr
6c1fd09bdb wip
Change-Id: I5ae90bba9b5ad792d6d11be15a806846748a2d3f
2024-08-18 02:23:49 +02:00
Neels Janosch Hofmeyr
0eed0079e0 gtp_flood trials
Change-Id: I8611946d24e7fcb2f7cc0a9092fbe052d9b2ca6b
2024-08-17 06:27:23 +02:00
Neels Janosch Hofmeyr
cb1a3ea0da gtp_flood.vty
Change-Id: If7e552ba4d438d43143538fbd1fc2eb9e5b01229
2024-08-17 06:27:23 +02:00
Neels Janosch Hofmeyr
9ee5959151 udp_responder
Change-Id: I39418fab40b073cd0eedf370ad6f9e1fed8efffe
2024-08-17 06:27:23 +02:00
Neels Janosch Hofmeyr
0ad6aaeda3 osmo-pfcp-tool: add GTP flooding using io_uring
Freely copying from gtp-load-gen.c, implement GTP flooding in
osmo-pfcp-tool scripts.

Add dependency liburing. It can be disabled with
  configure --disable-uring
in which case the new 'gtp flood' command only logs an error.

This verbosely commented example script serves as a good explanation:
contrib/osmo-pfcp-tool-scripts/gtp_flood.vty

Related: SYS#6590
Change-Id: I332aa0e2efd55f6e357cde4752a3d8b584db531b
2024-08-13 08:02:22 +02:00
Neels Janosch Hofmeyr
c7119ea26c move pfcp_tool.h to include/osmocom/pfcptool/
Related: SYS#6590
Change-Id: If3e7cc4df3defd08df9e75965715a1be0388ed01
2024-08-13 07:52:19 +02:00
Neels Janosch Hofmeyr
f31d604eb9 pfcp_tool: add 'date'
Allow scripts to output timestamps to easily measure how long certain
actions took to complete. Related: measuring PFCP session management
performance bounds of osmo-upf.

Change-Id: I0486cc92ea298bb9926a0e5c26da17ba5970a72c
2024-08-13 07:52:19 +02:00
Neels Janosch Hofmeyr
09de12b6dc pfcp-tool: n-sessions [4/4]: implement 'n <0-2147483647> session create'
Related: SYS#6590
Change-Id: I74a21cc31296ab89a2acda1da8ae9693c1992e66
2024-08-13 07:52:16 +02:00
Neels Janosch Hofmeyr
61d4aea56a pfcp-tool: n-sessions [3/4]: add poll arg to pfcp_tool_mainloop()
Allow calling in non-blocking mode, prepare for N sessions.

Related: SYS#6590
Change-Id: I20bb2803b28681face18ee665d8a1aad06d58091
2024-08-13 07:52:12 +02:00
Neels Janosch Hofmeyr
54169ccddf pfcp-tool: n-sessions [2/4]: generalize function args
Change some VTY DEFUN into generally callable functions. Prepare for N
sessions commands.

Related: SYS#6590
Change-Id: I112206049e704b7adad7072b1f7953f7ee4f18ca
2024-08-13 07:52:10 +02:00
Neels Janosch Hofmeyr
a8629aa2f1 pfcp-tool: n-sessions [1/4]: add generators for TEID and UE IP
Add the ability to establish a large number of sessions automatically.
Useful for load testing.

Related: SYS#6590
Change-Id: Iec164a222782d382aefe8d0342f398ebba1eac05
2024-08-13 07:51:57 +02:00
Neels Janosch Hofmeyr
a91f4ec88e pfcp-tool: always use specific PDR ids for access and core
Makes it easier to find the right one later.
This will be used to fetch the UPF chosen TEIDs from PFCP responses in
upcoming "n-sessions [4/4]" I74a21cc31296ab89a2acda1da8ae9693c1992e66.

Related: SYS#6590
Change-Id: Ic343494001c70a84f3402ce5749d08e729551b26
2024-08-13 07:51:51 +02:00
19 changed files with 2300 additions and 65 deletions

View File

@@ -7,3 +7,4 @@
# If any interfaces have been added since the last public release: c:r:a + 1.
# If any interfaces have been removed or changed since the last public release: c:r:0.
#library what description / commit summary line
osmo-pfcp-tool liburing new dependency to support GTP flooding

View File

@@ -43,6 +43,20 @@ PKG_CHECK_MODULES(LIBOSMOPFCP, libosmo-pfcp >= 0.1.0)
PKG_CHECK_MODULES(LIBGTPNL, libgtpnl >= 1.2.0)
PKG_CHECK_MODULES(LIBNFTABLES, libnftables >= 1.0.2)
AC_ARG_ENABLE([uring], [AS_HELP_STRING([--disable-uring], [Build without io_uring support])],
[
ENABLE_URING=$enableval
],
[
ENABLE_URING="yes"
])
AS_IF([test "x$ENABLE_URING" = "xyes"], [
PKG_CHECK_MODULES(LIBURING, [liburing >= 0.7])
AC_DEFINE([HAVE_URING],[1],[Build with io_uring support for GTP flood commands])
])
AM_CONDITIONAL(ENABLE_URING, test "x$ENABLE_URING" = "xyes")
AC_SUBST(ENABLE_URING)
dnl checks for header files
AC_HEADER_STDC
@@ -198,6 +212,7 @@ AC_OUTPUT(
include/Makefile
include/osmocom/Makefile
include/osmocom/upf/Makefile
include/osmocom/pfcptool/Makefile
src/Makefile
src/osmo-upf/Makefile
src/osmo-pfcp-tool/Makefile

View File

@@ -1 +1,29 @@
SUBDIRS = systemd
AM_CPPFLAGS = \
$(all_includes) \
-I$(top_srcdir)/include \
-I$(top_builddir)/include \
-I$(top_builddir) \
$(NULL)
AM_CFLAGS = \
-Wall \
$(LIBOSMOCORE_CFLAGS) \
$(LIBURING_CFLAGS) \
$(COVERAGE_CFLAGS) \
$(NULL)
AM_LDFLAGS = \
$(LIBOSMOCORE_LIBS) \
$(LIBURING_LIBS) \
$(COVERAGE_LDFLAGS) \
$(NULL)
bin_PROGRAMS = \
osmo-udp-responder \
$(NULL)
osmo_udp_responder_SOURCES = \
udp_responder.c \
$(NULL)

View File

@@ -0,0 +1,53 @@
# Establish N PFCP sessions for tunend, and emit massive GTP traffic to the UPF
# to each established tunnel.
#
# osmo-pfcp-tool UPF "internet host"
# |GTP-ep -------GTP-----> GTP-ep|UE-IP-addr -------IP------> arbitrary-IP|
# |10.0.1.1 10.0.2.1|192.168.10.23 123.234.42.23|
# |10.0.1.2
# ^ ^ ^
# ^ | | |
# | | configure by configure by
# configure by from UPF 'ue ip' 'target ip',
# 'gtp ip' ("F-TEID=choose") 'target port'
# Configure one or more local GTP endpoints to emit GTP packets from.
# Established sessions will use these round-robin.
# These need to be local IP addresses for 'gtp flood' to work.
gtp local 127.0.1.1
gtp local 127.0.1.2
# use UE IP addresses from this range, +1 for each new UE:
# 192.168.0.1, 192.168.0.2, ...
ue ip range 192.168.23.2 192.168.23.254
# now associate with UPF and start N sessions.
pfcp-peer 127.0.0.11
tx assoc-setup-req
sleep 1
date
n 10 session create tunend
wait responses
# All sessions established
date
# For each established PFCP session, emit GTP packets
gtp flood
workers 1
io-uring queue-size 4
flows-per-session 1
packets-per-flow 1000
slew 0
# configure the generated GTP payload: send UDP packets from the UE address
# and these source UDP ports to these target addresses and target UDP ports.
# They are used round-robin.
# Source IP is the UE IP address.
payload source port udp range 10000 10010
payload target ip range 192.168.10.154 192.168.10.154
payload target port udp range 23000 23000
# All GTP is flowing.
# osmo-pfcp-tool will keep this up for as long as there still are active GTP flows,
# or until receiving a signal interrupt (ctrl-C).

523
contrib/udp_responder.c Normal file
View File

@@ -0,0 +1,523 @@
/* UDP responder: listen on a UDP port, and respond to each received UDP packet back to the sender. */
/*
* (C) 2024 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 "config.h"
#include <osmocom/core/application.h>
#include <osmocom/core/logging.h>
#include <osmocom/core/timer.h>
#if HAVE_URING
#define _GNU_SOURCE
#include <getopt.h>
#include <limits.h>
#include <liburing.h>
#include <osmocom/core/utils.h>
#include <osmocom/core/socket.h>
#include <osmocom/core/sockaddr_str.h>
struct cmdline_cmd {
const char *short_option;
const char *long_option;
const char *arg_name;
const char *doc;
const char *value;
};
#define cmdline_foreach(ITER, CMDS) \
for (const struct cmdline_cmd *ITER = (CMDS); \
ITER->short_option || ITER->long_option || ITER->arg_name; \
ITER++)
int cmdline_doc_str_buf(char *buf, size_t buflen, const struct cmdline_cmd *cmds)
{
struct osmo_strbuf sb = { .buf = buf, .len = buflen };
/* First find the longest options part */
int w = 0;
cmdline_foreach (cmd, cmds) {
int cmd_w = 0;
if (cmd->short_option)
cmd_w += 2 + strlen(cmd->short_option);
if (cmd->long_option)
cmd_w += 3 + strlen(cmd->long_option);
if (cmd->arg_name)
cmd_w += 1 + strlen(cmd->arg_name);
w = OSMO_MAX(w, cmd_w);
}
/* vertical gap */
w += 2;
OSMO_STRBUF_PRINTF(sb, "Options:\n");
cmdline_foreach (cmd, cmds) {
char *line_start = sb.pos;
if (cmd->short_option)
OSMO_STRBUF_PRINTF(sb, " -%s", cmd->short_option);
if (cmd->long_option)
OSMO_STRBUF_PRINTF(sb, " --%s", cmd->long_option);
if (cmd->arg_name)
OSMO_STRBUF_PRINTF(sb, " %s", cmd->arg_name);
if (cmd->doc) {
int have = sb.pos - line_start;
int spaces = OSMO_MAX(1, w - have);
OSMO_STRBUF_PRINTF(sb, "%*s", spaces, "");
OSMO_STRBUF_PRINTF(sb, "%s", cmd->doc);
}
OSMO_STRBUF_PRINTF(sb, "\n");
}
return sb.chars_needed;
}
void cmdline_print_help(const struct cmdline_cmd *cmds)
{
char buf[8192];
cmdline_doc_str_buf(buf, sizeof(buf), cmds);
printf("%s", buf);
}
void cmdline_cmd_store_optarg(struct cmdline_cmd *cmd)
{
if (cmd->arg_name)
cmd->value = optarg;
else
cmd->value = (cmd->short_option ? : cmd->long_option);
}
int cmdline_read(struct cmdline_cmd *cmds, int argc, char **argv)
{
char short_options[256] = {};
struct option long_options[128] = {};
int long_options_i = 0;
int long_option_val = 0;
struct osmo_strbuf short_sb = { .buf = short_options, .len = sizeof(short_options) };
cmdline_foreach (cmd, cmds) {
if (cmd->short_option) {
OSMO_STRBUF_PRINTF(short_sb, "%s", cmd->short_option);
if (cmd->arg_name)
OSMO_STRBUF_PRINTF(short_sb, ":");
}
if (cmd->long_option) {
long_options[long_options_i] = (struct option){
cmd->long_option,
cmd->arg_name ? 1 : 0,
&long_option_val,
long_options_i,
};
}
}
while (1) {
int option_index = 0;
char c = getopt_long(argc, argv, short_options, long_options, &option_index);
if (c == -1)
break;
if (c == 0) {
struct cmdline_cmd *long_cmd = &cmds[long_option_val];
cmdline_cmd_store_optarg(long_cmd);
} else {
bool found = false;
cmdline_foreach (cc, cmds) {
if (strchr(cc->short_option, c)) {
cmdline_cmd_store_optarg((struct cmdline_cmd *)cc);
found = true;
break;
}
}
if (!found) {
fprintf(stderr, "%s: Error in command line options. Exiting.\n", argv[0]);
return -1;
}
}
}
/* positional args */
cmdline_foreach (cmd, cmds) {
if (optind >= argc)
break;
if (cmd->short_option || cmd->long_option)
continue;
if (!cmd->arg_name)
continue;
((struct cmdline_cmd *)cmd)->value = argv[optind];
optind++;
}
if (optind < argc) {
cmdline_print_help(cmds);
fprintf(stderr, "%s: Unsupported positional argument on command line\n", argv[optind]);
return -1;
}
return 0;
}
const char *cmdline_get(const struct cmdline_cmd *cmds, const char *option_name, const char *default_val)
{
cmdline_foreach (cmd, cmds) {
if (cmd->long_option && !strcmp(cmd->long_option, option_name))
return cmd->value;
if (cmd->short_option && !strcmp(cmd->short_option, option_name))
return cmd->value;
if (cmd->arg_name && !strcmp(cmd->arg_name, option_name))
return cmd->value;
}
return default_val;
}
bool cmdline_get_int(int *dst, int minval, int maxval, int default_val,
const struct cmdline_cmd *cmds, const char *option_name)
{
const char *str = cmdline_get(cmds, option_name, NULL);
if (!str) {
*dst = default_val;
return true;
}
if (osmo_str_to_int(dst, str, 10, minval, maxval)) {
cmdline_print_help(cmds);
printf("ERROR: invalid integer number: %s\n", str);
return false;
}
if (*dst < minval || *dst > maxval) {
cmdline_print_help(cmds);
printf("ERROR: number out of range: %d <= %d <= %d\n", minval, *dst, maxval);
return false;
}
return true;
}
struct udp_port {
struct llist_head entry;
/* IP address and UDP port from user input */
struct osmo_sockaddr osa;
/* locally bound socket */
int fd;
};
enum data_io_type {
IO_UNUSED = 0,
IO_RECV,
IO_SEND,
};
struct data_io {
enum data_io_type type;
struct osmo_sockaddr osa;
struct iovec iov;
struct msghdr msgh;
uint8_t *data;
size_t data_size;
int n;
};
struct io_queue {
size_t d_size;
struct data_io d[0];
};
static void data_io_prep_recv(struct io_uring *ring, struct udp_port *port, struct data_io *d)
{
struct io_uring_sqe *sqe;
*d = (struct data_io){
.type = IO_RECV,
.iov = {
.iov_base = d->data,
.iov_len = d->data_size,
},
.msgh = {
.msg_name = &d->osa,
.msg_namelen = sizeof(d->osa),
.msg_iov = &d->iov,
.msg_iovlen = 1,
},
.data_size = d->data_size,
.data = d->data,
};
sqe = io_uring_get_sqe(ring);
OSMO_ASSERT(sqe);
io_uring_prep_recvmsg(sqe, port->fd, &d->msgh, 0);
io_uring_sqe_set_data(sqe, d);
}
static void data_io_prep_send(struct io_uring *ring, struct udp_port *port, struct data_io *d)
{
struct io_uring_sqe *sqe;
d->type = IO_SEND;
sqe = io_uring_get_sqe(ring);
OSMO_ASSERT(sqe);
io_uring_prep_sendmsg(sqe, port->fd, &d->msgh, 0);
io_uring_sqe_set_data(sqe, d);
}
uint32_t g_total_rx = 0;
uint32_t g_total_tx = 0;
static void data_io_handle_completion(struct io_uring *ring, struct udp_port *port, struct io_uring_cqe *cqe,
int response_size, int response_n)
{
struct data_io *d;
struct osmo_sockaddr *osa = NULL;
int rc;
d = io_uring_cqe_get_data(cqe);
osa = &d->osa;
rc = cqe->res;
if (rc < 0) {
LOGP(DLGLOBAL, LOGL_ERROR, "%s -> rx error rc=%d flags=0x%x\n",
osa ? osmo_sockaddr_to_str(osa) : "NULL",
rc, cqe->flags);
return;
}
switch (d->type) {
case IO_RECV:
/* done reading */
d->iov.iov_len = rc;
LOGP(DLGLOBAL, LOGL_DEBUG, "%s -> rx rc=%d flags=0x%x: %s\n",
osa ? osmo_sockaddr_to_str(osa) : "NULL",
rc, cqe->flags,
osmo_quote_str(d->iov.iov_base, d->iov.iov_len));
io_uring_cqe_seen(ring, cqe);
g_total_rx++;
if (response_n < 1)
break;
/* resubmit back to sender */
if (response_size > 0)
d->iov.iov_len = response_size;
data_io_prep_send(ring, port, d);
break;
case IO_SEND:
/* done writing. */
LOGP(DLGLOBAL, LOGL_DEBUG, "%s <- tx rc=%d flags=0x%x: %s\n",
osa ? osmo_sockaddr_to_str(osa) : "NULL",
rc, cqe->flags,
osmo_quote_str(d->iov.iov_base, rc));
io_uring_cqe_seen(ring, cqe);
g_total_tx++;
d->n++;
/* Send again? If not, re-submit open slot for reading. */
if (d->n < response_n)
data_io_prep_send(ring, port, d);
else
data_io_prep_recv(ring, port, d);
break;
default:
OSMO_ASSERT(0);
}
}
struct cmdline_cmd cmds[] = {
{
.short_option = "h",
.long_option = "help",
.doc = "Show this help",
},
{
.short_option = "l",
.long_option = "local-addr",
.arg_name = "IP-ADDR",
.doc = "Listen on local IP address (default is 0.0.0.0).",
},
{
.short_option = "p",
.long_option = "port",
.arg_name = "UDP-PORT",
.doc = "Listen on local UDP port.",
},
/*
{
.short_option = "P",
.long_option = "port-range-to",
.arg_name = "UDP-PORT-TO",
.doc = "Listen on a range of ports, from --port to --port-range-to, inclusive.",
},
*/
{
.short_option = "s",
.long_option = "response-size",
.arg_name = "BYTES",
.doc = "When responding, enlarge or shorten the payload to this size.",
},
{
.short_option = "n",
.long_option = "response-repeat",
.arg_name = "N",
.doc = "Respond N times, i.e. multiply the returned traffic.",
},
{
.long_option = "io-uring-queue",
.arg_name = "SIZE",
.doc = "I/O tuning: queue size to use for io_uring, default is 4000.",
},
{
.long_option = "io-uring-buf",
.arg_name = "SIZE",
.doc = "I/O tuning: maximum payload size, default is 2048.",
},
{}
};
static const struct log_info_cat categories[] = {
};
const struct log_info udp_responder_log_info = {
.cat = categories,
.num_cat = ARRAY_SIZE(categories),
};
int main(int argc, char **argv)
{
struct udp_port port = {};
struct osmo_sockaddr_str addr = {};
struct osmo_sockaddr osa = {};
osmo_init_logging2(OTC_GLOBAL, &udp_responder_log_info);
log_set_log_level(osmo_stderr_target, LOGL_ERROR);
if (cmdline_read(cmds, argc, argv)
|| cmdline_get(cmds, "help", NULL)) {
cmdline_print_help(cmds);
return -1;
}
int port_nr;
if (!cmdline_get_int(&port_nr, 1, 65525, 23000, cmds, "port"))
return -1;
const char *local_addr = cmdline_get(cmds, "local-addr", "0.0.0.0");
if (osmo_sockaddr_str_from_str(&addr, local_addr, port_nr)
|| osmo_sockaddr_str_to_osa(&addr, &osa)) {
printf("ERROR: invalid interface or port number: %s:%d\n", local_addr, port_nr);
return -1;
}
int queue_size;
if (!cmdline_get_int(&queue_size, 1, 65536, 4000, cmds, "io-uring-queue"))
return -1;
int buf_size;
if (!cmdline_get_int(&buf_size, 1, 65536, 2048, cmds, "io-uring-buf"))
return -1;
int response_size = 0;
if (!cmdline_get_int(&response_size, 0, buf_size, 0, cmds, "response-size"))
return -1;
int response_n = 1;
if (!cmdline_get_int(&response_n, 0, INT_MAX, 1, cmds, "response-repeat"))
return -1;
port.osa = osa;
/* create and bind socket */
int rc;
rc = osmo_sock_init_osa(SOCK_DGRAM, IPPROTO_UDP, &port.osa, NULL, OSMO_SOCK_F_BIND);
/* (logging of errors already happens in osmo_sock_init_osa() */
if (rc < 0)
return -1;
port.fd = rc;
LOGP(DLGLOBAL, LOGL_NOTICE, "bound UDP %s fd=%d\n", osmo_sock_get_name2(port.fd), port.fd);
struct io_uring ring = {};
rc = io_uring_queue_init(queue_size, &ring, 0);
OSMO_ASSERT(rc >= 0);
struct io_queue *q = talloc_size(OTC_GLOBAL, sizeof(struct io_queue) + queue_size * sizeof(struct data_io));
OSMO_ASSERT(q);
*q = (struct io_queue){
.d_size = queue_size,
};
for (int i = 0; i < q->d_size; i++) {
struct data_io *d = &q->d[i];
*d = (struct data_io){
.data = talloc_size(q, buf_size),
.data_size = buf_size,
};
/* fill once with random printable data */
for (int j = 0; j < d->data_size; j++)
d->data[j] = 32 + random() % (126 - 32 + 1);
}
for (int i = 0; i < q->d_size; i++) {
data_io_prep_recv(&ring, &port, &q->d[i]);
}
struct __kernel_timespec ts = {};
struct timespec next = {};
while (1) {
uint32_t submitted;
uint32_t completed = 0;
struct io_uring_cqe *cqe;
bool show = false;
/* submit any requests from previous loop */
submitted = io_uring_submit(&ring);
/* process all pending completions */
while (io_uring_wait_cqe_timeout(&ring, &cqe, &ts) == 0) {
data_io_handle_completion(&ring, &port, cqe, response_size, response_n);
completed++;
}
{
struct timespec now;
osmo_clock_gettime(CLOCK_MONOTONIC, &now);
if (timespeccmp(&now, &next, >=)) {
next = now;
next.tv_sec++;
show = true;
}
}
if (show || (!submitted && !completed)) {
printf("%6u rx %6u tx \r", g_total_rx, g_total_tx);
fflush(stdout);
}
if (!submitted && !completed) {
io_uring_wait_cqe(&ring, &cqe);
data_io_handle_completion(&ring, &port, cqe, response_size, response_n);
}
}
talloc_free(q);
return 0;
}
#endif /* HAVE_URING */

4
debian/control vendored
View File

@@ -2,6 +2,7 @@ Source: osmo-upf
Section: net
Priority: extra
Maintainer: Osmocom team <openbsc@lists.osmocom.org>
# liburing-dev: don't try to install it on debian 10 and ubuntu 20.04
Build-Depends: debhelper (>= 10),
dh-autoreconf,
autotools-dev,
@@ -16,7 +17,8 @@ Build-Depends: debhelper (>= 10),
libnftables-dev (>= 1.0.2),
libosmocore-dev (>= 1.6.0),
libosmo-pfcp-dev (>= 0.1.0),
osmo-gsm-manuals-dev (>= 1.2.0)
osmo-gsm-manuals-dev (>= 1.2.0),
liburing-dev | base-files (<< 11) | ubuntu-keyring (<< 2021),
Standards-Version: 3.9.8
Vcs-Git: https://gitea.osmocom.org/cellular-infrastructure/osmo-upf
Vcs-Browser: https://gitea.osmocom.org/cellular-infrastructure/osmo-upf

View File

@@ -1,3 +1,4 @@
SUBDIRS = \
upf \
pfcptool \
$(NULL)

View File

@@ -0,0 +1,6 @@
noinst_HEADERS = \
checksum.h \
gtp_flood.h \
pfcp_tool.h \
range.h \
$(NULL)

View File

@@ -0,0 +1,13 @@
#pragma once
#include <stdint.h>
#include <netinet/in.h>
uint16_t ip_fast_csum(const void *iph, unsigned int ihl);
uint32_t csum_partial(const void *buff, int len, uint32_t wsum);
uint16_t ip_compute_csum(const void *buff, int len);
uint16_t csum_ipv6_magic(const struct in6_addr *saddr,
const struct in6_addr *daddr,
uint32_t len, uint8_t proto, uint32_t csum);
uint16_t csum_fold(uint32_t csum);

View File

@@ -0,0 +1,39 @@
#pragma once
#include <osmocom/core/socket.h>
/* According to 3GPP TS 29.060. */
struct gtp1u_hdr {
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
uint8_t pn:1, s:1, e:1, spare:1, pt:1, version:3;
#else
uint8_t version:3, pt:1, spare:1, e:1, s:1, pn:1;
#endif
uint8_t type;
uint16_t length;
uint32_t tei;
};
struct gtp_flood;
struct udp_port;
struct gtp_flood_cfg {
unsigned int workers;
unsigned int queue_size;
unsigned int slew_us;
};
struct gtp_flood *gtp_flood_alloc(void *ctx, const struct gtp_flood_cfg *cfg);
struct gtp_flood_flow_cfg {
struct udp_port *gtp_local;
struct osmo_sockaddr gtp_remote;
uint32_t gtp_remote_teid;
struct osmo_sockaddr payload_src;
struct osmo_sockaddr payload_dst;
unsigned int num_packets;
};
void gtp_flood_add_flow(struct gtp_flood *gtp_flood,
const struct gtp_flood_flow_cfg *flow_cfg);
void gtp_flood_start(struct gtp_flood *gtp_flood);
bool gtp_flood_is_busy(struct gtp_flood *gtp_flood);

View File

@@ -28,14 +28,23 @@
#include <osmocom/core/tdef.h>
#include <osmocom/core/socket.h>
#include <osmocom/core/sockaddr_str.h>
#include <osmocom/vty/command.h>
#include <osmocom/pfcp/pfcp_msg.h>
#include <osmocom/upf/up_gtp_action.h>
#include <osmocom/pfcptool/range.h>
#include <osmocom/pfcptool/gtp_flood.h>
struct osmo_tdef;
struct ctrl_handle;
enum pfcp_tool_vty_node {
PEER_NODE = _LAST_OSMOVTY_NODE + 1,
SESSION_NODE,
GTP_FLOOD_NODE,
};
extern struct osmo_tdef g_pfcp_tool_tdefs[];
extern struct osmo_tdef_group g_pfcp_tool_tdef_groups[];
@@ -90,6 +99,16 @@ struct pfcp_tool_session {
};
};
struct udp_port {
struct llist_head entry;
/* IP address and UDP port from user input */
struct osmo_sockaddr osa;
/* In case this is a locally bound port, this is the fd for the socket. */
struct osmo_fd ofd;
};
struct g_pfcp_tool {
struct ctrl_handle *ctrl;
@@ -100,6 +119,38 @@ struct g_pfcp_tool {
struct osmo_pfcp_endpoint *ep;
struct llist_head peers;
uint32_t next_teid_state;
struct {
struct range ip_range;
} ue;
struct {
/* list of struct udp_port */
struct llist_head gtp_local_addrs;
struct udp_port *gtp_local_addrs_next;
struct {
struct {
/* source address is always the UE IP address */
struct range udp_port_range;
} source;
struct {
struct range ip_range;
struct range udp_port_range;
} target;
} payload;
struct {
struct gtp_flood_cfg cfg;
unsigned int flows_per_session;
unsigned int packets_per_flow;
struct gtp_flood *state;
} flood;
} gtp;
};
extern struct g_pfcp_tool *g_pfcp_tool;
@@ -108,7 +159,7 @@ void g_pfcp_tool_alloc(void *ctx);
void pfcp_tool_vty_init_cfg();
void pfcp_tool_vty_init_cmds();
int pfcp_tool_mainloop();
int pfcp_tool_mainloop(int poll);
struct pfcp_tool_peer *pfcp_tool_peer_find_or_create(const struct osmo_sockaddr *remote_addr);
struct pfcp_tool_session *pfcp_tool_session_find_or_create(struct pfcp_tool_peer *peer, uint64_t cp_seid,
@@ -117,3 +168,12 @@ void pfcp_tool_rx_msg(struct osmo_pfcp_endpoint *ep, struct osmo_pfcp_msg *m, st
int peer_tx(struct pfcp_tool_peer *peer, struct osmo_pfcp_msg *m);
uint64_t peer_new_seid(struct pfcp_tool_peer *peer);
uint32_t pfcp_tool_new_teid(void);
int pfcp_tool_next_ue_addr(struct osmo_sockaddr *dst);
struct udp_port *pfcp_tool_have_udp_port_by_str(const struct osmo_sockaddr_str *addr, uint16_t fallback_port);
void pfcp_tool_gtp_flood_start(void);
int pfcp_tool_vty_go_parent(struct vty *vty);

View File

@@ -0,0 +1,38 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
struct osmo_sockaddr;
/* A value large enough for an IPv6 address (128bit) */
struct range_val {
uint64_t buf[2];
size_t size;
};
/* Manage stepping through a defined range of values large enough to handle IPv6 addresses. */
struct range {
struct range_val first;
struct range_val last;
struct range_val next;
bool wrapped;
};
/* struct range r = {};
* range_val_set_int(&r.first, 23);
* range_val_set_int(&r.last, 42);
* while (1) {
* range_next();
* unsigned int val = range_val_get_int(&r.next);
* printf("%u\n", val);
* }
*/
void range_next(struct range *r);
void range_val_set_int(struct range_val *rv, uint32_t val);
uint32_t range_val_get_int(const struct range_val *rv);
void range_val_set_addr(struct range_val *rv, const struct osmo_sockaddr *val);
void range_val_get_addr(struct osmo_sockaddr *dst, const struct range_val *rv);
void range_val_inc(struct range_val *rv);
int range_val_cmp(const struct range_val *a, const struct range_val *b);

View File

@@ -11,6 +11,7 @@ AM_CFLAGS = \
$(LIBOSMOVTY_CFLAGS) \
$(LIBOSMOCTRL_CFLAGS) \
$(LIBOSMOPFCP_CFLAGS) \
$(LIBURING_CFLAGS) \
$(COVERAGE_CFLAGS) \
$(NULL)
@@ -19,19 +20,19 @@ AM_LDFLAGS = \
$(LIBOSMOVTY_LIBS) \
$(LIBOSMOCTRL_LIBS) \
$(LIBOSMOPFCP_LIBS) \
$(LIBURING_LIBS) \
$(COVERAGE_LDFLAGS) \
$(NULL)
noinst_HEADERS = \
pfcp_tool.h \
$(NULL)
bin_PROGRAMS = \
osmo-pfcp-tool \
$(NULL)
osmo_pfcp_tool_SOURCES = \
checksum.c \
gtp_flood.c \
osmo_pfcp_tool_main.c \
pfcp_tool.c \
pfcp_tool_vty.c \
range.c \
$(NULL)

View File

@@ -0,0 +1,211 @@
/*
*
* INET An implementation of the TCP/IP protocol suite for the LINUX
* operating system. INET is implemented using the BSD Socket
* interface as the means of communication with the user level.
*
* IP/TCP/UDP checksumming routines
*
* Authors: Jorge Cwik, <jorge@laser.satlink.net>
* Arnt Gulbrandsen, <agulbra@nvg.unit.no>
* Tom May, <ftom@netcom.com>
* Andreas Schwab, <schwab@issan.informatik.uni-dortmund.de>
* Lots of code moved from tcp.c and ip.c; see those files
* for more names.
*
* 03/02/96 Jes Sorensen, Andreas Schwab, Roman Hodek:
* Fixed some nasty bugs, causing some horrible crashes.
* A: At some points, the sum (%0) was used as
* length-counter instead of the length counter
* (%1). Thanks to Roman Hodek for pointing this out.
* B: GCC seems to mess up if one uses too many
* data-registers to hold input values and one tries to
* specify d0 and d1 as scratch registers. Letting gcc
* choose these registers itself solves the problem.
*
* 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.
*/
/* Revised by Kenneth Albanowski for m68knommu. Basic problem: unaligned access
kills, so most of the assembly has to go. */
#if defined(__FreeBSD__)
#define _KERNEL /* needed on FreeBSD 10.x for s6_addr32 */
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/endian.h>
#endif
#include <osmocom/pfcptool/checksum.h>
#include <arpa/inet.h>
static inline unsigned short from32to16(unsigned int x)
{
/* add up 16-bit and 16-bit for 16+c bit */
x = (x & 0xffff) + (x >> 16);
/* add up carry.. */
x = (x & 0xffff) + (x >> 16);
return x;
}
static unsigned int do_csum(const unsigned char *buff, int len)
{
int odd;
unsigned int result = 0;
if (len <= 0)
goto out;
odd = 1 & (unsigned long) buff;
if (odd) {
#if BYTE_ORDER == LITTLE_ENDIAN
result += (*buff << 8);
#else
result = *buff;
#endif
len--;
buff++;
}
if (len >= 2) {
if (2 & (unsigned long) buff) {
result += *(unsigned short *) buff;
len -= 2;
buff += 2;
}
if (len >= 4) {
const unsigned char *end = buff + ((unsigned)len & ~3);
unsigned int carry = 0;
do {
unsigned int w = *(unsigned int *) buff;
buff += 4;
result += carry;
result += w;
carry = (w > result);
} while (buff < end);
result += carry;
result = (result & 0xffff) + (result >> 16);
}
if (len & 2) {
result += *(unsigned short *) buff;
buff += 2;
}
}
if (len & 1)
#if BYTE_ORDER == LITTLE_ENDIAN
result += *buff;
#else
result += (*buff << 8);
#endif
result = from32to16(result);
if (odd)
result = ((result >> 8) & 0xff) | ((result & 0xff) << 8);
out:
return result;
}
/*
* This is a version of ip_compute_csum() optimized for IP headers,
* which always checksum on 4 octet boundaries.
*/
uint16_t ip_fast_csum(const void *iph, unsigned int ihl)
{
return (uint16_t)~do_csum(iph, ihl*4);
}
/*
* computes the checksum of a memory block at buff, length len,
* and adds in "sum" (32-bit)
*
* returns a 32-bit number suitable for feeding into itself
* or csum_tcpudp_magic
*
* this function must be called with even lengths, except
* for the last fragment, which may be odd
*
* it's best to have buff aligned on a 32-bit boundary
*/
uint32_t csum_partial(const void *buff, int len, uint32_t wsum)
{
unsigned int sum = (unsigned int)wsum;
unsigned int result = do_csum(buff, len);
/* add in old sum, and carry.. */
result += sum;
if (sum > result)
result += 1;
return (uint32_t)result;
}
/*
* this routine is used for miscellaneous IP-like checksums, mainly
* in icmp.c
*/
uint16_t ip_compute_csum(const void *buff, int len)
{
return (uint16_t)~do_csum(buff, len);
}
uint16_t csum_ipv6_magic(const struct in6_addr *saddr,
const struct in6_addr *daddr,
uint32_t len, uint8_t proto, uint32_t csum)
{
int carry;
uint32_t ulen;
uint32_t uproto;
uint32_t sum = (uint32_t)csum;
sum += (uint32_t)saddr->s6_addr32[0];
carry = (sum < (uint32_t)saddr->s6_addr32[0]);
sum += carry;
sum += (uint32_t)saddr->s6_addr32[1];
carry = (sum < (uint32_t)saddr->s6_addr32[1]);
sum += carry;
sum += (uint32_t)saddr->s6_addr32[2];
carry = (sum < (uint32_t)saddr->s6_addr32[2]);
sum += carry;
sum += (uint32_t)saddr->s6_addr32[3];
carry = (sum < (uint32_t)saddr->s6_addr32[3]);
sum += carry;
sum += (uint32_t)daddr->s6_addr32[0];
carry = (sum < (uint32_t)daddr->s6_addr32[0]);
sum += carry;
sum += (uint32_t)daddr->s6_addr32[1];
carry = (sum < (uint32_t)daddr->s6_addr32[1]);
sum += carry;
sum += (uint32_t)daddr->s6_addr32[2];
carry = (sum < (uint32_t)daddr->s6_addr32[2]);
sum += carry;
sum += (uint32_t)daddr->s6_addr32[3];
carry = (sum < (uint32_t)daddr->s6_addr32[3]);
sum += carry;
ulen = (uint32_t)htonl((uint32_t) len);
sum += ulen;
carry = (sum < ulen);
sum += carry;
uproto = (uint32_t)htonl(proto);
sum += uproto;
carry = (sum < uproto);
sum += carry;
return csum_fold((uint32_t)sum);
}
/* fold a partial checksum */
uint16_t csum_fold(uint32_t csum)
{
uint32_t sum = (uint32_t)csum;
sum = (sum & 0xffff) + (sum >> 16);
sum = (sum & 0xffff) + (sum >> 16);
return (uint16_t)~sum;
}

View File

@@ -0,0 +1,388 @@
/* GTP-U traffic/load generator. Generates a configurable amount of UDP/IP flows using io_uring.
*
* Based on gtp-load-gen.c from https://gitea.osmocom.org/cellular-infrastructure/gtp-load-gen
* which is marked (C) 2021 by Harald Welte <laforge@osmocom.org>
*/
/*
* (C) 2024 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 "config.h"
#include <osmocom/pfcptool/gtp_flood.h>
#include <osmocom/core/logging.h>
#if HAVE_URING
#include <unistd.h>
#include <liburing.h>
#include <pthread.h>
#include <netinet/ip.h>
#include <netinet/ip6.h>
#include <netinet/udp.h>
#include <osmocom/core/utils.h>
#include <osmocom/core/linuxlist.h>
#include <osmocom/core/talloc.h>
#include <osmocom/pfcptool/pfcp_tool.h>
#include <osmocom/pfcptool/checksum.h>
#define BUF_SIZE 1024
struct gtp_flood_worker;
struct gtp_flood {
struct gtp_flood_cfg cfg;
/* allocated array */
struct gtp_flood_worker *workers;
unsigned int workers_count;
unsigned int next_worker;
int workers_started;
int workers_running;
};
struct gtp_flood_worker {
/* backpointer */
struct gtp_flood *gtp_flood;
/* list of struct gtp_flood_flow */
struct llist_head flows;
struct io_uring ring;
int fd;
pthread_t worker;
};
struct gtp_flood_flow {
struct llist_head entry;
/* backpointer */
struct gtp_flood_worker *worker;
struct gtp_flood_flow_cfg cfg;
/* for logging and included in generated payloads */
unsigned int id;
/* must live until completion */
struct iovec iov[1];
struct msghdr msgh;
/* flow-private packet buffer */
uint8_t *pkt_buf;
unsigned int submitted_gtp_packets;
unsigned int sent_gtp_packets;
bool stop;
};
static void gtp_flood_worker_init(struct gtp_flood *gtp_flood, struct gtp_flood_worker *worker)
{
worker->gtp_flood = gtp_flood;
INIT_LLIST_HEAD(&worker->flows);
}
struct gtp_flood *gtp_flood_alloc(void *ctx, const struct gtp_flood_cfg *cfg)
{
struct gtp_flood *gtp_flood;
int i;
gtp_flood = talloc_zero(ctx, struct gtp_flood);
*gtp_flood = (struct gtp_flood){
.cfg = *cfg,
.workers = talloc_array(gtp_flood, struct gtp_flood_worker, cfg->workers),
.workers_count = cfg->workers,
};
cfg = &gtp_flood->cfg;
gtp_flood->cfg.workers = OSMO_MAX(1, gtp_flood->cfg.workers);
for (i = 0; i < gtp_flood->workers_count; i++)
gtp_flood_worker_init(gtp_flood, &gtp_flood->workers[i]);
LOGP(DLGLOBAL, LOGL_NOTICE, "workers: %u\n", gtp_flood->workers_count);
return gtp_flood;
}
static void gtp_flood_flow_init_payload(struct gtp_flood_flow *flow)
{
struct gtp1u_hdr *gtp_hdr = (void *)flow->pkt_buf;
*gtp_hdr = (struct gtp1u_hdr) {
.pn = 0,
.s = 0,
.e = 0,
.spare = 0,
.pt = 1,
.version = 1,
.type = 0xff, /* G-PDU */
.length = 0, /* filled in later */
.tei = htonl(flow->cfg.gtp_remote_teid),
};
uint8_t *cur = flow->pkt_buf + sizeof(*gtp_hdr);
/* FIXME: randomize this */
unsigned int udp_len = 1024;
struct iphdr *iph;
struct ip6_hdr *ip6h;
struct udphdr *uh;
struct osmo_sockaddr *src = &flow->cfg.payload_src;
struct osmo_sockaddr *dst = &flow->cfg.payload_dst;
if (src->u.sa.sa_family == AF_INET) {
iph = (struct iphdr *) cur;
cur += sizeof(*iph);
iph->ihl = 5;
iph->version = 4;
iph->tos = 0;
iph->tot_len = htons(udp_len + sizeof(struct udphdr) + sizeof(*iph));
iph->id = 0;
iph->frag_off = 0;
iph->ttl = 32;
iph->protocol = IPPROTO_UDP;
iph->saddr = src->u.sin.sin_addr.s_addr;
iph->daddr = dst->u.sin.sin_addr.s_addr;
iph->check = ip_fast_csum(iph, iph->ihl);
} else {
ip6h = (struct ip6_hdr *) cur;
cur += sizeof(*ip6h);
ip6h->ip6_flow = htonl((6 << 28));
ip6h->ip6_plen = htons(udp_len + sizeof(struct udphdr));
ip6h->ip6_nxt = IPPROTO_UDP;
ip6h->ip6_hlim = 32;
ip6h->ip6_src = src->u.sin6.sin6_addr;
ip6h->ip6_dst = dst->u.sin6.sin6_addr;
}
uh = (struct udphdr *) cur;
cur += sizeof(*uh);
uh->source = htons(osmo_sockaddr_port(&src->u.sa));
uh->dest = htons(osmo_sockaddr_port(&dst->u.sa));
uh->len = htons(udp_len);
uh->check = 0; // TODO
gtp_hdr->length = htons(udp_len + (cur - flow->pkt_buf) - sizeof(*gtp_hdr));
/* initialize this once, so we have it ready for each transmit */
flow->msgh.msg_name = &flow->cfg.gtp_remote.u.sa;
flow->msgh.msg_namelen = sizeof(flow->cfg.gtp_remote.u.sa);
flow->msgh.msg_iov = flow->iov;
flow->msgh.msg_iovlen = ARRAY_SIZE(flow->iov);
flow->msgh.msg_control = NULL;
flow->msgh.msg_controllen = 0;
flow->msgh.msg_flags = 0;
flow->iov[0].iov_base = flow->pkt_buf;
flow->iov[0].iov_len = udp_len + (cur - flow->pkt_buf);
/* write some payload */
struct osmo_strbuf sb = { .buf = (void *)cur, .len = udp_len };
OSMO_STRBUF_PRINTF(sb, "osmo-pfcp-tool gtp flood, emitted from %s to %s teid 0x%08x flow %u\n",
osmo_sockaddr_to_str_c(OTC_SELECT, &flow->cfg.gtp_local->osa),
osmo_sockaddr_to_str_c(OTC_SELECT, &flow->cfg.gtp_remote),
flow->cfg.gtp_remote_teid,
flow->id);
}
void gtp_flood_add_flow(struct gtp_flood *gtp_flood,
const struct gtp_flood_flow_cfg *flow_cfg)
{
static unsigned int next_flow_id = 0;
struct gtp_flood_worker *worker;
gtp_flood->next_worker %= gtp_flood->workers_count;
worker = &gtp_flood->workers[gtp_flood->next_worker];
gtp_flood->next_worker++;
struct gtp_flood_flow *flow = talloc_zero(gtp_flood, struct gtp_flood_flow);
flow->cfg = *flow_cfg;
flow->id = next_flow_id++;
flow->pkt_buf = talloc_zero_size(flow, BUF_SIZE);
OSMO_ASSERT(flow->pkt_buf);
flow->worker = worker;
llist_add_tail(&flow->entry, &worker->flows);
gtp_flood_flow_init_payload(flow);
}
/* transmit one packet for a given flow */
static bool gtp_flow_tx_one(struct gtp_flood_flow *flow)
{
struct gtp_flood_worker *worker = flow->worker;
struct io_uring_sqe *sqe;
sqe = io_uring_get_sqe(&worker->ring);
if (!sqe)
return false;
io_uring_prep_sendmsg(sqe, flow->cfg.gtp_local->ofd.fd, &flow->msgh, 0);
io_uring_sqe_set_data(sqe, flow);
return true;
}
static void *gtp_flood_worker_thread(void *_worker)
{
struct gtp_flood_worker *worker = (struct gtp_flood_worker *)_worker;
struct gtp_flood *gtp_flood = worker->gtp_flood;
osmo_ctx_init(__func__);
gtp_flood->workers_started++;
gtp_flood->workers_running++;
LOGP(DLGLOBAL, LOGL_INFO, "gtp flood worker starting (%u started, %u running)\n",
gtp_flood->workers_started,
gtp_flood->workers_running);
int flows_count = llist_count(&worker->flows);
int flows_ended = 0;
int num_submitted_total = 0;
int num_pending_total = 0;
while (flows_ended < flows_count) {
uint32_t num_submitted = 0;
int num_pending;
usleep(gtp_flood->cfg.slew_us);
/* fill up sqe with transmit submissions */
bool keep_submitting = true;
while (keep_submitting) {
int submitted_was = num_submitted;
struct gtp_flood_flow *flow;
llist_for_each_entry (flow, &worker->flows, entry) {
if (flow->stop)
continue;
if (flow->cfg.num_packets
&& flow->submitted_gtp_packets >= flow->cfg.num_packets)
continue;
if (gtp_flow_tx_one(flow)) {
flow->submitted_gtp_packets++;
num_submitted++;
} else {
/* out of sqe. */
keep_submitting = false;
break;
}
}
/* No change in number of submitted PDUs, all flows are done submitting. */
if (submitted_was == num_submitted)
keep_submitting = false;
}
/* actually submit; determines completions */
num_pending = io_uring_submit(&worker->ring);
/* process all completions */
for (int j = 0; j < num_pending; j++) {
struct io_uring_cqe *cqe;
struct gtp_flood_flow *flow;
int rc;
rc = io_uring_wait_cqe(&worker->ring, &cqe);
OSMO_ASSERT(rc >= 0);
if (cqe->res != 1060) printf(" %d", cqe->res);
if (cqe->res >= 0) {
flow = io_uring_cqe_get_data(cqe);
flow->sent_gtp_packets++;
if (flow->cfg.num_packets
&& flow->sent_gtp_packets >= flow->cfg.num_packets) {
flow->stop = true;
flows_ended++;
}
} else {
flow->submitted_gtp_packets--;
}
io_uring_cqe_seen(&worker->ring, cqe);
}
printf("\nnum_submitted=%5d/%5d num_pending=%5d/%5d flows_ended=%5d/%5d\n",
num_submitted, num_submitted_total, num_pending, num_pending_total, flows_ended, flows_count);
num_submitted_total += num_submitted;
if (num_pending > 0)
num_pending_total += num_pending;
}
gtp_flood->workers_running--;
LOGP(DLGLOBAL, LOGL_INFO, "gtp flood worker done (%u started, %u running)\n",
gtp_flood->workers_started,
gtp_flood->workers_running);
return NULL;
}
static void gtp_flood_worker_start(struct gtp_flood_worker *worker)
{
int rc;
rc = io_uring_queue_init(worker->gtp_flood->cfg.queue_size, &worker->ring, 0);
OSMO_ASSERT(rc >= 0);
rc = pthread_create(&worker->worker, NULL, gtp_flood_worker_thread, worker);
OSMO_ASSERT(rc >= 0);
}
void gtp_flood_start(struct gtp_flood *gtp_flood)
{
int i;
for (i = 0; i < gtp_flood->workers_count; i++)
gtp_flood_worker_start(&gtp_flood->workers[i]);
}
bool gtp_flood_is_busy(struct gtp_flood *gtp_flood)
{
if (!gtp_flood)
return false;
return gtp_flood->workers_started && gtp_flood->workers_running;
}
#else /* HAVE_URING */
struct gtp_flood *gtp_flood_alloc(void *ctx, unsigned int workers)
{
LOGP(DLGLOBAL, LOGL_ERROR, "Cannot start GTP flood: built without liburing support\n");
return NULL;
}
void gtp_flood_add_flow(struct gtp_flood *gtp_flood,
const struct gtp_flood_flow_cfg *flow_cfg)
{
}
void gtp_flood_start(struct gtp_flood *gtp_flood)
{
}
bool gtp_flood_is_busy(struct gtp_flood *gtp_flood)
{
return false;
}
#endif /* HAVE_URING */

View File

@@ -42,7 +42,8 @@
#include <osmocom/pfcp/pfcp_endpoint.h>
#include "pfcp_tool.h"
#include <osmocom/pfcptool/pfcp_tool.h>
#include <osmocom/pfcptool/gtp_flood.h>
#define _GNU_SOURCE
#include <getopt.h>
@@ -206,7 +207,6 @@ static void signal_handler(int signum)
}
}
static struct vty_app_info pfcp_tool_vty_app_info = {
.name = "osmo-pfcp-tool",
.version = PACKAGE_VERSION,
@@ -216,6 +216,7 @@ static struct vty_app_info pfcp_tool_vty_app_info = {
"License AGPLv3+: GNU AGPL version 3 or later <http://gnu.org/licenses/agpl-3.0.html>\r\n"
"This is free software: you are free to change and redistribute it.\r\n"
"There is NO WARRANTY, to the extent permitted by law.\r\n",
.go_parent_cb = pfcp_tool_vty_go_parent,
};
static const struct log_info_cat pfcp_tool_default_categories[] = {
@@ -226,14 +227,15 @@ const struct log_info log_info = {
.num_cat = ARRAY_SIZE(pfcp_tool_default_categories),
};
int pfcp_tool_mainloop()
int pfcp_tool_mainloop(int poll)
{
int rc;
log_reset_context();
osmo_select_main_ctx(0);
rc = osmo_select_main_ctx(poll);
/* If the user hits Ctrl-C the third time, just terminate immediately. */
if (quit >= 3)
return 1;
return -1;
/* Has SIGTERM been received (and not yet been handled)? */
if (quit && !osmo_select_shutdown_requested()) {
@@ -243,7 +245,7 @@ int pfcp_tool_mainloop()
osmo_select_shutdown_request();
/* continue the main select loop until all write queues are serviced. */
}
return 0;
return rc;
}
int main(int argc, char **argv)
@@ -326,7 +328,7 @@ int main(int argc, char **argv)
}
}
pfcp_tool_mainloop();
pfcp_tool_mainloop(1);
pfcp_tool_vty_init_cmds();
@@ -338,17 +340,18 @@ int main(int argc, char **argv)
pfcp_tool_cmdline_config.command_file);
return 1;
}
printf("Done reading '%s', waiting for retransmission queue...\n",
printf("Done reading '%s', waiting for tasks to conclude...\n",
pfcp_tool_cmdline_config.command_file);
do {
if (pfcp_tool_mainloop())
if (pfcp_tool_mainloop(0) == -1)
break;
} while (osmo_pfcp_endpoint_retrans_queue_is_busy(g_pfcp_tool->ep));
printf("Done\n");
} while (osmo_pfcp_endpoint_retrans_queue_is_busy(g_pfcp_tool->ep)
|| gtp_flood_is_busy(g_pfcp_tool->gtp.flood.state));
LOGP(DLGLOBAL, LOGL_NOTICE, "Done\n");
} else {
printf("Listening for commands on VTY...\n");
do {
if (pfcp_tool_mainloop())
if (pfcp_tool_mainloop(0) == -1)
break;
} while (!osmo_select_shutdown_done());
}

View File

@@ -26,7 +26,8 @@
#include <osmocom/pfcp/pfcp_endpoint.h>
#include "pfcp_tool.h"
#include <osmocom/pfcptool/pfcp_tool.h>
#include <osmocom/pfcptool/gtp_flood.h>
struct g_pfcp_tool *g_pfcp_tool = NULL;
@@ -45,9 +46,20 @@ void g_pfcp_tool_alloc(void *ctx)
.local_ip = talloc_strdup(g_pfcp_tool, "0.0.0.0"),
.local_port = OSMO_PFCP_PORT,
},
.next_teid_state = 23,
.gtp.flood = {
.cfg = {
.workers = 1,
.queue_size = 4096,
.slew_us = 0,
},
.flows_per_session = 1,
.packets_per_flow = 0,
},
};
INIT_LLIST_HEAD(&g_pfcp_tool->peers);
INIT_LLIST_HEAD(&g_pfcp_tool->gtp.gtp_local_addrs);
}
struct pfcp_tool_peer *pfcp_tool_peer_find(const struct osmo_sockaddr *remote_addr)
@@ -176,6 +188,8 @@ int peer_tx(struct pfcp_tool_peer *peer, struct osmo_pfcp_msg *m)
else
copy_msg(&peer->last_req, m);
rc = osmo_pfcp_endpoint_tx(g_pfcp_tool->ep, m);
if (rc)
LOGP(DLPFCP, LOGL_ERROR, "Failed to transmit PFCP: %s\n", strerror(-rc));
return rc;
}
@@ -183,3 +197,157 @@ uint64_t peer_new_seid(struct pfcp_tool_peer *peer)
{
return peer->next_seid_state++;
}
uint32_t pfcp_tool_new_teid(void)
{
return g_pfcp_tool->next_teid_state++;
}
int pfcp_tool_next_ue_addr(struct osmo_sockaddr *dst)
{
range_next(&g_pfcp_tool->ue.ip_range);
if (g_pfcp_tool->ue.ip_range.wrapped) {
LOGP(DLGLOBAL, LOGL_ERROR, "insufficient UE IP addresses, wrapped back to first\n");
g_pfcp_tool->ue.ip_range.wrapped = false;
}
range_val_get_addr(dst, &g_pfcp_tool->ue.ip_range.next);
return 0;
}
struct udp_port *pfcp_tool_have_udp_port_by_osa(const struct osmo_sockaddr *_osa, uint16_t fallback_port)
{
struct udp_port *port;
/* copy osa and have a non-const pointer */
struct osmo_sockaddr osa_mutable = *_osa;
struct osmo_sockaddr *osa = &osa_mutable;
if (!osmo_sockaddr_port(&osa->u.sa))
osmo_sockaddr_set_port(&osa->u.sa, fallback_port);
OSMO_ASSERT(osmo_sockaddr_port(&osa->u.sa));
llist_for_each_entry (port, &g_pfcp_tool->gtp.gtp_local_addrs, entry) {
if (osmo_sockaddr_cmp(&port->osa, osa) == 0)
return port;
}
LOGP(DLGLOBAL, LOGL_NOTICE, "new port UDP %s\n", osmo_sockaddr_to_str_c(OTC_SELECT, osa));
port = talloc_zero(g_pfcp_tool, struct udp_port);
port->osa = *osa;
/* create and bind socket */
int rc;
rc = osmo_sock_init_osa(SOCK_DGRAM, IPPROTO_UDP, &port->osa, NULL, OSMO_SOCK_F_BIND);
if (rc < 0) {
LOGP(DLGLOBAL, LOGL_ERROR, "Failed to bind socket on UDP %s\n",
osmo_sockaddr_to_str_c(OTC_SELECT, &port->osa));
exit(1);
}
port->ofd.fd = rc;
LOGP(DLGLOBAL, LOGL_NOTICE, "bound UDP %s fd=%d\n",
osmo_sockaddr_to_str_c(OTC_SELECT, &port->osa), rc);
llist_add_tail(&port->entry, &g_pfcp_tool->gtp.gtp_local_addrs);
return port;
}
struct udp_port *pfcp_tool_have_udp_port_by_str(const struct osmo_sockaddr_str *addr, uint16_t fallback_port)
{
struct osmo_sockaddr osa;
if (osmo_sockaddr_str_to_osa(addr, &osa))
return NULL;
return pfcp_tool_have_udp_port_by_osa(&osa, fallback_port);
}
static bool add_session_to_gtp_flow(struct gtp_flood *gf, struct pfcp_tool_session *session)
{
struct range *r;
struct gtp_flood_flow_cfg cfg = {};
const struct pfcp_tool_gtp_tun *tun_access;
const struct osmo_sockaddr_str *ue_local_addr = NULL;
switch (session->kind) {
case UP_GTP_U_TUNEND:
tun_access = &session->tunend.access;
ue_local_addr = &session->tunend.core.ue_local_addr;
break;
case UP_GTP_U_TUNMAP:
tun_access = &session->tunmap.access;
break;
default:
OSMO_ASSERT(false);
}
/* The 'local' and 'remote' naming clashes here. struct pfcp_tool_gtp_tun names its items from the UPF's point
* of view: so the GTP port of osmo-pfcp-tool is 'remote'.
* Here we are setting up a port to emit GTP from osmo-pfcp-tool, the same port is called 'gtp_local'.
*/
cfg.gtp_local = pfcp_tool_have_udp_port_by_str(&tun_access->remote.addr, 2152);
if (!cfg.gtp_local) {
LOGP(DLGLOBAL, LOGL_ERROR, "Cannot set up GTP flow for session, failed to setup local GTP port\n");
return false;
}
/* The 'local' and 'remote' naming clashes here. struct pfcp_tool_gtp_tun names its items from the UPF's point
* of view: so the GTP port of UPF is 'local'.
* Here we are reading the GTP port that the UPF has opened to receive GTP traffic, the same port is called
* 'gtp_remote' in cfg.
*/
if (osmo_sockaddr_str_to_osa(&tun_access->local.addr, &cfg.gtp_remote)) {
LOGP(DLGLOBAL, LOGL_ERROR, "Cannot set up GTP flow for session, failed to identify UPF's GTP port\n");
return false;
}
if (!osmo_sockaddr_port(&cfg.gtp_remote.u.sa))
osmo_sockaddr_set_port(&cfg.gtp_remote.u.sa, 2152);
cfg.gtp_remote_teid = tun_access->local.teid;
if (ue_local_addr) {
osmo_sockaddr_str_to_osa(ue_local_addr, &cfg.payload_src);
} else if (pfcp_tool_next_ue_addr(&cfg.payload_src)) {
LOGP(DLGLOBAL, LOGL_ERROR, "Cannot set up GTP flow for session, failed to identify UE's IP address\n");
return false;
}
r = &g_pfcp_tool->gtp.payload.source.udp_port_range;
range_next(r);
osmo_sockaddr_set_port(&cfg.payload_src.u.sa, range_val_get_int(&r->next));
r = &g_pfcp_tool->gtp.payload.target.ip_range;
range_next(r);
range_val_get_addr(&cfg.payload_dst, &r->next);
r = &g_pfcp_tool->gtp.payload.target.udp_port_range;
range_next(r);
osmo_sockaddr_set_port(&cfg.payload_dst.u.sa, range_val_get_int(&r->next));
cfg.num_packets = g_pfcp_tool->gtp.flood.packets_per_flow;
for (int i = 0; i < g_pfcp_tool->gtp.flood.flows_per_session; i++)
gtp_flood_add_flow(gf, &cfg);
return true;
}
void pfcp_tool_gtp_flood_start(void)
{
struct gtp_flood *gf;
struct pfcp_tool_peer *peer;
if (g_pfcp_tool->gtp.flood.state) {
LOGP(DLGLOBAL, LOGL_ERROR,
"another 'gtp flood' is still running; currently we can run only one gtp flood at a time\n");
return;
}
gf = g_pfcp_tool->gtp.flood.state = gtp_flood_alloc(g_pfcp_tool, &g_pfcp_tool->gtp.flood.cfg);
if (!gf)
return;
llist_for_each_entry (peer, &g_pfcp_tool->peers, entry) {
struct pfcp_tool_session *session;
llist_for_each_entry (session, &peer->sessions, entry) {
add_session_to_gtp_flow(gf, session);
}
}
gtp_flood_start(gf);
}

View File

@@ -34,12 +34,7 @@
#include <osmocom/vty/vty.h>
#include <osmocom/vty/command.h>
#include "pfcp_tool.h"
enum pfcp_tool_vty_node {
PEER_NODE = _LAST_OSMOVTY_NODE + 1,
SESSION_NODE,
};
#include <osmocom/pfcptool/pfcp_tool.h>
DEFUN(c_local_addr, c_local_addr_cmd,
"local-addr IP_ADDR",
@@ -124,7 +119,7 @@ DEFUN(c_sleep, c_sleep_cmd,
/* Still operate the message pump while waiting for time to pass */
while (t.active && !osmo_select_shutdown_done()) {
if (pfcp_tool_mainloop())
if (pfcp_tool_mainloop(0) == -1)
break;
}
@@ -134,6 +129,209 @@ DEFUN(c_sleep, c_sleep_cmd,
return CMD_SUCCESS;
}
DEFUN(c_date, c_date_cmd,
"date",
"print a timestamp\n")
{
struct timeval tv = {};
struct tm tm = {};
osmo_gettimeofday(&tv, NULL);
localtime_r(&tv.tv_sec, &tm);
vty_out(vty, "%04d-%02d-%02d,%02d:%02d:%02d.%03d%s",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec,
(int)(tv.tv_usec / 1000), VTY_NEWLINE);
vty_flush(vty);
return CMD_SUCCESS;
}
static bool parse_ip_to_range_val(struct vty *vty, struct range_val *val, const char *ip_addr_str)
{
struct osmo_sockaddr_str a;
struct osmo_sockaddr osa;
if (osmo_sockaddr_str_from_str(&a, ip_addr_str, 0)
|| osmo_sockaddr_str_to_osa(&a, &osa)) {
vty_out(vty, "%% Error: invalid IP address: '%s'%s", ip_addr_str, VTY_NEWLINE);
return false;
}
range_val_set_addr(val, &osa);
return true;
}
static bool parse_ip_range(struct vty *vty, struct range *dst, const char *argv[])
{
return parse_ip_to_range_val(vty, &dst->first, argv[0])
&& parse_ip_to_range_val(vty, &dst->last, argv[1]);
}
#define IP_RANGE_STR \
"Set a range with first and last address\n" \
"First IP address in the range, 1.2.3.4 or 1:2:3::4\n" \
"Last IP address in the range, 1.2.3.4 or 1:2:3::4\n"
DEFUN(c_ue_ip_range, c_ue_ip_range_cmd,
"ue ip range IP_ADDR_FIRST IP_ADDR_LAST",
"UE\n"
"Set the IP address range used for the UE IP addresses\n"
IP_RANGE_STR)
{
if (!parse_ip_range(vty, &g_pfcp_tool->ue.ip_range, &argv[0]))
return CMD_WARNING;
return CMD_SUCCESS;
}
#define GTP_STR "Configure GTP\n"
#define GTP_IP_STR "Add a GTP IP address\n"
#define IP_ADDR_STR "IP address, 1.2.3.4 or 1:2:3:4::1\n"
DEFUN(c_gtp_local, c_gtp_local_cmd,
"gtp local IP_ADDR [<1-65535>]",
GTP_STR
"Add a local GTP port, to emit GTP traffic from\n"
IP_ADDR_STR
"Specific UDP port to use, 2152 if omitted\n")
{
const char *ip_str = argv[0];
uint16_t port_nr;
struct osmo_sockaddr_str addr_str;
struct udp_port *p;
if (argc > 1)
port_nr = atoi(argv[1]);
else
port_nr = 2152;
if (osmo_sockaddr_str_from_str(&addr_str, ip_str, port_nr))
goto invalid_ip;
vty_out(vty, "local GTP port: " OSMO_SOCKADDR_STR_FMT "%s", OSMO_SOCKADDR_STR_FMT_ARGS(&addr_str), VTY_NEWLINE);
p = pfcp_tool_have_udp_port_by_str(&addr_str, port_nr);
if (!p)
goto invalid_ip;
return CMD_SUCCESS;
invalid_ip:
vty_out(vty, "%% Error: invalid IP address: '%s'%s", ip_str, VTY_NEWLINE);
return CMD_WARNING;
}
static struct cmd_node gtp_flood_node = {
GTP_FLOOD_NODE,
"%s(gtp-flood)# ",
1,
};
DEFUN(gtp_flood, gtp_flood_cmd,
"gtp flood",
GTP_STR
"Setup GTP traffic\n")
{
vty->node = GTP_FLOOD_NODE;
return CMD_SUCCESS;
}
DEFUN(gtpf_workers, gtpf_workers_cmd,
"workers <1-999>",
"Number of worker threads to launch, to emit GTP traffic from\n"
"Number\n")
{
g_pfcp_tool->gtp.flood.cfg.workers = atoi(argv[0]);
return CMD_SUCCESS;
}
DEFUN(gtpf_io_uring_queue_size, gtpf_io_uring_queue_size_cmd,
"io-uring queue-size <1-65536>",
"Fine-tune io-uring usage for optimal GTP packet flooding\n"
"Maximum number of packets to submit for sending simultaneously.\n"
"Number\n")
{
g_pfcp_tool->gtp.flood.cfg.queue_size = atoi(argv[0]);
return CMD_SUCCESS;
}
DEFUN(gtpf_flows, gtpf_flows_cmd,
"flows-per-session <1-999999>",
"Set number of GTP packet flows emitted per PFCP session's GTP tunnel\n"
"Number\n")
{
g_pfcp_tool->gtp.flood.flows_per_session = atoi(argv[0]);
return CMD_SUCCESS;
}
DEFUN(gtpf_packets, gtpf_packets_cmd,
"packets-per-flow (<1-2000000000>|infinite)",
"Set number of GTP packet flows emitted per PFCP session's GTP tunnel\n"
"Number\n")
{
unsigned int val;
if (!strcmp("infinite", argv[0]))
val = 0;
else
val = atoi(argv[0]);
g_pfcp_tool->gtp.flood.packets_per_flow = val;
return CMD_SUCCESS;
}
#define PAYLOAD_STR "Configure GTP payload\n"
#define RANGE_STR \
"Set a range to cycle through\n" \
"First value of the range\n" \
"Last value of the range\n"
DEFUN(gtpf_payload_src_port, gtpf_payload_src_port_cmd,
"payload source port udp range <1-65535> <1-65535>",
PAYLOAD_STR
"Source port of payload packets. Note: the source IP will always be the UE IP for that session.\n"
"Port\n"
"UDP port\n"
RANGE_STR)
{
range_val_set_int(&g_pfcp_tool->gtp.payload.source.udp_port_range.first,
atoi(argv[0]));
range_val_set_int(&g_pfcp_tool->gtp.payload.source.udp_port_range.last,
atoi(argv[1]));
return CMD_SUCCESS;
}
#define PAYLOAD_TARGET_STR PAYLOAD_STR "Target of payload packets\n"
DEFUN(gtpf_payload_target_ip, gtpf_payload_target_ip_cmd,
"payload target ip range IP_ADDR_FIRST IP_ADDR_LAST",
PAYLOAD_TARGET_STR
"IP address\n"
RANGE_STR)
{
if (!parse_ip_range(vty, &g_pfcp_tool->gtp.payload.target.ip_range, &argv[0]))
return CMD_WARNING;
return CMD_SUCCESS;
}
DEFUN(gtpf_payload_target_port, gtpf_payload_target_port_cmd,
"payload target port udp range <1-65535> <1-65535>",
PAYLOAD_STR
"Target port of payload packets\n"
"Port\n"
"UDP port\n"
RANGE_STR)
{
range_val_set_int(&g_pfcp_tool->gtp.payload.target.udp_port_range.first,
atoi(argv[0]));
range_val_set_int(&g_pfcp_tool->gtp.payload.target.udp_port_range.last,
atoi(argv[1]));
return CMD_SUCCESS;
}
DEFUN(gtpf_slew, gtpf_slew_cmd,
"slew <0-1000000000>",
"Wait N microseconds after each io_uring transmission submission\n"
"microseconds to wait (1000000000 == 1 second)\n")
{
g_pfcp_tool->gtp.flood.cfg.slew_us = atoi(argv[0]);
return CMD_SUCCESS;
}
static struct cmd_node peer_node = {
PEER_NODE,
"%s(peer)# ",
@@ -397,9 +595,16 @@ DEFUN(s_f_teid_choose, s_f_teid_choose_cmd,
return CMD_SUCCESS;
}
int session_tunend_tx_est_req(struct vty *vty, const char **argv, int argc)
enum pdr_id_fixed {
PDR_ID_CORE = 1,
PDR_ID_ACCESS = 2,
};
const char * const gtp_ip_core = "10.99.0.1";
const char * const fallback_gtp_ip_access = "10.99.0.2";
int session_tunend_tx_est_req(struct pfcp_tool_session *session, bool forw, osmo_pfcp_resp_cb resp_cb)
{
struct pfcp_tool_session *session = vty->index;
struct pfcp_tool_peer *peer = session->peer;
int rc;
struct osmo_pfcp_msg *m;
@@ -412,19 +617,19 @@ int session_tunend_tx_est_req(struct vty *vty, const char **argv, int argc)
OSMO_ASSERT(session->kind == UP_GTP_U_TUNEND);
if (!g_pfcp_tool->ep) {
vty_out(vty, "Endpoint not configured%s", VTY_NEWLINE);
LOGP(DLGLOBAL, LOGL_ERROR, "PFCP endpoint not configured\n");
return CMD_WARNING;
}
if (argc > 0 && !strcmp("drop", argv[0]))
osmo_pfcp_bits_set(aa.bits, OSMO_PFCP_APPLY_ACTION_DROP, true);
else
if (forw)
osmo_pfcp_bits_set(aa.bits, OSMO_PFCP_APPLY_ACTION_FORW, true);
else
osmo_pfcp_bits_set(aa.bits, OSMO_PFCP_APPLY_ACTION_DROP, true);
#define STR_TO_ADDR(DST, SRC) do { \
if (osmo_sockaddr_str_to_sockaddr(&SRC, &DST.u.sas)) { \
vty_out(vty, "Error in " #SRC ": " OSMO_SOCKADDR_STR_FMT "%s", \
OSMO_SOCKADDR_STR_FMT_ARGS(&SRC), VTY_NEWLINE); \
LOGP(DLGLOBAL, LOGL_ERROR, "Error in " #SRC ": " OSMO_SOCKADDR_STR_FMT "\n", \
OSMO_SOCKADDR_STR_FMT_ARGS(&SRC)); \
return CMD_WARNING; \
} \
} while (0)
@@ -464,6 +669,10 @@ int session_tunend_tx_est_req(struct vty *vty, const char **argv, int argc)
osmo_pfcp_ip_addrs_set(&cp_f_seid.ip_addr, osmo_pfcp_endpoint_get_local_addr(g_pfcp_tool->ep));
m = osmo_pfcp_msg_alloc_tx_req(OTC_SELECT, &peer->remote_addr, OSMO_PFCP_MSGT_SESSION_EST_REQ);
m->ctx.resp_cb = resp_cb;
m->ctx.priv = session;
m->h.seid_present = true;
/* the UPF has yet to assign a SEID for itself, no matter what SEID we (the CPF) use for this session */
m->h.seid = 0;
@@ -475,7 +684,7 @@ int session_tunend_tx_est_req(struct vty *vty, const char **argv, int argc)
.create_pdr_count = 2,
.create_pdr = {
{
.pdr_id = 1,
.pdr_id = PDR_ID_CORE,
.precedence = 255,
.pdi = {
.source_iface = OSMO_PFCP_SOURCE_IFACE_CORE,
@@ -492,7 +701,7 @@ int session_tunend_tx_est_req(struct vty *vty, const char **argv, int argc)
.far_id = 1,
},
{
.pdr_id = 2,
.pdr_id = PDR_ID_ACCESS,
.precedence = 255,
.pdi = {
.source_iface = OSMO_PFCP_SOURCE_IFACE_ACCESS,
@@ -531,16 +740,13 @@ int session_tunend_tx_est_req(struct vty *vty, const char **argv, int argc)
};
rc = peer_tx(peer, m);
if (rc) {
vty_out(vty, "Failed to transmit: %s%s", strerror(-rc), VTY_NEWLINE);
if (rc)
return CMD_WARNING;
}
return CMD_SUCCESS;
}
int session_tunmap_tx_est_req(struct vty *vty, const char **argv, int argc)
int session_tunmap_tx_est_req(struct pfcp_tool_session *session, bool forw, osmo_pfcp_resp_cb resp_cb)
{
struct pfcp_tool_session *session = vty->index;
struct pfcp_tool_peer *peer = session->peer;
int rc;
struct osmo_pfcp_msg *m;
@@ -556,14 +762,14 @@ int session_tunmap_tx_est_req(struct vty *vty, const char **argv, int argc)
struct osmo_pfcp_ie_apply_action aa = {};
if (!g_pfcp_tool->ep) {
vty_out(vty, "Endpoint not configured%s", VTY_NEWLINE);
LOGP(DLGLOBAL, LOGL_ERROR, "PFCP endpoint not configured\n");
return CMD_WARNING;
}
if (argc > 0 && !strcmp("drop", argv[0]))
osmo_pfcp_bits_set(aa.bits, OSMO_PFCP_APPLY_ACTION_DROP, true);
else
if (forw)
osmo_pfcp_bits_set(aa.bits, OSMO_PFCP_APPLY_ACTION_FORW, true);
else
osmo_pfcp_bits_set(aa.bits, OSMO_PFCP_APPLY_ACTION_DROP, true);
if (session->tunmap.access.local.teid == 0) {
f_teid_access_local = (struct osmo_pfcp_ie_f_teid){
@@ -625,6 +831,10 @@ int session_tunmap_tx_est_req(struct vty *vty, const char **argv, int argc)
osmo_pfcp_ip_addrs_set(&cp_f_seid.ip_addr, osmo_pfcp_endpoint_get_local_addr(g_pfcp_tool->ep));
m = osmo_pfcp_msg_alloc_tx_req(OTC_SELECT, &peer->remote_addr, OSMO_PFCP_MSGT_SESSION_EST_REQ);
m->ctx.resp_cb = resp_cb;
m->ctx.priv = session;
m->h.seid_present = true;
m->h.seid = 0;
/* GTP tunmap: remove header from both directions, and add header in both directions */
@@ -635,7 +845,7 @@ int session_tunmap_tx_est_req(struct vty *vty, const char **argv, int argc)
.create_pdr_count = 2,
.create_pdr = {
{
.pdr_id = 1,
.pdr_id = PDR_ID_CORE,
.precedence = 255,
.pdi = {
.source_iface = OSMO_PFCP_SOURCE_IFACE_CORE,
@@ -650,7 +860,7 @@ int session_tunmap_tx_est_req(struct vty *vty, const char **argv, int argc)
.far_id = 1,
},
{
.pdr_id = 2,
.pdr_id = PDR_ID_ACCESS,
.precedence = 255,
.pdi = {
.source_iface = OSMO_PFCP_SOURCE_IFACE_ACCESS,
@@ -691,10 +901,8 @@ int session_tunmap_tx_est_req(struct vty *vty, const char **argv, int argc)
};
rc = peer_tx(peer, m);
if (rc) {
vty_out(vty, "Failed to transmit: %s%s", strerror(-rc), VTY_NEWLINE);
if (rc)
return CMD_WARNING;
}
return CMD_SUCCESS;
}
@@ -705,24 +913,20 @@ DEFUN(session_tx_est_req, session_tx_est_req_cmd,
"Set FAR to DROP = 1\n")
{
struct pfcp_tool_session *session = vty->index;
bool forw = (argc == 0 || !strcmp("forw", argv[0]));
switch (session->kind) {
case UP_GTP_U_TUNEND:
return session_tunend_tx_est_req(vty, argv, argc);
return session_tunend_tx_est_req(session, forw, NULL);
case UP_GTP_U_TUNMAP:
return session_tunmap_tx_est_req(vty, argv, argc);
return session_tunmap_tx_est_req(session, forw, NULL);
default:
vty_out(vty, "unknown gtp action%s", VTY_NEWLINE);
return CMD_WARNING;
}
}
DEFUN(session_tx_mod_req, session_tx_mod_req_cmd,
"tx session-mod-req far [(forw|drop)]",
TX_STR "Send a Session Modification Request\n"
"Set FAR to FORW = 1\n"
"Set FAR to DROP = 1\n")
int session_tunmap_tx_mod_req(struct pfcp_tool_session *session, bool forw, osmo_pfcp_resp_cb resp_cb)
{
struct pfcp_tool_session *session = vty->index;
struct pfcp_tool_peer *peer = session->peer;
int rc;
struct osmo_pfcp_msg *m;
@@ -730,14 +934,14 @@ DEFUN(session_tx_mod_req, session_tx_mod_req_cmd,
struct osmo_pfcp_ie_f_seid cp_f_seid;
if (!g_pfcp_tool->ep) {
vty_out(vty, "Endpoint not configured%s", VTY_NEWLINE);
LOGP(DLGLOBAL, LOGL_ERROR, "PFCP endpoint not configured\n");
return CMD_WARNING;
}
if (argc > 0 && !strcmp("drop", argv[0]))
osmo_pfcp_bits_set(aa.bits, OSMO_PFCP_APPLY_ACTION_DROP, true);
else
if (forw)
osmo_pfcp_bits_set(aa.bits, OSMO_PFCP_APPLY_ACTION_FORW, true);
else
osmo_pfcp_bits_set(aa.bits, OSMO_PFCP_APPLY_ACTION_DROP, true);
cp_f_seid = (struct osmo_pfcp_ie_f_seid){
.seid = session->cp_seid,
@@ -745,6 +949,10 @@ DEFUN(session_tx_mod_req, session_tx_mod_req_cmd,
osmo_pfcp_ip_addrs_set(&cp_f_seid.ip_addr, osmo_pfcp_endpoint_get_local_addr(g_pfcp_tool->ep));
m = osmo_pfcp_msg_alloc_tx_req(OTC_SELECT, &peer->remote_addr, OSMO_PFCP_MSGT_SESSION_MOD_REQ);
m->ctx.resp_cb = resp_cb;
m->ctx.priv = session;
m->h.seid_present = true;
m->h.seid = session->up_f_seid.seid;
m->ies.session_mod_req = (struct osmo_pfcp_msg_session_mod_req){
@@ -766,13 +974,25 @@ DEFUN(session_tx_mod_req, session_tx_mod_req_cmd,
};
rc = peer_tx(peer, m);
if (rc) {
vty_out(vty, "Failed to transmit: %s%s", strerror(-rc), VTY_NEWLINE);
if (rc)
return CMD_WARNING;
}
return CMD_SUCCESS;
}
DEFUN(session_tx_mod_req, session_tx_mod_req_cmd,
"tx session-mod-req far [(forw|drop)]",
TX_STR "Send a Session Modification Request\n"
"Set FAR to FORW = 1\n"
"Set FAR to DROP = 1\n")
{
struct pfcp_tool_session *session = vty->index;
bool forw = (argc == 0 || !strcmp("forw", argv[0]));
int rc = session_tunmap_tx_mod_req(session, forw, NULL);
if (rc != CMD_SUCCESS)
vty_out(vty, "Failed to send Session Modification Request%s", VTY_NEWLINE);
return rc;
}
DEFUN(session_tx_del_req, session_tx_del_req_cmd,
"tx session-del-req",
TX_STR "Send a Session Deletion Request\n")
@@ -799,6 +1019,327 @@ DEFUN(session_tx_del_req, session_tx_del_req_cmd,
return CMD_SUCCESS;
}
/* N SESSIONS */
static int responses_pending = 0;
DEFUN(wait_responses, wait_responses_cmd,
"wait responses",
"Let some time pass until events have occurred\n"
"Wait for all PFCP responses for pending PFCP requests\n")
{
if (!responses_pending) {
vty_out(vty, "no responses pending, not waiting.%s", VTY_NEWLINE);
return CMD_SUCCESS;
}
vty_out(vty, "waiting for %d responses...%s", responses_pending, VTY_NEWLINE);
vty_flush(vty);
/* Still operate the message pump while waiting for time to pass */
while (!osmo_select_shutdown_done()) {
if (pfcp_tool_mainloop(0) == -1)
break;
if (responses_pending == 0)
break;
}
vty_out(vty, "...done waiting for responses%s", VTY_NEWLINE);
vty_flush(vty);
return CMD_SUCCESS;
}
/* N SESSIONS TUNEND */
int one_session_mod_tunend_resp_cb(struct osmo_pfcp_msg *req, struct osmo_pfcp_msg *rx_resp, const char *errmsg)
{
responses_pending--;
return 0;
}
int one_session_mod_tunend(struct pfcp_tool_session *session)
{
int rc;
rc = session_tunmap_tx_mod_req(session, true, one_session_mod_tunend_resp_cb);
if (rc == CMD_SUCCESS)
responses_pending++;
return rc;
}
void est_resp_get_created_f_teid(struct pfcp_tool_gtp_tun_ep *dst, const struct osmo_pfcp_msg *rx_resp, uint16_t pdr_id)
{
int i;
const struct osmo_pfcp_msg_session_est_resp *r;
if (rx_resp->h.message_type != OSMO_PFCP_MSGT_SESSION_EST_RESP)
return;
r = &rx_resp->ies.session_est_resp;
for (i = 0; i < r->created_pdr_count; i++) {
const struct osmo_pfcp_ie_created_pdr *p = &r->created_pdr[i];
if (p->pdr_id != pdr_id)
continue;
if (!p->local_f_teid_present)
continue;
osmo_sockaddr_str_from_sockaddr(&dst->addr,
&p->local_f_teid.fixed.ip_addr.v4.u.sas);
dst->teid = p->local_f_teid.fixed.teid;
}
}
int one_session_create_tunend_resp_cb(struct osmo_pfcp_msg *req, struct osmo_pfcp_msg *rx_resp, const char *errmsg)
{
struct pfcp_tool_session *session = req->ctx.priv;
enum osmo_pfcp_cause *cause;
const struct osmo_pfcp_msg_session_est_resp *r = NULL;
if (rx_resp)
r = &rx_resp->ies.session_est_resp;
responses_pending--;
if (errmsg)
LOGP(DLPFCP, LOGL_ERROR, "%s\n", errmsg);
cause = rx_resp ? osmo_pfcp_msg_cause(rx_resp) : NULL;
if (!cause || *cause != OSMO_PFCP_CAUSE_REQUEST_ACCEPTED)
return 0;
/* store SEID */
if (r && r->up_f_seid_present)
session->up_f_seid = r->up_f_seid;
/* store access local F-TEID */
est_resp_get_created_f_teid(&session->tunend.access.local, rx_resp, PDR_ID_ACCESS);
/* Success response, now continue with second step: Session Mod to set the CORE's remote side GTP */
one_session_mod_tunend(session);
return 0;
}
static int one_session_create_tunend(struct pfcp_tool_peer *peer)
{
struct pfcp_tool_session *session;
struct pfcp_tool_tunend *te;
struct pfcp_tool_gtp_tun_ep *dst;
struct osmo_sockaddr osa;
int rc;
session = pfcp_tool_session_find_or_create(peer, peer_new_seid(peer), UP_GTP_U_TUNEND);
te = &session->tunend;
/* Access: set remote GTP address from UPF's point of view.
* A local GTP port from osmo-pfcp-tool's point of view. */
dst = &te->access.remote;
if (llist_empty(&g_pfcp_tool->gtp.gtp_local_addrs)) {
/* No local GTP ports configured, just set an arbitrary address. */
if (osmo_sockaddr_str_from_str2(&dst->addr, fallback_gtp_ip_access)) {
LOGP(DLGLOBAL, LOGL_ERROR, "Error setting Access side GTP IP address from %s\n",
osmo_quote_cstr_c(OTC_SELECT, fallback_gtp_ip_access, -1));
return CMD_WARNING;
}
} else {
/* next local GTP address to emit from */
struct udp_port *next;
next = g_pfcp_tool->gtp.gtp_local_addrs_next;
if (next) {
/* Move on by one gtp_local_addr. If past the end of the list, wrap back to the start below. */
if (next->entry.next == &g_pfcp_tool->gtp.gtp_local_addrs)
next = NULL;
else
next = container_of(next->entry.next, struct udp_port, entry);
}
if (!next)
next = llist_first_entry_or_null(&g_pfcp_tool->gtp.gtp_local_addrs, struct udp_port,
entry);
OSMO_ASSERT(next);
g_pfcp_tool->gtp.gtp_local_addrs_next = next;
if (osmo_sockaddr_str_from_osa(&dst->addr, &next->osa)) {
LOGP(DLGLOBAL, LOGL_ERROR, "Error setting Access side GTP IP address from %s\n",
osmo_sockaddr_to_str_c(OTC_SELECT, &next->osa));
return CMD_WARNING;
}
}
LOGP(DLGLOBAL, LOGL_NOTICE, "session picked gtp local " OSMO_SOCKADDR_STR_FMT "\n", OSMO_SOCKADDR_STR_FMT_ARGS(&dst->addr));
dst->teid = pfcp_tool_new_teid();
/* Set UE address */
te->access.local = (struct pfcp_tool_gtp_tun_ep){};
pfcp_tool_next_ue_addr(&osa);
if (osmo_sockaddr_str_from_osa(&te->core.ue_local_addr, &osa)) {
LOGP(DLGLOBAL, LOGL_ERROR, "Error setting UE address from %s\n",
osmo_sockaddr_to_str_c(OTC_SELECT, &osa));
return CMD_WARNING;
};
/* Send initial Session Establishment Request */
rc = session_tunend_tx_est_req(session, false, one_session_create_tunend_resp_cb);
if (rc == CMD_SUCCESS)
responses_pending++;
return rc;
}
/* N SESSIONS TUNMAP */
int one_session_mod_tunmap_resp_cb(struct osmo_pfcp_msg *req, struct osmo_pfcp_msg *rx_resp, const char *errmsg)
{
responses_pending--;
return 0;
}
int one_session_mod_tunmap(struct pfcp_tool_session *session)
{
struct pfcp_tool_gtp_tun_ep *dst;
int rc;
dst = &session->tunmap.core.remote;
if (osmo_sockaddr_str_from_str2(&dst->addr, gtp_ip_core)) {
LOGP(DLGLOBAL, LOGL_ERROR, "Error setting GTP IP address from %s\n",
osmo_quote_cstr_c(OTC_SELECT, gtp_ip_core, -1));
return CMD_WARNING;
}
dst->teid = pfcp_tool_new_teid();
rc = session_tunmap_tx_mod_req(session, true, one_session_mod_tunmap_resp_cb);
if (rc == CMD_SUCCESS)
responses_pending++;
return rc;
}
int one_session_create_tunmap_resp_cb(struct osmo_pfcp_msg *req, struct osmo_pfcp_msg *rx_resp, const char *errmsg)
{
struct pfcp_tool_session *session = req->ctx.priv;
enum osmo_pfcp_cause *cause;
responses_pending--;
if (errmsg)
LOGP(DLPFCP, LOGL_ERROR, "%s\n", errmsg);
cause = rx_resp ? osmo_pfcp_msg_cause(rx_resp) : NULL;
if (!cause || *cause != OSMO_PFCP_CAUSE_REQUEST_ACCEPTED)
return 0;
/* store SEID */
if (rx_resp->ies.session_est_resp.up_f_seid_present)
session->up_f_seid = rx_resp->ies.session_est_resp.up_f_seid;
/* store local F-TEIDs */
est_resp_get_created_f_teid(&session->tunmap.access.local, rx_resp, PDR_ID_ACCESS);
est_resp_get_created_f_teid(&session->tunmap.core.local, rx_resp, PDR_ID_CORE);
/* Success response, now continue with second step: Session Mod to set the CORE's remote side GTP */
one_session_mod_tunmap(session);
return 0;
}
static int one_session_create_tunmap(struct pfcp_tool_peer *peer)
{
struct pfcp_tool_session *session;
struct pfcp_tool_tunmap *tm;
struct pfcp_tool_gtp_tun_ep *dst;
int rc;
session = pfcp_tool_session_find_or_create(peer, peer_new_seid(peer), UP_GTP_U_TUNMAP);
tm = &session->tunmap;
/* Access: set remote GTP address */
dst = &tm->access.remote;
if (osmo_sockaddr_str_from_str2(&dst->addr, fallback_gtp_ip_access)) {
LOGP(DLGLOBAL, LOGL_ERROR, "Error setting GTP IP address from %s\n",
osmo_quote_cstr_c(OTC_SELECT, fallback_gtp_ip_access, -1));
return CMD_WARNING;
}
dst->teid = pfcp_tool_new_teid();
/* Core: set remote GTP address */
dst = &tm->core.remote;
if (osmo_sockaddr_str_from_str2(&dst->addr, gtp_ip_core)) {
LOGP(DLGLOBAL, LOGL_ERROR, "Error setting GTP IP address from %s\n",
osmo_quote_cstr_c(OTC_SELECT, gtp_ip_core, -1));
return CMD_WARNING;
}
dst->teid = pfcp_tool_new_teid();
/* Set local F-TEIDs == CHOOSE */
tm->access.local = (struct pfcp_tool_gtp_tun_ep){};
tm->core.local = (struct pfcp_tool_gtp_tun_ep){};
/* Send initial Session Establishment Request */
rc = session_tunmap_tx_est_req(session, false, one_session_create_tunmap_resp_cb);
if (rc == CMD_SUCCESS)
responses_pending++;
return rc;
return CMD_WARNING;
}
static void n_sessions_create(struct vty *vty, struct pfcp_tool_peer *peer, int n, enum up_gtp_action_kind kind)
{
int i;
for (i = 0; i < n; i++) {
int rc;
if (kind == UP_GTP_U_TUNMAP)
rc = one_session_create_tunmap(peer);
else
rc = one_session_create_tunend(peer);
if (rc != CMD_SUCCESS)
break;
/* handle any pending select work */
while (!osmo_select_shutdown_done()) {
rc = pfcp_tool_mainloop(1);
/* quit requested */
if (rc < 0)
return;
/* no fd needed service */
if (rc == 0)
break;
}
/* Every N created sessions, wait for pending responses */
if (!(i & 0x3f) && responses_pending) {
vty_out(vty, "waiting for %d responses...%s", responses_pending, VTY_NEWLINE);
vty_flush(vty);
while (!osmo_select_shutdown_done()) {
if (pfcp_tool_mainloop(0) == -1)
break;
if (responses_pending == 0)
break;
}
}
}
}
static void n_sessions_delete(struct pfcp_tool_peer *peer, int n, enum up_gtp_action_kind kind)
{
}
DEFUN(n_sessions, n_sessions_cmd,
"n (<0-2147483647>|all) session (create|delete) (tunend|tunmap)",
"Batch run\n"
"Perform the action N times, or on all available entries\n"
"In a batch run, create and later delete a number of sessions at once.\n"
"Create N new sessions\n"
"Delete N sessions created earlier\n"
TUNEND_STR TUNMAP_STR)
{
struct pfcp_tool_peer *peer = vty->index;
int n = ((strcmp("all", argv[0]) == 0) ? -1 : atoi(argv[0]));
bool create = (strcmp("create", argv[1]) == 0);
enum up_gtp_action_kind kind;
if (!strcmp(argv[2], "tunmap"))
kind = UP_GTP_U_TUNMAP;
else
kind = UP_GTP_U_TUNEND;
if (create)
n_sessions_create(vty, peer, n, kind);
else
n_sessions_delete(peer, n, kind);
return CMD_SUCCESS;
}
static void install_ve_and_config(struct cmd_element *cmd)
{
install_element_ve(cmd);
@@ -818,23 +1359,56 @@ void pfcp_tool_vty_init_cmds()
OSMO_ASSERT(g_pfcp_tool != NULL);
install_ve_and_config(&c_sleep_cmd);
install_ve_and_config(&c_date_cmd);
install_ve_and_config(&wait_responses_cmd);
install_ve_and_config(&peer_cmd);
install_node(&peer_node, NULL);
install_element(PEER_NODE, &c_sleep_cmd);
install_element(PEER_NODE, &c_date_cmd);
install_element(PEER_NODE, &peer_tx_heartbeat_cmd);
install_element(PEER_NODE, &peer_tx_assoc_setup_req_cmd);
install_element(PEER_NODE, &peer_retrans_req_cmd);
install_element(PEER_NODE, &n_sessions_cmd);
install_element(PEER_NODE, &wait_responses_cmd);
install_element(PEER_NODE, &session_cmd);
install_element(PEER_NODE, &session_endecaps_cmd);
install_node(&session_node, NULL);
install_element(SESSION_NODE, &c_sleep_cmd);
install_element(SESSION_NODE, &c_date_cmd);
install_element(SESSION_NODE, &session_tx_est_req_cmd);
install_element(SESSION_NODE, &session_tx_mod_req_cmd);
install_element(SESSION_NODE, &session_tx_del_req_cmd);
install_element(SESSION_NODE, &s_ue_cmd);
install_element(SESSION_NODE, &s_f_teid_cmd);
install_element(SESSION_NODE, &s_f_teid_choose_cmd);
install_ve_and_config(&c_gtp_local_cmd);
install_ve_and_config(&c_ue_ip_range_cmd);
install_ve_and_config(&gtp_flood_cmd);
install_node(&gtp_flood_node, NULL);
install_element(GTP_FLOOD_NODE, &gtpf_workers_cmd);
install_element(GTP_FLOOD_NODE, &gtpf_io_uring_queue_size_cmd);
install_element(GTP_FLOOD_NODE, &gtpf_flows_cmd);
install_element(GTP_FLOOD_NODE, &gtpf_packets_cmd);
install_element(GTP_FLOOD_NODE, &gtpf_payload_src_port_cmd);
install_element(GTP_FLOOD_NODE, &gtpf_payload_target_ip_cmd);
install_element(GTP_FLOOD_NODE, &gtpf_payload_target_port_cmd);
install_element(GTP_FLOOD_NODE, &gtpf_slew_cmd);
}
int pfcp_tool_vty_go_parent(struct vty *vty)
{
switch (vty->node) {
case GTP_FLOOD_NODE:
/* exiting a 'gtp flood' configuration, start the flooding */
pfcp_tool_gtp_flood_start();
break;
default:
break;
}
return 0;
}

111
src/osmo-pfcp-tool/range.c Normal file
View File

@@ -0,0 +1,111 @@
/*
* (C) 2024 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/socket.h>
#include <osmocom/core/utils.h>
#include <osmocom/pfcptool/range.h>
void range_val_set_int(struct range_val *rv, uint32_t val)
{
*rv = (struct range_val){
.buf = { (uint64_t)val, 0 },
.size = sizeof(val),
};
}
uint32_t range_val_get_int(const struct range_val *rv)
{
return rv->buf[0];
}
void range_val_set_addr(struct range_val *rv, const struct osmo_sockaddr *val)
{
*rv = (struct range_val){};
rv->size = osmo_sockaddr_to_octets((void *)rv->buf, sizeof(rv->buf), val);
#if !OSMO_IS_BIG_ENDIAN
for (int i = 0; i < rv->size / 2; i++) {
uint8_t *rvbuf = (void *)rv->buf;
uint8_t tmp = rvbuf[i];
rvbuf[i] = rvbuf[rv->size - 1 - i];
rvbuf[rv->size - 1 - i] = tmp;
}
#endif
}
void range_val_get_addr(struct osmo_sockaddr *dst, const struct range_val *rv)
{
void *buf;
#if OSMO_IS_BIG_ENDIAN
buf = rv->buf;
#else
int i;
uint8_t rev[sizeof(rv->buf)];
uint8_t *val = (void *)rv->buf;
for (i = 0; i < rv->size; i++)
rev[i] = val[rv->size - 1 - i];
buf = rev;
#endif
osmo_sockaddr_from_octets(dst, buf, rv->size);
}
void range_val_inc(struct range_val *rv)
{
uint64_t was = rv->buf[0];
rv->buf[0]++;
if (rv->buf[0] < was)
rv->buf[1]++;
}
int range_val_cmp(const struct range_val *a, const struct range_val *b)
{
int rc;
if (a == b)
return 0;
if (!a)
return -1;
if (!b)
return 1;
rc = OSMO_CMP(a->buf[0], b->buf[0]);
if (rc)
return rc;
rc = OSMO_CMP(a->buf[1], b->buf[1]);
if (rc)
return rc;
return OSMO_CMP(a->size, b->size);
}
void range_next(struct range *r)
{
if (range_val_cmp(&r->next, &r->first) < 0) {
r->next = r->first;
return;
}
if (range_val_cmp(&r->next, &r->last) >= 0) {
r->next = r->first;
r->wrapped = true;
return;
}
range_val_inc(&r->next);
}