From ff60be3726fc766e27ad7d5890b11405b8c70041 Mon Sep 17 00:00:00 2001 From: Harald Welte Date: Sat, 8 Apr 2017 20:52:33 +0200 Subject: Add osmo_sock_init2() function, allowing both BIND *and* CONNECT The old osmo_sock_init() function allows only either a bind (for a server socket), or a connect (for a client socket), but not both together. So there's no way to have a client socket that is bound to a specific local IP and/or port, which is needed for some use cases. Change-Id: Idab124bcca47872f55311a82d6818aed590965e6 --- include/osmocom/core/socket.h | 4 + src/socket.c | 220 +++++++++++++++++++++++++++++++++++------- tests/socket/socket_test.c | 47 +++++++++ tests/socket/socket_test.err | 1 + tests/socket/socket_test.ok | 4 + 5 files changed, 243 insertions(+), 33 deletions(-) diff --git a/include/osmocom/core/socket.h b/include/osmocom/core/socket.h index 4f00e300..e19e8f28 100644 --- a/include/osmocom/core/socket.h +++ b/include/osmocom/core/socket.h @@ -24,6 +24,10 @@ struct osmo_fd; int osmo_sock_init(uint16_t family, uint16_t type, uint8_t proto, const char *host, uint16_t port, unsigned int flags); +int osmo_sock_init2(uint16_t family, uint16_t type, uint8_t proto, + const char *local_host, uint16_t local_port, + const char *remote_host, uint16_t remote_port, unsigned int flags); + int osmo_sock_init_ofd(struct osmo_fd *ofd, int family, int type, int proto, const char *host, uint16_t port, unsigned int flags); diff --git a/src/socket.c b/src/socket.c index 2c1b547b..ad0f69bb 100644 --- a/src/socket.c +++ b/src/socket.c @@ -51,6 +51,188 @@ #include #include +static struct addrinfo *addrinfo_helper(uint16_t family, uint16_t type, uint8_t proto, + const char *host, uint16_t port, bool passive) +{ + struct addrinfo hints, *result; + char portbuf[16]; + int rc; + + snprintf(portbuf, sizeof(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; + } + + if (passive) + hints.ai_flags |= AI_PASSIVE; + + rc = getaddrinfo(host, portbuf, &hints, &result); + if (rc != 0) { + LOGP(DLGLOBAL, LOGL_ERROR, "getaddrinfo returned NULL: %s:%u: %s\n", + host, port, strerror(errno)); + return NULL; + } + + return result; +} + +static int socket_helper(const struct addrinfo *rp, unsigned int flags) +{ + int sfd, on = 1; + + sfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (sfd == -1) + return sfd; + if (flags & OSMO_SOCK_F_NONBLOCK) { + if (ioctl(sfd, FIONBIO, (unsigned char *)&on) < 0) { + LOGP(DLGLOBAL, LOGL_ERROR, + "cannot set this socket unblocking: %s\n", + strerror(errno)); + close(sfd); + sfd = -EINVAL; + } + } + return sfd; +} + + +/*! \brief Initialize a socket (including bind and/or connect) + * \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] local_host local host name or IP address in string form + * \param[in] local_port local port number in host byte order + * \param[in] remote_host remote host name or IP address in string form + * \param[in] remote_port remote port number in host byte order + * \param[in] flags flags like \ref OSMO_SOCK_F_CONNECT + * \returns socket file descriptor on success; negative on error + * + * This function creates a new socket of the designated \a family, \a + * type and \a proto and optionally binds it to the \a local_host and \a + * local_port as well as optionally connects it to the \a remote_host + * and \q remote_port, depending on the value * of \a flags parameter. + * + * As opposed to \ref osmo_sock_init(), this function allows to combine + * the \ref OSMO_SOCK_F_BIND and \ref OSMO_SOCK_F_CONNECT flags. This + * is useful if you want to connect to a remote host/port, but still + * want to bind that socket to either a specific local alias IP and/or a + * specific local source port. + * + * You must specify either \ref OSMO_SOCK_F_BIND, or \ref + * OSMO_SOCK_F_CONNECT, or both. + * + * If \ref OSMO_SOCK_F_NONBLOCK is specified, the socket will be set to + * non-blocking mode. + */ +int osmo_sock_init2(uint16_t family, uint16_t type, uint8_t proto, + const char *local_host, uint16_t local_port, + const char *remote_host, uint16_t remote_port, unsigned int flags) +{ + struct addrinfo *result, *rp; + int sfd = -1, rc, on = 1; + + if ((flags & (OSMO_SOCK_F_BIND | OSMO_SOCK_F_CONNECT)) == 0) { + LOGP(DLGLOBAL, LOGL_ERROR, "invalid: you have to specify either " + "BIND or CONNECT flags\n"); + return -EINVAL; + } + + /* figure out local side of socket */ + if (flags & OSMO_SOCK_F_BIND) { + result = addrinfo_helper(family, type, proto, local_host, local_port, true); + if (!result) + return -EINVAL; + + for (rp = result; rp != NULL; rp = rp->ai_next) { + /* Workaround for glibc again */ + if (type == SOCK_RAW) { + rp->ai_socktype = SOCK_RAW; + rp->ai_protocol = proto; + } + + sfd = socket_helper(rp, flags); + if (sfd < 0) + continue; + + rc = setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, + &on, sizeof(on)); + if (rc < 0) { + LOGP(DLGLOBAL, LOGL_ERROR, + "cannot setsockopt socket:" + " %s:%u: %s\n", + local_host, local_port, strerror(errno)); + break; + } + if (bind(sfd, rp->ai_addr, rp->ai_addrlen) != -1) + break; + close(sfd); + } + freeaddrinfo(result); + if (rp == NULL) { + LOGP(DLGLOBAL, LOGL_ERROR, "unable to bind socket: %s:%u: %s\n", + local_host, local_port, strerror(errno)); + return -ENODEV; + } + } + + /* figure out remote side of socket */ + if (flags & OSMO_SOCK_F_CONNECT) { + result = addrinfo_helper(family, type, proto, remote_host, remote_port, false); + if (!result) { + close(sfd); + return -EINVAL; + } + + for (rp = result; rp != NULL; rp = rp->ai_next) { + /* Workaround for glibc again */ + if (type == SOCK_RAW) { + rp->ai_socktype = SOCK_RAW; + rp->ai_protocol = proto; + } + + if (!sfd) { + sfd = socket_helper(rp, flags); + if (sfd < 0) + continue; + } + + rc = connect(sfd, rp->ai_addr, rp->ai_addrlen); + if (rc != -1 || (rc == -1 && errno == EINPROGRESS)) + break; + + close(sfd); + sfd = -1; + } + freeaddrinfo(result); + if (rp == NULL) { + LOGP(DLGLOBAL, LOGL_ERROR, "unable to connect socket: %s:%u: %s\n", + remote_host, remote_port, strerror(errno)); + return -ENODEV; + } + } + + /* Make sure to call 'listen' on a bound, connection-oriented sock */ + if ((flags & (OSMO_SOCK_F_BIND|OSMO_SOCK_F_CONNECT)) == OSMO_SOCK_F_BIND) { + switch (type) { + case SOCK_STREAM: + case SOCK_SEQPACKET: + listen(sfd, 10); + break; + } + } + return sfd; +} + + /*! \brief Initialize a socket (including bind/connect) * \param[in] family Address Family like AF_INET, AF_INET6, AF_UNSPEC * \param[in] type Socket type like SOCK_DGRAM, SOCK_STREAM @@ -67,9 +249,8 @@ int osmo_sock_init(uint16_t family, uint16_t type, uint8_t proto, const char *host, uint16_t port, unsigned int flags) { - struct addrinfo hints, *result, *rp; + struct addrinfo *result, *rp; int sfd, rc, on = 1; - char portbuf[16]; if ((flags & (OSMO_SOCK_F_BIND | OSMO_SOCK_F_CONNECT)) == (OSMO_SOCK_F_BIND | OSMO_SOCK_F_CONNECT)) { @@ -78,25 +259,8 @@ int osmo_sock_init(uint16_t family, uint16_t type, uint8_t proto, return -EINVAL; } - 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; - } - - if (flags & OSMO_SOCK_F_BIND) - hints.ai_flags |= AI_PASSIVE; - - rc = getaddrinfo(host, portbuf, &hints, &result); - if (rc != 0) { + result = addrinfo_helper(family, type, proto, host, port, flags & OSMO_SOCK_F_BIND); + if (!result) { LOGP(DLGLOBAL, LOGL_ERROR, "getaddrinfo returned NULL: %s:%u: %s\n", host, port, strerror(errno)); return -EINVAL; @@ -109,20 +273,10 @@ int osmo_sock_init(uint16_t family, uint16_t type, uint8_t proto, rp->ai_protocol = proto; } - sfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + sfd = socket_helper(rp, flags); if (sfd == -1) continue; - if (flags & OSMO_SOCK_F_NONBLOCK) { - if (ioctl(sfd, FIONBIO, (unsigned char *)&on) < 0) { - LOGP(DLGLOBAL, LOGL_ERROR, - "cannot set this socket unblocking:" - " %s:%u: %s\n", - host, port, strerror(errno)); - close(sfd); - freeaddrinfo(result); - return -EINVAL; - } - } + if (flags & OSMO_SOCK_F_CONNECT) { rc = connect(sfd, rp->ai_addr, rp->ai_addrlen); if (rc != -1 || (rc == -1 && errno == EINPROGRESS)) diff --git a/tests/socket/socket_test.c b/tests/socket/socket_test.c index 5b6abc42..57425ef5 100644 --- a/tests/socket/socket_test.c +++ b/tests/socket/socket_test.c @@ -73,6 +73,52 @@ static int test_sockinit(void) return 0; } +static int test_sockinit2(void) +{ + int fd, rc; + char *name; + + printf("Checking osmo_sock_init2() with bind to a random local UDP port\n"); + fd = osmo_sock_init2(AF_INET, SOCK_DGRAM, IPPROTO_UDP, + "0.0.0.0", 0, NULL, 0, OSMO_SOCK_F_BIND); + OSMO_ASSERT(fd >= 0); + name = osmo_sock_get_name(NULL, fd); + /* expect it to be not connected. We cannot match on INADDR_ANY, + * as apparently that won't work on FreeBSD if there's only one + * address (e.g. 127.0.0.1) assigned to the entire system, like + * the Osmocom FreeBSD build slaves */ + OSMO_ASSERT(!strncmp(name, "(NULL<->", 7)); + talloc_free(name); + /* expect it to be blocking */ + rc = fcntl(fd, F_GETFL); + OSMO_ASSERT(!(rc & O_NONBLOCK)); + close(fd); + + printf("Checking osmo_sock_init2() for OSMO_SOCK_F_NONBLOCK\n"); + fd = osmo_sock_init2(AF_INET, SOCK_DGRAM, IPPROTO_UDP, + "0.0.0.0", 0, NULL, 0, OSMO_SOCK_F_BIND|OSMO_SOCK_F_NONBLOCK); + OSMO_ASSERT(fd >= 0); + /* expect it to be blocking */ + rc = fcntl(fd, F_GETFL); + OSMO_ASSERT(rc & O_NONBLOCK); + close(fd); + + printf("Checking osmo_sock_init2() for invalid flags\n"); + fd = osmo_sock_init2(AF_INET, SOCK_DGRAM, IPPROTO_UDP, "0.0.0.0", 0, NULL, 0, 0); + OSMO_ASSERT(fd < 0); + + printf("Checking osmo_sock_init2() for combined BIND + CONNECT\n"); + fd = osmo_sock_init2(AF_INET, SOCK_DGRAM, IPPROTO_UDP, "127.0.0.1", 0, "127.0.0.1", 53, + OSMO_SOCK_F_BIND|OSMO_SOCK_F_CONNECT); + OSMO_ASSERT(fd >= 0); + name = osmo_sock_get_name(NULL, fd); + OSMO_ASSERT(!strncmp(name, "(127.0.0.1:53<->127.0.0.1", 25)); + talloc_free(name); + + return 0; +} + + const struct log_info_cat default_categories[] = { }; @@ -88,6 +134,7 @@ int main(int argc, char *argv[]) log_set_print_filename(osmo_stderr_target, 0); test_sockinit(); + test_sockinit2(); return EXIT_SUCCESS; } diff --git a/tests/socket/socket_test.err b/tests/socket/socket_test.err index 5367239c..ed6e1865 100644 --- a/tests/socket/socket_test.err +++ b/tests/socket/socket_test.err @@ -1 +1,2 @@ invalid: both bind and connect flags set: 0.0.0.0:0 +invalid: you have to specify either BIND or CONNECT flags diff --git a/tests/socket/socket_test.ok b/tests/socket/socket_test.ok index d6ec40ed..4b24fbce 100644 --- a/tests/socket/socket_test.ok +++ b/tests/socket/socket_test.ok @@ -1,3 +1,7 @@ Checking osmo_sock_init() with bind to a random local UDP port Checking for OSMO_SOCK_F_NONBLOCK Checking for invalid flags +Checking osmo_sock_init2() with bind to a random local UDP port +Checking osmo_sock_init2() for OSMO_SOCK_F_NONBLOCK +Checking osmo_sock_init2() for invalid flags +Checking osmo_sock_init2() for combined BIND + CONNECT -- cgit v1.2.3