diff options
Diffstat (limited to 'src/gb/gprs_ns2_udp.c')
-rw-r--r-- | src/gb/gprs_ns2_udp.c | 597 |
1 files changed, 597 insertions, 0 deletions
diff --git a/src/gb/gprs_ns2_udp.c b/src/gb/gprs_ns2_udp.c new file mode 100644 index 00000000..1dcc3030 --- /dev/null +++ b/src/gb/gprs_ns2_udp.c @@ -0,0 +1,597 @@ +/*! \file gprs_ns2_udp.c + * NS-over-UDP implementation. + * GPRS Networks Service (NS) messages on the Gb interface. + * 3GPP TS 08.16 version 8.0.1 Release 1999 / ETSI TS 101 299 V8.0.1 (2002-05) + * as well as its successor 3GPP TS 48.016 */ + +/* (C) 2020 sysmocom - s.f.m.c. GmbH + * Author: Alexander Couzens <lynxis@fe80.eu> + * + * All Rights Reserved + * + * SPDX-License-Identifier: GPL-2.0+ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +#include <errno.h> + +#include <osmocom/core/osmo_io.h> +#include <osmocom/core/select.h> +#include <osmocom/core/sockaddr_str.h> +#include <osmocom/core/socket.h> +#include <osmocom/gprs/gprs_ns2.h> + +#include "common_vty.h" +#include "gprs_ns2_internal.h" + + +static void free_bind(struct gprs_ns2_vc_bind *bind); + + +struct gprs_ns2_vc_driver vc_driver_ip = { + .name = "GB UDP IPv4/IPv6", + .free_bind = free_bind, +}; + +struct priv_bind { + struct osmo_io_fd *iofd; + struct osmo_sockaddr addr; + int dscp; + uint8_t priority; +}; + +struct priv_vc { + struct osmo_sockaddr remote; +}; + +/*! clean up all private driver state. Should be only called by gprs_ns2_free_bind() */ +static void free_bind(struct gprs_ns2_vc_bind *bind) +{ + struct priv_bind *priv; + + if (!bind) + return; + + OSMO_ASSERT(gprs_ns2_is_ip_bind(bind)); + + priv = bind->priv; + + osmo_iofd_free(priv->iofd); + priv->iofd = NULL; + talloc_free(priv); +} + +static void free_vc(struct gprs_ns2_vc *nsvc) +{ + if (!nsvc) + return; + + if (!nsvc->priv) + return; + + OSMO_ASSERT(gprs_ns2_is_ip_bind(nsvc->bind)); + talloc_free(nsvc->priv); + nsvc->priv = NULL; +} + +static void dump_vty(const struct gprs_ns2_vc_bind *bind, + struct vty *vty, bool stats) +{ + struct priv_bind *priv; + struct gprs_ns2_vc *nsvc; + struct osmo_sockaddr_str sockstr = {}; + unsigned long nsvcs = 0; + + if (!bind) + return; + + priv = bind->priv; + if (osmo_sockaddr_str_from_sockaddr(&sockstr, &priv->addr.u.sas)) + strcpy(sockstr.ip, "invalid"); + + llist_for_each_entry(nsvc, &bind->nsvc, blist) { + nsvcs++; + } + + vty_out(vty, "UDP bind: %s:%d DSCP: %d Priority: %u%s", sockstr.ip, sockstr.port, + priv->dscp, priv->priority, VTY_NEWLINE); + vty_out(vty, " IP-SNS signalling weight: %u data weight: %u%s", + bind->sns_sig_weight, bind->sns_data_weight, VTY_NEWLINE); + vty_out(vty, " %lu NS-VC:%s", nsvcs, VTY_NEWLINE); + + llist_for_each_entry(nsvc, &bind->nsvc, blist) { + ns2_vty_dump_nsvc(vty, nsvc, stats); + } +} + + +/*! Find a NS-VC by its remote socket address. + * \param[in] bind in which to search + * \param[in] rem_addr remote peer socket address to search + * \returns NS-VC matching sockaddr; NULL if none found */ +struct gprs_ns2_vc *gprs_ns2_nsvc_by_sockaddr_bind(struct gprs_ns2_vc_bind *bind, + const struct osmo_sockaddr *rem_addr) +{ + struct gprs_ns2_vc *nsvc; + struct priv_vc *vcpriv; + + OSMO_ASSERT(gprs_ns2_is_ip_bind(bind)); + + llist_for_each_entry(nsvc, &bind->nsvc, blist) { + vcpriv = nsvc->priv; + if (vcpriv->remote.u.sa.sa_family != rem_addr->u.sa.sa_family) + continue; + if (osmo_sockaddr_cmp(&vcpriv->remote, rem_addr)) + continue; + + return nsvc; + } + + return NULL; +} + +static inline int nsip_sendmsg(struct gprs_ns2_vc_bind *bind, + struct msgb *msg, + const struct osmo_sockaddr *dest) +{ + struct priv_bind *priv = bind->priv; + + return osmo_iofd_sendto_msgb(priv->iofd, msg, 0, dest); +} + +/*! send the msg and free it afterwards. + * \param nsvc NS-VC on which the message shall be sent + * \param msg message to be sent + * \return number of bytes transmitted; negative on error */ +static int nsip_vc_sendmsg(struct gprs_ns2_vc *nsvc, struct msgb *msg) +{ + int rc; + struct gprs_ns2_vc_bind *bind = nsvc->bind; + struct priv_vc *priv = nsvc->priv; + + rc = nsip_sendmsg(bind, msg, &priv->remote); + + return rc; +} + +static struct priv_vc *ns2_driver_alloc_vc(struct gprs_ns2_vc_bind *bind, struct gprs_ns2_vc *nsvc, const struct osmo_sockaddr *remote) +{ + struct priv_vc *priv = talloc_zero(bind, struct priv_vc); + if (!priv) + return NULL; + + nsvc->priv = priv; + priv->remote = *remote; + + return priv; +} + +static void handle_nsip_recvfrom(struct osmo_io_fd *iofd, int error, struct msgb *msg, + const struct osmo_sockaddr *saddr) +{ + int rc = 0; + struct gprs_ns2_vc_bind *bind = osmo_iofd_get_data(iofd); + struct gprs_ns2_vc *nsvc; + + struct msgb *reject; + + msg->l2h = msgb_data(msg); + + /* check if a vc is available */ + nsvc = gprs_ns2_nsvc_by_sockaddr_bind(bind, saddr); + if (!nsvc) { + /* VC not found */ + rc = ns2_create_vc(bind, msg, saddr, "newconnection", &reject, &nsvc); + switch (rc) { + case NS2_CS_FOUND: + break; + case NS2_CS_ERROR: + case NS2_CS_SKIPPED: + rc = 0; + goto out; + case NS2_CS_REJECTED: + /* nsip_sendmsg will free reject */ + rc = nsip_sendmsg(bind, reject, saddr); + goto out; + case NS2_CS_CREATED: + ns2_driver_alloc_vc(bind, nsvc, saddr); + /* only start the fsm for non SNS. SNS will take care of its own */ + if (nsvc->nse->dialect != GPRS_NS2_DIALECT_SNS) + ns2_vc_fsm_start(nsvc); + break; + } + } + + ns2_recv_vc(nsvc, msg); + return; + +out: + msgb_free(msg); +} + +static void handle_nsip_sendto(struct osmo_io_fd *iofd, int res, + struct msgb *msg, + const struct osmo_sockaddr *daddr) +{ + struct gprs_ns2_vc_bind *bind = osmo_iofd_get_data(iofd); + struct gprs_ns2_vc *nsvc; + + nsvc = gprs_ns2_nsvc_by_sockaddr_bind(bind, daddr); + if (!nsvc) + return; + + if (OSMO_LIKELY(res >= 0)) { + RATE_CTR_INC_NS(nsvc, NS_CTR_PKTS_OUT); + RATE_CTR_ADD_NS(nsvc, NS_CTR_BYTES_OUT, res); + } else { + RATE_CTR_INC_NS(nsvc, NS_CTR_PKTS_OUT_DROP); + RATE_CTR_ADD_NS(nsvc, NS_CTR_BYTES_OUT_DROP, msgb_length(msg)); + } +} + +/*! Find NS bind for a given socket address + * \param[in] nsi NS instance + * \param[in] sockaddr socket address to search for + * \return + */ +struct gprs_ns2_vc_bind *gprs_ns2_ip_bind_by_sockaddr(struct gprs_ns2_inst *nsi, + const struct osmo_sockaddr *sockaddr) +{ + struct gprs_ns2_vc_bind *bind; + const struct osmo_sockaddr *local; + + OSMO_ASSERT(nsi); + OSMO_ASSERT(sockaddr); + + llist_for_each_entry(bind, &nsi->binding, list) { + if (!gprs_ns2_is_ip_bind(bind)) + continue; + + local = gprs_ns2_ip_bind_sockaddr(bind); + if (!osmo_sockaddr_cmp(sockaddr, local)) + return bind; + } + + return NULL; +} + +/*! Bind to an IPv4/IPv6 address + * \param[in] nsi NS Instance in which to create the NSVC + * \param[in] local the local address to bind to + * \param[in] dscp the DSCP/TOS bits used for transmitted data + * \param[out] result pointer to the created bind or if a bind with the name exists return the bind. + * \return 0 on success; negative on error. -EALREADY returned in case a bind with the name exists */ +int gprs_ns2_ip_bind(struct gprs_ns2_inst *nsi, + const char *name, + const struct osmo_sockaddr *local, + int dscp, + struct gprs_ns2_vc_bind **result) +{ + struct gprs_ns2_vc_bind *bind; + struct priv_bind *priv; + int rc; + + struct osmo_io_ops ioops = { + .sendto_cb = &handle_nsip_sendto, + .recvfrom_cb = &handle_nsip_recvfrom, + }; + + if (local->u.sa.sa_family != AF_INET && local->u.sa.sa_family != AF_INET6) + return -EINVAL; + + if (dscp < 0 || dscp > 63) + return -EINVAL; + + bind = gprs_ns2_ip_bind_by_sockaddr(nsi, local); + if (bind) { + if (result) + *result = bind; + return -EBUSY; + } + + rc = ns2_bind_alloc(nsi, name, &bind); + if (rc < 0) + return rc; + + bind->driver = &vc_driver_ip; + bind->ll = GPRS_NS2_LL_UDP; + /* expect 100 mbit at least. + * TODO: ask the network layer about the speed. But would require + * notification on change. */ + bind->transfer_capability = 100; + bind->send_vc = nsip_vc_sendmsg; + bind->free_vc = free_vc; + bind->dump_vty = dump_vty; + + priv = bind->priv = talloc_zero(bind, struct priv_bind); + if (!priv) { + gprs_ns2_free_bind(bind); + return -ENOMEM; + } + + priv->addr = *local; + priv->dscp = dscp; + + rc = osmo_sock_init_osa(SOCK_DGRAM, IPPROTO_UDP, + local, NULL, + OSMO_SOCK_F_BIND | OSMO_SOCK_F_DSCP(priv->dscp)); + if (rc < 0) { + gprs_ns2_free_bind(bind); + return rc; + } + + priv->iofd = osmo_iofd_setup(bind, rc, "NS bind", OSMO_IO_FD_MODE_RECVFROM_SENDTO, &ioops, bind); + osmo_iofd_register(priv->iofd, rc); + osmo_iofd_set_alloc_info(priv->iofd, 4096, 128); + osmo_iofd_set_txqueue_max_length(priv->iofd, nsi->txqueue_max_length); + + /* IPv4: max fragmented payload can be (13 bit) * 8 byte => 65535. + * IPv6: max payload can be 65535 (RFC 2460). + * UDP header = 8 byte */ + bind->mtu = 65535 - 8; + if (result) + *result = bind; + + return 0; +} + +/*! Create new NS-VC to a given remote address + * \param[in] bind the bind we want to connect + * \param[in] nse NS entity to be used for the new NS-VC + * \param[in] remote remote address to connect to + * \return pointer to newly-allocated and connected NS-VC; NULL on error */ +struct gprs_ns2_vc *ns2_ip_bind_connect(struct gprs_ns2_vc_bind *bind, + struct gprs_ns2_nse *nse, + const struct osmo_sockaddr *remote) +{ + struct gprs_ns2_vc *nsvc; + const struct osmo_sockaddr *local; + struct priv_vc *priv; + enum gprs_ns2_vc_mode vc_mode; + char idbuf[256], tmp[INET6_ADDRSTRLEN + 8]; + + OSMO_ASSERT(gprs_ns2_is_ip_bind(bind)); + + vc_mode = ns2_dialect_to_vc_mode(nse->dialect); + if ((int) vc_mode == -1) { + LOGNSE(nse, LOGL_ERROR, "Can not derive vc mode from dialect %d. Maybe libosmocore is too old.\n", + nse->dialect); + return NULL; + } + + /* duplicate */ + if (gprs_ns2_nsvc_by_sockaddr_bind(bind, remote)) + return NULL; + + local = gprs_ns2_ip_bind_sockaddr(bind); + osmo_sockaddr_to_str_buf(tmp, sizeof(tmp), local); + snprintf(idbuf, sizeof(idbuf), "NSE%05u-NSVC-%s-%s-%s", nse->nsei, gprs_ns2_lltype_str(nse->ll), + tmp, osmo_sockaddr_to_str(remote)); + osmo_identifier_sanitize_buf(idbuf, NULL, '_'); + + nsvc = ns2_vc_alloc(bind, nse, true, vc_mode, idbuf); + if (!nsvc) + return NULL; + + nsvc->priv = talloc_zero(bind, struct priv_vc); + if (!nsvc->priv) { + gprs_ns2_free_nsvc(nsvc); + return NULL; + } + + priv = nsvc->priv; + priv->remote = *remote; + + return nsvc; +} + +/*! Return the socket address of the local peer of a NS-VC. + * \param[in] nsvc NS-VC whose local peer we want to know + * \return address of the local peer; NULL in case of error */ +const struct osmo_sockaddr *gprs_ns2_ip_vc_local(const struct gprs_ns2_vc *nsvc) +{ + struct priv_bind *priv; + + if (nsvc->bind->driver != &vc_driver_ip) + return NULL; + + priv = nsvc->bind->priv; + return &priv->addr; +} + +/*! Return the socket address of the remote peer of a NS-VC. + * \param[in] nsvc NS-VC whose remote peer we want to know + * \return address of the remote peer; NULL in case of error */ +const struct osmo_sockaddr *gprs_ns2_ip_vc_remote(const struct gprs_ns2_vc *nsvc) +{ + struct priv_vc *priv; + + if (nsvc->bind->driver != &vc_driver_ip) + return NULL; + + priv = nsvc->priv; + return &priv->remote; +} + +/*! Compare the NS-VC with the given parameter + * \param[in] nsvc NS-VC to compare with + * \param[in] local The local address + * \param[in] remote The remote address + * \param[in] nsvci NS-VCI will only be used if the NS-VC in BLOCKRESET mode otherwise NS-VCI isn't applicable. + * \return true if the NS-VC has the same properties as given + */ +bool gprs_ns2_ip_vc_equal(const struct gprs_ns2_vc *nsvc, + const struct osmo_sockaddr *local, + const struct osmo_sockaddr *remote, + uint16_t nsvci) +{ + struct priv_vc *vpriv; + struct priv_bind *bpriv; + + if (nsvc->bind->driver != &vc_driver_ip) + return false; + + vpriv = nsvc->priv; + bpriv = nsvc->bind->priv; + + if (osmo_sockaddr_cmp(local, &bpriv->addr)) + return false; + + if (osmo_sockaddr_cmp(remote, &vpriv->remote)) + return false; + + if (nsvc->mode == GPRS_NS2_VC_MODE_BLOCKRESET) + if (nsvc->nsvci != nsvci) + return false; + + return true; +} + +/*! Return the locally bound socket address of the bind. + * \param[in] bind The bind whose local address we want to know + * \return address of the local bind */ +const struct osmo_sockaddr *gprs_ns2_ip_bind_sockaddr(struct gprs_ns2_vc_bind *bind) +{ + struct priv_bind *priv; + OSMO_ASSERT(gprs_ns2_is_ip_bind(bind)); + + priv = bind->priv; + return &priv->addr; +} + +/*! Is the given bind an IP bind? */ +int gprs_ns2_is_ip_bind(struct gprs_ns2_vc_bind *bind) +{ + return (bind->driver == &vc_driver_ip); +} + +/*! Set the DSCP (TOS) bit value of the given bind. */ +int gprs_ns2_ip_bind_set_dscp(struct gprs_ns2_vc_bind *bind, int dscp) +{ + struct priv_bind *priv; + int rc = 0; + + if (dscp < 0 || dscp > 63) + return -EINVAL; + + OSMO_ASSERT(gprs_ns2_is_ip_bind(bind)); + priv = bind->priv; + + if (dscp != priv->dscp) { + priv->dscp = dscp; + + rc = osmo_sock_set_dscp(osmo_iofd_get_fd(priv->iofd), dscp); + if (rc < 0) { + LOGBIND(bind, LOGL_ERROR, "Failed to set the DSCP to %u with ret(%d) errno(%d)\n", + dscp, rc, errno); + } + } + + return rc; +} + +/*! Set the socket priority of the given bind. */ +int gprs_ns2_ip_bind_set_priority(struct gprs_ns2_vc_bind *bind, uint8_t priority) +{ + struct priv_bind *priv; + int rc = 0; + + OSMO_ASSERT(gprs_ns2_is_ip_bind(bind)); + priv = bind->priv; + + if (priority != priv->priority) { + priv->priority = priority; + + rc = osmo_sock_set_priority(osmo_iofd_get_fd(priv->iofd), priority); + if (rc < 0) { + LOGBIND(bind, LOGL_ERROR, "Failed to set the priority to %u with ret(%d) errno(%d)\n", + priority, rc, errno); + } + } + + return rc; +} + + +/*! Count UDP binds compatible with remote */ +int ns2_ip_count_bind(struct gprs_ns2_inst *nsi, struct osmo_sockaddr *remote) +{ + struct gprs_ns2_vc_bind *bind; + const struct osmo_sockaddr *sa; + int count = 0; + + llist_for_each_entry(bind, &nsi->binding, list) { + if (!gprs_ns2_is_ip_bind(bind)) + continue; + + sa = gprs_ns2_ip_bind_sockaddr(bind); + if (!sa) + continue; + + if (sa->u.sa.sa_family == remote->u.sa.sa_family) + count++; + } + + return count; +} + +/* return the matching bind by index */ +struct gprs_ns2_vc_bind *ns2_ip_get_bind_by_index(struct gprs_ns2_inst *nsi, + struct osmo_sockaddr *remote, + int index) +{ + struct gprs_ns2_vc_bind *bind; + const struct osmo_sockaddr *sa; + int i = 0; + + llist_for_each_entry(bind, &nsi->binding, list) { + if (!gprs_ns2_is_ip_bind(bind)) + continue; + + sa = gprs_ns2_ip_bind_sockaddr(bind); + if (!sa) + continue; + + if (sa->u.sa.sa_family == remote->u.sa.sa_family) { + if (index == i) + return bind; + i++; + } + } + + return NULL; +} + +void ns2_ip_set_txqueue_max_length(struct gprs_ns2_vc_bind *bind, unsigned int max_length) +{ + struct priv_bind *priv = bind->priv; + OSMO_ASSERT(gprs_ns2_is_ip_bind(bind)); + + osmo_iofd_set_txqueue_max_length(priv->iofd, max_length); +} + +/*! set the signalling and data weight for this bind + * \param[in] bind + * \param[in] signalling the signalling weight + * \param[in] data the data weight + */ +void gprs_ns2_ip_bind_set_sns_weight(struct gprs_ns2_vc_bind *bind, uint8_t signalling, uint8_t data) +{ + OSMO_ASSERT(gprs_ns2_is_ip_bind(bind)); + bind->sns_sig_weight = signalling; + bind->sns_data_weight = data; + ns2_sns_update_weights(bind); +} |