net: sockets: add support for SO_REUSEPORT

This commits adds support for the SO_REUSEPORT socket option.

The implementation follows the behavior of BSD and tries to also follow
the specific additional features of linux with the following
limitations:
* SO_REUSEADDR and SO_REUSEPORT are not "the same" for client sockets,
  as we do not have a trivial way so identify a socket as "client"
  during binding. To get the Linux behavior, one has to use SO_REUSEPORT
  with Zephyr
* No prevention of "port hijacking"
* No support for the load balancing stuff for incoming
  packets/connections

There is also a new Kconfig option to control this feature, which is
enabled by default if TCP or UDP is enabled.

Signed-off-by: Tobias Frauenschläger <t.frauenschlaeger@me.com>
This commit is contained in:
Tobias Frauenschläger 2023-09-01 11:22:13 +02:00 committed by Carles Cufí
commit dcc63120cf
7 changed files with 200 additions and 63 deletions

View file

@ -325,6 +325,9 @@ __net_socket struct net_context {
#endif
#if defined(CONFIG_NET_CONTEXT_REUSEADDR)
bool reuseaddr;
#endif
#if defined(CONFIG_NET_CONTEXT_REUSEPORT)
bool reuseport;
#endif
} options;
@ -1077,6 +1080,7 @@ enum net_context_option {
NET_OPT_SNDBUF = 7,
NET_OPT_DSCP_ECN = 8,
NET_OPT_REUSEADDR = 9,
NET_OPT_REUSEPORT = 10,
};
/**

View file

@ -1035,7 +1035,7 @@ struct ifreq {
#define SO_OOBINLINE 10
/** sockopt: Socket lingers on close (ignored, for compatibility) */
#define SO_LINGER 13
/** sockopt: Allow multiple sockets to reuse a single port (ignored, for compatibility) */
/** sockopt: Allow multiple sockets to reuse a single port */
#define SO_REUSEPORT 15
/** sockopt: Receive low watermark (ignored, for compatibility) */

View file

@ -696,6 +696,13 @@ config NET_CONTEXT_REUSEADDR
Allow to set the SO_REUSEADDR flag on a socket. This enables multiple
sockets to bind to the same local IP address.
config NET_CONTEXT_REUSEPORT
bool "Add REUSEPORT support to net_context"
default y if NET_TCP || NET_UDP
help
Allow to set the SO_REUSEPORT flag on a socket. This enables multiple
sockets to bind to the same local IP address and port combination.
config NET_TEST
bool "Network Testing"
help

View file

@ -158,7 +158,8 @@ static struct net_conn *conn_find_handler(uint16_t proto, uint8_t family,
const struct sockaddr *remote_addr,
const struct sockaddr *local_addr,
uint16_t remote_port,
uint16_t local_port)
uint16_t local_port,
bool reuseport_set)
{
struct net_conn *conn;
struct net_conn *tmp;
@ -174,38 +175,6 @@ static struct net_conn *conn_find_handler(uint16_t proto, uint8_t family,
continue;
}
if (remote_addr) {
if (!(conn->flags & NET_CONN_REMOTE_ADDR_SET)) {
continue;
}
if (IS_ENABLED(CONFIG_NET_IPV6) &&
remote_addr->sa_family == AF_INET6 &&
remote_addr->sa_family ==
conn->remote_addr.sa_family) {
if (!net_ipv6_addr_cmp(
&net_sin6(remote_addr)->sin6_addr,
&net_sin6(&conn->remote_addr)->
sin6_addr)) {
continue;
}
} else if (IS_ENABLED(CONFIG_NET_IPV4) &&
remote_addr->sa_family == AF_INET &&
remote_addr->sa_family ==
conn->remote_addr.sa_family) {
if (!net_ipv4_addr_cmp(
&net_sin(remote_addr)->sin_addr,
&net_sin(&conn->remote_addr)->
sin_addr)) {
continue;
}
} else {
continue;
}
} else if (conn->flags & NET_CONN_REMOTE_ADDR_SET) {
continue;
}
if (local_addr) {
if (!(conn->flags & NET_CONN_LOCAL_ADDR_SET)) {
continue;
@ -238,13 +207,48 @@ static struct net_conn *conn_find_handler(uint16_t proto, uint8_t family,
continue;
}
if (net_sin(&conn->remote_addr)->sin_port !=
htons(remote_port)) {
if (net_sin(&conn->local_addr)->sin_port !=
htons(local_port)) {
continue;
}
if (net_sin(&conn->local_addr)->sin_port !=
htons(local_port)) {
if (remote_addr) {
if (!(conn->flags & NET_CONN_REMOTE_ADDR_SET)) {
continue;
}
if (IS_ENABLED(CONFIG_NET_IPV6) &&
remote_addr->sa_family == AF_INET6 &&
remote_addr->sa_family ==
conn->remote_addr.sa_family) {
if (!net_ipv6_addr_cmp(
&net_sin6(remote_addr)->sin6_addr,
&net_sin6(&conn->remote_addr)->
sin6_addr)) {
continue;
}
} else if (IS_ENABLED(CONFIG_NET_IPV4) &&
remote_addr->sa_family == AF_INET &&
remote_addr->sa_family ==
conn->remote_addr.sa_family) {
if (!net_ipv4_addr_cmp(
&net_sin(remote_addr)->sin_addr,
&net_sin(&conn->remote_addr)->
sin_addr)) {
continue;
}
} else {
continue;
}
} else if (conn->flags & NET_CONN_REMOTE_ADDR_SET) {
continue;
} else if (reuseport_set && conn->context != NULL &&
net_context_is_reuseport_set(conn->context)) {
continue;
}
if (net_sin(&conn->remote_addr)->sin_port !=
htons(remote_port)) {
continue;
}
@ -270,10 +274,13 @@ int net_conn_register(uint16_t proto, uint8_t family,
uint8_t flags = 0U;
conn = conn_find_handler(proto, family, remote_addr, local_addr,
remote_port, local_port);
remote_port, local_port,
context != NULL ?
net_context_is_reuseport_set(context) :
false);
if (conn) {
NET_ERR("Identical connection handler %p already found.", conn);
return -EALREADY;
return -EADDRINUSE;
}
conn = conn_get_unused();
@ -736,14 +743,6 @@ enum net_verdict net_conn_input(struct net_pkt *pkt,
continue; /* wrong local address */
}
/* If we have an existing best_match, and that one
* specifies a remote port, then we've matched to a
* LISTENING connection that we should not override.
*/
if (best_match != NULL && best_match->flags & NET_CONN_REMOTE_PORT_SPEC) {
continue; /* do not override listening connection */
}
if (best_rank < NET_CONN_RANK(conn->flags)) {
struct net_pkt *mcast_pkt;

View file

@ -69,6 +69,15 @@ bool net_context_is_reuseaddr_set(struct net_context *context)
#endif
}
bool net_context_is_reuseport_set(struct net_context *context)
{
#if defined(CONFIG_NET_CONTEXT_REUSEPORT)
return context->options.reuseport;
#else
return false;
#endif
}
#if defined(CONFIG_NET_UDP) || defined(CONFIG_NET_TCP)
static inline bool is_in_tcp_listen_state(struct net_context *context)
{
@ -104,7 +113,8 @@ static inline bool is_in_tcp_time_wait_state(struct net_context *context)
static int check_used_port(enum net_ip_protocol proto,
uint16_t local_port,
const struct sockaddr *local_addr,
bool reuseaddr_set)
bool reuseaddr_set,
bool reuseport_set)
{
int i;
@ -130,11 +140,17 @@ static int check_used_port(enum net_ip_protocol proto,
net_sin6_ptr(&contexts[i].local)->sin6_addr) ||
net_ipv6_is_addr_unspecified(
&net_sin6(local_addr)->sin6_addr))) {
if (reuseaddr_set &&
!is_in_tcp_listen_state(&contexts[i]) &&
!(net_ipv6_is_addr_unspecified(
if (reuseport_set &&
net_context_is_reuseport_set(&contexts[i])) {
/* When both context have the REUSEPORT set, both
* may be unspecified.
*/
continue;
} else if (reuseaddr_set &&
!is_in_tcp_listen_state(&contexts[i]) &&
!(net_ipv6_is_addr_unspecified(
net_sin6_ptr(&contexts[i].local)->sin6_addr) &&
net_ipv6_is_addr_unspecified(
net_ipv6_is_addr_unspecified(
&net_sin6(local_addr)->sin6_addr))) {
/* In case of REUSEADDR, only one context may be
* bound to the unspecified address, but not both.
@ -153,7 +169,14 @@ static int check_used_port(enum net_ip_protocol proto,
sin6_addr,
&((struct sockaddr_in6 *)
local_addr)->sin6_addr)) {
if (reuseaddr_set && is_in_tcp_time_wait_state(&contexts[i])) {
if (reuseport_set &&
net_context_is_reuseport_set(&contexts[i])) {
/* When both context have the REUSEPORT set, both
* may be bound to exactly the same address.
*/
continue;
} else if (reuseaddr_set &&
is_in_tcp_time_wait_state(&contexts[i])) {
/* With REUSEADDR, the existing context must be
* in the TCP TIME_WAIT state.
*/
@ -172,11 +195,17 @@ static int check_used_port(enum net_ip_protocol proto,
net_sin_ptr(&contexts[i].local)->sin_addr) ||
net_ipv4_is_addr_unspecified(
&net_sin(local_addr)->sin_addr))) {
if (reuseaddr_set &&
!is_in_tcp_listen_state(&contexts[i]) &&
!(net_ipv4_is_addr_unspecified(
if (reuseport_set &&
net_context_is_reuseport_set(&contexts[i])) {
/* When both context have the REUSEPORT set, both
* may be unspecified.
*/
continue;
} else if (reuseaddr_set &&
!is_in_tcp_listen_state(&contexts[i]) &&
!(net_ipv4_is_addr_unspecified(
net_sin_ptr(&contexts[i].local)->sin_addr) &&
net_ipv4_is_addr_unspecified(
net_ipv4_is_addr_unspecified(
&net_sin(local_addr)->sin_addr))) {
/* In case of REUSEADDR, only one context may be
* bound to the unspecified address, but not both.
@ -195,7 +224,14 @@ static int check_used_port(enum net_ip_protocol proto,
sin_addr,
&((struct sockaddr_in *)
local_addr)->sin_addr)) {
if (reuseaddr_set && is_in_tcp_time_wait_state(&contexts[i])) {
if (reuseport_set &&
net_context_is_reuseport_set(&contexts[i])) {
/* When both context have the REUSEPORT set, both
* may be bound to exactly the same address.
*/
continue;
} else if (reuseaddr_set &&
is_in_tcp_time_wait_state(&contexts[i])) {
/* With REUSEADDR, the existing context must be
* in the TCP TIME_WAIT state.
*/
@ -218,7 +254,7 @@ static uint16_t find_available_port(struct net_context *context,
do {
local_port = sys_rand32_get() | 0x8000;
} while (check_used_port(net_context_get_proto(context),
htons(local_port), addr, false) == -EEXIST);
htons(local_port), addr, false, false) == -EEXIST);
return htons(local_port);
}
@ -231,7 +267,7 @@ bool net_context_port_in_use(enum net_ip_protocol proto,
uint16_t local_port,
const struct sockaddr *local_addr)
{
return check_used_port(proto, htons(local_port), local_addr, false) != 0;
return check_used_port(proto, htons(local_port), local_addr, false, false) != 0;
}
#if defined(CONFIG_NET_CONTEXT_CHECK)
@ -693,7 +729,8 @@ int net_context_bind(struct net_context *context, const struct sockaddr *addr,
ret = check_used_port(context->proto,
addr6->sin6_port,
addr,
net_context_is_reuseaddr_set(context));
net_context_is_reuseaddr_set(context),
net_context_is_reuseport_set(context));
if (ret != 0) {
NET_ERR("Port %d is in use!",
ntohs(addr6->sin6_port));
@ -794,7 +831,8 @@ int net_context_bind(struct net_context *context, const struct sockaddr *addr,
ret = check_used_port(context->proto,
addr4->sin_port,
addr,
net_context_is_reuseaddr_set(context));
net_context_is_reuseaddr_set(context),
net_context_is_reuseport_set(context));
if (ret != 0) {
NET_ERR("Port %d is in use!",
ntohs(addr4->sin_port));
@ -1440,6 +1478,32 @@ static int get_context_reuseaddr(struct net_context *context,
#endif
}
static int get_context_reuseport(struct net_context *context,
void *value, size_t *len)
{
#if defined(CONFIG_NET_CONTEXT_REUSEPORT)
if (!value || !len) {
return -EINVAL;
}
if (*len != sizeof(int)) {
return -EINVAL;
}
if (context->options.reuseport == true) {
*((int *)value) = (int) true;
} else {
*((int *)value) = (int) false;
}
*len = sizeof(int);
return 0;
#else
return -ENOTSUP;
#endif
}
/* If buf is not NULL, then use it. Otherwise read the data to be written
* to net_pkt from msghdr.
*/
@ -2516,6 +2580,28 @@ static int set_context_reuseaddr(struct net_context *context,
#endif
}
static int set_context_reuseport(struct net_context *context,
const void *value, size_t len)
{
#if defined(CONFIG_NET_CONTEXT_REUSEPORT)
bool reuseport = false;
if (len != sizeof(int)) {
return -EINVAL;
}
if (*((int *) value) != 0) {
reuseport = true;
}
context->options.reuseport = reuseport;
return 0;
#else
return -ENOTSUP;
#endif
}
int net_context_set_option(struct net_context *context,
enum net_context_option option,
const void *value, size_t len)
@ -2558,6 +2644,9 @@ int net_context_set_option(struct net_context *context,
case NET_OPT_REUSEADDR:
ret = set_context_reuseaddr(context, value, len);
break;
case NET_OPT_REUSEPORT:
ret = set_context_reuseport(context, value, len);
break;
}
k_mutex_unlock(&context->lock);
@ -2607,6 +2696,9 @@ int net_context_get_option(struct net_context *context,
case NET_OPT_REUSEADDR:
ret = get_context_reuseaddr(context, value, len);
break;
case NET_OPT_REUSEPORT:
ret = get_context_reuseport(context, value, len);
break;
}
k_mutex_unlock(&context->lock);

View file

@ -59,6 +59,7 @@ extern void net_process_tx_packet(struct net_pkt *pkt);
extern void net_context_init(void);
extern const char *net_context_state(struct net_context *context);
extern bool net_context_is_reuseaddr_set(struct net_context *context);
extern bool net_context_is_reuseport_set(struct net_context *context);
extern void net_pkt_init(void);
extern void net_tc_tx_init(void);
extern void net_tc_rx_init(void);
@ -77,6 +78,11 @@ static inline bool net_context_is_reuseaddr_set(struct net_context *context)
ARG_UNUSED(context);
return false;
}
static inline bool net_context_is_reuseport_set(struct net_context *context)
{
ARG_UNUSED(context);
return false;
}
#endif
#if defined(CONFIG_NET_NATIVE)

View file

@ -2027,6 +2027,20 @@ int zsock_getsockopt_ctx(struct net_context *ctx, int level, int optname,
return 0;
}
break;
case SO_REUSEPORT:
if (IS_ENABLED(CONFIG_NET_CONTEXT_REUSEPORT)) {
ret = net_context_get_option(ctx,
NET_OPT_REUSEPORT,
optval, optlen);
if (ret < 0) {
errno = -ret;
return -1;
}
return 0;
}
break;
}
break;
@ -2177,6 +2191,21 @@ int zsock_setsockopt_ctx(struct net_context *ctx, int level, int optname,
break;
case SO_REUSEPORT:
if (IS_ENABLED(CONFIG_NET_CONTEXT_REUSEPORT)) {
ret = net_context_set_option(ctx,
NET_OPT_REUSEPORT,
optval, optlen);
if (ret < 0) {
errno = -ret;
return -1;
}
return 0;
}
break;
case SO_PRIORITY:
if (IS_ENABLED(CONFIG_NET_CONTEXT_PRIORITY)) {
ret = net_context_set_option(ctx,