From b963542e8f15da25a9b98ed4c2e6048ebed8f8d1 Mon Sep 17 00:00:00 2001 From: Brian Russell Date: Fri, 28 Jul 2017 14:35:09 +0100 Subject: [PATCH] Add socket option BINDTODEVICE Linux now supports Virtual Routing and Forwarding (VRF) as per: https://www.kernel.org/doc/Documentation/networking/vrf.txt In order for an application to bind or connect to a socket with an address in a VRF, they need to first bind the socket to the VRF device: setsockopt(sd, SOL_SOCKET, SO_BINDTODEVICE, dev, strlen(dev)+1); Note "dev" is the VRF device, eg. VRF "blue", rather than an interface enslaved to the VRF. Add a new socket option, ZMQ_BINDTODEVICE, to bind a socket to a device. In general, if a socket is bound to a device, eg. an interface, only packets received from that particular device are processed by the socket. If device is a VRF device, then subsequent binds/connects to that socket use addresses in the VRF routing table. --- CMakeLists.txt | 1 + acinclude.m4 | 27 +++++++++++++++ builds/cmake/Modules/ZMQSourceRunChecks.cmake | 19 +++++++++++ builds/gyp/platform.hpp | 1 + configure.ac | 6 ++++ doc/zmq_getsockopt.txt | 17 ++++++++++ doc/zmq_setsockopt.txt | 18 ++++++++++ include/zmq.h | 1 + src/ip.cpp | 13 ++++++++ src/ip.hpp | 3 ++ src/options.cpp | 31 +++++++++++++++++ src/options.hpp | 3 ++ src/socks_connecter.cpp | 4 +++ src/tcp_connecter.cpp | 4 +++ src/tcp_listener.cpp | 4 +++ src/udp_engine.cpp | 4 +++ tests/test_setsockopt.cpp | 33 +++++++++++++++++++ 17 files changed, 189 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index beb48c34..3fef2ac2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -324,6 +324,7 @@ endif () if (NOT CMAKE_CROSSCOMPILING) zmq_check_sock_cloexec () zmq_check_o_cloexec () + zmq_check_so_bindtodevice () zmq_check_so_keepalive () zmq_check_tcp_keepcnt () zmq_check_tcp_keepidle () diff --git a/acinclude.m4 b/acinclude.m4 index 635d27f3..f648ed0f 100644 --- a/acinclude.m4 +++ b/acinclude.m4 @@ -682,6 +682,33 @@ int main (int, char **) ) }]) +dnl ################################################################################ +dnl # LIBZMQ_CHECK_SO_BINDTODEVICE([action-if-found], [action-if-not-found]) # +dnl # Check if SO_BINDTODEVICE is supported # +dnl ################################################################################ +AC_DEFUN([LIBZMQ_CHECK_SO_BINDTODEVICE], [{ + AC_CACHE_CHECK([whether SO_BINDTODEVICE is supported], [libzmq_cv_so_bindtodevice], + [AC_TRY_RUN([/* SO_BINDTODEVICE test */ +#include + +int main (int argc, char *argv []) +{ +/* Actually making the setsockopt() call requires CAP_NET_RAW */ +#ifndef SO_BINDTODEVICE + return 1; +#else + return 0; +#endif +} + ], + [libzmq_cv_so_bindtodevice="yes"], + [libzmq_cv_so_bindtodevice="no"], + [libzmq_cv_so_bindtodevice="not during cross-compile"] + )] + ) + AS_IF([test "x$libzmq_cv_so_bindtodevice" = "xyes"], [$1], [$2]) +}]) + dnl ################################################################################ dnl # LIBZMQ_CHECK_SO_KEEPALIVE([action-if-found], [action-if-not-found]) # dnl # Check if SO_KEEPALIVE is supported # diff --git a/builds/cmake/Modules/ZMQSourceRunChecks.cmake b/builds/cmake/Modules/ZMQSourceRunChecks.cmake index 8f16372f..67bd0eea 100644 --- a/builds/cmake/Modules/ZMQSourceRunChecks.cmake +++ b/builds/cmake/Modules/ZMQSourceRunChecks.cmake @@ -48,6 +48,25 @@ int main(int argc, char *argv []) ZMQ_HAVE_O_CLOEXEC) endmacro() +macro(zmq_check_so_bindtodevice) + message(STATUS "Checking whether SO_BINDTODEVICE is supported") + check_c_source_runs( +" +#include + +int main(int argc, char *argv []) +{ +/* Actually making the setsockopt() call requires CAP_NET_RAW */ +#ifndef SO_BINDTODEVICE + return 1; +#else + return 0; +#endif +} +" + ZMQ_HAVE_SO_BINDTODEVICE) +endmacro() + # TCP keep-alives Checks. macro(zmq_check_so_keepalive) diff --git a/builds/gyp/platform.hpp b/builds/gyp/platform.hpp index 0b50178d..d80bb845 100644 --- a/builds/gyp/platform.hpp +++ b/builds/gyp/platform.hpp @@ -62,6 +62,7 @@ # define ZMQ_HAVE_EVENTFD 1 # define ZMQ_HAVE_IFADDRS 1 # define ZMQ_HAVE_SOCK_CLOEXEC 1 +# define ZMQ_HAVE_SO_BINDTODEVICE 1 # define ZMQ_HAVE_SO_KEEPALIVE 1 # define ZMQ_HAVE_SO_PEERCRED 1 # define ZMQ_HAVE_TCP_KEEPCNT 1 diff --git a/configure.ac b/configure.ac index 99447de8..47a888f4 100644 --- a/configure.ac +++ b/configure.ac @@ -668,6 +668,12 @@ LIBZMQ_CHECK_O_CLOEXEC([ [Whether O_CLOEXEC is defined and functioning.]) ]) +LIBZMQ_CHECK_SO_BINDTODEVICE([ + AC_DEFINE([ZMQ_HAVE_SO_BINDTODEVICE], + [1], + [Whether SO_BINDTODEVICE is supported.]) + ]) + # TCP keep-alives Checks. LIBZMQ_CHECK_SO_KEEPALIVE([ AC_DEFINE([ZMQ_HAVE_SO_KEEPALIVE], diff --git a/doc/zmq_getsockopt.txt b/doc/zmq_getsockopt.txt index 07b1e84a..83d18819 100644 --- a/doc/zmq_getsockopt.txt +++ b/doc/zmq_getsockopt.txt @@ -63,6 +63,23 @@ Default value:: 100 Applicable socket types:: all, only for connection-oriented transports +ZMQ_BINDTODEVICE: Retrieve name of device the socket is bound to +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The 'ZMQ_BINDTODEVICE' option retrieves the name of the device this socket is +bound to, eg. an interface or VRF. If a socket is bound to an interface, only +packets received from that interface are processed by the socket. If device +is a VRF device, then subsequent binds/connects to that socket use addresses +in the VRF routing table. + +NOTE: in DRAFT state, not yet available in stable releases. + +[horizontal] +Option value type:: character string +Option value unit:: N/A +Default value:: not set +Applicable socket types:: all, when using TCP or UDP transports. + + ZMQ_CONNECT_TIMEOUT: Retrieve connect() timeout ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Retrieves how long to wait before timing-out a connect() system call. diff --git a/doc/zmq_setsockopt.txt b/doc/zmq_setsockopt.txt index 57847d51..0cde9f31 100644 --- a/doc/zmq_setsockopt.txt +++ b/doc/zmq_setsockopt.txt @@ -70,6 +70,24 @@ Default value:: 100 Applicable socket types:: all, only for connection-oriented transports. +ZMQ_BINDTODEVICE: Set name of device to bind the socket to +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The 'ZMQ_BINDTODEVICE' option binds this socket to a particular device, eg. +an interface or VRF. If a socket is bound to an interface, only packets +received from that particular interface are processed by the socket. If device +is a VRF device, then subsequent binds/connects to that socket use addresses +in the VRF routing table. + +NOTE: requires setting CAP_NET_RAW on the compiled program. +NOTE: in DRAFT state, not yet available in stable releases. + +[horizontal] +Option value type:: character string +Option value unit:: N/A +Default value:: not set +Applicable socket types:: all, when using TCP or UDP transports. + + ZMQ_CONNECT_RID: Assign the next outbound connection id ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The 'ZMQ_CONNECT_RID' option sets the peer id of the next host connected diff --git a/include/zmq.h b/include/zmq.h index 4858d693..e1606a77 100644 --- a/include/zmq.h +++ b/include/zmq.h @@ -368,6 +368,7 @@ ZMQ_EXPORT const char *zmq_msg_gets (const zmq_msg_t *msg, const char *property) #define ZMQ_VMCI_BUFFER_MAX_SIZE 87 #define ZMQ_VMCI_CONNECT_TIMEOUT 88 #define ZMQ_USE_FD 89 +#define ZMQ_BINDTODEVICE 90 /* Message options */ #define ZMQ_MORE 1 diff --git a/src/ip.cpp b/src/ip.cpp index eb0e1181..bdb42ddd 100644 --- a/src/ip.cpp +++ b/src/ip.cpp @@ -217,3 +217,16 @@ int zmq::set_nosigpipe (fd_t s_) return 0; } + +void zmq::bind_to_device (fd_t s_, std::string &bound_device_) +{ +#ifdef ZMQ_HAVE_SO_BINDTODEVICE + int rc = setsockopt(s_, SOL_SOCKET, SO_BINDTODEVICE, bound_device_.c_str (), bound_device_.length ()); + +#ifdef ZMQ_HAVE_WINDOWS + wsa_assert (rc != SOCKET_ERROR); +#else + errno_assert (rc == 0); +#endif +#endif +} diff --git a/src/ip.hpp b/src/ip.hpp index 26cea3e2..5c718189 100644 --- a/src/ip.hpp +++ b/src/ip.hpp @@ -56,6 +56,9 @@ namespace zmq // Return 0 on success, -1 if the connection has been closed by the peer int set_nosigpipe (fd_t s_); + // Binds the underlying socket to the given device, eg. VRF or interface + void bind_to_device (fd_t s_, std::string &bound_device_); + } #endif diff --git a/src/options.cpp b/src/options.cpp index 4de092a5..2f48a890 100644 --- a/src/options.cpp +++ b/src/options.cpp @@ -34,6 +34,16 @@ #include "err.hpp" #include "macros.hpp" +#ifndef ZMQ_HAVE_WINDOWS +#include +#endif + +#if defined IFNAMSIZ +#define BINDDEVSIZ IFNAMSIZ +#else +#define BINDDEVSIZ 16 +#endif + zmq::options_t::options_t () : sndhwm (1000), rcvhwm (1000), @@ -605,6 +615,19 @@ int zmq::options_t::setsockopt (int option_, const void *optval_, } break; + case ZMQ_BINDTODEVICE: + if (optval_ == NULL && optvallen_ == 0) { + bound_device.clear (); + return 0; + } + else + if (optval_ != NULL && optvallen_ > 0 && optvallen_ <= BINDDEVSIZ) { + bound_device = + std::string ((const char *) optval_, optvallen_); + return 0; + } + break; + default: #if defined (ZMQ_ACT_MILITANT) // There are valid scenarios for probing with unknown socket option @@ -1021,6 +1044,14 @@ int zmq::options_t::getsockopt (int option_, void *optval_, size_t *optvallen_) } break; + case ZMQ_BINDTODEVICE: + if (*optvallen_ >= bound_device.size () + 1) { + memcpy (optval_, bound_device.c_str (), bound_device.size () + 1); + *optvallen_ = bound_device.size () + 1; + return 0; + } + break; + default: #if defined (ZMQ_ACT_MILITANT) malformed = false; diff --git a/src/options.hpp b/src/options.hpp index 267341ca..b5672394 100644 --- a/src/options.hpp +++ b/src/options.hpp @@ -240,6 +240,9 @@ namespace zmq // will be used as the File Descriptor instead of allocating a new // one via the socket () system call. int use_fd; + + // Device to bind the underlying socket to, eg. VRF or interface + std::string bound_device; }; } diff --git a/src/socks_connecter.cpp b/src/socks_connecter.cpp index 1962a6aa..ab14dcd5 100644 --- a/src/socks_connecter.cpp +++ b/src/socks_connecter.cpp @@ -334,6 +334,10 @@ int zmq::socks_connecter_t::connect_to_proxy () if (options.tos != 0) set_ip_type_of_service (s, options.tos); + // Bind the socket to a device if applicable + if (!options.bound_device.empty ()) + bind_to_device (s, options.bound_device); + // Set the socket to non-blocking mode so that we get async connect(). unblock_socket (s); diff --git a/src/tcp_connecter.cpp b/src/tcp_connecter.cpp index 4ea24644..68878735 100644 --- a/src/tcp_connecter.cpp +++ b/src/tcp_connecter.cpp @@ -305,6 +305,10 @@ int zmq::tcp_connecter_t::open () if (options.tos != 0) set_ip_type_of_service (s, options.tos); + // Bind the socket to a device if applicable + if (!options.bound_device.empty ()) + bind_to_device (s, options.bound_device); + // Set the socket to non-blocking mode so that we get async connect(). unblock_socket (s); diff --git a/src/tcp_listener.cpp b/src/tcp_listener.cpp index d62c10a0..7fb05c98 100644 --- a/src/tcp_listener.cpp +++ b/src/tcp_listener.cpp @@ -212,6 +212,10 @@ int zmq::tcp_listener_t::set_address (const char *addr_) if (options.tos != 0) set_ip_type_of_service (s, options.tos); + // Bind the socket to a device if applicable + if (!options.bound_device.empty ()) + bind_to_device (s, options.bound_device); + // Set the socket buffer limits for the underlying socket. if (options.sndbuf >= 0) set_tcp_send_buffer (s, options.sndbuf); diff --git a/src/udp_engine.cpp b/src/udp_engine.cpp index 6e7b2511..4803913d 100644 --- a/src/udp_engine.cpp +++ b/src/udp_engine.cpp @@ -123,6 +123,10 @@ void zmq::udp_engine_t::plug (io_thread_t* io_thread_, session_base_t *session_) errno_assert (rc == 0); #endif + // Bind the socket to a device if applicable + if (!options.bound_device.empty ()) + bind_to_device (fd, options.bound_device); + rc = bind (fd, address->resolved.udp_addr->bind_addr (), address->resolved.udp_addr->bind_addrlen ()); #ifdef ZMQ_HAVE_WINDOWS diff --git a/tests/test_setsockopt.cpp b/tests/test_setsockopt.cpp index eb59ea87..31a35d7a 100644 --- a/tests/test_setsockopt.cpp +++ b/tests/test_setsockopt.cpp @@ -110,9 +110,42 @@ void test_setsockopt_use_fd () zmq_ctx_term (ctx); } +#define BOUNDDEVBUFSZ 16 +void test_setsockopt_bindtodevice () +{ + int rc; + void *ctx = zmq_ctx_new (); + void *socket = zmq_socket (ctx, ZMQ_PUSH); + + char devname[BOUNDDEVBUFSZ]; + size_t buflen = BOUNDDEVBUFSZ; + + rc = zmq_getsockopt (socket, ZMQ_BINDTODEVICE, devname, &buflen); + assert(rc == 0); + assert(devname[0] == '\0'); + assert(buflen == 1); + + sprintf(devname, "testdev"); + buflen = strlen(devname); + + rc = zmq_setsockopt (socket, ZMQ_BINDTODEVICE, devname, buflen); + assert(rc == 0); + + buflen = BOUNDDEVBUFSZ; + memset(devname, 0, buflen); + + rc = zmq_getsockopt (socket, ZMQ_BINDTODEVICE, devname, &buflen); + assert(rc == 0); + assert(!strncmp("testdev", devname, buflen)); + + zmq_close (socket); + zmq_ctx_term (ctx); +} + int main (void) { test_setsockopt_tcp_recv_buffer (); test_setsockopt_tcp_send_buffer (); test_setsockopt_use_fd (); + test_setsockopt_bindtodevice (); }