From acba6bdd6cd203f70702745a821945f4a83c7fab Mon Sep 17 00:00:00 2001 From: Sergey KHripchenko Date: Thu, 12 Apr 2012 18:37:14 +0400 Subject: [PATCH] Implement ZMQ_TCP_ACCEPT_FILTER setsockopt() for listening TCP sockets. Assign arbitrary number of filters that will be applied for each new TCP transport connection on a listening socket. If no filters applied, then TCP transport allows connections from any ip. If at least one filter is applied then new connection source ip should be matched. To clear all filters call zmq_setsockopt(socket, ZMQ_TCP_ACCEPT_FILTER, NULL, 0). Filter is a null-terminated string with ipv6 or ipv4 CIDR. For example: localhost 127.0.0.1 mail.ru/24 ::1 ::1/128 3ffe:1:: 3ffe:1::/56 Returns -1 if the filter couldn't be assigned(format error or ipv6 filter with ZMQ_IPV4ONLY set) P.S. The only thing that worries me is that I had to re-enable 'default assign by reference constructor/operator' for 'tcp_address_t' (and for my inherited class tcp_address_mask_t) to store it in std::vector in 'options_t'... --- doc/zmq_setsockopt.txt | 16 +++++++ include/zmq.h | 1 + src/options.cpp | 26 +++++++++++ src/options.hpp | 6 +++ src/tcp_address.cpp | 102 +++++++++++++++++++++++++++++++++++++++++ src/tcp_address.hpp | 28 ++++++++--- src/tcp_listener.cpp | 28 ++++++++++- src/tcp_listener.hpp | 3 +- 8 files changed, 202 insertions(+), 8 deletions(-) diff --git a/doc/zmq_setsockopt.txt b/doc/zmq_setsockopt.txt index 9cc0a6d0..c0434faf 100644 --- a/doc/zmq_setsockopt.txt +++ b/doc/zmq_setsockopt.txt @@ -415,6 +415,22 @@ Default value:: -1 (leave to OS default) Applicable socket types:: all, when using TCP transports. +ZMQ_TCP_ACCEPT_FILTER: Assign filters to allow new TCP connections +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Assign arbitrary number of filters that will be applied for each new TCP transport +connection on a listening socket. +If no filters applied, then TCP transport allows connections from any ip. +If at least one filter is applied then new connection source ip should be matched. +To clear all filters call zmq_setsockopt(socket, ZMQ_TCP_ACCEPT_FILTER, NULL, 0). +Filter is a null-terminated string with ipv6 or ipv4 CIDR. + +[horizontal] +Option value type:: binary data +Option value unit:: N/A +Default value:: no filters (allow from all) +Applicable socket types:: all listening sockets, when using TCP transports. + + RETURN VALUE ------------ The _zmq_setsockopt()_ function shall return zero if successful. Otherwise it diff --git a/include/zmq.h b/include/zmq.h index 713d9f2b..66c158e4 100644 --- a/include/zmq.h +++ b/include/zmq.h @@ -226,6 +226,7 @@ ZMQ_EXPORT int zmq_msg_set (zmq_msg_t *msg, int option, int optval); #define ZMQ_TCP_KEEPALIVE_CNT 35 #define ZMQ_TCP_KEEPALIVE_IDLE 36 #define ZMQ_TCP_KEEPALIVE_INTVL 37 +#define ZMQ_TCP_ACCEPT_FILTER 38 /* Message options */ diff --git a/src/options.cpp b/src/options.cpp index ef608829..3be8ba65 100644 --- a/src/options.cpp +++ b/src/options.cpp @@ -287,6 +287,32 @@ int zmq::options_t::setsockopt (int option_, const void *optval_, #endif return 0; } + + case ZMQ_TCP_ACCEPT_FILTER: + { + if (optvallen_ == 0 && optval_ == NULL) { + tcp_accept_filters.clear (); + return 0; + } + else + if (optvallen_ < 1 || optvallen_ > 255 || optval_ == NULL || *((const char*) optval_) == 0) { + errno = EINVAL; + return -1; + } + else { + std::string filter_str ((const char*) optval_, optvallen_); + + tcp_address_mask_t filter; + int rc = filter.resolve (filter_str.c_str (), ipv4only ? true : false); + if (rc != 0) { + errno = EINVAL; + return -1; + } + tcp_accept_filters.push_back(filter); + + return 0; + } + } } errno = EINVAL; return -1; diff --git a/src/options.hpp b/src/options.hpp index d1302d93..9f6658ed 100644 --- a/src/options.hpp +++ b/src/options.hpp @@ -24,9 +24,11 @@ #define __ZMQ_OPTIONS_HPP_INCLUDED__ #include +#include #include "stddef.h" #include "stdint.hpp" +#include "tcp_address.hpp" namespace zmq { @@ -118,6 +120,10 @@ namespace zmq int tcp_keepalive_idle; int tcp_keepalive_intvl; + // TCP accept() filters + typedef std::vector tcp_accept_filters_t; + tcp_accept_filters_t tcp_accept_filters; + // ID of the socket. int socket_id; }; diff --git a/src/tcp_address.cpp b/src/tcp_address.cpp index 44579565..6ec00e88 100644 --- a/src/tcp_address.cpp +++ b/src/tcp_address.cpp @@ -445,3 +445,105 @@ sa_family_t zmq::tcp_address_t::family () const return address.generic.sa_family; } +zmq::tcp_address_mask_t::tcp_address_mask_t () : + tcp_address_t () +{ + address_mask = -1; +} + +const int zmq::tcp_address_mask_t::mask () const +{ + return address_mask; +} + +int zmq::tcp_address_mask_t::resolve (const char *name_, bool ipv4only_) +{ + std::string addr_str; + std::string mask_str; + + // Find '/' at the end that separates address from the cidr mask number. + // Allow empty mask clause and threat it like '/32' for ipv4 or '/128' for ipv6. + const char *delimiter = strrchr (name_, '/'); + if (delimiter != NULL) { + addr_str.assign (name_, delimiter - name_); + mask_str.assign (delimiter + 1); + if (mask_str.empty ()) { + errno = EINVAL; + return -1; + } + } + else { + addr_str.assign (name_); + } + + // Parse address part using standard routines. + int rc = tcp_address_t::resolve_hostname (addr_str.c_str (), ipv4only_); + if (rc != 0) + return rc; + + // Parse the cidr mask number. + if (mask_str.empty ()) { + if (address.generic.sa_family == AF_INET6) + address_mask = 128; + else + address_mask = 32; + } + else + if (mask_str == "0") { + address_mask = 0; + } + else { + int mask = atoi (mask_str.c_str ()); + if ( + (mask < 1) || + (address.generic.sa_family == AF_INET6 && mask > 128) || + (address.generic.sa_family != AF_INET6 && mask > 32) + ) { + errno = EINVAL; + return -1; + } + address_mask = mask; + } + + return 0; +} + +const bool zmq::tcp_address_mask_t::match_address (const struct sockaddr *ss, socklen_t ss_len) const +{ + zmq_assert (ss != NULL && ss_len >= sizeof(struct sockaddr)); + + if (ss->sa_family != address.generic.sa_family) + return false; + + if (address_mask > 0) { + int mask; + const int8_t *our_bytes, *their_bytes; + if (ss->sa_family == AF_INET6) { + zmq_assert (ss_len == sizeof (struct sockaddr_in6)); + their_bytes = (const int8_t *) &(((const struct sockaddr_in6 *) ss)->sin6_addr); + our_bytes = (const int8_t *) &address.ipv6.sin6_addr; + mask = sizeof (struct in6_addr) * 8; + } + else { + zmq_assert (ss_len == sizeof (struct sockaddr_in)); + their_bytes = (const int8_t *) &(((const struct sockaddr_in *) ss)->sin_addr); + our_bytes = (const int8_t *) &address.ipv4.sin_addr; + mask = sizeof (struct in_addr) * 8; + } + if (address_mask < mask) mask = address_mask; + + int full_bytes = mask / 8; + if (full_bytes) { + if (memcmp(our_bytes, their_bytes, full_bytes)) + return false; + } + + int last_byte_bits = (0xffU << (8 - (mask % 8))) & 0xffU; + if (last_byte_bits) { + if ((their_bytes[full_bytes] & last_byte_bits) != (our_bytes[full_bytes] & last_byte_bits)) + return false; + } + } + + return true; +} diff --git a/src/tcp_address.hpp b/src/tcp_address.hpp index 2d369751..61067c84 100644 --- a/src/tcp_address.hpp +++ b/src/tcp_address.hpp @@ -55,7 +55,7 @@ namespace zmq const sockaddr *addr () const; socklen_t addrlen () const; - private: + protected: int resolve_nic_name (const char *nic_, bool ipv4only_); int resolve_interface (const char *interface_, bool ipv4only_); @@ -66,12 +66,28 @@ namespace zmq sockaddr_in ipv4; sockaddr_in6 ipv6; } address; - - tcp_address_t (const tcp_address_t&); - const tcp_address_t &operator = (const tcp_address_t&); }; - + + class tcp_address_mask_t : public tcp_address_t + { + public: + + tcp_address_mask_t (); + + // This function enhances tcp_address_t::resolve() with ability to parse + // additional cidr-like(/xx) mask value at the end of the name string. + // Works only with remote hostnames. + int resolve (const char* name_, bool ipv4only_); + + const int mask () const; + + const bool match_address (const struct sockaddr *sa, socklen_t ss_len) const; + + private: + + int address_mask; + }; + } #endif - diff --git a/src/tcp_listener.cpp b/src/tcp_listener.cpp index 95ad792a..4f80add2 100644 --- a/src/tcp_listener.cpp +++ b/src/tcp_listener.cpp @@ -235,7 +235,11 @@ zmq::fd_t zmq::tcp_listener_t::accept () { // Accept one connection and deal with different failure modes. zmq_assert (s != retired_fd); - fd_t sock = ::accept (s, NULL, NULL); + + struct sockaddr_storage ss = {0}; + socklen_t ss_len = sizeof (ss); + fd_t sock = ::accept (s, (struct sockaddr *) &ss, &ss_len); + #ifdef ZMQ_HAVE_WINDOWS if (sock == INVALID_SOCKET) { wsa_assert (WSAGetLastError () == WSAEWOULDBLOCK || @@ -250,6 +254,28 @@ zmq::fd_t zmq::tcp_listener_t::accept () return retired_fd; } #endif + + if (!options.tcp_accept_filters.empty ()) { + bool matched = false; +//ss_len = 1; + for (options_t::tcp_accept_filters_t::size_type i = 0; i != options.tcp_accept_filters.size (); ++i) { + if (options.tcp_accept_filters[i].match_address ((struct sockaddr *) &ss, ss_len)) { + matched = true; + break; + } + } + if (!matched) { +#ifdef ZMQ_HAVE_WINDOWS + int rc = closesocket (sock); + wsa_assert (rc != SOCKET_ERROR); +#else + int rc = ::close (sock); + errno_assert (rc == 0); +#endif + return retired_fd; + } + } + return sock; } diff --git a/src/tcp_listener.hpp b/src/tcp_listener.hpp index 08cb0b27..2daa50b8 100644 --- a/src/tcp_listener.hpp +++ b/src/tcp_listener.hpp @@ -62,7 +62,8 @@ namespace zmq // Accept the new connection. Returns the file descriptor of the // newly created connection. The function may return retired_fd - // if the connection was dropped while waiting in the listen backlog. + // if the connection was dropped while waiting in the listen backlog + // or was denied because of accept filters. fd_t accept (); // Address to listen on.