diff --git a/app/src/sm_at_socket.c b/app/src/sm_at_socket.c index 06ae9330..6f614733 100644 --- a/app/src/sm_at_socket.c +++ b/app/src/sm_at_socket.c @@ -86,9 +86,11 @@ static struct sm_socket { int family; /* Socket address family */ int fd; /* Socket descriptor. */ uint16_t cid; /* PDP Context ID, 0: primary; 1~10: secondary */ + uint16_t local_port; /* Explicitly bound local port. */ int send_flags; /* Send flags */ bool send_cb_set: 1; /* Send callback set */ bool connected: 1; /* Connected flag. */ + bool listen: 1; /* Listen flag for TCP server sockets. */ struct sm_async_poll async_poll; /* Async poll info. */ struct sm_send_ntf send_ntf; /* Send notification info. */ struct modem_pipe *pipe; /* AT pipe associated with this socket */ @@ -117,9 +119,11 @@ static void init_socket(struct sm_socket *socket) socket->family = NRF_AF_UNSPEC; socket->fd = INVALID_SOCKET; socket->cid = 0; + socket->local_port = 0; socket->send_flags = 0; socket->send_cb_set = false; socket->connected = false; + socket->listen = false; socket->send_ntf = (struct sm_send_ntf){0}; socket->async_poll = (struct sm_async_poll){0}; socket->pipe = sm_at_host_get_current_pipe(); @@ -233,6 +237,10 @@ static void auto_reception(struct sm_socket *sock) if (sock == NULL) { return; } + if (sock->listen) { + return; + } + if (sock->connected || sock->type == NRF_SOCK_RAW) { err = do_recv(sock, 0, NRF_MSG_DONTWAIT, sock->async_poll.adr_hex ? AT_SOCKET_MODE_HEX @@ -541,10 +549,6 @@ static int do_socket_open(struct sm_socket *sock) ret = nrf_socket(sock->family, NRF_SOCK_DGRAM, NRF_IPPROTO_UDP); proto = NRF_IPPROTO_UDP; } else if (sock->type == NRF_SOCK_RAW) { - if (sock->role != NRF_SO_SEC_ROLE_CLIENT) { - LOG_ERR("Raw socket: Role must be client"); - return -EINVAL; - } ret = nrf_socket(sock->family, NRF_SOCK_RAW, NRF_IPPROTO_RAW); proto = NRF_IPPROTO_IP; } else { @@ -649,18 +653,6 @@ static int do_secure_socket_open(struct sm_socket *sock, int peer_verify) ret = -errno; goto error; } - /* Set up (D)TLS server role if applicable */ - if (sock->role == AT_SOCKET_ROLE_SERVER) { - int tls_role = NRF_SO_SEC_ROLE_SERVER; - - ret = nrf_setsockopt(sock->fd, NRF_SOL_SECURE, NRF_SO_SEC_ROLE, &tls_role, - sizeof(int)); - if (ret) { - LOG_ERR("nrf_setsockopt(%d) error: %d", NRF_SO_SEC_ROLE, -errno); - ret = -errno; - goto error; - } - } rsp_send("\r\n#XSSOCKET: %d,%d,%d\r\n", sock->fd, sock->type, proto); @@ -745,6 +737,10 @@ static int at_sockopt_to_sockopt(enum at_sockopt at_option, int *level, int *opt *level = NRF_SOL_SOCKET; *option = NRF_SO_RAI; break; + case AT_SO_TCP_SRV_SESSTIMEO: + *level = NRF_IPPROTO_TCP; + *option = NRF_SO_TCP_SRV_SESSTIMEO; + break; default: LOG_WRN("Unsupported option: %d", at_option); @@ -922,7 +918,7 @@ static int sec_sockopt_get(struct sm_socket *sock, enum at_sec_sockopt at_option return ret; } -int bind_to_local_addr(struct sm_socket *sock, uint16_t port) +static int bind_to_local_addr(struct sm_socket *sock, uint16_t port) { int ret; @@ -981,6 +977,7 @@ int bind_to_local_addr(struct sm_socket *sock, uint16_t port) return -EINVAL; } + sock->local_port = port; return 0; } @@ -1385,15 +1382,6 @@ STATIC int handle_at_secure_socket(enum at_parser_cmd_type cmd_type, if (err) { goto error; } - /** Peer verification level for TLS connection. - * - 0 - none - * - 1 - optional - * - 2 - required - * If not set, socket will use defaults (none for servers, - * required for clients) - */ - uint16_t peer_verify; - err = at_parser_num_get(parser, 2, &sock->type); if (err) { goto error; @@ -1402,19 +1390,14 @@ STATIC int handle_at_secure_socket(enum at_parser_cmd_type cmd_type, if (err) { goto error; } - if (sock->role == AT_SOCKET_ROLE_SERVER) { - peer_verify = ZSOCK_TLS_PEER_VERIFY_NONE; - } else if (sock->role == AT_SOCKET_ROLE_CLIENT) { - peer_verify = ZSOCK_TLS_PEER_VERIFY_REQUIRED; - } else { - err = -EINVAL; - goto error; - } sock->sec_tag = SEC_TAG_TLS_INVALID; err = at_parser_num_get(parser, 4, &sock->sec_tag); if (err) { goto error; } + + uint16_t peer_verify = ZSOCK_TLS_PEER_VERIFY_REQUIRED; + if (param_count > 5) { err = at_parser_num_get(parser, 5, &peer_verify); if (err) { @@ -1695,10 +1678,6 @@ STATIC int handle_at_connect(enum at_parser_cmd_type cmd_type, struct at_parser if (sock == NULL) { return -EINVAL; } - if (sock->role != AT_SOCKET_ROLE_CLIENT) { - LOG_ERR("Invalid role"); - return -EOPNOTSUPP; - } err = util_string_get(parser, 2, url, &size); if (err) { return err; @@ -2012,6 +1991,161 @@ STATIC int handle_at_recvfrom(enum at_parser_cmd_type cmd_type, struct at_parser return err; } +static int do_listen(struct sm_socket *sock) +{ + int ret; + + if (sock->type != NRF_SOCK_STREAM || sock->local_port == 0 || + sock->sec_tag != SEC_TAG_TLS_INVALID) { + return -EOPNOTSUPP; + } + + /* Set the socket to non-blocking mode, so accept() won't block. */ + ret = nrf_fcntl(sock->fd, NRF_F_SETFL, NRF_O_NONBLOCK); + if (ret) { + LOG_ERR("nrf_fcntl() failed: %d", -errno); + return -errno; + } + + /* nRF modem ignores the backlog parameter. Backlog in modem is fixed to 2. */ + ret = nrf_listen(sock->fd, 2); + if (ret) { + LOG_ERR("nrf_listen() failed: %d", -errno); + return -errno; + } + + sock->listen = true; + + return 0; +} + +SM_AT_CMD_CUSTOM(xlisten, "AT#XLISTEN", handle_at_listen); +STATIC int handle_at_listen(enum at_parser_cmd_type cmd_type, struct at_parser *parser, uint32_t) +{ + int err = -EINVAL; + int fd; + + struct sm_socket *sock = NULL; + + switch (cmd_type) { + case AT_PARSER_CMD_TYPE_SET: + err = at_parser_num_get(parser, 1, &fd); + if (err) { + return err; + } + sock = find_socket(fd); + if (sock == NULL) { + return -EINVAL; + } + err = do_listen(sock); + break; + + case AT_PARSER_CMD_TYPE_READ: + for (int i = 0; i < SM_MAX_SOCKET_COUNT; i++) { + if (socks[i].fd != INVALID_SOCKET && socks[i].listen) { + rsp_send("\r\n#XLISTEN: %d,%d,%d\r\n", socks[i].fd, socks[i].cid, + socks[i].local_port); + } + } + err = 0; + break; + + case AT_PARSER_CMD_TYPE_TEST: + rsp_send("\r\n#XLISTEN: \r\n"); + err = 0; + break; + + default: + break; + } + + return err; +} + +static int do_accept(struct sm_socket *sock) +{ + int ret; + struct net_sockaddr remote; + net_socklen_t addrlen = sizeof(struct net_sockaddr); + char peer_addr[NRF_INET6_ADDRSTRLEN] = {0}; + uint16_t peer_port = 0; + struct async_poll_ctx *poll_ctx = poll_ctx_from_sock(sock); + + if (sock->type != NRF_SOCK_STREAM || !sock->listen) { + return -EOPNOTSUPP; + } + + ret = nrf_accept(sock->fd, (struct nrf_sockaddr *)&remote, (nrf_socklen_t *)&addrlen); + if (ret < 0) { + LOG_ERR("nrf_accept() failed: %d", -errno); + return -errno; + } + + struct sm_socket *new_sock = find_avail_socket(); + if (new_sock == NULL) { + LOG_ERR("Max socket count reached, closing accepted socket"); + nrf_close(ret); + return -EINVAL; + } + init_socket(new_sock); + new_sock->fd = ret; + new_sock->family = remote.sa_family; + new_sock->type = NRF_SOCK_STREAM; + new_sock->role = AT_SOCKET_ROLE_CLIENT; + new_sock->cid = sock->cid; + new_sock->connected = true; + + util_get_peer_addr(&remote, peer_addr, &peer_port); + rsp_send("\r\n#XACCEPT: %d,%d,\"%s\",%d\r\n", new_sock->fd, new_sock->cid, peer_addr, + peer_port); + + /* Update poll events for xapoll and automatic data reception */ + new_sock->async_poll.adr_flags = poll_ctx->adr_flags; + new_sock->async_poll.adr_hex = poll_ctx->adr_hex; + new_sock->async_poll.xapoll_events_requested = poll_ctx->xapoll_events_requested; + update_poll_events(new_sock, + NRF_POLLIN | NRF_POLLOUT | NRF_POLLERR | NRF_POLLHUP | NRF_POLLNVAL, + true); + + /* Restore POLLIN for listening socket. */ + update_poll_events(sock, NRF_POLLIN, true); + + return 0; +} + +SM_AT_CMD_CUSTOM(xaccept, "AT#XACCEPT", handle_at_accept); +STATIC int handle_at_accept(enum at_parser_cmd_type cmd_type, struct at_parser *parser, uint32_t) +{ + int err = -EINVAL; + int fd; + + struct sm_socket *sock = NULL; + + switch (cmd_type) { + case AT_PARSER_CMD_TYPE_SET: + err = at_parser_num_get(parser, 1, &fd); + if (err) { + return err; + } + sock = find_socket(fd); + if (sock == NULL) { + return -EINVAL; + } + err = do_accept(sock); + break; + + case AT_PARSER_CMD_TYPE_TEST: + rsp_send("\r\n#XACCEPT: \r\n"); + err = 0; + break; + + default: + break; + } + + return err; +} + SM_AT_CMD_CUSTOM(xgetaddrinfo, "AT#XGETADDRINFO", handle_at_getaddrinfo); STATIC int handle_at_getaddrinfo(enum at_parser_cmd_type cmd_type, struct at_parser *parser, uint32_t param_count) diff --git a/app/src/sm_sockopt.h b/app/src/sm_sockopt.h index 343c2131..f2a483e5 100644 --- a/app/src/sm_sockopt.h +++ b/app/src/sm_sockopt.h @@ -24,6 +24,7 @@ enum at_sockopt { AT_SO_IP_ECHO_REPLY = 31, AT_SO_IPV6_ECHO_REPLY = 32, AT_SO_BINDTOPDN = 40, + AT_SO_TCP_SRV_SESSTIMEO = 55, AT_SO_RAI = 61, AT_SO_IPV6_DELAYED_ADDR_REFRESH = 62, }; diff --git a/app/tests/at_socket/src/nrf_modem_at_wrapper.c b/app/tests/at_socket/src/nrf_modem_at_wrapper.c index a7443a0f..87672e44 100644 --- a/app/tests/at_socket/src/nrf_modem_at_wrapper.c +++ b/app/tests/at_socket/src/nrf_modem_at_wrapper.c @@ -38,6 +38,8 @@ extern int handle_at_recvcfg_wrapper_xrecvcfg(char *buf, size_t len, char *at_cm extern int handle_at_socketopt_wrapper_xsocketopt(char *buf, size_t len, char *at_cmd); extern int handle_at_secure_socket_wrapper_xssocket(char *buf, size_t len, char *at_cmd); extern int handle_at_secure_socketopt_wrapper_xssocketopt(char *buf, size_t len, char *at_cmd); +extern int handle_at_listen_wrapper_xlisten(char *buf, size_t len, char *at_cmd); +extern int handle_at_accept_wrapper_xaccept(char *buf, size_t len, char *at_cmd); /* Wrapper for nrf_modem_at_cmd that handles custom commands */ int nrf_modem_at_cmd(void *buf, size_t buf_size, const char *fmt, ...) @@ -73,6 +75,10 @@ int nrf_modem_at_cmd(void *buf, size_t buf_size, const char *fmt, ...) ret = handle_at_close_wrapper_xclose((char *)buf, buf_size, at_cmd); } else if (strncasecmp(at_cmd, "AT#XBIND", 8) == 0) { ret = handle_at_bind_wrapper_xbind((char *)buf, buf_size, at_cmd); + } else if (strncasecmp(at_cmd, "AT#XLISTEN", 10) == 0) { + ret = handle_at_listen_wrapper_xlisten((char *)buf, buf_size, at_cmd); + } else if (strncasecmp(at_cmd, "AT#XACCEPT", 10) == 0) { + ret = handle_at_accept_wrapper_xaccept((char *)buf, buf_size, at_cmd); } else if (strncasecmp(at_cmd, "AT#XCONNECT", 11) == 0) { ret = handle_at_connect_wrapper_xconnect((char *)buf, buf_size, at_cmd); } else if (strncasecmp(at_cmd, "AT#XSENDTO", 10) == 0) { diff --git a/app/tests/at_socket/src/test_at_socket.c b/app/tests/at_socket/src/test_at_socket.c index f941eadc..b1658865 100644 --- a/app/tests/at_socket/src/test_at_socket.c +++ b/app/tests/at_socket/src/test_at_socket.c @@ -64,6 +64,20 @@ static int mock_getsockopt_int_callback(int socket, int level, int option_name, return 0; } +static int mock_getsockopt_tcp_srv_sesstimeo_callback(int socket, int level, int option_name, + void *option_value, net_socklen_t *option_len, + int num_calls) +{ + int *value = (int *)option_value; + + /* Return 135 on first call, 0 on second call for SO_TCP_SRV_SESSTIMEO */ + if (*option_len >= sizeof(int)) { + *value = (num_calls == 0) ? 135 : 0; + *option_len = sizeof(int); + } + return 0; +} + static int mock_getsockopt_hostname_callback(int socket, int level, int option_name, void *option_value, net_socklen_t *option_len, int num_calls) @@ -79,6 +93,30 @@ static int mock_getsockopt_hostname_callback(int socket, int level, int option_n return 0; } +static char *mock_zsock_inet_ntop_192_168_0_100_callback( + net_sa_family_t af, const void *src, char *dst, net_socklen_t size, int num_calls) +{ + strcpy(dst, "192.168.0.100"); + return dst; +} + +static int mock_nrf_accept_with_peer_callback(int socket, struct nrf_sockaddr *restrict address, + nrf_socklen_t *restrict address_len, int num_calls) +{ + /* Populate the address structure with peer information */ + struct nrf_sockaddr_in *addr_in = (struct nrf_sockaddr_in *)address; + + addr_in->sin_family = NRF_AF_INET; + addr_in->sin_port = nrf_htons(5555); /* Port 5555 in network byte order */ + addr_in->sin_addr.s_addr = nrf_htonl(0xC0A80064); /* 192.168.0.100 */ + + if (address_len) { + *address_len = sizeof(struct nrf_sockaddr_in); + } + + return 7; /* Return new socket fd */ +} + void setUp(void) { /* This is run before EACH test */ @@ -90,6 +128,8 @@ void tearDown(void) /* This is run after EACH test */ /* Reset any stubs to prevent interference between tests */ __cmock_nrf_getsockopt_Stub(NULL); + __cmock_zsock_inet_ntop_Stub(NULL); + __cmock_nrf_accept_Stub(NULL); } /* @@ -761,6 +801,234 @@ void test_xconnect_operation(void) send_at_command("AT#XCLOSE=1\r\n"); } +/* + * Test: Socket listen operation via AT command + * - Command: AT#XLISTEN=\r\n + * - Tests: Putting a TCP socket into listening mode + */ +void test_xlisten_operation(void) +{ + const char *response; + const char *cgpaddr_resp = "+CGPADDR: 0,\"10.0.0.1\",\"\"\r\nOK\r\n"; + + /* Create TCP server socket first */ + __cmock_nrf_socket_ExpectAndReturn(NRF_AF_INET, NRF_SOCK_STREAM, NRF_IPPROTO_TCP, 5); + __cmock_nrf_setsockopt_ExpectAnyArgsAndReturn(0); /* SO_SNDTIMEO */ + __cmock_nrf_setsockopt_ExpectAnyArgsAndReturn(0); /* SO_POLLCB */ + send_at_command("AT#XSOCKET=1,1,1\r\n"); /* family=1, type=1, role=1 (server) */ + response = get_captured_response(); + TEST_ASSERT_TRUE(strstr(response, "#XSOCKET: 5,1,6") != NULL); + clear_captured_response(); + + /* Bind to port 8080 before listening */ + __cmock_nrf_modem_at_cmd_CMockExpectAnyArgsAndReturn(__LINE__, 0); + __cmock_nrf_modem_at_cmd_CMockReturnMemThruPtr_buf(__LINE__, (void *)cgpaddr_resp, + strlen(cgpaddr_resp) + 1); + __cmock_nrf_modem_at_cmd_CMockIgnoreArg_len(__LINE__); + __cmock_nrf_modem_at_cmd_CMockIgnoreArg_fmt(__LINE__); + __cmock_zsock_inet_pton_ExpectAnyArgsAndReturn(1); + __cmock_nrf_inet_pton_ExpectAnyArgsAndReturn(1); + __cmock_nrf_bind_ExpectAndReturn(5, NULL, sizeof(struct nrf_sockaddr_in), 0); + __cmock_nrf_bind_IgnoreArg_address(); + __cmock_nrf_bind_IgnoreArg_address_len(); + send_at_command("AT#XBIND=5,8080\r\n"); + response = get_captured_response(); + TEST_ASSERT_TRUE(strstr(response, "OK") != NULL); + clear_captured_response(); + + /* Mock fcntl to set non-blocking mode and listen */ + __cmock_nrf_fcntl_ExpectAndReturn(5, NRF_F_SETFL, NRF_O_NONBLOCK, 0); + __cmock_nrf_listen_ExpectAndReturn(5, 2, 0); /* backlog=2 (fixed in modem) */ + + /* Execute listen command */ + send_at_command("AT#XLISTEN=5\r\n"); + + /* Verify successful listen response */ + response = get_captured_response(); + if (strstr(response, "OK") == NULL) { + printf("XLISTEN response: %s\n", response); + } + TEST_ASSERT_TRUE(strstr(response, "OK") != NULL); + + /* Close socket */ + __cmock_nrf_close_ExpectAndReturn(5, 0); + send_at_command("AT#XCLOSE=5\r\n"); +} + +/* + * Test: AT#XLISTEN? (READ command type) + * - Verifies READ command lists all listening sockets + */ +void test_xlisten_read_command(void) +{ + const char *response; + const char *cgpaddr_resp = "+CGPADDR: 0,\"10.0.0.1\",\"\"\r\nOK\r\n"; + + /* Create TCP server socket */ + __cmock_nrf_socket_ExpectAndReturn(NRF_AF_INET, NRF_SOCK_STREAM, NRF_IPPROTO_TCP, 3); + __cmock_nrf_setsockopt_ExpectAnyArgsAndReturn(0); + __cmock_nrf_setsockopt_ExpectAnyArgsAndReturn(0); + send_at_command("AT#XSOCKET=1,1,1\r\n"); + clear_captured_response(); + + /* Bind to port 9000 */ + __cmock_nrf_modem_at_cmd_CMockExpectAnyArgsAndReturn(__LINE__, 0); + __cmock_nrf_modem_at_cmd_CMockReturnMemThruPtr_buf(__LINE__, (void *)cgpaddr_resp, + strlen(cgpaddr_resp) + 1); + __cmock_nrf_modem_at_cmd_CMockIgnoreArg_len(__LINE__); + __cmock_nrf_modem_at_cmd_CMockIgnoreArg_fmt(__LINE__); + __cmock_zsock_inet_pton_ExpectAnyArgsAndReturn(1); + __cmock_nrf_inet_pton_ExpectAnyArgsAndReturn(1); + __cmock_nrf_bind_ExpectAndReturn(3, NULL, sizeof(struct nrf_sockaddr_in), 0); + __cmock_nrf_bind_IgnoreArg_address(); + __cmock_nrf_bind_IgnoreArg_address_len(); + send_at_command("AT#XBIND=3,9000\r\n"); + clear_captured_response(); + + /* Put socket in listening mode */ + __cmock_nrf_fcntl_ExpectAndReturn(3, NRF_F_SETFL, NRF_O_NONBLOCK, 0); + __cmock_nrf_listen_ExpectAndReturn(3, 2, 0); + send_at_command("AT#XLISTEN=3\r\n"); + clear_captured_response(); + + /* Execute read command */ + send_at_command("AT#XLISTEN?\r\n"); + + /* Verify response contains listening socket details */ + response = get_captured_response(); + /* Format: #XLISTEN: ,, */ + TEST_ASSERT_TRUE(strstr(response, "#XLISTEN: 3,0,9000") != NULL); + TEST_ASSERT_TRUE(strstr(response, "OK") != NULL); + + /* Close socket */ + __cmock_nrf_close_ExpectAndReturn(3, 0); + send_at_command("AT#XCLOSE=3\r\n"); +} + +/* + * Test: AT#XLISTEN with socket that is not bound + * - Should fail because socket must be bound before listen + */ +void test_xlisten_not_bound(void) +{ + const char *response; + + /* Create TCP server socket but don't bind it */ + __cmock_nrf_socket_ExpectAndReturn(NRF_AF_INET, NRF_SOCK_STREAM, NRF_IPPROTO_TCP, 2); + __cmock_nrf_setsockopt_ExpectAnyArgsAndReturn(0); + __cmock_nrf_setsockopt_ExpectAnyArgsAndReturn(0); + send_at_command("AT#XSOCKET=1,1,1\r\n"); + clear_captured_response(); + + /* Try to listen without binding - should fail */ + send_at_command("AT#XLISTEN=2\r\n"); + + /* Verify error response */ + response = get_captured_response(); + TEST_ASSERT_TRUE(strstr(response, "ERROR") != NULL); + + /* Close socket */ + __cmock_nrf_close_ExpectAndReturn(2, 0); + send_at_command("AT#XCLOSE=2\r\n"); +} + +/* + * Test: Socket accept operation via AT command + * - Command: AT#XACCEPT=\r\n + * - Tests: Accepting an incoming connection on a listening socket + */ +void test_xaccept_operation(void) +{ + const char *response; + const char *cgpaddr_resp = "+CGPADDR: 0,\"10.0.0.1\",\"\"\r\nOK\r\n"; + + /* Create TCP server socket */ + __cmock_nrf_socket_ExpectAndReturn(NRF_AF_INET, NRF_SOCK_STREAM, NRF_IPPROTO_TCP, 1); + __cmock_nrf_setsockopt_ExpectAnyArgsAndReturn(0); + __cmock_nrf_setsockopt_ExpectAnyArgsAndReturn(0); + send_at_command("AT#XSOCKET=1,1,1\r\n"); + clear_captured_response(); + + /* Bind to port 7000 */ + __cmock_nrf_modem_at_cmd_CMockExpectAnyArgsAndReturn(__LINE__, 0); + __cmock_nrf_modem_at_cmd_CMockReturnMemThruPtr_buf(__LINE__, (void *)cgpaddr_resp, + strlen(cgpaddr_resp) + 1); + __cmock_nrf_modem_at_cmd_CMockIgnoreArg_len(__LINE__); + __cmock_nrf_modem_at_cmd_CMockIgnoreArg_fmt(__LINE__); + __cmock_zsock_inet_pton_ExpectAnyArgsAndReturn(1); + __cmock_nrf_inet_pton_ExpectAnyArgsAndReturn(1); + __cmock_nrf_bind_ExpectAndReturn(1, NULL, sizeof(struct nrf_sockaddr_in), 0); + __cmock_nrf_bind_IgnoreArg_address(); + __cmock_nrf_bind_IgnoreArg_address_len(); + send_at_command("AT#XBIND=1,7000\r\n"); + clear_captured_response(); + + /* Put socket in listening mode */ + __cmock_nrf_fcntl_ExpectAndReturn(1, NRF_F_SETFL, NRF_O_NONBLOCK, 0); + __cmock_nrf_listen_ExpectAndReturn(1, 2, 0); + send_at_command("AT#XLISTEN=1\r\n"); + clear_captured_response(); + + /* Mock successful accept - returns new socket fd=7 with peer info */ + /* Use callback to properly populate the address structure */ + __cmock_nrf_accept_Stub(mock_nrf_accept_with_peer_callback); + __cmock_zsock_inet_ntop_Stub(mock_zsock_inet_ntop_192_168_0_100_callback); + __cmock_nrf_setsockopt_ExpectAnyArgsAndReturn(0); /* POLLCB for new socket */ + __cmock_nrf_setsockopt_ExpectAnyArgsAndReturn(0); /* POLLCB restore for listening socket */ + + /* Execute accept command */ + send_at_command("AT#XACCEPT=1\r\n"); + + /* Verify successful accept response */ + /* Format: #XACCEPT: ,,"", */ + response = get_captured_response(); + TEST_ASSERT_TRUE(strstr(response, "#XACCEPT: 7,0,\"192.168.0.100\",5555") != NULL); + TEST_ASSERT_TRUE(strstr(response, "OK") != NULL); + + /* Close sockets */ + __cmock_nrf_close_ExpectAndReturn(7, 0); + send_at_command("AT#XCLOSE=7\r\n"); + __cmock_nrf_close_ExpectAndReturn(1, 0); + send_at_command("AT#XCLOSE=1\r\n"); +} + +/* + * Test: AT#XACCEPT with invalid socket scenarios + * - Tests: + * 1. Accepting from a non-existing socket handle + * 2. Accepting from a socket that exists but is not listening + */ +void test_xaccept_not_listening(void) +{ + const char *response; + + /* Test 1: Try to accept on non-existing socket handle */ + send_at_command("AT#XACCEPT=99\r\n"); + + /* Verify error response */ + response = get_captured_response(); + TEST_ASSERT_TRUE(strstr(response, "ERROR") != NULL); + clear_captured_response(); + + /* Test 2: Create TCP server socket but don't put it in listening mode */ + __cmock_nrf_socket_ExpectAndReturn(NRF_AF_INET, NRF_SOCK_STREAM, NRF_IPPROTO_TCP, 4); + __cmock_nrf_setsockopt_ExpectAnyArgsAndReturn(0); + __cmock_nrf_setsockopt_ExpectAnyArgsAndReturn(0); + send_at_command("AT#XSOCKET=1,1,1\r\n"); /* role=1 (server) */ + clear_captured_response(); + + /* Try to accept on socket that is not listening - should fail */ + send_at_command("AT#XACCEPT=4\r\n"); + + /* Verify error response */ + response = get_captured_response(); + TEST_ASSERT_TRUE(strstr(response, "ERROR") != NULL); + + /* Close socket */ + __cmock_nrf_close_ExpectAndReturn(4, 0); + send_at_command("AT#XCLOSE=4\r\n"); +} + /* * Test: Send data via AT#XSEND with unformatted string * - Command: AT#XSEND=,,,""\r\n @@ -1566,13 +1834,6 @@ static char *mock_zsock_inet_ntop_10_0_0_1_callback( return dst; } -static char *mock_zsock_inet_ntop_192_168_0_100_callback( - net_sa_family_t af, const void *src, char *dst, net_socklen_t size, int num_calls) -{ - strcpy(dst, "192.168.0.100"); - return dst; -} - /* Helper callback for mocking nrf_recvfrom with data and address */ static ssize_t mock_nrf_recvfrom_callback(int socket, void *buffer, size_t length, int flags, struct nrf_sockaddr *address, nrf_socklen_t *address_len, @@ -2468,7 +2729,6 @@ void test_xssocket_read_operation(void) __cmock_nrf_setsockopt_ExpectAnyArgsAndReturn(0); /* SO_SNDTIMEO */ __cmock_nrf_setsockopt_ExpectAnyArgsAndReturn(0); /* SO_SEC_TAG_LIST */ __cmock_nrf_setsockopt_ExpectAnyArgsAndReturn(0); /* SO_SEC_PEER_VERIFY */ - __cmock_nrf_setsockopt_ExpectAnyArgsAndReturn(0); /* SO_SEC_ROLE */ __cmock_nrf_setsockopt_ExpectAnyArgsAndReturn(0); /* SO_POLLCB */ send_at_command("AT#XSSOCKET=2,1,1,42\r\n"); response = get_captured_response(); @@ -2639,26 +2899,11 @@ void test_xssocket_ipv4_tcp_server(void) * 1. SO_SNDTIMEO (SOL_SOCKET) * 2. SO_SEC_TAG_LIST (SOL_SECURE) * 3. SO_SEC_PEER_VERIFY (SOL_SECURE) - * 4. SO_SEC_ROLE (SOL_SECURE) - server only, verified below - * 5. SO_POLLCB (SOL_SOCKET) + * 4. SO_POLLCB (SOL_SOCKET) */ __cmock_nrf_setsockopt_ExpectAnyArgsAndReturn(0); /* SO_SNDTIMEO */ __cmock_nrf_setsockopt_ExpectAnyArgsAndReturn(0); /* SO_SEC_TAG_LIST */ __cmock_nrf_setsockopt_ExpectAnyArgsAndReturn(0); /* SO_SEC_PEER_VERIFY */ - - /* Verify SO_SEC_ROLE is set to server (value 1) */ - int expected_role = 1; /* NRF_SO_SEC_ROLE_SERVER */ - - __cmock_nrf_setsockopt_ExpectWithArrayAndReturn( - 0, /* socket fd */ - NRF_SOL_SECURE, /* level */ - NRF_SO_SEC_ROLE, /* option_name */ - &expected_role, /* option_value */ - 1, /* option_value array size */ - sizeof(int), /* option_len */ - 0 /* return value */ - ); - __cmock_nrf_setsockopt_ExpectAnyArgsAndReturn(0); /* SO_POLLCB */ /* Send AT command: family=1(IPv4), type=1(STREAM), role=1(server), sec_tag=42 */ @@ -2853,6 +3098,55 @@ void test_xsocketopt_reuseaddr(void) send_at_command("AT#XCLOSE=0\r\n"); } +/* + * Test: Set and get socket option SO_TCP_SRV_SESSTIMEO + * - Command: AT#XSOCKETOPT=,1,55, (set) + * AT#XSOCKETOPT=,0,55 (get) + * - Tests: Setting and getting SO_TCP_SRV_SESSTIMEO option (option 55) + */ +void test_xsocketopt_tcp_srv_sesstimeo(void) +{ + const char *response; + + /* Create a TCP socket */ + __cmock_nrf_socket_ExpectAndReturn(NRF_AF_INET, NRF_SOCK_STREAM, NRF_IPPROTO_TCP, 0); + __cmock_nrf_setsockopt_ExpectAnyArgsAndReturn(0); /* SO_SNDTIMEO */ + __cmock_nrf_setsockopt_ExpectAnyArgsAndReturn(0); /* Bind to PDN */ + send_at_command("AT#XSOCKET=1,1,0\r\n"); + response = get_captured_response(); + TEST_ASSERT_TRUE(strstr(response, "#XSOCKET: 0") != NULL); + TEST_ASSERT_TRUE(strstr(response, "OK") != NULL); + + /* Set SO_TCP_SRV_SESSTIMEO (option 55) to 135 */ + __cmock_nrf_setsockopt_ExpectAnyArgsAndReturn(0); + send_at_command("AT#XSOCKETOPT=0,1,55,135\r\n"); + response = get_captured_response(); + TEST_ASSERT_TRUE(strstr(response, "OK") != NULL); + + /* Get SO_TCP_SRV_SESSTIMEO (option 55) - should return 135 */ + __cmock_nrf_getsockopt_Stub(mock_getsockopt_tcp_srv_sesstimeo_callback); + send_at_command("AT#XSOCKETOPT=0,0,55\r\n"); + response = get_captured_response(); + TEST_ASSERT_TRUE(strstr(response, "#XSOCKETOPT: 0,135") != NULL); + TEST_ASSERT_TRUE(strstr(response, "OK") != NULL); + + /* Set SO_TCP_SRV_SESSTIMEO (option 55) to 0 */ + __cmock_nrf_setsockopt_ExpectAnyArgsAndReturn(0); + send_at_command("AT#XSOCKETOPT=0,1,55,0\r\n"); + response = get_captured_response(); + TEST_ASSERT_TRUE(strstr(response, "OK") != NULL); + + /* Get SO_TCP_SRV_SESSTIMEO (option 55) - should return 0 */ + send_at_command("AT#XSOCKETOPT=0,0,55\r\n"); + response = get_captured_response(); + TEST_ASSERT_TRUE(strstr(response, "#XSOCKETOPT: 0,0") != NULL); + TEST_ASSERT_TRUE(strstr(response, "OK") != NULL); + + /* Close socket */ + __cmock_nrf_close_ExpectAndReturn(0, 0); + send_at_command("AT#XCLOSE=0\r\n"); +} + /* * Test: Set and get secure socket options * - Commands: AT#XSSOCKETOPT=,1,, (set) diff --git a/doc/app/at_socket.rst b/doc/app/at_socket.rst index 746b9499..2cb456c0 100644 --- a/doc/app/at_socket.rst +++ b/doc/app/at_socket.rst @@ -481,6 +481,11 @@ Syntax * ```` is an integer that indicates the packet data network ID to bind to. + * ``55`` - ``AT_SO_TCP_SRV_SESSTIMEO``. + + * ```` is an integer that indicates the TCP server session inactivity timeout for a socket. + It accepts values from the range ``0`` to ``135``, where ``0`` is no timeout and ``135`` is 2 hours, 15 minutes. + * ``61`` - ``AT_SO_RAI`` (set-only). Release Assistance Indication (RAI). @@ -715,6 +720,7 @@ Syntax * The ```` parameter is an unsigned 16-bit integer (0 - 65535). It represents the specific port to use for binding the socket. + ``0`` is a special value that indicates that the modem should choose the port automatically. Example ~~~~~~~ @@ -1866,3 +1872,245 @@ Test command ------------ The test command is not supported. + +Socket listen #XLISTEN +======================= + +The ``#XLISTEN`` command allows you to put a TCP server socket in listening mode to accept incoming connections. + +The socket must be a stream socket (TCP) that has been bound to a non-zero local port using the ``#XBIND`` command before it can be put in listening mode. + +.. note:: + + The nRF91 modem does not support TLS server sockets, so you cannot use the ``#XLISTEN`` command with TLS sockets created with the ``#XSSOCKET`` command. + +Set command +----------- + +The set command allows you to put a socket in listening mode. + +Syntax +~~~~~~ + +:: + + AT#XLISTEN= + +* The ```` parameter is an integer that specifies the socket handle returned from ``#XSOCKET`` command. + The socket must be a stream socket (TCP) that has been bound to a non-zero local port with ``#XBIND``. + +Example +~~~~~~~ + +:: + + AT#XLISTEN=0 + OK + +Read command +------------ + +The read command allows you to list all sockets that are in listening mode. + +Syntax +~~~~~~ + +:: + + AT#XLISTEN? + +Response syntax +~~~~~~~~~~~~~~~ + +:: + + #XLISTEN: ,, + +* The ```` parameter is an integer indicating the socket handle. + +* The ```` parameter is an integer indicating the PDN connection ID. + +* The ```` parameter is an unsigned 16-bit integer (0 - 65535). + It represents the local port the socket is bound to and listening on. + +Example +~~~~~~~ + +:: + + AT#XLISTEN? + #XLISTEN: 0,0,1000 + + OK + +Test command +------------ + +The test command tests the existence of the command and provides information about the type of its subparameters. + +Syntax +~~~~~~ + +:: + + AT#XLISTEN=? + +Response syntax +~~~~~~~~~~~~~~~ + +:: + + #XLISTEN: + +Example +~~~~~~~ + +:: + + AT#XLISTEN=? + #XLISTEN: + OK + +Socket accept #XACCEPT +======================= + +The ``#XACCEPT`` command allows you to accept an incoming connection on a listening socket. + +This command is used with TCP server sockets that have been put in listening mode with the ``#XLISTEN`` command. +When a connection is accepted, a new socket is created to handle the connection, and the original listening socket continues to listen for additional connections. + +.. note:: + + The listening socket is set to non-blocking mode when the ``#XLISTEN`` command is executed. + If there are no incoming connections when the ``#XACCEPT`` command is executed, it returns an error. + You can use the ``#XAPOLL`` command to receive asynchronous notifications (``POLLIN``) for incoming connections on the listening socket. + + +Set command +----------- + +The set command allows you to accept an incoming connection on a listening socket. + +Syntax +~~~~~~ + +:: + + AT#XACCEPT= + +* The ```` parameter is an integer that specifies the socket handle that was used for the ``AT#XLISTEN`` command. + +Response syntax +~~~~~~~~~~~~~~~ + +:: + + #XACCEPT: ,,"", + +* The ```` parameter is an integer indicating the new socket handle created for the accepted connection. + You can use this new socket to send and receive data with the connected client. + +* The ```` parameter is an integer indicating the PDN connection ID. + +* The ```` parameter is a string containing the IP address of the remote peer that connected. + It supports both IPv4 and IPv6 addresses. + +* The ```` parameter is an unsigned 16-bit integer (0 - 65535). + It represents the port number of the remote peer. + +Example +~~~~~~~ + +:: + + AT#XSOCKET=2,1,0 + #XSOCKET: 0,1,6 + + OK + + // Enable asynchronous polling for all the sockets with POLLIN event. + AT#XAPOLL=,1,1 + OK + + AT#XBIND=0,1000 + OK + + AT#XLISTEN=0 + OK + + // POLLIN for listening socket is received when a client connects. + #XAPOLL: 0,1 + + // Accept the incoming connection. + AT#XACCEPT=0 + #XACCEPT: 1,0,"1111:2222:3333:4444::1",1234 + + OK + + // Once the connection is accepted, data can be received and sent with the new socket handle. + #XAPOLL: 1,1 + + AT#XRECV=1,0,0,1 + #XRECV: 1,0,17 + Hello from client + OK + + AT#XSEND=1,0,0,"Hello from server" + #XSEND: 1,0,17 + + OK + + #XAPOLL: 1,1 + + AT#XRECV=1,0,0,1 + #XRECV: 1,0,15 + Bye from client + OK + + // Client closes the connection. POLLIN event is always received with POLLHUP, if POLLIN is enabled for polling. + #XAPOLL: 1,17 + AT#XRECV=1,0,0,1 + OK + + AT#XCLOSE=1 + #XCLOSE: 1,0 + + OK + + AT#XCLOSE + #XCLOSE: 0,0 + + OK + +Read command +------------ + +The read command is not supported. + +Test command +------------ + +The test command tests the existence of the command and provides information about the type of its subparameters. + +Syntax +~~~~~~ + +:: + + AT#XACCEPT=? + +Response syntax +~~~~~~~~~~~~~~~ + +:: + + #XACCEPT: + +Example +~~~~~~~ + +:: + + AT#XACCEPT=? + #XACCEPT: + OK diff --git a/doc/migration_notes.rst b/doc/migration_notes.rst index e73edd60..894ae10c 100644 --- a/doc/migration_notes.rst +++ b/doc/migration_notes.rst @@ -175,15 +175,32 @@ The following is the list of changes: * ``AT#XBIND=,`` (handle parameter added) * ``AT#XCONNECT=,,`` (handle parameter added) - * Response format changes: +* ``AT#XLISTEN`` parameter changes: - * ``AT#XSOCKETOPT`` - Response to get options now includes socket handle: ``#XSOCKETOPT: ,`` (previously just ``#XSOCKETOPT: ``) - * ``AT#XSSOCKETOPT`` - Response to get options now includes socket handle: ``#XSSOCKETOPT: ,`` (previously just ``#XSSOCKETOPT: ``) - * ``AT#XCONNECT`` - Response now includes socket handle: ``#XCONNECT: ,`` (previously just ``#XCONNECT: ``) - * ``AT#XSEND`` - Response now includes socket handle: ``#XSEND: ,`` (previously just ``#XSEND: ``) - * ``AT#XRECV`` - Response now includes socket handle and mode: ``#XRECV: ,,`` (previously just ``#XRECV: ``) - * ``AT#XSENDTO`` - Response now includes socket handle: ``#XSENDTO: ,`` (previously just ``#XSENDTO: ``) - * ``AT#XRECVFROM`` - Response now includes socket handle and mode: ``#XRECVFROM: ,,,"",`` (previously just ``#XRECVFROM: ,"",``) + * Added ```` parameter to the ``AT#XLISTEN`` command. + + * Old syntax: ``AT#XLISTEN`` + * New syntax: ``AT#XLISTEN=`` + +* ``AT#XACCEPT`` parameter changes: + + * Removed ```` parameter and added ```` parameter to the ``AT#XACCEPT`` command. + The command is now non-blocking and returns immediately with an error if there is no incoming connection to accept. + + * Old syntax: ``AT#XACCEPT=`` + * New syntax: ``AT#XACCEPT=`` + + * Response format now includes CID and port: ``#XACCEPT: ,,"",`` (previously ``#XACCEPT: ,""``) + +* Response format changes: + + * ``AT#XSOCKETOPT`` - Response to get options now includes socket handle: ``#XSOCKETOPT: ,`` (previously just ``#XSOCKETOPT: ``) + * ``AT#XSSOCKETOPT`` - Response to get options now includes socket handle: ``#XSSOCKETOPT: ,`` (previously just ``#XSSOCKETOPT: ``) + * ``AT#XCONNECT`` - Response now includes socket handle: ``#XCONNECT: ,`` (previously just ``#XCONNECT: ``) + * ``AT#XSEND`` - Response now includes socket handle: ``#XSEND: ,`` (previously just ``#XSEND: ``) + * ``AT#XRECV`` - Response now includes socket handle and mode: ``#XRECV: ,,`` (previously just ``#XRECV: ``) + * ``AT#XSENDTO`` - Response now includes socket handle: ``#XSENDTO: ,`` (previously just ``#XSENDTO: ``) + * ``AT#XRECVFROM`` - Response now includes socket handle and mode: ``#XRECVFROM: ,,,"",`` (previously just ``#XRECVFROM: ,"",``) Migration example: @@ -392,16 +409,183 @@ If you need any of those features with this |SM|, please contact customer suppor You can set the parameters such as ```` and ```` using the ``AT#XSSOCKETOPT`` command. * TCP and UDP servers. - This includes the removal of the following AT commands: + The following AT commands have been removed: * ``AT#XTCPSVR`` * ``AT#XTCPHANGUP`` * ``AT#XUDPSVR`` - * ``AT#XLISTEN`` - * ``AT#XACCEPT`` - There is no direct replacement for these commands. - In addition, the ``AT_SO_TCP_SRV_SESSTIMEO`` socket option has been removed. + The ``AT#XLISTEN`` and ``AT#XACCEPT`` commands have been reintroduced and you can use them to implement TCP server functionality using the socket AT commands. + However, there is no support for TLS or DTLS servers, as the nRF91 modem does not support TLS server sockets. + + You can replace this functionality by using the socket AT commands. + + Migration examples: + + * TCP IPv4 server + + |NCS| SLM approach: + + .. code-block:: + + AT#XTCPSVR=1,1000 + + #XTCPSVR: 0,"started" + + OK + + #XTCPSVR: "192.0.2.1","connected" + + #XTCPDATA: 9 + echo this + + AT#XTCPSEND="echo this" + + #XTCPSEND: 9 + + OK + + AT#XTCPSVR? + + #XTCPSVR: 0,2,1 + + OK + + AT#XTCPHANGUP=2 + + #XTCPSVR: 0,"disconnected" + + OK + + AT#XTCPSVR=0 + + #XTCPSVR: 0,"stopped" + + OK + + + |SM| approach: + + .. code-block:: + + AT#XSOCKET=1,1,0 + + #XSOCKET: 0,1,6 + + OK + + AT#XAPOLL=,1,1 + + OK + + AT#XBIND=0,1000 + + OK + + AT#XLISTEN=0 + + OK + + #XAPOLL: 0,1 + + AT#XACCEPT=0 + + #XACCEPT: 1,0,"192.0.2.1",54321 + + OK + + #XAPOLL: 1,1 + + AT#XRECV=1,0,0,1 + + #XRECV: 1,0,9 + echo this + + OK + + AT#XSEND=1,0,0,"echo this" + + #XSEND: 1,0,9 + + OK + + AT#XCLOSE=1 + + #XCLOSE: 1,0 + + OK + + AT#XCLOSE=0 + + #XCLOSE: 0,0 + + OK + + * UDP IPv6 server + + |NCS| SLM approach: + + .. code-block:: + + AT#XUDPSVR=2,1235 + + #XUDPSVR: 0,"started" + + OK + + #XUDPDATA: 9,"2001:db8::1",54321 + echo this + + AT#XUDPSEND="echo this" + + #XUDPSEND: 9 + + OK + + AT#XUDPSVR=0 + + #XUDPSVR: 0,"stopped" + + OK + + |SM| approach: + + .. code-block:: + + AT#XSOCKET=2,2,0 + + #XSOCKET: 0,2,17 + + OK + + AT#XAPOLL=0,1,1 + + OK + + AT#XBIND=0,1235 + + OK + + #XAPOLL: 0,1 + + AT#XRECVFROM=0,0,0,1 + + #XRECVFROM: 0,0,9,"2001:db8::1",54321 + echo this + + OK + + AT#XSENDTO=0,0,0,"2001:db8::1",54321,"echo this" + + #XSENDTO: 0,9 + + OK + + AT#XCLOSE=0 + + #XCLOSE: 0,0 + + OK * HTTP client functionality, including ``AT#XHTTPCCON`` and ``AT#XHTTPCREQ`` commands, and ``#XHTTPCRSP`` notification. * FTP and TFTP clients, including ``AT#XFTP`` and ``AT#XTFTP`` commands.