From 5c3c8c67045b9d1e7dd0b74996e7c87e837d4a22 Mon Sep 17 00:00:00 2001 From: Jukka Rissanen Date: Sun, 17 Nov 2024 14:12:25 +0200 Subject: [PATCH] net: sockets: Add support for IP_LOCAL_PORT_RANGE socket option. Add support for IP_LOCAL_PORT_RANGE socket option. The option supports both IPv4 and IPv6 sockets although the type is IPPROTO_IP. The option can be used to enforce the ephemeral port number selection to be in certain range. Signed-off-by: Jukka Rissanen --- include/zephyr/net/net_context.h | 15 ++ include/zephyr/net/socket.h | 3 + subsys/net/ip/Kconfig | 9 + subsys/net/ip/net_context.c | 228 ++++++++++++++++++++++---- subsys/net/lib/sockets/sockets_inet.c | 32 ++++ 5 files changed, 254 insertions(+), 33 deletions(-) diff --git a/include/zephyr/net/net_context.h b/include/zephyr/net/net_context.h index 275496d91b20f8..80d84426789ecd 100644 --- a/include/zephyr/net/net_context.h +++ b/include/zephyr/net/net_context.h @@ -316,6 +316,20 @@ __net_socket struct net_context { socklen_t addrlen; } proxy; #endif +#if defined(CONFIG_NET_CONTEXT_CLAMP_PORT_RANGE) + /** Restrict local port range between these values. + * The option takes an uint32_t value with the high 16 bits + * set to the upper range bound, and the low 16 bits set to + * the lower range bound. Range bounds are inclusive. The + * 16-bit values should be in host byte order. + * The lower bound has to be less than the upper bound when + * both bounds are not zero. Otherwise, setting the option + * fails with EINVAL. + * If either bound is outside of the global local port range, + * or is zero, then that bound has no effect. + */ + uint32_t port_range; +#endif #if defined(CONFIG_NET_CONTEXT_RCVTIMEO) /** Receive timeout */ k_timeout_t rcvtimeo; @@ -1310,6 +1324,7 @@ enum net_context_option { NET_OPT_TIMESTAMPING = 18, /**< Packet timestamping */ NET_OPT_MCAST_IFINDEX = 19, /**< IPv6 multicast output network interface index */ NET_OPT_MTU = 20, /**< IPv4 socket path MTU */ + NET_OPT_LOCAL_PORT_RANGE = 21, /**< Clamp local port range */ }; /** diff --git a/include/zephyr/net/socket.h b/include/zephyr/net/socket.h index c5282fdde3792c..9112c95fdc4186 100644 --- a/include/zephyr/net/socket.h +++ b/include/zephyr/net/socket.h @@ -1222,6 +1222,9 @@ struct ip_mreq { struct in_addr imr_interface; /**< IP address of local interface */ }; +/** Clamp down the global port range for a given socket */ +#define IP_LOCAL_PORT_RANGE 51 + /** @} */ /** diff --git a/subsys/net/ip/Kconfig b/subsys/net/ip/Kconfig index f123854a7a32c1..a27ee386c98c86 100644 --- a/subsys/net/ip/Kconfig +++ b/subsys/net/ip/Kconfig @@ -757,6 +757,15 @@ config NET_CONTEXT_TIMESTAMPING Allow to set the TIMESTAMPING option on a socket. This way timestamp for a network packet will be added to the net_pkt structure. +config NET_CONTEXT_CLAMP_PORT_RANGE + bool "Allow clamping down the global local port range for net_context" + depends on NET_UDP || NET_TCP + help + Set or get the per-context default local port range. This + option can be used to clamp down the global local UDP/TCP port + range for a given context. The port range is typically set by + IP_LOCAL_PORT_RANGE socket option. + endif # NET_RAW_MODE config NET_SLIP_TAP diff --git a/subsys/net/ip/net_context.c b/subsys/net/ip/net_context.c index 8ad8a2ad5a3ec7..886d6325a3b4e5 100644 --- a/subsys/net/ip/net_context.c +++ b/subsys/net/ip/net_context.c @@ -178,7 +178,8 @@ static int check_used_port(struct net_context *context, uint16_t local_port, const struct sockaddr *local_addr, bool reuseaddr_set, - bool reuseport_set) + bool reuseport_set, + bool check_port_range) { int i; @@ -324,18 +325,79 @@ static int check_used_port(struct net_context *context, } } + /* Make sure that if the port range is active, the port is + * within the range. + */ + if (IS_ENABLED(CONFIG_NET_CONTEXT_CLAMP_PORT_RANGE) && check_port_range) { + uint16_t upper, lower; + + upper = COND_CODE_1(CONFIG_NET_CONTEXT_CLAMP_PORT_RANGE, + (context->options.port_range >> 16), + (0)); + lower = COND_CODE_1(CONFIG_NET_CONTEXT_CLAMP_PORT_RANGE, + (context->options.port_range & 0xffff), + (0)); + + if (upper != 0 && lower != 0 && lower < upper) { + if (ntohs(local_port) < lower || ntohs(local_port) > upper) { + return -ERANGE; + } + } + } + return 0; } +/* How many times we try to find a free port */ +#define MAX_PORT_RETRIES 5 + static uint16_t find_available_port(struct net_context *context, const struct sockaddr *addr) { uint16_t local_port; + int count = MAX_PORT_RETRIES; do { - local_port = sys_rand16_get() | 0x8000; - } while (check_used_port(context, NULL, net_context_get_proto(context), - htons(local_port), addr, false, false) == -EEXIST); + if (IS_ENABLED(CONFIG_NET_CONTEXT_CLAMP_PORT_RANGE)) { + uint16_t upper, lower; + + upper = COND_CODE_1(CONFIG_NET_CONTEXT_CLAMP_PORT_RANGE, + (context->options.port_range >> 16), + (0)); + lower = COND_CODE_1(CONFIG_NET_CONTEXT_CLAMP_PORT_RANGE, + (context->options.port_range & 0xffff), + (0)); + + /* This works the same way as in Linux. If either port + * range is 0, then we use random port. If both are set, + * then we use the range. Also make sure that upper is + * greater than lower. + */ + if (upper == 0 || lower == 0 || upper <= lower) { + local_port = sys_rand16_get() | 0x8000; + } else { + local_port = lower + sys_rand16_get() % (upper - lower); + + NET_DBG("Port range %d - %d, proposing port %d", + lower, upper, local_port); + } + } else { + local_port = sys_rand16_get() | 0x8000; + } + + count--; + } while (count > 0 && check_used_port(context, + NULL, + net_context_get_proto(context), + htons(local_port), + addr, + false, + false, + false) == -EEXIST); + + if (count == 0) { + return 0; + } return htons(local_port); } @@ -349,7 +411,7 @@ bool net_context_port_in_use(enum net_ip_protocol proto, const struct sockaddr *local_addr) { return check_used_port(NULL, NULL, proto, htons(local_port), - local_addr, false, false) != 0; + local_addr, false, false, false) != 0; } #if defined(CONFIG_NET_CONTEXT_CHECK) @@ -733,6 +795,49 @@ static int bind_default(struct net_context *context) return -EINVAL; } +static int recheck_port(struct net_context *context, + struct net_if *iface, + int proto, + uint16_t port, + const struct sockaddr *addr) +{ + int ret; + + ret = check_used_port(context, iface, + proto, + net_sin(addr)->sin_port, + addr, + net_context_is_reuseaddr_set(context), + net_context_is_reuseport_set(context), + true); + if (ret != 0) { + if (IS_ENABLED(CONFIG_NET_CONTEXT_CLAMP_PORT_RANGE) && ret == -ERANGE) { + uint16_t re_port; + + NET_DBG("Port %d is out of range, re-selecting!", + ntohs(net_sin(addr)->sin_port)); + re_port = find_available_port(context, addr); + if (re_port == 0U) { + NET_ERR("No available port found (iface %d)", + iface ? net_if_get_by_iface(iface) : 0); + return -EADDRINUSE; + } + + net_sin_ptr(&context->local)->sin_port = re_port; + net_sin(addr)->sin_port = re_port; + } else { + NET_ERR("Port %d is in use!", ntohs(net_sin(addr)->sin_port)); + NET_DBG("Interface %d (%p)", + iface ? net_if_get_by_iface(iface) : 0, iface); + return -EADDRINUSE; + } + } else { + net_sin_ptr(&context->local)->sin_port = net_sin(addr)->sin_port; + } + + return 0; +} + int net_context_bind(struct net_context *context, const struct sockaddr *addr, socklen_t addrlen) { @@ -827,26 +932,22 @@ int net_context_bind(struct net_context *context, const struct sockaddr *addr, ret = 0; if (addr6->sin6_port) { - ret = check_used_port(context, iface, - context->proto, - addr6->sin6_port, - addr, - net_context_is_reuseaddr_set(context), - net_context_is_reuseport_set(context)); + ret = recheck_port(context, iface, context->proto, + addr6->sin6_port, addr); if (ret != 0) { - NET_ERR("Port %d is in use!", - ntohs(addr6->sin6_port)); - NET_DBG("Interface %d (%p)", - iface ? net_if_get_by_iface(iface) : 0, iface); - ret = -EADDRINUSE; goto unlock_ipv6; - } else { - net_sin6_ptr(&context->local)->sin6_port = - addr6->sin6_port; } } else { addr6->sin6_port = net_sin6_ptr(&context->local)->sin6_port; + + if (IS_ENABLED(CONFIG_NET_CONTEXT_CLAMP_PORT_RANGE)) { + ret = recheck_port(context, iface, context->proto, + addr6->sin6_port, addr); + if (ret != 0) { + goto unlock_ipv6; + } + } } NET_DBG("Context %p binding to %s [%s]:%d iface %d (%p)", @@ -938,26 +1039,22 @@ int net_context_bind(struct net_context *context, const struct sockaddr *addr, ret = 0; if (addr4->sin_port) { - ret = check_used_port(context, iface, - context->proto, - addr4->sin_port, - addr, - net_context_is_reuseaddr_set(context), - net_context_is_reuseport_set(context)); + ret = recheck_port(context, iface, context->proto, + addr4->sin_port, addr); if (ret != 0) { - NET_ERR("Port %d is in use!", - ntohs(addr4->sin_port)); - ret = -EADDRINUSE; - NET_DBG("Interface %d (%p)", - iface ? net_if_get_by_iface(iface) : 0, iface); goto unlock_ipv4; - } else { - net_sin_ptr(&context->local)->sin_port = - addr4->sin_port; } } else { addr4->sin_port = net_sin_ptr(&context->local)->sin_port; + + if (IS_ENABLED(CONFIG_NET_CONTEXT_CLAMP_PORT_RANGE)) { + ret = recheck_port(context, iface, context->proto, + addr4->sin_port, addr); + if (ret != 0) { + goto unlock_ipv4; + } + } } NET_DBG("Context %p binding to %s %s:%d iface %d (%p)", @@ -1916,6 +2013,26 @@ static int get_context_mcast_ifindex(struct net_context *context, #endif } +static int get_context_local_port_range(struct net_context *context, + void *value, size_t *len) +{ +#if defined(CONFIG_NET_CONTEXT_CLAMP_PORT_RANGE) + if (len == NULL || *len != sizeof(uint32_t)) { + return -EINVAL; + } + + *((uint32_t *)value) = context->options.port_range; + + return 0; +#else + ARG_UNUSED(context); + ARG_UNUSED(value); + ARG_UNUSED(len); + + return -ENOTSUP; +#endif +} + /* If buf is not NULL, then use it. Otherwise read the data to be written * to net_pkt from msghdr. */ @@ -3384,6 +3501,45 @@ static int set_context_mcast_ifindex(struct net_context *context, #endif } +static int set_context_local_port_range(struct net_context *context, + const void *value, size_t len) +{ +#if defined(CONFIG_NET_CONTEXT_CLAMP_PORT_RANGE) + uint16_t lower_range, upper_range; + uint32_t port_range; + + if (len != sizeof(uint32_t)) { + return -EINVAL; + } + + port_range = *((uint32_t *)value); + lower_range = port_range & 0xffff; + upper_range = port_range >> 16; + + /* If the range is 0, then it means that the port range clamping + * is disabled. If the range is not 0, then the lower range must + * be smaller than the upper range. + */ + if (lower_range != 0U && upper_range != 0U && + lower_range >= upper_range) { + return -EINVAL; + } + + /* If either of the range is 0, then that bound has no effect. + * This is checked when the emphemeral port is selected. + */ + context->options.port_range = port_range; + + return 0; +#else + ARG_UNUSED(context); + ARG_UNUSED(value); + ARG_UNUSED(len); + + return -ENOTSUP; +#endif +} + int net_context_set_option(struct net_context *context, enum net_context_option option, const void *value, size_t len) @@ -3467,6 +3623,9 @@ int net_context_set_option(struct net_context *context, case NET_OPT_MCAST_IFINDEX: ret = set_context_mcast_ifindex(context, value, len); break; + case NET_OPT_LOCAL_PORT_RANGE: + ret = set_context_local_port_range(context, value, len); + break; } k_mutex_unlock(&context->lock); @@ -3549,6 +3708,9 @@ int net_context_get_option(struct net_context *context, case NET_OPT_MCAST_IFINDEX: ret = get_context_mcast_ifindex(context, value, len); break; + case NET_OPT_LOCAL_PORT_RANGE: + ret = get_context_local_port_range(context, value, len); + break; } k_mutex_unlock(&context->lock); diff --git a/subsys/net/lib/sockets/sockets_inet.c b/subsys/net/lib/sockets/sockets_inet.c index e88a6daa7f8073..83326b0b3849e2 100644 --- a/subsys/net/lib/sockets/sockets_inet.c +++ b/subsys/net/lib/sockets/sockets_inet.c @@ -1957,6 +1957,23 @@ int zsock_getsockopt_ctx(struct net_context *ctx, int level, int optname, return 0; } + + break; + + case IP_LOCAL_PORT_RANGE: + if (IS_ENABLED(CONFIG_NET_CONTEXT_CLAMP_PORT_RANGE)) { + ret = net_context_get_option(ctx, + NET_OPT_LOCAL_PORT_RANGE, + optval, optlen); + if (ret < 0) { + errno = -ret; + return -1; + } + + return 0; + } + + break; } break; @@ -2559,6 +2576,21 @@ int zsock_setsockopt_ctx(struct net_context *ctx, int level, int optname, optlen, false); } + break; + + case IP_LOCAL_PORT_RANGE: + if (IS_ENABLED(CONFIG_NET_CONTEXT_CLAMP_PORT_RANGE)) { + ret = net_context_set_option(ctx, + NET_OPT_LOCAL_PORT_RANGE, + optval, optlen); + if (ret < 0) { + errno = -ret; + return -1; + } + + return 0; + } + break; }