aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPau Espin Pedrol <pespin@sysmocom.de>2021-09-23 13:34:20 +0200
committerPau Espin Pedrol <pespin@sysmocom.de>2021-10-04 14:06:51 +0200
commitae81195418ebed5280d0753d037df483bb914a0b (patch)
tree7a12d88d646501921868143ff04d6fd728490b43
parent6ee5fa939a4ae3176b839f6f33f8b68b690e3ff3 (diff)
Introduce program gtp-echo-responder
This is a small standalone program (under MIT license, hence cannot make use of libosmocore) whose only purpose is to answer GTPC (v1 and v2) Echo Request messages with Echo Reply ones, with information provided from the command line. A small python script companion is provided to easily test the program. Related: SYS#5598 Change-Id: Ibdd6d8f6920571db0c60cf8b3b25d541b15ad3f1
-rw-r--r--Makefile.am2
-rw-r--r--configure.ac1
-rw-r--r--contrib/osmo-ggsn.spec.in12
-rw-r--r--debian/control15
-rw-r--r--debian/copyright5
-rw-r--r--debian/gtp-echo-responder.install1
-rw-r--r--utils/Makefile.am3
-rw-r--r--utils/gtp_echo_responder.c470
-rwxr-xr-xutils/gtp_echo_responder_test.py111
9 files changed, 619 insertions, 1 deletions
diff --git a/Makefile.am b/Makefile.am
index f431bd9..7fb2529 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1,5 +1,5 @@
## Process this file with automake to produce Makefile.in
-SUBDIRS = lib gtp ggsn sgsnemu doc contrib tests
+SUBDIRS = lib gtp ggsn sgsnemu doc contrib utils tests
pkgconfigdir = $(libdir)/pkgconfig
pkgconfig_DATA = libgtp.pc
diff --git a/configure.ac b/configure.ac
index 2cf44fc..cfc9f21 100644
--- a/configure.ac
+++ b/configure.ac
@@ -257,6 +257,7 @@ AC_CONFIG_FILES([Makefile
lib/Makefile
intl/Makefile
po/Makefile
+ utils/Makefile
sgsnemu/Makefile
doc/manuals/Makefile
contrib/Makefile
diff --git a/contrib/osmo-ggsn.spec.in b/contrib/osmo-ggsn.spec.in
index 6d55af8..bee8fc1 100644
--- a/contrib/osmo-ggsn.spec.in
+++ b/contrib/osmo-ggsn.spec.in
@@ -61,6 +61,15 @@ libgtp implements the GPRS Tunneling Protocol between SGSN and GGSN.
This subpackage contains libraries and header files for developing
applications that want to make use of libgtp.
+%package -n gtp-echo-responder
+Summary: Small program answering GTP ECHO Request with GTP ECHO Response
+License: MIT
+Group: System/Libraries
+
+%description -n gtp-echo-responder
+Small program answering GTP ECHO Request with GTP ECHO Response for both GTPCv1
+and GTPCv2.
+
%prep
%setup -q
@@ -122,4 +131,7 @@ make %{?_smp_mflags} check || (find . -name testsuite.log -exec cat {} +)
%{_libdir}/libgtp.so
%{_libdir}/pkgconfig/libgtp.pc
+%files -n gtp-echo-responder
+%{_bindir}/gtp-echo-responder
+
%changelog
diff --git a/debian/control b/debian/control
index bbffd49..6d5cde5 100644
--- a/debian/control
+++ b/debian/control
@@ -38,6 +38,12 @@ Description: library implementing the GTP protocol between SGSN and GGSN
This library is part of OsmoGGSN and implements the GTP protocol between
SGSN (Serving GPRS support node) and GGSN.
+Package: gtp-echo-responder
+Architecture: any
+Depends: ${shlibs:Depends},
+ ${misc:Depends}
+Description: Small program answering GTP ECHO Request with GTP ECHO Response
+
Package: libgtp-dev
Architecture: any
Multi-Arch: same
@@ -63,6 +69,15 @@ Description: Debug symbols for OsmoGGSN
operators as the interface between the Internet and the rest of the
mobile network infrastructure.
+Package: gtp-echo-responder-dbg
+Section: debug
+Architecture: any
+Priority: extra
+Depends: ${shlibs:Depends}, ${misc:Depends}, gtp-echo-responder (= ${binary:Version})
+Multi-Arch: same
+Description: Debug symbols for gtp-echo-responder
+ Small program answering GTP ECHO Request with GTP ECHO Response.
+
Package: libgtp-dbg
Section: debug
Architecture: any
diff --git a/debian/copyright b/debian/copyright
index e2a4b2d..034c84d 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -16,6 +16,11 @@ Files: lib/getopt.c
Copyright: 1987-2001 Free Software Foundation, Inc.
License: LGPL-2.1+
+Files: utils/gtp_echo_responder.c
+ utils/gtp_echo_responder_test.py
+Copyright: 2021 sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+License: MIT
+
Files: debian/*
Copyright: 2010-2017 Harald Welte <laforge@gnumonks.org>
2016 Ruben Undheim <ruben.undheim@gmail.com>
diff --git a/debian/gtp-echo-responder.install b/debian/gtp-echo-responder.install
new file mode 100644
index 0000000..1d6d96f
--- /dev/null
+++ b/debian/gtp-echo-responder.install
@@ -0,0 +1 @@
+/usr/bin/gtp-echo-responder
diff --git a/utils/Makefile.am b/utils/Makefile.am
new file mode 100644
index 0000000..7ba0ff4
--- /dev/null
+++ b/utils/Makefile.am
@@ -0,0 +1,3 @@
+bin_PROGRAMS = gtp-echo-responder
+
+gtp_echo_responder_SOURCES = gtp_echo_responder.c
diff --git a/utils/gtp_echo_responder.c b/utils/gtp_echo_responder.c
new file mode 100644
index 0000000..e077c81
--- /dev/null
+++ b/utils/gtp_echo_responder.c
@@ -0,0 +1,470 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2021 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ * Author: Pau Espin Pedrol <pespin@sysmocom.de>
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+/* For more info see:
+ * 3GPP TS 29.060 (GTPv1 and GTPv0)
+ * 3GPP TS 29.274 (GTPv2C)
+ */
+
+#include "../config.h"
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <inttypes.h>
+#include <unistd.h>
+#include <limits.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <getopt.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <sys/select.h>
+#include <sys/socket.h>
+
+#define GTP1C_PORT 2123
+#define GTP_MSGTYPE_ECHO_REQ 1
+#define GTP_MSGTYPE_ECHO_RSP 2
+#define GTP1C_IE_RECOVERY 14
+#define GTP2C_IE_RECOVERY 3
+#define GTP2C_IE_NODE_FEATURES 152
+
+struct gtp1_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;
+ uint16_t seq;
+ uint8_t npdu;
+ uint8_t next;
+} __attribute__((packed));
+
+struct gtp2_hdr {
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+ uint8_t reserved:3, t:1, p:1, version:3;
+#else
+ uint8_t version:3, p:1, t:1, reserved:1;
+#endif
+ uint8_t type;
+ uint16_t length;
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+ uint32_t reserved2:8, seq:24;
+#else
+ uint8_t seq:24, reserved2:1;
+#endif
+} __attribute__((packed));
+
+struct gtp_echo_resp_state {
+ struct {
+ char laddr[INET6_ADDRSTRLEN];
+ uint8_t recovery_ctr;
+ uint8_t node_features;
+ } cfg;
+ struct sockaddr_storage laddr_gtpc;
+ int fd_gtpc;
+};
+
+struct gtp_echo_resp_state *g_st;
+
+static void print_usage(void)
+{
+ printf("Usage: gtp-echo-responder [-h] [-V] [-l listen_addr]\n");
+}
+
+static void print_help(void)
+{
+ printf(" Some useful help...\n"
+ " -h --help This help text\n"
+ " -V --version Print the version of gtp-echo-responder\n"
+ " -l --listen-addr Listend address for GTPCv1 and GTPCv2\n"
+ " -R --recovery-counter GTP Recovery Counter to transmit in GTP Echo Response message\n"
+ " -n --node-features GTPCv2 Node Features bitmask to transmit in GTP Echo Response message\n"
+ );
+}
+
+static void print_version(void)
+{
+ printf("gtp-echo-responder version %s\n", PACKAGE_VERSION);
+}
+
+static uint8_t parse_node_features_mask(const char *arg)
+{
+ unsigned long res;
+ char *end;
+ errno = 0;
+
+ res = strtoul(arg, &end, 0);
+ if ((errno == ERANGE && res == ULONG_MAX) || (errno && !res) ||
+ arg == end) {
+ fprintf(stderr, "Failed parsing Node Features bitmask: '%s'\n", arg);
+ exit(1);
+ }
+ if (res > 0xff) {
+ fprintf(stderr, "Failed parsing Node Features bitmask: '%s' > 0xFF\n", arg);
+ exit(1);
+ }
+ return (uint8_t)res;
+}
+static void handle_options(int argc, char **argv)
+{
+ while (1) {
+ int option_index = 0, c;
+ static struct option long_options[] = {
+ { "help", 0, 0, 'h' },
+ { "version", 0, 0, 'V' },
+ { "listen-addr", 1, 0, 'l'},
+ { "recovery-counter", 1, 0, 'R'},
+ { "node-features", 1, 0, 'N'},
+ { 0, 0, 0, 0 }
+ };
+
+ c = getopt_long(argc, argv, "hVl:R:N:", long_options, &option_index);
+ if (c == -1)
+ break;
+
+ switch (c) {
+ case 'h':
+ print_usage();
+ print_help();
+ exit(0);
+ case 'V':
+ print_version();
+ exit(0);
+ break;
+ case 'l':
+ strncpy(&g_st->cfg.laddr[0], optarg, sizeof(g_st->cfg.laddr));
+ g_st->cfg.laddr[sizeof(g_st->cfg.laddr) - 1] = '\0';
+ break;
+ case 'R':
+ g_st->cfg.recovery_ctr = (uint8_t)atoi(optarg);
+ break;
+ case 'N':
+ g_st->cfg.node_features = parse_node_features_mask(optarg);
+ break;
+ }
+ }
+}
+
+static int init_socket(void)
+{
+ struct in_addr addr;
+ struct in6_addr addr6;
+ struct sockaddr_in *saddr;
+ struct sockaddr_in6 *saddr6;
+ int family;
+
+ if (inet_pton(AF_INET6, g_st->cfg.laddr, &addr6) == 1) {
+ family = AF_INET6;
+ saddr6 = (struct sockaddr_in6 *)&g_st->laddr_gtpc;
+ saddr6->sin6_family = family;
+ saddr6->sin6_port = htons(GTP1C_PORT);
+ memcpy(&saddr6->sin6_addr, &addr6, sizeof(addr6));
+ } else if (inet_pton(AF_INET, g_st->cfg.laddr, &addr) == 1) {
+ family = AF_INET;
+ saddr = (struct sockaddr_in *)&g_st->laddr_gtpc;
+ saddr->sin_family = family;
+ saddr->sin_port = htons(GTP1C_PORT);
+ memcpy(&saddr->sin_addr, &addr, sizeof(addr));
+ } else {
+ fprintf(stderr, "Failed parsing address %s\n", g_st->cfg.laddr);
+ return -1;
+ }
+
+ if ((g_st->fd_gtpc = socket(family, SOCK_DGRAM, 0)) < 0) {
+ fprintf(stderr, "socket() failed: %s\n", strerror(errno));
+ return -2;
+ }
+
+ if (bind(g_st->fd_gtpc, (struct sockaddr *)&g_st->laddr_gtpc, sizeof(g_st->laddr_gtpc)) < 0) {
+ fprintf(stderr, "bind() failed: %s\n", strerror(errno));
+ return -3;
+ }
+
+ return 0;
+}
+
+static const char *sockaddr2str(const struct sockaddr *saddr)
+{
+ static char _rem_addr_str[INET6_ADDRSTRLEN];
+ struct sockaddr_in *saddr4;
+ struct sockaddr_in6 *saddr6;
+
+ switch (saddr->sa_family) {
+ case AF_INET6:
+ saddr6 = (struct sockaddr_in6 *)saddr;
+ if (!inet_ntop(saddr6->sin6_family, &saddr6->sin6_addr, _rem_addr_str, sizeof(_rem_addr_str)))
+ strcpy(_rem_addr_str, "unknown");
+ return _rem_addr_str;
+ case AF_INET:
+ saddr4 = (struct sockaddr_in *)saddr;
+ if (!inet_ntop(saddr4->sin_family, &saddr4->sin_addr, _rem_addr_str, sizeof(_rem_addr_str)))
+ strcpy(_rem_addr_str, "unknown");
+ return _rem_addr_str;
+ default:
+ strcpy(_rem_addr_str, "unknown-family");
+ return _rem_addr_str;
+ }
+}
+
+static int write_cb(int fd, const uint8_t *buf, size_t buf_len, const struct sockaddr *rem_saddr)
+{
+ ssize_t rc;
+
+ rc = sendto(fd, buf, buf_len, 0, rem_saddr, sizeof(struct sockaddr_storage));
+ if (rc < 0) {
+ fprintf(stderr, "sendto() failed: %s\n", strerror(errno));
+ return -1;
+ }
+ if (rc != buf_len) {
+ fprintf(stderr, "sendto() short write: %zd vs exp %zu\n", rc, buf_len);
+ return -1;
+ }
+ return 0;
+}
+
+static int gen_gtpc1_echo_rsp(uint8_t *buf, struct gtp1_hdr *echo_req)
+{
+ int offset = 0;
+ struct gtp1_hdr *echo_rsp = (struct gtp1_hdr *)buf;
+ unsigned exp_hdr_len = (echo_req->s || echo_req->pn || echo_req->e) ? 12 : 8;
+
+ memcpy(echo_rsp, echo_req, exp_hdr_len);
+ echo_rsp->type = GTP_MSGTYPE_ECHO_RSP;
+ offset = exp_hdr_len;
+ buf[offset++] = GTP1C_IE_RECOVERY;
+ buf[offset++] = g_st->cfg.recovery_ctr;
+
+ /* Update Length */
+ echo_rsp->length = htons(offset - 8);
+ return offset;
+}
+
+static int gen_gtpc2_echo_rsp(uint8_t *buf, struct gtp2_hdr *echo_req)
+{
+ int offset = 0;
+ struct gtp1_hdr *echo_rsp = (struct gtp1_hdr *)buf;
+ unsigned exp_hdr_len = 8;
+
+ memcpy(echo_rsp, echo_req, exp_hdr_len);
+ echo_rsp->type = GTP_MSGTYPE_ECHO_RSP;
+ offset = exp_hdr_len;
+
+ /* 3GPP TS 29.274 sec 8.5 Recovery (Restart Counter) */
+ buf[offset++] = GTP2C_IE_RECOVERY;
+ buf[offset++] = 0; /* IE Length (high) */
+ buf[offset++] = 1; /* IE Length (low) */
+ buf[offset++] = 0; /* Spare=0 | Instance=0 (Table 7.1.1-1) */
+ buf[offset++] = g_st->cfg.recovery_ctr;
+
+ /* 3GPP TS 29.274 sec 8.83 Node Features */
+ if (g_st->cfg.node_features > 0) {
+ buf[offset++] = GTP2C_IE_NODE_FEATURES;
+ buf[offset++] = 0; /* IE Length (high) */
+ buf[offset++] = 1; /* IE Length (low) */
+ buf[offset++] = 0; /* Spare=0 | Instance=0 (Table 7.1.1-1) */
+ buf[offset++] = g_st->cfg.node_features;
+ }
+
+ /* Update Length */
+ echo_rsp->length = htons(offset - 4);
+ return offset;
+}
+
+static int rx_gtpc1_echo_req(struct gtp1_hdr *echo_req, unsigned buf_len, const struct sockaddr *rem_saddr)
+{
+ int rc;
+ const size_t tx_buf_len = buf_len + 128; /* Leave some extra room */
+ uint8_t *tx_buf = alloca(tx_buf_len);
+
+ printf("Rx GTPCv1_ECHO_REQ from %s, Tx GTPCv1_ECHO_RSP\n", sockaddr2str(rem_saddr));
+
+ memset(tx_buf, 0, tx_buf_len);
+ rc = gen_gtpc1_echo_rsp(tx_buf, echo_req);
+ return write_cb(g_st->fd_gtpc, tx_buf, rc, rem_saddr);
+}
+
+static int rx_gtpc1(struct gtp1_hdr *hdr, unsigned buf_len, const struct sockaddr *rem_saddr)
+{
+ unsigned exp_hdr_len = (hdr->s || hdr->pn || hdr->e) ? 12 : 8;
+ unsigned pdu_len;
+
+ if (buf_len < exp_hdr_len) {
+ fprintf(stderr, "GTPCv1 packet size smaller than header! %u < exp %u\n", buf_len, exp_hdr_len);
+ return -1;
+ }
+
+ pdu_len = ntohs(hdr->length);
+ if (buf_len < 8 + pdu_len) {
+ fprintf(stderr, "GTPCv1 packet size smaller than announced! %u < exp %u\n", buf_len, 8 + pdu_len);
+ return -1;
+ }
+
+ if (hdr->pt != 1) {
+ fprintf(stderr, "GTPCv1 Protocol Type GTP' not supported!\n");
+ return -1;
+ }
+
+ switch (hdr->type) {
+ case GTP_MSGTYPE_ECHO_REQ:
+ return rx_gtpc1_echo_req(hdr, buf_len, rem_saddr);
+ default:
+ fprintf(stderr, "Silently ignoring unexpected packet of type %u\n", hdr->type);
+ return 0;
+ }
+}
+
+static int rx_gtpc2_echo_req(struct gtp2_hdr *echo_req, unsigned buf_len, const struct sockaddr *rem_saddr)
+{
+ int rc;
+ const size_t tx_buf_len = buf_len + 128; /* Leave some extra room */
+ uint8_t *tx_buf = alloca(tx_buf_len);
+
+ if (echo_req->t) {
+ fprintf(stderr, "GTPCv2 ECHO message should contain T=0!\n");
+ return -1;
+ }
+
+ printf("Rx GTPCv2_ECHO_REQ from %s, Tx GTPCv2_ECHO_RSP\n", sockaddr2str(rem_saddr));
+
+ memset(tx_buf, 0, tx_buf_len);
+ rc = gen_gtpc2_echo_rsp(tx_buf, echo_req);
+ return write_cb(g_st->fd_gtpc, tx_buf, rc, rem_saddr);
+}
+
+static int rx_gtpc2(struct gtp2_hdr *hdr, unsigned buf_len, const struct sockaddr *rem_saddr)
+{
+ unsigned exp_hdr_len = hdr->t ? 12 : 8;
+ unsigned pdu_len;
+
+ if (hdr->p) {
+ fprintf(stderr, "GTPCv2 piggybacked message not supported!\n");
+ return -1;
+ }
+
+ if (buf_len < exp_hdr_len) {
+ fprintf(stderr, "GTPCv2 packet size smaller than header! %u < exp %u\n", buf_len, exp_hdr_len);
+ return -1;
+ }
+
+ pdu_len = ntohs(hdr->length);
+ /* 3GPP TS 29.274 sec 5.5.1: "Octets 3 to 4 represent the Message Length
+ * field. This field shall indicate the length of the message in octets
+ * excluding the mandatory part of the GTP-C header (the first 4
+ * octets). The TEID (if present) and the Sequence Number shall be
+ * included in the length count" */
+ if (buf_len < 4 + pdu_len) {
+ fprintf(stderr, "GTPCv2 packet size smaller than announced! %u < exp %u\n", buf_len, 4 + pdu_len);
+ return -1;
+ }
+
+ switch (hdr->type) {
+ case GTP_MSGTYPE_ECHO_REQ:
+ return rx_gtpc2_echo_req(hdr, buf_len, rem_saddr);
+ default:
+ fprintf(stderr, "Silently ignoring unexpected packet of type %u\n", hdr->type);
+ return 0;
+ }
+}
+
+static int read_cb(int fd)
+{
+ ssize_t sz;
+ uint8_t buf[4096];
+ struct sockaddr_storage rem_saddr;
+ socklen_t rem_saddr_len = sizeof(rem_saddr);
+ struct gtp1_hdr *hdr1;
+
+ if ((sz = recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr *)&rem_saddr, &rem_saddr_len)) < 0) {
+ fprintf(stderr, "recvfrom() failed: %s\n", strerror(errno));
+ return -1;
+ }
+ if (sz == 0) {
+ fprintf(stderr, "recvfrom() read zero bytes!\n");
+ return -1;
+ }
+
+ hdr1 = (struct gtp1_hdr *)&buf[0];
+ switch (hdr1->version) {
+ case 1:
+ return rx_gtpc1(hdr1, sz, (const struct sockaddr *)&rem_saddr);
+ case 2:
+ return rx_gtpc2((struct gtp2_hdr *)&buf[0], sz, (const struct sockaddr *)&rem_saddr);
+ default:
+ fprintf(stderr, "Rx GTPv%u: not supported (flags=0x%x)\n", hdr1->version, buf[0]);
+ return -1;
+ }
+}
+
+static int loop(void)
+{
+ int rc;
+ fd_set rfds;
+ int nfds;
+
+ while (true) {
+ FD_ZERO(&rfds);
+ FD_SET(g_st->fd_gtpc, &rfds);
+ nfds = g_st->fd_gtpc + 1;
+ rc = select(nfds, &rfds, NULL, NULL, NULL);
+ if (rc == 0)
+ continue;
+ if (rc < 0) {
+ fprintf(stderr, "select() failed: %s\n", strerror(errno));
+ return -1;
+ }
+
+ if (FD_ISSET(g_st->fd_gtpc, &rfds))
+ read_cb(g_st->fd_gtpc);
+ }
+}
+
+int main(int argc, char **argv)
+{
+ g_st = calloc(1, sizeof(struct gtp_echo_resp_state));
+
+ strcpy(g_st->cfg.laddr, "::");
+
+ handle_options(argc, argv);
+
+ printf("Listening on: %s\n", g_st->cfg.laddr);
+
+ if (init_socket() < 0)
+ exit(1);
+
+ printf("Socket bound successfully, listening for requests...\n");
+
+ if (loop() < 0)
+ exit(1);
+
+ return 0;
+}
diff --git a/utils/gtp_echo_responder_test.py b/utils/gtp_echo_responder_test.py
new file mode 100755
index 0000000..632d23b
--- /dev/null
+++ b/utils/gtp_echo_responder_test.py
@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+# MIT License
+#
+# Copyright (c) 2021 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+# Author: Pau Espin Pedrol <pespin@sysmocom.de>
+#
+# SPDX-License-Identifier: MIT
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice (including the next
+# paragraph) shall be included in all copies or substantial portions of the
+# Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+
+import socket
+import argparse
+import struct
+from ipaddress import ip_address, IPv4Address
+
+GTP1C_PORT = 2123
+BUF_SIZE = 4096
+
+GTP_HDRv1_FLAG_PN = (1<<0)
+GTP_HDRv1_FLAG_S = (1<<1)
+GTP_HDRv1_FLAG_E = (1<<2)
+GTP_HDRv1_PT_GTP = (1<<4)
+GTP_HDRv1_VER_GTP1 = (1<<5)
+
+GTP_HDRv2_FLAG_T = (1<<3)
+GTP_HDRv2_FLAG_P = (1<<4)
+GTP_HDRv2_VER_GTP2 = (2<<5)
+
+def gen_gtpc_v1_hdr(flags, type, length, tei, seq=0, npdu=0, next=0):
+ spare = 0
+ if (flags & (GTP_HDRv1_FLAG_PN|GTP_HDRv1_FLAG_S|GTP_HDRv1_FLAG_E)):
+ #long format
+ length += 4
+ d = struct.pack('!BBHIHBB', flags, type, length, tei, seq, npdu, next)
+ else:
+ #short format
+ d = struct.pack('!BBHI', flags, type, length, tei)
+ return d
+
+def gen_gtpc_v2_hdr(flags, type, length, tei=0, seq=0):
+ spare = 0
+ if (flags & (GTP_HDRv2_FLAG_T)):
+ #long format, with TEI
+ length += 4 + 4
+ d = struct.pack('!BBHIHBB', flags, type, length, tei, seq >> 8, seq & 0xff, spare)
+ else:
+ #short format
+ length += 4
+ d = struct.pack('!BBHHBB', flags, type, length, seq >> 8, seq & 0xff, spare)
+ return d
+
+def gen_gtpc_v1_echo_req(tei=0, append_flags=0, seq=0, npdu=0, next=0):
+ return gen_gtpc_v1_hdr(GTP_HDRv1_VER_GTP1 | GTP_HDRv1_PT_GTP | append_flags, 1, 0, tei, seq, npdu, next)
+
+def gen_gtpc_v2_echo_req(append_flags=0, seq=0, recovery=0, node_features=-1):
+ length = 0
+ payload = b''
+ if (recovery > 0):
+ recovery_ie = struct.pack('!BHBB', 3, 1, 0, recovery)
+ payload += recovery_ie
+ length += len(recovery_ie)
+ if (node_features > 0):
+ node_features_ie = struct.pack('!BHBB', 152, 1, 0, node_features)
+ payload += node_features_ie
+ length += len(node_features_ie)
+ return gen_gtpc_v2_hdr(GTP_HDRv2_VER_GTP2 | append_flags, 1, length, 0, seq) + payload
+
+def tx_rx(sk, rem_addr, tx_buf, exp_rx = True):
+ print('Tx ECHO_REQ to %r: %r' % (repr(rem_addr), repr(tx_buf)))
+ sk.sendto(tx_buf, rem_addr)
+ if exp_rx:
+ rx_buf = sk.recvfrom(BUF_SIZE)
+ msg = "Message from Server {}".format(rx_buf)
+ print(msg)
+
+if __name__ == '__main__':
+ p = argparse.ArgumentParser(description='Tester for gtp-echo-recorder.')
+ p.add_argument('-l', '--local-address', default='127.0.0.2', help="Local GTP address")
+ p.add_argument('-r', '--remote-address', default='127.0.0.1', help="Remote GTP address")
+ args = p.parse_args()
+
+ print('Binding socket on %r...' % repr((args.local_address, GTP1C_PORT)))
+ family = socket.AF_INET if type(ip_address(args.local_address)) is IPv4Address else socket.AF_INET6
+ sk = socket.socket(family=family, type=socket.SOCK_DGRAM)
+ sk.bind((args.local_address, GTP1C_PORT));
+
+ rem_addr = (args.remote_address, GTP1C_PORT)
+
+ tx_rx(sk, rem_addr, gen_gtpc_v1_echo_req())
+ tx_rx(sk, rem_addr, gen_gtpc_v1_echo_req(1, GTP_HDRv1_FLAG_S, seq=67))
+ tx_rx(sk, rem_addr, gen_gtpc_v2_echo_req(0, seq=300, recovery=-1, node_features=-1))
+ tx_rx(sk, rem_addr, gen_gtpc_v2_echo_req(0, seq=20, recovery=99, node_features=-1))
+ tx_rx(sk, rem_addr, gen_gtpc_v2_echo_req(0, seq=20, recovery=100, node_features=0xbb))