blob: 2b011f1097c7d7b629eaad9a41cb1dfbafb2f451 [file]
/*
* Copyright (c) 2024 Grant Ramsay <grant.ramsay@hotmail.com>
* Copyright (c) 2026 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(net_ssh_sample, LOG_LEVEL_INF);
#include <zephyr/kernel.h>
#include <zephyr/shell/shell.h>
#include <zephyr/net/conn_mgr_monitor.h>
#include <zephyr/net/ssh/common.h>
#include <zephyr/net/ssh/server.h>
#include <zephyr/net/ssh/client.h>
#include <zephyr/net/net_config.h>
#if defined(CONFIG_NET_SAMPLE_SSH_USE_PASSWORD)
#define SSH_PASSWORD CONFIG_NET_SAMPLE_SSH_PASSWORD
#else
#define SSH_PASSWORD NULL
#endif
#define KEY_SIZE_BITS 2048
NET_CONFIG_SSH_KEY_BUF_DEFINE_STATIC(my_key_buf, KEY_SIZE_BITS);
/* We store the our private key to slot 0 and public key to slot 1 */
NET_CONFIG_SSH_PRIV_KEY_DEFINE_STATIC(my_priv_key_config,
my_key_buf, sizeof(my_key_buf),
"id_rsa", 0,
SSH_HOST_KEY_TYPE_RSA, KEY_SIZE_BITS);
NET_CONFIG_SSH_PUB_KEY_DEFINE_STATIC(my_pub_key_config,
my_key_buf, sizeof(my_key_buf),
"authorized_key_0", 1);
NET_CONFIG_SSH_PASSWORD_AUTH_DEFINE_STATIC(my_password_auth,
CONFIG_NET_SAMPLE_SSH_USERNAME,
SSH_PASSWORD);
static bool connected;
static struct net_mgmt_event_callback mgmt_cb;
/* Protects the transport and channel bookkeeping below. It is updated from the
* SSH server/client threads (the event callbacks) and read from the shell
* thread (the sample commands).
*/
static K_MUTEX_DEFINE(sample_lock);
#if defined(CONFIG_SSH_SERVER) || defined(CONFIG_SSH_CLIENT)
/* Store a transport into the first free slot of an array. Returns the slot
* index, or -1 if the array is full. If the transport is already stored its
* existing index is returned.
*/
static int transport_track(struct ssh_transport **arr, size_t count,
struct ssh_transport *transport)
{
int free_slot = -1;
for (size_t i = 0; i < count; i++) {
if (arr[i] == transport) {
return (int)i;
}
if (arr[i] == NULL && free_slot < 0) {
free_slot = (int)i;
}
}
if (free_slot >= 0) {
arr[free_slot] = transport;
}
return free_slot;
}
static void transport_untrack(struct ssh_transport **arr, size_t count,
struct ssh_transport *transport)
{
for (size_t i = 0; i < count; i++) {
if (arr[i] == transport) {
arr[i] = NULL;
}
}
}
#endif /* CONFIG_SSH_SERVER || CONFIG_SSH_CLIENT */
#if defined(CONFIG_SSH_SERVER)
/* Transports of the clients connected to our server. */
static struct ssh_transport *server_transport[CONFIG_SSH_SERVER_MAX_CLIENTS];
static int server_transport_event_handler(struct ssh_transport *transport,
const struct ssh_transport_event *event,
void *user_data);
/* The server transport configuration. This must not be allocated from the
* stack as it is used by the SSH server until it is unregistered.
*/
static struct ssh_transport_conf server_transport_conf = {
.cb = server_transport_event_handler,
};
static int server_transport_event_handler(struct ssh_transport *transport,
const struct ssh_transport_event *event,
void *user_data)
{
ARG_UNUSED(user_data);
switch (event->type) {
case SSH_TRANSPORT_EVENT_CHANNEL_OPEN:
k_mutex_lock(&sample_lock, K_FOREVER);
if (transport_track(server_transport, ARRAY_SIZE(server_transport),
transport) < 0) {
LOG_WRN("No free server transport slots");
}
k_mutex_unlock(&sample_lock);
break;
case SSH_TRANSPORT_EVENT_CLOSED:
k_mutex_lock(&sample_lock, K_FOREVER);
transport_untrack(server_transport, ARRAY_SIZE(server_transport),
transport);
k_mutex_unlock(&sample_lock);
break;
default:
break;
}
return 0;
}
#endif /* CONFIG_SSH_SERVER */
#if defined(CONFIG_SSH_CLIENT)
/* Transports of our client connections. The connection itself is managed by
* the SSH shell ("net ssh start"/"net ssh stop"); here we just record the
* authenticated transports so that a channel can be opened on them.
*/
static struct ssh_transport *client_transport[CONFIG_SSH_CLIENT_MAX_CLIENTS];
/* Channels we have opened on our client connections. */
static struct ssh_channel *client_channels[CONFIG_SSH_MAX_CHANNELS];
static int client_transport_event_handler(struct ssh_transport *transport,
const struct ssh_transport_event *event,
void *user_data);
/* The client transport configuration. This must not be allocated from the
* stack as it is used by the SSH client until it is unregistered.
*/
static struct ssh_transport_conf client_transport_conf = {
.cb = client_transport_event_handler,
};
static int client_transport_event_handler(struct ssh_transport *transport,
const struct ssh_transport_event *event,
void *user_data)
{
ARG_UNUSED(user_data);
switch (event->type) {
case SSH_TRANSPORT_EVENT_AUTHENTICATE_RESULT:
if (!event->authenticate_result.success) {
break;
}
/* The connection is authenticated, so remember the transport
* to be able to open a channel on it later.
*/
k_mutex_lock(&sample_lock, K_FOREVER);
if (transport_track(client_transport, ARRAY_SIZE(client_transport),
transport) < 0) {
LOG_WRN("No free client transport slots");
}
k_mutex_unlock(&sample_lock);
break;
case SSH_TRANSPORT_EVENT_CLOSED:
k_mutex_lock(&sample_lock, K_FOREVER);
transport_untrack(client_transport, ARRAY_SIZE(client_transport),
transport);
k_mutex_unlock(&sample_lock);
break;
default:
break;
}
return 0;
}
static int channel_track(struct ssh_channel *channel)
{
int free_slot = -1;
for (size_t i = 0; i < ARRAY_SIZE(client_channels); i++) {
if (client_channels[i] == channel) {
return (int)i;
}
if (client_channels[i] == NULL && free_slot < 0) {
free_slot = (int)i;
}
}
if (free_slot >= 0) {
client_channels[free_slot] = channel;
}
return free_slot;
}
static void channel_untrack(struct ssh_channel *channel)
{
for (size_t i = 0; i < ARRAY_SIZE(client_channels); i++) {
if (client_channels[i] == channel) {
client_channels[i] = NULL;
}
}
}
/* Channel event callback. The channel is created with "sample open" and
* removed with "sample close".
*/
static int channel_event_handler(struct ssh_channel *channel,
const struct ssh_channel_event *event,
void *user_data)
{
ARG_UNUSED(user_data);
switch (event->type) {
case SSH_CHANNEL_EVENT_OPEN_RESULT: {
int i;
k_mutex_lock(&sample_lock, K_FOREVER);
i = channel_track(channel);
k_mutex_unlock(&sample_lock);
if (i < 0) {
LOG_WRN("No free channel slots");
return 0;
}
LOG_INF("Channel %d opened", i);
break;
}
case SSH_CHANNEL_EVENT_RX_DATA_READY: {
uint8_t buf[64];
int len;
while ((len = ssh_channel_read(channel, buf, sizeof(buf))) > 0) {
LOG_INF("Channel RX: %.*s", len, buf);
}
break;
}
case SSH_CHANNEL_EVENT_CLOSED:
k_mutex_lock(&sample_lock, K_FOREVER);
channel_untrack(channel);
k_mutex_unlock(&sample_lock);
LOG_INF("Channel closed");
break;
default:
break;
}
return 0;
}
static int cmd_sample_open(const struct shell *sh, size_t argc, char *argv[])
{
struct ssh_transport *transport = NULL;
int ret;
ARG_UNUSED(argc);
ARG_UNUSED(argv);
/* Use the first authenticated client connection. Establish one first
* with the SSH shell, e.g. "net ssh start <user>@<host>".
*/
k_mutex_lock(&sample_lock, K_FOREVER);
for (size_t i = 0; i < ARRAY_SIZE(client_transport); i++) {
if (client_transport[i] != NULL) {
transport = client_transport[i];
break;
}
}
k_mutex_unlock(&sample_lock);
if (transport == NULL) {
shell_error(sh, "No client connection, use \"net ssh start\" first");
return -ENOTCONN;
}
ret = ssh_transport_channel_open(transport, channel_event_handler, NULL);
if (ret < 0) {
shell_error(sh, "Failed to open SSH channel: %d", ret);
return ret;
}
shell_print(sh, "Channel open requested");
return 0;
}
static int cmd_sample_close(const struct shell *sh, size_t argc, char *argv[])
{
struct ssh_channel *channel;
int ch;
int err = 0;
ARG_UNUSED(argc);
ch = shell_strtol(argv[1], 10, &err);
if (err != 0 || ch < 0 || ch >= (int)ARRAY_SIZE(client_channels)) {
shell_error(sh, "Invalid channel number: %s", argv[1]);
return -EINVAL;
}
k_mutex_lock(&sample_lock, K_FOREVER);
channel = client_channels[ch];
k_mutex_unlock(&sample_lock);
if (channel == NULL) {
shell_error(sh, "Channel %d not open", ch);
return -ENOENT;
}
err = ssh_channel_close(channel);
if (err < 0) {
shell_error(sh, "Failed to close channel %d: %d", ch, err);
return err;
}
shell_print(sh, "Channel %d close requested", ch);
return 0;
}
#endif /* CONFIG_SSH_CLIENT */
static int cmd_sample_info(const struct shell *sh, size_t argc, char *argv[])
{
ARG_UNUSED(argc);
ARG_UNUSED(argv);
shell_print(sh, "Network %sconnected", connected ? "" : "dis");
k_mutex_lock(&sample_lock, K_FOREVER);
#if defined(CONFIG_SSH_SERVER)
for (size_t i = 0; i < ARRAY_SIZE(server_transport); i++) {
if (server_transport[i] != NULL) {
shell_print(sh, "Server transport %zu: %p", i,
(void *)server_transport[i]);
}
}
#endif
#if defined(CONFIG_SSH_CLIENT)
for (size_t i = 0; i < ARRAY_SIZE(client_transport); i++) {
if (client_transport[i] != NULL) {
shell_print(sh, "Client transport %zu: %p", i,
(void *)client_transport[i]);
}
}
for (size_t i = 0; i < ARRAY_SIZE(client_channels); i++) {
if (client_channels[i] != NULL) {
shell_print(sh, "Channel %zu: %p", i,
(void *)client_channels[i]);
}
}
#endif
k_mutex_unlock(&sample_lock);
return 0;
}
SHELL_STATIC_SUBCMD_SET_CREATE(sample_commands,
#if defined(CONFIG_SSH_CLIENT)
SHELL_CMD(open, NULL,
SHELL_HELP("Create a channel on the client connection", ""),
cmd_sample_open),
SHELL_CMD_ARG(close, NULL,
SHELL_HELP("Remove a channel", "<channel>"),
cmd_sample_close, 2, 0),
#endif /* CONFIG_SSH_CLIENT */
SHELL_CMD(info, NULL,
SHELL_HELP("List connections and channels", ""),
cmd_sample_info),
SHELL_SUBCMD_SET_END
);
SHELL_CMD_REGISTER(sample, &sample_commands,
"Sample application commands", NULL);
#define EVENT_MASK (NET_EVENT_L4_CONNECTED | NET_EVENT_L4_DISCONNECTED)
static void event_handler(struct net_mgmt_event_callback *cb,
uint64_t mgmt_event, struct net_if *iface)
{
ARG_UNUSED(cb);
ARG_UNUSED(iface);
if ((mgmt_event & EVENT_MASK) != mgmt_event) {
return;
}
if (mgmt_event == NET_EVENT_L4_CONNECTED) {
LOG_INF("Network connected");
connected = true;
return;
}
if (mgmt_event == NET_EVENT_L4_DISCONNECTED) {
LOG_INF("Network disconnected");
connected = false;
return;
}
}
int main(void)
{
int ret;
LOG_INF("SSH sample for %s", CONFIG_BOARD_TARGET);
#if defined(CONFIG_SSH_SERVER)
ret = ssh_server_register_transport_callback(&server_transport_conf);
if (ret < 0) {
LOG_WRN("Failed to register SSH server transport callback: %d", ret);
}
#endif
#if defined(CONFIG_SSH_CLIENT)
ret = ssh_client_register_transport_callback(&client_transport_conf);
if (ret < 0) {
LOG_WRN("Failed to register SSH client transport callback: %d", ret);
}
#endif
if (IS_ENABLED(CONFIG_NET_CONNECTION_MANAGER)) {
net_mgmt_init_event_callback(&mgmt_cb,
event_handler, EVENT_MASK);
net_mgmt_add_event_callback(&mgmt_cb);
conn_mgr_mon_resend_status();
}
#if defined(CONFIG_SSH_SERVER)
ret = net_config_init_sshd(NULL, &my_priv_key_config,
&my_pub_key_config, &my_password_auth, 0);
if (ret != 0) {
LOG_ERR("Failed to initialize SSH server");
return 0;
}
#endif /* CONFIG_SSH_SERVER */
k_sleep(K_FOREVER);
}