Skip to content

Commit 15edb02

Browse files
LucasCholletnico
authored andcommitted
LibSSH+SSHServer: Support basic channels and sessions
This allows a client to execute commands on the server. This is very minimal and don't support interactive sessions yet. It is however enough to run simple commands like `uname` over the network :^)
1 parent bb5fe8d commit 15edb02

File tree

6 files changed

+224
-4
lines changed

6 files changed

+224
-4
lines changed

Userland/Libraries/LibSSH/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ set(SOURCES
44
IdentificationString.cpp
55
KeyExchangeData.cpp
66
Peer.cpp
7+
Session.cpp
78
)
89

910
serenity_lib(LibSSH ssh)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright (c) 2026, Lucas Chollet <lucas.chollet@serenityos.org>
3+
*
4+
* SPDX-License-Identifier: BSD-2-Clause
5+
*/
6+
7+
#include "Session.h"
8+
9+
namespace SSH {
10+
11+
ErrorOr<Session> Session::create(u32 sender_channel_id, u32 window_size, u32 maximum_packet_size)
12+
{
13+
Session session;
14+
session.local_channel_id = sender_channel_id;
15+
session.sender_channel_id = sender_channel_id;
16+
session.maximum_packet_size = maximum_packet_size;
17+
session.window = TRY(ByteBuffer::create_zeroed(window_size));
18+
return session;
19+
}
20+
21+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright (c) 2026, Lucas Chollet <lucas.chollet@serenityos.org>
3+
*
4+
* SPDX-License-Identifier: BSD-2-Clause
5+
*/
6+
7+
#pragma once
8+
9+
#include <AK/ByteBuffer.h>
10+
#include <AK/Error.h>
11+
12+
namespace SSH {
13+
14+
// 6.1. Opening a Session
15+
// https://datatracker.ietf.org/doc/html/rfc4254#section-6.1
16+
struct Session {
17+
static ErrorOr<Session> create(u32 sender_channel_id, u32 window_size, u32 maximum_packet_size);
18+
19+
u32 local_channel_id {};
20+
u32 sender_channel_id {};
21+
u32 maximum_packet_size {};
22+
ByteBuffer window {};
23+
};
24+
25+
}

Userland/Services/SSHServer/SSHClient.cpp

Lines changed: 161 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <AK/MemoryStream.h>
1313
#include <AK/Random.h>
1414
#include <LibCore/Account.h>
15+
#include <LibCore/Command.h>
1516
#include <LibCore/Socket.h>
1617
#include <LibCrypto/Curves/Ed25519.h>
1718
#include <LibCrypto/Curves/X25519.h>
@@ -23,7 +24,6 @@ namespace SSH::Server {
2324

2425
ErrorOr<void> SSHClient::handle_data(ByteBuffer& data)
2526
{
26-
2727
switch (m_state) {
2828
case State::Constructed:
2929
return handle_protocol_version(data);
@@ -40,7 +40,8 @@ ErrorOr<void> SSHClient::handle_data(ByteBuffer& data)
4040
case State::WaitingForUserAuthentication:
4141
return handle_user_authentication(TRY(unpack_generic_message(data)));
4242
case State::Authentified:
43-
return Error::from_string_literal("Draw the rest of the owl");
43+
TRY(handle_generic_packet(TRY(unpack_generic_message(data))));
44+
return {};
4445
}
4546
VERIFY_NOT_REACHED();
4647
}
@@ -324,4 +325,162 @@ ErrorOr<void> SSHClient::send_user_authentication_success()
324325
return {};
325326
}
326327

328+
ErrorOr<void> SSHClient::handle_generic_packet(GenericMessage&& message)
329+
{
330+
switch (message.type) {
331+
case MessageID::CHANNEL_OPEN:
332+
return handle_channel_open_message(message);
333+
case MessageID::CHANNEL_REQUEST:
334+
return handle_channel_request(message);
335+
default:
336+
dbgln_if(SSH_DEBUG, "Unexpected packet: {}", to_underlying(message.type));
337+
return Error::from_string_literal("Unexpected packet type");
338+
}
339+
VERIFY_NOT_REACHED();
340+
}
341+
342+
// 5.1. Opening a Channel
343+
// https://datatracker.ietf.org/doc/html/rfc4254#section-5.1
344+
ErrorOr<void> SSHClient::handle_channel_open_message(GenericMessage& message)
345+
{
346+
auto channel_type = TRY(decode_string(message.payload));
347+
u32 sender_channel_id = TRY(message.payload.read_value<NetworkOrdered<u32>>());
348+
u32 initial_window_size = TRY(message.payload.read_value<NetworkOrdered<u32>>());
349+
u32 maximum_packet_size = TRY(message.payload.read_value<NetworkOrdered<u32>>());
350+
351+
dbgln_if(SSH_DEBUG, "Channel open request with: {:s} - {} - {} - {}",
352+
channel_type.bytes(), sender_channel_id, initial_window_size, maximum_packet_size);
353+
354+
if (channel_type != "session"sv.bytes())
355+
return Error::from_string_literal("Unexpected channel type");
356+
357+
m_sessions.empend(TRY(Session::create(sender_channel_id, initial_window_size, maximum_packet_size)));
358+
359+
TRY(send_channel_open_confirmation(m_sessions.last()));
360+
361+
return {};
362+
}
363+
364+
// 5.1. Opening a Channel
365+
// https://datatracker.ietf.org/doc/html/rfc4254#section-5.1
366+
ErrorOr<void> SSHClient::send_channel_open_confirmation(Session const& session)
367+
{
368+
AllocatingMemoryStream stream;
369+
370+
// byte SSH_MSG_CHANNEL_OPEN_CONFIRMATION
371+
// uint32 recipient channel
372+
// uint32 sender channel
373+
// uint32 initial window size
374+
// uint32 maximum packet size
375+
376+
TRY(stream.write_value(MessageID::CHANNEL_OPEN_CONFIRMATION));
377+
// "The 'recipient channel' is the channel number given in the original
378+
// open request, and 'sender channel' is the channel number allocated by
379+
// the other side."
380+
TRY(stream.write_value<NetworkOrdered<u32>>(session.sender_channel_id));
381+
TRY(stream.write_value<NetworkOrdered<u32>>(session.local_channel_id));
382+
383+
TRY(stream.write_value<NetworkOrdered<u32>>(session.window.size()));
384+
TRY(stream.write_value<NetworkOrdered<u32>>(session.maximum_packet_size));
385+
386+
TRY(write_packet(TRY(stream.read_until_eof())));
387+
return {};
388+
}
389+
390+
ErrorOr<Session*> SSHClient::find_session(u32 sender_channel_id)
391+
{
392+
for (auto& session : m_sessions) {
393+
if (session.sender_channel_id == sender_channel_id)
394+
return &session;
395+
}
396+
return Error::from_string_literal("Session not found");
397+
}
398+
399+
// 5.4. Channel-Specific Requests
400+
// https://datatracker.ietf.org/doc/html/rfc4254#section-5.4
401+
ErrorOr<void> SSHClient::handle_channel_request(GenericMessage& message)
402+
{
403+
auto recipient_channel_id = TRY(message.payload.read_value<NetworkOrdered<u32>>());
404+
auto request_type = TRY(decode_string(message.payload));
405+
auto want_reply = TRY(message.payload.read_value<bool>());
406+
407+
auto& session = *TRY(find_session(recipient_channel_id));
408+
409+
dbgln_if(SSH_DEBUG, "CHANNEL_REQUEST id({}): {:s}", session.local_channel_id, request_type.bytes());
410+
411+
if (request_type == "env"sv.bytes() && !want_reply) {
412+
dbgln("FIXME: Ignored channel request: {:s}", request_type.bytes());
413+
return {};
414+
}
415+
416+
if (request_type == "exec"sv.bytes()) {
417+
auto command = TRY(decode_string(message.payload));
418+
419+
// FIXME: This is a naive implementation, we should stream the result back
420+
// to the user and not block the event loop during the execution of
421+
// the command.
422+
// We should also use the user's shell rather than hardcoding it.
423+
424+
#ifdef AK_OS_SERENITY
425+
auto shell = "/bin/Shell"sv;
426+
#else
427+
auto shell = "/bin/sh"sv;
428+
#endif
429+
430+
Vector<ByteString> args;
431+
args.append(shell);
432+
args.append("-c");
433+
args.append(ByteString(command.bytes()));
434+
435+
Vector<char const*> raw_args;
436+
raw_args.ensure_capacity(args.size() + 1);
437+
for (auto& arg : args)
438+
raw_args.append(arg.characters());
439+
440+
raw_args.append(nullptr);
441+
442+
auto child = TRY(Core::Command::create(shell, raw_args.data()));
443+
auto output = TRY(child->read_all());
444+
auto status = TRY(child->status());
445+
446+
if (status != Core::Command::ProcessResult::DoneWithZeroExitCode)
447+
return Error::from_string_literal("Unable to run command");
448+
449+
TRY(send_channel_success_message(session));
450+
TRY(send_channel_data(session, output.standard_output));
451+
TRY(send_channel_close(session));
452+
return {};
453+
}
454+
455+
return Error::from_string_literal("Unsupported channel request");
456+
}
457+
458+
ErrorOr<void> SSHClient::send_channel_success_message(Session const& session)
459+
{
460+
AllocatingMemoryStream stream;
461+
TRY(stream.write_value(MessageID::CHANNEL_SUCCESS));
462+
TRY(stream.write_value<NetworkOrdered<u32>>(session.local_channel_id));
463+
TRY(write_packet(TRY(stream.read_until_eof())));
464+
return {};
465+
}
466+
467+
ErrorOr<void> SSHClient::send_channel_data(Session const& session, ByteBuffer const& data)
468+
{
469+
AllocatingMemoryStream stream;
470+
TRY(stream.write_value(MessageID::CHANNEL_DATA));
471+
TRY(stream.write_value<NetworkOrdered<u32>>(session.local_channel_id));
472+
TRY(encode_string(stream, data));
473+
TRY(write_packet(TRY(stream.read_until_eof())));
474+
return {};
475+
}
476+
477+
ErrorOr<void> SSHClient::send_channel_close(Session const& session)
478+
{
479+
AllocatingMemoryStream stream;
480+
TRY(stream.write_value(MessageID::CHANNEL_CLOSE));
481+
TRY(stream.write_value<NetworkOrdered<u32>>(session.local_channel_id));
482+
TRY(write_packet(TRY(stream.read_until_eof())));
483+
return {};
484+
}
485+
327486
} // SSHServer

Userland/Services/SSHServer/SSHClient.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <LibCore/Forward.h>
1111
#include <LibSSH/KeyExchangeData.h>
1212
#include <LibSSH/Peer.h>
13+
#include <LibSSH/Session.h>
1314

1415
namespace SSH::Server {
1516

@@ -75,10 +76,22 @@ class SSHClient : public Peer {
7576
ErrorOr<void> handle_user_authentication(GenericMessage data);
7677
ErrorOr<void> send_user_authentication_success();
7778

79+
ErrorOr<void> handle_generic_packet(GenericMessage&&);
80+
81+
ErrorOr<void> handle_channel_open_message(GenericMessage&);
82+
ErrorOr<void> send_channel_open_confirmation(Session const&);
83+
ErrorOr<void> handle_channel_request(GenericMessage&);
84+
ErrorOr<void> send_channel_success_message(Session const&);
85+
ErrorOr<void> send_channel_data(Session const&, ByteBuffer const&);
86+
ErrorOr<void> send_channel_close(Session const&);
87+
ErrorOr<Session*> find_session(u32 sender_channel_id);
88+
7889
State m_state { State::Constructed };
7990
Core::TCPSocket& m_tcp_socket;
8091

8192
KeyExchangeData m_key_exchange_data {};
93+
94+
Vector<Session> m_sessions;
8295
};
8396

8497
} // SSHServer

Userland/Services/SSHServer/main.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ static constexpr auto DEFAULT_PORT = 22;
1818

1919
ErrorOr<int> serenity_main(Main::Arguments args)
2020
{
21-
TRY(Core::System::pledge("stdio accept inet unix rpath"));
21+
TRY(Core::System::pledge("stdio accept inet unix rpath proc exec"));
2222
TRY(Core::System::unveil("/etc/passwd", "r"));
23+
TRY(Core::System::unveil("/bin/Shell", "rx"));
2324
TRY(Core::System::unveil(nullptr, nullptr));
2425

2526
Optional<u32> port {};
@@ -55,6 +56,6 @@ ErrorOr<int> serenity_main(Main::Arguments args)
5556

5657
outln("Listening on {}:{}", tcp_server->local_address().value(), tcp_server->local_port());
5758

58-
TRY(Core::System::pledge("stdio accept rpath"));
59+
TRY(Core::System::pledge("stdio accept rpath proc exec"));
5960
return loop.exec();
6061
}

0 commit comments

Comments
 (0)