Skip to content

Commit e2f6e66

Browse files
committed
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 ec2f4c4 commit e2f6e66

File tree

6 files changed

+204
-4
lines changed

6 files changed

+204
-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: 141 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
}
@@ -326,4 +327,142 @@ ErrorOr<void> SSHClient::send_user_authentication_success()
326327
return {};
327328
}
328329

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

@@ -57,10 +58,22 @@ class SSHClient : public Peer {
5758
ErrorOr<void> handle_user_authentication(GenericMessage data);
5859
ErrorOr<void> send_user_authentication_success();
5960

61+
ErrorOr<void> handle_generic_packet(GenericMessage&&);
62+
63+
ErrorOr<void> handle_channel_open_message(GenericMessage&);
64+
ErrorOr<void> send_channel_open_confirmation(Session const&);
65+
ErrorOr<void> handle_channel_request(GenericMessage&);
66+
ErrorOr<void> send_channel_success_message(Session const&);
67+
ErrorOr<void> send_channel_data(Session const&, ByteBuffer const&);
68+
ErrorOr<void> send_channel_close(Session const&);
69+
ErrorOr<Session*> find_session(u32 sender_channel_id);
70+
6071
State m_state { State::Constructed };
6172
Core::TCPSocket& m_tcp_socket;
6273

6374
KeyExchangeData m_key_exchange_data {};
75+
76+
Vector<Session> m_sessions;
6477
};
6578

6679
} // 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)