feat: add ssh server (sshd)

This commit is contained in:
2026-08-11 12:53:36 +02:00
parent 69725658cf
commit 0a91131cb0
15 changed files with 2365 additions and 4 deletions
+29
View File
@@ -0,0 +1,29 @@
MAKEFLAGS += -rR
.SUFFIXES:
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-montauk-
CXX := $(TOOLCHAIN_PREFIX)g++
PROG_INC := ../../include
LINK_LD := ../../link.ld
BINDIR := ../../bin
OBJDIR := obj
LIBDIR := ../../lib
CXXFLAGS := -std=gnu++20 -g -O2 -pipe -Wall -Wextra -Wno-unused-parameter \
-ffreestanding -fno-stack-protector -fno-stack-check -fno-rtti -fno-exceptions \
-ffunction-sections -fdata-sections -msse -msse2 -MMD -MP -I $(PROG_INC) \
-I $(LIBDIR)/bearssl/inc -isystem $(PROG_INC)/libc
LDFLAGS := -nostdlib -Wl,--gc-sections -T $(LINK_LD)
SRCS := main.cpp stb_truetype_impl.cpp
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
TARGET := $(BINDIR)/apps/sshserver/sshserver.elf
LIBS := $(LIBDIR)/bearssl/libbearssl.a $(LIBDIR)/libc/liblibc.a
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LIBS)
mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
$(OBJDIR)/%.o: %.cpp Makefile
mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) -c $< -o $@
-include $(OBJS:.o=.d)
clean:
rm -rf $(OBJDIR) $(TARGET)
+328
View File
@@ -0,0 +1,328 @@
/*
* main.cpp
* MontaukOS SSH Server settings applet
*/
#include <gui/mtk.hpp>
#include <gui/mtk/settings.hpp>
#include <gui/standalone.hpp>
#include <montauk/config.h>
#include <montauk/ssh.h>
#include <montauk/syscall.h>
extern "C" {
#include <stdio.h>
}
using namespace gui;
static constexpr int WIN_W = 620;
static constexpr int WIN_H = 500;
static constexpr int PAD = 20;
static constexpr int FOOTER_H = 48;
static constexpr int USER_ROW_H = 38;
static constexpr int VISIBLE_USERS = 7;
static WsWindow g_win;
static Color g_accent = colors::ACCENT;
static int g_mouse_x = -1;
static int g_mouse_y = -1;
static bool g_is_admin = false;
static bool g_enabled = false;
static bool g_saved_enabled = false;
static bool g_dirty = false;
static montauk::user::UserInfo g_users[montauk::user::MAX_USERS];
static bool g_allowed[montauk::user::MAX_USERS];
static bool g_saved_allowed[montauk::user::MAX_USERS];
static int g_user_count = 0;
static int g_user_scroll = 0;
static char g_status[128] = {};
static Rect service_row() {
return {PAD, 76, WIN_W - PAD * 2, 32};
}
static Rect user_row(int visibleIndex) {
return {PAD, 164 + visibleIndex * USER_ROW_H,
WIN_W - PAD * 2, USER_ROW_H};
}
static Rect apply_button() {
return {WIN_W - PAD - 92, WIN_H - FOOTER_H + 8, 92, 31};
}
static Rect revert_button() {
Rect apply = apply_button();
return {apply.x - 104, apply.y, 92, apply.h};
}
static mtk::Theme app_theme() {
return mtk::make_theme(g_accent);
}
static void load_state() {
char currentUser[32] = {};
montauk::getuser(currentUser, sizeof(currentUser));
auto init = montauk::config::load("init");
g_enabled = init.get_bool("services.ssh.enabled", false);
init.destroy();
auto ssh = montauk::config::load("ssh");
g_user_count = montauk::user::load_users(
g_users, montauk::user::MAX_USERS);
for (int i = 0; i < g_user_count; i++) {
g_allowed[i] = montauk::ssh::user_allowed(ssh, g_users[i].username);
}
ssh.destroy();
/* Reuse the table just loaded rather than parsing the user file twice. */
g_is_admin = false;
for (int i = 0; i < g_user_count; i++) {
if (montauk::streq(g_users[i].username, currentUser)) {
g_is_admin = montauk::streq(g_users[i].role, "admin");
break;
}
}
g_status[0] = 0;
g_saved_enabled = g_enabled;
for (int i = 0; i < g_user_count; i++) {
g_saved_allowed[i] = g_allowed[i];
}
g_dirty = false;
}
static void recompute_dirty() {
g_dirty = g_enabled != g_saved_enabled;
for (int i = 0; i < g_user_count; i++) {
if (g_allowed[i] != g_saved_allowed[i]) g_dirty = true;
}
}
static bool save_state() {
if (!g_is_admin) {
snprintf(g_status, sizeof(g_status),
"Administrator access is required");
return false;
}
auto init = montauk::config::load("init");
/*
* init discovers services by enumerating [services.<id>] tables and needs
* a path to launch. On a system whose init.toml predates the SSH service,
* writing only the enabled flag would produce a key init cannot act on, so
* write the whole service definition and let set_* overwrite in place where
* it already exists.
*/
if (!init.get_string("services.ssh.path", nullptr)) {
montauk::config::set_string(&init, "services.ssh.path",
"0:/os/sshd.elf");
montauk::config::set_string(&init, "services.ssh.name", "SSH server");
montauk::config::set_bool(&init, "services.ssh.wait", false);
montauk::config::set_bool(&init, "services.ssh.optional", true);
}
montauk::config::set_bool(&init, "services.ssh.enabled", g_enabled);
int initResult = montauk::config::save("init", &init);
init.destroy();
auto ssh = montauk::config::load("ssh");
for (int i = 0; i < g_user_count; i++) {
char key[64];
montauk::ssh::allow_key(key, sizeof(key), g_users[i].username);
montauk::config::set_bool(&ssh, key, g_allowed[i]);
}
int sshResult = montauk::config::save("ssh", &ssh);
ssh.destroy();
if (initResult < 0 || sshResult < 0) {
snprintf(g_status, sizeof(g_status),
"Could not save SSH configuration");
return false;
}
g_saved_enabled = g_enabled;
for (int i = 0; i < g_user_count; i++) {
g_saved_allowed[i] = g_allowed[i];
}
g_dirty = false;
snprintf(g_status, sizeof(g_status),
g_enabled ? "Saved; SSH starts on the next boot"
: "Saved; SSH is disabled on the next boot");
return true;
}
static void render() {
mtk::StandaloneHost host(&g_win);
Canvas canvas = host.canvas();
mtk::Theme theme = app_theme();
canvas.fill(theme.window_bg);
canvas.text(PAD, 20, "SSH Server", theme.text);
canvas.text(PAD, 44,
"Secure remote access to the MontaukOS shell",
theme.text_subtle);
Rect service = service_row();
mtk::draw_checkbox(canvas, service,
"Start the SSH server at boot",
mtk::check_state(g_enabled), theme,
g_is_admin,
service.contains(g_mouse_x, g_mouse_y));
canvas.text(PAD + 24, 112,
"Listens on TCP port 22 and uses MontaukOS account passwords.",
theme.text_subtle);
canvas.text(PAD, 142, "USERS ALLOWED TO CONNECT", theme.text_muted);
for (int row = 0; row < VISIBLE_USERS; row++) {
int userIndex = g_user_scroll + row;
if (userIndex >= g_user_count) break;
Rect bounds = user_row(row);
if (userIndex & 1) {
canvas.fill_rect(bounds.x, bounds.y, bounds.w, bounds.h,
mtk::mix(theme.window_bg, theme.surface, 55));
}
char label[112];
const char* displayName = g_users[userIndex].display_name[0]
? g_users[userIndex].display_name
: g_users[userIndex].username;
snprintf(label, sizeof(label), "%s (%s)",
displayName, g_users[userIndex].username);
mtk::draw_checkbox(canvas, bounds, label,
mtk::check_state(g_allowed[userIndex]),
theme, g_is_admin,
bounds.contains(g_mouse_x, g_mouse_y));
if (montauk::streq(g_users[userIndex].role, "admin")) {
const char* role = "Administrator";
canvas.text(bounds.x + bounds.w - text_width(role) - 10,
bounds.y + 10, role, theme.text_muted);
}
}
/* Without an indicator there is nothing to show the list continues. */
if (g_user_count > VISIBLE_USERS) {
Rect track = {WIN_W - PAD + 4, user_row(0).y, 4,
VISIBLE_USERS * USER_ROW_H};
canvas.fill_rect(track.x, track.y, track.w, track.h,
mtk::mix(theme.window_bg, theme.surface, 120));
int thumbHeight = track.h * VISIBLE_USERS / g_user_count;
if (thumbHeight < 20) thumbHeight = 20;
int span = g_user_count - VISIBLE_USERS;
int thumbY = track.y +
(track.h - thumbHeight) * g_user_scroll / (span ? span : 1);
canvas.fill_rect(track.x, thumbY, track.w, thumbHeight,
theme.text_muted);
}
if (!g_is_admin) {
canvas.text(PAD, WIN_H - FOOTER_H - 27,
"Only administrators can change system SSH settings.",
theme.danger);
}
Rect footer = {0, WIN_H - FOOTER_H, WIN_W, FOOTER_H};
canvas.fill_rect(footer.x, footer.y, footer.w, footer.h, theme.surface);
mtk::draw_separator(canvas, 0, footer.y, WIN_W, theme);
const char* footerStatus = g_status[0]
? g_status
: (g_dirty ? "Unsaved SSH changes" : "SSH configuration ready");
canvas.text(PAD, footer.y + 16, footerStatus, theme.text_subtle);
Rect revert = revert_button();
Rect apply = apply_button();
mtk::draw_button(
canvas, revert, "Revert", mtk::BUTTON_SECONDARY,
mtk::widget_state(false, revert.contains(g_mouse_x, g_mouse_y),
g_dirty && g_is_admin),
theme);
mtk::draw_button(
canvas, apply, "Apply", mtk::BUTTON_PRIMARY,
mtk::widget_state(false, apply.contains(g_mouse_x, g_mouse_y),
g_dirty && g_is_admin),
theme);
host.present();
}
static void handle_mouse(const montauk::abi::WinEvent& event) {
g_mouse_x = event.mouse.x;
g_mouse_y = event.mouse.y;
bool pressed = (event.mouse.buttons & 1) &&
!(event.mouse.prev_buttons & 1);
if (event.mouse.scroll) {
g_user_scroll += event.mouse.scroll > 0 ? -1 : 1;
int maximum = g_user_count > VISIBLE_USERS
? g_user_count - VISIBLE_USERS
: 0;
if (g_user_scroll < 0) g_user_scroll = 0;
if (g_user_scroll > maximum) g_user_scroll = maximum;
}
if (!pressed || !g_is_admin) return;
if (service_row().contains(g_mouse_x, g_mouse_y)) {
g_enabled = !g_enabled;
/* A stale "Saved" message would otherwise hide the unsaved state. */
g_status[0] = 0;
} else {
for (int row = 0; row < VISIBLE_USERS; row++) {
int userIndex = g_user_scroll + row;
if (userIndex < g_user_count &&
user_row(row).contains(g_mouse_x, g_mouse_y)) {
g_allowed[userIndex] = !g_allowed[userIndex];
g_status[0] = 0;
break;
}
}
}
if (revert_button().contains(g_mouse_x, g_mouse_y) && g_dirty) {
load_state();
} else if (apply_button().contains(g_mouse_x, g_mouse_y) && g_dirty) {
save_state();
}
recompute_dirty();
}
extern "C" void _start() {
if (!fonts::init()) montauk::exit(1);
g_accent = mtk::load_system_accent();
load_state();
if (!g_win.create("SSH Server", WIN_W, WIN_H)) montauk::exit(1);
render();
while (g_win.id >= 0 && !g_win.closed) {
montauk::abi::WinEvent event;
int count = g_win.poll(&event);
if (count <= 0) {
montauk::yield();
continue;
}
if (event.type == 1) {
handle_mouse(event);
} else if (event.type == 2) {
g_win.width = event.resize.w;
g_win.height = event.resize.h;
} else if (event.type == 3) {
g_win.closed = true;
} else if (event.type == 0 && event.key.pressed &&
event.key.scancode == 1) {
g_win.closed = true;
}
render();
}
g_win.destroy();
montauk::exit(0);
}
+13
View File
@@ -0,0 +1,13 @@
[app]
id = "sshserver"
name = "SSH Server"
binary = "sshserver.elf"
icon = "utilities-terminal.svg"
[menu]
category = "System"
visible = false
[desktop]
section = "settings"
admin_only = false
@@ -0,0 +1,2 @@
#define STB_TRUETYPE_IMPLEMENTATION
#include <gui/stb_truetype.h>