zephyr/subsys/net/lib/shell/tcp.c
Robert Lubos 05361edb1b net: shell: Fix TCP connect behavior
Fix two issues with net tcp command:

* The `net tcp` commands are still based on net_context API. For TCP,
  the API caller (net shell) should add one extra reference to the
  allocated net context, to prevent premature context release in case of
  connection teardown. Currently that was not the case, and the context
  was released too early, resulting in missing final ACK from the Zephyr
  side on connection close.

* The net context API should not be called from the registered connect
  callback, as this creates a temporary deadlock situation. The
  net_context_connect() function blocks until the connection is
  established, or an error or timeout occurs. For that time the
  net_context mutex is being locked. In case of connection error (for
  example after receiving RST packet) the connect callback is called,
  indicating an error. If we try to call net_context API from within, a
  deadlock situation takes place, as the context mutex is still locked
  by the net_context_connect() (called from the shell thread). This
  blocks the further execution of the TCP stack and can result in an
  unexpected behavior (like for example retransmitting the SYN packet,
  which takes place from yet another thread, TCP work queue).
  Fix this, by releasing the net context not from the callback directly,
  but based on the return value from net_context_connect().

Signed-off-by: Robert Lubos <robert.lubos@nordicsemi.no>
2023-10-23 16:08:37 +02:00

418 lines
9.6 KiB
C

/*
* Copyright (c) 2016 Intel Corporation
* Copyright (c) 2023 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/logging/log.h>
LOG_MODULE_DECLARE(net_shell);
#include <stdlib.h>
#include "common.h"
#if defined(CONFIG_NET_TCP) && defined(CONFIG_NET_NATIVE_TCP)
static struct net_context *tcp_ctx;
static const struct shell *tcp_shell;
#define TCP_CONNECT_TIMEOUT K_SECONDS(5) /* ms */
#define TCP_TIMEOUT K_SECONDS(2) /* ms */
static void tcp_connected(struct net_context *context,
int status,
void *user_data)
{
if (status < 0) {
PR_SHELL(tcp_shell, "TCP connection failed (%d)\n", status);
} else {
PR_SHELL(tcp_shell, "TCP connected\n");
}
}
static void get_my_ipv6_addr(struct net_if *iface,
struct sockaddr *myaddr)
{
#if defined(CONFIG_NET_IPV6)
const struct in6_addr *my6addr;
my6addr = net_if_ipv6_select_src_addr(iface,
&net_sin6(myaddr)->sin6_addr);
memcpy(&net_sin6(myaddr)->sin6_addr, my6addr, sizeof(struct in6_addr));
net_sin6(myaddr)->sin6_port = 0U; /* let the IP stack to select */
#endif
}
static void get_my_ipv4_addr(struct net_if *iface,
struct sockaddr *myaddr)
{
#if defined(CONFIG_NET_NATIVE_IPV4)
/* Just take the first IPv4 address of an interface. */
memcpy(&net_sin(myaddr)->sin_addr,
&iface->config.ip.ipv4->unicast[0].address.in_addr,
sizeof(struct in_addr));
net_sin(myaddr)->sin_port = 0U; /* let the IP stack to select */
#endif
}
static void print_connect_info(const struct shell *sh,
int family,
struct sockaddr *myaddr,
struct sockaddr *addr)
{
switch (family) {
case AF_INET:
if (IS_ENABLED(CONFIG_NET_IPV4)) {
PR("Connecting from %s:%u ",
net_sprint_ipv4_addr(&net_sin(myaddr)->sin_addr),
ntohs(net_sin(myaddr)->sin_port));
PR("to %s:%u\n",
net_sprint_ipv4_addr(&net_sin(addr)->sin_addr),
ntohs(net_sin(addr)->sin_port));
} else {
PR_INFO("IPv4 not supported\n");
}
break;
case AF_INET6:
if (IS_ENABLED(CONFIG_NET_IPV6)) {
PR("Connecting from [%s]:%u ",
net_sprint_ipv6_addr(&net_sin6(myaddr)->sin6_addr),
ntohs(net_sin6(myaddr)->sin6_port));
PR("to [%s]:%u\n",
net_sprint_ipv6_addr(&net_sin6(addr)->sin6_addr),
ntohs(net_sin6(addr)->sin6_port));
} else {
PR_INFO("IPv6 not supported\n");
}
break;
default:
PR_WARNING("Unknown protocol family (%d)\n", family);
break;
}
}
static void tcp_connect(const struct shell *sh, char *host, uint16_t port,
struct net_context **ctx)
{
struct net_if *iface = net_if_get_default();
struct sockaddr myaddr;
struct sockaddr addr;
struct net_nbr *nbr;
int addrlen;
int family;
int ret;
if (IS_ENABLED(CONFIG_NET_IPV6) && !IS_ENABLED(CONFIG_NET_IPV4)) {
ret = net_addr_pton(AF_INET6, host,
&net_sin6(&addr)->sin6_addr);
if (ret < 0) {
PR_WARNING("Invalid IPv6 address\n");
return;
}
net_sin6(&addr)->sin6_port = htons(port);
addrlen = sizeof(struct sockaddr_in6);
nbr = net_ipv6_nbr_lookup(NULL, &net_sin6(&addr)->sin6_addr);
if (nbr) {
iface = nbr->iface;
}
get_my_ipv6_addr(iface, &myaddr);
family = addr.sa_family = myaddr.sa_family = AF_INET6;
} else if (IS_ENABLED(CONFIG_NET_IPV4) &&
!IS_ENABLED(CONFIG_NET_IPV6)) {
ARG_UNUSED(nbr);
ret = net_addr_pton(AF_INET, host, &net_sin(&addr)->sin_addr);
if (ret < 0) {
PR_WARNING("Invalid IPv4 address\n");
return;
}
get_my_ipv4_addr(iface, &myaddr);
net_sin(&addr)->sin_port = htons(port);
addrlen = sizeof(struct sockaddr_in);
family = addr.sa_family = myaddr.sa_family = AF_INET;
} else if (IS_ENABLED(CONFIG_NET_IPV6) &&
IS_ENABLED(CONFIG_NET_IPV4)) {
ret = net_addr_pton(AF_INET6, host,
&net_sin6(&addr)->sin6_addr);
if (ret < 0) {
ret = net_addr_pton(AF_INET, host,
&net_sin(&addr)->sin_addr);
if (ret < 0) {
PR_WARNING("Invalid IP address\n");
return;
}
net_sin(&addr)->sin_port = htons(port);
addrlen = sizeof(struct sockaddr_in);
get_my_ipv4_addr(iface, &myaddr);
family = addr.sa_family = myaddr.sa_family = AF_INET;
} else {
net_sin6(&addr)->sin6_port = htons(port);
addrlen = sizeof(struct sockaddr_in6);
nbr = net_ipv6_nbr_lookup(NULL,
&net_sin6(&addr)->sin6_addr);
if (nbr) {
iface = nbr->iface;
}
get_my_ipv6_addr(iface, &myaddr);
family = addr.sa_family = myaddr.sa_family = AF_INET6;
}
} else {
PR_WARNING("No IPv6 nor IPv4 is enabled\n");
return;
}
print_connect_info(sh, family, &myaddr, &addr);
ret = net_context_get(family, SOCK_STREAM, IPPROTO_TCP, ctx);
if (ret < 0) {
PR_WARNING("Cannot get TCP context (%d)\n", ret);
return;
}
ret = net_context_bind(*ctx, &myaddr, addrlen);
if (ret < 0) {
PR_WARNING("Cannot bind TCP (%d)\n", ret);
return;
}
/* Note that we cannot put shell as a user_data when connecting
* because the tcp_connected() will be called much later and
* all local stack variables are lost at that point.
*/
tcp_shell = sh;
#if defined(CONFIG_NET_SOCKETS_CONNECT_TIMEOUT)
#define CONNECT_TIMEOUT K_MSEC(CONFIG_NET_SOCKETS_CONNECT_TIMEOUT)
#else
#define CONNECT_TIMEOUT K_SECONDS(3)
#endif
net_context_ref(*ctx);
ret = net_context_connect(*ctx, &addr, addrlen, tcp_connected,
CONNECT_TIMEOUT, NULL);
if (ret < 0) {
PR_WARNING("Connect failed!\n");
net_context_put(*ctx);
tcp_ctx = NULL;
}
}
static void tcp_sent_cb(struct net_context *context,
int status, void *user_data)
{
PR_SHELL(tcp_shell, "Message sent\n");
}
static void tcp_recv_cb(struct net_context *context, struct net_pkt *pkt,
union net_ip_header *ip_hdr,
union net_proto_header *proto_hdr,
int status, void *user_data)
{
int ret, len;
if (pkt == NULL) {
if (!tcp_ctx || !net_context_is_used(tcp_ctx)) {
return;
}
ret = net_context_put(tcp_ctx);
if (ret < 0) {
PR_SHELL(tcp_shell,
"Cannot close the connection (%d)\n", ret);
return;
}
PR_SHELL(tcp_shell, "Connection closed by remote peer.\n");
tcp_ctx = NULL;
return;
}
len = net_pkt_remaining_data(pkt);
(void)net_context_update_recv_wnd(context, len);
PR_SHELL(tcp_shell, "%zu bytes received\n", net_pkt_get_len(pkt));
net_pkt_unref(pkt);
}
#endif
static int cmd_net_tcp_connect(const struct shell *sh, size_t argc, char *argv[])
{
#if defined(CONFIG_NET_TCP) && defined(CONFIG_NET_NATIVE_TCP)
int arg = 0;
/* tcp connect <ip> port */
char *endptr;
char *ip;
uint16_t port;
/* tcp connect <ip> port */
if (tcp_ctx && net_context_is_used(tcp_ctx)) {
PR("Already connected\n");
return -ENOEXEC;
}
if (!argv[++arg]) {
PR_WARNING("Peer IP address missing.\n");
return -ENOEXEC;
}
ip = argv[arg];
if (!argv[++arg]) {
PR_WARNING("Peer port missing.\n");
return -ENOEXEC;
}
port = strtol(argv[arg], &endptr, 10);
if (*endptr != '\0') {
PR_WARNING("Invalid port %s\n", argv[arg]);
return -ENOEXEC;
}
tcp_connect(sh, ip, port, &tcp_ctx);
#else
PR_INFO("Set %s to enable %s support.\n",
"CONFIG_NET_TCP and CONFIG_NET_NATIVE", "TCP");
#endif /* CONFIG_NET_NATIVE_TCP */
return 0;
}
static int cmd_net_tcp_send(const struct shell *sh, size_t argc, char *argv[])
{
#if defined(CONFIG_NET_TCP) && defined(CONFIG_NET_NATIVE_TCP)
int arg = 0;
int ret;
struct net_shell_user_data user_data;
/* tcp send <data> */
if (!tcp_ctx || !net_context_is_used(tcp_ctx)) {
PR_WARNING("Not connected\n");
return -ENOEXEC;
}
if (!argv[++arg]) {
PR_WARNING("No data to send.\n");
return -ENOEXEC;
}
user_data.sh = sh;
ret = net_context_send(tcp_ctx, (uint8_t *)argv[arg],
strlen(argv[arg]), tcp_sent_cb,
TCP_TIMEOUT, &user_data);
if (ret < 0) {
PR_WARNING("Cannot send msg (%d)\n", ret);
return -ENOEXEC;
}
#else
PR_INFO("Set %s to enable %s support.\n",
"CONFIG_NET_TCP and CONFIG_NET_NATIVE", "TCP");
#endif /* CONFIG_NET_NATIVE_TCP */
return 0;
}
static int cmd_net_tcp_recv(const struct shell *sh, size_t argc, char *argv[])
{
#if defined(CONFIG_NET_TCP) && defined(CONFIG_NET_NATIVE_TCP)
int ret;
struct net_shell_user_data user_data;
/* tcp recv */
if (!tcp_ctx || !net_context_is_used(tcp_ctx)) {
PR_WARNING("Not connected\n");
return -ENOEXEC;
}
user_data.sh = sh;
ret = net_context_recv(tcp_ctx, tcp_recv_cb, K_NO_WAIT, &user_data);
if (ret < 0) {
PR_WARNING("Cannot recv data (%d)\n", ret);
return -ENOEXEC;
}
#else
PR_INFO("Set %s to enable %s support.\n",
"CONFIG_NET_TCP and CONFIG_NET_NATIVE", "TCP");
#endif /* CONFIG_NET_NATIVE_TCP */
return 0;
}
static int cmd_net_tcp_close(const struct shell *sh, size_t argc, char *argv[])
{
#if defined(CONFIG_NET_TCP) && defined(CONFIG_NET_NATIVE_TCP)
int ret;
/* tcp close */
if (!tcp_ctx || !net_context_is_used(tcp_ctx)) {
PR_WARNING("Not connected\n");
return -ENOEXEC;
}
ret = net_context_put(tcp_ctx);
if (ret < 0) {
PR_WARNING("Cannot close the connection (%d)\n", ret);
return -ENOEXEC;
}
PR("Connection closed.\n");
tcp_ctx = NULL;
#else
PR_INFO("Set %s to enable %s support.\n",
"CONFIG_NET_TCP and CONFIG_NET_NATIVE", "TCP");
#endif /* CONFIG_NET_TCP */
return 0;
}
static int cmd_net_tcp(const struct shell *sh, size_t argc, char *argv[])
{
ARG_UNUSED(argc);
ARG_UNUSED(argv);
return 0;
}
SHELL_STATIC_SUBCMD_SET_CREATE(net_cmd_tcp,
SHELL_CMD(connect, NULL,
"'net tcp connect <address> <port>' connects to TCP peer.",
cmd_net_tcp_connect),
SHELL_CMD(send, NULL,
"'net tcp send <data>' sends data to peer using TCP.",
cmd_net_tcp_send),
SHELL_CMD(recv, NULL,
"'net tcp recv' receives data using TCP.",
cmd_net_tcp_recv),
SHELL_CMD(close, NULL,
"'net tcp close' closes TCP connection.", cmd_net_tcp_close),
SHELL_SUBCMD_SET_END
);
SHELL_SUBCMD_ADD((net), tcp, &net_cmd_tcp,
"Connect/send/close TCP connection.",
cmd_net_tcp, 1, 0);