diff --git a/include/zephyr/net/net_context.h b/include/zephyr/net/net_context.h index 275496d91b20f..80d84426789ec 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 c5282fdde3792..9112c95fdc418 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 f123854a7a32c..a27ee386c98c8 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 8ad8a2ad5a3ec..886d6325a3b4e 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 e88a6daa7f807..83326b0b3849e 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; } diff --git a/tests/net/socket/udp/prj.conf b/tests/net/socket/udp/prj.conf index f90a1807c3d70..ebdbb0d24e73a 100644 --- a/tests/net/socket/udp/prj.conf +++ b/tests/net/socket/udp/prj.conf @@ -21,7 +21,7 @@ CONFIG_TEST_RANDOM_GENERATOR=y CONFIG_MAIN_STACK_SIZE=2048 CONFIG_ZTEST_STACK_SIZE=2048 -CONFIG_HEAP_MEM_POOL_SIZE=256 +CONFIG_HEAP_MEM_POOL_SIZE=512 CONFIG_ZTEST=y CONFIG_NET_TEST=y diff --git a/tests/net/socket/udp/src/main.c b/tests/net/socket/udp/src/main.c index c98714e5f3365..ac06590c71ce3 100644 --- a/tests/net/socket/udp/src/main.c +++ b/tests/net/socket/udp/src/main.c @@ -2925,6 +2925,173 @@ ZTEST(net_socket_udp, test_39_ipv4_multicast_ifindex) loopback_enable_address_swap(true); } +#if defined(CONFIG_NET_CONTEXT_CLAMP_PORT_RANGE) + +#define PORT_RANGE(lower, upper) \ + (uint32_t)(((uint16_t)(upper) << 16) | (uint16_t)(lower)) + +static void check_port_range(struct sockaddr *my_addr, + size_t my_addr_len, + struct sockaddr *local_addr, + size_t local_addr_len) +{ + sa_family_t family = AF_UNSPEC; + uint32_t optval; + size_t addr_len; + size_t optlen; + int sock; + int ret, err; + + addr_len = local_addr_len; + + if (my_addr->sa_family == AF_INET) { + family = AF_INET; + } else if (my_addr->sa_family == AF_INET6) { + family = AF_INET6; + } else { + zassert_true(false, "Invalid address family %d", + my_addr->sa_family); + } + + sock = zsock_socket(family, SOCK_DGRAM, IPPROTO_UDP); + zassert_true(sock >= 0, "Cannot create socket (%d)", -errno); + + optval = PORT_RANGE(1024, 1500); + ret = zsock_setsockopt(sock, IPPROTO_IP, IP_LOCAL_PORT_RANGE, + &optval, sizeof(optval)); + err = -errno; + zexpect_equal(ret, 0, "setsockopt failed (%d)", err); + + optval = 0; optlen = 0U; + ret = zsock_setsockopt(sock, IPPROTO_IP, IP_LOCAL_PORT_RANGE, + &optval, optlen); + err = -errno; + zexpect_equal(ret, -1, "setsockopt failed (%d)", err); + + optval = 0; optlen = sizeof(uint64_t); + ret = zsock_setsockopt(sock, IPPROTO_IP, IP_LOCAL_PORT_RANGE, + &optval, optlen); + err = -errno; + zexpect_equal(ret, -1, "setsockopt failed (%d)", err); + + optval = PORT_RANGE(0, 0); + ret = zsock_setsockopt(sock, IPPROTO_IP, IP_LOCAL_PORT_RANGE, + &optval, sizeof(optval)); + zexpect_equal(ret, 0, "setsockopt failed (%d)", err); + + /* Linux allows setting the invalid port range but that is not + * then taken into use when we bind the socket. + */ + optval = PORT_RANGE(1024, 0); + ret = zsock_setsockopt(sock, IPPROTO_IP, IP_LOCAL_PORT_RANGE, + &optval, sizeof(optval)); + zexpect_equal(ret, 0, "setsockopt failed (%d)", err); + + optval = PORT_RANGE(0, 1024); + ret = zsock_setsockopt(sock, IPPROTO_IP, IP_LOCAL_PORT_RANGE, + &optval, sizeof(optval)); + zexpect_equal(ret, 0, "setsockopt failed (%d)", err); + + /* Then set a valid range and verify that bound socket is using it */ + optval = PORT_RANGE(10000, 10010); + ret = zsock_setsockopt(sock, IPPROTO_IP, IP_LOCAL_PORT_RANGE, + &optval, sizeof(optval)); + zexpect_equal(ret, 0, "setsockopt failed (%d)", err); + + optval = 0; optlen = sizeof(optval); + ret = zsock_getsockopt(sock, IPPROTO_IP, IP_LOCAL_PORT_RANGE, + &optval, &optlen); + zexpect_equal(ret, 0, "setsockopt failed (%d)", err); + zexpect_equal(optval, PORT_RANGE(10000, 10010), "Invalid port range"); + + ret = zsock_bind(sock, my_addr, my_addr_len); + err = -errno; + zexpect_equal(ret, 0, "bind failed (%d)", err); + + ret = zsock_getsockname(sock, local_addr, &addr_len); + err = -errno; + zexpect_equal(ret, 0, "getsockname failed (%d)", err); + + /* The port should be in the range */ + zexpect_true(ntohs(net_sin(local_addr)->sin_port) >= 10000 && + ntohs(net_sin(local_addr)->sin_port) <= 10010, + "Invalid port %d", ntohs(net_sin(local_addr)->sin_port)); + + (void)zsock_close(sock); + + /* Try setting invalid range and verify that we do not net a port from that + * range. + */ + sock = zsock_socket(family, SOCK_DGRAM, IPPROTO_UDP); + zassert_true(sock >= 0, "Cannot create socket (%d)", -errno); + + optval = PORT_RANGE(1001, 1000); + ret = zsock_setsockopt(sock, IPPROTO_IP, IP_LOCAL_PORT_RANGE, + &optval, sizeof(optval)); + err = -errno; + zexpect_equal(ret, -1, "setsockopt failed (%d)", err); + zexpect_equal(err, -EINVAL, "Invalid errno (%d)", -err); + + /* Port range cannot be just one port */ + optval = PORT_RANGE(1001, 1001); + ret = zsock_setsockopt(sock, IPPROTO_IP, IP_LOCAL_PORT_RANGE, + &optval, sizeof(optval)); + err = -errno; + zexpect_equal(ret, -1, "setsockopt failed (%d)", err); + zexpect_equal(err, -EINVAL, "Invalid errno (%d)", -err); + + optval = PORT_RANGE(0, 1000); + ret = zsock_setsockopt(sock, IPPROTO_IP, IP_LOCAL_PORT_RANGE, + &optval, sizeof(optval)); + err = -errno; + zexpect_equal(ret, 0, "setsockopt failed (%d)", err); + + ret = zsock_bind(sock, my_addr, my_addr_len); + err = -errno; + zexpect_equal(ret, 0, "bind failed (%d)", err); + + addr_len = local_addr_len; + ret = zsock_getsockname(sock, local_addr, &addr_len); + err = -errno; + zexpect_equal(ret, 0, "getsockname failed (%d)", err); + + /* The port should not be in the range */ + zexpect_false(ntohs(net_sin(local_addr)->sin_port) >= 1000 && + ntohs(net_sin(local_addr)->sin_port) <= 1001, + "Invalid port %d", ntohs(net_sin(local_addr)->sin_port)); + + (void)zsock_close(sock); +} +#endif + +ZTEST(net_socket_udp, test_40_clamp_udp_tcp_port_range) +{ +#if defined(CONFIG_NET_CONTEXT_CLAMP_PORT_RANGE) + struct sockaddr_in my_addr4 = { + .sin_family = AF_INET, + .sin_port = 0, + .sin_addr = { { { 192, 0, 2, 2 } } }, + }; + struct sockaddr_in6 my_addr6 = { + .sin6_family = AF_INET6, + .sin6_port = 0, + .sin6_addr = in6addr_loopback, + }; + struct sockaddr_in local_addr4; + struct sockaddr_in6 local_addr6; + + /* First try with a IPv4 socket */ + check_port_range((struct sockaddr *)&my_addr4, sizeof(my_addr4), + (struct sockaddr *)&local_addr4, sizeof(local_addr4)); + + /* Finally try with a IPv6 socket */ + check_port_range((struct sockaddr *)&my_addr6, sizeof(my_addr6), + (struct sockaddr *)&local_addr6, sizeof(local_addr6)); +#else + ztest_test_skip(); +#endif +} + static void after(void *arg) { ARG_UNUSED(arg); diff --git a/tests/net/socket/udp/testcase.yaml b/tests/net/socket/udp/testcase.yaml index 55477ca69042c..9ae33f49a7357 100644 --- a/tests/net/socket/udp/testcase.yaml +++ b/tests/net/socket/udp/testcase.yaml @@ -19,6 +19,9 @@ tests: net.socket.udp.pktinfo: extra_configs: - CONFIG_NET_CONTEXT_RECV_PKTINFO=y + net.socket.udp.port_range: + extra_configs: + - CONFIG_NET_CONTEXT_CLAMP_PORT_RANGE=y net.socket.udp.ttl: extra_configs: - CONFIG_NET_SOCKETS_PACKET=y