blob: 8de8c250c713e85fe48c1e34de43ca5a05bd6372 [file]
/*
* Copyright (c) 2024 Grant Ramsay <grant.ramsay@hotmail.com>
* Copyright (c) 2026 Nordic Semiconductor
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/kernel.h>
#include <zephyr/sys/util.h>
#include <zephyr/zvfs/eventfd.h>
#include <zephyr/logging/log.h>
LOG_MODULE_DECLARE(ssh, CONFIG_SSH_LOG_LEVEL);
#include <zephyr/net/ssh/server.h>
#include "ssh_connection.h"
#include "ssh_transport.h"
static void ssh_server_thread_entry(void *p1, void *p2, void *p3);
static int ssh_server_thread_run(struct ssh_server *ssh_server);
static struct ssh_server ssh_server_instances[CONFIG_SSH_SERVER_MAX_SERVERS];
void ssh_server_foreach(ssh_service_server_cb_t cb, void *user_data)
{
ARRAY_FOR_EACH(ssh_server_instances, i) {
struct ssh_server *sshd = &ssh_server_instances[i];
if (sshd->running) {
cb(sshd, i, user_data);
}
}
}
struct ssh_server *ssh_server_instance(int instance)
{
if (instance < ARRAY_SIZE(ssh_server_instances)) {
return &ssh_server_instances[instance];
}
return NULL;
}
int ssh_server_start(struct ssh_server *sshd,
const struct net_sockaddr *bind_addr,
int host_key_index,
const char *username,
const char *password,
const int *authorized_keys,
size_t authorized_keys_len,
ssh_server_event_callback_t server_callback,
ssh_transport_event_callback_t transport_callback,
void *user_data)
{
if (password == NULL) {
password = "";
}
if (sshd == NULL || bind_addr == NULL || host_key_index < 0 ||
server_callback == NULL || transport_callback == NULL ||
strlen(password) > sizeof(sshd->password) - 1 ||
authorized_keys_len > ARRAY_SIZE(sshd->authorized_keys)) {
return -EINVAL;
}
if (sshd->running) {
return -EALREADY;
}
*sshd = (struct ssh_server) {
.host_key_index = host_key_index,
.server_callback = server_callback,
.transport_callback = transport_callback,
.callback_user_data = user_data,
.authorized_keys_len = authorized_keys_len,
};
memcpy(&sshd->bind_addr, bind_addr,
bind_addr->sa_family == NET_AF_INET6 ?
sizeof(struct net_sockaddr_in6) : sizeof(struct net_sockaddr_in));
memset(sshd->username, 0, sizeof(sshd->username));
if (username != NULL) {
strncpy(sshd->username, username, sizeof(sshd->username) - 1);
}
strcpy(sshd->password, password);
if (authorized_keys != NULL) {
memcpy(sshd->authorized_keys, authorized_keys,
authorized_keys_len * sizeof(authorized_keys[0]));
}
k_thread_create(&sshd->thread, sshd->thread_stack,
K_KERNEL_STACK_SIZEOF(sshd->thread_stack),
ssh_server_thread_entry, sshd, NULL, NULL,
5, 0, K_FOREVER);
k_thread_name_set(&sshd->thread, "ssh_server");
k_thread_start(&sshd->thread);
return 0;
}
static void ssh_server_thread_entry(void *p1, void *p2, void *p3)
{
ARG_UNUSED(p2);
ARG_UNUSED(p3);
struct ssh_server *sshd = p1;
struct ssh_server_event event;
int ret;
ret = ssh_server_thread_run(sshd);
event.type = SSH_SERVER_EVENT_CLOSED;
sshd->server_callback(sshd, &event, sshd->callback_user_data);
}
static int ssh_server_thread_run(struct ssh_server *sshd)
{
const struct net_sockaddr *bind_addr = net_sad(&sshd->bind_addr);
struct zsock_pollfd fds[2 + CONFIG_SSH_SERVER_MAX_CLIENTS];
net_socklen_t addr_len = bind_addr->sa_family == NET_AF_INET ?
sizeof(struct net_sockaddr_in) : sizeof(struct net_sockaddr_in6);
int ret;
/* Wake up every 10 seconds to check timeouts */
const int timeout = 10000;
NET_DBG("Starting SSH Server");
sshd->eventfd = zvfs_eventfd(0, 0);
if (sshd->eventfd < 0) {
NET_ERR("Failed to create eventfd: %d", errno);
return -1;
}
sshd->sock = zsock_socket(bind_addr->sa_family,
NET_SOCK_STREAM, NET_IPPROTO_TCP);
if (sshd->sock < 0) {
NET_ERR("Failed to create TCP socket: %d", errno);
zsock_close(sshd->eventfd);
return -1;
}
/* If IPv4-to-IPv6 mapping is enabled, then turn off V6ONLY option
* so that IPv6 socket can serve IPv4 connections.
*/
if (IS_ENABLED(CONFIG_NET_IPV4_MAPPING_TO_IPV6) &&
bind_addr->sa_family == NET_AF_INET6) {
int optval = 0;
(void)zsock_setsockopt(sshd->sock, NET_IPPROTO_IPV6, ZSOCK_IPV6_V6ONLY,
&optval, sizeof(optval));
}
ret = zsock_bind(sshd->sock, bind_addr, addr_len);
if (ret < 0) {
NET_ERR("Failed to bind TCP socket: %d", errno);
zsock_close(sshd->sock);
zsock_close(sshd->eventfd);
return -1;
}
ret = zsock_listen(sshd->sock, 1);
if (ret < 0) {
NET_ERR("Failed to listen on TCP socket: %d", errno);
zsock_close(sshd->sock);
zsock_close(sshd->eventfd);
return -1;
}
fds[0] = (struct zsock_pollfd){
.fd = sshd->sock, .events = ZSOCK_POLLIN,
};
fds[1] = (struct zsock_pollfd){
.fd = sshd->eventfd, .events = ZSOCK_POLLIN,
};
BUILD_ASSERT(CONFIG_ZVFS_POLL_MAX >= ARRAY_SIZE(fds));
for (int i = 2; i < ARRAY_SIZE(fds); i++) {
fds[i].fd = -1;
}
sshd->running = true;
NET_DBG("Waiting for connection...");
while (true) {
void *addr_ptr;
ret = zsock_poll(fds, ARRAY_SIZE(fds), timeout);
if (ret < 0) {
NET_ERR("Poll error (%d)", errno);
ret = -errno;
break;
}
if (fds[0].revents & ZSOCK_POLLIN) {
struct net_sockaddr_storage client_addr;
net_socklen_t client_addr_len = sizeof(client_addr);
struct ssh_transport *transport = NULL;
int client_sock;
char str[NET_INET6_ADDRSTRLEN];
char *addr_str;
client_sock = zsock_accept(sshd->sock,
net_sad(&client_addr),
&client_addr_len);
if (client_sock < 0) {
NET_ERR("Accept error (%d)", errno);
ret = -1;
break;
}
for (int i = 2; i < ARRAY_SIZE(fds); i++) {
if (fds[i].fd == -1) {
fds[i] = (struct zsock_pollfd){
.fd = client_sock,
.events = ZSOCK_POLLIN
};
transport = &sshd->transport[i-2];
break;
}
}
addr_ptr = client_addr.ss_family == NET_AF_INET ?
(void *)&net_sin(net_sad(&client_addr))->sin_addr :
(void *)&net_sin6(net_sad(&client_addr))->sin6_addr;
addr_str = zsock_inet_ntop(client_addr.ss_family, addr_ptr,
str, sizeof(str));
if (addr_str == NULL) {
NET_DBG("Failed to convert %s address to string", "client");
strncpy(str, "<invalid>", sizeof(str));
}
if (transport != NULL) {
ret = ssh_transport_start(
transport, true, sshd,
client_sock, net_sad(&client_addr),
sshd->host_key_index,
sshd->transport_callback,
sshd->callback_user_data);
if (ret == 0) {
struct ssh_server_event event;
NET_DBG("Connection %s %s%s%s:%d", "from",
client_addr.ss_family == NET_AF_INET6 ? "[" : "",
str,
client_addr.ss_family == NET_AF_INET6 ? "]" : "",
net_ntohs(net_sin(net_sad(&client_addr))->sin_port));
event.type = SSH_SERVER_EVENT_CLIENT_CONNECTED;
event.client_connected.transport = transport;
sshd->server_callback(sshd, &event,
sshd->callback_user_data);
} else {
NET_DBG("Failed to init connection %s%s%s:%d",
client_addr.ss_family == NET_AF_INET6 ? "[" : "",
str,
client_addr.ss_family == NET_AF_INET6 ? "]" : "",
net_ntohs(net_sin(net_sad(&client_addr))->sin_port));
ssh_transport_close(transport);
zsock_close(transport->sock);
}
} else {
NET_DBG("Too many connections, refusing %s%s%s:%d",
client_addr.ss_family == NET_AF_INET6 ? "[" : "",
str,
client_addr.ss_family == NET_AF_INET6 ? "]" : "",
net_ntohs(net_sin(net_sad(&client_addr))->sin_port));
zsock_close(client_sock);
}
}
if (fds[1].revents) {
zvfs_eventfd_t value;
zvfs_eventfd_read(sshd->eventfd, &value);
if (sshd->stopping) {
/* Requested stop */
ret = 0;
break;
}
}
for (int i = 2; i < ARRAY_SIZE(fds); i++) {
if (fds[i].fd >= 0 && fds[i].revents) {
struct ssh_transport *transport = &sshd->transport[i-2];
ret = ssh_transport_input(transport);
if (ret < 0) {
struct ssh_server_event event;
event.type = SSH_SERVER_EVENT_CLIENT_DISCONNECTED;
event.client_disconnected.transport = transport;
sshd->server_callback(sshd, &event,
sshd->callback_user_data);
zsock_close(transport->sock);
ssh_transport_close(transport);
fds[i].fd = -1;
}
}
}
/* Update the transport */
for (int i = 0; i < ARRAY_SIZE(sshd->transport); i++) {
struct ssh_transport *transport;
transport = &sshd->transport[i];
if (!transport->running) {
continue;
}
ret = ssh_transport_update(transport);
if (ret != 0) {
struct ssh_server_event event;
event.type = SSH_SERVER_EVENT_CLIENT_DISCONNECTED;
event.client_disconnected.transport = transport;
sshd->server_callback(sshd, &event,
sshd->callback_user_data);
zsock_close(transport->sock);
ssh_transport_close(transport);
fds[i+2].fd = -1;
}
}
}
for (int i = 0; i < ARRAY_SIZE(sshd->transport); i++) {
struct ssh_transport *transport;
struct ssh_server_event event;
transport = &sshd->transport[i];
if (!transport->running) {
continue;
}
event.type = SSH_SERVER_EVENT_CLIENT_DISCONNECTED;
event.client_disconnected.transport = transport;
sshd->server_callback(sshd, &event, sshd->callback_user_data);
zsock_close(transport->sock);
ssh_transport_close(transport);
}
zsock_close(sshd->sock);
zsock_close(sshd->eventfd);
sshd->running = false;
return ret;
}
int ssh_server_stop(struct ssh_server *sshd)
{
zvfs_eventfd_t value = 1;
int ret;
if (!sshd->running) {
return -EALREADY;
}
sshd->stopping = true;
/* Wake up the thread */
ret = zvfs_eventfd_write(sshd->eventfd, value);
if (ret == 0) {
(void)k_thread_join(&sshd->thread, K_FOREVER);
}
return ret;
}
int ssh_server_transport_close(struct ssh_server *sshd, int idx)
{
zvfs_eventfd_t value = 1;
struct ssh_server_event event;
if (sshd == NULL) {
return -EINVAL;
}
if (!PART_OF_ARRAY(ssh_server_instances, sshd)) {
return -ENOENT;
}
if (idx < 0 || idx >= CONFIG_SSH_SERVER_MAX_CLIENTS) {
return -EINVAL;
}
if (!sshd->transport[idx].running) {
return -EALREADY;
}
event.type = SSH_SERVER_EVENT_CLIENT_DISCONNECTED;
event.client_disconnected.transport = &sshd->transport[idx];
sshd->server_callback(sshd, &event, sshd->callback_user_data);
zsock_close(sshd->transport[idx].sock);
ssh_transport_close(&sshd->transport[idx]);
/* Wake up the thread */
(void)zvfs_eventfd_write(sshd->eventfd, value);
return 0;
}
int ssh_server_register_transport_callback(struct ssh_transport_conf *conf)
{
return ssh_transport_register_callback(conf, true);
}
int ssh_server_unregister_transport_callback(struct ssh_transport_conf *conf)
{
return ssh_transport_unregister_callback(conf, true);
}
struct ssh_server *ssh_transport_get_server(struct ssh_transport *transport)
{
if (transport == NULL) {
return NULL;
}
return transport->sshd;
}