genode/repos/gems/src/server/ssh_terminal/server.cc

645 lines
16 KiB
C++

/*
* \brief Component providing a Terminal session via SSH
* \author Josef Soentgen
* \author Pirmin Duss
* \author Sid Hussmann
* \date 2019-05-29
*/
/*
* Copyright (C) 2018 Genode Labs GmbH
* Copyright (C) 2019 gapfruit AG
*
* This file is part of the Genode OS framework, which is distributed
* under the terms of the GNU Affero General Public License version 3.
*/
/* local includes */
#include "server.h"
/*
* Add the libssh callback forward declarations here so that we can use them
* from within the classes and thereby document the ones currently implemented.
*/
extern int channel_data_cb(ssh_session, ssh_channel, void *, uint32_t, int, void *);
extern int channel_env_request_cb(ssh_session, ssh_channel, char const *, char const *, void *);
extern int channel_pty_request_cb(ssh_session, ssh_channel, char const *, int, int, int, int, void *);
extern int channel_pty_window_change_cb(ssh_session, ssh_channel, int, int, int, int, void *);
extern int channel_shell_request_cb(ssh_session, ssh_channel, void *);
extern int channel_exec_request_cb(ssh_session, ssh_channel, char const *, void *);
extern void bind_incoming_connection(ssh_bind, void *);
extern int session_service_request_cb(ssh_session, char const *, void *);
extern int session_auth_password_cb(ssh_session, char const *, char const *, void *);
extern int session_auth_pubkey_cb(ssh_session, char const *, struct ssh_key_struct *, char, void *);
extern ssh_channel session_channel_open_request_cb(ssh_session, void *);
/**
* forward declaration of the write available callback.
*/
static int write_avail_cb(socket_t fd, int revents, void *userdata);
Ssh::Terminal_session::Terminal_session(Genode::Registry<Terminal_session> &reg,
Ssh::Terminal &conn,
ssh_event event_loop)
:
Element(reg, *this), conn(conn), _event_loop(event_loop)
{
if (pipe(_fds) ||
ssh_event_add_fd(_event_loop,
_fds[0],
POLLIN,
write_avail_cb,
this) != SSH_OK ) {
Genode::error("Failed to create wakeup pipe");
throw -1;
}
conn.write_avail_fd = _fds[1];
}
Ssh::Server::Server(Genode::Env &env,
Genode::Xml_node const &config,
Ssh::Login_registry &logins)
:
_env(env), _heap(env.ram(), env.rm()), _logins(logins)
{
Libc::with_libc([&] {
_parse_config(config);
if (ssh_init() < 0) {
Genode::error("ssh_init failed.");
throw Init_failed();
}
_ssh_bind = ssh_bind_new();
if (!_ssh_bind) {
Genode::error("ssh_bind failed.");
throw Init_failed();
}
ssh_bind_options_set(_ssh_bind, SSH_BIND_OPTIONS_LOG_VERBOSITY, &_log_level);
ssh_bind_options_set(_ssh_bind, SSH_BIND_OPTIONS_BINDPORT, &_port);
_initialize_bind_callbacks();
_initialize_session_callbacks();
_initialize_channel_callbacks();
/*
* Always try to load all types of host key and error-out if
* the file is set but could not be loaded.
*/
try {
_load_hostkey(_rsa_key);
_load_hostkey(_ecdsa_key);
_load_hostkey(_ed25519_key);
} catch (...) {
Genode::error("loading keys failed.");
throw Init_failed();
}
_event_loop = ssh_event_new();
if (ssh_bind_listen(_ssh_bind) < 0) {
Genode::error("could not listen on port ", _port, ": ",
ssh_get_error(_ssh_bind));
throw Init_failed();
}
/* add AFTER(!) ssh_bind_listen call */
if (ssh_event_add_bind(_event_loop, _ssh_bind) < 0) {
Genode::error("unable to add server to event loop: ",
ssh_get_error(_ssh_bind));
throw Init_failed();
}
if (pthread_create(&_event_thread, nullptr, _server_loop, this)) {
Genode::error("could not create event thread");
throw Init_failed();
}
/* add pipe to wake up loop on late connecting terminal */
if (pipe(_server_fds) ||
ssh_event_add_fd(_event_loop,
_server_fds[0],
POLLIN,
write_avail_cb,
this) != SSH_OK ) {
Genode::error("Failed to create wakeup pipe");
throw -1;
}
Genode::log("Listen on port: ", _port);
}); /* Libc::with_libc */
}
Ssh::Server::~Server()
{
close(_server_fds[0]);
close(_server_fds[1]);
}
void Ssh::Server::_initialize_channel_callbacks()
{
Genode::memset(&_channel_cb, 0, sizeof(_channel_cb));
_channel_cb.userdata = this;
_channel_cb.channel_data_function = channel_data_cb;
_channel_cb.channel_env_request_function = channel_env_request_cb;
_channel_cb.channel_pty_request_function = channel_pty_request_cb;
_channel_cb.channel_pty_window_change_function = channel_pty_window_change_cb;
_channel_cb.channel_shell_request_function = channel_shell_request_cb;
_channel_cb.channel_exec_request_function = channel_exec_request_cb;
ssh_callbacks_init(&_channel_cb);
}
void Ssh::Server::_initialize_session_callbacks()
{
Genode::memset(&_session_cb, 0, sizeof(_session_cb));
_session_cb.userdata = this;
_session_cb.auth_password_function = session_auth_password_cb;
_session_cb.auth_pubkey_function = session_auth_pubkey_cb;
_session_cb.service_request_function = session_service_request_cb;
_session_cb.channel_open_request_session_function = session_channel_open_request_cb;
ssh_callbacks_init(&_session_cb);
}
void Ssh::Server::_initialize_bind_callbacks()
{
Genode::memset(&_bind_cb, 0, sizeof(_bind_cb));
_bind_cb.incoming_connection = bind_incoming_connection;
ssh_callbacks_init(&_bind_cb);
ssh_bind_set_callbacks(_ssh_bind, &_bind_cb, this);
}
void Ssh::Server::_cleanup_session(Session &s)
{
if (s.auth_sucessful) {
_log_logout(s);
}
ssh_channel_free(s.channel);
s.channel = nullptr;
ssh_event_remove_session(_event_loop, s.session);
ssh_disconnect(s.session);
ssh_free(s.session);
s.session = nullptr;
if (s.terminal) {
s.terminal->detach_channel();
}
try {
_request_terminal_reporter.generate([&] (Xml_generator& xml) {
xml.attribute("user", s.user());
xml.attribute("exit", "now");
});
} catch (...) {
Genode::warning("could not enable exit reporting");
}
Genode::destroy(&_heap, &s);
}
void Ssh::Server::_cleanup_sessions()
{
auto cleanup = [&] (Session &s) {
if (!ssh_is_connected(s.session)) {
_cleanup_session(s);
}
};
_sessions.for_each(cleanup);
}
void Ssh::Server::_parse_config(Genode::Xml_node const &config)
{
using Util::Filename;
_verbose = config.attribute_value("verbose", false);
_log_level = config.attribute_value("debug", 0u);
_log_logins = config.attribute_value("log_logins", true);
Genode::Lock::Guard g(_logins.lock());
auto print = [&] (Login const &login) {
Genode::log("Login configured: ", login);
};
_logins.for_each(print);
if (_config_once) { return; }
_config_once = true;
_port = config.attribute_value("port", 0u);
if (!_port) {
error("port invalid");
throw Invalid_config();
}
_allow_password = config.attribute_value("allow_password", false);
_allow_publickey = config.attribute_value("allow_publickey", false);
if (!_allow_password && !_allow_publickey) {
error("authentication methods missing");
throw Invalid_config();
}
_rsa_key = config.attribute_value("rsa_key", Filename());
_ecdsa_key = config.attribute_value("ecdsa_key", Filename());
_ed25519_key = config.attribute_value("ed25519_key", Filename());
Genode::log("Allowed auth methods: ",
_allow_password ? "password " : "",
_allow_publickey ? "public-key" : "");
}
void Ssh::Server::_load_hostkey(Util::Filename const &file)
{
if (file.valid() &&
ssh_bind_options_set(_ssh_bind, SSH_BIND_OPTIONS_HOSTKEY,
file.string()) < 0) {
Genode::error("could not load hostkey '", file, "'");
throw -1;
}
}
void *Ssh::Server::_server_loop(void *arg)
{
Ssh::Server *server = reinterpret_cast<Ssh::Server *>(arg);
server->loop();
return nullptr;
}
bool Ssh::Server::_allow_multi_login(ssh_session s, Login const &login)
{
if (login.multi_login) { return true; }
bool found = false;
auto lookup = [&] (Session const &s) {
if (s.user() == login.user) { found = true; }
};
_sessions.for_each(lookup);
return !found;
}
void Ssh::Server::_log_failed(char const *user, Session const &s, bool pubkey)
{
if (!_log_logins) { return; }
char const *date = Util::get_time();
Genode::log(date, " failed user ", user, " (", s.id(), ") ",
"with ", pubkey ? "public-key" : "password");
}
void Ssh::Server::_log_logout(Session const &s)
{
if (!_log_logins) { return; }
char const *date = Util::get_time();
Genode::log(date, " logout user ", s.user(), " (", s.id(), ")");
}
void Ssh::Server::_log_login(User const &user, Session const &s, bool pubkey)
{
if (!_log_logins) { return; }
char const *date = Util::get_time();
Genode::log(date, " login user ", user, " (", s.id(), ") ",
"with ", pubkey ? "public-key" : "password");
}
void Ssh::Server::attach_terminal(Ssh::Terminal &conn)
{
Genode::Lock::Guard g(_terminals.lock());
try {
new (&_heap) Terminal_session(_terminals,
conn, _event_loop);
} catch (...) {
Genode::error("could not attach Terminal for user ",
conn.user());
throw -1;
}
/* there might be sessions already waiting on the terminal */
bool attached = false;
auto lookup = [&] (Session &s) {
if (s.user() == conn.user() && !s.terminal) {
s.terminal = &conn;
s.terminal->attach_channel();
attached = true;
}
};
_sessions.for_each(lookup);
_wake_loop();
}
void Ssh::Server::detach_terminal(Ssh::Terminal &conn)
{
Genode::Lock::Guard g(_terminals.lock());
Terminal_session *p = nullptr;
auto lookup = [&] (Terminal_session &t) {
if (&t.conn == &conn) {
p = &t;
}
};
_terminals.for_each(lookup);
if (!p) {
Genode::error("could not detach Terminal for user ", conn.user());
return;
}
auto invalidate_terminal = [&] (Session &sess) {
Libc::with_libc([&] () {
ssh_blocking_flush(sess.session, 10000);
ssh_disconnect(sess.session);
if (sess.terminal != &conn) { return; }
sess.terminal = nullptr;
});
};
_sessions.for_each(invalidate_terminal);
_cleanup_sessions();
Genode::destroy(&_heap, p);
}
void Ssh::Server::update_config(Genode::Xml_node const &config)
{
Genode::Lock::Guard g(_terminals.lock());
_parse_config(config);
ssh_bind_options_set(_ssh_bind, SSH_BIND_OPTIONS_LOG_VERBOSITY, &_log_level);
}
Ssh::Terminal *Ssh::Server::lookup_terminal(Session &s)
{
Ssh::Terminal *p = nullptr;
auto lookup = [&] (Terminal_session &t) {
if (t.conn.user() == s.user()) { p = &t.conn; }
};
_terminals.for_each(lookup);
return p;
}
Ssh::Session *Ssh::Server::lookup_session(ssh_session s)
{
Session *p = nullptr;
auto lookup = [&] (Session &sess) {
if (sess.session == s) { p = &sess; }
};
_sessions.for_each(lookup);
return p;
}
bool Ssh::Server::request_terminal(Session &session,
const char* command)
{
Genode::Lock::Guard g(_logins.lock());
Login const *l = _logins.lookup(session.user().string());
if (!l || !l->request_terminal) {
return false;
}
try {
_request_terminal_reporter.generate([&] (Xml_generator& xml) {
xml.attribute("user", session.user());
if (command) {
xml.attribute("command", command);
}
});
} catch (...) {
Genode::warning("could not enable login reporting");
return false;
}
if (_log_logins) {
char const *date = Util::get_time();
Genode::log(date, " request Terminal for user ", session.user(),
" (", session.session, ")");
}
return true;
}
void Ssh::Server::incoming_connection(ssh_session s)
{
/*
* In case we get bombarded by incoming connections, deny
* all attempts when this arbritray level is reached.
*/
enum { MEM_RESERVE = 128u * 1024, };
if (_env.pd().avail_ram().value < (size_t)MEM_RESERVE) {
error("Too many connections");
throw -1;
}
new (&_heap) Session(_sessions, s, &_channel_cb, ++_session_id);
ssh_set_server_callbacks(s, &_session_cb);
int auth_methods = SSH_AUTH_METHOD_UNKNOWN;
auth_methods += _allow_password ? SSH_AUTH_METHOD_PASSWORD : 0;
auth_methods += _allow_publickey ? SSH_AUTH_METHOD_PUBLICKEY : 0;
ssh_set_auth_methods(s, auth_methods);
/*
* Normally we would check the result of the key exchange
* function but for better or worse using callbacks leads to
* a false negative. So ignore any result and move on in hope
* that the callsbacks will handle the situation.
*
* FIXME investigate why it somtimes fails in the first place.
*/
int key_exchange_result = ssh_handle_key_exchange(s);
if (SSH_OK != key_exchange_result) {
Genode::warning("key exchange returned ", key_exchange_result);
}
ssh_event_add_session(_event_loop, s);
}
bool Ssh::Server::auth_password(ssh_session s, char const *u, char const *pass)
{
Session *p = lookup_session(s);
if (!p || p->session != s) {
Genode::warning("session not found");
return false;
}
Session &session = *p;
/*
* Even if there is no valid login for the user, let
* the client try anyway and check multi login afterwards.
*/
Genode::Lock::Guard g(_logins.lock());
Login const *l = _logins.lookup(u);
if (l && l->user == u && l->password == pass) {
if (_allow_multi_login(s, *l)) {
session.bad_auth_attempts = 0;
session.auth_sucessful = true;
session.adopt(l->user);
_log_login(l->user, session, false);
return true;
} else {
ssh_disconnect(session.session);
_log_failed(u, session, false);
return false;
}
}
_log_failed(u, *p, false);
int &i = session.bad_auth_attempts;
if (++i >= _max_auth_attempts) {
if (_log_logins) {
char const *date = Util::get_time();
Genode::log(date, " disconnect user ", u, " (", session.id(),
") after ", i, " failed authentication attempts"
" with password");
}
ssh_disconnect(session.session);
}
return false;
}
bool Ssh::Server::auth_pubkey(ssh_session s, char const *u,
struct ssh_key_struct *pubkey,
char signature_state)
{
Session *p = lookup_session(s);
if (!p || p->session != s) {
Genode::warning("session not found");
return false;
}
Session &session = *p;
/*
* In this first state the given pubkey is solely probed.
* Ideally we would check here if the given pubkey is in fact to the
* configured one, i.e., reading a 'authorized_keys' like file and
* check its entries.
*
* For now we simple accept all keys and reject them in the later
* state.
*/
if (signature_state == SSH_PUBLICKEY_STATE_NONE) {
return true;
}
/*
* In this second state we check the provided pubkey and if it
* matches allow authentication to proceed.
*/
if (signature_state == SSH_PUBLICKEY_STATE_VALID) {
Genode::Lock::Guard g(_logins.lock());
Login const *l = _logins.lookup(u);
if (l && !ssh_key_cmp(pubkey, l->pub_key,
SSH_KEY_CMP_PUBLIC)) {
if (_allow_multi_login(s, *l)) {
session.auth_sucessful = true;
session.adopt(l->user);
_log_login(l->user, session, true);
return true;
}
}
}
_log_failed(u, session, true);
return false;
}
void Ssh::Server::loop()
{
while (true) {
int const events = ssh_event_dopoll(_event_loop, -1);
if (events == SSH_ERROR) {
_cleanup_sessions();
}
{
Genode::Lock::Guard g(_terminals.lock());
/* first remove all stale sessions */
auto cleanup = [&] (Session &s) {
if (ssh_is_connected(s.session)) { return ; }
_cleanup_session(s);
};
_sessions.for_each(cleanup);
/* second reset all active terminals */
auto reset_pending = [&] (Terminal_session &t) {
if (!t.conn.attached_channels()) { return; }
t.conn.reset_pending();
};
_terminals.for_each(reset_pending);
/*
* third send data on all sessions being attached
* to a terminal.
*/
auto send = [&] (Session &s) {
if (!s.terminal) { return; }
try { s.terminal->send(s.channel); }
catch (...) { _cleanup_session(s); }
};
_sessions.for_each(send);
}
}
}
void Ssh::Server::_wake_loop()
{
/* wake the event loop up */
Libc::with_libc([&] {
char c = 1;
::write(_server_fds[1], &c, sizeof(c));
});
}
static int write_avail_cb(socket_t fd, int revents, void *userdata)
{
int n = 0;
Libc::with_libc([&] {
char c;
n = ::read(fd, &c, sizeof(char));
});
return n;
}