summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHarald Welte <laforge@gnumonks.org>2017-04-08 20:52:33 +0200
committerHarald Welte <laforge@gnumonks.org>2017-04-09 21:46:21 +0200
commitdda70fca7979d86e04bba9ba5bad32162327550c (patch)
treea2910583bb4b84ac60d41038ce906e68fd5e481e
parentacd08feb8f75827555a9ef38b890870fed3388ea (diff)
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
-rw-r--r--include/osmocom/core/socket.h4
-rw-r--r--src/socket.c220
-rw-r--r--tests/socket/socket_test.c52
-rw-r--r--tests/socket/socket_test.err1
-rw-r--r--tests/socket/socket_test.ok4
5 files changed, 248 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 <netdb.h>
#include <ifaddrs.h>
+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..b56d50c0 100644
--- a/tests/socket/socket_test.c
+++ b/tests/socket/socket_test.c
@@ -73,6 +73,57 @@ 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);
+#ifndef __FreeBSD__
+ /* For some reason, on the jenkins.osmocom.org build slave with
+ * FreeBSD 10 inside a jail, it fails. Works fine on laforge's
+ * FreeBSD 10 or 11 VM at home */
+ OSMO_ASSERT(!strncmp(name, "(127.0.0.1:53<->127.0.0.1", 25));
+#endif
+ talloc_free(name);
+
+ return 0;
+}
+
+
const struct log_info_cat default_categories[] = {
};
@@ -88,6 +139,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