I can talk all day about atomic commits, but chunks of gtphub have transformed twice over (that's me figuring out how to map and resolve GTP data elements), so that half the patches are obsoleted by later ones, in a hard-to-rebase way.
So here is a joint patch of gtphub for review. If anyone requests it, I will gladly separate it into a handful of patches adding each subsection / family of API at a time, to better show where each part reaches. Just ask...
Many thanks for any reviews!
~Neels
Neels Hofmeyr (1): Add GTP hub (code bomb).
openbsc/.gitignore | 2 + openbsc/configure.ac | 1 + openbsc/include/openbsc/Makefile.am | 1 + openbsc/include/openbsc/debug.h | 1 + openbsc/include/openbsc/gtphub.h | 345 +++++ openbsc/include/openbsc/vty.h | 1 + openbsc/src/gprs/Makefile.am | 6 + openbsc/src/gprs/gtphub.c | 1794 +++++++++++++++++++++++ openbsc/src/gprs/gtphub_main.c | 283 ++++ openbsc/src/gprs/gtphub_sep.c | 26 + openbsc/src/gprs/gtphub_vty.c | 258 ++++ openbsc/tests/Makefile.am | 2 +- openbsc/tests/gtphub/Makefile.am | 20 + openbsc/tests/gtphub/gtphub_nc_test.gtphub.conf | 5 + openbsc/tests/gtphub/gtphub_nc_test.ok | 7 + openbsc/tests/gtphub/gtphub_nc_test.sh | 85 ++ openbsc/tests/gtphub/gtphub_test.c | 675 +++++++++ openbsc/tests/gtphub/gtphub_test.ok | 3 + openbsc/tests/gtphub/hex2bin.py | 13 + openbsc/tests/testsuite.at | 12 + 20 files changed, 3539 insertions(+), 1 deletion(-) create mode 100644 openbsc/include/openbsc/gtphub.h create mode 100644 openbsc/src/gprs/gtphub.c create mode 100644 openbsc/src/gprs/gtphub_main.c create mode 100644 openbsc/src/gprs/gtphub_sep.c create mode 100644 openbsc/src/gprs/gtphub_vty.c create mode 100644 openbsc/tests/gtphub/Makefile.am create mode 100644 openbsc/tests/gtphub/gtphub_nc_test.gtphub.conf create mode 100644 openbsc/tests/gtphub/gtphub_nc_test.ok create mode 100755 openbsc/tests/gtphub/gtphub_nc_test.sh create mode 100644 openbsc/tests/gtphub/gtphub_test.c create mode 100644 openbsc/tests/gtphub/gtphub_test.ok create mode 100755 openbsc/tests/gtphub/hex2bin.py
First steps towards a new GTP hub. The aim is to mux GTP connections, so that multiple SGSN <--> GGSN links can pass through a single point. Background: allow having more than one SGSN, possibly in various remote locations.
The recent addition of OAP to GSUP is related to the same background idea.
(This is a collapsed patch of various changes that do not make sense to review in chronological order anymore, since a lot of it has thorougly transmorphed after it was first committed.)
Sponsored-by: On-Waves ehf --- openbsc/.gitignore | 2 + openbsc/configure.ac | 1 + openbsc/include/openbsc/Makefile.am | 1 + openbsc/include/openbsc/debug.h | 1 + openbsc/include/openbsc/gtphub.h | 345 +++++ openbsc/include/openbsc/vty.h | 1 + openbsc/src/gprs/Makefile.am | 6 + openbsc/src/gprs/gtphub.c | 1794 +++++++++++++++++++++++ openbsc/src/gprs/gtphub_main.c | 283 ++++ openbsc/src/gprs/gtphub_sep.c | 26 + openbsc/src/gprs/gtphub_vty.c | 258 ++++ openbsc/tests/Makefile.am | 2 +- openbsc/tests/gtphub/Makefile.am | 20 + openbsc/tests/gtphub/gtphub_nc_test.gtphub.conf | 5 + openbsc/tests/gtphub/gtphub_nc_test.ok | 7 + openbsc/tests/gtphub/gtphub_nc_test.sh | 85 ++ openbsc/tests/gtphub/gtphub_test.c | 675 +++++++++ openbsc/tests/gtphub/gtphub_test.ok | 3 + openbsc/tests/gtphub/hex2bin.py | 13 + openbsc/tests/testsuite.at | 12 + 20 files changed, 3539 insertions(+), 1 deletion(-) create mode 100644 openbsc/include/openbsc/gtphub.h create mode 100644 openbsc/src/gprs/gtphub.c create mode 100644 openbsc/src/gprs/gtphub_main.c create mode 100644 openbsc/src/gprs/gtphub_sep.c create mode 100644 openbsc/src/gprs/gtphub_vty.c create mode 100644 openbsc/tests/gtphub/Makefile.am create mode 100644 openbsc/tests/gtphub/gtphub_nc_test.gtphub.conf create mode 100644 openbsc/tests/gtphub/gtphub_nc_test.ok create mode 100755 openbsc/tests/gtphub/gtphub_nc_test.sh create mode 100644 openbsc/tests/gtphub/gtphub_test.c create mode 100644 openbsc/tests/gtphub/gtphub_test.ok create mode 100755 openbsc/tests/gtphub/hex2bin.py
diff --git a/openbsc/.gitignore b/openbsc/.gitignore index ca73db6..fc3d0bf 100644 --- a/openbsc/.gitignore +++ b/openbsc/.gitignore @@ -53,6 +53,7 @@ src/utils/isdnsync src/nat/bsc_nat src/gprs/osmo-sgsn src/gprs/osmo-gbproxy +src/gprs/osmo-gtphub src/osmo-bsc_nat/osmo-bsc_nat
#tests @@ -78,6 +79,7 @@ tests/mgcp/mgcp_transcoding_test tests/sgsn/sgsn_test tests/subscr/subscr_test tests/oap/oap_test +tests/gtphub/gtphub_test
tests/atconfig tests/atlocal diff --git a/openbsc/configure.ac b/openbsc/configure.ac index 8b7ce62..098e5b4 100644 --- a/openbsc/configure.ac +++ b/openbsc/configure.ac @@ -210,6 +210,7 @@ AC_OUTPUT( tests/sgsn/Makefile tests/subscr/Makefile tests/oap/Makefile + tests/gtphub/Makefile doc/Makefile doc/examples/Makefile Makefile) diff --git a/openbsc/include/openbsc/Makefile.am b/openbsc/include/openbsc/Makefile.am index 8a074c2..15c38d1 100644 --- a/openbsc/include/openbsc/Makefile.am +++ b/openbsc/include/openbsc/Makefile.am @@ -18,6 +18,7 @@ noinst_HEADERS = abis_nm.h abis_rsl.h db.h gsm_04_08.h gsm_data.h \ gprs_gb_parse.h smpp.h meas_feed.h gprs_gsup_messages.h \ gprs_gsup_client.h bsc_msg_filter.h \ oap.h oap_messages.h + gtphub.h
openbsc_HEADERS = gsm_04_08.h meas_rep.h bsc_api.h openbscdir = $(includedir)/openbsc diff --git a/openbsc/include/openbsc/debug.h b/openbsc/include/openbsc/debug.h index 19d8fc2..189ca47 100644 --- a/openbsc/include/openbsc/debug.h +++ b/openbsc/include/openbsc/debug.h @@ -33,6 +33,7 @@ enum { DCTRL, DSMPP, DFILTER, + DGTPHUB, Debug_LastEntry, };
diff --git a/openbsc/include/openbsc/gtphub.h b/openbsc/include/openbsc/gtphub.h new file mode 100644 index 0000000..ebd4058 --- /dev/null +++ b/openbsc/include/openbsc/gtphub.h @@ -0,0 +1,345 @@ +/* GTP Hub Implementation */ + +/* (C) 2015 by sysmocom s.f.m.c. GmbH info@sysmocom.de + * All Rights Reserved + * + * Author: Neels Hofmeyr + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation; either version 3 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +#pragma once + +#include <stdint.h> +#include <sys/socket.h> + +#include <osmocom/core/select.h> +#include <osmocom/core/timer.h> + + +/* support */ + +/* TODO move to osmocom/core/socket.c ? */ +#include <netdb.h> /* for IPPROTO_* etc */ +struct osmo_sockaddr { + struct sockaddr_storage a; + socklen_t l; +}; + +/* TODO move to osmocom/core/socket.c ? */ +/*! \brief Initialize a sockaddr + * \param[out] addr Valid osmo_sockaddr pointer to write result to + * \param[in] family Address Family like AF_INET, AF_INET6, AF_UNSPEC + * \param[in] type Socket type like SOCK_DGRAM, SOCK_STREAM + * \param[in] proto Protocol like IPPROTO_TCP, IPPROTO_UDP + * \param[in] host Remote host name or IP address in string form + * \param[in] port Remote port number in host byte order + * \returns 0 on success, otherwise an error code (from getaddrinfo()). + * + * Copy the first result from a getaddrinfo() call with the given parameters to + * *addr and *addr_len. On error, do not change *addr and return nonzero. + */ +int osmo_sockaddr_init(struct osmo_sockaddr *addr, + uint16_t family, uint16_t type, uint8_t proto, + const char *host, uint16_t port); + +/* Conveniently pass AF_UNSPEC, SOCK_DGRAM and IPPROTO_UDP to + * osmo_sockaddr_init(). */ +static inline int osmo_sockaddr_init_udp(struct osmo_sockaddr *addr, + const char *host, uint16_t port) +{ + return osmo_sockaddr_init(addr, AF_UNSPEC, SOCK_DGRAM, IPPROTO_UDP, host, port); +} + +/*! \brief convert sockaddr to human readable string. + * \param[out] addr_str Valid pointer to a buffer of length addr_str_len. + * \param[in] addr_str_len Size of buffer addr_str points at. + * \param[out] port_str Valid pointer to a buffer of length port_str_len. + * \param[in] port_str_len Size of buffer port_str points at. + * \param[in] addr Binary representation as returned by osmo_sockaddr_init(). + * \param[in] flags flags as passed to getnameinfo(). + * \returns 0 on success, an error code on error. + * + * Return the IPv4 or IPv6 address string and the port (a.k.a. service) string + * representations of the given struct osmo_sockaddr in two caller provided + * char buffers. Flags of (NI_NUMERICHOST | NI_NUMERICSERV) return numeric + * address and port. Either one of addr_str or port_str may be NULL, in which + * case nothing is returned there. + * + * See also osmo_sockaddr_to_str() (less flexible, but much more convenient). */ +int osmo_sockaddr_to_strs(char *addr_str, size_t addr_str_len, + char *port_str, size_t port_str_len, + const struct osmo_sockaddr *addr, + int flags); + + +/*! \brief conveniently concatenate the parts returned by osmo_sockaddr_to_strs(). + * \param[in] addr Binary representation as returned by osmo_sockaddr_init(). + * \param[in] buf A buffer to use for string operations. + * \param[in] buf_len Length of the buffer. + * \returns Address string (in buffer). + * + * Compose a string of the numeric IP-address and port represented by *addr of + * the form "<ip-addr> port <port>". The returned string is valid until the + * next invocation of this function. + */ +const char *osmo_sockaddr_to_strb(const struct osmo_sockaddr *addr, + char *buf, size_t buf_len); + +/*! \brief conveniently return osmo_sockaddr_to_strb() in a static buffer. + * \param[in] addr Binary representation as returned by osmo_sockaddr_init(). + * \returns Address string in static buffer. + * + * See osmo_sockaddr_to_strb(). + * + * Note: only one osmo_sockaddr_to_str() call will work per print/log + * statement. For two or more, use osmo_sockaddr_to_strb() with a separate + * buffer each. + */ +const char *osmo_sockaddr_to_str(const struct osmo_sockaddr *addr); + +/*! \brief compare two osmo_sockaddr. + * \param[in] a The first address to compare. + * \param[in] b The other address to compare. + * \returns 0 if equal, otherwise -1 or 1. + */ +int osmo_sockaddr_cmp(const struct osmo_sockaddr *a, const struct osmo_sockaddr *b); + +/*! \brief Overwrite *dst with *src. + * Like memcpy(), but copy only the valid bytes. */ +void osmo_sockaddr_copy(struct osmo_sockaddr *dst, const struct osmo_sockaddr *src); + + +/* general */ + +enum gtphub_port_idx { + GTPH_PORT_CTRL = 0, + GTPH_PORT_USER = 1, + GTPH_PORT_N +}; + +extern const char* const gtphub_port_idx_names[GTPH_PORT_N]; + + +/* A number map assigns a "random" mapped number to each user provided number. + * If the same number is requested multiple times, the same mapped number is + * returned. + * + * Number maps plug into possibly shared pools and expiry queues, for example: + * + * mapA -----------+-> pool1 <-+-- mapB + * {10->1, 11->5} | {1, 2, 3, ...} | {10->2, 11->3} + * | | + * | | + * /-> -> expiry1 <-/ + * | (30 seconds) + * | + * mapC -------+-----> pool2 <-+-- mapD + * {10->1, 11->3} {1, 2, 3, ...} | {10->2, 11->5} + * | + * expiry2 <-/ + * (60 seconds) + * + * A map contains mappings ("10->1"). Each map needs a number pool, which can + * be shared with other maps. Each new mapping receives a number from the pool, + * which is then unavailable to any other map using the same pool. + * + * A map may point at an expiry queue, in which case all mappings added to it + * are also appended to the expiry queue (using a separate llist entry in the + * mapping). Any number of maps may submit to the same expiry queue, if they + * desire the same expiry timeout. An expiry queue stores the mappings in + * chronological order, so that expiry checking is needed only from the start + * of the queue; hence only mappings with identical expiry timeout can be added + * to the same expiry queue. Upon expiry, a mapping is dropped from the map it + * was submitted at. nr_map_expiry_tick() needs to be called regularly for + * each expiry queue. + * + * A nr_mapping can be embedded in a larger struct: each mapping can have a + * distinct destructor (del_cb), and each del_cb can figure out the container + * struct's address and free that upon expiry or manual deletion. So in expiry + * queues (and even maps), mappings of different container types can be mixed. + * This can help to drastically reduce the amount of unnecessary visits during + * expiry checking, for the case that no expiry is pending. An expiry queue + * always knows which mappings to expire next, because they are right at the + * start of its list. + * + * Mapping allocation and a del_cb are provided by the caller. If del_cb is + * NULL, no deallocation will be done (allowing statically allocated entries). + */ +/* TODO at some point I thought the allocation & del_cb complexity was + * needed/helpful, but by now it seems like overkill. Maybe lose that again. */ + +typedef int nr_t; + +/* Generator for unused numbers. So far this counts upwards from zero, but the + * implementation may change in the future. Treat this like an opaque struct. + * If this becomes random, the tests need to be fixed. */ +struct nr_pool { + nr_t last_nr; + /* TODO add min, max, for safe wrapping */ +}; + +struct nr_mapping; +typedef void (*nr_mapping_del_cb_t)(struct nr_mapping *); + +struct nr_mapping { + struct llist_head entry; + struct llist_head expiry_entry; + time_t expiry; + + void *origin; + nr_t orig; + nr_t repl; + + nr_mapping_del_cb_t del_cb; +}; + +struct nr_map_expiry { + int expiry_in_seconds; + struct llist_head mappings; +}; + +struct nr_map { + struct nr_pool *pool; /* multiple nr_maps can share a nr_pool. */ + struct nr_map_expiry *expiry; + struct llist_head mappings; +}; + + +void nr_pool_init(struct nr_pool *pool); + +/* Return the next unused number from the nr_pool. */ +nr_t nr_pool_next(struct nr_pool *pool); + +/* Initialize the nr_mapping to zero/empty values. */ +void nr_mapping_init(struct nr_mapping *mapping); + +/* Remove the given mapping from its parent map and expiry queue, and call + * mapping->del_cb, if set. */ +void nr_mapping_del(struct nr_mapping *mapping); + +/* Initialize an expiry queue exq. */ +void nr_map_expiry_init(struct nr_map_expiry *exq, int expiry_in_seconds); + +/* Add a new mapping, or restart the expiry timeout for an already listed mapping. */ +void nr_map_expiry_add(struct nr_map_expiry *exq, struct nr_mapping *mapping, time_t now); + +/* Carry out due expiry of mappings. Must be invoked regularly. + * 'now' is the current clock count in seconds and must correspond to the clock + * count passed to nr_map_add(). A monotonous clock counter should be used. */ +int nr_map_expiry_tick(struct nr_map_expiry *exq, time_t now); + +/* Initialize an (already allocated) nr_map, and set the map's number pool. + * Multiple nr_map instances may use the same nr_pool. Set the nr_map's expiry + * queue to exq, so that all added mappings are automatically expired after the + * time configured in exq. exq may be NULL to disable automatic expiry. */ +void nr_map_init(struct nr_map *map, struct nr_pool *pool, + struct nr_map_expiry *exq); + +/* Add a new entry to the map. mapping->orig, mapping->origin and + * mapping->del_cb must be set before calling this function. The remaining + * fields of *mapping will be overwritten. mapping->repl is set to the next + * available mapped number from map->pool. 'now' is the current clock count in + * seconds; if no map->expiry is used, just pass 0 for 'now'. */ +void nr_map_add(struct nr_map *map, struct nr_mapping *mapping, + time_t now); + +/* Return a known mapping from nr_orig and the given origin. If nr_orig is + * unknown, return NULL. */ +struct nr_mapping *nr_map_get(const struct nr_map *map, + void *origin, nr_t nr_orig); + +/* Return a known mapping to nr_repl. If nr_repl is unknown, return NULL. */ +struct nr_mapping *nr_map_get_inv(const struct nr_map *map, nr_t nr_repl); + +/* Remove all mappings from map. */ +void nr_map_clear(struct nr_map *map); + +/* Return 1 if map has no entries, 0 otherwise. */ +int nr_map_empty(const struct nr_map *map); + + +/* config */ + +static const int GTPH_SEQ_MAPPING_EXPIRY_SECS = 30; /* TODO is there a spec for this? */ +static const int GTPH_TEI_MAPPING_EXPIRY_MINUTES = 6 * 60; /* TODO is there a spec for this? */ + +struct gtphub_cfg_addr { + const char *addr_str; + uint16_t port; +}; + +struct gtphub_cfg_bind { + struct gtphub_cfg_addr bind; +}; + +struct gtphub_cfg { + struct gtphub_cfg_bind to_sgsns[GTPH_PORT_N]; + struct gtphub_cfg_bind to_ggsns[GTPH_PORT_N]; + struct gtphub_cfg_addr sgsn_proxy[GTPH_PORT_N]; + struct gtphub_cfg_addr ggsn_proxy[GTPH_PORT_N]; +}; + + +/* state */ + +struct gtphub_peer { + struct llist_head entry; + + struct osmo_sockaddr addr; + struct nr_map tei_map; + struct nr_pool seq_pool; + struct nr_map seq_map; + unsigned int ref_count; /* references from other peers' seq_maps */ + struct gtphub_peer *association[GTPH_PORT_N]; /* One points to "this" */ +}; + +struct gtphub_bind { + struct osmo_fd ofd; + struct nr_pool tei_pool; + + /* list of struct gtphub_peer */ + struct llist_head peers; +}; + +struct gtphub { + struct gtphub_bind to_sgsns[GTPH_PORT_N]; + struct gtphub_bind to_ggsns[GTPH_PORT_N]; + + /* pointers to an entry of to_sgsns[x].peers */ + struct gtphub_peer *sgsn_proxy[GTPH_PORT_N]; + + /* pointers to an entry of to_ggsns[x].peers */ + struct gtphub_peer *ggsn_proxy[GTPH_PORT_N]; + + struct osmo_timer_list gc_timer; + struct nr_map_expiry expire_seq_maps; + struct nr_map_expiry expire_tei_maps; +}; + +struct gtp_packet_desc; + + +/* api */ + +int gtphub_vty_init(void); +int gtphub_cfg_read(struct gtphub_cfg *cfg, const char *config_file); + +/* Initialize and start gtphub: bind to ports, run expiry timers. */ +int gtphub_start(struct gtphub *hub, struct gtphub_cfg *cfg); + +time_t gtphub_now(void); + +/* Remove expired items, empty peers, ... */ +void gtphub_gc(struct gtphub *hub, time_t now); diff --git a/openbsc/include/openbsc/vty.h b/openbsc/include/openbsc/vty.h index 818a20e..bc30e23 100644 --- a/openbsc/include/openbsc/vty.h +++ b/openbsc/include/openbsc/vty.h @@ -36,6 +36,7 @@ enum bsc_vty_node { BSC_NODE, SMPP_NODE, SMPP_ESME_NODE, + GTPHUB_NODE, };
extern int bsc_vty_is_config_node(struct vty *vty, int node); diff --git a/openbsc/src/gprs/Makefile.am b/openbsc/src/gprs/Makefile.am index c8e3696..b2d1774 100644 --- a/openbsc/src/gprs/Makefile.am +++ b/openbsc/src/gprs/Makefile.am @@ -11,6 +11,7 @@ noinst_HEADERS = gprs_sndcp.h bin_PROGRAMS = osmo-gbproxy
if HAVE_LIBGTP +bin_PROGRAMS += osmo-gtphub if HAVE_LIBCARES bin_PROGRAMS += osmo-sgsn endif @@ -33,3 +34,8 @@ osmo_sgsn_LDADD = \ $(top_builddir)/src/libcommon/libcommon.a \ -lgtp $(OSMO_LIBS) $(LIBOSMOABIS_LIBS) $(LIBCARES_LIBS) \ $(LIBCRYPTO_LIBS) -lrt + +osmo_gtphub_SOURCES = gtphub_main.c gtphub.c gtphub_sep.c gtphub_vty.c +osmo_gtphub_LDADD = \ + $(top_builddir)/src/libcommon/libcommon.a \ + -lgtp $(LIBOSMOCORE_LIBS) $(LIBOSMOVTY_LIBS) -lrt diff --git a/openbsc/src/gprs/gtphub.c b/openbsc/src/gprs/gtphub.c new file mode 100644 index 0000000..19246e2 --- /dev/null +++ b/openbsc/src/gprs/gtphub.c @@ -0,0 +1,1794 @@ +/* GTP Hub Implementation */ + +/* (C) 2015 by sysmocom s.f.m.c. GmbH info@sysmocom.de + * All Rights Reserved + * + * Author: Neels Hofmeyr + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation; either version 3 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +#include <string.h> +#include <errno.h> +#include <inttypes.h> +#include <time.h> +#include <limits.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <arpa/inet.h> + +#include <gtp.h> +#include <gtpie.h> + +#include <openbsc/gtphub.h> +#include <openbsc/debug.h> + +#include <osmocom/core/utils.h> +#include <osmocom/core/logging.h> +#include <osmocom/core/socket.h> + +#define GTPHUB_DEBUG 1 + +static const int GTPH_GC_TICK_SECONDS = 1; + +void *osmo_gtphub_ctx; + +#define LOGERR(fmt, args...) \ + LOGP(DGTPHUB, LOGL_ERROR, fmt, ##args) + +#define LOG(fmt, args...) \ + LOGP(DGTPHUB, LOGL_NOTICE, fmt, ##args) + +#define ZERO_STRUCT(struct_pointer) memset(struct_pointer, '\0', sizeof(*(struct_pointer))) + +/* TODO move this to osmocom/core/select.h ? */ +typedef int (*osmo_fd_cb_t)(struct osmo_fd *fd, unsigned int what); + +/* TODO move this to osmocom/core/linuxlist.h ? */ +#define __llist_first(head) (((head)->next == (head)) ? NULL : (head)->next) +#define llist_first(head, type, entry) llist_entry(__llist_first(head), type, entry) + +/* TODO move GTP header stuff to openggsn/gtp/ ? See gtp_decaps*() */ + +enum gtp_rc { + GTP_RC_UNKNOWN = 0, + GTP_RC_TINY = 1, /* no IEs (like ping/pong) */ + GTP_RC_PDU = 2, /* a real packet with IEs */ + + GTP_RC_TOOSHORT = -1, + GTP_RC_UNSUPPORTED_VERSION = -2, + GTP_RC_INVALID_IE = -3, +}; + +struct gtp_packet_desc { + union gtp_packet *data; + int data_len; + int header_len; + int version; + int rc; /* enum gtp_rc */ + unsigned int port_idx; + union gtpie_member *ie[GTPIE_SIZE]; +}; + +/* Validate GTP version 0 data; analogous to validate_gtp1_header(), see there. + */ +void validate_gtp0_header(struct gtp_packet_desc *p) +{ + const struct gtp0_header *pheader = &(p->data->gtp0.h); + p->rc = GTP_RC_UNKNOWN; + p->header_len = 0; + + OSMO_ASSERT(p->data_len >= 1); + OSMO_ASSERT(p->version == 0); + + if (p->data_len < GTP0_HEADER_SIZE) { + LOGERR("GTP0 packet too short: %d\n", p->data_len); + p->rc = GTP_RC_TOOSHORT; + return; + } + + if (p->data_len == GTP0_HEADER_SIZE) { + p->rc = GTP_RC_TINY; + p->header_len = GTP0_HEADER_SIZE; + return; + } + + /* Check packet length field versus length of packet */ + if (p->data_len != (ntoh16(pheader->length) + GTP0_HEADER_SIZE)) { + LOGERR("GTP packet length field (%d + %d) does not match" + " actual length (%d)\n", + GTP0_HEADER_SIZE, (int)ntoh16(pheader->length), + p->data_len); + p->rc = GTP_RC_TOOSHORT; + return; + } + + LOG("GTP v0 TID = %" PRIu64 "\n", pheader->tid); + p->header_len = GTP0_HEADER_SIZE; + p->rc = GTP_RC_PDU; +} + +/* Validate GTP version 1 data, and update p->rc with the result, as well as + * p->header_len in case of a valid header. */ +void validate_gtp1_header(struct gtp_packet_desc *p) +{ + const struct gtp1_header_long *pheader = &(p->data->gtp1l.h); + p->rc = GTP_RC_UNKNOWN; + p->header_len = 0; + + OSMO_ASSERT(p->data_len >= 1); + OSMO_ASSERT(p->version == 1); + + if ((p->data_len < GTP1_HEADER_SIZE_LONG) + && (p->data_len != GTP1_HEADER_SIZE_SHORT)){ + LOGERR("GTP packet too short: %d\n", p->data_len); + p->rc = GTP_RC_TOOSHORT; + return; + } + + LOG("|GTPv1\n"); + LOG("| type = %" PRIu8 " 0x%02" PRIx8 "\n", + pheader->type, pheader->type); + LOG("| length = %" PRIu16 " 0x%04" PRIx16 "\n", + ntoh16(pheader->length), ntoh16(pheader->length)); + LOG("| TEI = %" PRIu32 " 0x%08" PRIx32 "\n", + ntoh32(pheader->tei), ntoh32(pheader->tei)); + LOG("| seq = %" PRIu16 " 0x%04" PRIx16 "\n", + ntoh16(pheader->seq), ntoh16(pheader->seq)); + LOG("| npdu = %" PRIu8 " 0x%02" PRIx8 "\n", + pheader->npdu, pheader->npdu); + LOG("| next = %" PRIu8 " 0x%02" PRIx8 "\n", + pheader->next, pheader->next); + + if (p->data_len <= GTP1_HEADER_SIZE_LONG) { + p->rc = GTP_RC_TINY; + p->header_len = GTP1_HEADER_SIZE_SHORT; + return; + } + + /* Check packet length field versus length of packet */ + if (p->data_len != (ntoh16(pheader->length) + GTP1_HEADER_SIZE_SHORT)) { + LOGERR("GTP packet length field (%d + %d) does not match" + " actual length (%d)\n", + GTP1_HEADER_SIZE_SHORT, (int)ntoh16(pheader->length), + p->data_len); + p->rc = GTP_RC_TOOSHORT; + return; + } + + p->rc = GTP_RC_PDU; + p->header_len = GTP1_HEADER_SIZE_LONG; +} + +/* Examine whether p->data of size p->data_len has a valid GTP header. Set + * p->version, p->rc and p->header_len. On error, p->rc <= 0 (see enum + * gtp_rc). p->data must point at a buffer with p->data_len set. */ +void validate_gtp_header(struct gtp_packet_desc *p) +{ + p->rc = GTP_RC_UNKNOWN; + + /* Need at least 1 byte in order to check version */ + if (p->data_len < 1) { + LOGERR("Discarding packet - too small: %d\n", p->data_len); + p->rc = GTP_RC_TOOSHORT; + return; + } + + p->version = p->data->flags >> 5; + + switch (p->version) { + case 0: + validate_gtp0_header(p); + break; + case 1: + validate_gtp1_header(p); + break; + default: + LOGERR("Unsupported GTP version: %d\n", p->version); + p->rc = GTP_RC_UNSUPPORTED_VERSION; + break; + } +} + + +/* Return the value of the i'th IMSI IEI by copying to *imsi. + * The first IEI is reached by passing i = 0. + * imsi must point at allocated space of (at least) 8 bytes. + * Return 1 on success, or 0 if not found. */ +static int get_ie_imsi(union gtpie_member *ie[], uint8_t *imsi, int i) +{ + return gtpie_gettv0(ie, GTPIE_IMSI, i, imsi, 8) == 0; +} + +/* Analogous to get_ie_imsi(). nsapi must point at a single uint8_t. */ +static int get_ie_nsapi(union gtpie_member *ie[], uint8_t *nsapi, int i) +{ + return gtpie_gettv1(ie, GTPIE_NSAPI, i, nsapi) == 0; +} + +static char imsi_digit_to_char(uint8_t nibble) +{ + nibble &= 0x0f; + if (nibble > 9) + return (nibble == 0x0f) ? '\0' : '?'; + return '0' + nibble; +} + +/* Return a human readable IMSI string, in a static buffer. + * imsi must point at 8 octets of IMSI IE encoded IMSI data. */ +static const char *imsi_to_str(uint8_t *imsi) +{ + static char str[17]; + int i; + + for (i = 0; i < 8; i++) { + str[2*i] = imsi_digit_to_char(imsi[i]); + str[2*i + 1] = imsi_digit_to_char(imsi[i] >> 4); + } + str[16] = '\0'; + return str; +} + +/* Validate header, and index information elements. Write decoded packet + * information to *res. res->data will point at the given data buffer. On + * error, p->rc is set <= 0 (see enum gtp_rc). */ +static void gtp_decode(const uint8_t *data, int data_len, + unsigned int from_port_idx, + struct gtp_packet_desc *res) +{ + ZERO_STRUCT(res); + res->data = (union gtp_packet*)data; + res->data_len = data_len; + res->port_idx = from_port_idx; + + validate_gtp_header(res); + + if (res->rc <= 0) { + LOGERR("INVALID: dropping GTP packet.\n"); + return; + } + + LOG("Valid GTP header (v%d)\n", res->version); + + if (res->rc != GTP_RC_PDU) { + LOG("no IEs in this GTP packet\n"); + return; + } + + if (gtpie_decaps(res->ie, res->version, + (void*)(data + res->header_len), + res->data_len - res->header_len) != 0) { + res->rc = GTP_RC_INVALID_IE; + return; + } + +#if GTPHUB_DEBUG + int i; + + for (i = 0; i < 10; i++) { + uint8_t imsi[8]; + if (!get_ie_imsi(res->ie, imsi, i)) + break; + LOG("| IMSI %s\n", imsi_to_str(imsi)); + } + + for (i = 0; i < 10; i++) { + uint8_t nsapi; + if (!get_ie_nsapi(res->ie, &nsapi, i)) + break; + LOG("| NSAPI %d\n", (int)nsapi); + } + + for (i = 0; i < 10; i++) { + unsigned int addr_len; + struct in_addr addr; + if (gtpie_gettlv(res->ie, GTPIE_GSN_ADDR, i, &addr_len, &addr, + sizeof(addr)) != 0) + break; + LOG("| addr %s\n", inet_ntoa(addr)); + } + + for (i = 0; i < 10; i++) { + uint32_t tei; + if (gtpie_gettv4(res->ie, GTPIE_TEI_DI, i, &tei) != 0) + break; + LOG("| TEI DI (USER) %" PRIu32 " 0x%08" PRIx32 "\n", + tei, tei); + } + + for (i = 0; i < 10; i++) { + uint32_t tei; + if (gtpie_gettv4(res->ie, GTPIE_TEI_C, i, &tei) != 0) + break; + LOG("| TEI (CTRL) %" PRIu32 " 0x%08" PRIx32 "\n", + tei, tei); + } +#endif +} + + +/* general */ + +const char* const gtphub_port_idx_names[GTPH_PORT_N] = { + "CTRL", + "USER", +}; + +time_t gtphub_now(void) +{ + struct timespec now_tp; + OSMO_ASSERT(clock_gettime(CLOCK_MONOTONIC, &now_tp) >= 0); + return now_tp.tv_sec; +} + + +/* nr_map, nr_pool */ + +void nr_pool_init(struct nr_pool *pool) +{ + *pool = (struct nr_pool){}; +} + +nr_t nr_pool_next(struct nr_pool *pool) +{ + pool->last_nr ++; + + OSMO_ASSERT(pool->last_nr > 0); + /* TODO: gracefully handle running out of TEIs. */ + /* TODO: random TEIs. */ + + return pool->last_nr; +} + +void nr_map_init(struct nr_map *map, struct nr_pool *pool, + struct nr_map_expiry *exq) +{ + ZERO_STRUCT(map); + map->pool = pool; + map->expiry = exq; + INIT_LLIST_HEAD(&map->mappings); +} + +void nr_mapping_init(struct nr_mapping *m) +{ + ZERO_STRUCT(m); + INIT_LLIST_HEAD(&m->entry); + INIT_LLIST_HEAD(&m->expiry_entry); +} + +void nr_map_add(struct nr_map *map, struct nr_mapping *mapping, time_t now) +{ + /* Generate a mapped number */ + mapping->repl = nr_pool_next(map->pool); + + /* Add to the tail to always yield a list sorted by expiry, in + * ascending order. */ + llist_add_tail(&mapping->entry, &map->mappings); + if (map->expiry) + nr_map_expiry_add(map->expiry, mapping, now); +} + +void nr_map_clear(struct nr_map *map) +{ + struct nr_mapping *m; + struct nr_mapping *n; + llist_for_each_entry_safe(m, n, &map->mappings, entry) { + nr_mapping_del(m); + } +} + +int nr_map_empty(const struct nr_map *map) +{ + return llist_empty(&map->mappings); +} + +struct nr_mapping *nr_map_get(const struct nr_map *map, + void *origin, nr_t nr_orig) +{ + struct nr_mapping *mapping; + llist_for_each_entry(mapping, &map->mappings, entry) { + if ((mapping->origin == origin) + && (mapping->orig == nr_orig)) + return mapping; + } + /* Not found. */ + return NULL; +} + +struct nr_mapping *nr_map_get_inv(const struct nr_map *map, nr_t nr_repl) +{ + struct nr_mapping *mapping; + llist_for_each_entry(mapping, &map->mappings, entry) { + if (mapping->repl == nr_repl) { + return mapping; + } + } + /* Not found. */ + return NULL; +} + +void nr_mapping_del(struct nr_mapping *mapping) +{ + OSMO_ASSERT(mapping); + llist_del(&mapping->entry); + llist_del(&mapping->expiry_entry); + if (mapping->del_cb) + (mapping->del_cb)(mapping); +} + +void nr_map_expiry_init(struct nr_map_expiry *exq, int expiry_in_seconds) +{ + ZERO_STRUCT(exq); + exq->expiry_in_seconds = expiry_in_seconds; + INIT_LLIST_HEAD(&exq->mappings); +} + +void nr_map_expiry_add(struct nr_map_expiry *exq, struct nr_mapping *mapping, + time_t now) +{ + mapping->expiry = now + exq->expiry_in_seconds; + + /* Add/move to the tail to always sort by expiry, ascending. */ + llist_del(&mapping->expiry_entry); + llist_add_tail(&mapping->expiry_entry, &exq->mappings); +} + +int nr_map_expiry_tick(struct nr_map_expiry *exq, time_t now) +{ + int expired = 0; + struct nr_mapping *m; + struct nr_mapping *n; + llist_for_each_entry_safe(m, n, &exq->mappings, expiry_entry) { + if (m->expiry <= now) { + nr_mapping_del(m); + expired ++; + } + else { + /* The items are added sorted by expiry. So when we hit + * an unexpired entry, only more unexpired ones will + * follow. */ + break; + } + } + return expired; +} + + +/* gtphub */ + +/* Remove a gtphub_peer from its list and free it. */ +static void gtphub_peer_del(struct gtphub_peer *peer); + +/* From the information in the gtp_packet_desc, return the address of a GGSN. + * Return -1 on error. */ +static struct gtphub_peer *gtphub_resolve_ggsn(struct gtphub *hub, + struct gtp_packet_desc *p, + unsigned int port_idx); + +/* (wrapped by unit test) */ +int gtphub_resolve_ggsn_addr(struct gtphub *hub, + struct osmo_sockaddr *result, + struct gtp_packet_desc *p); + +/* comment: see at definition */ +static struct gtphub_peer *gtphub_peer_new(struct gtphub *hub, + struct gtphub_bind bind[], + unsigned int port_idx, + const char *addr_str, + uint16_t port, + const char *other_addr_str, + uint16_t other_port); + +static struct gtphub_peer *gtphub_peer_new_from_sockaddr(struct gtphub *hub, + struct gtphub_bind bind[], + unsigned int port_idx, + const struct osmo_sockaddr *addr, + int port_override); + +static void gtphub_zero(struct gtphub *hub) +{ + ZERO_STRUCT(hub); +} + +static int gtphub_sock_init(struct osmo_fd *ofd, + const struct gtphub_cfg_addr *addr, + osmo_fd_cb_t cb, + void *data, + int ofd_id) +{ + if (!addr->addr_str) { + LOGERR("Cannot bind: empty address.\n"); + return -1; + } + if (!addr->port) { + LOGERR("Cannot bind: zero port not permitted.\n"); + return -1; + } + + ofd->when = BSC_FD_READ; + ofd->cb = cb; + ofd->data = data; + ofd->priv_nr = ofd_id; + + int rc; + rc = osmo_sock_init_ofd(ofd, + AF_UNSPEC, SOCK_DGRAM, IPPROTO_UDP, + addr->addr_str, addr->port, + OSMO_SOCK_F_BIND); + if (rc < 1) { + LOGERR("Cannot bind to %s port %d (rc %d)\n", + addr->addr_str, (int)addr->port, rc); + return -1; + } + + return 0; +} + +static void gtphub_gtp_bind_init(struct gtphub_bind *b) +{ + ZERO_STRUCT(b); + + nr_pool_init(&b->tei_pool); + INIT_LLIST_HEAD(&b->peers); +} + +static int gtphub_gtp_bind_start(struct gtphub_bind *b, + const struct gtphub_cfg_bind *cfg, + osmo_fd_cb_t cb, void *cb_data, + unsigned int ofd_id) +{ + if (gtphub_sock_init(&b->ofd, &cfg->bind, cb, cb_data, ofd_id) != 0) + return -1; + return 0; +} + +/* Recv datagram from from->fd, optionally write sender's address to *from_addr. + * Return the number of bytes read, zero on error. */ +static int gtphub_read(const struct osmo_fd *from, + struct osmo_sockaddr *from_addr, + uint8_t *buf, size_t buf_len) +{ + /* recvfrom requires the available length to be set in *from_addr_len. */ + if (from_addr) + from_addr->l = sizeof(from_addr->a); + + errno = 0; + ssize_t received = recvfrom(from->fd, buf, buf_len, 0, + (struct sockaddr*)&from_addr->a, &from_addr->l); + /* TODO use recvmsg and get a MSG_TRUNC flag to make sure the message + * is not truncated. Then maybe reduce buf's size. */ + + if (received <= 0) { + if (errno != EAGAIN) + LOGERR("error: %s\n", strerror(errno)); + return 0; + } + + if (from_addr) { + LOG("from %s\n", osmo_sockaddr_to_str(from_addr)); + } + + if (received <= 0) { + LOGERR("error: %s\n", strerror(errno)); + return 0; + } + + LOG("Received %d\n%s\n", (int)received, osmo_hexdump(buf, received)); + return received; +} + +inline void gtphub_peer_ref_count_inc(struct gtphub_peer *p) +{ + OSMO_ASSERT(p->ref_count < UINT_MAX); + p->ref_count++; +} + +inline void gtphub_peer_ref_count_dec(struct gtphub_peer *p) +{ + OSMO_ASSERT(p->ref_count > 0); + p->ref_count--; +} + +inline uint16_t get_seq(struct gtp_packet_desc *p) +{ + OSMO_ASSERT(p->version == 1); + return ntoh16(p->data->gtp1l.h.seq); +} + +inline void set_seq(struct gtp_packet_desc *p, uint16_t seq) +{ + OSMO_ASSERT(p->version == 1); + p->data->gtp1l.h.seq = hton16(seq); +} + +inline uint32_t get_tei(struct gtp_packet_desc *p) +{ + OSMO_ASSERT(p->version == 1); + return ntoh32(p->data->gtp1l.h.tei); +} + +inline void set_tei(struct gtp_packet_desc *p, uint32_t tei) +{ + OSMO_ASSERT(p->version == 1); + p->data->gtp1l.h.tei = hton32(tei); +} + +static void gtphub_mapping_del_cb(struct nr_mapping *nrm); + +static struct nr_mapping *gtphub_mapping_new() +{ + struct nr_mapping *nrm; + nrm = talloc_zero(osmo_gtphub_ctx, struct nr_mapping); + OSMO_ASSERT(nrm); + + nr_mapping_init(nrm); + nrm->del_cb = gtphub_mapping_del_cb; + return nrm; +} + +static void gtphub_mapping_del_cb(struct nr_mapping *nrm) +{ + struct gtphub_peer *from = nrm->origin; + OSMO_ASSERT(from); + LOG("expired: %d: nr mapping from %s: %d->%d\n", + (int)nrm->expiry, + osmo_sockaddr_to_str(&from->addr), + (int)nrm->orig, (int)nrm->repl); + + gtphub_peer_ref_count_dec(from); + talloc_free(nrm); +} + +static struct nr_mapping *gtphub_mapping_have(struct nr_map *map, + struct gtphub_peer *from, + uint16_t orig_nr) +{ + struct nr_mapping *nrm; + + nrm = nr_map_get(map, from, orig_nr); + + if (!nrm) { + nrm = gtphub_mapping_new(); + nrm->orig = orig_nr; + nrm->origin = from; + nr_map_add(map, nrm, gtphub_now()); + gtphub_peer_ref_count_inc(from); + LOG("peer %p: MAP %d --> %d\n", from, (int)(nrm->orig), (int)(nrm->repl)); + } + else { + /* restart expiry timeout */ + nr_map_expiry_add(map->expiry, nrm, gtphub_now()); + } + + OSMO_ASSERT(nrm); + return nrm; +} + +static int gtphub_map_seq(struct gtp_packet_desc *p, + struct gtphub_peer *from_peer, struct gtphub_peer *to_peer) +{ + /* Store a mapping in to_peer's map, so when we later receive a GTP + * packet back from to_peer, the seq nr can be unmapped back to its + * origin (from_peer here). */ + struct nr_mapping *nrm; + nrm = gtphub_mapping_have(&to_peer->seq_map, from_peer, get_seq(p)); + + /* Change the GTP packet to yield the new, mapped seq nr */ + set_seq(p, nrm->repl); + + return 0; +} + +static struct gtphub_peer *gtphub_unmap_seq(struct gtp_packet_desc *p, + struct gtphub_peer *responding_peer) +{ + OSMO_ASSERT(p->version == 1); + struct nr_mapping *nrm = nr_map_get_inv(&responding_peer->seq_map, + get_seq(p)); + if (!nrm) + return NULL; + LOG("peer %p: UNMAP %d <-- %d\n", nrm->origin, (int)(nrm->orig), (int)(nrm->repl)); + set_seq(p, nrm->orig); + return nrm->origin; +} + +static void gtphub_check_restart_counter(struct gtphub *hub, + struct gtp_packet_desc *p, + struct gtphub_peer *from) +{ + /* TODO */ + /* If the peer is sending a Recovery IE (7.7.11) with a restart counter + * that doesn't match the peer's previously sent restart counter, clear + * that peer and cancel PDP contexts. */ +} + +static void gtphub_map_restart_counter(struct gtphub *hub, + struct gtp_packet_desc *p, + struct gtphub_peer *from, + struct gtphub_peer *to) +{ + /* TODO */ +} + +/* gtphub_map_ie_teis() and gtphub_unmap_header_tei(): + * + * TEI mapping must happen symmetrically. An SGSN contacts gtphub instead of N + * GGSNs, and a GGSN replies to gtphub for N SGSNs. From either end, TEIs may + * collide: two GGSNs picking the same TEIs, or two SGSNs picking the same + * TEIs. Since the opposite side sees the sender address being gtphub's + * address, TEIs among the SGSNs, and among the GGSNs, must not overlap. If a + * peer sends a TEI already sent before from a peer of the same side, gtphub + * replaces it with a TEI not yet seen from that side and remembers the + * mapping. + * + * Consider two SGSNs A and B contacting two GGSNs C and D thru gtphub. + * + * A: Create PDP Ctx, I have TEI 1. + * ---> gtphub: A has TEI 1, sending 1 for C. + * ---> C: gtphub has TEI 1. + * <--- C: Reponse to TEI 1: I have TEI 11. + * <--- gtphub: ok, telling A: 11. + * A: gtphub's first TEI is 11. (1) + * + * B: Create PDP Ctx, I have TEIs 1. + * ---> gtphub: 1 already taken for C, sending 2 for B. (map) + * ---> C: gtphub also has 2. + * <--- C: Reponse to TEI 2: I have TEI 12. + * <--- gtphub: ok, TEI 2 is actually B with TEI 1. (unmap) + * B: gtphub's first TEI is 12, as far as I can tell. + * + * Now the second GGSN comes into play: + * + * A: Create PDP Ctx, I have TEI 2. + * ---> gtphub: A also has TEI 2, but for D, sending 1. (2) + * ---> D: gtphub has 1. + * <--- D: Reponse to TEI 1: I have TEI 11. + * <--- gtphub: from D, 1 is A. 11 already taken by C, sending 13. (3) + * A: gtphub also has TEI 13. (4) + * + * And some messages routed through: + * + * A: message to TEI 11, see (1). + * ---> gtphub: ok, telling C with TEI 11. + * ---> C: I see, 11 means reply with 1. + * <--- C: Response to TEI 1 + * <--- gtphub: 1 from C is actually for A with TEI 1. + * A: ah, my TEI 1, thanks! + * + * A: message to TEI 13, see (4). + * ---> gtphub: ok, but not 13, D wanted TEI 11 instead, see (3). + * ---> D: I see, 11 means reply with 1. + * <--- D: Response to TEI 1 + * <--- gtphub: 1 from D is actually for A with TEI 2, see (2). + * A: ah, my TEI 2, thanks! + * + * What if a GGSN initiates a request: + * + * <--- D: Request to gtphub TEI 1 + * <--- gtphub: 1 from D is for A with 2, see (2). + * A: my TEI 2 means reply with 13. + * ---> gtphub: 13 was D with 11, see (3). + * ---> D: 11 from gtphub: a reply to my request for TEI 1. + * + * Note that usually, it's the sequence numbers that route a response back to + * the requesting peer. Nevertheless, the TEI mappings must be carried out to + * replace the TEIs in the GTP packet that is relayed. + * + * Also note: the TEI in the GTP header is "reversed" from the TEI in the IEs: + * the TEI in the header is used to send something *to* a peer, while the TEI + * in e.g. a Create PDP Context Request's IE is for routing messages *back* + * later. */ + +static int gtphub_unmap_header_tei(struct gtphub_peer **to_peer_p, + struct gtphub *hub, + struct gtp_packet_desc *p, + struct gtphub_peer *from_peer) +{ + OSMO_ASSERT(p->version == 1); + + struct nr_mapping *nrm; + uint32_t tei; + + *to_peer_p = NULL; + + /* If the header's TEI is zero, no PDP context has been established + * yet. If nonzero, a mapping should actually already exist for this + * TEI, since it must have been announced in a PDP context creation. */ + tei = get_tei(p); + + if (!tei) + return 0; + + /* to_peer has previously announced a TEI, which was stored and + * mapped in from_peer's tei_map. */ + nrm = nr_map_get_inv(&from_peer->tei_map, tei); + if (!nrm) { + LOGERR("Received unknown TEI %" PRIu32 " from %s\n", + tei, osmo_sockaddr_to_str(&from_peer->addr)); + return -1; + } + + struct gtphub_peer *to_peer = nrm->origin; + uint32_t unmapped_tei = nrm->orig; + set_tei(p, unmapped_tei); + + char buf[256]; + LOG("Unmapped TEI coming from %p %s: %d -> %d (to %s)\n", + from_peer, osmo_sockaddr_to_str(&from_peer->addr), tei, + unmapped_tei, osmo_sockaddr_to_strb(&to_peer->addr, buf, sizeof(buf))); + + *to_peer_p = to_peer; + return 0; +} + +/* In a packet coming from from_peer, find TEI IE with type number ie_type. If + * it does not exist yet, create a mapping for the TEI in to_peer's map. A + * message coming from to_peer can then be addressed to the (new) mapped TEI, + * and will trace back to from_peer and the TEI in this p's IE. + * + * Also replace the TEI with a mapped TEI in the GTP packet's data buffer. The + * buffer can then be sent on to to_peer. */ +static int gtphub_map_tei_ie(struct gtp_packet_desc *p, + uint8_t ie_type, int mandatory, + struct gtphub_peer *from_peer, + struct gtphub_peer *to_peer, + unsigned int port_idx) +{ + int ie_idx; + uint32_t tei; + uint32_t mapped_tei; + + OSMO_ASSERT(from_peer); + OSMO_ASSERT(to_peer); + + from_peer = from_peer->association[port_idx]; + to_peer = to_peer->association[port_idx]; + + if (!(from_peer && to_peer)) { + LOGERR("Missing peer in %s plane /" + " missing Create PDP Context Request\n", + gtphub_port_idx_names[port_idx]); + return -1; + } + + ie_idx = gtpie_getie(p->ie, ie_type, 0); + if (ie_idx < 0) { + if (! mandatory) + return 0; + + LOGERR("Create PDP Context Request: Invalid: missing IE %d\n", ie_type); + return -1; + } + tei = ntoh32(p->ie[ie_idx]->tv4.v); + /* When to_peer later sends the mapped tei for this tei, we + * want to obtain this tei and from_peer. */ + struct nr_mapping *nrm; + nrm = gtphub_mapping_have(&to_peer->tei_map, + from_peer, tei); + + mapped_tei = nrm->repl; + p->ie[ie_idx]->tv4.v = hton32(mapped_tei); + + char buf[256]; + LOG("Storing new %s TEI in peer %p %s's map: (from %s, TEI %d) -> TEI %d\n", + gtphub_port_idx_names[port_idx], + to_peer, + osmo_sockaddr_to_str(&to_peer->addr), + osmo_sockaddr_to_strb(&from_peer->addr, buf, sizeof(buf)), + tei, mapped_tei); + + return 0; +} + +/* The responding peer does not know that it is going to be the responding peer + * yet, but it must already be resolved and initialized. The GTP packet is + * coming from requesting_peer.*/ +static int gtphub_handle_create_pdp_req(struct gtphub *hub, + struct gtp_packet_desc *p, + struct gtphub_peer *requesting_peer, + struct gtphub_peer *responding_peer) +{ + /* This is a Create PDP Context Request. Expecting this + * to come from the SGSN side. */ + /* TODO enforce? */ + int rc; + + rc = gtphub_map_tei_ie(p, GTPIE_TEI_DI, 1, + requesting_peer, responding_peer, + GTPH_PORT_USER); + if (rc < 0) + return rc; + + rc = gtphub_map_tei_ie(p, GTPIE_TEI_C, 1, + requesting_peer, responding_peer, + GTPH_PORT_CTRL); + return rc; +} + +/* The GTP packet is coming from responding_peer; "responding peer" in the + * sense of the Create PDP Context messages. requesting_peer will have been + * figured out from sequence number and/or header TEI. */ +static int gtphub_handle_create_pdp_resp(struct gtphub *hub, + struct gtp_packet_desc *p, + struct gtphub_peer *responding_peer, + struct gtphub_peer *requesting_peer) +{ + /* This is a Create PDP Context Response. Expecting this + * to come from the GGSN side. */ + /* TODO enforce? */ + int rc; + + rc = gtphub_map_tei_ie(p, GTPIE_TEI_DI, 0, + responding_peer, requesting_peer, + GTPH_PORT_USER); + if (rc < 0) + return rc; + + rc = gtphub_map_tei_ie(p, GTPIE_TEI_C, 0, + responding_peer, requesting_peer, + GTPH_PORT_CTRL); + return rc; +} + +static int gtphub_map_ie_teis(struct gtphub *hub, + struct gtp_packet_desc *p, + struct gtphub_peer *from_peer, + struct gtphub_peer *to_peer) +{ + OSMO_ASSERT(p->version == 1); + struct gtp1_header_long *h = &p->data->gtp1l.h; + uint8_t type = ntoh8(h->type); + + switch (type) { + case GTP_CREATE_PDP_REQ: + gtphub_handle_create_pdp_req(hub, p, from_peer, to_peer); + break; + + case GTP_CREATE_PDP_RSP: + gtphub_handle_create_pdp_resp(hub, p, from_peer, to_peer); + break; + + default: + break; + } + return 0; +} + +static void gtphub_replace_addresses(struct gtphub *hub, + struct gtp_packet_desc *p, + struct gtphub_peer *from, + struct gtphub_bind *from_bind, + struct gtphub_peer *to, + struct gtphub_bind *to_bind) +{ + /* TODO */ +} + +static int gtphub_write(struct osmo_fd *to, + struct osmo_sockaddr *to_addr, + uint8_t *buf, size_t buf_len) +{ + errno = 0; + ssize_t sent = sendto(to->fd, buf, buf_len, 0, + (struct sockaddr*)&to_addr->a, to_addr->l); + + if (to_addr) { + LOG("to %s\n", osmo_sockaddr_to_str(to_addr)); + } + + if (sent == -1) { + LOGERR("error: %s\n", strerror(errno)); + return -EINVAL; + } + + if (sent != buf_len) + LOGERR("sent(%d) != data_len(%d)\n", (int)sent, (int)buf_len); + else + LOG("Sent %d\n%s\n", (int)sent, osmo_hexdump(buf, sent)); + + return 0; +} + +int gtphub_from_ggsns_handle_buf(struct gtphub *hub, + unsigned int port_idx, + const struct osmo_sockaddr *from_addr, + uint8_t *buf, + size_t received, + struct osmo_fd **to_ofd, + struct osmo_sockaddr **to_addr); + +static struct gtphub_peer *gtphub_peer_find(const struct gtphub_bind *bind, + const struct osmo_sockaddr *addr); + +static int from_ggsns_read_cb(struct osmo_fd *from_ggsns_ofd, unsigned int what) +{ + unsigned int port_idx = from_ggsns_ofd->priv_nr; + OSMO_ASSERT(port_idx < GTPH_PORT_N); + LOG("\n\n=== reading from GGSN (%s)\n", gtphub_port_idx_names[port_idx]); + if (!(what & BSC_FD_READ)) + return 0; + + struct gtphub *hub = from_ggsns_ofd->data; + + static uint8_t buf[4096]; + struct osmo_sockaddr from_addr; + struct osmo_sockaddr *to_addr; + struct osmo_fd *to_ofd; + size_t len; + + len = gtphub_read(from_ggsns_ofd, &from_addr, buf, sizeof(buf)); + if (len < 1) + return 0; + + len = gtphub_from_ggsns_handle_buf(hub, port_idx, &from_addr, buf, len, + &to_ofd, &to_addr); + if (len < 1) + return 0; + + return gtphub_write(to_ofd, to_addr, buf, len); +} + +static int gtphub_unmap(struct gtphub *hub, + struct gtp_packet_desc *p, + struct gtphub_peer *from, + struct gtphub_peer *to_proxy, + struct gtphub_peer **final_unmapped, + struct gtphub_peer **unmapped_from_seq, + struct gtphub_peer **unmapped_from_tei) +{ + /* Always (try to) unmap sequence and TEI numbers, which need to be + * replaced in the packet. Either way, give precedence to the proxy, if + * configured. */ + + struct gtphub_peer *from_seq = NULL; + struct gtphub_peer *from_tei = NULL; + struct gtphub_peer *unmapped = NULL; + + if (unmapped_from_seq) + *unmapped_from_seq = from_seq; + if (unmapped_from_tei) + *unmapped_from_tei = from_tei; + if (final_unmapped) + *final_unmapped = unmapped; + + from_seq = gtphub_unmap_seq(p, from); + + if (gtphub_unmap_header_tei(&from_tei, hub, p, from) < 0) + return -1; + + if (from_seq && from_tei && (from_seq != from_tei)) { + char b0[256]; + char b1[256]; + LOGERR("Seq unmap and TEI unmap yield two different peers. Using seq unmap." + "(from %s %s: seq %d yields %s, tei %u yields %s)\n", + gtphub_port_idx_names[p->port_idx], + osmo_sockaddr_to_str(&from->addr), + (int)get_seq(p), + osmo_sockaddr_to_strb(&from_seq->addr, b0, sizeof(b0)), + (int)get_tei(p), + osmo_sockaddr_to_strb(&from_tei->addr, b1, sizeof(b1)) + ); + } + unmapped = (from_seq? from_seq : from_tei); + + if (unmapped && to_proxy && (unmapped != to_proxy)) { + char buf[256]; + LOGERR("Unmap yields a different peer than the configured proxy. Using proxy." + " unmapped: %s proxy: %s\n", + osmo_sockaddr_to_str(&unmapped->addr), + osmo_sockaddr_to_strb(&to_proxy->addr, buf, sizeof(buf)) + ); + } + unmapped = (to_proxy? to_proxy : unmapped); + + if (!unmapped) { + /* Return no error, but returned pointers are all NULL. */ + return 0; + } + + LOG("from seq %p; from tei %p; unmapped => %p\n", + from_seq, from_tei, unmapped); + + if (unmapped_from_seq) + *unmapped_from_seq = from_seq; + if (unmapped_from_tei) + *unmapped_from_tei = from_tei; + if (final_unmapped) + *final_unmapped = unmapped; + return 0; +} + +/* Parse buffer as GTP packet, replace elements in-place and return the ofd and + * address to forward to. Return the number of bytes to forward, 0 or less on + * failure. Return the fd and address to forward to in to_ofd and to_addr. */ +int gtphub_from_ggsns_handle_buf(struct gtphub *hub, + unsigned int port_idx, + const struct osmo_sockaddr *from_addr, + uint8_t *buf, + size_t received, + struct osmo_fd **to_ofd, + struct osmo_sockaddr **to_addr) +{ + LOG("<- rx from GGSN %s\n", osmo_sockaddr_to_str(from_addr)); + + static struct gtp_packet_desc p; + gtp_decode(buf, received, port_idx, &p); + + if (p.rc <= 0) + return -1; + + /* If a GGSN proxy is configured, check that it's indeed that proxy + * talking to us. */ + struct gtphub_peer *ggsn = hub->ggsn_proxy[port_idx]; + if (ggsn && (osmo_sockaddr_cmp(&ggsn->addr, from_addr) != 0)) { + char buf[256]; + LOGERR("Rejecting: GGSN proxy configured, but GTP packet" + " received on GGSN bind is from another sender:" + " proxy: %s sender: %s\n", + osmo_sockaddr_to_str(&ggsn->addr), + osmo_sockaddr_to_strb(from_addr, buf, sizeof(buf))); + return -1; + } + + if (!ggsn) { + /* If any PDP context has been created, we already have an + * entry for this GGSN. If we don't have an entry, the GGSN has + * nothing to tell us about. */ + ggsn = gtphub_peer_find(&hub->to_ggsns[port_idx], from_addr); + LOG("Found peer %p for %s\n", ggsn, osmo_sockaddr_to_str(from_addr)); + } + + if (!ggsn) { + LOGERR("no ggsn\n"); + return -1; + } + + struct gtphub_peer *sgsn_from_seq; + struct gtphub_peer *sgsn; + if (gtphub_unmap(hub, &p, ggsn, + hub->sgsn_proxy[port_idx], + &sgsn, &sgsn_from_seq, NULL) + != 0) { + return -1; + } + + if (!sgsn) { + LOGERR("No SGSN to send to. Dropping packet.\n"); + return -1; + } + + gtphub_check_restart_counter(hub, &p, ggsn); + gtphub_map_restart_counter(hub, &p, ggsn, sgsn); + gtphub_replace_addresses(hub, &p, + ggsn, &hub->to_ggsns[port_idx], + sgsn, &hub->to_sgsns[port_idx]); + + /* If the GGSN is replying to an SGSN request, the sequence nr has + * already been unmapped above (sgsn_from_seq != NULL), and we need not + * create a new mapping. */ + if (!sgsn_from_seq) + gtphub_map_seq(&p, ggsn, sgsn); + + /* If IEs announce new TEIs, map those. */ + if (gtphub_map_ie_teis(hub, &p, ggsn, sgsn) < 0) { + LOGERR("Dropping invalid packet\n"); + return -1; + } + + *to_ofd = &hub->to_sgsns[port_idx].ofd; + *to_addr = &sgsn->addr; + + return received; +} + +int gtphub_from_sgsns_handle_buf(struct gtphub *hub, + unsigned int port_idx, + const struct osmo_sockaddr *from_addr, + uint8_t *buf, + size_t received, + struct osmo_fd **to_ofd, + struct osmo_sockaddr **to_addr); + +static int from_sgsns_read_cb(struct osmo_fd *from_sgsns_ofd, unsigned int what) +{ + unsigned int port_idx = from_sgsns_ofd->priv_nr; + OSMO_ASSERT(port_idx < GTPH_PORT_N); + LOG("\n\n=== reading from SGSN (%s)\n", gtphub_port_idx_names[port_idx]); + + if (!(what & BSC_FD_READ)) + return 0; + + struct gtphub *hub = from_sgsns_ofd->data; + + static uint8_t buf[4096]; + struct osmo_sockaddr from_addr; + struct osmo_sockaddr *to_addr; + struct osmo_fd *to_ofd; + size_t len; + + len = gtphub_read(from_sgsns_ofd, &from_addr, buf, sizeof(buf)); + if (len < 1) + return 0; + + len = gtphub_from_sgsns_handle_buf(hub, port_idx, &from_addr, buf, len, + &to_ofd, &to_addr); + if (len < 1) + return 0; + + return gtphub_write(to_ofd, to_addr, buf, len); +} + +/* Parse buffer as GTP packet, replace elements in-place and return the ofd and + * address to forward to. Return the number of bytes to forward, 0 or less on + * failure. Return the fd and address to forward to in to_ofd and to_addr. */ +int gtphub_from_sgsns_handle_buf(struct gtphub *hub, + unsigned int port_idx, + const struct osmo_sockaddr *from_addr, + uint8_t *buf, + size_t received, + struct osmo_fd **to_ofd, + struct osmo_sockaddr **to_addr) +{ + LOG("-> rx from SGSN %s\n", osmo_sockaddr_to_str(from_addr)); + + static struct gtp_packet_desc p; + gtp_decode(buf, received, port_idx, &p); + + if (p.rc <= 0) + return -1; + + /* If an SGSN proxy is configured, check that it's indeed that proxy + * talking to us. */ + struct gtphub_peer *sgsn = hub->sgsn_proxy[port_idx]; + if (sgsn && (osmo_sockaddr_cmp(&sgsn->addr, from_addr) != 0)) { + char buf[256]; + LOGERR("Rejecting: SGSN proxy configured, but GTP packet" + " received on SGSN bind is from another sender: " + "proxy: %s sender: %s\n", + osmo_sockaddr_to_str(from_addr), + osmo_sockaddr_to_strb(&sgsn->addr, buf, sizeof(buf))); + return -1; + } + + if (!sgsn) { + /* TODO this is a worthless hack to test things. Instead: + sgsn = gtphub_sgsn_get(hub, ...); */ + sgsn = llist_first(&hub->to_sgsns[port_idx].peers, + struct gtphub_peer, entry); + if (!sgsn) { + sgsn = gtphub_peer_new_from_sockaddr(hub, + hub->to_sgsns, + port_idx, + from_addr, + -1); + if (!sgsn) { + LOG("Packet dropped by preliminary debug code.\n"); + return -1; + } + LOG("Created peer %p\n", sgsn); + } + + if (osmo_sockaddr_cmp(&sgsn->addr, from_addr) != 0) { + char buf[256]; + LOG("SGSN changed its address from %s to %s\n", + osmo_sockaddr_to_str(&sgsn->addr), + osmo_sockaddr_to_strb(from_addr, buf, sizeof(buf))); + memcpy(&sgsn->addr, from_addr, sizeof(sgsn->addr)); + } + LOG("Peer %p = %s\n", sgsn, osmo_sockaddr_to_str(&sgsn->addr)); + } + + struct gtphub_peer *ggsn_from_seq; + struct gtphub_peer *ggsn; + if (gtphub_unmap(hub, &p, sgsn, + hub->ggsn_proxy[port_idx], + &ggsn, &ggsn_from_seq, NULL) + != 0) { + return -1; + } + + /* See what our GGSN guess would be from the packet data per se. */ + /* TODO maybe not do this always? */ + struct gtphub_peer *ggsn_from_packet; + ggsn_from_packet = gtphub_resolve_ggsn(hub, &p, port_idx); + + if (ggsn_from_packet && ggsn + && (ggsn_from_packet != ggsn)) { + char buf[256]; + LOGERR("GGSN implied from packet does not match unmapped" + " GGSN, using unmapped GGSN:" + " from packet: %s unmapped: %s\n", + osmo_sockaddr_to_str(&ggsn_from_packet->addr), + osmo_sockaddr_to_strb(&ggsn_from_packet->addr, + buf, sizeof(buf))); + /* TODO return -1; ? */ + } + + if (!ggsn) + ggsn = ggsn_from_packet; + + if (!ggsn) { + LOGERR("No GGSN to send to. Dropping packet.\n"); + return -1; + } + + gtphub_check_restart_counter(hub, &p, sgsn); + gtphub_map_restart_counter(hub, &p, sgsn, ggsn); + gtphub_replace_addresses(hub, &p, + sgsn, &hub->to_sgsns[port_idx], + ggsn, &hub->to_ggsns[port_idx]); + + /* If the SGSN is replying to a GGSN request, the sequence nr has + * already been unmapped above (unmap_ggsn != NULL), and we need not + * create a new outgoing sequence map. */ + if (!ggsn_from_seq) + gtphub_map_seq(&p, sgsn, ggsn); + + /* If IEs announce new TEIs, map those. */ + if (gtphub_map_ie_teis(hub, &p, sgsn, ggsn) < 0) { + LOGERR("Dropping invalid packet\n"); + return -1; + } + + *to_ofd = &hub->to_ggsns[port_idx].ofd; + *to_addr = &ggsn->addr; + + return received; +} + +static void gtphub_gc_bind(struct gtphub *hub, struct gtphub_bind *b) +{ + struct gtphub_peer *p, *n; + llist_for_each_entry_safe(p, n, &b->peers, entry) { + + if ((!p->ref_count) + && nr_map_empty(&p->seq_map)) { + + LOG("expired: peer %s\n", + osmo_sockaddr_to_str(&p->addr)); + gtphub_peer_del(p); + } + } +} + +void gtphub_gc(struct gtphub *hub, time_t now) +{ + int expired; + expired = nr_map_expiry_tick(&hub->expire_seq_maps, now); + expired += nr_map_expiry_tick(&hub->expire_tei_maps, now); + + /* ... */ + + if (expired) { + int i; + for (i = 0; i < GTPH_PORT_N; i++) { + gtphub_gc_bind(hub, &hub->to_sgsns[i]); + gtphub_gc_bind(hub, &hub->to_ggsns[i]); + } + } +} + +static void gtphub_gc_cb(void *data) +{ + struct gtphub *hub = data; + gtphub_gc(hub, gtphub_now()); + osmo_timer_schedule(&hub->gc_timer, GTPH_GC_TICK_SECONDS, 0); +} + +static void gtphub_gc_start(struct gtphub *hub) +{ + hub->gc_timer.cb = gtphub_gc_cb; + hub->gc_timer.data = hub; + + osmo_timer_schedule(&hub->gc_timer, GTPH_GC_TICK_SECONDS, 0); +} + +/* called by unit tests */ +void gtphub_init(struct gtphub *hub) +{ + gtphub_zero(hub); + + nr_map_expiry_init(&hub->expire_seq_maps, GTPH_SEQ_MAPPING_EXPIRY_SECS); + nr_map_expiry_init(&hub->expire_tei_maps, GTPH_TEI_MAPPING_EXPIRY_MINUTES * 60); + + int port_idx; + for (port_idx = 0; port_idx < GTPH_PORT_N; port_idx++) { + gtphub_gtp_bind_init(&hub->to_ggsns[port_idx]); + gtphub_gtp_bind_init(&hub->to_sgsns[port_idx]); + } +} + +int gtphub_start(struct gtphub *hub, struct gtphub_cfg *cfg) +{ + int rc; + + gtphub_init(hub); + + int port_idx; + for (port_idx = 0; port_idx < GTPH_PORT_N; port_idx++) { + rc = gtphub_gtp_bind_start(&hub->to_ggsns[port_idx], + &cfg->to_ggsns[port_idx], + from_ggsns_read_cb, hub, port_idx); + if (rc) { + LOGERR("Failed to bind for GGSNs (%s)\n", + gtphub_port_idx_names[port_idx]); + return rc; + } + + rc = gtphub_gtp_bind_start(&hub->to_sgsns[port_idx], + &cfg->to_sgsns[port_idx], + from_sgsns_read_cb, hub, port_idx); + if (rc) { + LOGERR("Failed to bind for SGSNs (%s)\n", + gtphub_port_idx_names[port_idx]); + return rc; + } + } + + /* SGSN proxy. Trigger only on the control port address. */ + if (cfg->sgsn_proxy[GTPH_PORT_CTRL].addr_str) { + struct gtphub_cfg_addr *addr_c = &cfg->sgsn_proxy[GTPH_PORT_CTRL]; + struct gtphub_cfg_addr *addr_u = &cfg->sgsn_proxy[GTPH_PORT_USER]; + + struct gtphub_peer *sgsn_c; + struct gtphub_peer *sgsn_u; + sgsn_c = gtphub_peer_new(hub, hub->to_sgsns, GTPH_PORT_CTRL, + addr_c->addr_str, addr_c->port, + addr_u->addr_str, addr_u->port); + sgsn_u = sgsn_c->association[GTPH_PORT_USER]; + + hub->sgsn_proxy[GTPH_PORT_CTRL] = sgsn_c; + hub->sgsn_proxy[GTPH_PORT_USER] = sgsn_u; + + /* This is *the* proxy SGSN. Make sure it is never expired. */ + gtphub_peer_ref_count_inc(sgsn_c); + gtphub_peer_ref_count_inc(sgsn_u); + + LOG("Using SGSN %s proxy %s port %d\n", + gtphub_port_idx_names[GTPH_PORT_CTRL], + addr_c->addr_str, + (int)addr_c->port); + LOG("Using SGSN %s proxy %s port %d\n", + gtphub_port_idx_names[GTPH_PORT_USER], + addr_u->addr_str, + (int)addr_u->port); + } + + /* GGSN proxy. Trigger only on the control port address. */ + if (cfg->ggsn_proxy[GTPH_PORT_CTRL].addr_str) { + struct gtphub_cfg_addr *addr_c = &cfg->ggsn_proxy[GTPH_PORT_CTRL]; + struct gtphub_cfg_addr *addr_u = &cfg->ggsn_proxy[GTPH_PORT_USER]; + + struct gtphub_peer *ggsn_c; + struct gtphub_peer *ggsn_u; + ggsn_c = gtphub_peer_new(hub, hub->to_ggsns, GTPH_PORT_CTRL, + addr_c->addr_str, addr_c->port, + addr_u->addr_str, addr_u->port); + ggsn_u = ggsn_c->association[GTPH_PORT_USER]; + + hub->ggsn_proxy[GTPH_PORT_CTRL] = ggsn_c; + hub->ggsn_proxy[GTPH_PORT_USER] = ggsn_u; + + /* This is *the* proxy GGSN. Make sure it is never expired. */ + gtphub_peer_ref_count_inc(ggsn_c); + gtphub_peer_ref_count_inc(ggsn_u); + + LOG("Using GGSN %s proxy %s port %d\n", + gtphub_port_idx_names[GTPH_PORT_CTRL], + addr_c->addr_str, + (int)addr_c->port); + LOG("Using GGSN %s proxy %s port %d\n", + gtphub_port_idx_names[GTPH_PORT_USER], + addr_u->addr_str, + (int)addr_u->port); + } + + gtphub_gc_start(hub); + return 0; +} + +static struct gtphub_peer *_gtphub_peer_new(struct gtphub *hub, + struct gtphub_bind bind[], + unsigned int port_idx, + const char *addr_str, + uint16_t port) +{ + struct gtphub_peer *n = talloc_zero(osmo_gtphub_ctx, struct gtphub_peer); + + nr_map_init(&n->tei_map, &bind[port_idx].tei_pool, &hub->expire_tei_maps); + + nr_pool_init(&n->seq_pool); + nr_map_init(&n->seq_map, &n->seq_pool, &hub->expire_seq_maps); + + /* TODO use something random to pick the initial sequence nr. + 0x6d31 produces the ASCII character sequence 'm1', currently used in + gtphub_nc_test.sh. */ + n->seq_pool.last_nr = 0x6d31 - 1; + + llist_add(&n->entry, &bind[port_idx].peers); + + if (!port) { + port = (port_idx == GTPH_PORT_USER)? GTP1U_PORT : GTP1C_PORT; + } + + LOG("New peer: %s %s %d\n", gtphub_port_idx_names[port_idx], addr_str, + (int)port); + + if (osmo_sockaddr_init_udp(&n->addr, addr_str, port) != 0) { + LOGERR("Cannot resolve '%s port %d'\n", + addr_str, (int)port); + talloc_free(n); + return NULL; + } + + return n; +} + +/* Create two new gtphub_peer instances added to bind[CTRL]->peers and + * bind[USER]->peers. + * Initialize: + * - point association[port_idx]es to the respective instances, + * - resolve the provided addresses, + * - set default port numbers for the respective planes, if a given port is 0. + * addr_str and port define the peer for the GTP plane indicated by port_idx. + * other_addr_str and other_port are used to initialize the peer on the + * respective other plane (if port_idx is GTPH_PORT_CTRL, other_addr_str and + * other_port are for the User plane). other_addr_str may be NULL, in which + * case addr_str is used for both planes. One or both ports may be 0, in which + * case the default port for that plane is used. Return a pointer to the new + * instance in the <port_idx> plane, or NULL on error. */ +static struct gtphub_peer *gtphub_peer_new(struct gtphub *hub, + struct gtphub_bind bind[], + unsigned int port_idx, + const char *addr_str, + uint16_t port, + const char *other_addr_str, + uint16_t other_port) +{ + if (!other_addr_str) + other_addr_str = addr_str; + + const char *addr_str_c; + const char *addr_str_u; + uint16_t port_c; + uint16_t port_u; + if (port_idx == GTPH_PORT_CTRL) { + addr_str_c = addr_str; + addr_str_u = other_addr_str; + port_c = port; + port_u = other_port; + } + else { + OSMO_ASSERT(port_idx == GTPH_PORT_USER); + addr_str_u = addr_str; + addr_str_c = other_addr_str; + port_u = port; + port_c = other_port; + } + + struct gtphub_peer *c = _gtphub_peer_new(hub, bind, GTPH_PORT_CTRL, + addr_str_c, port_c); + struct gtphub_peer *u = _gtphub_peer_new(hub, bind, GTPH_PORT_USER, + addr_str_u, port_u); + + c->association[GTPH_PORT_CTRL] = u->association[GTPH_PORT_CTRL] = c; + c->association[GTPH_PORT_USER] = u->association[GTPH_PORT_USER] = u; + + return c->association[port_idx]; +} + +/* port_override: -1: use port from addr; 0: use plane's default port; >0: use + * this number as port. */ +static struct gtphub_peer *gtphub_peer_new_from_sockaddr(struct gtphub *hub, + struct gtphub_bind bind[], + unsigned int port_idx, + const struct osmo_sockaddr *addr, + int port_override) +{ + char addr_str[256]; + char port_str[6]; + + if (osmo_sockaddr_to_strs(addr_str, sizeof(addr_str), + port_str, sizeof(port_str), + addr, + (NI_NUMERICHOST | NI_NUMERICSERV)) + != 0) { + return NULL; + } + + uint16_t port = port_override < 0? atoi(port_str) : port_override; + + return gtphub_peer_new(hub, bind, port_idx, addr_str, port, NULL, 0); +} + +static void gtphub_peer_del(struct gtphub_peer *peer) +{ + nr_map_clear(&peer->seq_map); + llist_del(&peer->entry); + talloc_free(peer); +} + +static struct gtphub_peer *gtphub_peer_find(const struct gtphub_bind *bind, + const struct osmo_sockaddr *addr) +{ + struct gtphub_peer *peer; + llist_for_each_entry(peer, &bind->peers, entry) { + if (osmo_sockaddr_cmp(addr, &peer->addr) == 0) + return peer; + } + return NULL; +} + +static struct gtphub_peer *gtphub_have_ggsn(struct gtphub *hub, + struct osmo_sockaddr *addr, + unsigned int port_idx) +{ + struct gtphub_peer *peer; + peer = gtphub_peer_find(&hub->to_ggsns[port_idx], addr); + if (peer) + return peer; + + /* Not found, create one. */ + peer = gtphub_peer_new_from_sockaddr(hub, hub->to_ggsns, port_idx, addr, -1); + return peer; +} + +static struct gtphub_peer *gtphub_resolve_ggsn(struct gtphub *hub, + struct gtp_packet_desc *p, + unsigned int port_idx) +{ + int rc; + + struct osmo_sockaddr addr; + + rc = gtphub_resolve_ggsn_addr(hub, &addr, p); + if (rc < 0) + return NULL; + + return gtphub_have_ggsn(hub, &addr, port_idx); +} + + +/* TODO move to osmocom/core/socket.c ? */ +/* The caller is required to call freeaddrinfo(*result), iff zero is returned. */ +/* use this in osmo_sock_init() to remove dup. */ +static int _osmo_getaddrinfo(struct addrinfo **result, + uint16_t family, uint16_t type, uint8_t proto, + const char *host, uint16_t port) +{ + struct addrinfo hints; + char portbuf[16]; + + sprintf(portbuf, "%u", port); + memset(&hints, '\0', sizeof(struct addrinfo)); + hints.ai_family = family; + if (type == SOCK_RAW) { + /* Workaround for glibc, that returns EAI_SERVICE (-8) if + * SOCK_RAW and IPPROTO_GRE is used. + */ + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + } else { + hints.ai_socktype = type; + hints.ai_protocol = proto; + } + + return getaddrinfo(host, portbuf, &hints, result); +} + +/* TODO move to osmocom/core/socket.c ? */ +int osmo_sockaddr_init(struct osmo_sockaddr *addr, + uint16_t family, uint16_t type, uint8_t proto, + const char *host, uint16_t port) +{ + struct addrinfo *res; + int rc; + rc = _osmo_getaddrinfo(&res, family, type, proto, host, port); + + if (rc != 0) { + LOGERR("getaddrinfo returned error %d\n", (int)rc); + return -EINVAL; + } + + OSMO_ASSERT(res->ai_addrlen <= sizeof(addr->a)); + memcpy(&addr->a, res->ai_addr, res->ai_addrlen); + addr->l = res->ai_addrlen; + freeaddrinfo(res); + + return 0; +} + +int osmo_sockaddr_to_strs(char *addr_str, size_t addr_str_len, + char *port_str, size_t port_str_len, + const struct osmo_sockaddr *addr, + int flags) +{ + int rc; + + if ((addr->l < 1) || (addr->l > sizeof(addr->a))) { + LOGP(DGTPHUB, LOGL_ERROR, "Invalid address size: %d\n", addr->l); + return -1; + } + + if (addr->l > sizeof(addr->a)) { + LOGP(DGTPHUB, LOGL_ERROR, "Invalid address: too long: %d\n", addr->l); + return -1; + } + + rc = getnameinfo((struct sockaddr*)&addr->a, addr->l, + addr_str, addr_str_len, + port_str, port_str_len, + flags); + + if (rc) + LOGP(DGTPHUB, LOGL_ERROR, "Invalid address: %s: %s\n", gai_strerror(rc), + osmo_hexdump((uint8_t*)&addr->a, addr->l)); + + return rc; +} + +const char *osmo_sockaddr_to_strb(const struct osmo_sockaddr *addr, + char *buf, size_t buf_len) +{ + const int portbuf_len = 6; + OSMO_ASSERT(buf_len > portbuf_len); + char *portbuf = buf + buf_len - portbuf_len; + buf_len -= portbuf_len; + if (osmo_sockaddr_to_strs(buf, buf_len, + portbuf, portbuf_len, + addr, + NI_NUMERICHOST | NI_NUMERICSERV)) + return NULL; + + char *pos = buf + strnlen(buf, buf_len-1); + size_t len = buf_len - (pos - buf); + + snprintf(pos, len, " port %s", portbuf); + buf[buf_len-1] = '\0'; + + return buf; +} + +const char *osmo_sockaddr_to_str(const struct osmo_sockaddr *addr) +{ + static char buf[256]; + const char *result = osmo_sockaddr_to_strb(addr, buf, sizeof(buf)); + if (! result) + return "(invalid)"; + return result; +} + +int osmo_sockaddr_cmp(const struct osmo_sockaddr *a, const struct osmo_sockaddr *b) +{ + if (a->l != b->l) { + /* Lengths are not the same, but determine the order. Will + * anyone ever sort a list by osmo_sockaddr though...? */ + int cmp = memcmp(&a->a, &b->a, (a->l < b->l)? a->l : b->l); + if (cmp == 0) { + if (a->l < b->l) + return -1; + else + return 1; + } + return cmp; + } + return memcmp(&a->a, &b->a, a->l); +} + +void osmo_sockaddr_copy(struct osmo_sockaddr *dst, const struct osmo_sockaddr *src) +{ + OSMO_ASSERT(src->l <= sizeof(dst->a)); + memcpy(&dst->a, &src->a, src->l); + dst->l = src->l; +} diff --git a/openbsc/src/gprs/gtphub_main.c b/openbsc/src/gprs/gtphub_main.c new file mode 100644 index 0000000..84c7969 --- /dev/null +++ b/openbsc/src/gprs/gtphub_main.c @@ -0,0 +1,283 @@ +/* GTP Hub main program */ + +/* (C) 2015 by sysmocom s.f.m.c. GmbH info@sysmocom.de + * All Rights Reserved + * + * Author: Neels Hofmeyr + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation; either version 3 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +#include <unistd.h> +#include <signal.h> +#include <string.h> +#include <errno.h> + +#define _GNU_SOURCE +#include <getopt.h> + +#include <osmocom/core/signal.h> +#include <osmocom/core/application.h> +#include <osmocom/core/logging.h> +#include <osmocom/core/utils.h> +#include <osmocom/core/rate_ctr.h> + +#include <osmocom/vty/logging.h> +#include <osmocom/vty/telnet_interface.h> + +#include <openbsc/debug.h> +#include <openbsc/gtphub.h> +#include <openbsc/vty.h> + +#include "../../bscconfig.h" + +#define LOGERR(fmt, args...) \ + LOGP(DGTPHUB, LOGL_ERROR, fmt, ##args) + +#define LOG(fmt, args...) \ + LOGP(DGTPHUB, LOGL_NOTICE, fmt, ##args) + +#ifndef OSMO_VTY_PORT_GTPHUB +/* should come from libosmocore */ +#define OSMO_VTY_PORT_GTPHUB 4253 +#endif + +extern void *osmo_gtphub_ctx; + + +const char *gtphub_copyright = + "Copyright (C) 2015 sysmocom s.f.m.c GmbH info@sysmocom.de\r\n" + "License AGPLv3+: GNU AGPL version 2 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"; + +static struct log_info_cat gtphub_categories[] = { + [DGTPHUB] = { + .name = "DGTPHUB", + .description = "GTP Hub", + .color = "\033[1;33m", + .enabled = 1, .loglevel = LOGL_NOTICE, + }, +}; + +int gtphub_log_filter_fn(const struct log_context *ctx, + struct log_target *tar) +{ + return 0; +} + +static const struct log_info gtphub_log_info = { + .filter_fn = gtphub_log_filter_fn, + .cat = gtphub_categories, + .num_cat = ARRAY_SIZE(gtphub_categories), +}; + +void log_cfg(struct gtphub_cfg *cfg) +{ + struct gtphub_cfg_addr *a; + a = &cfg->to_sgsns[GTPH_PORT_CTRL].bind; + LOG("to-SGSNs bind, Control: %s port %d\n", + a->addr_str, a->port); + a = &cfg->to_sgsns[GTPH_PORT_USER].bind; + LOG("to-SGSNs bind, User: %s port %d\n", + a->addr_str, a->port); + a = &cfg->to_ggsns[GTPH_PORT_CTRL].bind; + LOG("to-GGSNs bind, Control: %s port %d\n", + a->addr_str, a->port); + a = &cfg->to_ggsns[GTPH_PORT_USER].bind; + LOG("to-GGSNs bind, User: %s port %d\n", + a->addr_str, a->port); +} + +static void signal_handler(int signal) +{ + fprintf(stdout, "signal %u received\n", signal); + + switch (signal) { + case SIGINT: + osmo_signal_dispatch(SS_L_GLOBAL, S_L_GLOBAL_SHUTDOWN, NULL); + sleep(1); + exit(0); + break; + case SIGABRT: + /* in case of abort, we want to obtain a talloc report + * and then return to the caller, who will abort the process */ + case SIGUSR1: + case SIGUSR2: + talloc_report_full(osmo_gtphub_ctx, stderr); + break; + default: + break; + } +} + +extern int bsc_vty_go_parent(struct vty *vty); + +static struct vty_app_info vty_info = { + .name = "OsmoGTPhub", + .version = PACKAGE_VERSION, + .go_parent_cb = bsc_vty_go_parent, + .is_config_node = bsc_vty_is_config_node, +}; + +struct cmdline_cfg { + const char *config_file; + int daemonize; +}; + +static void print_help(struct cmdline_cfg *ccfg) +{ + printf("gtphub commandline options\n"); + printf(" -h --help This text.\n"); + printf(" -D --daemonize Fork the process into a background daemon.\n"); + printf(" -d,--debug <cat> Enable Debugging for this category.\n"); + printf(" Pass '-d list' to get a category listing.\n"); + printf(" -s --disable-color"); + printf(" -c --config-file The config file to use [%s].\n", ccfg->config_file); + printf(" -e,--log-level <nr> Set a global log level.\n"); +} + +static void list_categories(void) +{ + printf("Avaliable debug categories:\n"); + int i; + for (i = 0; i < gtphub_log_info.num_cat; ++i) { + if (!gtphub_log_info.cat[i].name) + continue; + + printf("%s\n", gtphub_log_info.cat[i].name); + } +} + +static void handle_options(struct cmdline_cfg *ccfg, int argc, char **argv) +{ + while (1) { + int option_index = 0, c; + static struct option long_options[] = { + {"help", 0, 0, 'h'}, + {"debug", 1, 0, 'd'}, + {"daemonize", 0, 0, 'D'}, + {"config-file", 1, 0, 'c'}, + {"disable-color", 0, 0, 's'}, + {"timestamp", 0, 0, 'T'}, + {"log-level", 1, 0, 'e'}, + {NULL, 0, 0, 0} + }; + + c = getopt_long(argc, argv, "hd:Dc:sTe:", + long_options, &option_index); + if (c == -1) + break; + + switch (c) { + case 'h': + //print_usage(); + print_help(ccfg); + exit(0); + case 's': + log_set_use_color(osmo_stderr_target, 0); + break; + case 'd': + if (strcmp("list", optarg) == 0) { + list_categories(); + exit(0); + } + else + log_parse_category_mask(osmo_stderr_target, optarg); + break; + case 'D': + ccfg->daemonize = 1; + break; + case 'c': + ccfg->config_file = optarg; + break; + case 'T': + log_set_print_timestamp(osmo_stderr_target, 1); + break; + case 'e': + log_set_log_level(osmo_stderr_target, atoi(optarg)); + break; + default: + /* ignore */ + break; + } + } +} + +int main(int argc, char **argv) +{ + int rc; + + osmo_gtphub_ctx = talloc_named_const(NULL, 0, "osmo_gtphub"); + + signal(SIGINT, &signal_handler); + signal(SIGABRT, &signal_handler); + signal(SIGUSR1, &signal_handler); + signal(SIGUSR2, &signal_handler); + osmo_init_ignore_signals(); + + osmo_init_logging(>phub_log_info); + + vty_info.copyright = gtphub_copyright; + vty_init(&vty_info); + logging_vty_add_cmds(>phub_log_info); + gtphub_vty_init(); + + rate_ctr_init(osmo_gtphub_ctx); + rc = telnet_init(osmo_gtphub_ctx, 0, OSMO_VTY_PORT_GTPHUB); + if (rc < 0) + exit(1); + + struct cmdline_cfg _ccfg; + struct cmdline_cfg *ccfg = &_ccfg; + memset(ccfg, '\0', sizeof(*ccfg)); + ccfg->config_file = "./gtphub.conf"; + + struct gtphub_cfg _cfg; + struct gtphub_cfg *cfg = &_cfg; + memset(cfg, '\0', sizeof(*cfg)); + + struct gtphub _hub; + struct gtphub *hub = &_hub; + + handle_options(ccfg, argc, argv); + + rc = gtphub_cfg_read(cfg, ccfg->config_file); + if (rc < 0) { + LOGP(DGTPHUB, LOGL_FATAL, "Cannot parse config file '%s'\n", ccfg->config_file); + exit(2); + } + + if (gtphub_start(hub, cfg) != 0) + return -1; + + log_cfg(cfg); + + if (ccfg->daemonize) { + rc = osmo_daemonize(); + if (rc < 0) { + LOGERR("Error during daemonize"); + exit(1); + } + } + + while (1) { + rc = osmo_select_main(0); + if (rc < 0) + exit(3); + } + + /* not reached */ + exit(0); +} diff --git a/openbsc/src/gprs/gtphub_sep.c b/openbsc/src/gprs/gtphub_sep.c new file mode 100644 index 0000000..bb31834 --- /dev/null +++ b/openbsc/src/gprs/gtphub_sep.c @@ -0,0 +1,26 @@ +/* This file is kept separate so that these functions can be wrapped for + * gtphub_test.c. When a function and its callers are in the same compilational + * unit, the wrappability may be optimized away. */ +#include <string.h> + +#include <openbsc/gtphub.h> +#include <osmocom/core/utils.h> + +#define __llist_first(head) (((head)->next == (head)) ? NULL : (head)->next) +#define llist_first(head, type, entry) llist_entry(__llist_first(head), type, entry) + +int gtphub_resolve_ggsn_addr(struct gtphub *hub, + struct osmo_sockaddr *result, + struct gtp_packet_desc *p) +{ + /* TODO This is just hardcodedly returning the first known address. + * Should resolve from actual subscriber data. */ + struct gtphub_peer *ggsn = llist_first(&hub->to_ggsns[GTPH_PORT_CTRL].peers, + struct gtphub_peer, entry); + if (!ggsn) + return -1; + + memcpy(result, &ggsn->addr, sizeof(struct osmo_sockaddr)); + return 0; +} + diff --git a/openbsc/src/gprs/gtphub_vty.c b/openbsc/src/gprs/gtphub_vty.c new file mode 100644 index 0000000..2227d48 --- /dev/null +++ b/openbsc/src/gprs/gtphub_vty.c @@ -0,0 +1,258 @@ +/* (C) 2015 by sysmocom s.f.m.c. GmbH + * All Rights Reserved + * + * Author: Neels Hofmeyr + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation; either version 3 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +#include <string.h> + +#include <osmocom/core/talloc.h> +#include <osmocom/vty/command.h> + +#include <openbsc/vty.h> +#include <openbsc/gtphub.h> + +static struct gtphub_cfg *g_cfg = 0; + +static struct cmd_node gtphub_node = { + GTPHUB_NODE, + "%s(config-gtphub)# ", + 1, +}; + +#define GTPH_DEFAULT_CONTROL_PORT 2123 +#define GTPH_DEFAULT_USER_PORT 2152 + +static void write_addrs(struct vty *vty, const char *name, + struct gtphub_cfg_addr *c, struct gtphub_cfg_addr *u) +{ + if ((c->port == GTPH_DEFAULT_CONTROL_PORT) + && (u->port == GTPH_DEFAULT_USER_PORT) + && (strcmp(c->addr_str, u->addr_str) == 0)) { + /* Default port numbers and same IP address: write "short" + * variant. */ + vty_out(vty, " %s %s%s", + name, + c->addr_str, + VTY_NEWLINE); + return; + } + + vty_out(vty, " %s ctrl %s %d user %s %d%s", + name, + c->addr_str, (int)c->port, + u->addr_str, (int)u->port, + VTY_NEWLINE); +} + +static int config_write_gtphub(struct vty *vty) +{ + vty_out(vty, "gtphub%s", VTY_NEWLINE); + + write_addrs(vty, "bind-to-sgsns", + &g_cfg->to_sgsns[GTPH_PORT_CTRL].bind, + &g_cfg->to_sgsns[GTPH_PORT_USER].bind); + + write_addrs(vty, "bind-to-ggsns", + &g_cfg->to_ggsns[GTPH_PORT_CTRL].bind, + &g_cfg->to_ggsns[GTPH_PORT_USER].bind); + + if (g_cfg->ggsn_proxy[GTPH_PORT_CTRL].addr_str) { + write_addrs(vty, "ggsn-proxy", + &g_cfg->ggsn_proxy[GTPH_PORT_CTRL], + &g_cfg->ggsn_proxy[GTPH_PORT_USER]); + } + + return CMD_SUCCESS; +} + +DEFUN(cfg_gtphub, cfg_gtphub_cmd, + "gtphub", + "Configure the GTP hub") +{ + vty->node = GTPHUB_NODE; + return CMD_SUCCESS; +} + +#define BIND_ARGS "ctrl ADDR <0-65535> user ADDR <0-65535>" +#define BIND_DOCS \ + "Set GTP-C bind\n" \ + "GTP-C local IP address (v4 or v6)\n" \ + "GTP-C local port\n" \ + "Set GTP-U bind\n" \ + "GTP-U local IP address (v4 or v6)\n" \ + "GTP-U local port\n" + + +DEFUN(cfg_gtphub_bind_to_sgsns_short, cfg_gtphub_bind_to_sgsns_short_cmd, + "bind-to-sgsns ADDR", + "GTP Hub Parameters\n" + "Set the local bind address to listen for SGSNs, for both GTP-C and GTP-U\n" + "Local IP address (v4 or v6)\n" + ) +{ + int i; + for (i = 0; i < GTPH_PORT_N; i++) + g_cfg->to_sgsns[i].bind.addr_str = talloc_strdup(tall_vty_ctx, argv[0]); + g_cfg->to_sgsns[GTPH_PORT_CTRL].bind.port = GTPH_DEFAULT_CONTROL_PORT; + g_cfg->to_sgsns[GTPH_PORT_USER].bind.port = GTPH_DEFAULT_USER_PORT; + return CMD_SUCCESS; +} + +DEFUN(cfg_gtphub_bind_to_ggsns_short, cfg_gtphub_bind_to_ggsns_short_cmd, + "bind-to-ggsns ADDR", + "GTP Hub Parameters\n" + "Set the local bind address to listen for GGSNs, for both GTP-C and GTP-U\n" + "Local IP address (v4 or v6)\n" + ) +{ + int i; + for (i = 0; i < GTPH_PORT_N; i++) + g_cfg->to_ggsns[i].bind.addr_str = talloc_strdup(tall_vty_ctx, argv[0]); + g_cfg->to_ggsns[GTPH_PORT_CTRL].bind.port = GTPH_DEFAULT_CONTROL_PORT; + g_cfg->to_ggsns[GTPH_PORT_USER].bind.port = GTPH_DEFAULT_USER_PORT; + return CMD_SUCCESS; +} + + +static int handle_binds(struct gtphub_cfg_bind *b, const char **argv) +{ + b[GTPH_PORT_CTRL].bind.addr_str = talloc_strdup(tall_vty_ctx, argv[0]); + b[GTPH_PORT_CTRL].bind.port = atoi(argv[1]); + b[GTPH_PORT_USER].bind.addr_str = talloc_strdup(tall_vty_ctx, argv[2]); + b[GTPH_PORT_USER].bind.port = atoi(argv[3]); + return CMD_SUCCESS; +} + +DEFUN(cfg_gtphub_bind_to_sgsns, cfg_gtphub_bind_to_sgsns_cmd, + "bind-to-sgsns " BIND_ARGS, + "GTP Hub Parameters\n" + "Set the local bind addresses and ports to listen for SGSNs\n" + BIND_DOCS + ) +{ + return handle_binds(g_cfg->to_sgsns, argv); +} + +DEFUN(cfg_gtphub_bind_to_ggsns, cfg_gtphub_bind_to_ggsns_cmd, + "bind-to-ggsns " BIND_ARGS, + "GTP Hub Parameters\n" + "Set the local bind addresses and ports to listen for GGSNs\n" + BIND_DOCS + ) +{ + return handle_binds(g_cfg->to_ggsns, argv); +} + +DEFUN(cfg_gtphub_ggsn_proxy_short, cfg_gtphub_ggsn_proxy_short_cmd, + "ggsn-proxy ADDR", + "GTP Hub Parameters\n" + "Redirect all GGSN bound traffic to default ports on this address (another gtphub)\n" + "Remote IP address (v4 or v6)\n" + ) +{ + g_cfg->ggsn_proxy[GTPH_PORT_CTRL].addr_str = talloc_strdup(tall_vty_ctx, argv[0]); + g_cfg->ggsn_proxy[GTPH_PORT_CTRL].port = GTPH_DEFAULT_CONTROL_PORT; + g_cfg->ggsn_proxy[GTPH_PORT_USER].addr_str = talloc_strdup(tall_vty_ctx, argv[0]); + g_cfg->ggsn_proxy[GTPH_PORT_USER].port = GTPH_DEFAULT_USER_PORT; + return CMD_SUCCESS; +} + +DEFUN(cfg_gtphub_ggsn_proxy, cfg_gtphub_ggsn_proxy_cmd, + "ggsn-proxy " BIND_ARGS, + "GTP Hub Parameters\n" + "Redirect all GGSN bound traffic to these addresses and ports (another gtphub)\n" + BIND_DOCS + ) +{ + g_cfg->ggsn_proxy[GTPH_PORT_CTRL].addr_str = talloc_strdup(tall_vty_ctx, argv[0]); + g_cfg->ggsn_proxy[GTPH_PORT_CTRL].port = atoi(argv[1]); + g_cfg->ggsn_proxy[GTPH_PORT_USER].addr_str = talloc_strdup(tall_vty_ctx, argv[2]); + g_cfg->ggsn_proxy[GTPH_PORT_USER].port = atoi(argv[3]); + return CMD_SUCCESS; +} + +DEFUN(cfg_gtphub_sgsn_proxy_short, cfg_gtphub_sgsn_proxy_short_cmd, + "sgsn-proxy ADDR", + "GTP Hub Parameters\n" + "Redirect all SGSN bound traffic to default ports on this address (another gtphub)\n" + "Remote IP address (v4 or v6)\n" + ) +{ + g_cfg->sgsn_proxy[GTPH_PORT_CTRL].addr_str = talloc_strdup(tall_vty_ctx, argv[0]); + g_cfg->sgsn_proxy[GTPH_PORT_CTRL].port = GTPH_DEFAULT_CONTROL_PORT; + g_cfg->sgsn_proxy[GTPH_PORT_USER].addr_str = talloc_strdup(tall_vty_ctx, argv[0]); + g_cfg->sgsn_proxy[GTPH_PORT_USER].port = GTPH_DEFAULT_USER_PORT; + return CMD_SUCCESS; +} + +DEFUN(cfg_gtphub_sgsn_proxy, cfg_gtphub_sgsn_proxy_cmd, + "sgsn-proxy " BIND_ARGS, + "GTP Hub Parameters\n" + "Redirect all SGSN bound traffic to these addresses and ports (another gtphub)\n" + BIND_DOCS + ) +{ + g_cfg->sgsn_proxy[GTPH_PORT_CTRL].addr_str = talloc_strdup(tall_vty_ctx, argv[0]); + g_cfg->sgsn_proxy[GTPH_PORT_CTRL].port = atoi(argv[1]); + g_cfg->sgsn_proxy[GTPH_PORT_USER].addr_str = talloc_strdup(tall_vty_ctx, argv[2]); + g_cfg->sgsn_proxy[GTPH_PORT_USER].port = atoi(argv[3]); + return CMD_SUCCESS; +} + +DEFUN(show_gtphub, show_gtphub_cmd, "show gtphub", + SHOW_STR "Display information about the GTP hub") +{ + vty_out(vty, "gtphub has nothing to say yet%s", VTY_NEWLINE); + return CMD_SUCCESS; +} + + +int gtphub_vty_init(void) +{ + install_element_ve(&show_gtphub_cmd); + + install_element(CONFIG_NODE, &cfg_gtphub_cmd); + install_node(>phub_node, config_write_gtphub); + vty_install_default(GTPHUB_NODE); + + install_element(GTPHUB_NODE, &cfg_gtphub_bind_to_sgsns_short_cmd); + install_element(GTPHUB_NODE, &cfg_gtphub_bind_to_sgsns_cmd); + install_element(GTPHUB_NODE, &cfg_gtphub_bind_to_ggsns_short_cmd); + install_element(GTPHUB_NODE, &cfg_gtphub_bind_to_ggsns_cmd); + install_element(GTPHUB_NODE, &cfg_gtphub_ggsn_proxy_short_cmd); + install_element(GTPHUB_NODE, &cfg_gtphub_ggsn_proxy_cmd); + install_element(GTPHUB_NODE, &cfg_gtphub_sgsn_proxy_short_cmd); + install_element(GTPHUB_NODE, &cfg_gtphub_sgsn_proxy_cmd); + + return 0; +} + +int gtphub_cfg_read(struct gtphub_cfg *cfg, const char *config_file) +{ + int rc; + + g_cfg = cfg; + + rc = vty_read_config_file(config_file, NULL); + if (rc < 0) { + fprintf(stderr, "Failed to parse the config file: '%s'\n", config_file); + return rc; + } + + return 0; +} diff --git a/openbsc/tests/Makefile.am b/openbsc/tests/Makefile.am index 1b557d4..aacfe0b 100644 --- a/openbsc/tests/Makefile.am +++ b/openbsc/tests/Makefile.am @@ -1,4 +1,4 @@ -SUBDIRS = gsm0408 db channel mgcp gprs abis gbproxy trau subscr oap +SUBDIRS = gsm0408 db channel mgcp gprs abis gbproxy trau subscr oap gtphub
if BUILD_NAT SUBDIRS += bsc-nat bsc-nat-trie diff --git a/openbsc/tests/gtphub/Makefile.am b/openbsc/tests/gtphub/Makefile.am new file mode 100644 index 0000000..ecc6d62 --- /dev/null +++ b/openbsc/tests/gtphub/Makefile.am @@ -0,0 +1,20 @@ +AM_CPPFLAGS = $(all_includes) -I$(top_srcdir)/include +AM_CFLAGS=-Wall -ggdb3 $(LIBOSMOCORE_CFLAGS) + +EXTRA_DIST = \ + gtphub_test.ok \ + gtphub_nc_test.sh \ + gtphub_nc_test.ok \ + hex2bin.py + +noinst_PROGRAMS = gtphub_test + +gtphub_test_SOURCES = gtphub_test.c +gtphub_test_LDFLAGS = \ + -Wl,--wrap=gtphub_resolve_ggsn_addr + +gtphub_test_LDADD = \ + $(top_builddir)/src/gprs/gtphub.o \ + $(LIBOSMOCORE_LIBS) \ + -lgtp -lrt + diff --git a/openbsc/tests/gtphub/gtphub_nc_test.gtphub.conf b/openbsc/tests/gtphub/gtphub_nc_test.gtphub.conf new file mode 100644 index 0000000..17cc756 --- /dev/null +++ b/openbsc/tests/gtphub/gtphub_nc_test.gtphub.conf @@ -0,0 +1,5 @@ +gtphub + bind-to-sgsns ctrl 127.0.0.1 21231 user 127.0.0.1 21521 + bind-to-ggsns ctrl 127.0.0.1 21232 user 127.0.0.1 21522 + ggsn-proxy 127.0.0.1 +end diff --git a/openbsc/tests/gtphub/gtphub_nc_test.ok b/openbsc/tests/gtphub/gtphub_nc_test.ok new file mode 100644 index 0000000..6a6db53 --- /dev/null +++ b/openbsc/tests/gtphub/gtphub_nc_test.ok @@ -0,0 +1,7 @@ +--- recv_server: +32 01 00 04 00 00 00 00 6d 31 00 00 +OK +--- recv_client: +32 02 00 06 00 00 00 00 7c 00 00 00 0e 01 +OK +done diff --git a/openbsc/tests/gtphub/gtphub_nc_test.sh b/openbsc/tests/gtphub/gtphub_nc_test.sh new file mode 100755 index 0000000..febb5a7 --- /dev/null +++ b/openbsc/tests/gtphub/gtphub_nc_test.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# gtphub_nc_test.sh + +# TODO does this work with all relevant netcat implementations? +# TODO skip if netcat not found? +# TODO use only 127.0.0.1 once gtphub is configurable. + +dump() { + # echo result to strip trailing space + echo $(hexdump -v -e '/1 "%02x "' $@) +} + +sendhex() { + hex="$1" + from_port="$2" + to_port="$3" + echo "$hex" | ./hex2bin.py | nc --send-only -u -s 127.0.0.1 -p "$from_port" 127.0.0.1 "$to_port" +} + +gtphub_bin="./osmo-gtphub" +if [ ! -x "$gtphub_bin" ]; then + echo "executable not found: $gtphub_bin" + exit 1; +fi + +# client osmo-gtphub gtp server +# 127.0.0.1:9876 <--> 127.0.0.1:21231 | 127.0.0.1:21232 <--> 127.0.0.1 2123 +# (netcat) ($gtphub_bin) (netcat) + +# start gtphub relay +"$gtphub_bin" -c gtphub.conf & +sleep 0.1 + +# log what reaches client and server +nc --recv-only -u -l -p 9876 -s 127.0.0.1 > recv_client & +nc --recv-only -u -l -p 2123 -s 127.0.0.1 > recv_server & +sleep .1 + +# send test messages, both ways... +# When sending the ping, the sequence number gets mapped (7c00 -> 6d31). +# In the pong, the mapped sequence nr is sent and gets mapped backwards. +gtp_ping_seq1="32 01 00 04 00 00 00 00 7c 00 00 00" +gtp_ping_seq2="32 01 00 04 00 00 00 00 6d 31 00 00" +gtp_pong_seq2="32 02 00 06 00 00 00 00 6d 31 00 00 0e 01" +gtp_pong_seq1="32 02 00 06 00 00 00 00 7c 00 00 00 0e 01" + +sendhex "$gtp_ping_seq1" 9876 21231 + +# server sends reply with the mapped sequence nr., but wrong sender in terms of +# the configured GGSN proxy address. +sendhex "$gtp_pong_seq2" 7777 21232 + +# server sends reply with the mapped sequence nr., correct sender +sendhex "$gtp_pong_seq2" 2123 21232 + +sleep .1 +kill %1 %2 %3 + +# log what has reached the server and client ends, matched against +# gtphub_nc_test.ok +retval=0 +rx_srv="$(dump recv_server)" +echo "--- recv_server:" +echo "$rx_srv" + +if [ "$rx_srv" = "$gtp_ping_seq2" ]; then + echo "OK" +else + echo "*** FAILURE" + retval=1 +fi + +rx_clt="$(dump recv_client)" +echo "--- recv_client:" +echo "$rx_clt" + +if [ "$rx_clt" = "$gtp_pong_seq1" ]; then + echo "OK" +else + echo "*** FAILURE" + retval=2 +fi + +echo "done" +exit "$retval" diff --git a/openbsc/tests/gtphub/gtphub_test.c b/openbsc/tests/gtphub/gtphub_test.c new file mode 100644 index 0000000..5a5c4d1 --- /dev/null +++ b/openbsc/tests/gtphub/gtphub_test.c @@ -0,0 +1,675 @@ +/* Test the GTP hub */ + +/* (C) 2015 by sysmocom s.f.m.c. GmbH + * All Rights Reserved + * + * Author: Neels Hofmeyr nhofmeyr@sysmcom.de + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation; either version 3 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +#include <stdio.h> +#include <string.h> +#include <limits.h> +#include <unistd.h> + +#include <osmocom/core/utils.h> +#include <osmocom/core/msgb.h> +#include <osmocom/core/application.h> + +#include <openbsc/debug.h> + +#include <openbsc/gtphub.h> +#include <gtp.h> +#include <gtpie.h> + +#define EXPIRE_ALL (gtphub_now() + (60 * GTPH_TEI_MAPPING_EXPIRY_MINUTES) + 1) + +/* Make non-public API accessible */ + +void gtphub_init(struct gtphub *hub); + +int gtphub_from_sgsns_handle_buf(struct gtphub *hub, + unsigned int port_idx, + const struct osmo_sockaddr *from_addr, + uint8_t *buf, + size_t received, + struct osmo_fd **to_ofd, + struct osmo_sockaddr **to_addr); + +int gtphub_from_ggsns_handle_buf(struct gtphub *hub, + unsigned int port_idx, + const struct osmo_sockaddr *from_addr, + uint8_t *buf, + size_t received, + struct osmo_fd **to_ofd, + struct osmo_sockaddr **to_addr); + +void *osmo_gtphub_ctx; + +/* TODO copied from libosmo-abis/src/subchan_demux.c, remove dup */ +static int llist_len(struct llist_head *head) +{ + struct llist_head *entry; + int i = 0; + + llist_for_each(entry, head) + i++; + + return i; +} + +static void nr_mapping_free(struct nr_mapping *m) +{ + talloc_free(m); +} + +static struct nr_mapping *nr_mapping_alloc(void) +{ + struct nr_mapping *m; + m = talloc(osmo_gtphub_ctx, struct nr_mapping); + nr_mapping_init(m); + m->del_cb = nr_mapping_free; + return m; +} + +static struct nr_mapping *nr_map_have(struct nr_map *map, void *origin, nr_t orig, time_t now) +{ + struct nr_mapping *mapping; + + mapping = nr_map_get(map, origin, orig); + if (!mapping) { + mapping = nr_mapping_alloc(); + mapping->origin = origin; + mapping->orig = orig; + nr_map_add(map, mapping, now); + } + + return mapping; +} + +static nr_t nr_map_verify(const struct nr_map *map, void *origin, nr_t orig, nr_t expect_repl) +{ + struct nr_mapping *m; + m = nr_map_get(map, origin, orig); + + if (!m) { + printf("mapping not found for %p %d\n", origin, orig); + return 0; + } + + if (m->repl != expect_repl) { + printf("mapping found, but nr mismatches: expect %d, got %d\n", + (int)expect_repl, (int)m->repl); + return 0; + } + + return 1; +} + +static int nr_map_verify_inv(const struct nr_map *map, nr_t repl, + void *expect_origin, nr_t expect_orig) +{ + struct nr_mapping *m; + m = nr_map_get_inv(map, repl); + if (!m) { + printf("mapping not found for %d\n", (int)repl); + return 0; + } + + if (m->origin != expect_origin) { + printf("mapping found, but origin mismatches: expect %p, got %p\n", + expect_origin, m->origin); + return 0; + } + + if (m->orig != expect_orig) { + printf("mapping found, but nr mismatches: expect %d, got %d\n", + (int)expect_orig, (int)m->orig); + return 0; + } + + return 1; +} + + +static void test_nr_map_basic(void) +{ + struct nr_pool _pool; + struct nr_pool *pool = &_pool; + struct nr_map _map; + struct nr_map *map = &_map; + + nr_pool_init(pool); + nr_map_init(map, pool, NULL); + + OSMO_ASSERT(llist_empty(&map->mappings)); + +#define TEST_N_HALF 100 +#define TEST_N (2*TEST_N_HALF) +#define TEST_I 123 + uint32_t i, check_i; + uint32_t m[TEST_N]; + struct nr_mapping *mapping; + + /* create half of TEST_N mappings from one origin */ + void *origin1 = (void*)0x1234; + for (i = 0; i < TEST_N_HALF; i++) { + nr_t orig = TEST_I + i; + mapping = nr_map_have(map, origin1, orig, 0); + m[i] = mapping->repl; + OSMO_ASSERT(m[i] != 0); + OSMO_ASSERT(llist_len(&map->mappings) == (i+1)); + for (check_i = 0; check_i < i; check_i++) + OSMO_ASSERT(m[check_i] != m[i]); + } + OSMO_ASSERT(llist_len(&map->mappings) == TEST_N_HALF); + + /* create another TEST_N mappings with the same original numbers, but + * from a different origin */ + void *origin2 = (void*)0x5678; + for (i = 0; i < TEST_N_HALF; i++) { + int i2 = TEST_N_HALF + i; + nr_t orig = TEST_I + i; + mapping = nr_map_have(map, origin2, orig, 0); + m[i2] = mapping->repl; + OSMO_ASSERT(m[i2] != 0); + OSMO_ASSERT(llist_len(&map->mappings) == (i2+1)); + for (check_i = 0; check_i < i2; check_i++) + OSMO_ASSERT(m[check_i] != m[i2]); + } + OSMO_ASSERT(llist_len(&map->mappings) == TEST_N); + + /* verify mappings */ + for (i = 0; i < TEST_N_HALF; i++) { + nr_t orig = TEST_I + i; + { + OSMO_ASSERT(nr_map_verify(map, origin1, orig, m[i])); + OSMO_ASSERT(nr_map_verify_inv(map, m[i], origin1, orig)); + } + { + int i2 = TEST_N_HALF + i; + OSMO_ASSERT(nr_map_verify(map, origin2, orig, m[i2])); + OSMO_ASSERT(nr_map_verify_inv(map, m[i2], origin2, orig)); + } + } + + /* remove all mappings */ + for (i = 0; i < TEST_N_HALF; i++) { + OSMO_ASSERT(llist_len(&map->mappings) == (TEST_N - 2*i)); + + nr_t orig = TEST_I + i; + nr_mapping_del(nr_map_get(map, origin1, orig)); + nr_mapping_del(nr_map_get(map, origin2, orig)); + } + OSMO_ASSERT(llist_empty(&map->mappings)); +#undef TEST_N +#undef TEST_I +} + +static int seqmap_is(struct nr_map *map, const char *str) +{ + static char buf[4096]; + char *pos = buf; + size_t len = sizeof(buf); + struct nr_mapping *m; + llist_for_each_entry(m, &map->mappings, entry) { + size_t wrote = snprintf(pos, len, "(%d->%d@%d), ", + (int)m->orig, + (int)m->repl, + (int)m->expiry); + OSMO_ASSERT(wrote < len); + pos += wrote; + len -= wrote; + } + *pos = '\0'; + + if (strncmp(buf, str, sizeof(buf)) != 0) { + printf("FAILURE: seqmap_is() mismatches expected value:\n" + "expected: %s\n" + "is: %s\n", + str, buf); + return 0; + } + return 1; +} + +static void test_nr_map_expiry(void) +{ + struct nr_map_expiry expiry; + struct nr_pool pool; + struct nr_map map; + int i; + + nr_map_expiry_init(&expiry, 30); + nr_pool_init(&pool); + nr_map_init(&map, &pool, &expiry); + OSMO_ASSERT(seqmap_is(&map, "")); + + /* tick on empty map */ + OSMO_ASSERT(nr_map_expiry_tick(&expiry, 10000) == 0); + OSMO_ASSERT(seqmap_is(&map, "")); + +#define MAP1 \ + "(10->1@10040), " \ + "" + +#define MAP2 \ + "(20->2@10050), " \ + "(21->3@10051), " \ + "(22->4@10052), " \ + "(23->5@10053), " \ + "(24->6@10054), " \ + "(25->7@10055), " \ + "(26->8@10056), " \ + "(27->9@10057), " \ + "" + +#define MAP3 \ + "(420->10@10072), " \ + "(421->11@10072), " \ + "(422->12@10072), " \ + "(423->13@10072), " \ + "(424->14@10072), " \ + "(425->15@10072), " \ + "(426->16@10072), " \ + "(427->17@10072), " \ + "" + + /* add mapping at time 10010. */ + nr_map_have(&map, 0, 10, 10010); + OSMO_ASSERT(seqmap_is(&map, MAP1)); + + /* tick on unexpired item. */ + OSMO_ASSERT(nr_map_expiry_tick(&expiry, 10010) == 0); + OSMO_ASSERT(nr_map_expiry_tick(&expiry, 10011) == 0); + OSMO_ASSERT(seqmap_is(&map, MAP1)); + + /* Spread mappings at 10020, 10021, ... 10027. */ + for (i = 0; i < 8; i++) + nr_map_have(&map, 0, 20 + i, 10020 + i); + OSMO_ASSERT(seqmap_is(&map, MAP1 MAP2)); + + /* tick on unexpired items. */ + OSMO_ASSERT(nr_map_expiry_tick(&expiry, 10030) == 0); + OSMO_ASSERT(nr_map_expiry_tick(&expiry, 10039) == 0); + OSMO_ASSERT(seqmap_is(&map, MAP1 MAP2)); + + /* expire the first item (from 10010). */ + OSMO_ASSERT(nr_map_expiry_tick(&expiry, 10010 + 30) == 1); + OSMO_ASSERT(seqmap_is(&map, MAP2)); + + /* again nothing to expire */ + OSMO_ASSERT(nr_map_expiry_tick(&expiry, 10041) == 0); + OSMO_ASSERT(seqmap_is(&map, MAP2)); + + /* Mappings all at the same time. */ + for (i = 0; i < 8; i++) + nr_map_have(&map, 0, 420 + i, 10042); + OSMO_ASSERT(seqmap_is(&map, MAP2 MAP3)); + + /* Eight to expire, were added further above to be chronologically + * correct, at 10020..10027. */ + OSMO_ASSERT(nr_map_expiry_tick(&expiry, 10027 + 30) == 8); + OSMO_ASSERT(seqmap_is(&map, MAP3)); + + /* again nothing to expire */ + OSMO_ASSERT(nr_map_expiry_tick(&expiry, 10027 + 30) == 0); + OSMO_ASSERT(seqmap_is(&map, MAP3)); + + /* Eight to expire, from 10042. Now at 10042 + 30: */ + OSMO_ASSERT(nr_map_expiry_tick(&expiry, 10042 + 30) == 8); + OSMO_ASSERT(seqmap_is(&map, "")); + +#undef MAP1 +#undef MAP2 +#undef MAP3 +} + + + +/* override, requires '-Wl,--wrap=gtphub_resolve_ggsn_addr' */ +int __real_gtphub_resolve_ggsn_addr(struct gtphub *hub, + struct osmo_sockaddr *result, + struct gtp_packet_desc *p); + +struct osmo_sockaddr resolved_ggsn_addr = {.l = 0}; +int __wrap_gtphub_resolve_ggsn_addr(struct gtphub *hub, + struct osmo_sockaddr *result, + struct gtp_packet_desc *p) +{ + osmo_sockaddr_copy(result, &resolved_ggsn_addr); + printf("Wrap: returning GGSN addr: %s\n", + osmo_sockaddr_to_str(result)); + return (resolved_ggsn_addr.l != 0)? 0 : -1; +} + +#define buf_len 1024 +static uint8_t buf[buf_len]; + +static unsigned int msg(const char *hex) +{ + unsigned int l = osmo_hexparse(hex, buf, buf_len); + OSMO_ASSERT(l > 0); + return l; +} + +/* Compare static buf to given string constant. The amount of bytes is obtained + * from parsing the GTP header in buf. hex must match an osmo_hexdump() of the + * desired message. Return 1 if size and content match. */ +#define msg_is(MSG) _msg_is(MSG, __FILE__, __LINE__) +static int _msg_is(const char *hex, const char *file, int line) +{ + struct gtp1_header_long *h = (void*)buf; + int len = ntoh16(h->length) + 8; + const char *dump = osmo_hexdump_nospc(buf, len); + int cmp = strcmp(dump, hex); + + if (cmp != 0) { + printf("\n%s:%d: msg_is(): MISMATCH\n" + " expecting:\n'%s'\n" + " got:\n'%s'\n", + file, + line, + hex, dump); + int i; + int l = strlen(hex); + int m = strlen(dump); + if (m < l) + l = m; + for (i = 0; i < l; i++) { + if (hex[i] != dump[i]) { + printf("First mismatch at position %d:\n" + " %s\n %s\n", i, hex + i, dump + i); + break; + } + } + } + return cmp == 0; +} + +#define same_addr(GOT, EXPECTED) _same_addr((GOT),(EXPECTED), __FILE__, __LINE__) +static int _same_addr(const struct osmo_sockaddr *got, + const struct osmo_sockaddr *expected, + const char *file, int line) +{ + int cmp = osmo_sockaddr_cmp(got, expected); + if (!cmp) + return 1; + char buf[256]; + printf("\n%s:%d: addr_is(): MISMATCH\n" + " expecting: '%s'\n" + " got: '%s'\n", + file, line, + osmo_sockaddr_to_str(expected), + osmo_sockaddr_to_strb(got, buf, sizeof(buf))); + return 0; +} + +static void test_echo(void) +{ + struct gtphub _hub; + struct gtphub *hub = &_hub; + + gtphub_init(hub); + + const char *gtp_ping_from_sgsn = + "32" /* 0b001'1 0010: version 1, protocol GTP, with seq nr. */ + "01" /* type 01: Echo request */ + "0004" /* length of 4 after header TEI */ + "00000000" /* header TEI == 0 in Echo */ + "abcd" /* some 16 octet sequence nr */ + "0000" /* N-PDU 0, no extension header (why is this here?) */ + ; + + /* Same with mapped sequence number */ + const char *gtp_ping_to_ggsn = + "32" "01" "0004" "00000000" + "6d31" /* mapped seq */ + "00" "00"; + + const char *gtp_pong_from_ggsn = + "32" + "02" /* type 02: Echo response */ + "0006" /* len */ + "00000000" /* tei */ + "6d31" /* mapped seq */ + "0000" /* ext */ + "0e01" /* 0e: Recovery, val == 1 */ + ; + /* Same with unmapped sequence number */ + const char *gtp_pong_to_sgsn = + "32" "02" "0006" "00000000" + "abcd" /* unmapped seq */ + "00" "00" "0e01"; + + /* Set the GGSN address that gtphub is forced to resolve to. */ + OSMO_ASSERT(osmo_sockaddr_init_udp(&resolved_ggsn_addr, + "192.168.43.34", 434) + == 0); + + /* according to spec, we'd always send to port 2123 instead... + struct osmo_sockaddr ggsn_standard_port; + OSMO_ASSERT(osmo_sockaddr_init_udp(&ggsn_standard_port, + "192.168.43.34", 2123) + == 0); + */ + + struct osmo_sockaddr orig_sgsn_addr; + OSMO_ASSERT(osmo_sockaddr_init(&orig_sgsn_addr, + AF_UNSPEC, SOCK_DGRAM, IPPROTO_UDP, + "192.168.42.23", 423) == 0); + struct osmo_fd *ggsn_ofd = NULL; + struct osmo_sockaddr *ggsn_addr = NULL; + int send; + send = gtphub_from_sgsns_handle_buf(hub, GTPH_PORT_CTRL, &orig_sgsn_addr, + buf, msg(gtp_ping_from_sgsn), + &ggsn_ofd, &ggsn_addr); + OSMO_ASSERT(send > 0); + OSMO_ASSERT(ggsn_addr); + OSMO_ASSERT(same_addr(ggsn_addr, &resolved_ggsn_addr)); + OSMO_ASSERT(msg_is(gtp_ping_to_ggsn)); + + struct osmo_fd *sgsn_ofd; + struct osmo_sockaddr *sgsn_addr; + send = gtphub_from_ggsns_handle_buf(hub, GTPH_PORT_CTRL, ggsn_addr, + buf, msg(gtp_pong_from_ggsn), + &sgsn_ofd, &sgsn_addr); + OSMO_ASSERT(send > 0); + OSMO_ASSERT(sgsn_addr); + OSMO_ASSERT(same_addr(sgsn_addr, &orig_sgsn_addr)); + OSMO_ASSERT(msg_is(gtp_pong_to_sgsn)); + + gtphub_gc(hub, EXPIRE_ALL); +} + +static void test_create_pdp_ctx(void) +{ + struct gtphub _hub; + struct gtphub *hub = &_hub; + + gtphub_init(hub); + + /* This is copied from a packet that sgsnemu sends. */ + const char *gtp_req_from_sgsn = + "32" /* 0b001'1 0010: version 1, protocol GTP, with seq nr. */ + "10" /* type 16: Create PDP Context Request */ + "0067" /* length = 8 + 103 */ + "00000000" /* No TEI yet */ + "abcd" /* Sequence nr */ + "00" /* N-PDU 0 */ + "00" /* No extensions */ + /* IEs */ + "02" /* 2 = IMSI */ + "42000121436587f9" + "0e" "60" /* 14: Recovery = 96 */ + "0f01" /* 15: Selection mode = MS provided APN, subscription not verified*/ + "10" /* 16: TEI Data I */ + "00000123" + "11" /* 17: TEI Control Plane */ + "00000321" + "1400" /* 20: NSAPI = 0*/ + "1a" /* 26: Charging Characteristics */ + "0800" + "80" /* 128: End User Address */ + "0002" /* length = 2: empty PDP Address */ + "f121" /* spare 0xf0, PDP organization 1, PDP type number 0x21 = 33 */ + "83" /* 131: Access Point Name */ + "0008" /* length = 8 */ + "696e7465726e6574" /* "internet" */ + "84" /* 132: Protocol Configuration Options */ + "0015" /* length = 21 */ + "80c0231101010011036d69670868656d6d656c6967" + "85" /* 133: GSN Address */ + "0004" /* length */ + "7f000001" + "85" /* 133: GSN Address (second entry) */ + "0004" /* length */ + "7f000001" + "86" /* 134: MS International PSTN/ISDN Number (MSISDN) */ + "0007" /* length */ + "916407123254f6" /* 1946702123456(f) */ + "87" /* 135: Quality of Service (QoS) Profile */ + "0004" /* length */ + "00" /* priority */ + "0b921f" /* QoS profile data */ + ; + + const char *gtp_req_to_ggsn = + "32" "10" "0067" "00000000" + "6d31" /* mapped seq ("abcd") */ + "00" "00" "02" "42000121436587f9" "0e60" "0f01" + "10" "00000001" /* mapped TEI Data I ("123") */ + "11" "00000001" /* mapped TEI Control ("321") */ + "1400" "1a" "0800" "80" "0002" "f121" "83" + "0008" "696e7465726e6574" "84" "0015" + "80c0231101010011036d69670868656d6d656c6967" "85" "0004" + "7f000001" "85" "0004" "7f000001" "86" "0007" "916407123254f6" + "87" "0004" "00" "0b921f" + ; + + const char *gtp_resp_from_ggsn = + "32" + "11" /* Create PDP Context Response */ + "004e" /* length = 78 + 8 */ + "00000001" /* destination TEI (sent in req above) */ + "6d31" /* mapped seq */ + "00" "00" + /* IEs */ + "01" /* 1: Cause */ + "80" /* value = 0b10000000 = response, no rejection. */ + "08" /* 8: Reordering Required */ + "00" /* not required. */ + "0e" "01" /* 14: Recovery = 1 */ + "10" /* 16: TEI Data I */ + "00000567" + "11" /* 17: TEI Control */ + "00000765" + "7f" /* 127: Charging ID */ + "00000001" + "80" /* 128: End User Address */ + "0006" /* length = 6 */ + "f121" /* spare 0xf0, PDP organization 1, PDP type number 0x21 = 33 */ + "7f000002" + "84" /* 132: Protocol Configuration Options */ + "0014" /* len = 20 */ + "8080211002000010810608080808830600000000" + "85" /* 133: GSN Address */ + "0004" /* length */ + "7f000002" + "85" /* 133: GSN Address (again) */ + "0004" /* length */ + "7f000002" + "87" /* 135: Quality of Service (QoS) Profile */ + "0004" /* length */ + "00" /* priority */ + "0b921f" /* QoS profile data */ + ; + + const char *gtp_resp_to_sgsn = + "32" "11" "004e" + "00000321" /* unmapped TEI ("001") */ + "abcd" /* unmapped seq ("6d31") */ + "00" "00" "01" "80" "08" "00" "0e" "01" + "10" "00000001" /* mapped TEI from GGSN ("567") */ + "11" "00000001" /* mapped TEI from GGSN ("765") */ + "7f" "00000001" "80" "0006" "f121" "7f000002" "84" "0014" + "8080211002000010810608080808830600000000" "85" "0004" + "7f000002" "85" "0004" "7f000002" "87" "0004" "00" "0b921f" + ; + + /* Set the GGSN address that gtphub is forced to resolve to. */ + OSMO_ASSERT(osmo_sockaddr_init_udp(&resolved_ggsn_addr, + "192.168.43.34", 434) + == 0); + + struct osmo_sockaddr orig_sgsn_addr; + OSMO_ASSERT(osmo_sockaddr_init(&orig_sgsn_addr, + AF_UNSPEC, SOCK_DGRAM, IPPROTO_UDP, + "192.168.42.23", 423) == 0); + struct osmo_fd *ggsn_ofd = NULL; + struct osmo_sockaddr *ggsn_addr = NULL; + int send; + send = gtphub_from_sgsns_handle_buf(hub, GTPH_PORT_CTRL, &orig_sgsn_addr, + buf, msg(gtp_req_from_sgsn), + &ggsn_ofd, &ggsn_addr); + OSMO_ASSERT(send > 0); + OSMO_ASSERT(ggsn_addr); + OSMO_ASSERT(same_addr(ggsn_addr, &resolved_ggsn_addr)); + OSMO_ASSERT(msg_is(gtp_req_to_ggsn)); + + struct osmo_fd *sgsn_ofd; + struct osmo_sockaddr *sgsn_addr; + send = gtphub_from_ggsns_handle_buf(hub, GTPH_PORT_CTRL, ggsn_addr, + buf, msg(gtp_resp_from_ggsn), + &sgsn_ofd, &sgsn_addr); + OSMO_ASSERT(send > 0); + OSMO_ASSERT(sgsn_addr); + OSMO_ASSERT(same_addr(sgsn_addr, &orig_sgsn_addr)); + OSMO_ASSERT(msg_is(gtp_resp_to_sgsn)); + + gtphub_gc(hub, EXPIRE_ALL); +} + +static struct log_info_cat gtphub_categories[] = { + [DGTPHUB] = { + .name = "DGTPHUB", + .description = "GTP Hub", + .color = "\033[1;33m", + .enabled = 1, .loglevel = LOGL_NOTICE, + }, +}; + +static struct log_info info = { + .cat = gtphub_categories, + .num_cat = ARRAY_SIZE(gtphub_categories), +}; + +int main(int argc, char **argv) +{ + osmo_init_logging(&info); + osmo_gtphub_ctx = talloc_named_const(NULL, 0, "osmo_gtphub"); + + test_nr_map_basic(); + test_nr_map_expiry(); + test_echo(); + test_create_pdp_ctx(); + printf("Done\n"); + + talloc_report_full(osmo_gtphub_ctx, stderr); + OSMO_ASSERT(talloc_total_blocks(osmo_gtphub_ctx) == 1); + return 0; +} + diff --git a/openbsc/tests/gtphub/gtphub_test.ok b/openbsc/tests/gtphub/gtphub_test.ok new file mode 100644 index 0000000..7be13fe --- /dev/null +++ b/openbsc/tests/gtphub/gtphub_test.ok @@ -0,0 +1,3 @@ +Wrap: returning GGSN addr: 192.168.43.34 port 434 +Wrap: returning GGSN addr: 192.168.43.34 port 434 +Done diff --git a/openbsc/tests/gtphub/hex2bin.py b/openbsc/tests/gtphub/hex2bin.py new file mode 100755 index 0000000..6a906b1 --- /dev/null +++ b/openbsc/tests/gtphub/hex2bin.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python + +import sys + +blob = [] + +for l in sys.stdin.readlines(): + l = ''.join(c for c in l if not c.isspace()) + + for i in range(0, len(l), 2): + h = l[i:i+2] + sys.stdout.write(chr(int(h, 16))) + diff --git a/openbsc/tests/testsuite.at b/openbsc/tests/testsuite.at index 78aa47e..3a5883e 100644 --- a/openbsc/tests/testsuite.at +++ b/openbsc/tests/testsuite.at @@ -110,3 +110,15 @@ AT_CHECK([test "$enable_oap_test" != no || exit 77]) cat $abs_srcdir/oap/oap_test.ok > expout AT_CHECK([$abs_top_builddir/tests/oap/oap_test], [], [expout], [ignore]) AT_CLEANUP + +AT_SETUP([gtphub]) +AT_KEYWORDS([gtphub]) +AT_CHECK([test "$enable_gtphub_test" != no || exit 77]) +cat $abs_srcdir/gtphub/gtphub_test.ok > expout +AT_CHECK([$abs_top_builddir/tests/gtphub/gtphub_test], [], [expout], [ignore]) +cat $abs_srcdir/gtphub/gtphub_nc_test.ok > expout +cat $abs_srcdir/gtphub/gtphub_nc_test.gtphub.conf > gtphub.conf +ln -s $abs_srcdir/gtphub/hex2bin.py . +ln -s $abs_top_builddir/src/gprs/osmo-gtphub . +AT_CHECK([$abs_top_builddir/tests/gtphub/gtphub_nc_test.sh], [], [expout], [ignore]) +AT_CLEANUP
On Mon, Nov 02, 2015 at 08:20:39PM +0100, Holger Freyther wrote:
To reiterate, I'd like to know whether gtphub should/really must ;) be changed to the latter style.
yes.
ok, the patch I just sent still uses my style though. I'll modify it later. And also I'm aware that my special LOG() makros are still in there, too. Not forgotten. Hope that doesn't harm reviews.
~Neels
Hi Neels,
I'm trying to understand how you handle a PDP Create Context message where the SGSN Address for control plane and an SGSN address for user traffic differ from the sender IP?
Background:
3GPP TS 29.060 version 12.9.0 Release 12, Section 7.3.2 Create PDP Context Request has this to say about the SGSN Addresses:
The SGSN shall include an SGSN Address for control plane and an SGSN address for user traffic, which may differ from that provided by the underlying network service (e.g. IP).
To me the implies that a maximum of four IP's need to handled per PDP Context. Those are the two GTP-C/U sender IP's the SGSN uses to talk to us and the two GTP-C/U IP's we are supposed to use when talking to the SGSN.
The same applies to the GGSN connection.
Any thoughts on this?
Andreas
On 11/04/2015 02:56 AM, Neels Hofmeyr wrote:
I can talk all day about atomic commits, but chunks of gtphub have transformed twice over (that's me figuring out how to map and resolve GTP data elements), so that half the patches are obsoleted by later ones, in a hard-to-rebase way.
So here is a joint patch of gtphub for review. If anyone requests it, I will gladly separate it into a handful of patches adding each subsection / family of API at a time, to better show where each part reaches. Just ask...
Many thanks for any reviews!
~Neels
Neels Hofmeyr (1): Add GTP hub (code bomb).
openbsc/.gitignore | 2 + openbsc/configure.ac | 1 + openbsc/include/openbsc/Makefile.am | 1 + openbsc/include/openbsc/debug.h | 1 + openbsc/include/openbsc/gtphub.h | 345 +++++ openbsc/include/openbsc/vty.h | 1 + openbsc/src/gprs/Makefile.am | 6 + openbsc/src/gprs/gtphub.c | 1794 +++++++++++++++++++++++ openbsc/src/gprs/gtphub_main.c | 283 ++++ openbsc/src/gprs/gtphub_sep.c | 26 + openbsc/src/gprs/gtphub_vty.c | 258 ++++ openbsc/tests/Makefile.am | 2 +- openbsc/tests/gtphub/Makefile.am | 20 + openbsc/tests/gtphub/gtphub_nc_test.gtphub.conf | 5 + openbsc/tests/gtphub/gtphub_nc_test.ok | 7 + openbsc/tests/gtphub/gtphub_nc_test.sh | 85 ++ openbsc/tests/gtphub/gtphub_test.c | 675 +++++++++ openbsc/tests/gtphub/gtphub_test.ok | 3 + openbsc/tests/gtphub/hex2bin.py | 13 + openbsc/tests/testsuite.at | 12 + 20 files changed, 3539 insertions(+), 1 deletion(-) create mode 100644 openbsc/include/openbsc/gtphub.h create mode 100644 openbsc/src/gprs/gtphub.c create mode 100644 openbsc/src/gprs/gtphub_main.c create mode 100644 openbsc/src/gprs/gtphub_sep.c create mode 100644 openbsc/src/gprs/gtphub_vty.c create mode 100644 openbsc/tests/gtphub/Makefile.am create mode 100644 openbsc/tests/gtphub/gtphub_nc_test.gtphub.conf create mode 100644 openbsc/tests/gtphub/gtphub_nc_test.ok create mode 100755 openbsc/tests/gtphub/gtphub_nc_test.sh create mode 100644 openbsc/tests/gtphub/gtphub_test.c create mode 100644 openbsc/tests/gtphub/gtphub_test.ok create mode 100755 openbsc/tests/gtphub/hex2bin.py
On Wed, Nov 04, 2015 at 10:20:45AM +0100, Andreas Schultz wrote:
Hi Neels,
I'm trying to understand how you handle a PDP Create Context message where the SGSN Address for control plane and an SGSN address for user traffic differ from the sender IP?
That part is ultimately still missing, as may be visible from the PDP Context unit test. So far it's recording only the actual originating address as returned by the socket read. It's my plan for today.
But the internal storage is ready. gtphub will examine the IEs SGSN Address for signalling SGSN Address for user traffic and GGSN Address for Control Plane GGSN Address for user traffic and store these addresses for future use. The addresses are stored separately, with association pointers from one to the other.
There's a bit of a handwavy situation there, e.g. if an SGSN sends a PDP Create request from 1.2.3.4, but the address in the IE is 5.6.7.8. gtphub later on needs to find the proper peer record; if I stored a peer for 5.6.7.8 and the SGSN again were to send from 1.2.3.4, gtphub would not be able to match that up. I will so far only store the IE's address (5.6.7.8), and expect the SGSN to continue sending ctrl messages for this PDP context from 5.6.7.8 from then on. After all that's what it told me to use. The user plane is not affected, because the only place to find the user address is in the IE from a Create PDP Context message, sent on the ctrl plane, so there's no mismatch situation arising.
If any GSN in practice actually wanted to send all ctrl messages from 1.2.3.4 but expected the returned ctrl responses to come back on 5.6.7.8, then gtphub would have to store the sender's address 1.2.3.4 as well.
Concerning the ports, we've discussed before that the proper way according to spec is to always use the standard port number for the respective plane when sending out a packet to a peer. gtphub can manage both (sender's port or override with standard port), but when the IE's address mismatches the sender's address, and if I henceforth want use the IE's address, I have no port information for that one (see 7.7.32 in 29.060 and 5.1 in 23.003). The sender's port is actually not relevant at all, so I will semantically have to use the default port number. gtphub would also be able to manage "return to sender", if that's what we need (that's what it's doing now, preliminarily).
Thinkable configs: - Use exclusively the address from the IEs. - Disallow mismatch of CTRL sender and address in CTRL IE. - When addresses match, use sender's port (ugly, but OpenGGSN seems to use the sender's port, so maybe there's something to it; yet I don't trust OpenGGSN by now).
I'm handling IPv4 and v6 transparently, BTW. The difference in the IE is only detectable from the IE length, as 7.7.32 doesn't include the GSN address' type field. Let's hope IPv8 will have the same length as IPv6 :P
Does that answer your question?
And as always, please let me know if you find any dent in my reasoning :)
~Neels
On 11/04/2015 04:24 PM, Neels Hofmeyr wrote:
On Wed, Nov 04, 2015 at 10:20:45AM +0100, Andreas Schultz wrote:
Hi Neels,
I'm trying to understand how you handle a PDP Create Context message where the SGSN Address for control plane and an SGSN address for user traffic differ from the sender IP?
That part is ultimately still missing, as may be visible from the PDP Context unit test. So far it's recording only the actual originating address as returned by the socket read. It's my plan for today.
But the internal storage is ready. gtphub will examine the IEs SGSN Address for signalling SGSN Address for user traffic and GGSN Address for Control Plane GGSN Address for user traffic and store these addresses for future use. The addresses are stored separately, with association pointers from one to the other.
There's a bit of a handwavy situation there, e.g. if an SGSN sends a PDP Create request from 1.2.3.4, but the address in the IE is 5.6.7.8. gtphub later on needs to find the proper peer record; if I stored a peer for 5.6.7.8 and the SGSN again were to send from 1.2.3.4, gtphub would not be able to match that up. I will so far only store the IE's address (5.6.7.8), and expect the SGSN to continue sending ctrl messages for this PDP context from 5.6.7.8 from then on. After all that's what it told me to use. The user plane is not affected, because the only place to find the user address is in the IE from a Create PDP Context message, sent on the ctrl plane, so there's no mismatch situation arising.
If any GSN in practice actually wanted to send all ctrl messages from 1.2.3.4 but expected the returned ctrl responses to come back on 5.6.7.8, then gtphub would have to store the sender's address 1.2.3.4 as well.
The above doesn't seem right.
The specs say to send replies to the IP and port where the request originated. The GSN control plan address is only to be used when we want to send a request to the other side.
So, shouldn't this work like this:
* for each request, cache the source IP+Port and forward the reply back to that, * for each PDP context, remember the ctrl and data IP as requested by the GSN, forward requests to that
I had some fun trying to understand how path management is supposed to work when control source IP != GSN control IP. I still don't get how the restart counter in the Recovery IE is supposed to be match to the GSN.
Concerning the ports, we've discussed before that the proper way according to spec is to always use the standard port number for the respective plane when sending out a packet to a peer.
That would apply to requests only, replies go back to the original source.
Note: in GTPv2 this is getting even more complicated with triggered messages.
gtphub can manage both (sender's port or override with standard port), but when the IE's address mismatches the sender's address, and if I henceforth want use the IE's address, I have no port information for that one (see 7.7.32 in 29.060 and 5.1 in 23.003). The sender's port is actually not relevant at all, so I will semantically have to use the default port number. gtphub would also be able to manage "return to sender", if that's what we need (that's what it's doing now, preliminarily).
Thinkable configs:
- Use exclusively the address from the IEs.
- Disallow mismatch of CTRL sender and address in CTRL IE.
Seems like the reasonable thing to do.
- When addresses match, use sender's port (ugly, but OpenGGSN seems to use the sender's port, so maybe there's something to it; yet I don't trust OpenGGSN by now).
Replies should always go back to the request source IP. Only requests got to the GSN control plane IP.
I'm handling IPv4 and v6 transparently, BTW. The difference in the IE is only detectable from the IE length, as 7.7.32 doesn't include the GSN address' type field. Let's hope IPv8 will have the same length as IPv6 :P
Lets hope all peers you are talking to are on the same IP version. Mixing IPv4 and IPv6 peer will be fun (see the Alternative GGSN Address).
Does that answer your question?
And as always, please let me know if you find any dent in my reasoning :)
I'm still trying to understand some of the finer points of GTP myself.
- Andreas
~Neels
On Wed, Nov 04, 2015 at 04:46:41PM +0100, Andreas Schultz wrote:
The specs say to send replies to the IP and port where the request originated. The GSN control plan address is only to be used when we want to send a request to the other side.
Goodness, really?
- for each request, cache the source IP+Port and forward the reply back to that,
Let's consider IP address and port separately.
- IP address:
The only time when the sender IP *can* even differ is during Create PDP Context messages. The only question is, where does the response to a Create PDP Context Request go. All other cases are clear. (For Update, only the Response can have differing IP address, but that's already the reply.)
And even there it doesn't really make sense to have different addresses: the Create PDP Context Response coming back from the GGSN is explicitly addressed to the TEI that was declared in the Request. Why then would it be sent to a different IP address than the one that was also declared in the Request, explicitly to negotiate that TEI for that IP address?
Like, for one more reply, the TEI belongs to the sender address, and just after that it belongs to the negotiated address?
If this is used in practice, can anyone illustrate for what situation it is useful to negotiate a PDP context where the sender differs from the negotiated Ctrl address? Load balancing for User, yes, but Ctrl??
In the user plane the entire ambiguity does not exist at all. Messages are sent to the address that was in the Create PDP Context request, period. So I actually assume the ctrl plane works the same.
OpenGGSN does actually reply to the same IP *and* port it received from, always. It also replies to the same IP address even if the IE's address in the Create Context Request differs. This may just disprove all my points.
- Disallow mismatch of CTRL sender and address in CTRL IE.
Seems like the reasonable thing to do.
In that case, the SGSN sender address and the CTRL address in the Create PDP Context Request must be identical and all the awkwardness is cut out. And wouldn't work if someone tried to do that, of course. Does anyone?
- Port:
Ports can differ all the time. Any port number can be used as sender to send to a standard port of a peer.
This becomes weird as soon as we're talking GTP packets that contain a nonzero destination TEI to send to.
Echos send no TEI (zero), but they also don't require any resolution or tracing of peers: just reply, done. They go back to where they came from: 29.060 10.1.2.2: "NOTE: The source IP address of the Echo Response message shall be the same as the destination IP address of the Echo Request message." To see this "NOTE"d as loudly makes me assume that Echo is an exception in that way.
If I'm sending a message for a specific TEI, sending it to a different port feels wrong. The TEI was negotiated as part of a Create PDP Context for an IP address, with no port information attached. The TEI hence, at least in my head, resolves to an IP address with the standard port. No matter if it's a request or a response. That was my first guess...
But again OpenGGSN always replies to the sender.
Say different ports are used, what is the scope for TEI, recovery and sequence number? Does an IP address share these across all its ports, or does each port have a separate set of TEIs?
In case two separate ports know about the same TEIs, how then is it a problem if replies only ever go back to the default port?
gtphub implementation wise, a known sequence number can be used to trace back to the sender port, and the TEI can then be ignored. Once an unknown sequence number arises, the TEI resolves to the default port.
Having to manage ports per request seems unneccessary, on top of all the TEI business. Is there a benefit? Load balancing is done on the IP address level already, right? Is this ever used in the field?
I'm still trying to understand some of the finer points of GTP myself.
If anyone can enlighten us with hard facts from the daily grunge of actual GTP usage, that would be appreciated :)
We could examine some traces and check... - whether sender addresses ever differ from PDP Context message IE addresses, and where responses go. - whether port numbers are ever nonstandard, - where responses go, and - how TEIs and seq nrs seem to scope.
I'm handling IPv4 and v6 transparently, BTW. The difference in the IE is only detectable from the IE length, as 7.7.32 doesn't include the GSN address' type field. Let's hope IPv8 will have the same length as IPv6 :P
Lets hope all peers you are talking to are on the same IP version. Mixing IPv4 and IPv6 peer will be fun (see the Alternative GGSN Address).
Hmm yes, I just noticed that, too. Shall this and shall that. According to spec, you can't run a v6 GSN without also supplying alternative v4 addresses. I'll just let v6 tag along until it becomes relevant...
Thanks again, this discussion helps a lot!
~Neels
----- Original Message -----
From: "Neels Hofmeyr" nhofmeyr@sysmocom.de To: "Andreas Schultz" aschultz@tpip.net Cc: openbsc@lists.osmocom.org Sent: Wednesday, November 4, 2015 11:47:07 PM Subject: Re: [PATCH] gtphub collapsed patch (aka bomb)
On Wed, Nov 04, 2015 at 04:46:41PM +0100, Andreas Schultz wrote:
The specs say to send replies to the IP and port where the request originated. The GSN control plan address is only to be used when we want to send a request to the other side.
Goodness, really?
- for each request, cache the source IP+Port and forward the reply back to that,
Let's consider IP address and port separately.
- IP address:
The only time when the sender IP *can* even differ is during Create PDP Context messages. The only question is, where does the response to a Create PDP Context Request go. All other cases are clear. (For Update, only the Response can have differing IP address, but that's already the reply.)
I think you are over complicating thinks.
Lets take this step by step.
The simplest case is a reply. It will *always* be sent to the source IP and port of the request. The source of the response should be the destination IP and port of the request the triggered the response. So, you basically just reverse source and destination and be done.
The GSN arguments in the request do not change that.
The GSN arguments are only used when you have to send a request to the other side after the initial request.
So, for any new PDP context, the initial request from the SGNS will go to the GGSN address that you AAA told you use. Any follow up request will go to the IP that the GGSN told you to use. If the GGSN needs to send a request to the SGSN it will use the IP the GGSN told it to use.
Thinks get interesting when you look at the Echo requests. For our GGSN implementation, I decided to only use the IP's from the GSN information elements.
And even there it doesn't really make sense to have different addresses: the Create PDP Context Response coming back from the GGSN is explicitly addressed to the TEI that was declared in the Request. Why then would it be sent to a different IP address than the one that was also declared in the Request, explicitly to negotiate that TEI for that IP address?
The reply will be sent back to the source in any case.
TEI's belong to the GSN IP's from the IE, not to the source IP.
Like, for one more reply, the TEI belongs to the sender address, and just after that it belongs to the negotiated address?
If this is used in practice, can anyone illustrate for what situation it is useful to negotiate a PDP context where the sender differs from the negotiated Ctrl address? Load balancing for User, yes, but Ctrl??
In the user plane the entire ambiguity does not exist at all. Messages are sent to the address that was in the Create PDP Context request, period. So I actually assume the ctrl plane works the same.
OpenGGSN does actually reply to the same IP *and* port it received from, always. It also replies to the same IP address even if the IE's address in the Create Context Request differs. This may just disprove all my points.
For the reply, that is absolutely correct. What does it do with any follow up request? Would it use the IP from the IE or the original source IP?
- Disallow mismatch of CTRL sender and address in CTRL IE.
Seems like the reasonable thing to do.
In that case, the SGSN sender address and the CTRL address in the Create PDP Context Request must be identical and all the awkwardness is cut out. And wouldn't work if someone tried to do that, of course. Does anyone?
I have played only with Cisco (CSR 1000v) and there you can't configure the source IP to differ from the ctrl IP.
Andreas
- Port:
Ports can differ all the time. Any port number can be used as sender to send to a standard port of a peer.
This becomes weird as soon as we're talking GTP packets that contain a nonzero destination TEI to send to.
Echos send no TEI (zero), but they also don't require any resolution or tracing of peers: just reply, done. They go back to where they came from: 29.060 10.1.2.2: "NOTE: The source IP address of the Echo Response message shall be the same as the destination IP address of the Echo Request message." To see this "NOTE"d as loudly makes me assume that Echo is an exception in that way.
If I'm sending a message for a specific TEI, sending it to a different port feels wrong. The TEI was negotiated as part of a Create PDP Context for an IP address, with no port information attached. The TEI hence, at least in my head, resolves to an IP address with the standard port. No matter if it's a request or a response. That was my first guess...
But again OpenGGSN always replies to the sender.
Say different ports are used, what is the scope for TEI, recovery and sequence number? Does an IP address share these across all its ports, or does each port have a separate set of TEIs?
In case two separate ports know about the same TEIs, how then is it a problem if replies only ever go back to the default port?
gtphub implementation wise, a known sequence number can be used to trace back to the sender port, and the TEI can then be ignored. Once an unknown sequence number arises, the TEI resolves to the default port.
Having to manage ports per request seems unneccessary, on top of all the TEI business. Is there a benefit? Load balancing is done on the IP address level already, right? Is this ever used in the field?
I'm still trying to understand some of the finer points of GTP myself.
If anyone can enlighten us with hard facts from the daily grunge of actual GTP usage, that would be appreciated :)
We could examine some traces and check...
- whether sender addresses ever differ from PDP Context message IE
addresses, and where responses go.
- whether port numbers are ever nonstandard,
- where responses go, and
- how TEIs and seq nrs seem to scope.
I'm handling IPv4 and v6 transparently, BTW. The difference in the IE is only detectable from the IE length, as 7.7.32 doesn't include the GSN address' type field. Let's hope IPv8 will have the same length as IPv6 :P
Lets hope all peers you are talking to are on the same IP version. Mixing IPv4 and IPv6 peer will be fun (see the Alternative GGSN Address).
Hmm yes, I just noticed that, too. Shall this and shall that. According to spec, you can't run a v6 GSN without also supplying alternative v4 addresses. I'll just let v6 tag along until it becomes relevant...
Thanks again, this discussion helps a lot!
~Neels
On Fri, Nov 06, 2015 at 10:43:38AM +0100, Andreas Schultz wrote:
Thinks get interesting when you look at the Echo requests. For our GGSN implementation, I decided to only use the IP's from the GSN information elements.
The way I understand it, Echo is actually defined to be limited to the path, so it is the simplest of them all: reply directly to the IP and port of the sender. Echos don't pass through gtphub, they "rebound".
~Neels