Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions net/tinyssh/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#
# This is free software, licensed under the GNU General Public License v2.
# See /LICENSE for more information.
#

include $(TOPDIR)/rules.mk

PKG_NAME:=tinyssh
PKG_RELEASE:=1

PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/janmojzis/tinyssh
PKG_SOURCE_DATE:=2026-06-13
PKG_SOURCE_VERSION:=3d93382cde06c109d5d274fa2dddea064e543c85
PKG_MIRROR_HASH:=716bc30d70776be6b925fa0454a2597483b694635ee88f855ecbc8d6ac97eb01

PKG_MAINTAINER:=Mario Rugiero <mrugiero@gmail.com>
PKG_LICENSE:=CC0-1.0 OR 0BSD OR MIT-0 OR MIT
PKG_LICENSE_FILES:=LICENSE.md

PKG_BUILD_PARALLEL:=1

include $(INCLUDE_DIR)/package.mk

define Package/tinyssh
SECTION:=net
CATEGORY:=Network
TITLE:=Small SSHv2-only server
URL:=https://tinyssh.org/
DEPENDS:=
endef

define Package/tinyssh/description
TinySSH is a minimalistic SSH server which implements only a subset
of SSHv2 features.
endef

comma:=,
# RELRO bloats tinysshd-listen ~2.7kb -> ~65kb (ld.bfd/mips16 quirk);
# drop it there only, it parses no untrusted input.
TINYSSHD_LISTEN_CFLAGS:=$(filter-out -Wl$(comma)-z$(comma)relro,$(TARGET_CFLAGS))
TINYSSHD_LISTEN_LDFLAGS:=$(filter-out -zrelro,$(TARGET_LDFLAGS))

define Build/Compile
+$(MAKE) -C $(PKG_BUILD_DIR) \
CC="$(TARGET_CC)" \
CFLAGS="$(TARGET_CFLAGS) -fwrapv -Icryptoint" \
CPPFLAGS="$(TARGET_CPPFLAGS)" \
LDFLAGS="$(TARGET_LDFLAGS)" \
tinysshd tinysshd-makekey tinysshd-printkey tinysshnoneauthd
$(TARGET_CC) $(TINYSSHD_LISTEN_CFLAGS) $(TARGET_CPPFLAGS) $(TINYSSHD_LISTEN_LDFLAGS) \
-o $(PKG_BUILD_DIR)/tinysshd-listen ./src/tinysshd-listen.c
endef

define Package/tinyssh/install
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/tinysshd $(1)/usr/sbin/tinysshd
$(LN) tinysshd $(1)/usr/sbin/tinysshd-makekey
$(LN) tinysshd $(1)/usr/sbin/tinysshd-printkey
$(LN) tinysshd $(1)/usr/sbin/tinysshnoneauthd
$(INSTALL_BIN) $(PKG_BUILD_DIR)/tinysshd-listen $(1)/usr/sbin/tinysshd-listen
$(INSTALL_DIR) $(1)/etc/init.d $(1)/etc/config
$(INSTALL_BIN) ./files/tinyssh.init $(1)/etc/init.d/tinyssh
$(INSTALL_CONF) ./files/tinyssh.config $(1)/etc/config/tinyssh
endef

define Package/tinyssh/conffiles
/etc/config/tinyssh
endef

$(eval $(call BuildPackage,tinyssh))
7 changes: 7 additions & 0 deletions net/tinyssh/files/tinyssh.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
config tinyssh 'main'
option enabled '1'
option port '22'
option maxconnections '20'
option verbose '0'
# option interface 'lan'
# option keydir '/etc/tinyssh/sshkeydir'
75 changes: 75 additions & 0 deletions net/tinyssh/files/tinyssh.init
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/bin/sh /etc/rc.common
# tinyssh

START=50
STOP=10

USE_PROCD=1

NAME=tinyssh
LISTEN=/usr/sbin/tinysshd-listen
TINYSSHD=/usr/sbin/tinysshd

ensure_keys() {
local keydir="$1" tmp

[ -d "$keydir" ] && return 0

tmp=$(mktemp -d) || return 1
/usr/sbin/tinysshd-makekey -q "$tmp/keys" || {
rm -rf "$tmp"
return 1
}
mkdir -p "$(dirname "$keydir")"
mv "$tmp/keys" "$keydir"
rmdir "$tmp"
}

start_service() {
local enabled interface port maxconnections verbose keydir bindaddr ipaddr

. /lib/functions.sh
. /lib/functions/network.sh

config_load tinyssh
config_get_bool enabled main enabled 1
[ "$enabled" = 1 ] || return 0

config_get interface main interface
config_get port main port 22
config_get maxconnections main maxconnections 20
config_get_bool verbose main verbose 0
config_get keydir main keydir /etc/tinyssh/sshkeydir

ensure_keys "$keydir" || {
logger -s -t "$NAME" -p daemon.err "unable to generate host keys in '$keydir'"
return 1
}

bindaddr="0.0.0.0"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: bindaddr defaults to 0.0.0.0 and, when an interface is configured, only network_get_ipaddr (IPv4) is consulted, so tinysshd-listen is only ever handed an IPv4 address — even though it fully supports AF_INET6. As written IPv6 clients can never reach the daemon, which differs from dropbear (the in-tree SSH server) that listens on both families by default. Is IPv4-only intended, or should the default / interface handling also cover IPv6 (e.g. a second procd instance bound to :: or the interface's IPv6 address via network_get_ipaddr6)?


Generated by Claude Code

if [ -n "$interface" ]; then
if network_is_up "$interface"; then
network_get_ipaddr ipaddr "$interface"
[ -n "$ipaddr" ] && bindaddr="$ipaddr"
else
logger -t "$NAME" -p daemon.warn \
"interface '$interface' is not up, binding to all interfaces instead"
fi
fi

procd_open_instance
procd_set_param command "$LISTEN"
case "$maxconnections" in
''|*[!0-9]*|0) ;;
*) procd_append_param command -c "$maxconnections" ;;
esac
procd_append_param command "$bindaddr" "$port" "$TINYSSHD"
[ "$verbose" = 1 ] && procd_append_param command -v
procd_append_param command "$keydir"
procd_set_param respawn
procd_close_instance
}

service_triggers() {
procd_add_reload_trigger "tinyssh"
}
88 changes: 88 additions & 0 deletions net/tinyssh/src/tinysshd-listen.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* tinysshd-listen: minimal IPv4/IPv6 accept-and-fork TCP supervisor.
* usage: tinysshd-listen [-c maxconns] ip port prog [args...]
*/

#include <arpa/inet.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>

static volatile sig_atomic_t live = 0;

static void reap(int sig) {
(void)sig;
while (waitpid(-1, NULL, WNOHANG) > 0) live--;
}

int main(int argc, char **argv) {
long maxconns = 0;

if (argc >= 3 && strcmp(argv[1], "-c") == 0) {
maxconns = atol(argv[2]);
argv += 2;
argc -= 2;
}
if (argc < 4) _exit(100);

struct sockaddr_in a4 = {0};
struct sockaddr_in6 a6 = {0};
struct sockaddr *addr;
socklen_t addrlen;
int family;

unsigned short port = (unsigned short)atoi(argv[2]);

if (inet_pton(AF_INET, argv[1], &a4.sin_addr) == 1) {
family = AF_INET;
a4.sin_family = AF_INET;
a4.sin_port = htons(port);
addr = (struct sockaddr *)&a4;
addrlen = sizeof(a4);
} else if (inet_pton(AF_INET6, argv[1], &a6.sin6_addr) == 1) {
family = AF_INET6;
a6.sin6_family = AF_INET6;
a6.sin6_port = htons(port);
addr = (struct sockaddr *)&a6;
addrlen = sizeof(a6);
} else {
_exit(100);
}

int lfd = socket(family, SOCK_STREAM, 0);
if (lfd < 0) _exit(111);

int one = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));

if (bind(lfd, addr, addrlen) < 0) _exit(111);
if (listen(lfd, 20) < 0) _exit(111);

signal(SIGCHLD, reap);

for (;;) {
int cfd = accept(lfd, NULL, NULL);
if (cfd < 0) continue;

if (maxconns > 0 && live >= maxconns) {
close(cfd);
continue;
}

pid_t pid = fork();
if (pid == 0) {
close(lfd);
dup2(cfd, 0);
dup2(cfd, 1);
close(cfd);
execvp(argv[3], &argv[3]);
_exit(111);
}
if (pid > 0) live++;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

live is read-modify-written both here (live++) and in the reap() SIGCHLD handler (live--, line 19). sig_atomic_t only guarantees atomicity for a single read or write, not for a read-modify-write, and reap() can interrupt the main thread mid-update. If SIGCHLD is delivered while live++ is in flight (load/add/store), the handler's decrement is overwritten and lost, so live drifts upward over time. Once it is stuck at >= maxconns the supervisor refuses every new connection — a slow self-inflicted DoS whenever -c is used (the init passes -c 20 by default).

Consider blocking SIGCHLD with sigprocmask around the live >= maxconns check and the increment, or reaping synchronously in the accept loop instead of mutating the counter from the handler.


Generated by Claude Code

close(cfd);
}
}
Loading