From 3eafae7b11699768ac949bd504e9cdcf72f00dcf Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Tue, 6 May 2025 22:45:36 +0800 Subject: [PATCH 001/122] Support ~R for rekey --- manpages/dbclient.1 | 2 ++ src/cli-chansession.c | 5 +++++ src/common-session.c | 7 ++++++- src/kex.h | 1 + 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/manpages/dbclient.1 b/manpages/dbclient.1 index f924a59db..f331dcfa3 100644 --- a/manpages/dbclient.1 +++ b/manpages/dbclient.1 @@ -244,6 +244,8 @@ Typing a newline followed by the key sequence \fI~.\fR (tilde, dot) will termin The sequence \fI~^Z\fR (tilde, ctrl-z) will background the connection. This behaviour only applies when a PTY is used. +\fI~R\fR will perform a key re-exchange of ephemeral session keys. + .SH ENVIRONMENT .TP .B DROPBEAR_PASSWORD diff --git a/src/cli-chansession.c b/src/cli-chansession.c index 73bee176b..46c807e7a 100644 --- a/src/cli-chansession.c +++ b/src/cli-chansession.c @@ -443,6 +443,11 @@ do_escape(unsigned char c) { cli_tty_setup(); cli_ses.winchange = 1; return 1; + case 'R': + /* rekey */ + dropbear_log(LOG_INFO, "rekey"); + ses.kexstate.needrekey = 1; + return 1; default: return 0; } diff --git a/src/common-session.c b/src/common-session.c index c9a76a0c8..d420d6ffa 100644 --- a/src/common-session.c +++ b/src/common-session.c @@ -559,8 +559,10 @@ static void checktimeouts() { if (!ses.kexstate.sentkexinit && (elapsed(now, ses.kexstate.lastkextime) >= KEX_REKEY_TIMEOUT - || ses.kexstate.datarecv+ses.kexstate.datatrans >= KEX_REKEY_DATA)) { + || ses.kexstate.datarecv+ses.kexstate.datatrans >= KEX_REKEY_DATA + || ses.kexstate.needrekey)) { TRACE(("rekeying after timeout or max data reached")) + ses.kexstate.needrekey = 0; send_msg_kexinit(); } @@ -612,6 +614,9 @@ static long select_timeout() { if (!ses.kexstate.sentkexinit) { update_timeout(KEX_REKEY_TIMEOUT, now, ses.kexstate.lastkextime, &timeout); } + if (ses.kexstate.needrekey) { + timeout = 0; + } if (ses.authstate.authdone != 1 && IS_DROPBEAR_SERVER) { /* AUTH_TIMEOUT is only relevant before authdone */ diff --git a/src/kex.h b/src/kex.h index dad599178..83b4404c2 100644 --- a/src/kex.h +++ b/src/kex.h @@ -98,6 +98,7 @@ struct KEXState { unsigned int strict_kex; time_t lastkextime; /* time of the last kex */ + unsigned int needrekey; /* manually trigger a rekey */ unsigned int datatrans; /* data transmitted since last kex */ unsigned int datarecv; /* data received since last kex */ From 6b0e0041bc5cc8adca0e1ff1823821935e843dbc Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Tue, 6 May 2025 22:46:00 +0800 Subject: [PATCH 002/122] Don't end strict kex until receiving newkeys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously non-strict packets would be allowed once Dropbear's own SSH_MSG_NEWKEYS had been sent, but for correctness it should only be allowed once receiving the SSH_MSG_NEWKEYS from the peer. Reported by Fabian Bäumer --- src/common-kex.c | 1 + src/kex.h | 1 + src/process-packet.c | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/common-kex.c b/src/common-kex.c index 5aa04e80e..b8e04eb68 100644 --- a/src/common-kex.c +++ b/src/common-kex.c @@ -197,6 +197,7 @@ void recv_msg_newkeys() { if (ses.kexstate.strict_kex) { ses.recvseq = 0; } + ses.kexstate.recvfirstnewkeys = 1; TRACE(("leave recv_msg_newkeys")) } diff --git a/src/kex.h b/src/kex.h index 83b4404c2..2b7ecd0bf 100644 --- a/src/kex.h +++ b/src/kex.h @@ -91,6 +91,7 @@ struct KEXState { unsigned int donefirstkex; /* Set to 1 after the first kex has completed, ie the transport layer has been set up */ unsigned int donesecondkex; /* Set to 1 after the second kex has completed */ + unsigned int recvfirstnewkeys; /* Set to 1 after the first valid newkeys has been received */ unsigned our_first_follows_matches : 1; diff --git a/src/process-packet.c b/src/process-packet.c index 133a152d0..d3a2a9473 100644 --- a/src/process-packet.c +++ b/src/process-packet.c @@ -44,7 +44,7 @@ void process_packet() { unsigned char type; unsigned int i; - unsigned int first_strict_kex = ses.kexstate.strict_kex && !ses.kexstate.donefirstkex; + unsigned int first_strict_kex = ses.kexstate.strict_kex && !ses.kexstate.recvfirstnewkeys; time_t now; TRACE2(("enter process_packet")) From 753eefea3731261a6bd26e5537556ea26067622f Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Tue, 6 May 2025 22:52:52 +0800 Subject: [PATCH 003/122] Only report "rekey" as a trace log --- src/cli-chansession.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli-chansession.c b/src/cli-chansession.c index 46c807e7a..55fe6d13f 100644 --- a/src/cli-chansession.c +++ b/src/cli-chansession.c @@ -445,7 +445,7 @@ do_escape(unsigned char c) { return 1; case 'R': /* rekey */ - dropbear_log(LOG_INFO, "rekey"); + TRACE(("rekey")); ses.kexstate.needrekey = 1; return 1; default: From e5a0ef27c227f7ae69d9a9fec98a056494409b9b Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 5 May 2025 23:14:19 +0800 Subject: [PATCH 004/122] Execute multihop commands directly, no shell This avoids problems with shell escaping if arguments contain special characters. --- src/cli-main.c | 61 ++++++++++++++++++--------- src/cli-runopts.c | 104 ++++++++++++++++++++++++++++------------------ src/dbutil.c | 9 +++- src/dbutil.h | 1 + src/runopts.h | 5 +++ 5 files changed, 118 insertions(+), 62 deletions(-) diff --git a/src/cli-main.c b/src/cli-main.c index 065fd7630..2fafa8890 100644 --- a/src/cli-main.c +++ b/src/cli-main.c @@ -77,9 +77,8 @@ int main(int argc, char ** argv) { } #if DROPBEAR_CLI_PROXYCMD - if (cli_opts.proxycmd) { + if (cli_opts.proxycmd || cli_opts.proxyexec) { cli_proxy_cmd(&sock_in, &sock_out, &proxy_cmd_pid); - m_free(cli_opts.proxycmd); if (signal(SIGINT, kill_proxy_sighandler) == SIG_ERR || signal(SIGTERM, kill_proxy_sighandler) == SIG_ERR || signal(SIGHUP, kill_proxy_sighandler) == SIG_ERR) { @@ -101,7 +100,8 @@ int main(int argc, char ** argv) { } #endif /* DBMULTI stuff */ -static void exec_proxy_cmd(const void *user_data_cmd) { +#if DROPBEAR_CLI_PROXYCMD +static void shell_proxy_cmd(const void *user_data_cmd) { const char *cmd = user_data_cmd; char *usershell; @@ -110,41 +110,62 @@ static void exec_proxy_cmd(const void *user_data_cmd) { dropbear_exit("Failed to run '%s'\n", cmd); } -#if DROPBEAR_CLI_PROXYCMD +static void exec_proxy_cmd(const void *unused) { + (void)unused; + run_command(cli_opts.proxyexec[0], cli_opts.proxyexec, ses.maxfd); + dropbear_exit("Failed to run '%s'\n", cli_opts.proxyexec[0]); +} + static void cli_proxy_cmd(int *sock_in, int *sock_out, pid_t *pid_out) { - char * ex_cmd = NULL; - size_t ex_cmdlen; + char * cmd_arg = NULL; + void (*exec_fn)(const void *user_data) = NULL; int ret; + /* exactly one of cli_opts.proxycmd or cli_opts.proxyexec should be set */ + /* File descriptor "-j &3" */ - if (*cli_opts.proxycmd == '&') { + if (cli_opts.proxycmd && *cli_opts.proxycmd == '&') { char *p = cli_opts.proxycmd + 1; int sock = strtoul(p, &p, 10); /* must be a single number, and not stdin/stdout/stderr */ if (sock > 2 && sock < 1024 && *p == '\0') { *sock_in = sock; *sock_out = sock; - return; + goto cleanup; } } - /* Normal proxycommand */ - - /* So that spawn_command knows which shell to run */ - fill_passwd(cli_opts.own_user); - - ex_cmdlen = strlen(cli_opts.proxycmd) + 6; /* "exec " + command + '\0' */ - ex_cmd = m_malloc(ex_cmdlen); - snprintf(ex_cmd, ex_cmdlen, "exec %s", cli_opts.proxycmd); + if (cli_opts.proxycmd) { + /* Normal proxycommand */ + size_t shell_cmdlen; + /* So that spawn_command knows which shell to run */ + fill_passwd(cli_opts.own_user); + + shell_cmdlen = strlen(cli_opts.proxycmd) + 6; /* "exec " + command + '\0' */ + cmd_arg = m_malloc(shell_cmdlen); + snprintf(cmd_arg, shell_cmdlen, "exec %s", cli_opts.proxycmd); + exec_fn = shell_proxy_cmd; + } else { + /* No shell */ + exec_fn = exec_proxy_cmd; + } - ret = spawn_command(exec_proxy_cmd, ex_cmd, - sock_out, sock_in, NULL, pid_out); - DEBUG1(("cmd: %s pid=%d", ex_cmd,*pid_out)) - m_free(ex_cmd); + ret = spawn_command(exec_fn, cmd_arg, sock_out, sock_in, NULL, pid_out); if (ret == DROPBEAR_FAILURE) { dropbear_exit("Failed running proxy command"); *sock_in = *sock_out = -1; } + +cleanup: + m_free(cli_opts.proxycmd); + m_free(cmd_arg); + if (cli_opts.proxyexec) { + char **a = NULL; + for (a = cli_opts.proxyexec; *a; a++) { + m_free_direct(*a); + } + m_free(cli_opts.proxyexec); + } } static void kill_proxy_sighandler(int UNUSED(signo)) { diff --git a/src/cli-runopts.c b/src/cli-runopts.c index b66429366..a21b7a265 100644 --- a/src/cli-runopts.c +++ b/src/cli-runopts.c @@ -556,62 +556,88 @@ void loadidentityfile(const char* filename, int warnfail) { /* Fill out -i, -y, -W options that make sense for all * the intermediate processes */ -static char* multihop_passthrough_args(void) { - char *args = NULL; - unsigned int len, total; +static char** multihop_args(const char* argv0, const char* prior_hops) { + /* null terminated array */ + char **args = NULL; + size_t max_args = 14, pos = 0, len; #if DROPBEAR_CLI_PUBKEY_AUTH m_list_elem *iter; #endif - /* Sufficient space for non-string args */ - len = 100; - /* String arguments have arbitrary length, so determine space required */ - if (cli_opts.proxycmd) { - len += strlen(cli_opts.proxycmd); - } #if DROPBEAR_CLI_PUBKEY_AUTH for (iter = cli_opts.privkeys->first; iter; iter = iter->next) { - sign_key * key = (sign_key*)iter->item; - len += 4 + strlen(key->filename); + /* "-i file" for each */ + max_args += 2; } #endif - args = m_malloc(len); - total = 0; + args = m_malloc(sizeof(char*) * max_args); + pos = 0; - /* Create new argument string */ + args[pos] = m_strdup(argv0); + pos++; if (cli_opts.quiet) { - total += m_snprintf(args+total, len-total, "-q "); + args[pos] = m_strdup("-q"); + pos++; } if (cli_opts.no_hostkey_check) { - total += m_snprintf(args+total, len-total, "-y -y "); + args[pos] = m_strdup("-y"); + pos++; + args[pos] = m_strdup("-y"); + pos++; } else if (cli_opts.always_accept_key) { - total += m_snprintf(args+total, len-total, "-y "); + args[pos] = m_strdup("-y"); + pos++; } if (cli_opts.batch_mode) { - total += m_snprintf(args+total, len-total, "-o BatchMode=yes "); + args[pos] = m_strdup("-o"); + pos++; + args[pos] = m_strdup("BatchMode=yes"); + pos++; } if (cli_opts.proxycmd) { - total += m_snprintf(args+total, len-total, "-J '%s' ", cli_opts.proxycmd); + args[pos] = m_strdup("-J"); + pos++; + args[pos] = m_strdup(cli_opts.proxycmd); + pos++; } if (opts.recv_window != DEFAULT_RECV_WINDOW) { - total += m_snprintf(args+total, len-total, "-W %u ", opts.recv_window); + args[pos] = m_strdup("-W"); + pos++; + args[pos] = m_malloc(11); + m_snprintf(args[pos], 11, "%u", opts.recv_window); + pos++; } #if DROPBEAR_CLI_PUBKEY_AUTH for (iter = cli_opts.privkeys->first; iter; iter = iter->next) { sign_key * key = (sign_key*)iter->item; - total += m_snprintf(args+total, len-total, "-i %s ", key->filename); + args[pos] = m_strdup("-i"); + pos++; + args[pos] = m_strdup(key->filename); + pos++; } #endif /* DROPBEAR_CLI_PUBKEY_AUTH */ + /* last hop */ + args[pos] = m_strdup("-B"); + pos++; + len = strlen(cli_opts.remotehost) + strlen(cli_opts.remoteport) + 2; + args[pos] = m_malloc(len); + snprintf(args[pos], len, "%s:%s", cli_opts.remotehost, cli_opts.remoteport); + pos++; + + /* hostnames of prior hops */ + args[pos] = m_strdup(prior_hops); + pos++; + return args; } @@ -626,7 +652,7 @@ static char* multihop_passthrough_args(void) { * etc for as many hosts as we want. * * Note that "-J" arguments aren't actually used, instead - * below sets cli_opts.proxycmd directly. + * below sets cli_opts.proxyexec directly. * * Ports for hosts can be specified as host/port. */ @@ -634,7 +660,7 @@ static void parse_multihop_hostname(const char* orighostarg, const char* argv0) char *userhostarg = NULL; char *hostbuf = NULL; char *last_hop = NULL; - char *remainder = NULL; + char *prior_hops = NULL; /* both scp and rsync parse a user@host argument * and turn it into "-l user host". This breaks @@ -652,6 +678,8 @@ static void parse_multihop_hostname(const char* orighostarg, const char* argv0) } userhostarg = hostbuf; + /* Split off any last hostname and use that as remotehost/remoteport. + * That is used for authorized_keys checking etc */ last_hop = strrchr(userhostarg, ','); if (last_hop) { if (last_hop == userhostarg) { @@ -659,32 +687,28 @@ static void parse_multihop_hostname(const char* orighostarg, const char* argv0) } *last_hop = '\0'; last_hop++; - remainder = userhostarg; + prior_hops = userhostarg; userhostarg = last_hop; } + /* Update cli_opts.remotehost and cli_opts.remoteport */ parse_hostname(userhostarg); - if (last_hop) { - /* Set up the proxycmd */ - unsigned int cmd_len = 0; - char *passthrough_args = multihop_passthrough_args(); - cmd_len = strlen(argv0) + strlen(remainder) - + strlen(cli_opts.remotehost) + strlen(cli_opts.remoteport) - + strlen(passthrough_args) - + 30; - /* replace proxycmd. old -J arguments have been copied - to passthrough_args */ - cli_opts.proxycmd = m_realloc(cli_opts.proxycmd, cmd_len); - m_snprintf(cli_opts.proxycmd, cmd_len, "%s -B %s:%s %s %s", - argv0, cli_opts.remotehost, cli_opts.remoteport, - passthrough_args, remainder); + /* Construct any multihop proxy command. Use proxyexec to + * avoid worrying about shell escaping. */ + if (prior_hops) { + cli_opts.proxyexec = multihop_args(argv0, prior_hops); + /* Any -J argument has been copied to proxyexec */ + if (cli_opts.proxycmd) { + m_free(cli_opts.proxycmd); + } + #ifndef DISABLE_ZLIB - /* The stream will be incompressible since it's encrypted. */ + /* This outer stream will be incompressible since it's encrypted. */ opts.allow_compress = 0; #endif - m_free(passthrough_args); } + m_free(hostbuf); } #endif /* DROPBEAR_CLI_MULTIHOP */ diff --git a/src/dbutil.c b/src/dbutil.c index 2b4492118..a70025ea4 100644 --- a/src/dbutil.c +++ b/src/dbutil.c @@ -371,7 +371,6 @@ int spawn_command(void(*exec_fn)(const void *user_data), const void *exec_data, void run_shell_command(const char* cmd, unsigned int maxfd, char* usershell) { char * argv[4]; char * baseshell = NULL; - unsigned int i; baseshell = basename(usershell); @@ -393,6 +392,12 @@ void run_shell_command(const char* cmd, unsigned int maxfd, char* usershell) { argv[1] = NULL; } + run_command(usershell, argv, maxfd); +} + +void run_command(const char* argv0, char** args, unsigned int maxfd) { + unsigned int i; + /* Re-enable SIGPIPE for the executed process */ if (signal(SIGPIPE, SIG_DFL) == SIG_ERR) { dropbear_exit("signal() error"); @@ -404,7 +409,7 @@ void run_shell_command(const char* cmd, unsigned int maxfd, char* usershell) { m_close(i); } - execv(usershell, argv); + execv(argv0, args); } #if DEBUG_TRACE diff --git a/src/dbutil.h b/src/dbutil.h index 05fc50ce8..bfbed7306 100644 --- a/src/dbutil.h +++ b/src/dbutil.h @@ -63,6 +63,7 @@ char * stripcontrol(const char * text); int spawn_command(void(*exec_fn)(const void *user_data), const void *exec_data, int *writefd, int *readfd, int *errfd, pid_t *pid); void run_shell_command(const char* cmd, unsigned int maxfd, char* usershell); +void run_command(const char* argv0, char** args, unsigned int maxfd); #if ENABLE_CONNECT_UNIX int connect_unix(const char* addr); #endif diff --git a/src/runopts.h b/src/runopts.h index c4061a07e..f25588283 100644 --- a/src/runopts.h +++ b/src/runopts.h @@ -194,7 +194,12 @@ typedef struct cli_runopts { unsigned int netcat_port; #endif #if DROPBEAR_CLI_PROXYCMD + /* A proxy command to run via the user's shell */ char *proxycmd; +#endif +#if DROPBEAR_CLI_MULTIHOP + /* Similar to proxycmd, but is arguments for execve(), not shell */ + char **proxyexec; #endif const char *bind_arg; char *bind_address; From 887241694277b3816da5d174932c10546ea9d2c5 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Wed, 7 May 2025 17:48:28 +0800 Subject: [PATCH 005/122] 2025.88 changelog --- CHANGES | 27 +++++++++++++++++++++++++++ debian/changelog | 6 ++++++ src/sysoptions.h | 2 +- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 65786041e..3ad4c4da8 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,30 @@ +2025.88 - 7 May 2025 + +- Security: Don't allow dbclient hostname arguments to be interpreted + by the shell. + + dbclient hostname arguments with a comma (for multihop) would be + passed to the shell which could result in running arbitrary shell + commands locally. That could be a security issue in situations + where dbclient is passed untrusted hostname arguments. + + Now the multihop command is executed directly, no shell is involved. + Thanks to Marcin Nowak for the report, tracked as CVE-2025-47203 + +- Fix compatibility for htole64 and htole32, regression in 2025.87 + Patch from Peter Fichtner to work with old GCC versions, and + patch from Matt Robinson to check different header files. + +- Fix building on older compilers or libc that don't support + static_assert(). Regression in 2025.87 + +- Support ~R in the client to force a key re-exchange. + +- Improve strict KEX handling. Dropbear previously would allow other + packets at the end of key exchange prior to receiving the remote + peer's NEWKEYS message, which should be forbidden by strict KEX. + Reported by Fabian Bäumer. + 2025.87 - 5 March 2025 Note >> for compatibility/configuration changes diff --git a/debian/changelog b/debian/changelog index 0b77de55b..bce63c7c8 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +dropbear (2025.88-0.1) unstable; urgency=low + + * New upstream release. + + -- Matt Johnston Wed, 7 May 2025 22:51:57 +0800 + dropbear (2025.87-0.1) unstable; urgency=low * New upstream release. diff --git a/src/sysoptions.h b/src/sysoptions.h index c84eab823..2f3e64e4b 100644 --- a/src/sysoptions.h +++ b/src/sysoptions.h @@ -4,7 +4,7 @@ *******************************************************************/ #ifndef DROPBEAR_VERSION -#define DROPBEAR_VERSION "2025.87" +#define DROPBEAR_VERSION "2025.88" #endif /* IDENT_VERSION_PART is the optional part after "SSH-2.0-dropbear". Refer to RFC4253 for requirements. */ From dbb998c37ba9967719687e43aa91f049584c3953 Mon Sep 17 00:00:00 2001 From: Todd Zullinger Date: Mon, 12 May 2025 15:07:34 -0400 Subject: [PATCH 006/122] Fix reference to distrooptions.h in default_options.h The path for distribution customizations is src/distrooptions.h, not src/distoptions.h. This minor typo was added in 454591d (Use src/distrooptions.h as another source, 2024-01-22). --- src/default_options.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/default_options.h b/src/default_options.h index 6e58a29f5..9a0f06461 100644 --- a/src/default_options.h +++ b/src/default_options.h @@ -9,7 +9,7 @@ Local customisation should be added to localoptions.h which is used if it exists in the build directory. Options defined there will override any options in this file. -Customisations will also be taken from src/distoptions.h if it exists. +Customisations will also be taken from src/distrooptions.h if it exists. Options can also be defined with -DDROPBEAR_XXX=[0,1] in Makefile CFLAGS From 5cc0127000db5f7567b54d0495fb91a8e452fe09 Mon Sep 17 00:00:00 2001 From: Konstantin Demin Date: Fri, 9 May 2025 22:39:35 +0300 Subject: [PATCH 007/122] Fix proxycmd without netcat fixes e5a0ef27c2 "Execute multihop commands directly, no shell" Signed-off-by: Konstantin Demin --- src/cli-main.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/cli-main.c b/src/cli-main.c index 2fafa8890..0a052a351 100644 --- a/src/cli-main.c +++ b/src/cli-main.c @@ -77,7 +77,11 @@ int main(int argc, char ** argv) { } #if DROPBEAR_CLI_PROXYCMD - if (cli_opts.proxycmd || cli_opts.proxyexec) { + if (cli_opts.proxycmd +#if DROPBEAR_CLI_MULTIHOP + || cli_opts.proxyexec +#endif + ) { cli_proxy_cmd(&sock_in, &sock_out, &proxy_cmd_pid); if (signal(SIGINT, kill_proxy_sighandler) == SIG_ERR || signal(SIGTERM, kill_proxy_sighandler) == SIG_ERR || @@ -110,11 +114,13 @@ static void shell_proxy_cmd(const void *user_data_cmd) { dropbear_exit("Failed to run '%s'\n", cmd); } +#if DROPBEAR_CLI_MULTIHOP static void exec_proxy_cmd(const void *unused) { (void)unused; run_command(cli_opts.proxyexec[0], cli_opts.proxyexec, ses.maxfd); dropbear_exit("Failed to run '%s'\n", cli_opts.proxyexec[0]); } +#endif static void cli_proxy_cmd(int *sock_in, int *sock_out, pid_t *pid_out) { char * cmd_arg = NULL; @@ -145,9 +151,11 @@ static void cli_proxy_cmd(int *sock_in, int *sock_out, pid_t *pid_out) { cmd_arg = m_malloc(shell_cmdlen); snprintf(cmd_arg, shell_cmdlen, "exec %s", cli_opts.proxycmd); exec_fn = shell_proxy_cmd; +#if DROPBEAR_CLI_MULTIHOP } else { /* No shell */ exec_fn = exec_proxy_cmd; +#endif } ret = spawn_command(exec_fn, cmd_arg, sock_out, sock_in, NULL, pid_out); @@ -159,6 +167,7 @@ static void cli_proxy_cmd(int *sock_in, int *sock_out, pid_t *pid_out) { cleanup: m_free(cli_opts.proxycmd); m_free(cmd_arg); +#if DROPBEAR_CLI_MULTIHOP if (cli_opts.proxyexec) { char **a = NULL; for (a = cli_opts.proxyexec; *a; a++) { @@ -166,6 +175,7 @@ static void cli_proxy_cmd(int *sock_in, int *sock_out, pid_t *pid_out) { } m_free(cli_opts.proxyexec); } +#endif } static void kill_proxy_sighandler(int UNUSED(signo)) { From 16106997d11615e5e2dfe477def062aed7ed0bca Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Tue, 13 May 2025 17:49:19 +0800 Subject: [PATCH 008/122] Don't run hg in configure --- configure | 7 ------- configure.ac | 6 ------ 2 files changed, 13 deletions(-) diff --git a/configure b/configure index 1836e83a7..13c911ef5 100755 --- a/configure +++ b/configure @@ -2979,13 +2979,6 @@ ac_compiler_gnu=$ac_cv_c_compiler_gnu -# Record which revision is being built -if test -s "`which hg`" && test -d "$srcdir/.hg"; then - hgrev=`hg id -i -R "$srcdir"` - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: Source directory Mercurial base revision $hgrev" >&5 -printf "%s\n" "$as_me: Source directory Mercurial base revision $hgrev" >&6;} -fi - ORIGCFLAGS="$CFLAGS" LATE_CFLAGS="" # Checks for programs. diff --git a/configure.ac b/configure.ac index 83fe68c30..674fd4d8d 100644 --- a/configure.ac +++ b/configure.ac @@ -8,12 +8,6 @@ AC_PREREQ([2.59]) AC_INIT -# Record which revision is being built -if test -s "`which hg`" && test -d "$srcdir/.hg"; then - hgrev=`hg id -i -R "$srcdir"` - AC_MSG_NOTICE([Source directory Mercurial base revision $hgrev]) -fi - ORIGCFLAGS="$CFLAGS" LATE_CFLAGS="" # Checks for programs. From a8610f7b98ad4b33ab723602863d60d462fa5af2 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Sun, 10 Aug 2025 19:46:01 +0800 Subject: [PATCH 009/122] Don't limit channel window to 500MB Previously the channel window and increments were limited to 500MB. That is incorrect and causes stuck connections if peers advertise a large window, then don't send an increment within the first 500MB. That's seen with SSH.NET https://github.com/sshnet/SSH.NET/issues/1671 --- src/common-channel.c | 17 ++++++++++------- src/sysoptions.h | 3 --- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/common-channel.c b/src/common-channel.c index 9926972f3..f86da6ee5 100644 --- a/src/common-channel.c +++ b/src/common-channel.c @@ -858,17 +858,21 @@ void common_recv_msg_channel_data(struct Channel *channel, int fd, void recv_msg_channel_window_adjust() { struct Channel * channel; - unsigned int incr; + unsigned int incr, newwin; channel = getchannel(); incr = buf_getint(ses.payload); - TRACE(("received window increment %d", incr)) - incr = MIN(incr, TRANS_MAX_WIN_INCR); + TRACE(("received window increment %u", incr)) - channel->transwindow += incr; - channel->transwindow = MIN(channel->transwindow, TRANS_MAX_WINDOW); - + newwin = channel->transwindow + incr; + if (newwin < channel->transwindow) { + /* Integer overflow, clamp it at maximum. + * Behaviour may be unexpected, senders MUST NOT overflow per rfc4254. */ + TRACE(("overflow window, prev %u", channel->transwindow)); + newwin = 0xffffffff; + } + channel->transwindow = newwin; } /* Increment the incoming data window for a channel, and let the remote @@ -906,7 +910,6 @@ void recv_msg_channel_open() { remotechan = buf_getint(ses.payload); transwindow = buf_getint(ses.payload); - transwindow = MIN(transwindow, TRANS_MAX_WINDOW); transmaxpacket = buf_getint(ses.payload); transmaxpacket = MIN(transmaxpacket, TRANS_MAX_PAYLOAD_LEN); diff --git a/src/sysoptions.h b/src/sysoptions.h index 2f3e64e4b..56d07899a 100644 --- a/src/sysoptions.h +++ b/src/sysoptions.h @@ -243,9 +243,6 @@ #define RECV_MAX_PACKET_LEN (MAX(35000, ((RECV_MAX_PAYLOAD_LEN)+100))) /* for channel code */ -#define TRANS_MAX_WINDOW 500000000 /* 500MB is sufficient, stopping overflow */ -#define TRANS_MAX_WIN_INCR 500000000 /* overflow prevention */ - #define RECV_WINDOWEXTEND (opts.recv_window / 3) /* We send a "window extend" every RECV_WINDOWEXTEND bytes */ #define MAX_RECV_WINDOW (10*1024*1024) /* 10 MB should be enough */ From 1d4c4a542cd5df08cdcebcb2176b2518fdb7cdae Mon Sep 17 00:00:00 2001 From: Konstantin Demin Date: Thu, 31 Jul 2025 14:13:35 +0300 Subject: [PATCH 010/122] fix build without pubkey options fixes: - 98ef42a856 "Don't set pubkey_info directly in checkpubkey_line" - 62ea53c1e5 "Implement no-touch-required and verify-requred for authorized_keys file" Signed-off-by: Konstantin Demin --- src/svr-authpubkey.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/svr-authpubkey.c b/src/svr-authpubkey.c index 61f4d22df..94ae728ce 100644 --- a/src/svr-authpubkey.c +++ b/src/svr-authpubkey.c @@ -186,12 +186,14 @@ void svr_auth_pubkey(int valid_user) { #if DROPBEAR_SK_ECDSA || DROPBEAR_SK_ED25519 key->sk_flags_mask = SSH_SK_USER_PRESENCE_REQD; +#if DROPBEAR_SVR_PUBKEY_OPTIONS_BUILT if (ses.authstate.pubkey_options && ses.authstate.pubkey_options->no_touch_required_flag) { key->sk_flags_mask &= ~SSH_SK_USER_PRESENCE_REQD; } if (ses.authstate.pubkey_options && ses.authstate.pubkey_options->verify_required_flag) { key->sk_flags_mask |= SSH_SK_USER_VERIFICATION_REQD; } +#endif /* DROPBEAR_SVR_PUBKEY_OPTIONS */ #endif /* create the data which has been signed - this a string containing @@ -513,7 +515,13 @@ static int checkpubkey(const char* keyalgo, unsigned int keyalgolen, line_num++; ret = checkpubkey_line(line, line_num, filename, keyalgo, keyalgolen, - keyblob, keybloblen, &ses.authstate.pubkey_info); + keyblob, keybloblen, +#if DROPBEAR_SVR_PUBKEY_OPTIONS_BUILT + &ses.authstate.pubkey_info +#else + NULL +#endif + ); if (ret == DROPBEAR_SUCCESS) { break; } From 1a2c1e649a18245d84526dedf4c2effce7238579 Mon Sep 17 00:00:00 2001 From: Konstantin Demin Date: Fri, 8 Aug 2025 10:02:44 +0300 Subject: [PATCH 011/122] fix missing depends for sntrup761x25519-sha512 fixes 440b7b5c4f "Add sntrup761x25519-sha512 post-quantum key exchange" Signed-off-by: Konstantin Demin --- src/sysoptions.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sysoptions.h b/src/sysoptions.h index 56d07899a..2e9ccff23 100644 --- a/src/sysoptions.h +++ b/src/sysoptions.h @@ -207,7 +207,7 @@ /* LTC SHA384 depends on SHA512 */ #define DROPBEAR_SHA512 ((DROPBEAR_SHA2_512_HMAC) || (DROPBEAR_ECC_521) \ || (DROPBEAR_SHA384) || (DROPBEAR_DH_GROUP16) \ - || (DROPBEAR_ED25519)) + || (DROPBEAR_ED25519) || (DROPBEAR_SNTRUP761)) #define DROPBEAR_DH_GROUP14 ((DROPBEAR_DH_GROUP14_SHA256) || (DROPBEAR_DH_GROUP14_SHA1)) From beb0616d21539c3eb024e30a76d370392f205620 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Tue, 2 Sep 2025 22:18:18 +0800 Subject: [PATCH 012/122] Use $(MAKE) for "make check" Should help for non-gmake platforms --- Makefile.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.in b/Makefile.in index 14db0732e..fe63a80d9 100644 --- a/Makefile.in +++ b/Makefile.in @@ -290,7 +290,7 @@ lint: cd $(srcdir); ./dropbear_lint.sh check: lint - make -C test + $(MAKE) -C test ## Fuzzing targets From 003c5fcaabc114430d5d14142e95ffdbbd2d19b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Mangeonjean?= Date: Thu, 11 Sep 2025 08:13:56 +0200 Subject: [PATCH 013/122] fix: add missing signals --- src/common-chansession.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/common-chansession.c b/src/common-chansession.c index b350c6ce4..556d3a217 100644 --- a/src/common-chansession.c +++ b/src/common-chansession.c @@ -28,6 +28,7 @@ const struct SigMap signames[] = { {SIGABRT, "ABRT"}, {SIGALRM, "ALRM"}, + {SIGBUS, "BUS"}, {SIGFPE, "FPE"}, {SIGHUP, "HUP"}, {SIGILL, "ILL"}, @@ -37,6 +38,7 @@ const struct SigMap signames[] = { {SIGQUIT, "QUIT"}, {SIGSEGV, "SEGV"}, {SIGTERM, "TERM"}, + {SIGTRAP, "TRAP"}, {SIGUSR1, "USR1"}, {SIGUSR2, "USR2"}, {0, NULL} From 4aecd8849c694687e7ebffbac5fa8b942bf11bc3 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Tue, 4 Nov 2025 22:40:32 +0800 Subject: [PATCH 014/122] Fix incorrect server auth delay Auth delay was meant to be 250-350ms, it was actually 150-350ms or possibly negative. Fixes #387 --- src/svr-auth.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/svr-auth.c b/src/svr-auth.c index 895aff682..0a6b33a97 100644 --- a/src/svr-auth.c +++ b/src/svr-auth.c @@ -389,9 +389,9 @@ void send_msg_userauth_failure(int partial, int incrfail) { /* Desired total delay 300ms +-50ms (in nanoseconds). Beware of integer overflow if increasing these values */ - const int mindelay = 250000000; - const unsigned int vardelay = 100000000; - suseconds_t rand_delay; + const uint32_t mindelay = 250000000; + const uint32_t vardelay = 100000000; + uint32_t rand_delay; struct timespec delay; gettime_wrapper(&delay); @@ -407,7 +407,7 @@ void send_msg_userauth_failure(int partial, int incrfail) { genrandom((unsigned char*)&rand_delay, sizeof(rand_delay)); rand_delay = mindelay + (rand_delay % vardelay); - if (delay.tv_sec == 0 && delay.tv_nsec <= mindelay) { + if (delay.tv_sec == 0 && delay.tv_nsec <= rand_delay) { /* Compensate for elapsed time */ delay.tv_nsec = rand_delay - delay.tv_nsec; } else { From 3ffa4384e8ae2a0a518da85bd6549ab7f0ce94c9 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Wed, 12 Nov 2025 19:41:49 +0800 Subject: [PATCH 015/122] tests: Fix SO_REUSEADDR for TCP tests Previous allow_reuse_addr was incorrect, it should be allow_reuse_address. Confirmed looking at setsockopt in strace when running tcpflushout test. This is a partial fix for github #386 --- test/test_dropbear.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_dropbear.py b/test/test_dropbear.py index 6652428ce..621c4c46b 100644 --- a/test/test_dropbear.py +++ b/test/test_dropbear.py @@ -76,7 +76,7 @@ def own_venv_command(): class HandleTcp(socketserver.ThreadingMixIn, socketserver.TCPServer): # override TCPServer's default, avoids TIME_WAIT - allow_reuse_addr = True + allow_reuse_address = True """ Listens for a single incoming request, sends a response if given, and returns the inbound data. From 4a9610942606d6fe043b9696b6b5327fea88fc5b Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Wed, 12 Nov 2025 21:07:07 +0800 Subject: [PATCH 016/122] Fix typo in PAM compiler error message --- src/sysoptions.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sysoptions.h b/src/sysoptions.h index 2e9ccff23..9e72314b4 100644 --- a/src/sysoptions.h +++ b/src/sysoptions.h @@ -332,7 +332,7 @@ /* PAM requires ./configure --enable-pam */ #if !defined(HAVE_LIBPAM) && DROPBEAR_SVR_PAM_AUTH -#error "DROPBEAR_SVR_PATM_AUTH requires PAM headers. Perhaps ./configure --enable-pam ?" +#error "DROPBEAR_SVR_PAM_AUTH requires PAM headers. Perhaps ./configure --enable-pam ?" #endif #if DROPBEAR_SVR_PASSWORD_AUTH && !HAVE_CRYPT From 7b8e47a74398efbef02429825c0e37d835cc9eef Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Wed, 12 Nov 2025 21:25:43 +0800 Subject: [PATCH 017/122] Fix too small ecdsa private key buffer When RSA or DSS are not built, the private key buffer size is too small, resulting in a "bad_getbuf" or similar exit. This was a regression in 2025.87. Fixes: 440b7b5c4ffb ("Add sntrup761x25519-sha512 post-quantum key exchange") ECDSA private key requires around 241 bytes 4 + len("ecdsa-sha2-nistp521") + 4 + (521/8+1 + 4)*3 + 4 Fixes #368 on github. --- src/sysoptions.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sysoptions.h b/src/sysoptions.h index 9e72314b4..cea9688e9 100644 --- a/src/sysoptions.h +++ b/src/sysoptions.h @@ -266,7 +266,7 @@ #else /* 521 bit ecdsa key */ #define MAX_PUBKEY_SIZE 200 -#define MAX_PRIVKEY_SIZE 200 +#define MAX_PRIVKEY_SIZE 250 #endif /* For kex hash buffer, worst case size for Q_C || Q_S || K */ From e0251be2354e1a5c6eccfc2cf4b64243625dafcc Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Tue, 9 Dec 2025 15:08:06 +0900 Subject: [PATCH 018/122] Drop privileges after user authentication Instead of switching user privileges after forking to a shell, switch to the user immediately upon successful authentication. This will require further commits to fix utmp and hostkey handling. The DROPBEAR_SVR_DROP_PRIVS configuration option controls this behaviour. This should generally be enabled, but can be set to 0 for incompatible platforms. In future it may become non-optional, those platforms should be investigated. Most uses of DROPBEAR_SVR_MULTIUSER have been replaced by !DROPBEAR_SVR_DROP_PRIVS. --- .github/workflows/build.yml | 2 ++ src/auth.h | 1 + src/default_options.h | 6 +++++ src/svr-agentfwd.c | 14 ++++++++---- src/svr-auth.c | 45 +++++++++++++++++++++++++++++++++++++ src/svr-authpubkey.c | 6 +++-- src/svr-chansession.c | 26 ++------------------- src/sysoptions.h | 3 +++ 8 files changed, 73 insertions(+), 30 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 61e64a183..5c07d2825 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -227,6 +227,8 @@ jobs: echo "#define DROPBEAR_SVR_PASSWORD_AUTH 0" >> localoptions.h # 1 second timeout is too short sed -i "s/DEFAULT_IDLE_TIMEOUT 1/DEFAULT_IDLE_TIMEOUT 99/" localoptions.h + # DROPBEAR_SVR_DROP_PRIVS is on by default, turn it off + echo "#define DROPBEAR_SVR_DROP_PRIVS 0" >> localoptions.h - name: make run: | diff --git a/src/auth.h b/src/auth.h index 0e854fbb8..096d23daa 100644 --- a/src/auth.h +++ b/src/auth.h @@ -40,6 +40,7 @@ void send_msg_userauth_banner(const buffer *msg); void svr_auth_password(int valid_user); void svr_auth_pubkey(int valid_user); void svr_auth_pam(int valid_user); +void svr_switch_user(void); #if DROPBEAR_SVR_PUBKEY_OPTIONS_BUILT int svr_pubkey_allows_agentfwd(void); diff --git a/src/default_options.h b/src/default_options.h index 9a0f06461..705da740b 100644 --- a/src/default_options.h +++ b/src/default_options.h @@ -303,6 +303,12 @@ group1 in Dropbear server too */ /* -T server option overrides */ #define MAX_AUTH_TRIES 10 +/* Change server process to user privileges after authentication. */ +#ifndef DROPBEAR_SVR_DROP_PRIVS +/* Default is enabled. Should only be disabled if platforms are incompatible */ +#define DROPBEAR_SVR_DROP_PRIVS DROPBEAR_SVR_MULTIUSER +#endif + /* Delay introduced before closing an unauthenticated session (seconds). Disabled by default, can be set to say 30 seconds to reduce the speed of password brute forcing. Note that there is a risk of denial of diff --git a/src/svr-agentfwd.c b/src/svr-agentfwd.c index a8941ea64..5ee8c2508 100644 --- a/src/svr-agentfwd.c +++ b/src/svr-agentfwd.c @@ -151,7 +151,7 @@ void svr_agentcleanup(struct ChanSess * chansess) { if (chansess->agentfile != NULL && chansess->agentdir != NULL) { -#if DROPBEAR_SVR_MULTIUSER +#if !DROPBEAR_SVR_DROP_PRIVS /* Remove the dir as the user. That way they can't cause problems except * for themselves */ uid = getuid(); @@ -160,6 +160,9 @@ void svr_agentcleanup(struct ChanSess * chansess) { (seteuid(ses.authstate.pw_uid)) < 0) { dropbear_exit("Failed to set euid"); } +#else + (void)uid; + (void)gid; #endif /* 2 for "/" and "\0" */ @@ -172,7 +175,7 @@ void svr_agentcleanup(struct ChanSess * chansess) { rmdir(chansess->agentdir); -#if DROPBEAR_SVR_MULTIUSER +#if !DROPBEAR_SVR_DROP_PRIVS if ((seteuid(uid)) < 0 || (setegid(gid)) < 0) { dropbear_exit("Failed to revert euid"); @@ -219,7 +222,7 @@ static int bindagent(int fd, struct ChanSess * chansess) { gid_t gid; int ret = DROPBEAR_FAILURE; -#if DROPBEAR_SVR_MULTIUSER +#if !DROPBEAR_SVR_DROP_PRIVS /* drop to user privs to make the dir/file */ uid = getuid(); gid = getgid(); @@ -227,6 +230,9 @@ static int bindagent(int fd, struct ChanSess * chansess) { (seteuid(ses.authstate.pw_uid)) < 0) { dropbear_exit("Failed to set euid"); } +#else + (void)uid; + (void)gid; #endif memset((void*)&addr, 0x0, sizeof(addr)); @@ -267,7 +273,7 @@ static int bindagent(int fd, struct ChanSess * chansess) { out: -#if DROPBEAR_SVR_MULTIUSER +#if !DROPBEAR_SVR_DROP_PRIVS if ((seteuid(uid)) < 0 || (setegid(gid)) < 0) { dropbear_exit("Failed to revert euid"); diff --git a/src/svr-auth.c b/src/svr-auth.c index 0a6b33a97..46ba01289 100644 --- a/src/svr-auth.c +++ b/src/svr-auth.c @@ -457,12 +457,22 @@ void send_msg_userauth_success() { /* authdone must be set after encrypt_packet() for * delayed-zlib mode */ ses.authstate.authdone = 1; + +#if DROPBEAR_DROP_PRIVS + svr_switch_user(); +#endif ses.connect_time = 0; +#if DROPBEAR_DROP_PRIVS + /* If running as the user, we can rely on the OS + * to limit allowed ports */ + ses.allowprivport = 1; +#else if (ses.authstate.pw_uid == 0) { ses.allowprivport = 1; } +#endif /* Remove from the list of pre-auth sockets. Should be m_close(), since if * we fail, we might end up leaking connection slots, and disallow new @@ -472,3 +482,38 @@ void send_msg_userauth_success() { TRACE(("leave send_msg_userauth_success")) } + +/* Switch to the ses.authstate user. + * Fails if not running as root and the user differs. + * + * This may be called either after authentication, or + * after shell/command fork if DROPBEAR_SVR_DROP_PRIVS is unset. + */ +void svr_switch_user(void) { + assert(ses.authstate.authdone); + + /* We can only change uid/gid as root ... */ + if (getuid() == 0) { + + if ((setgid(ses.authstate.pw_gid) < 0) || + (initgroups(ses.authstate.pw_name, + ses.authstate.pw_gid) < 0)) { + dropbear_exit("Error changing user group"); + } + if (setuid(ses.authstate.pw_uid) < 0) { + dropbear_exit("Error changing user"); + } + } else { + /* ... but if the daemon is the same uid as the requested uid, we don't + * need to */ + + /* XXX - there is a minor issue here, in that if there are multiple + * usernames with the same uid, but differing groups, then the + * differing groups won't be set (as with initgroups()). The solution + * is for the sysadmin not to give out the UID twice */ + if (getuid() != ses.authstate.pw_uid) { + dropbear_exit("Couldn't change user as non-root"); + } + } +} + diff --git a/src/svr-authpubkey.c b/src/svr-authpubkey.c index 94ae728ce..e26b0eefd 100644 --- a/src/svr-authpubkey.c +++ b/src/svr-authpubkey.c @@ -464,12 +464,14 @@ static int checkpubkey(const char* keyalgo, unsigned int keyalgolen, int ret = DROPBEAR_FAILURE; buffer * line = NULL; int line_num; +#if !DROPBEAR_SVR_DROP_PRIVS uid_t origuid; gid_t origgid; +#endif TRACE(("enter checkpubkey")) -#if DROPBEAR_SVR_MULTIUSER +#if !DROPBEAR_SVR_DROP_PRIVS /* access the file as the authenticating user. */ origuid = getuid(); origgid = getgid(); @@ -490,7 +492,7 @@ static int checkpubkey(const char* keyalgo, unsigned int keyalgolen, TRACE(("checkpubkey: failed opening %s: %s", filename, strerror(errno))) } } -#if DROPBEAR_SVR_MULTIUSER +#if !DROPBEAR_SVR_DROP_PRIVS if ((seteuid(origuid)) < 0 || (setegid(origgid)) < 0) { dropbear_exit("Failed to revert euid"); diff --git a/src/svr-chansession.c b/src/svr-chansession.c index 2ca6fc141..0a37fbf28 100644 --- a/src/svr-chansession.c +++ b/src/svr-chansession.c @@ -980,30 +980,8 @@ static void execchild(const void *user_data) { #endif /* DEBUG_VALGRIND */ } -#if DROPBEAR_SVR_MULTIUSER - /* We can only change uid/gid as root ... */ - if (getuid() == 0) { - - if ((setgid(ses.authstate.pw_gid) < 0) || - (initgroups(ses.authstate.pw_name, - ses.authstate.pw_gid) < 0)) { - dropbear_exit("Error changing user group"); - } - if (setuid(ses.authstate.pw_uid) < 0) { - dropbear_exit("Error changing user"); - } - } else { - /* ... but if the daemon is the same uid as the requested uid, we don't - * need to */ - - /* XXX - there is a minor issue here, in that if there are multiple - * usernames with the same uid, but differing groups, then the - * differing groups won't be set (as with initgroups()). The solution - * is for the sysadmin not to give out the UID twice */ - if (getuid() != ses.authstate.pw_uid) { - dropbear_exit("Couldn't change user as non-root"); - } - } +#if !DROPBEAR_SVR_DROP_PRIVS + svr_switch_user(); #endif /* set env vars */ diff --git a/src/sysoptions.h b/src/sysoptions.h index cea9688e9..32b0a135e 100644 --- a/src/sysoptions.h +++ b/src/sysoptions.h @@ -440,6 +440,9 @@ #define DROPBEAR_MULTI 0 #endif +#if !DROPBEAR_SVR_MULTIUSER && DROPBEAR_SVR_DROP_PRIVS +#error DROPBEAR_SVR_DROP_PRIVS needs DROPBEAR_SVR_MULTIUSER +#endif /* Fuzzing expects all key types to be enabled */ #if DROPBEAR_FUZZ #if defined(DROPBEAR_DSS) From b47fe5df58f0b459bb49accdd8cb961d969209fb Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Tue, 9 Dec 2025 09:04:04 +0900 Subject: [PATCH 019/122] Remove return code from login_login Previously this was always 0, so not useful. --- src/loginrec.c | 19 +++++-------------- src/loginrec.h | 6 +++--- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/loginrec.c b/src/loginrec.c index b543bcb35..d4fdb625c 100644 --- a/src/loginrec.c +++ b/src/loginrec.c @@ -193,32 +193,24 @@ int wtmpx_get_entry(struct logininfo *li); * * Call with a pointer to a struct logininfo initialised with * login_init_entry() or login_alloc_entry() - * - * Returns: - * >0 if successful - * 0 on failure (will use OpenSSH's logging facilities for diagnostics) */ -int +void login_login (struct logininfo *li) { li->type = LTYPE_LOGIN; - return login_write(li); + login_write(li); } /* login_logout(struct logininfo *) - Record a logout * * Call as with login_login() - * - * Returns: - * >0 if successful - * 0 on failure (will use OpenSSH's logging facilities for diagnostics) */ -int +void login_logout(struct logininfo *li) { li->type = LTYPE_LOGOUT; - return login_write(li); + login_write(li); } @@ -309,7 +301,7 @@ login_set_current_time(struct logininfo *li) ** login_write: Call low-level recording functions based on autoconf ** results **/ -int +void login_write (struct logininfo *li) { #ifndef HAVE_CYGWIN @@ -340,7 +332,6 @@ login_write (struct logininfo *li) #ifdef USE_WTMPX wtmpx_write_entry(li); #endif - return 0; } #ifdef LOGIN_NEEDS_UTMPX diff --git a/src/loginrec.h b/src/loginrec.h index 6abde484e..f8c98ba5b 100644 --- a/src/loginrec.h +++ b/src/loginrec.h @@ -161,8 +161,8 @@ int login_init_entry(struct logininfo *li, int pid, const char *username, void login_set_current_time(struct logininfo *li); /* record the entry */ -int login_login (struct logininfo *li); -int login_logout(struct logininfo *li); +void login_login (struct logininfo *li); +void login_logout(struct logininfo *li); #ifdef LOGIN_NEEDS_UTMPX int login_utmp_only(struct logininfo *li); #endif @@ -170,7 +170,7 @@ int login_utmp_only(struct logininfo *li); /** End of public functions */ /* record the entry */ -int login_write (struct logininfo *li); +void login_write (struct logininfo *li); int login_log_entry(struct logininfo *li); /* produce various forms of the line filename */ From 73e4e70ea8e6b890c3918b52bb2e647313a09faa Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Tue, 9 Dec 2025 09:05:30 +0900 Subject: [PATCH 020/122] Retain utmp saved group when dropping privileges utmp is required to record logout. The saved group is reset by the OS for the executed user shell. This requires setresgid() function which is not available on all platforms. Notable platforms are netbsd and macos. Those platforms will have to set DROPBEAR_SVR_DROP_PRIVS 0 unless an alternative approach is found. --- .github/workflows/build.yml | 6 ++++ configure | 7 +++++ configure.ac | 1 + src/auth.h | 2 ++ src/config.h.in | 3 ++ src/loginrec.c | 6 ---- src/session.h | 6 ++++ src/svr-auth.c | 61 +++++++++++++++++++++++++++++++++++-- src/svr-chansession.c | 8 +++++ src/sysoptions.h | 4 +++ 10 files changed, 96 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5c07d2825..4fe41bd43 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -78,6 +78,9 @@ jobs: # fails with: # .../ranlib: file: libtomcrypt.a(cbc_setiv.o) has no symbols ranlib: ranlib -no_warning_for_no_symbols + # macos doesn't have setresgid + localoptions: | + #define DROPBEAR_SVR_DROP_PRIVS 0 - name: macos 15 os: macos-15 @@ -90,6 +93,9 @@ jobs: # fails with: # .../ranlib: file: libtomcrypt.a(cbc_setiv.o) has no symbols ranlib: ranlib -no_warning_for_no_symbols + # macos doesn't have setresgid + localoptions: | + #define DROPBEAR_SVR_DROP_PRIVS 0 # Check that debug code doesn't bitrot - name: DEBUG_TRACE diff --git a/configure b/configure index 13c911ef5..8867f8a3c 100755 --- a/configure +++ b/configure @@ -7590,6 +7590,13 @@ then : fi +ac_fn_c_check_func "$LINENO" "setresgid" "ac_cv_func_setresgid" +if test "x$ac_cv_func_setresgid" = xyes +then : + printf "%s\n" "#define HAVE_SETRESGID 1" >>confdefs.h + +fi + # Might be a macro. Might be sys/endian.h on BSDs ac_fn_c_check_header_compile "$LINENO" "endian.h" "ac_cv_header_endian_h" "$ac_includes_default" diff --git a/configure.ac b/configure.ac index 674fd4d8d..0e7e331b3 100644 --- a/configure.ac +++ b/configure.ac @@ -539,6 +539,7 @@ AC_CHECK_FUNCS(utmpname) AC_CHECK_FUNCS(endutxent getutxent getutxid getutxline pututxline ) AC_CHECK_FUNCS(setutxent utmpxname) AC_CHECK_FUNCS(logout updwtmp logwtmp) +AC_CHECK_FUNCS(setresgid) # Might be a macro. Might be sys/endian.h on BSDs AC_CHECK_HEADERS([endian.h]) diff --git a/src/auth.h b/src/auth.h index 096d23daa..1145ad797 100644 --- a/src/auth.h +++ b/src/auth.h @@ -41,6 +41,8 @@ void svr_auth_password(int valid_user); void svr_auth_pubkey(int valid_user); void svr_auth_pam(int valid_user); void svr_switch_user(void); +void svr_raise_gid_utmp(void); +void svr_restore_gid(void); #if DROPBEAR_SVR_PUBKEY_OPTIONS_BUILT int svr_pubkey_allows_agentfwd(void); diff --git a/src/config.h.in b/src/config.h.in index 0590e0c6d..589786e7b 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -231,6 +231,9 @@ /* Define to 1 if you have the header file. */ #undef HAVE_SECURITY_PAM_APPL_H +/* Define to 1 if you have the `setresgid' function. */ +#undef HAVE_SETRESGID + /* Define to 1 if you have the `setutent' function. */ #undef HAVE_SETUTENT diff --git a/src/loginrec.c b/src/loginrec.c index d4fdb625c..3118bf6cd 100644 --- a/src/loginrec.c +++ b/src/loginrec.c @@ -304,12 +304,6 @@ login_set_current_time(struct logininfo *li) void login_write (struct logininfo *li) { -#ifndef HAVE_CYGWIN - if ((int)geteuid() != 0) { - return 1; - } -#endif - /* set the timestamp */ login_set_current_time(li); #ifdef USE_LOGIN diff --git a/src/session.h b/src/session.h index f37e7ff42..e1a5cfa0d 100644 --- a/src/session.h +++ b/src/session.h @@ -276,6 +276,12 @@ struct serversession { /* The instance created by the plugin_new function */ struct PluginInstance *plugin_instance; #endif + +#if DROPBEAR_SVR_DROP_PRIVS + /* Set to 1 when utmp_gid is valid */ + int have_utmp_gid; + gid_t utmp_gid; +#endif }; typedef enum { diff --git a/src/svr-auth.c b/src/svr-auth.c index 46ba01289..de0145824 100644 --- a/src/svr-auth.c +++ b/src/svr-auth.c @@ -458,13 +458,14 @@ void send_msg_userauth_success() { * delayed-zlib mode */ ses.authstate.authdone = 1; -#if DROPBEAR_DROP_PRIVS +#if DROPBEAR_SVR_DROP_PRIVS + /* Drop privileges as soon as authentication has happened. */ svr_switch_user(); #endif ses.connect_time = 0; -#if DROPBEAR_DROP_PRIVS +#if DROPBEAR_SVR_DROP_PRIVS /* If running as the user, we can rely on the OS * to limit allowed ports */ ses.allowprivport = 1; @@ -483,6 +484,20 @@ void send_msg_userauth_success() { } +#if DROPBEAR_SVR_DROP_PRIVS +/* Returns DROPBEAR_SUCCESS or DROPBEAR_FAILURE */ +static int utmp_gid(gid_t *ret_gid) { + struct group *utmp_gr = getgrnam("utmp"); + if (!utmp_gr) { + TRACE(("No utmp group")); + return DROPBEAR_FAILURE; + } + + *ret_gid = utmp_gr->gr_gid; + return DROPBEAR_SUCCESS; +} +#endif + /* Switch to the ses.authstate user. * Fails if not running as root and the user differs. * @@ -500,6 +515,25 @@ void svr_switch_user(void) { ses.authstate.pw_gid) < 0)) { dropbear_exit("Error changing user group"); } + +#if DROPBEAR_SVR_DROP_PRIVS + /* Retain utmp saved group so that wtmp/utmp can be written */ + int ret = utmp_gid(&svr_ses.utmp_gid); + if (ret == DROPBEAR_SUCCESS) { + /* Set saved gid to utmp so that it can be + * restored for login_logout() etc. This saved + * group is cleared by the OS on execve() */ + int rc = setresgid(-1, -1, svr_ses.utmp_gid); + if (rc == 0) { + svr_ses.have_utmp_gid = 1; + } else { + /* Will not attempt to switch to utmp gid. + * login() etc may fail. */ + TRACE(("utmp setresgid failed")); + } + } +#endif + if (setuid(ses.authstate.pw_uid) < 0) { dropbear_exit("Error changing user"); } @@ -517,3 +551,26 @@ void svr_switch_user(void) { } } +void svr_raise_gid_utmp(void) { +#if DROPBEAR_SVR_DROP_PRIVS + if (!svr_ses.have_utmp_gid) { + return; + } + + if (setegid(svr_ses.utmp_gid) != 0) { + dropbear_log(LOG_WARNING, "failed setegid"); + } +#endif +} + +void svr_restore_gid(void) { +#if DROPBEAR_SVR_DROP_PRIVS + if (!svr_ses.have_utmp_gid) { + return; + } + + if (setegid(getgid()) != 0) { + dropbear_log(LOG_WARNING, "failed setegid"); + } +#endif +} diff --git a/src/svr-chansession.c b/src/svr-chansession.c index 0a37fbf28..11205f3b1 100644 --- a/src/svr-chansession.c +++ b/src/svr-chansession.c @@ -326,7 +326,11 @@ static void cleanupchansess(const struct Channel *channel) { if (chansess->tty) { /* write the utmp/wtmp login record */ li = chansess_login_alloc(chansess); + + svr_raise_gid_utmp(); login_logout(li); + svr_restore_gid(); + login_free_entry(li); pty_release(chansess->tty); @@ -847,7 +851,11 @@ static int ptycommand(struct Channel *channel, struct ChanSess *chansess) { * terminal used for stdout with the dup2 above, otherwise * the wtmp login will not be recorded */ li = chansess_login_alloc(chansess); + + svr_raise_gid_utmp(); login_login(li); + svr_restore_gid(); + login_free_entry(li); /* Can now dup2 stderr. Messages from login_login() have gone diff --git a/src/sysoptions.h b/src/sysoptions.h index 32b0a135e..9bdcb0ce2 100644 --- a/src/sysoptions.h +++ b/src/sysoptions.h @@ -355,6 +355,10 @@ #error "At least one hostkey or public-key algorithm must be enabled; RSA is recommended." #endif +#if DROPBEAR_SVR_DROP_PRIVS && !defined(HAVE_SETRESGID) + #error "DROPBEAR_SVR_DROP_PRIVS requires setresgid()." +#endif + /* Source for randomness. This must be able to provide hundreds of bytes per SSH * connection without blocking. */ #ifndef DROPBEAR_URANDOM_DEV From a4043dac4e0e0237255200603672ddb0122017a4 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Tue, 9 Dec 2025 09:08:37 +0900 Subject: [PATCH 021/122] Limit rekey to current hostkey type During rekey dropbear process may be running with user privileges, that can't write a new hostkey when auto-generating keys. Only offer the original hostkey when rekeying, also for non-autogenerate case. --- src/runopts.h | 1 + src/svr-kex.c | 8 ++++++++ src/svr-runopts.c | 11 +++++++++++ 3 files changed, 20 insertions(+) diff --git a/src/runopts.h b/src/runopts.h index f25588283..c8072b365 100644 --- a/src/runopts.h +++ b/src/runopts.h @@ -61,6 +61,7 @@ extern runopts opts; int readhostkey(const char * filename, sign_key * hostkey, enum signkey_type *type); void load_all_hostkeys(void); +void disable_sig_except(enum signature_type sig_type); typedef struct svr_runopts { diff --git a/src/svr-kex.c b/src/svr-kex.c index 14df08a1a..c066dd816 100644 --- a/src/svr-kex.c +++ b/src/svr-kex.c @@ -99,6 +99,14 @@ void recv_msg_kexdh_init() { } #endif + if (!ses.kexstate.donesecondkex) { + /* Disable other signature types. + * During future rekeying, privileges may have been dropped + * so other keys won't be loadable. + * This must occur after send_msg_ext_info() which uses the hostkey list */ + disable_sig_except(ses.newkeys->algo_signature); + } + ses.requirenext = SSH_MSG_NEWKEYS; TRACE(("leave recv_msg_kexdh_init")) } diff --git a/src/svr-runopts.c b/src/svr-runopts.c index 709dc5764..5d114f83d 100644 --- a/src/svr-runopts.c +++ b/src/svr-runopts.c @@ -515,6 +515,17 @@ static void disablekey(enum signature_type type) { } } +void disable_sig_except(enum signature_type allow_type) { + int i; + TRACE(("Disabling other sigs except %d", allow_type)); + for (i = 0; sigalgs[i].name != NULL; i++) { + enum signature_type sig_type = sigalgs[i].val; + if (sig_type != allow_type) { + sigalgs[i].usable = 0; + } + } +} + static void loadhostkey_helper(const char *name, void** src, void** dst, int fatal_duplicate) { if (*dst) { if (fatal_duplicate) { From 5ae507ddf082118f07231a236be2af6365c33a2b Mon Sep 17 00:00:00 2001 From: Norbert Lange Date: Wed, 3 Dec 2025 17:16:34 +0100 Subject: [PATCH 022/122] fix: ignore -g -s when passwords arent enabled --- src/svr-runopts.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/svr-runopts.c b/src/svr-runopts.c index 5d114f83d..7e39ec6fa 100644 --- a/src/svr-runopts.c +++ b/src/svr-runopts.c @@ -323,6 +323,10 @@ void svr_getopts(int argc, char ** argv) { case 't': svr_opts.multiauthmethod = 1; break; +#else + case 's': + case 'g': + break; #endif case 'h': printhelp(argv[0]); From 8328216418333598486365f794b29e5c4b265271 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Fri, 12 Dec 2025 12:15:49 +0900 Subject: [PATCH 023/122] Ignore more disabled server arguments Ignore "disable" arguments when the feature is compiled out. -j Disable local port forwarding -k Disable remote port forwarding -m Don't display the motd on login --- src/svr-runopts.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/svr-runopts.c b/src/svr-runopts.c index 7e39ec6fa..8034c50c5 100644 --- a/src/svr-runopts.c +++ b/src/svr-runopts.c @@ -253,6 +253,9 @@ void svr_getopts(int argc, char ** argv) { case 'j': svr_opts.nolocaltcp = 1; break; +#else + case 'j': + break; #endif #if DROPBEAR_SVR_REMOTETCPFWD case 'k': @@ -261,6 +264,9 @@ void svr_getopts(int argc, char ** argv) { case 'a': opts.listen_fwd_all = 1; break; +#else + case 'k': + break; #endif #if INETD_MODE case 'i': @@ -289,6 +295,9 @@ void svr_getopts(int argc, char ** argv) { case 'm': svr_opts.domotd = 0; break; +#else + case 'm': + break; #endif case 'w': svr_opts.norootlogin = 1; From d193731630a62482855b450daa1d5a5e13a90125 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Fri, 12 Dec 2025 12:31:40 +0900 Subject: [PATCH 024/122] Restore seteuid for authorized_keys Authorized_keys reading is pre-authentication so should not be modified in the post-auth drop-privilege change. Fixes: e0251be2354e ("Drop privileges after user authentication") --- src/svr-authpubkey.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/svr-authpubkey.c b/src/svr-authpubkey.c index e26b0eefd..94ae728ce 100644 --- a/src/svr-authpubkey.c +++ b/src/svr-authpubkey.c @@ -464,14 +464,12 @@ static int checkpubkey(const char* keyalgo, unsigned int keyalgolen, int ret = DROPBEAR_FAILURE; buffer * line = NULL; int line_num; -#if !DROPBEAR_SVR_DROP_PRIVS uid_t origuid; gid_t origgid; -#endif TRACE(("enter checkpubkey")) -#if !DROPBEAR_SVR_DROP_PRIVS +#if DROPBEAR_SVR_MULTIUSER /* access the file as the authenticating user. */ origuid = getuid(); origgid = getgid(); @@ -492,7 +490,7 @@ static int checkpubkey(const char* keyalgo, unsigned int keyalgolen, TRACE(("checkpubkey: failed opening %s: %s", filename, strerror(errno))) } } -#if !DROPBEAR_SVR_DROP_PRIVS +#if DROPBEAR_SVR_MULTIUSER if ((seteuid(origuid)) < 0 || (setegid(origgid)) < 0) { dropbear_exit("Failed to revert euid"); From e90a88d6fbf7ed5201b66a2154586e12fa02ea21 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Tue, 9 Dec 2025 15:08:17 +0900 Subject: [PATCH 025/122] Require dropped privileges for unix stream forward Otherwise the stream will be created by the root user and other programs may trust the root user via SO_PEERCRED/SO_PASSCRED --- src/sysoptions.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/sysoptions.h b/src/sysoptions.h index 9bdcb0ce2..df690bd52 100644 --- a/src/sysoptions.h +++ b/src/sysoptions.h @@ -447,6 +447,11 @@ #if !DROPBEAR_SVR_MULTIUSER && DROPBEAR_SVR_DROP_PRIVS #error DROPBEAR_SVR_DROP_PRIVS needs DROPBEAR_SVR_MULTIUSER #endif + +#if !(DROPBEAR_SVR_DROP_PRIVS || !DROPBEAR_SVR_MULTIUSER) && DROPBEAR_SVR_LOCALSTREAMFWD +#error DROPBEAR_SVR_LOCALSTREAMFWD requires DROPBEAR_SVR_DROP_PRIVS or !DROPBEAR_SVR_MULTIUSER +#endif + /* Fuzzing expects all key types to be enabled */ #if DROPBEAR_FUZZ #if defined(DROPBEAR_DSS) From 00cfa132fcbf122e863faccdc9a70e04fcfa4c99 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Tue, 9 Dec 2025 22:44:54 +0900 Subject: [PATCH 026/122] Disallow unix stream forwarding for forced command Being able to create a unix stream to a system may bypass forced command restrictions, so disallow that. --- src/auth.h | 2 ++ src/svr-authpubkeyoptions.c | 6 ++++++ src/svr-tcpfwd.c | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/src/auth.h b/src/auth.h index 1145ad797..5eb469015 100644 --- a/src/auth.h +++ b/src/auth.h @@ -49,6 +49,7 @@ int svr_pubkey_allows_agentfwd(void); int svr_pubkey_allows_tcpfwd(void); int svr_pubkey_allows_x11fwd(void); int svr_pubkey_allows_pty(void); +int svr_pubkey_has_forced_command(void); int svr_pubkey_allows_local_tcpfwd(const char *host, unsigned int port); void svr_pubkey_set_forced_command(struct ChanSess *chansess); void svr_pubkey_options_cleanup(void); @@ -59,6 +60,7 @@ int svr_add_pubkey_options(buffer *options_buf, int line_num, const char* filena #define svr_pubkey_allows_tcpfwd() 1 #define svr_pubkey_allows_x11fwd() 1 #define svr_pubkey_allows_pty() 1 +#define svr_pubkey_has_forced_command() 0 static inline int svr_pubkey_allows_local_tcpfwd(const char *host, unsigned int port) { (void)host; (void)port; return 1; } diff --git a/src/svr-authpubkeyoptions.c b/src/svr-authpubkeyoptions.c index df9a7dfc7..2ad32aa51 100644 --- a/src/svr-authpubkeyoptions.c +++ b/src/svr-authpubkeyoptions.c @@ -70,6 +70,12 @@ int svr_pubkey_allows_tcpfwd() { return 1; } +/* Returns 1 if a forced command pubkey option is set */ +int svr_pubkey_has_forced_command(void) { + return ses.authstate.pubkey_options + && ses.authstate.pubkey_options->forced_command; +} + /* Returns 1 if pubkey allows x11 forwarding, * 0 otherwise */ int svr_pubkey_allows_x11fwd() { diff --git a/src/svr-tcpfwd.c b/src/svr-tcpfwd.c index e6902ea99..a57637e5a 100644 --- a/src/svr-tcpfwd.c +++ b/src/svr-tcpfwd.c @@ -346,6 +346,11 @@ static int newstreamlocal(struct Channel * channel) { TRACE(("streamlocal channel %d", channel->index)) + if (svr_opts.forced_command || svr_pubkey_has_forced_command()) { + TRACE(("leave newstreamlocal: no unix forwarding for forced command")) + goto out; + } + if (svr_opts.nolocaltcp || !svr_pubkey_allows_tcpfwd()) { TRACE(("leave newstreamlocal: local unix forwarding disabled")) goto out; From 48a17cff6aa104b8e806ddb2191f83f1024060f1 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Tue, 9 Dec 2025 22:59:19 +0900 Subject: [PATCH 027/122] scp CVE-2019-6111 fix Cherry-pick from OpenSSH portable 391ffc4b9d31 ("upstream: check in scp client that filenames sent during") upstream: check in scp client that filenames sent during remote->local directory copies satisfy the wildcard specified by the user. This checking provides some protection against a malicious server sending unexpected filenames, but it comes at a risk of rejecting wanted files due to differences between client and server wildcard expansion rules. For this reason, this also adds a new -T flag to disable the check. reported by Harry Sintonen fix approach suggested by markus@; has been in snaps for ~1wk courtesy deraadt@ --- src/scp.c | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/scp.c b/src/scp.c index 384f2cb85..bf98986ca 100644 --- a/src/scp.c +++ b/src/scp.c @@ -76,6 +76,8 @@ #include "includes.h" /*RCSID("$OpenBSD: scp.c,v 1.130 2006/01/31 10:35:43 djm Exp $");*/ +#include + #include "atomicio.h" #include "compat.h" #include "scpmisc.h" @@ -291,14 +293,14 @@ void verifydir(char *); uid_t userid; int errs, remin, remout; -int pflag, iamremote, iamrecursive, targetshouldbedirectory; +int Tflag, pflag, iamremote, iamrecursive, targetshouldbedirectory; #define CMDNEEDS 64 char cmd[CMDNEEDS]; /* must hold "rcp -r -p -d\0" */ int response(void); void rsource(char *, struct stat *); -void sink(int, char *[]); +void sink(int, char *[], const char *); void source(int, char *[]); void tolocal(int, char *[]); void toremote(char *, int, char *[]); @@ -325,8 +327,8 @@ main(int argc, char **argv) args.list = NULL; addargs(&args, "%s", ssh_program); - fflag = tflag = 0; - while ((ch = getopt(argc, argv, "dfl:prtvBCc:i:P:q1246S:o:F:")) != -1) + fflag = Tflag = tflag = 0; + while ((ch = getopt(argc, argv, "dfl:prtTvBCc:i:P:q1246S:o:F:")) != -1) switch (ch) { /* User-visible flags. */ case '1': @@ -389,9 +391,12 @@ main(int argc, char **argv) setmode(0, O_BINARY); #endif break; + case 'T': + Tflag = 1; + break; default: usage(); - } + } argc -= optind; argv += optind; @@ -409,7 +414,7 @@ main(int argc, char **argv) } if (tflag) { /* Receive data. */ - sink(argc, argv); + sink(argc, argv, NULL); exit(errs != 0); } if (argc < 2) @@ -589,7 +594,7 @@ tolocal(int argc, char **argv) continue; } xfree(bp); - sink(1, argv + argc - 1); + sink(1, argv + argc - 1, src); (void) close(remin); remin = remout = -1; } @@ -822,7 +827,7 @@ bwlimit(int amount) } void -sink(int argc, char **argv) +sink(int argc, char **argv, const char *src) { static BUF buffer; struct stat stb; @@ -836,6 +841,7 @@ sink(int argc, char **argv) off_t size, statbytes; int setimes, targisdir, wrerrno = 0; char ch, *cp, *np, *targ, *why, *vect[1], buf[2048]; + char *src_copy = NULL, *restrict_pattern = NULL; struct timeval tv[2]; #define atime tv[0] @@ -857,6 +863,17 @@ sink(int argc, char **argv) (void) atomicio(vwrite, remout, "", 1); if (stat(targ, &stb) == 0 && S_ISDIR(stb.st_mode)) targisdir = 1; + if (src != NULL && !iamrecursive && !Tflag) { + /* + * Prepare to try to restrict incoming filenames to match + * the requested destination file glob. + */ + if ((src_copy = strdup(src)) == NULL) + fatal("strdup failed"); + if ((restrict_pattern = strrchr(src_copy, '/')) != NULL) { + *restrict_pattern++ = '\0'; + } + } for (first = 1;; first = 0) { cp = buf; if (atomicio(read, remin, cp, 1) != 1) @@ -939,6 +956,9 @@ sink(int argc, char **argv) run_err("error: unexpected filename: %s", cp); exit(1); } + if (restrict_pattern != NULL && + fnmatch(restrict_pattern, cp, 0) != 0) + SCREWUP("filename does not match request"); if (targisdir) { static char *namebuf = NULL; static size_t cursize = 0; @@ -977,7 +997,7 @@ sink(int argc, char **argv) goto bad; } vect[0] = xstrdup(np); - sink(1, vect); + sink(1, vect, src); if (setimes) { setimes = 0; if (utimes(vect[0], tv) < 0) From f1ac015585343170647eecf4fedfc169609a8e5b Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Wed, 10 Dec 2025 00:07:55 +0900 Subject: [PATCH 028/122] Fix release script for setresgid Script doesn't use config.h so define it manually --- release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release.sh b/release.sh index e03d3d88b..37690fb1f 100755 --- a/release.sh +++ b/release.sh @@ -9,7 +9,7 @@ else TESTREL=0 fi -VERSION=$(echo '#include "src/default_options.h"\n#include "src/sysoptions.h"\necho DROPBEAR_VERSION' | cpp -DHAVE_CRYPT - | sh) +VERSION=$(echo '#include "src/default_options.h"\n#include "src/sysoptions.h"\necho DROPBEAR_VERSION' | cpp -DHAVE_CRYPT -DHAVE_SETRESGID - | sh) if [ $TESTREL -eq 1 ]; then echo Making test tarball for "$VERSION" ... From 179de98f7b9584a309ffc48e39c61da940760740 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Tue, 9 Dec 2025 23:56:21 +0900 Subject: [PATCH 029/122] 2025.89 changelog --- CHANGES | 57 ++++++++++++++++++++++++++++++++++++++++++++++++ debian/changelog | 6 +++++ src/sysoptions.h | 2 +- 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 3ad4c4da8..a594d8989 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,60 @@ +2025.89 - 16 December 2025 + +- Security: Avoid privilege escalation via unix stream forwarding in Dropbear + server. Other programs on a system may authenticate unix sockets via + SO_PEERCRED, which would be root user for Dropbear forwarded connections, + allowing root privilege escalation. + Reported by Turistu, and thanks for advice on the fix. + This is tracked as CVE-2025-14282, and affects 2024.84 to 2025.88. + + It is fixed by dropping privileges of the dropbear process after + authentication. Unix stream sockets are now disallowed when a + forced command is used, either with authorized_key restrictions or + "dropbear -c command". + + In previous affected releases running with "dropbear -j" (will also disable + TCP fowarding) or building with localoptions.h/distrooptions.h + "#define DROPBEAR_SVR_LOCALSTREAMFWD 0" is a mitigation. + +- Security: Include scp fix for CVE-2019-6111. This allowed + a malicious server to overwrite arbitrary local files. + The missing fix was reported by Ashish Kunwar. + +- Server dropping privileges post-auth is enabled by default. This requires + setresgid() support, so some platforms such as netbsd or macos will have to + disable DROPBEAR_SVR_DROP_PRIVS in localoptions.h. Unix stream forwarding is + not available if DROPBEAR_SVR_DROP_PRIVS is disabled. + + Remote server TCP socket forwarding will now use OS privileged port + restrictions rather than having a fixed "allow >=1024 for non-root" rule. + + A future release may implement privilege dropping for netbsd/macos. + +- Fix a regression in 2025.87 when RSA and DSS are not built. This would lead + to a crash at startup with bad_bufptr(). + Reported by Dani Schmitt and Sebastian Priebe. + +- Don't limit channel window to 500MB. That is could cause stuck connections + if peers advise a large window and don't send an increment within 500MB. + Affects SSH.NET https://github.com/sshnet/SSH.NET/issues/1671 + Reported by Rob Hague. + +- Ignore -g -s when passwords arent enabled. Patch from Norbert Lange. + Ignore -m (disable MOTD), -j/-k (tcp forwarding) when not enabled. + +- Report SIGBUS and SIGTRAP signals. Patch from Loïc Mangeonjean. + +- Fix incorrect server auth delay. Was meant to be 250-350ms, it was actually + 150-350ms or possibly negative (zero). Reported by pickaxprograms. + +- Fix building without public key options. Thanks to Konstantin Demin + +- Fix building with proxycmd but without netcat. Thanks to Konstantin Demin + +- Fix incorrect path documentation for distrooptions, thanks to Todd Zullinger + +- Fix SO_REUSEADDR for TCP tests, reported by vt-alt. + 2025.88 - 7 May 2025 - Security: Don't allow dbclient hostname arguments to be interpreted diff --git a/debian/changelog b/debian/changelog index bce63c7c8..6135d6b1c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +dropbear (2025.89-0.1) unstable; urgency=low + + * New upstream release. + + -- Matt Johnston Tue, 16 Dec 2025 22:51:57 +0800 + dropbear (2025.88-0.1) unstable; urgency=low * New upstream release. diff --git a/src/sysoptions.h b/src/sysoptions.h index df690bd52..a13548d28 100644 --- a/src/sysoptions.h +++ b/src/sysoptions.h @@ -4,7 +4,7 @@ *******************************************************************/ #ifndef DROPBEAR_VERSION -#define DROPBEAR_VERSION "2025.88" +#define DROPBEAR_VERSION "2025.89" #endif /* IDENT_VERSION_PART is the optional part after "SSH-2.0-dropbear". Refer to RFC4253 for requirements. */ From bfb40653abfe524eba5f449f7c1bd43b85b5ec35 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Tue, 16 Dec 2025 22:12:07 +0800 Subject: [PATCH 030/122] Fix CI builds, disable unix socket forwarding macos doesn't currently support DROPBEAR_SVR_DROP_PRIVS so can't use unix socket forwarding either. Also disable it for nondefault builds. --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4fe41bd43..0458fec68 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -81,6 +81,7 @@ jobs: # macos doesn't have setresgid localoptions: | #define DROPBEAR_SVR_DROP_PRIVS 0 + #define DROPBEAR_SVR_LOCALSTREAMFWD 0 - name: macos 15 os: macos-15 @@ -96,6 +97,7 @@ jobs: # macos doesn't have setresgid localoptions: | #define DROPBEAR_SVR_DROP_PRIVS 0 + #define DROPBEAR_SVR_LOCALSTREAMFWD 0 # Check that debug code doesn't bitrot - name: DEBUG_TRACE @@ -235,6 +237,7 @@ jobs: sed -i "s/DEFAULT_IDLE_TIMEOUT 1/DEFAULT_IDLE_TIMEOUT 99/" localoptions.h # DROPBEAR_SVR_DROP_PRIVS is on by default, turn it off echo "#define DROPBEAR_SVR_DROP_PRIVS 0" >> localoptions.h + echo "#define DROPBEAR_SVR_LOCALSTREAMFWD 0" >> localoptions.h - name: make run: | From 8263b6629c087fb578dedb886ec110b91249eccc Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Tue, 16 Dec 2025 22:21:11 +0800 Subject: [PATCH 031/122] Avoid unused declaration with unix socket disabled --- src/svr-tcpfwd.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/svr-tcpfwd.c b/src/svr-tcpfwd.c index a57637e5a..2873fafca 100644 --- a/src/svr-tcpfwd.c +++ b/src/svr-tcpfwd.c @@ -56,7 +56,9 @@ void recv_msg_global_request_remotetcp() { static int svr_cancelremotetcp(void); static int svr_remotetcpreq(int *allocated_listen_port); static int newtcpdirect(struct Channel * channel); +#if DROPBEAR_SVR_LOCALSTREAMFWD static int newstreamlocal(struct Channel * channel); +#endif #if DROPBEAR_SVR_REMOTETCPFWD static const struct ChanType svr_chan_tcpremote = { From d7622f846f51e0b6dc3c2e2ad3edc1f2138b8f0e Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Wed, 17 Dec 2025 22:44:49 +0800 Subject: [PATCH 032/122] Fix changelog typo "fowarding" --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index a594d8989..aedd1b338 100644 --- a/CHANGES +++ b/CHANGES @@ -13,7 +13,7 @@ "dropbear -c command". In previous affected releases running with "dropbear -j" (will also disable - TCP fowarding) or building with localoptions.h/distrooptions.h + TCP forwarding) or building with localoptions.h/distrooptions.h "#define DROPBEAR_SVR_LOCALSTREAMFWD 0" is a mitigation. - Security: Include scp fix for CVE-2019-6111. This allowed From 4214d7c3954e85bc7a6e83bdc4407b3dc003453d Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 5 Jan 2026 13:25:47 +0800 Subject: [PATCH 033/122] Update configure.ac minimum to 2.70 Previous versions don't search src/ for install.sh as a AC_CONFIG_AUX_DIR path. Fixes #334 --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 0e7e331b3..bd5dc0250 100644 --- a/configure.ac +++ b/configure.ac @@ -5,7 +5,7 @@ # of the platform checks have been taken straight from OpenSSH's configure.ac # Huge thanks to them for dealing with the horrible platform-specifics :) -AC_PREREQ([2.59]) +AC_PREREQ([2.70]) AC_INIT ORIGCFLAGS="$CFLAGS" From b8d65a98e17e8139375be0381da9a42ae81e21ba Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Fri, 16 Jan 2026 13:20:07 +0800 Subject: [PATCH 034/122] More manpage documentation for forced commands --- manpages/dropbear.8 | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/manpages/dropbear.8 b/manpages/dropbear.8 index 72be0aefb..b194bbe11 100644 --- a/manpages/dropbear.8 +++ b/manpages/dropbear.8 @@ -112,9 +112,18 @@ By default Dropbear will send network traffic with the \fBAF21\fR setting for Qo Set the number of authentication attempts allowed per connection. If unspecified the default is 10 (MAX_AUTH_TRIES) .TP .B \-c \fIforced_command -Disregard the command provided by the user and always run \fIforced_command\fR. This also +Disregard the command/shell provided by the user and always run \fIforced_command\fR. This also overrides any authorized_keys command= option. The original command is saved in the SSH_ORIGINAL_COMMAND environment variable (see below). + +The forced command is started using the user's normal shell, the same as +occurs for normal commands. + +Unix stream forwarding is disabled when a forced command is set, since that +may allow a bypass of the forced command. Other SSH features such as TCP forwarding +are still allowed by default - it may be desirable to disable those using \fI-j\fR or \fI-k\fR +options when using forced command. + .TP .B \-D \fIauthorized_keys_dir Specify the directory to use for authorized_keys files. The default is ~/.ssh , paths with @@ -170,11 +179,14 @@ IP addresses. .TP .B command=\fR"\fIforced_command\fR" Disregard the command provided by the user and always run \fIforced_command\fR. -The -c command line option overrides this. +The -c command line option overrides this. See -c documentation for behaviour of +forced commands. -The authorized_keys file and its containing ~/.ssh directory must only be -writable by the user, otherwise Dropbear will not allow a login using public -key authentication. +When using +.B command= +it is often desirable to also use +.B restrict +option to avoid other SSH features that may allow additional access. .TP Host Key Files From 3c9f396a9339eeb65b16c8153345d09561ae4072 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Fri, 16 Jan 2026 13:20:33 +0800 Subject: [PATCH 035/122] Improve authorized_keys manpage formatting --- manpages/dropbear.8 | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/manpages/dropbear.8 b/manpages/dropbear.8 index b194bbe11..776a9708d 100644 --- a/manpages/dropbear.8 +++ b/manpages/dropbear.8 @@ -137,9 +137,14 @@ Print the version .TP Authorized Keys -~/.ssh/authorized_keys can be set up to allow remote login with a RSA, -ECDSA, Ed25519 or DSS -key. Each line is of the form +~/.ssh/authorized_keys can be set up to allow remote login with a +Ed25519, RSA, or ECDSA key. +The authorized_keys file and its containing ~/.ssh directory must only be +writable by the user, otherwise Dropbear will not allow a login using public +key authentication. + + +Each line is of the form .TP [restrictions] ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIgAsp... [comment] @@ -147,6 +152,8 @@ and can be extracted from a Dropbear private host key with "dropbearkey -y". Thi Restrictions are comma separated, with double quotes around spaces in arguments. Available restrictions are: +.RS + .TP .B no-port-forwarding Don't allow port forwarding for this connection, including unix streams. @@ -188,6 +195,10 @@ it is often desirable to also use .B restrict option to avoid other SSH features that may allow additional access. + + +.RE + .TP Host Key Files From 856e7e7a4a914f4779c2b0724b5140205cf80253 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Fri, 16 Jan 2026 13:24:19 +0800 Subject: [PATCH 036/122] Fix svr-authpubkey mixed tabs and spaces --- src/svr-authpubkey.c | 74 ++++++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/src/svr-authpubkey.c b/src/svr-authpubkey.c index 94ae728ce..c8b7585ca 100644 --- a/src/svr-authpubkey.c +++ b/src/svr-authpubkey.c @@ -127,44 +127,44 @@ void svr_auth_pubkey(int valid_user) { keyalgo = signkey_name_from_type(keytype, &keyalgolen); #if DROPBEAR_PLUGIN - if (svr_ses.plugin_instance != NULL) { - char *options_buf; - if (svr_ses.plugin_instance->checkpubkey( - svr_ses.plugin_instance, - &ses.plugin_session, - keyalgo, - keyalgolen, - keyblob, - keybloblen, - ses.authstate.username) == DROPBEAR_SUCCESS) { - /* Success */ - auth_failure = 0; - - /* Options provided? */ - options_buf = ses.plugin_session->get_options(ses.plugin_session); - if (options_buf) { - struct buf temp_buf = { - .data = (unsigned char *)options_buf, - .len = strlen(options_buf), - .pos = 0, - .size = 0 - }; - int ret = svr_add_pubkey_options(&temp_buf, 0, "N/A"); - if (ret == DROPBEAR_FAILURE) { - /* Fail immediately as the plugin provided wrong options */ - send_msg_userauth_failure(0, 0); - goto out; - } + if (svr_ses.plugin_instance != NULL) { + char *options_buf; + if (svr_ses.plugin_instance->checkpubkey( + svr_ses.plugin_instance, + &ses.plugin_session, + keyalgo, + keyalgolen, + keyblob, + keybloblen, + ses.authstate.username) == DROPBEAR_SUCCESS) { + /* Success */ + auth_failure = 0; + + /* Options provided? */ + options_buf = ses.plugin_session->get_options(ses.plugin_session); + if (options_buf) { + struct buf temp_buf = { + .data = (unsigned char *)options_buf, + .len = strlen(options_buf), + .pos = 0, + .size = 0 + }; + int ret = svr_add_pubkey_options(&temp_buf, 0, "N/A"); + if (ret == DROPBEAR_FAILURE) { + /* Fail immediately as the plugin provided wrong options */ + send_msg_userauth_failure(0, 0); + goto out; } } } + } #endif /* check if the key is valid */ - if (auth_failure) { - auth_failure = checkpubkey(keyalgo, keyalgolen, keyblob, keybloblen) == DROPBEAR_FAILURE; - } + if (auth_failure) { + auth_failure = checkpubkey(keyalgo, keyalgolen, keyblob, keybloblen) == DROPBEAR_FAILURE; + } - if (auth_failure) { + if (auth_failure) { send_msg_userauth_failure(0, 0); goto out; } @@ -234,10 +234,10 @@ void svr_auth_pubkey(int valid_user) { send_msg_userauth_success(); } #if DROPBEAR_PLUGIN - if ((ses.plugin_session != NULL) && (svr_ses.plugin_instance->auth_success != NULL)) { - /* Was authenticated through the external plugin. tell plugin that signature verification was ok */ - svr_ses.plugin_instance->auth_success(ses.plugin_session); - } + if ((ses.plugin_session != NULL) && (svr_ses.plugin_instance->auth_success != NULL)) { + /* Was authenticated through the external plugin. tell plugin that signature verification was ok */ + svr_ses.plugin_instance->auth_success(ses.plugin_session); + } #endif } else { dropbear_log(LOG_WARNING, @@ -396,7 +396,7 @@ static int checkpubkey_line(buffer* line, int line_num, const char* filename, if (infolen > 0) { info_str = m_malloc(infolen + 1); buf_setpos(line, infopos); - strncpy(info_str, buf_getptr(line, infolen), infolen); + strncpy(info_str, buf_getptr(line, infolen), infolen); } /* truncate to base64 data length */ From 8a8b440b111b7fe508d80d955852d741672ef68d Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Fri, 16 Jan 2026 18:04:27 +1100 Subject: [PATCH 037/122] Fix off by one in server fail count Setting MAX_AUTH_TRIES (or -T) to 0 is a valid configuration - it requires the first authentication attempt to succeed. --- src/svr-auth.c | 2 +- src/svr-runopts.c | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/svr-auth.c b/src/svr-auth.c index de0145824..f3fecd780 100644 --- a/src/svr-auth.c +++ b/src/svr-auth.c @@ -427,7 +427,7 @@ void send_msg_userauth_failure(int partial, int incrfail) { ses.authstate.failcount++; } - if (ses.authstate.failcount >= svr_opts.maxauthtries) { + if (ses.authstate.failcount > svr_opts.maxauthtries) { char * userstr; /* XXX - send disconnect ? */ TRACE(("Max auth tries reached, exiting")) diff --git a/src/svr-runopts.c b/src/svr-runopts.c index 8034c50c5..8aaeab026 100644 --- a/src/svr-runopts.c +++ b/src/svr-runopts.c @@ -426,8 +426,7 @@ void svr_getopts(int argc, char ** argv) { if (maxauthtries_arg) { unsigned int val = 0; - if (m_str_to_uint(maxauthtries_arg, &val) == DROPBEAR_FAILURE - || val == 0) { + if (m_str_to_uint(maxauthtries_arg, &val) == DROPBEAR_FAILURE) { dropbear_exit("Bad maxauthtries '%s'", maxauthtries_arg); } svr_opts.maxauthtries = val; From db0d3fd0a9e9efa5c4338d88bcdcd0cefd2b2c72 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Fri, 16 Jan 2026 17:53:28 +1100 Subject: [PATCH 038/122] Limit server number of public key queries A ssh client queries a server whether a public key will be accepted prior to performing a real signature. Previously there was no limit on the number of queries. Public key queries are a user enumeration/privacy concern if an attacker iterates with known public keys such as those published by github (mapping to github usernames). If total privacy is required, the proper solution is to use separate authorized_keys compared to publicly used ones (or avoid services that publish public keys). As a mitigation for reused public keys, Dropbear now limits to 15 public keys attempts per session. Public key queries start being counted as failed authentication attempts after (MAX_AUTH_TRIES - MAX_PUBKEY_QUERIES). This provides a rate limit on public key queries. Reported by HD Moore, details are in the SSHamble presentation. --- src/auth.h | 3 +++ src/default_options.h | 18 ++++++++++++++++++ src/svr-authpubkey.c | 11 ++++++++++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/auth.h b/src/auth.h index 5eb469015..bfb3e49d6 100644 --- a/src/auth.h +++ b/src/auth.h @@ -127,6 +127,9 @@ struct AuthState { struct timespec auth_starttime; /* Server only, time of receiving current SSH_MSG_USERAUTH_REQUEST */ + /* Count of public keys attempted, limited by MAX_PUBKEY_QUERIES */ + unsigned int serv_pubkey_query_count; + /* These are only used for the server */ uid_t pw_uid; gid_t pw_gid; diff --git a/src/default_options.h b/src/default_options.h index 705da740b..98cd0d543 100644 --- a/src/default_options.h +++ b/src/default_options.h @@ -303,6 +303,24 @@ group1 in Dropbear server too */ /* -T server option overrides */ #define MAX_AUTH_TRIES 10 +/* Maximum number of public key queries per session. + * Public key queries + * aren't a risk for brute forcing authentication, but can be a + * user enumeration/privacy concern if an attacker attempts + * to iterate known public keys such as those published by github. + * + * This limit has a trade-off. Having a smaller limit reduces the + * number of legitimate public keys that can be presented + * by a client/ssh agent. + * Assuming a 100ms session establishment time, + * MAX_UNAUTH_CLIENTS * MAX_PUBKEY_QUERIES / 0.1 = 750 queries/sec. + * That is still a a risk against a single host, + * but this limit may deter internet-wide scanning. + * + * If -T argument or MAX_AUTH_TRIES is larger that will be used instead. +*/ +#define MAX_PUBKEY_QUERIES 15 + /* Change server process to user privileges after authentication. */ #ifndef DROPBEAR_SVR_DROP_PRIVS /* Default is enabled. Should only be disabled if platforms are incompatible */ diff --git a/src/svr-authpubkey.c b/src/svr-authpubkey.c index c8b7585ca..9bb8db246 100644 --- a/src/svr-authpubkey.c +++ b/src/svr-authpubkey.c @@ -165,7 +165,16 @@ void svr_auth_pubkey(int valid_user) { } if (auth_failure) { - send_msg_userauth_failure(0, 0); + /* MAX_PUBKEY_QUERIES allows a greater limit of pubkey queries + * than the standard maxauthtries. + * Start counting failures (incrfail) only when it's reaching + * the limit. + */ + unsigned int free_query_limit = 0; + MAX(0, (int)svr_opts.maxauthtries - MAX_PUBKEY_QUERIES); + int incrfail = ses.authstate.serv_pubkey_query_count > free_query_limit; + send_msg_userauth_failure(0, incrfail); + ses.authstate.serv_pubkey_query_count++; goto out; } From 40714c76536b5571be9a801b91428592cedc8830 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Sun, 18 Jan 2026 21:56:03 +1100 Subject: [PATCH 039/122] server: Disallow signal with forced command When a forced command is set it is usually to limit functionality available to a client. Signals sent to the forced command process may have unexpected handling, so disallow them. --- src/svr-chansession.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/svr-chansession.c b/src/svr-chansession.c index 11205f3b1..ffc8a7fc1 100644 --- a/src/svr-chansession.c +++ b/src/svr-chansession.c @@ -438,6 +438,11 @@ static int sessionsignal(const struct ChanSess *chansess) { return DROPBEAR_FAILURE; } + if (svr_opts.forced_command || svr_pubkey_has_forced_command()) { + TRACE(("disallowed signal for forced_command")); + return DROPBEAR_FAILURE; + } + signame = buf_getstring(ses.payload, NULL); for (i = 0; signames[i].name != NULL; i++) { From 490a48e46bd5509ba9a9214ce63e93b72c342d1f Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Sun, 18 Jan 2026 21:58:02 +1100 Subject: [PATCH 040/122] server: Disallow signal without dropping privilege Signals were previously sent to the process with server privileges (usually root). That may have unexpected behaviour, so disallow it. Since 2025.89 with drop privileges the signal is now sent as the authenticated user instead. If needed, switching the euid could be implemented, but for the time being signals are just rejected if dropping privileges isn't supported. --- src/svr-chansession.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/svr-chansession.c b/src/svr-chansession.c index ffc8a7fc1..16d71113d 100644 --- a/src/svr-chansession.c +++ b/src/svr-chansession.c @@ -443,6 +443,11 @@ static int sessionsignal(const struct ChanSess *chansess) { return DROPBEAR_FAILURE; } + if (DROPBEAR_SVR_MULTIUSER && !DROPBEAR_SVR_DROP_PRIVS) { + TRACE(("disallow signal without drop privs")); + return DROPBEAR_FAILURE; + } + signame = buf_getstring(ses.payload, NULL); for (i = 0; signames[i].name != NULL; i++) { From ec7828dce092d2a5982a907af4a5f592470a4ff4 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Wed, 21 Jan 2026 16:41:56 +1100 Subject: [PATCH 041/122] client: check isatty when sending PTY request Otherwise can fail with ./dbclient -i .ssh/id root@x.x.x.x << EOF ls EOF ./dbclient: Failed reading termmodes Fixes #385 --- src/cli-runopts.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/cli-runopts.c b/src/cli-runopts.c index a21b7a265..501dd0f7d 100644 --- a/src/cli-runopts.c +++ b/src/cli-runopts.c @@ -472,7 +472,12 @@ void cli_getopts(int argc, char ** argv) { * there's a command, but we do otherwise */ if (cli_opts.wantpty == 9) { if (cli_opts.cmd == NULL) { - cli_opts.wantpty = 1; + if (isatty(STDIN_FILENO)) { + cli_opts.wantpty = 1; + } else { + TRACE(("Not a TTY")); + cli_opts.wantpty = 0; + } } else { cli_opts.wantpty = 0; } From 9145082b00b37da57b0453c968896c3373ab6f1b Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Wed, 4 Feb 2026 08:51:25 +0800 Subject: [PATCH 042/122] distclean config.h not src/config.h Generated config.h wasn't moved to src. Fixes: e49f2012adb3 ("Move some autoconf files to src/") --- DEVELOPING.md | 2 +- Makefile.in | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DEVELOPING.md b/DEVELOPING.md index b6d3df8a2..5ab8c73a3 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -18,7 +18,7 @@ Following are generated files in the format `: ()` ``` - configure: autoconf(configure.ac) - src/config.h.in: autoheader(configure.ac) -- src/config.h: configure(src/config.h.in) +- config.h: configure(src/config.h.in) - Makefile: configure(Makefile.in) - default_options_guard.h: make(default_options.h) ``` diff --git a/Makefile.in b/Makefile.in index fe63a80d9..137ef7618 100644 --- a/Makefile.in +++ b/Makefile.in @@ -279,7 +279,7 @@ thisclean: $(OBJ_DIR)/* distclean: clean tidy - -rm -f src/config.h config.status config.log + -rm -f config.h config.status config.log -rm -f Makefile test/Makefile -rm -f default_options_guard.h From 75f699bfe2c234418056776c4d9f651a07a76de6 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Wed, 4 Feb 2026 08:53:55 +0800 Subject: [PATCH 043/122] distclean also remove libtom*/Makefile --- Makefile.in | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile.in b/Makefile.in index 137ef7618..eabc7e342 100644 --- a/Makefile.in +++ b/Makefile.in @@ -282,6 +282,7 @@ distclean: clean tidy -rm -f config.h config.status config.log -rm -f Makefile test/Makefile -rm -f default_options_guard.h + -rm -f libtomcrypt/Makefile libtommath/Makefile tidy: -rm -f *~ *.gcov */*~ From 578c834eb9ab4cc46863536784e753db1695755f Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Tue, 24 Feb 2026 11:27:45 +0800 Subject: [PATCH 044/122] Add S < L check to curve25519 This is required by rfc8032 and avoids signature malleability (S' = S+L would also be accepted). That does not have any security impact on the way it is used in SSH protocol. The check was not present in original tweetnacl. Fixes #406 --- src/curve25519.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/curve25519.c b/src/curve25519.c index aa16434db..f51992eb4 100644 --- a/src/curve25519.c +++ b/src/curve25519.c @@ -426,6 +426,22 @@ void dropbear_ed25519_sign(const u8 *m,u32 mlen,u8 *s,u32 *slen,const u8 *sk, co } #if DROPBEAR_SIGNKEY_VERIFY + +/* Return 0 if S < L, -1 otherwise. + * Only used during verify so timing side-channel is OK */ +static int s_lt_l(const u8 *s) { + int i; + for (i = 31; i >= 0; i--) { + if (s[i] < L[i]) { + return 0; + } + if (s[i] > L[i]) { + return -1; + } + } + return -1; +} + static int unpackneg(gf r[4],const u8 p[32]) { gf t, chk, num, den, den2, den4, den6; @@ -470,6 +486,10 @@ int dropbear_ed25519_verify(const u8 *m,u32 mlen,const u8 *s,u32 slen,const u8 * if (slen < 64) return -1; + if (s_lt_l(s + 32) == -1) { + return -1; + } + if (unpackneg(q,pk)) return -1; sha512_init(&hs); From 58dafa6e60f4f97e2b574f43f5c478c02cbf9f02 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 16 Mar 2026 23:00:23 +0800 Subject: [PATCH 045/122] Check y < p in curve25519 This is required by rfc8032, otherwise a non-canonical public key y' = y+p could also validate. That would not pose any security concern for SSH protocol given the public key is compared against a fixed value during verification, so modification would fail. Fixes #408 --- src/curve25519.c | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/curve25519.c b/src/curve25519.c index f51992eb4..df9ca6ba4 100644 --- a/src/curve25519.c +++ b/src/curve25519.c @@ -54,7 +54,9 @@ static const gf #if DROPBEAR_SIGNKEY_VERIFY static const gf D = {0x78a3, 0x1359, 0x4dca, 0x75eb, 0xd8ab, 0x4141, 0x0a4d, 0x0070, 0xe898, 0x7779, 0x4079, 0x8cc7, 0xfe73, 0x2b6f, 0x6cee, 0x5203}, - I = {0xa0b0, 0x4a0e, 0x1b27, 0xc4ee, 0xe478, 0xad2f, 0x1806, 0x2f43, 0xd7a7, 0x3dfb, 0x0099, 0x2b4d, 0xdf0b, 0x4fc1, 0x2480, 0x2b83}; + I = {0xa0b0, 0x4a0e, 0x1b27, 0xc4ee, 0xe478, 0xad2f, 0x1806, 0x2f43, 0xd7a7, 0x3dfb, 0x0099, 0x2b4d, 0xdf0b, 0x4fc1, 0x2480, 0x2b83}, + /* p = 2^255 - 19 */ + field_prime = {0xffed, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0x7fff}; #endif /* DROPBEAR_SIGNKEY_VERIFY */ #endif /* DROPBEAR_ED25519 */ @@ -442,11 +444,33 @@ static int s_lt_l(const u8 *s) { return -1; } +/* Return 0 if y < p, -1 otherwise. + * field prime p = 2^255 - 19 + * Only used during verify so timing side-channel is OK */ +static int y_lt_p(const gf y) { + int i; + for (i = 15; i >= 0; i--) { + if (y[i] < field_prime[i]) { + return 0; + } + if (y[i] > field_prime[i]) { + return -1; + } + } + return -1; +} + static int unpackneg(gf r[4],const u8 p[32]) { gf t, chk, num, den, den2, den4, den6; set25519(r[2],gf1); unpack25519(r[1],p); + + /* Check that pubkey y < 2^255 - 19 */ + if (y_lt_p(r[1])) { + return -1; + } + S(num,r[1]); M(den,num,D); Z(num,num,r[2]); From 7d8ddaac351cc90deba9ddd09d9827b6f2ed4f04 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Wed, 25 Mar 2026 22:48:25 +0800 Subject: [PATCH 046/122] Fix incorrect ssh-connection check This would also accept other strings of the same length or prefix. Fixes #397 --- src/svr-auth.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/svr-auth.c b/src/svr-auth.c index f3fecd780..0975dc06e 100644 --- a/src/svr-auth.c +++ b/src/svr-auth.c @@ -99,11 +99,9 @@ void recv_msg_userauth_request() { methodname = buf_getstring(ses.payload, &methodlen); /* only handle 'ssh-connection' currently */ - if (servicelen != SSH_SERVICE_CONNECTION_LEN + if (!(servicelen == SSH_SERVICE_CONNECTION_LEN && (strncmp(servicename, SSH_SERVICE_CONNECTION, - SSH_SERVICE_CONNECTION_LEN) != 0)) { - - /* TODO - disconnect here */ + SSH_SERVICE_CONNECTION_LEN) == 0))) { m_free(username); m_free(servicename); m_free(methodname); From 067fd3846c425d1998c7c94d3feafe1f26b514eb Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Wed, 8 Apr 2026 20:51:47 +0800 Subject: [PATCH 047/122] Fix incorrect index closing listeners This could read beyond socks[] and close an arbitrary descriptor. The codepath runs when creating TCP listeners for forwarding (client or server). --- src/listener.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/listener.c b/src/listener.c index 4c60589a6..f85d1577c 100644 --- a/src/listener.c +++ b/src/listener.c @@ -95,7 +95,7 @@ struct Listener* new_listener(const int socks[], unsigned int nsocks, if (ses.listensize > MAX_LISTENERS) { TRACE(("leave newlistener: too many already")) for (j = 0; j < nsocks; j++) { - close(socks[i]); + close(socks[j]); } return NULL; } From f8cccdd0c54eb68b918a1c6531cb2e9d86718c36 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 13 Apr 2026 23:28:39 +0800 Subject: [PATCH 048/122] tests: Detect segfault in server processes A segfault in a server child process wouldn't have been detected. This detection is simple but seems adequate. --- test/test_dropbear.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test/test_dropbear.py b/test/test_dropbear.py index 621c4c46b..9ba51d22e 100644 --- a/test/test_dropbear.py +++ b/test/test_dropbear.py @@ -38,8 +38,15 @@ def dropbear(request): yield p p.terminate() print("Terminated dropbear. Flushing output:") - for l in p.stderr: - print(l.rstrip()) + lines = [l.rstrip() for l in p.stderr] + for l in lines: + print(l) + + for l in lines: + # Crude segfault detection to catch segfaults in server + # child processes. + assert "Aiee, segfault" not in l + print("Done") def dbclient(request, *args, **kwargs): From e43a3a633f71355442487c13448b4c7ea633a934 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 13 Apr 2026 23:00:25 +0800 Subject: [PATCH 049/122] Add ListenerType enum This has no functional effect, but improves the innacurate comment on the Listener "type" field. --- src/listener.c | 4 ++-- src/listener.h | 15 ++++++++++----- src/svr-agentfwd.c | 5 +++-- src/svr-tcpfwd.c | 2 +- src/svr-x11fwd.c | 3 ++- src/tcp-accept.c | 2 +- src/tcpfwd.h | 3 --- 7 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/listener.c b/src/listener.c index f85d1577c..c06d4348f 100644 --- a/src/listener.c +++ b/src/listener.c @@ -77,7 +77,7 @@ void handle_listeners(const fd_set * readfds) { /* acceptor(int fd, void* typedata) is a function to accept connections, * cleanup(void* typedata) happens when cleaning up */ struct Listener* new_listener(const int socks[], unsigned int nsocks, - int type, void* typedata, + enum ListenerType type, void* typedata, void (*acceptor)(const struct Listener* listener, int sock), void (*cleanup)(const struct Listener*)) { @@ -132,7 +132,7 @@ struct Listener* new_listener(const int socks[], unsigned int nsocks, /* Return the first listener which matches the type-specific comparison * function. Particularly needed for global requests, like tcp */ -struct Listener * get_listener(int type, const void* typedata, +struct Listener * get_listener(enum ListenerType type, const void* typedata, int (*match)(const void*, const void*)) { unsigned int i; diff --git a/src/listener.h b/src/listener.h index 4a7f5ffde..5fae6a80a 100644 --- a/src/listener.h +++ b/src/listener.h @@ -28,6 +28,13 @@ #define MAX_LISTENERS 20 #define LISTENER_EXTEND_SIZE 1 +/* X11 and Agent use default. TCP has a specific type since + it can be cancelled, so get_listener() needs to match it. */ +enum ListenerType { + LISTENER_TYPE_DEFAULT, + LISTENER_TYPE_TCPFORWARDED, +}; + struct Listener { int socks[DROPBEAR_MAX_SOCKS]; @@ -38,9 +45,7 @@ struct Listener { void (*acceptor)(const struct Listener*, int sock); void (*cleanup)(const struct Listener*); - int type; /* CHANNEL_ID_X11, CHANNEL_ID_AGENT, - CHANNEL_ID_TCPDIRECT (for clients), - CHANNEL_ID_TCPFORWARDED (for servers) */ + enum ListenerType type; void *typedata; @@ -51,11 +56,11 @@ void handle_listeners(const fd_set * readfds); void set_listener_fds(fd_set * readfds); struct Listener* new_listener(const int socks[], unsigned int nsocks, - int type, void* typedata, + enum ListenerType type, void* typedata, void (*acceptor)(const struct Listener* listener, int sock), void (*cleanup)(const struct Listener*)); -struct Listener * get_listener(int type, const void* typedata, +struct Listener * get_listener(enum ListenerType type, const void* typedata, int (*match)(const void*, const void*)); void remove_listener(struct Listener* listener); diff --git a/src/svr-agentfwd.c b/src/svr-agentfwd.c index 5ee8c2508..6dbe13854 100644 --- a/src/svr-agentfwd.c +++ b/src/svr-agentfwd.c @@ -80,8 +80,9 @@ int svr_agentreq(struct ChanSess * chansess) { setnonblocking(fd); /* pass if off to listener */ - chansess->agentlistener = new_listener( &fd, 1, 0, chansess, - agentaccept, NULL); + chansess->agentlistener = new_listener( &fd, 1, + LISTENER_TYPE_DEFAULT, chansess, + agentaccept, NULL); if (chansess->agentlistener == NULL) { goto fail; diff --git a/src/svr-tcpfwd.c b/src/svr-tcpfwd.c index 2873fafca..4522e4d73 100644 --- a/src/svr-tcpfwd.c +++ b/src/svr-tcpfwd.c @@ -159,7 +159,7 @@ static int svr_cancelremotetcp() { tcpinfo.sendport = 0; tcpinfo.listenaddr = bindaddr; tcpinfo.listenport = port; - listener = get_listener(CHANNEL_ID_TCPFORWARDED, &tcpinfo, matchtcp); + listener = get_listener(LISTENER_TYPE_TCPFORWARDED, &tcpinfo, matchtcp); if (listener) { remove_listener( listener ); ret = DROPBEAR_SUCCESS; diff --git a/src/svr-x11fwd.c b/src/svr-x11fwd.c index 5d9e6a96f..30c7a3c4a 100644 --- a/src/svr-x11fwd.c +++ b/src/svr-x11fwd.c @@ -108,7 +108,8 @@ int x11req(struct ChanSess * chansess) { /* listener code will handle the socket now. * No cleanup handler needed, since listener_remove only happens * from our cleanup anyway */ - chansess->x11listener = new_listener( &fd, 1, 0, chansess, x11accept, NULL); + chansess->x11listener = new_listener( &fd, 1, LISTENER_TYPE_DEFAULT, + chansess, x11accept, NULL); if (chansess->x11listener == NULL) { goto fail; } diff --git a/src/tcp-accept.c b/src/tcp-accept.c index 5998236b9..a8ae32f95 100644 --- a/src/tcp-accept.c +++ b/src/tcp-accept.c @@ -127,7 +127,7 @@ int listen_tcpfwd(struct TCPListener* tcpinfo, struct Listener **ret_listener) { m_free(errstring); /* new_listener will close the socks if it fails */ - listener = new_listener(socks, nsocks, CHANNEL_ID_TCPFORWARDED, tcpinfo, + listener = new_listener(socks, nsocks, LISTENER_TYPE_TCPFORWARDED, tcpinfo, tcp_acceptor, cleanup_tcp); if (listener == NULL) { diff --git a/src/tcpfwd.h b/src/tcpfwd.h index f4b44f639..4172f2238 100644 --- a/src/tcpfwd.h +++ b/src/tcpfwd.h @@ -75,7 +75,4 @@ void cli_recv_msg_request_failure(void); /* Common */ int listen_tcpfwd(struct TCPListener* tcpinfo, struct Listener **ret_listener); -/* A random identifier */ -#define CHANNEL_ID_TCPFORWARDED 0x43612c67 - #endif From f5d44406ef2952ca69a68d59c6b0f7f0ff777305 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 13 Apr 2026 22:32:59 +0800 Subject: [PATCH 050/122] Fix cancelling server TCP listeners There are three bugs involved here. get_listener() only tested the first element in the list rather than iterating. This was incorrect when first written in 444dbb536479 ("- Reworked non-channel fd handling to listener.c - More channel cleaning up") tcpinfo.chantype isn't initialised in svr_cancelremotetcp(), so matchtcp() never succeeded. The chantype comparison is unnecessary (LISTENER_TYPE_TCPFORWARDED has already been checked), so remove it. matchtcp() was comparing with listenaddr, but that will be NULL when listening on localhost, logic in svr_remotetcpreq(). That regressed in 1984aabc9509 ("Server shouldn't return "localhost" in response to -R forward connections if that wasn't what the client requested.") The NULL pointer previously couldn't be reached due to the chantype problem mentioned above. A testcase is added. Fixes #412, fixes #413 --- src/listener.c | 4 +-- src/svr-tcpfwd.c | 16 +++++------ test/test_canceltcplisten.py | 51 ++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 11 deletions(-) create mode 100644 test/test_canceltcplisten.py diff --git a/src/listener.c b/src/listener.c index c06d4348f..396f81adf 100644 --- a/src/listener.c +++ b/src/listener.c @@ -136,9 +136,9 @@ struct Listener * get_listener(enum ListenerType type, const void* typedata, int (*match)(const void*, const void*)) { unsigned int i; - struct Listener* listener; - for (i = 0, listener = ses.listeners[i]; i < ses.listensize; i++) { + for (i = 0; i < ses.listensize; i++) { + struct Listener* listener = ses.listeners[i]; if (listener && listener->type == type && match(typedata, listener->typedata)) { return listener; diff --git a/src/svr-tcpfwd.c b/src/svr-tcpfwd.c index 4522e4d73..fe9e75bed 100644 --- a/src/svr-tcpfwd.c +++ b/src/svr-tcpfwd.c @@ -132,14 +132,13 @@ static int matchtcp(const void* typedata1, const void* typedata2) { const struct TCPListener *info2 = (struct TCPListener*)typedata2; return (info1->listenport == info2->listenport) - && (info1->chantype == info2->chantype) - && (strcmp(info1->listenaddr, info2->listenaddr) == 0); + && (strcmp(info1->request_listenaddr, info2->request_listenaddr) == 0); } static int svr_cancelremotetcp() { int ret = DROPBEAR_FAILURE; - char * bindaddr = NULL; + char * request_addr = NULL; unsigned int addrlen; unsigned int port; struct Listener * listener = NULL; @@ -147,7 +146,7 @@ static int svr_cancelremotetcp() { TRACE(("enter cancelremotetcp")) - bindaddr = buf_getstring(ses.payload, &addrlen); + request_addr = buf_getstring(ses.payload, &addrlen); if (addrlen > MAX_HOST_LEN) { TRACE(("addr len too long: %d", addrlen)) goto out; @@ -155,18 +154,17 @@ static int svr_cancelremotetcp() { port = buf_getint(ses.payload); - tcpinfo.sendaddr = NULL; - tcpinfo.sendport = 0; - tcpinfo.listenaddr = bindaddr; + memset(&tcpinfo, 0x0, sizeof(tcpinfo)); + tcpinfo.request_listenaddr = request_addr; tcpinfo.listenport = port; listener = get_listener(LISTENER_TYPE_TCPFORWARDED, &tcpinfo, matchtcp); if (listener) { - remove_listener( listener ); + remove_listener(listener); ret = DROPBEAR_SUCCESS; } out: - m_free(bindaddr); + m_free(request_addr); TRACE(("leave cancelremotetcp")) return ret; } diff --git a/test/test_canceltcplisten.py b/test/test_canceltcplisten.py new file mode 100644 index 000000000..64be103ae --- /dev/null +++ b/test/test_canceltcplisten.py @@ -0,0 +1,51 @@ +""" +Tests cancel-tcpip-forward +""" +from test_dropbear import * + +import asyncssh +import asyncio +import socket +import errno +import random + +async def run(addr, port): + async with asyncssh.connect(addr, port = port) as conn: + + fwds = [] + COUNT=4 + + def connect(host, port): + print(f"connection from {host}:{port}") + + for x in range(COUNT): + server = await conn.start_server(connect, '', 0, + encoding='utf-8') + print(f"opened fwd {server} port {server.get_port()}") + fwds.append(server) + + # Check that they can be connected + for f in fwds: + c = socket.create_connection(("localhost", f.get_port())) + + # In case list ordering is important + random.shuffle(fwds) + + for f in fwds: + print(f"close fwd {f}") + f.close() + await f.wait_closed() + + # Check it's closed + try: + c = socket.create_connection(("localhost", f.get_port())) + assert False, f"Expected {f} port {f.get_port()} to be closed" + except OSError as e: + assert e.errno == errno.ECONNREFUSED + +def test_canceltcplisten(request, dropbear): + opt = request.config.option + host = opt.remote or LOCALADDR + port = int(opt.port) + + asyncio.run(run(host, port)) From c60db6408178aaca0a8aacd017ac344a2cfb30c8 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Thu, 26 Mar 2026 23:03:46 +0800 Subject: [PATCH 051/122] Rework pubkey_options struct Pubkey info is now included in the restriction struct. A temporary pubkey_options is filled then set in the global session after auth. --- fuzz/fuzzer-pubkey.c | 7 +--- src/auth.h | 28 ++++++++----- src/fuzz.h | 2 +- src/svr-authpubkey.c | 78 ++++++++++++++++++++---------------- src/svr-authpubkeyoptions.c | 80 +++++++++++++++++++------------------ src/svr-chansession.c | 5 ++- src/svr-session.c | 4 +- src/sysoptions.h | 4 ++ 8 files changed, 114 insertions(+), 94 deletions(-) diff --git a/fuzz/fuzzer-pubkey.c b/fuzz/fuzzer-pubkey.c index 3b00478f9..d8e825dd2 100644 --- a/fuzz/fuzzer-pubkey.c +++ b/fuzz/fuzzer-pubkey.c @@ -31,15 +31,10 @@ int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { dropbear_exit("fuzzer imagined a bogus algorithm"); } - int ret = fuzz_checkpubkey_line(line, 5, "/home/me/authorized_keys", + fuzz_checkpubkey_line(line, 5, "/home/me/authorized_keys", algoname, algolen, keyblob->data, keyblob->len); - if (ret == DROPBEAR_SUCCESS) { - /* fuzz_checkpubkey_line() should have cleaned up for failure */ - svr_pubkey_options_cleanup(); - } - buf_free(line); buf_free(keyblob); m_free(algoname); diff --git a/src/auth.h b/src/auth.h index bfb3e49d6..5fcf1c0c8 100644 --- a/src/auth.h +++ b/src/auth.h @@ -44,6 +44,7 @@ void svr_switch_user(void); void svr_raise_gid_utmp(void); void svr_restore_gid(void); +struct PubKeyOptions; #if DROPBEAR_SVR_PUBKEY_OPTIONS_BUILT int svr_pubkey_allows_agentfwd(void); int svr_pubkey_allows_tcpfwd(void); @@ -52,8 +53,9 @@ int svr_pubkey_allows_pty(void); int svr_pubkey_has_forced_command(void); int svr_pubkey_allows_local_tcpfwd(const char *host, unsigned int port); void svr_pubkey_set_forced_command(struct ChanSess *chansess); -void svr_pubkey_options_cleanup(void); -int svr_add_pubkey_options(buffer *options_buf, int line_num, const char* filename); +void svr_pubkey_options_cleanup(struct PubKeyOptions *pubkey_options); +struct PubKeyOptions* +svr_parse_pubkey_options(buffer *options_buf, int line_num, const char* filename); #else /* no option : success */ #define svr_pubkey_allows_agentfwd() 1 @@ -64,9 +66,14 @@ int svr_add_pubkey_options(buffer *options_buf, int line_num, const char* filena static inline int svr_pubkey_allows_local_tcpfwd(const char *host, unsigned int port) { (void)host; (void)port; return 1; } -static inline void svr_pubkey_set_forced_command(struct ChanSess *chansess) { } -static inline void svr_pubkey_options_cleanup(void) { } -#define svr_add_pubkey_options(x,y,z) DROPBEAR_SUCCESS +static inline void svr_pubkey_set_forced_command(struct ChanSess *chansess) { + (void)chansess; +} +static inline void svr_pubkey_options_cleanup(struct PubKeyOptions *pubkey_options) { + (void)pubkey_options; +} +struct PubKeyOptions* +svr_parse_pubkey_options(buffer *options_buf, int line_num, const char* filename); #endif /* Client functions */ @@ -137,14 +144,10 @@ struct AuthState { char *pw_shell; char *pw_name; char *pw_passwd; -#if DROPBEAR_SVR_PUBKEY_OPTIONS_BUILT struct PubKeyOptions* pubkey_options; - char *pubkey_info; -#endif }; #if DROPBEAR_SVR_PUBKEY_OPTIONS_BUILT -struct PubKeyOptions; struct PubKeyOptions { /* Flags */ int no_port_forwarding_flag; @@ -160,6 +163,13 @@ struct PubKeyOptions { int no_touch_required_flag; int verify_required_flag; #endif + + /* String from the end of authorized_keys entry, for SSH_PUBKEYINFO + environment variable. May be omitted (NULL) if + options contain non-shell-safe characters. + This is not involved in pubkey option restrictions, but is + parsed and kept similarly so is stored here. */ + char * info_env; }; struct PermitTCPFwdEntry { diff --git a/src/fuzz.h b/src/fuzz.h index 95cb4d82f..2fc1cf6d5 100644 --- a/src/fuzz.h +++ b/src/fuzz.h @@ -30,7 +30,7 @@ const void* fuzz_get_algo(const algo_type *algos, const char* name); // fuzzer functions that intrude into general code void fuzz_kex_fakealgos(void); -int fuzz_checkpubkey_line(buffer* line, int line_num, char* filename, +void fuzz_checkpubkey_line(buffer* line, int line_num, char* filename, const char* algo, unsigned int algolen, const unsigned char* keyblob, unsigned int keybloblen); extern const char * const * fuzz_signkey_names; diff --git a/src/svr-authpubkey.c b/src/svr-authpubkey.c index 9bb8db246..e38d86395 100644 --- a/src/svr-authpubkey.c +++ b/src/svr-authpubkey.c @@ -73,7 +73,8 @@ static char * authorized_keys_filepath(void); static int checkpubkey(const char* keyalgo, unsigned int keyalgolen, - const unsigned char* keyblob, unsigned int keybloblen); + const unsigned char* keyblob, unsigned int keybloblen, + struct PubKeyOptions **ret_options); static int checkpubkeyperms(void); static void send_msg_userauth_pk_ok(const char* sigalgo, unsigned int sigalgolen, const unsigned char* keyblob, unsigned int keybloblen); @@ -96,7 +97,8 @@ void svr_auth_pubkey(int valid_user) { char* fp = NULL; enum signature_type sigtype; enum signkey_type keytype; - int auth_failure = 1; + int auth_failure = 1; + struct PubKeyOptions *pubkey_options = NULL; TRACE(("enter pubkeyauth")) @@ -149,8 +151,8 @@ void svr_auth_pubkey(int valid_user) { .pos = 0, .size = 0 }; - int ret = svr_add_pubkey_options(&temp_buf, 0, "N/A"); - if (ret == DROPBEAR_FAILURE) { + pubkey_options = svr_parse_pubkey_options(&temp_buf, 0, "plugin"); + if (pubkey_options == NULL) { /* Fail immediately as the plugin provided wrong options */ send_msg_userauth_failure(0, 0); goto out; @@ -161,7 +163,8 @@ void svr_auth_pubkey(int valid_user) { #endif /* check if the key is valid */ if (auth_failure) { - auth_failure = checkpubkey(keyalgo, keyalgolen, keyblob, keybloblen) == DROPBEAR_FAILURE; + int status = checkpubkey(keyalgo, keyalgolen, keyblob, keybloblen, &pubkey_options); + auth_failure = (status != DROPBEAR_SUCCESS); } if (auth_failure) { @@ -196,13 +199,13 @@ void svr_auth_pubkey(int valid_user) { #if DROPBEAR_SK_ECDSA || DROPBEAR_SK_ED25519 key->sk_flags_mask = SSH_SK_USER_PRESENCE_REQD; #if DROPBEAR_SVR_PUBKEY_OPTIONS_BUILT - if (ses.authstate.pubkey_options && ses.authstate.pubkey_options->no_touch_required_flag) { + if (pubkey_options->no_touch_required_flag) { key->sk_flags_mask &= ~SSH_SK_USER_PRESENCE_REQD; } - if (ses.authstate.pubkey_options && ses.authstate.pubkey_options->verify_required_flag) { + if (pubkey_options->verify_required_flag) { key->sk_flags_mask |= SSH_SK_USER_VERIFICATION_REQD; } -#endif /* DROPBEAR_SVR_PUBKEY_OPTIONS */ +#endif /* DROPBEAR_SVR_PUBKEY_OPTIONS_BUILT */ #endif /* create the data which has been signed - this a string containing @@ -224,6 +227,10 @@ void svr_auth_pubkey(int valid_user) { /* ... and finally verify the signature */ fp = sign_key_fingerprint(keyblob, keybloblen); if (buf_verify(ses.payload, key, sigtype, signbuf) == DROPBEAR_SUCCESS) { + if (ses.authstate.pubkey_options == NULL) { + ses.authstate.pubkey_options = pubkey_options; + pubkey_options = NULL; + } if (svr_opts.multiauthmethod && (ses.authstate.authtypes & ~AUTH_TYPE_PUBKEY)) { /* successful pubkey authentication, but extra auth required */ dropbear_log(LOG_NOTICE, @@ -268,9 +275,9 @@ void svr_auth_pubkey(int valid_user) { sign_key_free(key); key = NULL; } - /* Retain pubkey options only if auth succeeded */ - if (!ses.authstate.authdone) { - svr_pubkey_options_cleanup(); + if (pubkey_options) { + svr_pubkey_options_cleanup(pubkey_options); + pubkey_options = NULL; } TRACE(("leave pubkeyauth")) } @@ -293,17 +300,22 @@ static void send_msg_userauth_pk_ok(const char* sigalgo, unsigned int sigalgolen } -/* Content for SSH_PUBKEYINFO is optionally returned malloced in ret_info (will be - freed if already set */ +/* Key options are optionally returned in ret_options. + Should be passed with *ret_options = NULL, will only be populated + on success return. */ static int checkpubkey_line(buffer* line, int line_num, const char* filename, const char* algo, unsigned int algolen, const unsigned char* keyblob, unsigned int keybloblen, - char ** ret_info) { + struct PubKeyOptions ** ret_options) { buffer *options_buf = NULL; char *info_str = NULL; unsigned int pos, len, infopos, infolen; int ret = DROPBEAR_FAILURE; + if (ret_options) { + *ret_options = NULL; + } + if (line->len < MIN_AUTHKEYS_LINE || line->len > MAX_AUTHKEYS_LINE) { TRACE(("checkpubkey_line: bad line length %d", line->len)) goto out; @@ -405,7 +417,7 @@ static int checkpubkey_line(buffer* line, int line_num, const char* filename, if (infolen > 0) { info_str = m_malloc(infolen + 1); buf_setpos(line, infopos); - strncpy(info_str, buf_getptr(line, infolen), infolen); + strncpy(info_str, buf_getptr(line, infolen), infolen); } /* truncate to base64 data length */ @@ -417,20 +429,19 @@ static int checkpubkey_line(buffer* line, int line_num, const char* filename, ret = cmp_base64_key(keyblob, keybloblen, (const unsigned char *) algo, algolen, line, NULL); /* free pubkey_info if it is filled */ - if (ret_info && *ret_info) { - m_free(*ret_info); - *ret_info = NULL; - } - if (ret == DROPBEAR_SUCCESS) { - if (options_buf) { - ret = svr_add_pubkey_options(options_buf, line_num, filename); - } - if (ret_info) { +#if DROPBEAR_SVR_PUBKEY_OPTIONS_BUILT + if (ret_options) { + *ret_options = svr_parse_pubkey_options(options_buf, line_num, filename); + if (*ret_options == NULL) { + ret = DROPBEAR_FAILURE; + goto out; + } /* take the (optional) public key information */ - *ret_info = info_str; + (*ret_options)->info_env = info_str; info_str = NULL; } +#endif } out: @@ -466,7 +477,8 @@ static char *authorized_keys_filepath() { * acceptable key for authentication */ /* Returns DROPBEAR_SUCCESS if key is ok for auth, DROPBEAR_FAILURE otherwise */ static int checkpubkey(const char* keyalgo, unsigned int keyalgolen, - const unsigned char* keyblob, unsigned int keybloblen) { + const unsigned char* keyblob, unsigned int keybloblen, + struct PubKeyOptions **ret_options) { FILE * authfile = NULL; char * filename = NULL; @@ -524,13 +536,7 @@ static int checkpubkey(const char* keyalgo, unsigned int keyalgolen, line_num++; ret = checkpubkey_line(line, line_num, filename, keyalgo, keyalgolen, - keyblob, keybloblen, -#if DROPBEAR_SVR_PUBKEY_OPTIONS_BUILT - &ses.authstate.pubkey_info -#else - NULL -#endif - ); + keyblob, keybloblen, ret_options); if (ret == DROPBEAR_SUCCESS) { break; } @@ -624,10 +630,12 @@ static int checkfileperm(char * filename) { } #if DROPBEAR_FUZZ -int fuzz_checkpubkey_line(buffer* line, int line_num, char* filename, +void fuzz_checkpubkey_line(buffer* line, int line_num, char* filename, const char* algo, unsigned int algolen, const unsigned char* keyblob, unsigned int keybloblen) { - return checkpubkey_line(line, line_num, filename, algo, algolen, keyblob, keybloblen, NULL); + struct PubKeyOptions *options = NULL; + checkpubkey_line(line, line_num, filename, algo, algolen, keyblob, keybloblen, &options); + svr_pubkey_options_cleanup(options); } #endif diff --git a/src/svr-authpubkeyoptions.c b/src/svr-authpubkeyoptions.c index 2ad32aa51..c533f3dae 100644 --- a/src/svr-authpubkeyoptions.c +++ b/src/svr-authpubkeyoptions.c @@ -138,25 +138,21 @@ void svr_pubkey_set_forced_command(struct ChanSess *chansess) { } /* Free potential public key options */ -void svr_pubkey_options_cleanup() { - if (ses.authstate.pubkey_options) { - if (ses.authstate.pubkey_options->forced_command) { - m_free(ses.authstate.pubkey_options->forced_command); - } - if (ses.authstate.pubkey_options->permit_open_destinations) { - m_list_elem *iter = ses.authstate.pubkey_options->permit_open_destinations->first; +void svr_pubkey_options_cleanup(struct PubKeyOptions *pubkey_options) { + if (pubkey_options) { + m_free(pubkey_options->forced_command); + if (pubkey_options->permit_open_destinations) { + m_list_elem *iter = pubkey_options->permit_open_destinations->first; while (iter) { struct PermitTCPFwdEntry *entry = (struct PermitTCPFwdEntry*)list_remove(iter); m_free(entry->host); m_free(entry); - iter = ses.authstate.pubkey_options->permit_open_destinations->first; + iter = pubkey_options->permit_open_destinations->first; } - m_free(ses.authstate.pubkey_options->permit_open_destinations); + m_free(pubkey_options->permit_open_destinations); } - m_free(ses.authstate.pubkey_options); - } - if (ses.authstate.pubkey_info) { - m_free(ses.authstate.pubkey_info); + m_free(pubkey_options->info_env); + m_free(pubkey_options); } } @@ -174,58 +170,65 @@ static int match_option(buffer *options_buf, const char *opt_name) { return DROPBEAR_FAILURE; } -/* Parse pubkey options and set ses.authstate.pubkey_options accordingly. - * Returns DROPBEAR_SUCCESS if key is ok for auth, DROPBEAR_FAILURE otherwise */ -int svr_add_pubkey_options(buffer *options_buf, int line_num, const char* filename) { - int ret = DROPBEAR_FAILURE; +/* Parse pubkey options and return a malloced struct. + * options_buf may be NULL, treated as empty. + * Returns NULL on failure */ +struct PubKeyOptions* +svr_parse_pubkey_options(buffer *options_buf, int line_num, const char* filename) { + struct PubKeyOptions *pubkey_options = NULL; TRACE(("enter addpubkeyoptions")) - ses.authstate.pubkey_options = (struct PubKeyOptions*)m_malloc(sizeof( struct PubKeyOptions )); + pubkey_options = (struct PubKeyOptions*)m_malloc(sizeof( struct PubKeyOptions )); + + if (!options_buf) { + TRACE(("leave addpubkeyoptions, null options")) + return pubkey_options; + } buf_setpos(options_buf, 0); while (options_buf->pos < options_buf->len) { if (match_option(options_buf, "no-port-forwarding") == DROPBEAR_SUCCESS) { dropbear_log(LOG_WARNING, "Port forwarding disabled."); - ses.authstate.pubkey_options->no_port_forwarding_flag = 1; + pubkey_options->no_port_forwarding_flag = 1; goto next_option; } if (match_option(options_buf, "no-agent-forwarding") == DROPBEAR_SUCCESS) { #if DROPBEAR_SVR_AGENTFWD dropbear_log(LOG_WARNING, "Agent forwarding disabled."); - ses.authstate.pubkey_options->no_agent_forwarding_flag = 1; + pubkey_options->no_agent_forwarding_flag = 1; #endif goto next_option; } if (match_option(options_buf, "no-X11-forwarding") == DROPBEAR_SUCCESS) { #if DROPBEAR_X11FWD dropbear_log(LOG_WARNING, "X11 forwarding disabled."); - ses.authstate.pubkey_options->no_x11_forwarding_flag = 1; + pubkey_options->no_x11_forwarding_flag = 1; #endif goto next_option; } if (match_option(options_buf, "no-pty") == DROPBEAR_SUCCESS) { dropbear_log(LOG_WARNING, "Pty allocation disabled."); - ses.authstate.pubkey_options->no_pty_flag = 1; + pubkey_options->no_pty_flag = 1; goto next_option; } if (match_option(options_buf, "restrict") == DROPBEAR_SUCCESS) { dropbear_log(LOG_WARNING, "Restrict option set"); - ses.authstate.pubkey_options->no_port_forwarding_flag = 1; + pubkey_options->no_port_forwarding_flag = 1; #if DROPBEAR_SVR_AGENTFWD - ses.authstate.pubkey_options->no_agent_forwarding_flag = 1; + pubkey_options->no_agent_forwarding_flag = 1; #endif #if DROPBEAR_X11FWD - ses.authstate.pubkey_options->no_x11_forwarding_flag = 1; + pubkey_options->no_x11_forwarding_flag = 1; #endif - ses.authstate.pubkey_options->no_pty_flag = 1; + pubkey_options->no_pty_flag = 1; goto next_option; } if (match_option(options_buf, "command=\"") == DROPBEAR_SUCCESS) { int escaped = 0; const unsigned char* command_start = buf_getptr(options_buf, 0); - if (ses.authstate.pubkey_options->forced_command) { + if (pubkey_options->forced_command) { /* multiple command= options */ goto bad_option; } @@ -234,10 +237,10 @@ int svr_add_pubkey_options(buffer *options_buf, int line_num, const char* filena const char c = buf_getbyte(options_buf); if (!escaped && c == '"') { const int command_len = buf_getptr(options_buf, 0) - command_start; - ses.authstate.pubkey_options->forced_command = m_malloc(command_len); - memcpy(ses.authstate.pubkey_options->forced_command, + pubkey_options->forced_command = m_malloc(command_len); + memcpy(pubkey_options->forced_command, command_start, command_len-1); - ses.authstate.pubkey_options->forced_command[command_len-1] = '\0'; + pubkey_options->forced_command[command_len-1] = '\0'; goto next_option; } escaped = (!escaped && c == '\\'); @@ -250,8 +253,8 @@ int svr_add_pubkey_options(buffer *options_buf, int line_num, const char* filena int valid_option = 0; const unsigned char* permitopen_start = buf_getptr(options_buf, 0); - if (!ses.authstate.pubkey_options->permit_open_destinations) { - ses.authstate.pubkey_options->permit_open_destinations = list_new(); + if (!pubkey_options->permit_open_destinations) { + pubkey_options->permit_open_destinations = list_new(); } while (options_buf->pos < options_buf->len) { @@ -263,7 +266,7 @@ int svr_add_pubkey_options(buffer *options_buf, int line_num, const char* filena struct PermitTCPFwdEntry *entry = (struct PermitTCPFwdEntry*)m_malloc(sizeof(struct PermitTCPFwdEntry)); - list_append(ses.authstate.pubkey_options->permit_open_destinations, entry); + list_append(pubkey_options->permit_open_destinations, entry); spec = m_malloc(permitopen_len); memcpy(spec, permitopen_start, permitopen_len - 1); spec[permitopen_len - 1] = '\0'; @@ -297,14 +300,14 @@ int svr_add_pubkey_options(buffer *options_buf, int line_num, const char* filena if (match_option(options_buf, "no-touch-required") == DROPBEAR_SUCCESS) { #if DROPBEAR_SK_ECDSA || DROPBEAR_SK_ED25519 dropbear_log(LOG_WARNING, "No user presence check required for U2F/FIDO key."); - ses.authstate.pubkey_options->no_touch_required_flag = 1; + pubkey_options->no_touch_required_flag = 1; #endif goto next_option; } if (match_option(options_buf, "verify-required") == DROPBEAR_SUCCESS) { #if DROPBEAR_SK_ECDSA || DROPBEAR_SK_ED25519 dropbear_log(LOG_WARNING, "User verification required for U2F/FIDO key."); - ses.authstate.pubkey_options->verify_required_flag = 1; + pubkey_options->verify_required_flag = 1; #endif goto next_option; } @@ -321,17 +324,16 @@ int svr_add_pubkey_options(buffer *options_buf, int line_num, const char* filena /* Process the next option. */ } /* parsed all options with no problem */ - ret = DROPBEAR_SUCCESS; goto end; bad_option: - ret = DROPBEAR_FAILURE; - svr_pubkey_options_cleanup(); + svr_pubkey_options_cleanup(pubkey_options); + pubkey_options = NULL; dropbear_log(LOG_WARNING, "Bad public key options at %s:%d", filename, line_num); end: TRACE(("leave addpubkeyoptions")) - return ret; + return pubkey_options; } #endif diff --git a/src/svr-chansession.c b/src/svr-chansession.c index 16d71113d..a2ea1fcf8 100644 --- a/src/svr-chansession.c +++ b/src/svr-chansession.c @@ -1036,8 +1036,9 @@ static void execchild(const void *user_data) { addnewvar("SSH_ORIGINAL_COMMAND", chansess->original_command); } #if DROPBEAR_SVR_PUBKEY_OPTIONS_BUILT - if (ses.authstate.pubkey_info != NULL) { - addnewvar("SSH_PUBKEYINFO", ses.authstate.pubkey_info); + if (ses.authstate.pubkey_options + && ses.authstate.pubkey_options->info_env) { + addnewvar("SSH_PUBKEYINFO", ses.authstate.pubkey_options->info_env); } #endif diff --git a/src/svr-session.c b/src/svr-session.c index a838cf5c6..8f4569552 100644 --- a/src/svr-session.c +++ b/src/svr-session.c @@ -84,8 +84,8 @@ static const struct ChanType *svr_chantypes[] = { static void svr_session_cleanup(void) { - /* free potential public key options */ - svr_pubkey_options_cleanup(); + svr_pubkey_options_cleanup(ses.authstate.pubkey_options); + ses.authstate.pubkey_options = NULL; m_free(svr_ses.addrstring); m_free(svr_ses.remotehost); diff --git a/src/sysoptions.h b/src/sysoptions.h index a13548d28..0779fc8ae 100644 --- a/src/sysoptions.h +++ b/src/sysoptions.h @@ -347,6 +347,10 @@ #error "You must define DROPBEAR_SVR_PUBKEY_AUTH in order to use plugins" #endif +#if (DROPBEAR_PLUGIN && !DROPBEAR_SVR_PUBKEY_OPTIONS_BUILT) + #error "DROPBEAR_PLUGIN requires DROPBEAR_SVR_PUBKEY_OPTIONS" +#endif + #if !(DROPBEAR_AES128 || DROPBEAR_3DES || DROPBEAR_AES256 || DROPBEAR_CHACHA20POLY1305) #error "At least one encryption algorithm must be enabled. AES128 is recommended." #endif From 8c5c56072295f629ceb761b91ea1d984f4782854 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Thu, 16 Apr 2026 23:55:50 +0800 Subject: [PATCH 052/122] Fix update_timeout check Already elapsed time was being added rather than subtracted to the remaining time. This could result in timeouts occurring later than expected. --- src/common-session.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common-session.c b/src/common-session.c index d420d6ffa..52516efe4 100644 --- a/src/common-session.c +++ b/src/common-session.c @@ -600,7 +600,7 @@ static void update_timeout(long limit, time_t now, time_t last_event, long * tim (unsigned long long)now, (unsigned long long)last_event, *timeout)) if (last_event > 0 && limit > 0) { - *timeout = MIN(*timeout, elapsed(now, last_event) + limit); + *timeout = MIN(*timeout, MAX(0, limit - elapsed(now, last_event))); TRACE2(("new timeout %ld", *timeout)) } } From 9cc71cf56674eb4d8b7c2404a5f31a007e664ede Mon Sep 17 00:00:00 2001 From: Martin Schiller Date: Wed, 4 Mar 2026 08:00:37 +0100 Subject: [PATCH 053/122] server: change auth timeout checking dependency Use authdone instead of connect_time as dependency for authentication timeout checking. This prevents connect_time from being set to 0 unnecessarily and allows it to be used for other purposes. Signed-off-by: Martin Schiller --- src/common-session.c | 2 +- src/svr-auth.c | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/common-session.c b/src/common-session.c index 52516efe4..4e3101236 100644 --- a/src/common-session.c +++ b/src/common-session.c @@ -547,7 +547,7 @@ static void checktimeouts() { time_t now; now = monotonic_now(); - if (IS_DROPBEAR_SERVER && ses.connect_time != 0 + if (IS_DROPBEAR_SERVER && ses.authstate.authdone != 1 && elapsed(now, ses.connect_time) >= AUTH_TIMEOUT) { dropbear_close("Timeout before auth"); } diff --git a/src/svr-auth.c b/src/svr-auth.c index 0975dc06e..8f519aff3 100644 --- a/src/svr-auth.c +++ b/src/svr-auth.c @@ -459,11 +459,7 @@ void send_msg_userauth_success() { #if DROPBEAR_SVR_DROP_PRIVS /* Drop privileges as soon as authentication has happened. */ svr_switch_user(); -#endif - ses.connect_time = 0; - -#if DROPBEAR_SVR_DROP_PRIVS /* If running as the user, we can rely on the OS * to limit allowed ports */ ses.allowprivport = 1; From 0b948e971a696c244d69da1884872ccb4b2d1fba Mon Sep 17 00:00:00 2001 From: Martin Schiller Date: Wed, 4 Mar 2026 08:18:21 +0100 Subject: [PATCH 054/122] Add new parameter -M for max. duration Some technical guidelines (e.g., TR-03148) and cybersecurity baselines require a hard maximum connection duration / session timeout for remote connections. Therefore, a parameter is hereby introduced that can be used to configure the maximum connection duration. The default value is 0 (disabled). Signed-off-by: Martin Schiller --- src/cli-runopts.c | 17 ++++++++++++++++- src/common-session.c | 8 ++++++++ src/default_options.h | 4 ++++ src/runopts.h | 1 + src/svr-runopts.c | 19 +++++++++++++++++-- 5 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/cli-runopts.c b/src/cli-runopts.c index 501dd0f7d..3853c96fc 100644 --- a/src/cli-runopts.c +++ b/src/cli-runopts.c @@ -84,6 +84,7 @@ static void printhelp() { "-W (default %d, larger may be faster, max 10MB)\n" "-K (0 is never, default %d)\n" "-I (0 is never, default %d)\n" + "-M (0 is off, default %d, in seconds)\n" "-z disable QoS\n" #if DROPBEAR_CLI_NETCAT "-B Netcat-alike forwarding\n" @@ -104,7 +105,8 @@ static void printhelp() { #if DROPBEAR_CLI_PUBKEY_AUTH DROPBEAR_DEFAULT_CLI_AUTHKEY, #endif - DEFAULT_RECV_WINDOW, DEFAULT_KEEPALIVE, DEFAULT_IDLE_TIMEOUT); + DEFAULT_RECV_WINDOW, DEFAULT_KEEPALIVE, DEFAULT_IDLE_TIMEOUT, + DEFAULT_MAX_DURATION); } @@ -132,6 +134,7 @@ void cli_getopts(int argc, char ** argv) { const char* recv_window_arg = NULL; const char* idle_timeout_arg = NULL; + const char* max_duration_arg = NULL; const char *host_arg = NULL; const char *proxycmd_arg = NULL; const char *remoteport_arg = NULL; @@ -198,6 +201,7 @@ void cli_getopts(int argc, char ** argv) { opts.recv_window = DEFAULT_RECV_WINDOW; opts.keepalive_secs = DEFAULT_KEEPALIVE; opts.idle_timeout_secs = DEFAULT_IDLE_TIMEOUT; + opts.max_duration_secs = DEFAULT_MAX_DURATION; fill_own_user(); @@ -299,6 +303,9 @@ void cli_getopts(int argc, char ** argv) { case 'I': next = &idle_timeout_arg; break; + case 'M': + next = &max_duration_arg; + break; #if DROPBEAR_CLI_AGENTFWD case 'A': cli_opts.agent_fwd = 1; @@ -507,6 +514,14 @@ void cli_getopts(int argc, char ** argv) { opts.idle_timeout_secs = val; } + if (max_duration_arg) { + unsigned int val; + if (m_str_to_uint(max_duration_arg, &val) == DROPBEAR_FAILURE) { + dropbear_exit("Bad max_duration '%s'", max_duration_arg); + } + opts.max_duration_secs = val; + } + #if DROPBEAR_CLI_NETCAT if (cli_opts.cmd && cli_opts.netcat_host) { dropbear_log(LOG_INFO, "Ignoring command '%s' in netcat mode", cli_opts.cmd); diff --git a/src/common-session.c b/src/common-session.c index 4e3101236..5031c38a9 100644 --- a/src/common-session.c +++ b/src/common-session.c @@ -592,6 +592,11 @@ static void checktimeouts() { && elapsed(now, ses.last_packet_time_idle) >= opts.idle_timeout_secs) { dropbear_close("Idle timeout"); } + + if (opts.max_duration_secs > 0 + && elapsed(now, ses.connect_time) >= opts.max_duration_secs) { + dropbear_close("Max duration reached"); + } } static void update_timeout(long limit, time_t now, time_t last_event, long * timeout) { @@ -632,6 +637,9 @@ static long select_timeout() { update_timeout(opts.idle_timeout_secs, now, ses.last_packet_time_idle, &timeout); + update_timeout(opts.max_duration_secs, now, ses.connect_time, + &timeout); + /* clamp negative timeouts to zero - event has already triggered */ return MAX(timeout, 0); } diff --git a/src/default_options.h b/src/default_options.h index 98cd0d543..579f304e9 100644 --- a/src/default_options.h +++ b/src/default_options.h @@ -389,6 +389,10 @@ for runtime configuration please mail the Dropbear list */ be overridden at runtime with -I. 0 disables idle timeouts */ #define DEFAULT_IDLE_TIMEOUT 0 +/* Disconnect after MAX_DURATION seconds. This can be overridden at +runtime with -M. 0 disables this feature. */ +#define DEFAULT_MAX_DURATION 0 + /* The default path. This will often get replaced by the shell */ #define DEFAULT_PATH "/usr/bin:/bin" #define DEFAULT_ROOT_PATH "/usr/sbin:/usr/bin:/sbin:/bin" diff --git a/src/runopts.h b/src/runopts.h index c8072b365..210e69d08 100644 --- a/src/runopts.h +++ b/src/runopts.h @@ -41,6 +41,7 @@ typedef struct runopts { unsigned int recv_window; long keepalive_secs; /* Time between sending keepalives. 0 is off */ long idle_timeout_secs; /* Exit if no traffic is sent/received in this time */ + long max_duration_secs; /* Exit after this time */ int usingsyslog; #ifndef DISABLE_ZLIB diff --git a/src/svr-runopts.c b/src/svr-runopts.c index 8aaeab026..31a0393bc 100644 --- a/src/svr-runopts.c +++ b/src/svr-runopts.c @@ -112,6 +112,7 @@ static void printhelp(const char * progname) { "-W (default %d, larger may be faster, max 10MB)\n" "-K (0 is never, default %d, in seconds)\n" "-I (0 is never, default %d, in seconds)\n" + "-M (0 is off, default %d, in seconds)\n" "-z disable QoS\n" #if DROPBEAR_PLUGIN "-A [,]\n" @@ -136,7 +137,8 @@ static void printhelp(const char * progname) { #endif MAX_AUTH_TRIES, DROPBEAR_MAX_PORTS, DROPBEAR_DEFPORT, DROPBEAR_PIDFILE, - DEFAULT_RECV_WINDOW, DEFAULT_KEEPALIVE, DEFAULT_IDLE_TIMEOUT); + DEFAULT_RECV_WINDOW, DEFAULT_KEEPALIVE, DEFAULT_IDLE_TIMEOUT, + DEFAULT_MAX_DURATION); } void svr_getopts(int argc, char ** argv) { @@ -147,6 +149,7 @@ void svr_getopts(int argc, char ** argv) { char* recv_window_arg = NULL; char* keepalive_arg = NULL; char* idle_timeout_arg = NULL; + char* max_duration_arg = NULL; char* maxauthtries_arg = NULL; char* reexec_fd_arg = NULL; char* keyfile = NULL; @@ -207,7 +210,8 @@ void svr_getopts(int argc, char ** argv) { opts.recv_window = DEFAULT_RECV_WINDOW; opts.keepalive_secs = DEFAULT_KEEPALIVE; opts.idle_timeout_secs = DEFAULT_IDLE_TIMEOUT; - + opts.max_duration_secs = DEFAULT_MAX_DURATION; + #if DROPBEAR_SVR_REMOTETCPFWD opts.listen_fwd_all = 0; #endif @@ -316,6 +320,9 @@ void svr_getopts(int argc, char ** argv) { case 'I': next = &idle_timeout_arg; break; + case 'M': + next = &max_duration_arg; + break; case 'T': next = &maxauthtries_arg; break; @@ -449,6 +456,14 @@ void svr_getopts(int argc, char ** argv) { opts.idle_timeout_secs = val; } + if (max_duration_arg) { + unsigned int val; + if (m_str_to_uint(max_duration_arg, &val) == DROPBEAR_FAILURE) { + dropbear_exit("Bad max_duration '%s'", max_duration_arg); + } + opts.max_duration_secs = val; + } + if (svr_opts.forced_command) { dropbear_log(LOG_INFO, "Forced command set to '%s'", svr_opts.forced_command); } From 861cd9ec29ec687c24e1ece8ffb0cca8c4907cf8 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Fri, 17 Apr 2026 00:00:59 +0800 Subject: [PATCH 055/122] tests: Fix DEFAULT_MAX_DURATION=0 becoming 1 nondefault option tests changes all 0 options to 1, but that results in a 1 second timeout so the test fails. --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0458fec68..c31db0ccc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -235,6 +235,7 @@ jobs: echo "#define DROPBEAR_SVR_PASSWORD_AUTH 0" >> localoptions.h # 1 second timeout is too short sed -i "s/DEFAULT_IDLE_TIMEOUT 1/DEFAULT_IDLE_TIMEOUT 99/" localoptions.h + sed -i "s/DEFAULT_MAX_DURATION 1/DEFAULT_MAX_DURATION 99/" localoptions.h # DROPBEAR_SVR_DROP_PRIVS is on by default, turn it off echo "#define DROPBEAR_SVR_DROP_PRIVS 0" >> localoptions.h echo "#define DROPBEAR_SVR_LOCALSTREAMFWD 0" >> localoptions.h From e1ce8683648461c9bb45f03813c5cfebd63c1237 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Thu, 16 Apr 2026 22:22:34 +0800 Subject: [PATCH 056/122] Github actions complains about checkout v4 Use checkout@v6, upload-artifact@v7 --- .github/workflows/autoconf.yml | 2 +- .github/workflows/build.yml | 6 +++--- .github/workflows/outoftree.yml | 2 +- .github/workflows/tarball.yml | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/autoconf.yml b/.github/workflows/autoconf.yml index fa2d52f31..20e9fe4c7 100644 --- a/.github/workflows/autoconf.yml +++ b/.github/workflows/autoconf.yml @@ -16,7 +16,7 @@ jobs: sudo apt-get -y update sudo apt-get -y install autoconf - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: run autoconf run: autoconf && autoheader diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c31db0ccc..6f28738a8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -211,7 +211,7 @@ jobs: sudo apt-get -y update sudo apt-get -y install zlib1g-dev libtomcrypt-dev libtommath-dev mercurial python3-venv libpam0g-dev $CC - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: configure run: | @@ -288,14 +288,14 @@ jobs: # upload config.log if something has failed - name: config.log if: ${{ !env.ACT && (failure() || cancelled()) }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: config.log path: config.log - name: upload sizes if: ${{ matrix.upload_sizes }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: sizes.txt path: sizes.txt diff --git a/.github/workflows/outoftree.yml b/.github/workflows/outoftree.yml index 44255cf09..13209535e 100644 --- a/.github/workflows/outoftree.yml +++ b/.github/workflows/outoftree.yml @@ -12,7 +12,7 @@ jobs: runs-on: 'ubuntu-22.04' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: build run: | diff --git a/.github/workflows/tarball.yml b/.github/workflows/tarball.yml index 0938ffa7d..f3c5f294e 100644 --- a/.github/workflows/tarball.yml +++ b/.github/workflows/tarball.yml @@ -8,7 +8,7 @@ jobs: runs-on: 'ubuntu-22.04' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: release.sh run: ./release.sh --testrel | tee log1.txt @@ -20,7 +20,7 @@ jobs: mv `tail -n1 log1.txt` rel.tar.bz2 - name: sha256sum - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: sha256sum path: | @@ -28,7 +28,7 @@ jobs: hash.txt - name: tarball - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: tarball # only keep for debugging From 8b579571b5584847caf72345b9073b3b2a0467a7 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Fri, 17 Apr 2026 21:24:08 +0800 Subject: [PATCH 057/122] server: open authorized_keys files nonblocking This avoids getting stuck with special files. --- src/svr-authpubkey.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/svr-authpubkey.c b/src/svr-authpubkey.c index e38d86395..b0e67baae 100644 --- a/src/svr-authpubkey.c +++ b/src/svr-authpubkey.c @@ -503,10 +503,18 @@ static int checkpubkey(const char* keyalgo, unsigned int keyalgolen, if (checkpubkeyperms() == DROPBEAR_FAILURE) { TRACE(("bad authorized_keys permissions, or file doesn't exist")) } else { + int fd; /* we don't need to check pw and pw_dir for validity, since * its been done in checkpubkeyperms. */ filename = authorized_keys_filepath(); - authfile = fopen(filename, "r"); + fd = open(filename, O_RDONLY | O_NONBLOCK); + if (fd >= 0) { + authfile = fdopen(fd, "r"); + if (!authfile) { + /* fdopen could fail with ENOMEM */ + m_close(fd); + } + } if (!authfile) { TRACE(("checkpubkey: failed opening %s: %s", filename, strerror(errno))) } From 9e19e3ea428475543cb56082c96566a2f84e5f4f Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Fri, 17 Apr 2026 23:35:56 +0800 Subject: [PATCH 058/122] Handle agent listener when fuzzing Using a real listener socket caused an assertion in the fuzz harness. https://issues.oss-fuzz.com/issues/470230567 --- src/svr-agentfwd.c | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/svr-agentfwd.c b/src/svr-agentfwd.c index 6dbe13854..e63b4a91b 100644 --- a/src/svr-agentfwd.c +++ b/src/svr-agentfwd.c @@ -60,20 +60,28 @@ int svr_agentreq(struct ChanSess * chansess) { return DROPBEAR_FAILURE; } - /* create listening socket */ - fd = socket(PF_UNIX, SOCK_STREAM, 0); - if (fd < 0) { - goto fail; +#if DROPBEAR_FUZZ + if (fuzz.fuzzing) { + fd = wrapfd_new_dummy(); } + else +#endif + { + /* create listening socket */ + fd = socket(PF_UNIX, SOCK_STREAM, 0); + if (fd < 0) { + goto fail; + } - /* create the unix socket dir and file */ - if (bindagent(fd, chansess) == DROPBEAR_FAILURE) { - goto fail; - } + /* create the unix socket dir and file */ + if (bindagent(fd, chansess) == DROPBEAR_FAILURE) { + goto fail; + } - /* listen */ - if (listen(fd, 20) < 0) { - goto fail; + /* listen */ + if (listen(fd, 20) < 0) { + goto fail; + } } /* set non-blocking */ From 5b3736956e864efeea041e100c39f4d06ad221e5 Mon Sep 17 00:00:00 2001 From: Mitar Date: Wed, 17 Sep 2025 23:47:40 +0200 Subject: [PATCH 059/122] Add support for permitlisten to authorized_keys --- manpages/dropbear.8 | 8 ++++ src/auth.h | 7 +++- src/svr-authpubkeyoptions.c | 79 ++++++++++++++++++++++++++++++++++++- src/svr-tcpfwd.c | 5 +++ 4 files changed, 96 insertions(+), 3 deletions(-) diff --git a/manpages/dropbear.8 b/manpages/dropbear.8 index 776a9708d..aaeb141bb 100644 --- a/manpages/dropbear.8 +++ b/manpages/dropbear.8 @@ -183,6 +183,14 @@ can be set in authorized_keys. Wildcard character ('*') may be used in port specification for matching any port. Hosts must be literal domain names or IP addresses. +.TP +.B permitlisten=\fR"\fIhost:port\fR" +Restrict remote port forwarding so that connection is allowed only from the +specified host and port. Multiple permitlisten options separated by commas +can be set in authorized_keys. Hosts must be literal domain names or +IP addresses or special addresses like 0.0.0.0. Port can be 0 to allow the +listen port to be dynamically allocated on the server. + .TP .B command=\fR"\fIforced_command\fR" Disregard the command provided by the user and always run \fIforced_command\fR. diff --git a/src/auth.h b/src/auth.h index 5fcf1c0c8..f1a96f758 100644 --- a/src/auth.h +++ b/src/auth.h @@ -52,6 +52,7 @@ int svr_pubkey_allows_x11fwd(void); int svr_pubkey_allows_pty(void); int svr_pubkey_has_forced_command(void); int svr_pubkey_allows_local_tcpfwd(const char *host, unsigned int port); +int svr_pubkey_allows_remote_tcpfwd(const char *host, unsigned int port); void svr_pubkey_set_forced_command(struct ChanSess *chansess); void svr_pubkey_options_cleanup(struct PubKeyOptions *pubkey_options); struct PubKeyOptions* @@ -65,6 +66,8 @@ svr_parse_pubkey_options(buffer *options_buf, int line_num, const char* filename #define svr_pubkey_has_forced_command() 0 static inline int svr_pubkey_allows_local_tcpfwd(const char *host, unsigned int port) { (void)host; (void)port; return 1; } +static inline int svr_pubkey_allows_remote_tcpfwd(const char *host, unsigned int port) + { (void)host; (void)port; return 1; } static inline void svr_pubkey_set_forced_command(struct ChanSess *chansess) { (void)chansess; @@ -158,7 +161,9 @@ struct PubKeyOptions { char * forced_command; /* "permitopen=" option */ m_list *permit_open_destinations; - + /* "permitlisten=" option */ + m_list *permit_listen_sources; + #if DROPBEAR_SK_ECDSA || DROPBEAR_SK_ED25519 int no_touch_required_flag; int verify_required_flag; diff --git a/src/svr-authpubkeyoptions.c b/src/svr-authpubkeyoptions.c index c533f3dae..f430aeb5f 100644 --- a/src/svr-authpubkeyoptions.c +++ b/src/svr-authpubkeyoptions.c @@ -95,7 +95,7 @@ int svr_pubkey_allows_pty() { return 1; } -/* Returns 1 if pubkey allows local tcp fowarding to the provided destination, +/* Returns 1 if pubkey allows local tcp forwarding to the provided destination, * 0 otherwise */ int svr_pubkey_allows_local_tcpfwd(const char *host, unsigned int port) { if (ses.authstate.pubkey_options @@ -118,7 +118,28 @@ int svr_pubkey_allows_local_tcpfwd(const char *host, unsigned int port) { return 1; } -/* Set chansession command to the one forced +/* Returns 1 if pubkey allows remote tcp forwarding from the provided source, + * 0 otherwise */ +int svr_pubkey_allows_remote_tcpfwd(const char *host, unsigned int port) { + if (ses.authstate.pubkey_options + && ses.authstate.pubkey_options->permit_listen_sources) { + m_list_elem *iter = ses.authstate.pubkey_options->permit_listen_sources->first; + while (iter) { + struct PermitTCPFwdEntry *entry = (struct PermitTCPFwdEntry*)iter->item; + if (strcmp(entry->host, host) == 0 && entry->port == port) { + return 1; + } + + iter = iter->next; + } + + return 0; + } + + return 1; +} + +/* Set chansession command to the one forced * by any 'command' public key option. */ void svr_pubkey_set_forced_command(struct ChanSess *chansess) { if (ses.authstate.pubkey_options && ses.authstate.pubkey_options->forced_command) { @@ -151,6 +172,16 @@ void svr_pubkey_options_cleanup(struct PubKeyOptions *pubkey_options) { } m_free(pubkey_options->permit_open_destinations); } + if (pubkey_options->permit_listen_sources) { + m_list_elem *iter = pubkey_options->permit_listen_sources->first; + while (iter) { + struct PermitTCPFwdEntry *entry = (struct PermitTCPFwdEntry*)list_remove(iter); + m_free(entry->host); + m_free(entry); + iter = pubkey_options->permit_listen_sources->first; + } + m_free(pubkey_options->permit_listen_sources); + } m_free(pubkey_options->info_env); m_free(pubkey_options); } @@ -297,6 +328,50 @@ svr_parse_pubkey_options(buffer *options_buf, int line_num, const char* filename } } + if (match_option(options_buf, "permitlisten=\"") == DROPBEAR_SUCCESS) { + int valid_option = 0; + const unsigned char* permitlisten_start = buf_getptr(options_buf, 0); + + if (!pubkey_options->permit_listen_sources) { + pubkey_options->permit_listen_sources = list_new(); + } + + while (options_buf->pos < options_buf->len) { + const char c = buf_getbyte(options_buf); + if (c == '"') { + char *spec = NULL; + char *portstring = NULL; + const int permitlisten_len = buf_getptr(options_buf, 0) - permitlisten_start; + struct PermitTCPFwdEntry *entry = + (struct PermitTCPFwdEntry*)m_malloc(sizeof(struct PermitTCPFwdEntry)); + + list_append(pubkey_options->permit_listen_sources, entry); + spec = m_malloc(permitlisten_len); + memcpy(spec, permitlisten_start, permitlisten_len - 1); + spec[permitlisten_len - 1] = '\0'; + if ((split_address_port(spec, &entry->host, &portstring) == DROPBEAR_SUCCESS) + && entry->host && portstring) { + if (m_str_to_uint(portstring, &entry->port) == DROPBEAR_SUCCESS) { + valid_option = 1; + TRACE(("remote port forwarding allowed from host '%s' and port '%u'", + entry->host, entry->port)); + } + } + + m_free(spec); + m_free(portstring); + break; + } + } + + if (valid_option) { + goto next_option; + } else { + dropbear_log(LOG_WARNING, "Badly formatted permitlisten= authorized_keys option"); + goto bad_option; + } + } + if (match_option(options_buf, "no-touch-required") == DROPBEAR_SUCCESS) { #if DROPBEAR_SK_ECDSA || DROPBEAR_SK_ED25519 dropbear_log(LOG_WARNING, "No user presence check required for U2F/FIDO key."); diff --git a/src/svr-tcpfwd.c b/src/svr-tcpfwd.c index fe9e75bed..54130f2d8 100644 --- a/src/svr-tcpfwd.c +++ b/src/svr-tcpfwd.c @@ -200,6 +200,11 @@ static int svr_remotetcpreq(int *allocated_listen_port) { } } + if (!svr_pubkey_allows_remote_tcpfwd(request_addr, port)) { + TRACE(("remote tcp forwarding not permitted from requested source")); + goto out; + } + tcpinfo = (struct TCPListener*)m_malloc(sizeof(struct TCPListener)); tcpinfo->sendaddr = NULL; tcpinfo->sendport = 0; From 5ed0ce094b799c9fc8b5922ce6698a354afa908b Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Thu, 16 Apr 2026 23:34:25 +0800 Subject: [PATCH 060/122] Reword permitlisten to avoid "source" The bind/listen address is what should be used. "Source" could be confused with the originating TCP host connecting to a listener. --- manpages/dropbear.8 | 7 ++++--- src/auth.h | 3 ++- src/svr-authpubkeyoptions.c | 18 +++++++++--------- src/svr-tcpfwd.c | 2 +- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/manpages/dropbear.8 b/manpages/dropbear.8 index aaeb141bb..7ad9c1322 100644 --- a/manpages/dropbear.8 +++ b/manpages/dropbear.8 @@ -185,10 +185,11 @@ IP addresses. .TP .B permitlisten=\fR"\fIhost:port\fR" -Restrict remote port forwarding so that connection is allowed only from the -specified host and port. Multiple permitlisten options separated by commas +Restrict remote port forwarding listeners to the given address. +Multiple permitlisten options separated by commas can be set in authorized_keys. Hosts must be literal domain names or -IP addresses or special addresses like 0.0.0.0. Port can be 0 to allow the +IP addresses or special addresses like 0.0.0.0. +Port can be 0 to allow the listen port to be dynamically allocated on the server. .TP diff --git a/src/auth.h b/src/auth.h index f1a96f758..8fe8074c5 100644 --- a/src/auth.h +++ b/src/auth.h @@ -162,7 +162,7 @@ struct PubKeyOptions { /* "permitopen=" option */ m_list *permit_open_destinations; /* "permitlisten=" option */ - m_list *permit_listen_sources; + m_list *permit_listens; #if DROPBEAR_SK_ECDSA || DROPBEAR_SK_ED25519 int no_touch_required_flag; @@ -178,6 +178,7 @@ struct PubKeyOptions { }; struct PermitTCPFwdEntry { + /* host may be NULL for permitlisten entries */ char *host; unsigned int port; }; diff --git a/src/svr-authpubkeyoptions.c b/src/svr-authpubkeyoptions.c index f430aeb5f..10e754015 100644 --- a/src/svr-authpubkeyoptions.c +++ b/src/svr-authpubkeyoptions.c @@ -122,8 +122,8 @@ int svr_pubkey_allows_local_tcpfwd(const char *host, unsigned int port) { * 0 otherwise */ int svr_pubkey_allows_remote_tcpfwd(const char *host, unsigned int port) { if (ses.authstate.pubkey_options - && ses.authstate.pubkey_options->permit_listen_sources) { - m_list_elem *iter = ses.authstate.pubkey_options->permit_listen_sources->first; + && ses.authstate.pubkey_options->permit_listens) { + m_list_elem *iter = ses.authstate.pubkey_options->permit_listens->first; while (iter) { struct PermitTCPFwdEntry *entry = (struct PermitTCPFwdEntry*)iter->item; if (strcmp(entry->host, host) == 0 && entry->port == port) { @@ -172,15 +172,15 @@ void svr_pubkey_options_cleanup(struct PubKeyOptions *pubkey_options) { } m_free(pubkey_options->permit_open_destinations); } - if (pubkey_options->permit_listen_sources) { - m_list_elem *iter = pubkey_options->permit_listen_sources->first; + if (pubkey_options->permit_listens) { + m_list_elem *iter = pubkey_options->permit_listens->first; while (iter) { struct PermitTCPFwdEntry *entry = (struct PermitTCPFwdEntry*)list_remove(iter); m_free(entry->host); m_free(entry); - iter = pubkey_options->permit_listen_sources->first; + iter = pubkey_options->permit_listens->first; } - m_free(pubkey_options->permit_listen_sources); + m_free(pubkey_options->permit_listens); } m_free(pubkey_options->info_env); m_free(pubkey_options); @@ -332,8 +332,8 @@ svr_parse_pubkey_options(buffer *options_buf, int line_num, const char* filename int valid_option = 0; const unsigned char* permitlisten_start = buf_getptr(options_buf, 0); - if (!pubkey_options->permit_listen_sources) { - pubkey_options->permit_listen_sources = list_new(); + if (!pubkey_options->permit_listens) { + pubkey_options->permit_listens = list_new(); } while (options_buf->pos < options_buf->len) { @@ -345,7 +345,7 @@ svr_parse_pubkey_options(buffer *options_buf, int line_num, const char* filename struct PermitTCPFwdEntry *entry = (struct PermitTCPFwdEntry*)m_malloc(sizeof(struct PermitTCPFwdEntry)); - list_append(pubkey_options->permit_listen_sources, entry); + list_append(pubkey_options->permit_listens, entry); spec = m_malloc(permitlisten_len); memcpy(spec, permitlisten_start, permitlisten_len - 1); spec[permitlisten_len - 1] = '\0'; diff --git a/src/svr-tcpfwd.c b/src/svr-tcpfwd.c index 54130f2d8..7d8f420c0 100644 --- a/src/svr-tcpfwd.c +++ b/src/svr-tcpfwd.c @@ -201,7 +201,7 @@ static int svr_remotetcpreq(int *allocated_listen_port) { } if (!svr_pubkey_allows_remote_tcpfwd(request_addr, port)) { - TRACE(("remote tcp forwarding not permitted from requested source")); + TRACE(("remote tcp forwarding listen address not permitted")); goto out; } From 02bf2ec84b9d5c520787b193eff8d2e9a142c9d0 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 27 Apr 2026 23:28:38 +0800 Subject: [PATCH 061/122] Only accept a port for permitlisten For the time being a plain permitlisten="port" is adequate. Host support is more complicated with various special hostnames of "*" or "localhost", "127.0.0.1", "::1", etc. There are also interactions with "-a"/GatewayPorts. --- manpages/dropbear.8 | 9 ++++----- src/svr-authpubkeyoptions.c | 25 +++++++++++++++---------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/manpages/dropbear.8 b/manpages/dropbear.8 index 7ad9c1322..7f2c990db 100644 --- a/manpages/dropbear.8 +++ b/manpages/dropbear.8 @@ -184,11 +184,10 @@ port specification for matching any port. Hosts must be literal domain names or IP addresses. .TP -.B permitlisten=\fR"\fIhost:port\fR" -Restrict remote port forwarding listeners to the given address. -Multiple permitlisten options separated by commas -can be set in authorized_keys. Hosts must be literal domain names or -IP addresses or special addresses like 0.0.0.0. +.B permitlisten=\fR"\fIport\fR" +Restrict remote port forwarding listeners to the given port. +Multiple permitlisten="port" options separated by commas +can be set in authorized_keys. Port can be 0 to allow the listen port to be dynamically allocated on the server. diff --git a/src/svr-authpubkeyoptions.c b/src/svr-authpubkeyoptions.c index 10e754015..61b5857c2 100644 --- a/src/svr-authpubkeyoptions.c +++ b/src/svr-authpubkeyoptions.c @@ -121,12 +121,15 @@ int svr_pubkey_allows_local_tcpfwd(const char *host, unsigned int port) { /* Returns 1 if pubkey allows remote tcp forwarding from the provided source, * 0 otherwise */ int svr_pubkey_allows_remote_tcpfwd(const char *host, unsigned int port) { + /* no host restrictions */ + (void)host; + if (ses.authstate.pubkey_options && ses.authstate.pubkey_options->permit_listens) { m_list_elem *iter = ses.authstate.pubkey_options->permit_listens->first; while (iter) { struct PermitTCPFwdEntry *entry = (struct PermitTCPFwdEntry*)iter->item; - if (strcmp(entry->host, host) == 0 && entry->port == port) { + if (entry->port == port) { return 1; } @@ -340,26 +343,28 @@ svr_parse_pubkey_options(buffer *options_buf, int line_num, const char* filename const char c = buf_getbyte(options_buf); if (c == '"') { char *spec = NULL; - char *portstring = NULL; const int permitlisten_len = buf_getptr(options_buf, 0) - permitlisten_start; struct PermitTCPFwdEntry *entry = (struct PermitTCPFwdEntry*)m_malloc(sizeof(struct PermitTCPFwdEntry)); list_append(pubkey_options->permit_listens, entry); + /* permitlisten_len includes trailing '"' */ spec = m_malloc(permitlisten_len); memcpy(spec, permitlisten_start, permitlisten_len - 1); spec[permitlisten_len - 1] = '\0'; - if ((split_address_port(spec, &entry->host, &portstring) == DROPBEAR_SUCCESS) - && entry->host && portstring) { - if (m_str_to_uint(portstring, &entry->port) == DROPBEAR_SUCCESS) { - valid_option = 1; - TRACE(("remote port forwarding allowed from host '%s' and port '%u'", - entry->host, entry->port)); - } + + /* Only a plain port accepted. + * OpenSSH supports [host:]port, but that isn't implemented. + * port="*" isn't supported either, since it only is useful + * with a host: part. */ + + if (m_str_to_uint(spec, &entry->port) == DROPBEAR_SUCCESS) { + valid_option = 1; + TRACE(("remote forwarding allows listening on port %u", + entry->port)); } m_free(spec); - m_free(portstring); break; } } From 7a8528974206f804a99b634e14ec2baaf26211b5 Mon Sep 17 00:00:00 2001 From: Brian Dentino Date: Mon, 10 Nov 2025 17:31:56 -0400 Subject: [PATCH 062/122] Support for remote forwarding with unix sockets (rebased with minimal compile fixes by Matt Johnston) --- .github/workflows/build.yml | 1 + src/default_options.h | 1 + src/listener.h | 1 + src/runopts.h | 3 + src/svr-runopts.c | 11 ++ src/svr-tcpfwd.c | 270 ++++++++++++++++++++++++++++++++++++ src/tcpfwd.h | 6 + 7 files changed, 293 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6f28738a8..ecb981adb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -123,6 +123,7 @@ jobs: #define DROPBEAR_CLI_REMOTETCPFWD 0 #define DROPBEAR_SVR_LOCALTCPFWD 0 #define DROPBEAR_SVR_REMOTETCPFWD 0 + #define DROPBEAR_SVR_REMOTESTREAMFWD 0 #define DROPBEAR_SVR_AGENTFWD 0 #define DROPBEAR_CLI_AGENTFWD 0 #define DROPBEAR_CLI_PROXYCMD 0 diff --git a/src/default_options.h b/src/default_options.h index 579f304e9..38e9b8c34 100644 --- a/src/default_options.h +++ b/src/default_options.h @@ -74,6 +74,7 @@ IMPORTANT: Some options will require "make clean" after changes */ #define DROPBEAR_SVR_LOCALTCPFWD 1 #define DROPBEAR_SVR_REMOTETCPFWD 1 #define DROPBEAR_SVR_LOCALSTREAMFWD 1 +#define DROPBEAR_SVR_REMOTESTREAMFWD 1 /* Enable Authentication Agent Forwarding */ #define DROPBEAR_SVR_AGENTFWD 1 diff --git a/src/listener.h b/src/listener.h index 5fae6a80a..ced700eee 100644 --- a/src/listener.h +++ b/src/listener.h @@ -33,6 +33,7 @@ enum ListenerType { LISTENER_TYPE_DEFAULT, LISTENER_TYPE_TCPFORWARDED, + LISTENER_TYPE_STREAMFORWARDED, }; struct Listener { diff --git a/src/runopts.h b/src/runopts.h index 210e69d08..827f3db9c 100644 --- a/src/runopts.h +++ b/src/runopts.h @@ -113,6 +113,9 @@ typedef struct svr_runopts { #if DROPBEAR_SVR_LOCALANYFWD int nolocaltcp; #endif +#if DROPBEAR_SVR_REMOTESTREAMFWD + int streamlocalbindunlink; +#endif sign_key *hostkey; diff --git a/src/svr-runopts.c b/src/svr-runopts.c index 31a0393bc..23dded049 100644 --- a/src/svr-runopts.c +++ b/src/svr-runopts.c @@ -95,6 +95,9 @@ static void printhelp(const char * progname) { "-k Disable remote port forwarding\n" "-a Allow connections to forwarded ports from any host\n" "-c command Force executed command\n" +#endif +#if DROPBEAR_SVR_REMOTESTREAMFWD + "-S Unlink existing local file when client-side socket is forwarded\n" #endif "-p [address:]port\n" " Listen on specified tcp port (and optionally address),\n" @@ -186,6 +189,9 @@ void svr_getopts(int argc, char ** argv) { #if DROPBEAR_SVR_REMOTETCPFWD svr_opts.noremotetcp = 0; #endif +#if DROPBEAR_SVR_REMOTESTREAMFWD + svr_opts.streamlocalbindunlink = 0; +#endif #if DROPBEAR_PLUGIN svr_opts.pubkey_plugin = NULL; svr_opts.pubkey_plugin_options = NULL; @@ -272,6 +278,11 @@ void svr_getopts(int argc, char ** argv) { case 'k': break; #endif +#if DROPBEAR_SVR_REMOTESTREAMFWD + case 'S': + svr_opts.streamlocalbindunlink = 1; + break; +#endif #if INETD_MODE case 'i': svr_opts.inetdmode = 1; diff --git a/src/svr-tcpfwd.c b/src/svr-tcpfwd.c index 7d8f420c0..ca1ba956e 100644 --- a/src/svr-tcpfwd.c +++ b/src/svr-tcpfwd.c @@ -55,6 +55,10 @@ void recv_msg_global_request_remotetcp() { static int svr_cancelremotetcp(void); static int svr_remotetcpreq(int *allocated_listen_port); +#if DROPBEAR_SVR_REMOTESTREAMFWD +static int svr_cancelremotestreamlocal(void); +static int svr_remotestreamlocalreq(void); +#endif static int newtcpdirect(struct Channel * channel); #if DROPBEAR_SVR_LOCALSTREAMFWD static int newstreamlocal(struct Channel * channel); @@ -70,6 +74,17 @@ static const struct ChanType svr_chan_tcpremote = { NULL }; +#if DROPBEAR_SVR_REMOTESTREAMFWD +static const struct ChanType svr_chan_streamlocalremote = { + "forwarded-streamlocal@openssh.com", + NULL, + NULL, + NULL, + NULL, + NULL +}; +#endif + /* At the moment this is completely used for tcp code (with the name reflecting * that). If new request types are added, this should be replaced with code * similar to the request-switching in chansession.c */ @@ -108,6 +123,12 @@ void recv_msg_global_request_remotetcp() { } } else if (strcmp("cancel-tcpip-forward", reqname) == 0) { ret = svr_cancelremotetcp(); +#if DROPBEAR_SVR_REMOTESTREAMFWD + } else if (strcmp("streamlocal-forward@openssh.com", reqname) == 0) { + ret = svr_remotestreamlocalreq(); + } else if (strcmp("cancel-streamlocal-forward@openssh.com", reqname) == 0) { + ret = svr_cancelremotestreamlocal(); +#endif } else { TRACE(("reqname isn't tcpip-forward: '%s'", reqname)) } @@ -242,6 +263,255 @@ static int svr_remotetcpreq(int *allocated_listen_port) { return ret; } +#if DROPBEAR_SVR_REMOTESTREAMFWD +static int matchstreamlocal(const void* typedata1, const void* typedata2) { + + const struct TCPListener *info1 = (struct TCPListener*)typedata1; + const struct TCPListener *info2 = (struct TCPListener*)typedata2; + + if (info1->socket_path == NULL || info2->socket_path == NULL) { + return 0; + } + + return (info1->chantype == info2->chantype) + && (strcmp(info1->socket_path, info2->socket_path) == 0); +} + +static int svr_cancelremotestreamlocal() { + + int ret = DROPBEAR_FAILURE; + char * socket_path = NULL; + unsigned int pathlen; + struct Listener * listener = NULL; + struct TCPListener tcpinfo; + + TRACE(("enter cancelremotestreamlocal")) + + socket_path = buf_getstring(ses.payload, &pathlen); + if (pathlen > MAX_HOST_LEN) { + TRACE(("path len too long: %d", pathlen)) + goto out; + } + + tcpinfo.socket_path = socket_path; + tcpinfo.chantype = &svr_chan_streamlocalremote; + listener = get_listener(CHANNEL_ID_STREAMLOCALFORWARDED, &tcpinfo, matchstreamlocal); + if (listener) { + remove_listener( listener ); + ret = DROPBEAR_SUCCESS; + } + +out: + m_free(socket_path); + TRACE(("leave cancelremotestreamlocal")) + return ret; +} + +static void cleanup_streamlocal(const struct Listener *listener) { + + struct TCPListener *tcpinfo = (struct TCPListener*)(listener->typedata); + + if (tcpinfo && tcpinfo->socket_path) { + unlink(tcpinfo->socket_path); + m_free(tcpinfo->socket_path); + } + m_free(tcpinfo->request_listenaddr); + m_free(tcpinfo); +} + +static void streamlocal_acceptor(const struct Listener *listener, int sock) { + + int fd; + struct TCPListener *tcpinfo = (struct TCPListener*)(listener->typedata); + + fd = accept(sock, NULL, NULL); + if (fd < 0) { + return; + } + + if (send_msg_channel_open_init(fd, tcpinfo->chantype) == DROPBEAR_SUCCESS) { + /* "forwarded-streamlocal@openssh.com" */ + /* socket path that was connected to */ + buf_putstring(ses.writepayload, tcpinfo->request_listenaddr, + strlen(tcpinfo->request_listenaddr)); + /* reserved field */ + buf_putstring(ses.writepayload, "", 0); + + encrypt_packet(); + + } else { + /* XXX debug? */ + close(fd); + } +} + +int listen_streamlocal(struct TCPListener* tcpinfo, struct Listener **ret_listener) { + + int sock; + struct Listener *listener; + struct sockaddr_un addr; + mode_t old_umask; +#if DROPBEAR_SVR_MULTIUSER + uid_t uid = 0; + gid_t gid = 0; +#endif + + TRACE(("enter listen_streamlocal")) + + if (tcpinfo->socket_path == NULL) { + TRACE(("leave listen_streamlocal: no socket path")) + return DROPBEAR_FAILURE; + } + + if (strlen(tcpinfo->socket_path) >= sizeof(addr.sun_path)) { + dropbear_log(LOG_INFO, "Streamlocal forward failed: socket path too long"); + TRACE(("leave listen_streamlocal: path too long")) + return DROPBEAR_FAILURE; + } + + sock = socket(PF_UNIX, SOCK_STREAM, 0); + if (sock < 0) { + dropbear_log(LOG_INFO, "Streamlocal forward failed: socket() failed"); + TRACE(("leave listen_streamlocal: socket() failed")) + return DROPBEAR_FAILURE; + } + + memset((void*)&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strlcpy(addr.sun_path, tcpinfo->socket_path, sizeof(addr.sun_path)); + +#if DROPBEAR_SVR_MULTIUSER + /* Save current privileges and drop to authenticated user */ + uid = getuid(); + gid = getgid(); + if ((setegid(ses.authstate.pw_gid)) < 0) { + dropbear_log(LOG_WARNING, "Streamlocal forward failed: Failed to set egid"); + close(sock); + TRACE(("leave listen_streamlocal: failed to set egid")) + return DROPBEAR_FAILURE; + } + if ((seteuid(ses.authstate.pw_uid)) < 0) { + dropbear_log(LOG_WARNING, "Streamlocal forward failed: Failed to set euid"); + if (setegid(gid) < 0) { + dropbear_exit("Failed to revert egid"); + } + close(sock); + TRACE(("leave listen_streamlocal: failed to set euid")) + return DROPBEAR_FAILURE; + } +#endif + + /* Unlink existing socket if it exists */ + if (svr_opts.streamlocalbindunlink) { + unlink(tcpinfo->socket_path); + } + + /* Set umask to allow proper permissions on the socket */ + old_umask = umask(0177); + + if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + dropbear_log(LOG_INFO, "Streamlocal forward failed: bind() failed: %s", strerror(errno)); + close(sock); + umask(old_umask); +#if DROPBEAR_SVR_MULTIUSER + if ((seteuid(uid)) < 0 || + (setegid(gid)) < 0) { + dropbear_exit("Failed to revert euid"); + } +#endif + TRACE(("leave listen_streamlocal: bind() failed")) + return DROPBEAR_FAILURE; + } + + umask(old_umask); + +#if DROPBEAR_SVR_MULTIUSER + /* Restore privileges after binding */ + if ((seteuid(uid)) < 0 || + (setegid(gid)) < 0) { + unlink(tcpinfo->socket_path); + close(sock); + dropbear_exit("Failed to revert euid"); + } +#endif + + if (listen(sock, DROPBEAR_LISTEN_BACKLOG) < 0) { + dropbear_log(LOG_INFO, "Streamlocal forward failed: listen() failed: %s", strerror(errno)); + unlink(tcpinfo->socket_path); + close(sock); + TRACE(("leave listen_streamlocal: listen() failed")) + return DROPBEAR_FAILURE; + } + + setnonblocking(sock); + + listener = new_listener(&sock, 1, LISTENER_TYPE_STREAMFORWARDED, tcpinfo, + streamlocal_acceptor, cleanup_streamlocal); + + if (listener == NULL) { + unlink(tcpinfo->socket_path); + close(sock); + TRACE(("leave listen_streamlocal: listener failed")) + return DROPBEAR_FAILURE; + } + + if (ret_listener) { + *ret_listener = listener; + } + + TRACE(("leave listen_streamlocal: success")) + return DROPBEAR_SUCCESS; +} + +static int svr_remotestreamlocalreq() { + + int ret = DROPBEAR_FAILURE; + char * request_path = NULL; + unsigned int pathlen; + struct TCPListener *tcpinfo = NULL; + struct Listener *listener = NULL; + + TRACE(("enter remotestreamlocalreq")) + + if (svr_opts.noremotetcp || !svr_pubkey_allows_tcpfwd()) { + TRACE(("leave remotestreamlocalreq: remote forwarding disabled")) + goto out; + } + + request_path = buf_getstring(ses.payload, &pathlen); + if (pathlen > MAX_HOST_LEN) { + TRACE(("path len too long: %d", pathlen)) + goto out; + } + + tcpinfo = (struct TCPListener*)m_malloc(sizeof(struct TCPListener)); + memset(tcpinfo, 0, sizeof(struct TCPListener)); + tcpinfo->sendaddr = NULL; + tcpinfo->sendport = 0; + tcpinfo->listenaddr = NULL; + tcpinfo->listenport = 0; + tcpinfo->chantype = &svr_chan_streamlocalremote; + tcpinfo->tcp_type = forwarded; + tcpinfo->interface = NULL; + tcpinfo->socket_path = m_strdup(request_path); + tcpinfo->request_listenaddr = request_path; + + ret = listen_streamlocal(tcpinfo, &listener); + +out: + if (ret == DROPBEAR_FAILURE) { + /* we only free it if a listener wasn't created, since the listener + * has to remember it if it's to be cancelled */ + m_free(request_path); + m_free(tcpinfo->socket_path); + m_free(tcpinfo); + } + + TRACE(("leave remotestreamlocalreq")) + return ret; +} +#endif /* DROPBEAR_SVR_REMOTESTREAMFWD */ + #endif /* DROPBEAR_SVR_REMOTETCPFWD */ #if DROPBEAR_SVR_LOCALTCPFWD diff --git a/src/tcpfwd.h b/src/tcpfwd.h index 4172f2238..72de93519 100644 --- a/src/tcpfwd.h +++ b/src/tcpfwd.h @@ -46,6 +46,8 @@ struct TCPListener { const struct ChanType *chantype; enum {direct, forwarded} tcp_type; + /* For Unix socket forwarding, this is the socket path */ + char *socket_path; }; /* A forwarding entry */ @@ -74,5 +76,9 @@ void cli_recv_msg_request_failure(void); /* Common */ int listen_tcpfwd(struct TCPListener* tcpinfo, struct Listener **ret_listener); +#if DROPBEAR_SVR_REMOTESTREAMFWD +int listen_streamlocal(struct TCPListener* tcpinfo, struct Listener **ret_listener); +#define CHANNEL_ID_STREAMLOCALFORWARDED 0x53747265 +#endif #endif From 86d2dbf0891a1f965fc85f0f65f975e677841abb Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Sat, 2 May 2026 19:41:02 +0800 Subject: [PATCH 063/122] remote unixfwd: More checks, adapt for drop privs Since the remote stream forward code was written, Dropbear now drops privileges. The seteuid() etc can be removed. Stream forwarding isn't allowed when privilege dropping is disabled. Stream listeners are disallowed when a forced command is in effect - a unix socket in certain user-writable locations could possibly bypass command restrictions. --- src/svr-runopts.c | 4 +- src/svr-tcpfwd.c | 106 +++++++++++++++++++++------------------------- src/sysoptions.h | 5 ++- 3 files changed, 54 insertions(+), 61 deletions(-) diff --git a/src/svr-runopts.c b/src/svr-runopts.c index 23dded049..9ff73a531 100644 --- a/src/svr-runopts.c +++ b/src/svr-runopts.c @@ -89,10 +89,10 @@ static void printhelp(const char * progname) { #endif "-T Maximum authentication tries (default %d)\n" #if DROPBEAR_SVR_LOCALANYFWD - "-j Disable local port forwarding\n" + "-j Disable local port/stream forwarding\n" #endif #if DROPBEAR_SVR_REMOTETCPFWD - "-k Disable remote port forwarding\n" + "-k Disable remote port/stream forwarding\n" "-a Allow connections to forwarded ports from any host\n" "-c command Force executed command\n" #endif diff --git a/src/svr-tcpfwd.c b/src/svr-tcpfwd.c index ca1ba956e..dfec178f7 100644 --- a/src/svr-tcpfwd.c +++ b/src/svr-tcpfwd.c @@ -266,8 +266,8 @@ static int svr_remotetcpreq(int *allocated_listen_port) { #if DROPBEAR_SVR_REMOTESTREAMFWD static int matchstreamlocal(const void* typedata1, const void* typedata2) { - const struct TCPListener *info1 = (struct TCPListener*)typedata1; - const struct TCPListener *info2 = (struct TCPListener*)typedata2; + const struct TCPListener *info1 = (const struct TCPListener*)typedata1; + const struct TCPListener *info2 = (const struct TCPListener*)typedata2; if (info1->socket_path == NULL || info2->socket_path == NULL) { return 0; @@ -292,6 +292,10 @@ static int svr_cancelremotestreamlocal() { TRACE(("path len too long: %d", pathlen)) goto out; } + if (strlen(socket_path) != pathlen) { + TRACE(("path has nul byte")); + goto out; + } tcpinfo.socket_path = socket_path; tcpinfo.chantype = &svr_chan_streamlocalremote; @@ -307,12 +311,20 @@ static int svr_cancelremotestreamlocal() { return ret; } +static void unlink_streamsocket(const char* path) { + if (unlink(path) < 0 && errno != ENOENT) { + /* Not fatal */ + DEBUG1(("Failed removing unix socket %s: %s", + path, strerror(errno))); + } +} + static void cleanup_streamlocal(const struct Listener *listener) { struct TCPListener *tcpinfo = (struct TCPListener*)(listener->typedata); if (tcpinfo && tcpinfo->socket_path) { - unlink(tcpinfo->socket_path); + unlink_streamsocket(tcpinfo->socket_path); m_free(tcpinfo->socket_path); } m_free(tcpinfo->request_listenaddr); @@ -347,14 +359,10 @@ static void streamlocal_acceptor(const struct Listener *listener, int sock) { int listen_streamlocal(struct TCPListener* tcpinfo, struct Listener **ret_listener) { - int sock; - struct Listener *listener; + int sock, rc, saved_errno; + struct Listener *listener = NULL; struct sockaddr_un addr; mode_t old_umask; -#if DROPBEAR_SVR_MULTIUSER - uid_t uid = 0; - gid_t gid = 0; -#endif TRACE(("enter listen_streamlocal")) @@ -369,6 +377,13 @@ int listen_streamlocal(struct TCPListener* tcpinfo, struct Listener **ret_listen return DROPBEAR_FAILURE; } +#if DROPBEAR_FUZZ + if (fuzz.fuzzing) { + // fuzzing streamlocal is unimplemented + return DROPBEAR_FAILURE; + } +#endif + sock = socket(PF_UNIX, SOCK_STREAM, 0); if (sock < 0) { dropbear_log(LOG_INFO, "Streamlocal forward failed: socket() failed"); @@ -376,69 +391,33 @@ int listen_streamlocal(struct TCPListener* tcpinfo, struct Listener **ret_listen return DROPBEAR_FAILURE; } - memset((void*)&addr, 0, sizeof(addr)); + memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; strlcpy(addr.sun_path, tcpinfo->socket_path, sizeof(addr.sun_path)); -#if DROPBEAR_SVR_MULTIUSER - /* Save current privileges and drop to authenticated user */ - uid = getuid(); - gid = getgid(); - if ((setegid(ses.authstate.pw_gid)) < 0) { - dropbear_log(LOG_WARNING, "Streamlocal forward failed: Failed to set egid"); - close(sock); - TRACE(("leave listen_streamlocal: failed to set egid")) - return DROPBEAR_FAILURE; - } - if ((seteuid(ses.authstate.pw_uid)) < 0) { - dropbear_log(LOG_WARNING, "Streamlocal forward failed: Failed to set euid"); - if (setegid(gid) < 0) { - dropbear_exit("Failed to revert egid"); - } - close(sock); - TRACE(("leave listen_streamlocal: failed to set euid")) - return DROPBEAR_FAILURE; - } -#endif - /* Unlink existing socket if it exists */ if (svr_opts.streamlocalbindunlink) { - unlink(tcpinfo->socket_path); + unlink_streamsocket(tcpinfo->socket_path); } /* Set umask to allow proper permissions on the socket */ old_umask = umask(0177); + rc = bind(sock, (struct sockaddr*)&addr, sizeof(addr)); + saved_errno = errno; + umask(old_umask); - if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { - dropbear_log(LOG_INFO, "Streamlocal forward failed: bind() failed: %s", strerror(errno)); - close(sock); - umask(old_umask); -#if DROPBEAR_SVR_MULTIUSER - if ((seteuid(uid)) < 0 || - (setegid(gid)) < 0) { - dropbear_exit("Failed to revert euid"); - } -#endif + if (rc < 0) { + dropbear_log(LOG_INFO, "Streamlocal forward failed: bind() failed: %s", strerror(saved_errno)); + m_close(sock); TRACE(("leave listen_streamlocal: bind() failed")) return DROPBEAR_FAILURE; } - umask(old_umask); - -#if DROPBEAR_SVR_MULTIUSER - /* Restore privileges after binding */ - if ((seteuid(uid)) < 0 || - (setegid(gid)) < 0) { - unlink(tcpinfo->socket_path); - close(sock); - dropbear_exit("Failed to revert euid"); - } -#endif if (listen(sock, DROPBEAR_LISTEN_BACKLOG) < 0) { dropbear_log(LOG_INFO, "Streamlocal forward failed: listen() failed: %s", strerror(errno)); - unlink(tcpinfo->socket_path); - close(sock); + unlink_streamsocket(tcpinfo->socket_path); + m_close(sock); TRACE(("leave listen_streamlocal: listen() failed")) return DROPBEAR_FAILURE; } @@ -449,8 +428,8 @@ int listen_streamlocal(struct TCPListener* tcpinfo, struct Listener **ret_listen streamlocal_acceptor, cleanup_streamlocal); if (listener == NULL) { - unlink(tcpinfo->socket_path); - close(sock); + unlink_streamsocket(tcpinfo->socket_path); + m_close(sock); TRACE(("leave listen_streamlocal: listener failed")) return DROPBEAR_FAILURE; } @@ -473,6 +452,15 @@ static int svr_remotestreamlocalreq() { TRACE(("enter remotestreamlocalreq")) + if (svr_opts.forced_command || svr_pubkey_has_forced_command()) { + /* Creating a unix socket in the right place could probably subvert + * a forcedcommand, so don't allow that. + * This could be relaxed if an authorized_keys "permitlisten" + * equivalent were added for streamlocal */ + TRACE(("leave newstreamlocal: no unix forwarding for forced command")) + goto out; + } + if (svr_opts.noremotetcp || !svr_pubkey_allows_tcpfwd()) { TRACE(("leave remotestreamlocalreq: remote forwarding disabled")) goto out; @@ -483,6 +471,10 @@ static int svr_remotestreamlocalreq() { TRACE(("path len too long: %d", pathlen)) goto out; } + if (strlen(request_path) != pathlen) { + TRACE(("path has nul byte")); + goto out; + } tcpinfo = (struct TCPListener*)m_malloc(sizeof(struct TCPListener)); memset(tcpinfo, 0, sizeof(struct TCPListener)); diff --git a/src/sysoptions.h b/src/sysoptions.h index 0779fc8ae..cccd81e23 100644 --- a/src/sysoptions.h +++ b/src/sysoptions.h @@ -452,8 +452,9 @@ #error DROPBEAR_SVR_DROP_PRIVS needs DROPBEAR_SVR_MULTIUSER #endif -#if !(DROPBEAR_SVR_DROP_PRIVS || !DROPBEAR_SVR_MULTIUSER) && DROPBEAR_SVR_LOCALSTREAMFWD -#error DROPBEAR_SVR_LOCALSTREAMFWD requires DROPBEAR_SVR_DROP_PRIVS or !DROPBEAR_SVR_MULTIUSER +#if !(DROPBEAR_SVR_DROP_PRIVS || !DROPBEAR_SVR_MULTIUSER) \ + && (DROPBEAR_SVR_LOCALSTREAMFWD || DROPBEAR_SVR_LOCALSTREAMFWD) +#error stream forwarding requires DROPBEAR_SVR_DROP_PRIVS or !DROPBEAR_SVR_MULTIUSER #endif /* Fuzzing expects all key types to be enabled */ From 05f524a5423fcd37be95a2a93789076d05c8a296 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Sat, 2 May 2026 19:47:55 +0800 Subject: [PATCH 064/122] remote unixfwd: Always unlink existing socket The streamlocalbindunlink setting is now always enabled - having to rely on external mechanisms to clean up stale sockets is awkward. --- src/runopts.h | 3 --- src/svr-runopts.c | 11 ----------- src/svr-tcpfwd.c | 4 +--- 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/runopts.h b/src/runopts.h index 827f3db9c..210e69d08 100644 --- a/src/runopts.h +++ b/src/runopts.h @@ -113,9 +113,6 @@ typedef struct svr_runopts { #if DROPBEAR_SVR_LOCALANYFWD int nolocaltcp; #endif -#if DROPBEAR_SVR_REMOTESTREAMFWD - int streamlocalbindunlink; -#endif sign_key *hostkey; diff --git a/src/svr-runopts.c b/src/svr-runopts.c index 9ff73a531..b0749c1f5 100644 --- a/src/svr-runopts.c +++ b/src/svr-runopts.c @@ -95,9 +95,6 @@ static void printhelp(const char * progname) { "-k Disable remote port/stream forwarding\n" "-a Allow connections to forwarded ports from any host\n" "-c command Force executed command\n" -#endif -#if DROPBEAR_SVR_REMOTESTREAMFWD - "-S Unlink existing local file when client-side socket is forwarded\n" #endif "-p [address:]port\n" " Listen on specified tcp port (and optionally address),\n" @@ -189,9 +186,6 @@ void svr_getopts(int argc, char ** argv) { #if DROPBEAR_SVR_REMOTETCPFWD svr_opts.noremotetcp = 0; #endif -#if DROPBEAR_SVR_REMOTESTREAMFWD - svr_opts.streamlocalbindunlink = 0; -#endif #if DROPBEAR_PLUGIN svr_opts.pubkey_plugin = NULL; svr_opts.pubkey_plugin_options = NULL; @@ -278,11 +272,6 @@ void svr_getopts(int argc, char ** argv) { case 'k': break; #endif -#if DROPBEAR_SVR_REMOTESTREAMFWD - case 'S': - svr_opts.streamlocalbindunlink = 1; - break; -#endif #if INETD_MODE case 'i': svr_opts.inetdmode = 1; diff --git a/src/svr-tcpfwd.c b/src/svr-tcpfwd.c index dfec178f7..ff7db6fe2 100644 --- a/src/svr-tcpfwd.c +++ b/src/svr-tcpfwd.c @@ -396,9 +396,7 @@ int listen_streamlocal(struct TCPListener* tcpinfo, struct Listener **ret_listen strlcpy(addr.sun_path, tcpinfo->socket_path, sizeof(addr.sun_path)); /* Unlink existing socket if it exists */ - if (svr_opts.streamlocalbindunlink) { - unlink_streamsocket(tcpinfo->socket_path); - } + unlink_streamsocket(tcpinfo->socket_path); /* Set umask to allow proper permissions on the socket */ old_umask = umask(0177); From 247d5dc698f5f0d41d89e1a9a8e4cd2fad75ab7a Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Sat, 2 May 2026 19:55:19 +0800 Subject: [PATCH 065/122] server: -c help text was hidden without remote TCP --- src/svr-runopts.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/svr-runopts.c b/src/svr-runopts.c index b0749c1f5..52cb44a01 100644 --- a/src/svr-runopts.c +++ b/src/svr-runopts.c @@ -94,8 +94,8 @@ static void printhelp(const char * progname) { #if DROPBEAR_SVR_REMOTETCPFWD "-k Disable remote port/stream forwarding\n" "-a Allow connections to forwarded ports from any host\n" - "-c command Force executed command\n" #endif + "-c command Force executed command\n" "-p [address:]port\n" " Listen on specified tcp port (and optionally address),\n" " up to %d can be specified\n" From 669343953c1782a35e0323f3e9d0b02d68de5882 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Sat, 2 May 2026 20:56:32 +0800 Subject: [PATCH 066/122] Rename tcpfwd filename to forward Now handles stream forwarding as well --- Makefile.in | 2 +- src/cli-runopts.c | 2 +- src/cli-session.c | 2 +- src/cli-tcpfwd.c | 2 +- src/{tcpfwd.h => forward.h} | 0 src/runopts.h | 2 +- src/session.h | 2 +- src/{svr-tcpfwd.c => svr-forward.c} | 4 ++-- src/svr-session.c | 2 +- src/tcp-accept.c | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) rename src/{tcpfwd.h => forward.h} (100%) rename src/{svr-tcpfwd.c => svr-forward.c} (99%) diff --git a/Makefile.in b/Makefile.in index eabc7e342..ed4513cd1 100644 --- a/Makefile.in +++ b/Makefile.in @@ -51,7 +51,7 @@ COMMONOBJS = $(patsubst %,$(OBJ_DIR)/%,$(_COMMONOBJS)) _SVROBJS=svr-kex.o svr-auth.o sshpty.o \ svr-authpasswd.o svr-authpubkey.o svr-authpubkeyoptions.o svr-session.o svr-service.o \ svr-chansession.o svr-runopts.o svr-agentfwd.o svr-main.o svr-x11fwd.o\ - svr-tcpfwd.o svr-authpam.o + svr-forward.o svr-authpam.o SVROBJS = $(patsubst %,$(OBJ_DIR)/%,$(_SVROBJS)) _CLIOBJS=cli-main.o cli-auth.o cli-authpasswd.o cli-kex.o \ diff --git a/src/cli-runopts.c b/src/cli-runopts.c index 3853c96fc..a223437fe 100644 --- a/src/cli-runopts.c +++ b/src/cli-runopts.c @@ -28,7 +28,7 @@ #include "buffer.h" #include "dbutil.h" #include "algo.h" -#include "tcpfwd.h" +#include "forward.h" #include "list.h" cli_runopts cli_opts; /* GLOBAL */ diff --git a/src/cli-session.c b/src/cli-session.c index 6cbbf5bfb..09b6818e2 100644 --- a/src/cli-session.c +++ b/src/cli-session.c @@ -29,7 +29,7 @@ #include "kex.h" #include "ssh.h" #include "packet.h" -#include "tcpfwd.h" +#include "forward.h" #include "channel.h" #include "dbrandom.h" #include "service.h" diff --git a/src/cli-tcpfwd.c b/src/cli-tcpfwd.c index 1b9561506..8f84af479 100644 --- a/src/cli-tcpfwd.c +++ b/src/cli-tcpfwd.c @@ -24,7 +24,7 @@ #include "includes.h" #include "dbutil.h" -#include "tcpfwd.h" +#include "forward.h" #include "channel.h" #include "runopts.h" #include "session.h" diff --git a/src/tcpfwd.h b/src/forward.h similarity index 100% rename from src/tcpfwd.h rename to src/forward.h diff --git a/src/runopts.h b/src/runopts.h index 210e69d08..4cb9ff644 100644 --- a/src/runopts.h +++ b/src/runopts.h @@ -29,7 +29,7 @@ #include "signkey.h" #include "buffer.h" #include "auth.h" -#include "tcpfwd.h" +#include "forward.h" typedef struct runopts { diff --git a/src/session.h b/src/session.h index e1a5cfa0d..da617b8e9 100644 --- a/src/session.h +++ b/src/session.h @@ -34,7 +34,7 @@ #include "queue.h" #include "listener.h" #include "packet.h" -#include "tcpfwd.h" +#include "forward.h" #include "chansession.h" #include "dbutil.h" #include "netio.h" diff --git a/src/svr-tcpfwd.c b/src/svr-forward.c similarity index 99% rename from src/svr-tcpfwd.c rename to src/svr-forward.c index ff7db6fe2..4304d81ac 100644 --- a/src/svr-tcpfwd.c +++ b/src/svr-forward.c @@ -25,7 +25,7 @@ #include "includes.h" #include "ssh.h" -#include "tcpfwd.h" +#include "forward.h" #include "dbutil.h" #include "session.h" #include "buffer.h" @@ -35,7 +35,7 @@ #include "auth.h" #include "netio.h" -#if !DROPBEAR_SVR_REMOTETCPFWD +#if !(DROPBEAR_SVR_REMOTETCPFWD || DROPBEAR_SVR_REMOTESTREAMFWD) /* This is better than SSH_MSG_UNIMPLEMENTED */ void recv_msg_global_request_remotetcp() { diff --git a/src/svr-session.c b/src/svr-session.c index 8f4569552..aa3d94e21 100644 --- a/src/svr-session.c +++ b/src/svr-session.c @@ -35,7 +35,7 @@ #include "channel.h" #include "chansession.h" #include "atomicio.h" -#include "tcpfwd.h" +#include "forward.h" #include "service.h" #include "auth.h" #include "runopts.h" diff --git a/src/tcp-accept.c b/src/tcp-accept.c index a8ae32f95..5ec57cfe2 100644 --- a/src/tcp-accept.c +++ b/src/tcp-accept.c @@ -24,7 +24,7 @@ #include "includes.h" #include "ssh.h" -#include "tcpfwd.h" +#include "forward.h" #include "dbutil.h" #include "session.h" #include "buffer.h" From de38e41ca2bc62fc1037268afc81a981a963f273 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Sat, 2 May 2026 21:50:18 +0800 Subject: [PATCH 067/122] Split tcp and stream forwarding files This mainly affects server handling. TcpListener is renamed to FwdListener, svr-tcpfwd.c is split into svr-forward.c, svr-tcpfwd.c, and svr-streamfwd.c --- Makefile.in | 2 +- src/cli-tcpfwd.c | 6 +- src/forward.h | 31 ++- src/runopts.h | 4 +- src/svr-forward.c | 643 ++++---------------------------------------- src/svr-runopts.c | 22 +- src/svr-session.c | 2 +- src/svr-streamfwd.c | 323 ++++++++++++++++++++++ src/svr-tcpfwd.c | 215 +++++++++++++++ src/sysoptions.h | 3 +- src/tcp-accept.c | 10 +- 11 files changed, 642 insertions(+), 619 deletions(-) create mode 100644 src/svr-streamfwd.c create mode 100644 src/svr-tcpfwd.c diff --git a/Makefile.in b/Makefile.in index ed4513cd1..a26f01127 100644 --- a/Makefile.in +++ b/Makefile.in @@ -51,7 +51,7 @@ COMMONOBJS = $(patsubst %,$(OBJ_DIR)/%,$(_COMMONOBJS)) _SVROBJS=svr-kex.o svr-auth.o sshpty.o \ svr-authpasswd.o svr-authpubkey.o svr-authpubkeyoptions.o svr-session.o svr-service.o \ svr-chansession.o svr-runopts.o svr-agentfwd.o svr-main.o svr-x11fwd.o\ - svr-forward.o svr-authpam.o + svr-forward.o svr-tcpfwd.o svr-streamfwd.o svr-authpam.o SVROBJS = $(patsubst %,$(OBJ_DIR)/%,$(_SVROBJS)) _CLIOBJS=cli-main.o cli-auth.o cli-authpasswd.o cli-kex.o \ diff --git a/src/cli-tcpfwd.c b/src/cli-tcpfwd.c index 8f84af479..b5f4a5521 100644 --- a/src/cli-tcpfwd.c +++ b/src/cli-tcpfwd.c @@ -107,13 +107,13 @@ static int cli_localtcp(const char* listenaddr, const char* remoteaddr, unsigned int remoteport) { - struct TCPListener* tcpinfo = NULL; + struct FwdListener* tcpinfo = NULL; int ret; TRACE(("enter cli_localtcp: %d %s %d", listenport, remoteaddr, remoteport)); - tcpinfo = (struct TCPListener*)m_malloc(sizeof(struct TCPListener)); + tcpinfo = (struct FwdListener*)m_malloc(sizeof(struct FwdListener)); tcpinfo->sendaddr = m_strdup(remoteaddr); tcpinfo->sendport = remoteport; @@ -133,7 +133,7 @@ static int cli_localtcp(const char* listenaddr, tcpinfo->listenport = listenport; tcpinfo->chantype = &cli_chan_tcplocal; - tcpinfo->tcp_type = direct; + tcpinfo->fwd_type = direct; ret = listen_tcpfwd(tcpinfo, NULL); diff --git a/src/forward.h b/src/forward.h index 72de93519..87d8e64a3 100644 --- a/src/forward.h +++ b/src/forward.h @@ -21,14 +21,15 @@ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -#ifndef DROPBEAR_TCPFWD_H -#define DROPBEAR_TCPFWD_H +#ifndef DROPBEAR_FORWARD_H +#define DROPBEAR_FORWARD_H #include "channel.h" #include "list.h" #include "listener.h" -struct TCPListener { +/* For TCP or stream listeners */ +struct FwdListener { /* For a direct-tcpip request, it's the addr/port we want the other * end to connect to */ @@ -45,7 +46,7 @@ struct TCPListener { char* interface; const struct ChanType *chantype; - enum {direct, forwarded} tcp_type; + enum {direct, forwarded} fwd_type; /* For Unix socket forwarding, this is the socket path */ char *socket_path; }; @@ -61,11 +62,25 @@ struct TCPFwdEntry { }; /* Server */ -void recv_msg_global_request_remotetcp(void); +void svr_recv_msg_global_request(void); -extern const struct ChanType svr_chan_tcpdirect; +#if DROPBEAR_SVR_REMOTESTREAMFWD +int svr_cancelremotestreamlocal(void); +int svr_remotestreamlocalreq(void); +#endif +#if DROPBEAR_SVR_LOCALSTREAMFWD extern const struct ChanType svr_chan_streamlocal; +#endif + +#if DROPBEAR_SVR_REMOTETCPFWD +int svr_remotetcpreq(int *allocated_listen_port); +int svr_cancelremotetcp(void); +#endif + +#if DROPBEAR_SVR_LOCALTCPFWD +extern const struct ChanType svr_chan_tcpdirect; +#endif /* Client */ void setup_localtcp(void); @@ -75,9 +90,9 @@ void cli_recv_msg_request_success(void); void cli_recv_msg_request_failure(void); /* Common */ -int listen_tcpfwd(struct TCPListener* tcpinfo, struct Listener **ret_listener); +int listen_tcpfwd(struct FwdListener* tcpinfo, struct Listener **ret_listener); #if DROPBEAR_SVR_REMOTESTREAMFWD -int listen_streamlocal(struct TCPListener* tcpinfo, struct Listener **ret_listener); +int listen_streamlocal(struct FwdListener* tcpinfo, struct Listener **ret_listener); #define CHANNEL_ID_STREAMLOCALFORWARDED 0x53747265 #endif diff --git a/src/runopts.h b/src/runopts.h index 4cb9ff644..e22458ce8 100644 --- a/src/runopts.h +++ b/src/runopts.h @@ -107,8 +107,8 @@ typedef struct svr_runopts { int multiauthmethod; unsigned int maxauthtries; -#if DROPBEAR_SVR_REMOTETCPFWD - int noremotetcp; +#if DROPBEAR_SVR_REMOTEANYFWD + int noremotefwd; #endif #if DROPBEAR_SVR_LOCALANYFWD int nolocaltcp; diff --git a/src/svr-forward.c b/src/svr-forward.c index 4304d81ac..6d61e0d94 100644 --- a/src/svr-forward.c +++ b/src/svr-forward.c @@ -33,609 +33,72 @@ #include "listener.h" #include "runopts.h" #include "auth.h" -#include "netio.h" -#if !(DROPBEAR_SVR_REMOTETCPFWD || DROPBEAR_SVR_REMOTESTREAMFWD) +void svr_recv_msg_global_request(void) { -/* This is better than SSH_MSG_UNIMPLEMENTED */ -void recv_msg_global_request_remotetcp() { - unsigned int wantreply = 0; + char* reqname = NULL; + unsigned int namelen; + unsigned int wantreply = 0; + int ret = DROPBEAR_FAILURE; - TRACE(("recv_msg_global_request_remotetcp: remote tcp forwarding not compiled in")) + TRACE(("enter recv_msg_global_request_remote")) - buf_eatstring(ses.payload); - wantreply = buf_getbool(ses.payload); - if (wantreply) { - send_msg_request_failure(); - } -} - -/* */ -#endif /* !DROPBEAR_SVR_REMOTETCPFWD */ + reqname = buf_getstring(ses.payload, &namelen); + wantreply = buf_getbool(ses.payload); -static int svr_cancelremotetcp(void); -static int svr_remotetcpreq(int *allocated_listen_port); -#if DROPBEAR_SVR_REMOTESTREAMFWD -static int svr_cancelremotestreamlocal(void); -static int svr_remotestreamlocalreq(void); -#endif -static int newtcpdirect(struct Channel * channel); -#if DROPBEAR_SVR_LOCALSTREAMFWD -static int newstreamlocal(struct Channel * channel); +#if DROPBEAR_SVR_REMOTEANYFWD + if (svr_opts.noremotefwd || !svr_pubkey_allows_tcpfwd()) { + TRACE(("remote forwarding is disabled")); + goto out; + } +#else + /* No request handlers */ + goto out; #endif -#if DROPBEAR_SVR_REMOTETCPFWD -static const struct ChanType svr_chan_tcpremote = { - "forwarded-tcpip", - NULL, - NULL, - NULL, - NULL, - NULL -}; + if (namelen > MAX_NAME_LEN) { + TRACE(("name len is wrong: %d", namelen)) + goto out; + } -#if DROPBEAR_SVR_REMOTESTREAMFWD -static const struct ChanType svr_chan_streamlocalremote = { - "forwarded-streamlocal@openssh.com", - NULL, - NULL, - NULL, - NULL, - NULL -}; + if (0) {} +#if DROPBEAR_SVR_REMOTETCPFWD + else if (strcmp("tcpip-forward", reqname) == 0) { + int allocated_listen_port = 0; + ret = svr_remotetcpreq(&allocated_listen_port); + /* client expects-port-number-to-make-use-of-server-allocated-ports */ + if (DROPBEAR_SUCCESS == ret) { + CHECKCLEARTOWRITE(); + buf_putbyte(ses.writepayload, SSH_MSG_REQUEST_SUCCESS); + buf_putint(ses.writepayload, allocated_listen_port); + encrypt_packet(); + wantreply = 0; /* avoid out: below sending another reply */ + } + } else if (strcmp("cancel-tcpip-forward", reqname) == 0) { + ret = svr_cancelremotetcp(); + } #endif - -/* At the moment this is completely used for tcp code (with the name reflecting - * that). If new request types are added, this should be replaced with code - * similar to the request-switching in chansession.c */ -void recv_msg_global_request_remotetcp() { - - char* reqname = NULL; - unsigned int namelen; - unsigned int wantreply = 0; - int ret = DROPBEAR_FAILURE; - - TRACE(("enter recv_msg_global_request_remotetcp")) - - reqname = buf_getstring(ses.payload, &namelen); - wantreply = buf_getbool(ses.payload); - - if (svr_opts.noremotetcp || !svr_pubkey_allows_tcpfwd()) { - TRACE(("leave recv_msg_global_request_remotetcp: remote tcp forwarding disabled")) - goto out; - } - - if (namelen > MAX_NAME_LEN) { - TRACE(("name len is wrong: %d", namelen)) - goto out; - } - - if (strcmp("tcpip-forward", reqname) == 0) { - int allocated_listen_port = 0; - ret = svr_remotetcpreq(&allocated_listen_port); - /* client expects-port-number-to-make-use-of-server-allocated-ports */ - if (DROPBEAR_SUCCESS == ret) { - CHECKCLEARTOWRITE(); - buf_putbyte(ses.writepayload, SSH_MSG_REQUEST_SUCCESS); - buf_putint(ses.writepayload, allocated_listen_port); - encrypt_packet(); - wantreply = 0; /* avoid out: below sending another reply */ - } - } else if (strcmp("cancel-tcpip-forward", reqname) == 0) { - ret = svr_cancelremotetcp(); #if DROPBEAR_SVR_REMOTESTREAMFWD - } else if (strcmp("streamlocal-forward@openssh.com", reqname) == 0) { - ret = svr_remotestreamlocalreq(); - } else if (strcmp("cancel-streamlocal-forward@openssh.com", reqname) == 0) { - ret = svr_cancelremotestreamlocal(); + else if (strcmp("streamlocal-forward@openssh.com", reqname) == 0) { + ret = svr_remotestreamlocalreq(); + } else if (strcmp("cancel-streamlocal-forward@openssh.com", reqname) == 0) { + ret = svr_cancelremotestreamlocal(); + } #endif - } else { - TRACE(("reqname isn't tcpip-forward: '%s'", reqname)) - } + else { + TRACE(("unhandled request '%s'", reqname)) + } out: - if (wantreply) { - if (ret == DROPBEAR_SUCCESS) { - send_msg_request_success(); - } else { - send_msg_request_failure(); - } - } - - m_free(reqname); - - TRACE(("leave recv_msg_global_request")) -} - -static int matchtcp(const void* typedata1, const void* typedata2) { + if (wantreply) { + if (ret == DROPBEAR_SUCCESS) { + send_msg_request_success(); + } else { + send_msg_request_failure(); + } + } - const struct TCPListener *info1 = (struct TCPListener*)typedata1; - const struct TCPListener *info2 = (struct TCPListener*)typedata2; + m_free(reqname); - return (info1->listenport == info2->listenport) - && (strcmp(info1->request_listenaddr, info2->request_listenaddr) == 0); + TRACE(("leave recv_msg_global_request")) } - -static int svr_cancelremotetcp() { - - int ret = DROPBEAR_FAILURE; - char * request_addr = NULL; - unsigned int addrlen; - unsigned int port; - struct Listener * listener = NULL; - struct TCPListener tcpinfo; - - TRACE(("enter cancelremotetcp")) - - request_addr = buf_getstring(ses.payload, &addrlen); - if (addrlen > MAX_HOST_LEN) { - TRACE(("addr len too long: %d", addrlen)) - goto out; - } - - port = buf_getint(ses.payload); - - memset(&tcpinfo, 0x0, sizeof(tcpinfo)); - tcpinfo.request_listenaddr = request_addr; - tcpinfo.listenport = port; - listener = get_listener(LISTENER_TYPE_TCPFORWARDED, &tcpinfo, matchtcp); - if (listener) { - remove_listener(listener); - ret = DROPBEAR_SUCCESS; - } - -out: - m_free(request_addr); - TRACE(("leave cancelremotetcp")) - return ret; -} - -static int svr_remotetcpreq(int *allocated_listen_port) { - - int ret = DROPBEAR_FAILURE; - char * request_addr = NULL; - unsigned int addrlen; - struct TCPListener *tcpinfo = NULL; - unsigned int port; - struct Listener *listener = NULL; - - TRACE(("enter remotetcpreq")) - - request_addr = buf_getstring(ses.payload, &addrlen); - if (addrlen > MAX_HOST_LEN) { - TRACE(("addr len too long: %d", addrlen)) - goto out; - } - - port = buf_getint(ses.payload); - - if (port != 0) { - if (port < 1 || port > 65535) { - TRACE(("invalid port: %d", port)) - goto out; - } - - if (!ses.allowprivport && port < IPPORT_RESERVED) { - TRACE(("can't assign port < 1024 for non-root")) - goto out; - } - } - - if (!svr_pubkey_allows_remote_tcpfwd(request_addr, port)) { - TRACE(("remote tcp forwarding listen address not permitted")); - goto out; - } - - tcpinfo = (struct TCPListener*)m_malloc(sizeof(struct TCPListener)); - tcpinfo->sendaddr = NULL; - tcpinfo->sendport = 0; - tcpinfo->listenport = port; - tcpinfo->chantype = &svr_chan_tcpremote; - tcpinfo->tcp_type = forwarded; - tcpinfo->interface = svr_opts.interface; - - tcpinfo->request_listenaddr = request_addr; - if (!opts.listen_fwd_all || (strcmp(request_addr, "localhost") == 0) ) { - /* NULL means "localhost only" */ - tcpinfo->listenaddr = NULL; - } - else - { - tcpinfo->listenaddr = m_strdup(request_addr); - } - - ret = listen_tcpfwd(tcpinfo, &listener); - if (DROPBEAR_SUCCESS == ret) { - tcpinfo->listenport = get_sock_port(listener->socks[0]); - *allocated_listen_port = tcpinfo->listenport; - } - -out: - if (ret == DROPBEAR_FAILURE) { - /* we only free it if a listener wasn't created, since the listener - * has to remember it if it's to be cancelled */ - m_free(request_addr); - m_free(tcpinfo); - } - - TRACE(("leave remotetcpreq")) - - return ret; -} - -#if DROPBEAR_SVR_REMOTESTREAMFWD -static int matchstreamlocal(const void* typedata1, const void* typedata2) { - - const struct TCPListener *info1 = (const struct TCPListener*)typedata1; - const struct TCPListener *info2 = (const struct TCPListener*)typedata2; - - if (info1->socket_path == NULL || info2->socket_path == NULL) { - return 0; - } - - return (info1->chantype == info2->chantype) - && (strcmp(info1->socket_path, info2->socket_path) == 0); -} - -static int svr_cancelremotestreamlocal() { - - int ret = DROPBEAR_FAILURE; - char * socket_path = NULL; - unsigned int pathlen; - struct Listener * listener = NULL; - struct TCPListener tcpinfo; - - TRACE(("enter cancelremotestreamlocal")) - - socket_path = buf_getstring(ses.payload, &pathlen); - if (pathlen > MAX_HOST_LEN) { - TRACE(("path len too long: %d", pathlen)) - goto out; - } - if (strlen(socket_path) != pathlen) { - TRACE(("path has nul byte")); - goto out; - } - - tcpinfo.socket_path = socket_path; - tcpinfo.chantype = &svr_chan_streamlocalremote; - listener = get_listener(CHANNEL_ID_STREAMLOCALFORWARDED, &tcpinfo, matchstreamlocal); - if (listener) { - remove_listener( listener ); - ret = DROPBEAR_SUCCESS; - } - -out: - m_free(socket_path); - TRACE(("leave cancelremotestreamlocal")) - return ret; -} - -static void unlink_streamsocket(const char* path) { - if (unlink(path) < 0 && errno != ENOENT) { - /* Not fatal */ - DEBUG1(("Failed removing unix socket %s: %s", - path, strerror(errno))); - } -} - -static void cleanup_streamlocal(const struct Listener *listener) { - - struct TCPListener *tcpinfo = (struct TCPListener*)(listener->typedata); - - if (tcpinfo && tcpinfo->socket_path) { - unlink_streamsocket(tcpinfo->socket_path); - m_free(tcpinfo->socket_path); - } - m_free(tcpinfo->request_listenaddr); - m_free(tcpinfo); -} - -static void streamlocal_acceptor(const struct Listener *listener, int sock) { - - int fd; - struct TCPListener *tcpinfo = (struct TCPListener*)(listener->typedata); - - fd = accept(sock, NULL, NULL); - if (fd < 0) { - return; - } - - if (send_msg_channel_open_init(fd, tcpinfo->chantype) == DROPBEAR_SUCCESS) { - /* "forwarded-streamlocal@openssh.com" */ - /* socket path that was connected to */ - buf_putstring(ses.writepayload, tcpinfo->request_listenaddr, - strlen(tcpinfo->request_listenaddr)); - /* reserved field */ - buf_putstring(ses.writepayload, "", 0); - - encrypt_packet(); - - } else { - /* XXX debug? */ - close(fd); - } -} - -int listen_streamlocal(struct TCPListener* tcpinfo, struct Listener **ret_listener) { - - int sock, rc, saved_errno; - struct Listener *listener = NULL; - struct sockaddr_un addr; - mode_t old_umask; - - TRACE(("enter listen_streamlocal")) - - if (tcpinfo->socket_path == NULL) { - TRACE(("leave listen_streamlocal: no socket path")) - return DROPBEAR_FAILURE; - } - - if (strlen(tcpinfo->socket_path) >= sizeof(addr.sun_path)) { - dropbear_log(LOG_INFO, "Streamlocal forward failed: socket path too long"); - TRACE(("leave listen_streamlocal: path too long")) - return DROPBEAR_FAILURE; - } - -#if DROPBEAR_FUZZ - if (fuzz.fuzzing) { - // fuzzing streamlocal is unimplemented - return DROPBEAR_FAILURE; - } -#endif - - sock = socket(PF_UNIX, SOCK_STREAM, 0); - if (sock < 0) { - dropbear_log(LOG_INFO, "Streamlocal forward failed: socket() failed"); - TRACE(("leave listen_streamlocal: socket() failed")) - return DROPBEAR_FAILURE; - } - - memset(&addr, 0, sizeof(addr)); - addr.sun_family = AF_UNIX; - strlcpy(addr.sun_path, tcpinfo->socket_path, sizeof(addr.sun_path)); - - /* Unlink existing socket if it exists */ - unlink_streamsocket(tcpinfo->socket_path); - - /* Set umask to allow proper permissions on the socket */ - old_umask = umask(0177); - rc = bind(sock, (struct sockaddr*)&addr, sizeof(addr)); - saved_errno = errno; - umask(old_umask); - - if (rc < 0) { - dropbear_log(LOG_INFO, "Streamlocal forward failed: bind() failed: %s", strerror(saved_errno)); - m_close(sock); - TRACE(("leave listen_streamlocal: bind() failed")) - return DROPBEAR_FAILURE; - } - - - if (listen(sock, DROPBEAR_LISTEN_BACKLOG) < 0) { - dropbear_log(LOG_INFO, "Streamlocal forward failed: listen() failed: %s", strerror(errno)); - unlink_streamsocket(tcpinfo->socket_path); - m_close(sock); - TRACE(("leave listen_streamlocal: listen() failed")) - return DROPBEAR_FAILURE; - } - - setnonblocking(sock); - - listener = new_listener(&sock, 1, LISTENER_TYPE_STREAMFORWARDED, tcpinfo, - streamlocal_acceptor, cleanup_streamlocal); - - if (listener == NULL) { - unlink_streamsocket(tcpinfo->socket_path); - m_close(sock); - TRACE(("leave listen_streamlocal: listener failed")) - return DROPBEAR_FAILURE; - } - - if (ret_listener) { - *ret_listener = listener; - } - - TRACE(("leave listen_streamlocal: success")) - return DROPBEAR_SUCCESS; -} - -static int svr_remotestreamlocalreq() { - - int ret = DROPBEAR_FAILURE; - char * request_path = NULL; - unsigned int pathlen; - struct TCPListener *tcpinfo = NULL; - struct Listener *listener = NULL; - - TRACE(("enter remotestreamlocalreq")) - - if (svr_opts.forced_command || svr_pubkey_has_forced_command()) { - /* Creating a unix socket in the right place could probably subvert - * a forcedcommand, so don't allow that. - * This could be relaxed if an authorized_keys "permitlisten" - * equivalent were added for streamlocal */ - TRACE(("leave newstreamlocal: no unix forwarding for forced command")) - goto out; - } - - if (svr_opts.noremotetcp || !svr_pubkey_allows_tcpfwd()) { - TRACE(("leave remotestreamlocalreq: remote forwarding disabled")) - goto out; - } - - request_path = buf_getstring(ses.payload, &pathlen); - if (pathlen > MAX_HOST_LEN) { - TRACE(("path len too long: %d", pathlen)) - goto out; - } - if (strlen(request_path) != pathlen) { - TRACE(("path has nul byte")); - goto out; - } - - tcpinfo = (struct TCPListener*)m_malloc(sizeof(struct TCPListener)); - memset(tcpinfo, 0, sizeof(struct TCPListener)); - tcpinfo->sendaddr = NULL; - tcpinfo->sendport = 0; - tcpinfo->listenaddr = NULL; - tcpinfo->listenport = 0; - tcpinfo->chantype = &svr_chan_streamlocalremote; - tcpinfo->tcp_type = forwarded; - tcpinfo->interface = NULL; - tcpinfo->socket_path = m_strdup(request_path); - tcpinfo->request_listenaddr = request_path; - - ret = listen_streamlocal(tcpinfo, &listener); - -out: - if (ret == DROPBEAR_FAILURE) { - /* we only free it if a listener wasn't created, since the listener - * has to remember it if it's to be cancelled */ - m_free(request_path); - m_free(tcpinfo->socket_path); - m_free(tcpinfo); - } - - TRACE(("leave remotestreamlocalreq")) - return ret; -} -#endif /* DROPBEAR_SVR_REMOTESTREAMFWD */ - -#endif /* DROPBEAR_SVR_REMOTETCPFWD */ - -#if DROPBEAR_SVR_LOCALTCPFWD - -const struct ChanType svr_chan_tcpdirect = { - "direct-tcpip", - newtcpdirect, /* init */ - NULL, /* checkclose */ - NULL, /* reqhandler */ - NULL, /* closehandler */ - NULL /* cleanup */ -}; - -/* Called upon creating a new direct tcp channel (ie we connect out to an - * address */ -static int newtcpdirect(struct Channel * channel) { - - char* desthost = NULL; - unsigned int destport; - char* orighost = NULL; - unsigned int origport; - char portstring[NI_MAXSERV]; - unsigned int len; - int err = SSH_OPEN_ADMINISTRATIVELY_PROHIBITED; - - TRACE(("newtcpdirect channel %d", channel->index)) - - if (svr_opts.nolocaltcp || !svr_pubkey_allows_tcpfwd()) { - TRACE(("leave newtcpdirect: local tcp forwarding disabled")) - goto out; - } - - desthost = buf_getstring(ses.payload, &len); - if (len > MAX_HOST_LEN) { - TRACE(("leave newtcpdirect: desthost too long")) - goto out; - } - - destport = buf_getint(ses.payload); - - orighost = buf_getstring(ses.payload, &len); - if (len > MAX_HOST_LEN) { - TRACE(("leave newtcpdirect: orighost too long")) - goto out; - } - - origport = buf_getint(ses.payload); - - /* best be sure */ - if (origport > 65535 || destport > 65535) { - TRACE(("leave newtcpdirect: port > 65535")) - goto out; - } - - if (!svr_pubkey_allows_local_tcpfwd(desthost, destport)) { - TRACE(("leave newtcpdirect: local tcp forwarding not permitted to requested destination")); - goto out; - } - - snprintf(portstring, sizeof(portstring), "%u", destport); - channel->conn_pending = connect_remote(desthost, portstring, channel_connect_done, - channel, NULL, NULL, DROPBEAR_PRIO_NORMAL); - - err = SSH_OPEN_IN_PROGRESS; - -out: - m_free(desthost); - m_free(orighost); - TRACE(("leave newtcpdirect: err %d", err)) - return err; -} - -#endif /* DROPBEAR_SVR_LOCALTCPFWD */ - - -#if DROPBEAR_SVR_LOCALSTREAMFWD - -const struct ChanType svr_chan_streamlocal = { - "direct-streamlocal@openssh.com", - newstreamlocal, /* init */ - NULL, /* checkclose */ - NULL, /* reqhandler */ - NULL, /* closehandler */ - NULL /* cleanup */ -}; - -/* Called upon creating a new stream local channel (ie we connect out to an - * address */ -static int newstreamlocal(struct Channel * channel) { - - /* - https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL#rev1.30 - - byte SSH_MSG_CHANNEL_OPEN - string "direct-streamlocal@openssh.com" - uint32 sender channel - uint32 initial window size - uint32 maximum packet size - string socket path - string reserved - uint32 reserved - */ - - char* destsocket = NULL; - unsigned int len; - int err = SSH_OPEN_ADMINISTRATIVELY_PROHIBITED; - - TRACE(("streamlocal channel %d", channel->index)) - - if (svr_opts.forced_command || svr_pubkey_has_forced_command()) { - TRACE(("leave newstreamlocal: no unix forwarding for forced command")) - goto out; - } - - if (svr_opts.nolocaltcp || !svr_pubkey_allows_tcpfwd()) { - TRACE(("leave newstreamlocal: local unix forwarding disabled")) - goto out; - } - - destsocket = buf_getstring(ses.payload, &len); - if (len > MAX_HOST_LEN) { - TRACE(("leave streamlocal: destsocket too long")) - goto out; - } - - channel->conn_pending = connect_streamlocal(destsocket, channel_connect_done, - channel, DROPBEAR_PRIO_NORMAL); - - err = SSH_OPEN_IN_PROGRESS; - -out: - m_free(destsocket); - TRACE(("leave streamlocal: err %d", err)) - return err; -} - -#endif /* DROPBEAR_SVR_LOCALSTREAMFWD */ diff --git a/src/svr-runopts.c b/src/svr-runopts.c index 52cb44a01..d8571c533 100644 --- a/src/svr-runopts.c +++ b/src/svr-runopts.c @@ -91,8 +91,10 @@ static void printhelp(const char * progname) { #if DROPBEAR_SVR_LOCALANYFWD "-j Disable local port/stream forwarding\n" #endif -#if DROPBEAR_SVR_REMOTETCPFWD +#if DROPBEAR_SVR_REMOTEANYFWD "-k Disable remote port/stream forwarding\n" +#endif +#if DROPBEAR_SVR_REMOTETCPFWD "-a Allow connections to forwarded ports from any host\n" #endif "-c command Force executed command\n" @@ -183,8 +185,8 @@ void svr_getopts(int argc, char ** argv) { #if DROPBEAR_SVR_LOCALANYFWD svr_opts.nolocaltcp = 0; #endif -#if DROPBEAR_SVR_REMOTETCPFWD - svr_opts.noremotetcp = 0; +#if DROPBEAR_SVR_REMOTEANYFWD + svr_opts.noremotefwd = 0; #endif #if DROPBEAR_PLUGIN svr_opts.pubkey_plugin = NULL; @@ -259,17 +261,21 @@ void svr_getopts(int argc, char ** argv) { break; #else case 'j': + /* Ignore the flag */ break; #endif -#if DROPBEAR_SVR_REMOTETCPFWD +#if DROPBEAR_SVR_REMOTEANYFWD case 'k': - svr_opts.noremotetcp = 1; - break; - case 'a': - opts.listen_fwd_all = 1; + svr_opts.noremotefwd = 1; break; #else case 'k': + /* Ignore the flag */ + break; +#endif +#if DROPBEAR_SVR_REMOTETCPFWD + case 'a': + opts.listen_fwd_all = 1; break; #endif #if INETD_MODE diff --git a/src/svr-session.c b/src/svr-session.c index aa3d94e21..c1cd6b9aa 100644 --- a/src/svr-session.c +++ b/src/svr-session.c @@ -55,7 +55,7 @@ static const packettype svr_packettypes[] = { {SSH_MSG_KEXINIT, recv_msg_kexinit}, {SSH_MSG_KEXDH_INIT, recv_msg_kexdh_init}, /* server */ {SSH_MSG_NEWKEYS, recv_msg_newkeys}, - {SSH_MSG_GLOBAL_REQUEST, recv_msg_global_request_remotetcp}, + {SSH_MSG_GLOBAL_REQUEST, svr_recv_msg_global_request}, {SSH_MSG_CHANNEL_REQUEST, recv_msg_channel_request}, {SSH_MSG_CHANNEL_OPEN, recv_msg_channel_open}, {SSH_MSG_CHANNEL_EOF, recv_msg_channel_eof}, diff --git a/src/svr-streamfwd.c b/src/svr-streamfwd.c new file mode 100644 index 000000000..f2a5350b1 --- /dev/null +++ b/src/svr-streamfwd.c @@ -0,0 +1,323 @@ +#include "includes.h" +#include "ssh.h" +#include "forward.h" +#include "dbutil.h" +#include "session.h" +#include "buffer.h" +#include "packet.h" +#include "listener.h" +#include "runopts.h" +#include "auth.h" +#include "netio.h" + + +#if DROPBEAR_SVR_REMOTESTREAMFWD +static const struct ChanType svr_chan_streamlocalremote = { + "forwarded-streamlocal@openssh.com", + NULL, + NULL, + NULL, + NULL, + NULL +}; + +static int matchstreamlocal(const void* typedata1, const void* typedata2) { + + const struct FwdListener *info1 = (const struct FwdListener*)typedata1; + const struct FwdListener *info2 = (const struct FwdListener*)typedata2; + + if (info1->socket_path == NULL || info2->socket_path == NULL) { + return 0; + } + + return (info1->chantype == info2->chantype) + && (strcmp(info1->socket_path, info2->socket_path) == 0); +} + +int svr_cancelremotestreamlocal() { + + int ret = DROPBEAR_FAILURE; + char * socket_path = NULL; + unsigned int pathlen; + struct Listener * listener = NULL; + struct FwdListener tcpinfo; + + TRACE(("enter cancelremotestreamlocal")) + + socket_path = buf_getstring(ses.payload, &pathlen); + if (pathlen > MAX_HOST_LEN) { + TRACE(("path len too long: %d", pathlen)) + goto out; + } + if (strlen(socket_path) != pathlen) { + TRACE(("path has nul byte")); + goto out; + } + + tcpinfo.socket_path = socket_path; + tcpinfo.chantype = &svr_chan_streamlocalremote; + listener = get_listener(CHANNEL_ID_STREAMLOCALFORWARDED, &tcpinfo, matchstreamlocal); + if (listener) { + remove_listener( listener ); + ret = DROPBEAR_SUCCESS; + } + +out: + m_free(socket_path); + TRACE(("leave cancelremotestreamlocal")) + return ret; +} + +static void unlink_streamsocket(const char* path) { + if (unlink(path) < 0 && errno != ENOENT) { + /* Not fatal */ + DEBUG1(("Failed removing unix socket %s: %s", + path, strerror(errno))); + } +} + +static void cleanup_streamlocal(const struct Listener *listener) { + + struct FwdListener *tcpinfo = (struct FwdListener*)(listener->typedata); + + if (tcpinfo && tcpinfo->socket_path) { + unlink_streamsocket(tcpinfo->socket_path); + m_free(tcpinfo->socket_path); + } + m_free(tcpinfo->request_listenaddr); + m_free(tcpinfo); +} + +static void streamlocal_acceptor(const struct Listener *listener, int sock) { + + int fd; + struct FwdListener *tcpinfo = (struct FwdListener*)(listener->typedata); + + fd = accept(sock, NULL, NULL); + if (fd < 0) { + return; + } + + if (send_msg_channel_open_init(fd, tcpinfo->chantype) == DROPBEAR_SUCCESS) { + /* "forwarded-streamlocal@openssh.com" */ + /* socket path that was connected to */ + buf_putstring(ses.writepayload, tcpinfo->request_listenaddr, + strlen(tcpinfo->request_listenaddr)); + /* reserved field */ + buf_putstring(ses.writepayload, "", 0); + + encrypt_packet(); + + } else { + /* XXX debug? */ + close(fd); + } +} + +int listen_streamlocal(struct FwdListener* tcpinfo, struct Listener **ret_listener) { + + int sock, rc, saved_errno; + struct Listener *listener = NULL; + struct sockaddr_un addr; + mode_t old_umask; + + TRACE(("enter listen_streamlocal")) + + if (tcpinfo->socket_path == NULL) { + TRACE(("leave listen_streamlocal: no socket path")) + return DROPBEAR_FAILURE; + } + + if (strlen(tcpinfo->socket_path) >= sizeof(addr.sun_path)) { + dropbear_log(LOG_INFO, "Streamlocal forward failed: socket path too long"); + TRACE(("leave listen_streamlocal: path too long")) + return DROPBEAR_FAILURE; + } + +#if DROPBEAR_FUZZ + if (fuzz.fuzzing) { + // fuzzing streamlocal is unimplemented + return DROPBEAR_FAILURE; + } +#endif + + sock = socket(PF_UNIX, SOCK_STREAM, 0); + if (sock < 0) { + dropbear_log(LOG_INFO, "Streamlocal forward failed: socket() failed"); + TRACE(("leave listen_streamlocal: socket() failed")) + return DROPBEAR_FAILURE; + } + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strlcpy(addr.sun_path, tcpinfo->socket_path, sizeof(addr.sun_path)); + + /* Unlink existing socket if it exists */ + unlink_streamsocket(tcpinfo->socket_path); + + /* Set umask to allow proper permissions on the socket */ + old_umask = umask(0177); + rc = bind(sock, (struct sockaddr*)&addr, sizeof(addr)); + saved_errno = errno; + umask(old_umask); + + if (rc < 0) { + dropbear_log(LOG_INFO, "Streamlocal forward failed: bind() failed: %s", strerror(saved_errno)); + m_close(sock); + TRACE(("leave listen_streamlocal: bind() failed")) + return DROPBEAR_FAILURE; + } + + + if (listen(sock, DROPBEAR_LISTEN_BACKLOG) < 0) { + dropbear_log(LOG_INFO, "Streamlocal forward failed: listen() failed: %s", strerror(errno)); + unlink_streamsocket(tcpinfo->socket_path); + m_close(sock); + TRACE(("leave listen_streamlocal: listen() failed")) + return DROPBEAR_FAILURE; + } + + setnonblocking(sock); + + listener = new_listener(&sock, 1, LISTENER_TYPE_STREAMFORWARDED, tcpinfo, + streamlocal_acceptor, cleanup_streamlocal); + + if (listener == NULL) { + unlink_streamsocket(tcpinfo->socket_path); + m_close(sock); + TRACE(("leave listen_streamlocal: listener failed")) + return DROPBEAR_FAILURE; + } + + if (ret_listener) { + *ret_listener = listener; + } + + TRACE(("leave listen_streamlocal: success")) + return DROPBEAR_SUCCESS; +} + +int svr_remotestreamlocalreq() { + + int ret = DROPBEAR_FAILURE; + char * request_path = NULL; + unsigned int pathlen; + struct FwdListener *tcpinfo = NULL; + struct Listener *listener = NULL; + + TRACE(("enter remotestreamlocalreq")) + + if (svr_opts.forced_command || svr_pubkey_has_forced_command()) { + /* Creating a unix socket in the right place could probably subvert + * a forcedcommand, so don't allow that. + * This could be relaxed if an authorized_keys "permitlisten" + * equivalent were added for streamlocal */ + TRACE(("leave newstreamlocal: no unix forwarding for forced command")) + goto out; + } + + if (svr_opts.noremotefwd || !svr_pubkey_allows_tcpfwd()) { + TRACE(("leave remotestreamlocalreq: remote forwarding disabled")) + goto out; + } + + request_path = buf_getstring(ses.payload, &pathlen); + if (pathlen > MAX_HOST_LEN) { + TRACE(("path len too long: %d", pathlen)) + goto out; + } + if (strlen(request_path) != pathlen) { + TRACE(("path has nul byte")); + goto out; + } + + tcpinfo = (struct FwdListener*)m_malloc(sizeof(struct FwdListener)); + memset(tcpinfo, 0, sizeof(struct FwdListener)); + tcpinfo->sendaddr = NULL; + tcpinfo->sendport = 0; + tcpinfo->listenaddr = NULL; + tcpinfo->listenport = 0; + tcpinfo->chantype = &svr_chan_streamlocalremote; + tcpinfo->fwd_type = forwarded; + tcpinfo->interface = NULL; + tcpinfo->socket_path = m_strdup(request_path); + tcpinfo->request_listenaddr = request_path; + + ret = listen_streamlocal(tcpinfo, &listener); + +out: + if (ret == DROPBEAR_FAILURE) { + /* we only free it if a listener wasn't created, since the listener + * has to remember it if it's to be cancelled */ + m_free(request_path); + m_free(tcpinfo->socket_path); + m_free(tcpinfo); + } + + TRACE(("leave remotestreamlocalreq")) + return ret; +} +#endif /* DROPBEAR_SVR_REMOTESTREAMFWD */ + +#if DROPBEAR_SVR_LOCALSTREAMFWD + +/* Called upon creating a new stream local channel (ie we connect out to an + * address */ +static int newstreamlocal(struct Channel * channel) { + + /* + https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL#rev1.30 + + byte SSH_MSG_CHANNEL_OPEN + string "direct-streamlocal@openssh.com" + uint32 sender channel + uint32 initial window size + uint32 maximum packet size + string socket path + string reserved + uint32 reserved + */ + + char* destsocket = NULL; + unsigned int len; + int err = SSH_OPEN_ADMINISTRATIVELY_PROHIBITED; + + TRACE(("streamlocal channel %d", channel->index)) + + if (svr_opts.forced_command || svr_pubkey_has_forced_command()) { + TRACE(("leave newstreamlocal: no unix forwarding for forced command")) + goto out; + } + + if (svr_opts.nolocaltcp || !svr_pubkey_allows_tcpfwd()) { + TRACE(("leave newstreamlocal: local unix forwarding disabled")) + goto out; + } + + destsocket = buf_getstring(ses.payload, &len); + if (len > MAX_HOST_LEN) { + TRACE(("leave streamlocal: destsocket too long")) + goto out; + } + + channel->conn_pending = connect_streamlocal(destsocket, channel_connect_done, + channel, DROPBEAR_PRIO_NORMAL); + + err = SSH_OPEN_IN_PROGRESS; + +out: + m_free(destsocket); + TRACE(("leave streamlocal: err %d", err)) + return err; +} + +const struct ChanType svr_chan_streamlocal = { + "direct-streamlocal@openssh.com", + newstreamlocal, /* init */ + NULL, /* checkclose */ + NULL, /* reqhandler */ + NULL, /* closehandler */ + NULL /* cleanup */ +}; + +#endif /* DROPBEAR_SVR_LOCALSTREAMFWD */ diff --git a/src/svr-tcpfwd.c b/src/svr-tcpfwd.c new file mode 100644 index 000000000..d0f5560b4 --- /dev/null +++ b/src/svr-tcpfwd.c @@ -0,0 +1,215 @@ +#include "includes.h" +#include "ssh.h" +#include "forward.h" +#include "dbutil.h" +#include "session.h" +#include "buffer.h" +#include "packet.h" +#include "listener.h" +#include "runopts.h" +#include "auth.h" +#include "netio.h" + +#if DROPBEAR_SVR_REMOTETCPFWD + +static const struct ChanType svr_chan_tcpremote = { + "forwarded-tcpip", + NULL, + NULL, + NULL, + NULL, + NULL +}; + +static int matchtcp(const void* typedata1, const void* typedata2) { + + const struct FwdListener *info1 = (struct FwdListener*)typedata1; + const struct FwdListener *info2 = (struct FwdListener*)typedata2; + + return (info1->listenport == info2->listenport) + && (strcmp(info1->request_listenaddr, info2->request_listenaddr) == 0); +} + +int svr_cancelremotetcp(void) { + + int ret = DROPBEAR_FAILURE; + char * request_addr = NULL; + unsigned int addrlen; + unsigned int port; + struct Listener * listener = NULL; + struct FwdListener tcpinfo; + + TRACE(("enter cancelremotetcp")) + + request_addr = buf_getstring(ses.payload, &addrlen); + if (addrlen > MAX_HOST_LEN) { + TRACE(("addr len too long: %d", addrlen)) + goto out; + } + + port = buf_getint(ses.payload); + + memset(&tcpinfo, 0x0, sizeof(tcpinfo)); + tcpinfo.request_listenaddr = request_addr; + tcpinfo.listenport = port; + listener = get_listener(LISTENER_TYPE_TCPFORWARDED, &tcpinfo, matchtcp); + if (listener) { + remove_listener(listener); + ret = DROPBEAR_SUCCESS; + } + +out: + m_free(request_addr); + TRACE(("leave cancelremotetcp")) + return ret; +} + +int svr_remotetcpreq(int *allocated_listen_port) { + + int ret = DROPBEAR_FAILURE; + char * request_addr = NULL; + unsigned int addrlen; + struct FwdListener *tcpinfo = NULL; + unsigned int port; + struct Listener *listener = NULL; + + TRACE(("enter remotetcpreq")) + + request_addr = buf_getstring(ses.payload, &addrlen); + if (addrlen > MAX_HOST_LEN) { + TRACE(("addr len too long: %d", addrlen)) + goto out; + } + + port = buf_getint(ses.payload); + + if (port != 0) { + if (port < 1 || port > 65535) { + TRACE(("invalid port: %d", port)) + goto out; + } + + if (!ses.allowprivport && port < IPPORT_RESERVED) { + TRACE(("can't assign port < 1024 for non-root")) + goto out; + } + } + + if (!svr_pubkey_allows_remote_tcpfwd(request_addr, port)) { + TRACE(("remote tcp forwarding listen address not permitted")); + goto out; + } + + tcpinfo = (struct FwdListener*)m_malloc(sizeof(struct FwdListener)); + tcpinfo->sendaddr = NULL; + tcpinfo->sendport = 0; + tcpinfo->listenport = port; + tcpinfo->chantype = &svr_chan_tcpremote; + tcpinfo->fwd_type = forwarded; + tcpinfo->interface = svr_opts.interface; + + tcpinfo->request_listenaddr = request_addr; + if (!opts.listen_fwd_all || (strcmp(request_addr, "localhost") == 0) ) { + /* NULL means "localhost only" */ + tcpinfo->listenaddr = NULL; + } + else + { + tcpinfo->listenaddr = m_strdup(request_addr); + } + + ret = listen_tcpfwd(tcpinfo, &listener); + if (DROPBEAR_SUCCESS == ret) { + tcpinfo->listenport = get_sock_port(listener->socks[0]); + *allocated_listen_port = tcpinfo->listenport; + } + +out: + if (ret == DROPBEAR_FAILURE) { + /* we only free it if a listener wasn't created, since the listener + * has to remember it if it's to be cancelled */ + m_free(request_addr); + m_free(tcpinfo); + } + + TRACE(("leave remotetcpreq")) + + return ret; +} + + +#endif /* DROPBEAR_SVR_REMOTETCPFWD */ + + +#if DROPBEAR_SVR_LOCALTCPFWD + +/* Called upon creating a new direct tcp channel (ie we connect out to an + * address */ +static int newtcpdirect(struct Channel * channel) { + + char* desthost = NULL; + unsigned int destport; + char* orighost = NULL; + unsigned int origport; + char portstring[NI_MAXSERV]; + unsigned int len; + int err = SSH_OPEN_ADMINISTRATIVELY_PROHIBITED; + + TRACE(("newtcpdirect channel %d", channel->index)) + + if (svr_opts.nolocaltcp || !svr_pubkey_allows_tcpfwd()) { + TRACE(("leave newtcpdirect: local tcp forwarding disabled")) + goto out; + } + + desthost = buf_getstring(ses.payload, &len); + if (len > MAX_HOST_LEN) { + TRACE(("leave newtcpdirect: desthost too long")) + goto out; + } + + destport = buf_getint(ses.payload); + + orighost = buf_getstring(ses.payload, &len); + if (len > MAX_HOST_LEN) { + TRACE(("leave newtcpdirect: orighost too long")) + goto out; + } + + origport = buf_getint(ses.payload); + + /* best be sure */ + if (origport > 65535 || destport > 65535) { + TRACE(("leave newtcpdirect: port > 65535")) + goto out; + } + + if (!svr_pubkey_allows_local_tcpfwd(desthost, destport)) { + TRACE(("leave newtcpdirect: local tcp forwarding not permitted to requested destination")); + goto out; + } + + snprintf(portstring, sizeof(portstring), "%u", destport); + channel->conn_pending = connect_remote(desthost, portstring, channel_connect_done, + channel, NULL, NULL, DROPBEAR_PRIO_NORMAL); + + err = SSH_OPEN_IN_PROGRESS; + +out: + m_free(desthost); + m_free(orighost); + TRACE(("leave newtcpdirect: err %d", err)) + return err; +} + +const struct ChanType svr_chan_tcpdirect = { + "direct-tcpip", + newtcpdirect, /* init */ + NULL, /* checkclose */ + NULL, /* reqhandler */ + NULL, /* closehandler */ + NULL /* cleanup */ +}; + +#endif /* DROPBEAR_SVR_LOCALTCPFWD */ + diff --git a/src/sysoptions.h b/src/sysoptions.h index cccd81e23..1f25af370 100644 --- a/src/sysoptions.h +++ b/src/sysoptions.h @@ -309,10 +309,11 @@ /* TCP and stream local fwds share the same restrictions */ #define DROPBEAR_SVR_LOCALANYFWD ((DROPBEAR_SVR_LOCALTCPFWD) || (DROPBEAR_SVR_LOCALSTREAMFWD)) +#define DROPBEAR_SVR_REMOTEANYFWD ((DROPBEAR_SVR_REMOTETCPFWD) || (DROPBEAR_SVR_REMOTESTREAMFWD)) #define DROPBEAR_LISTENERS \ ((DROPBEAR_CLI_REMOTETCPFWD) || (DROPBEAR_CLI_LOCALTCPFWD) || \ - (DROPBEAR_SVR_REMOTETCPFWD) || (DROPBEAR_SVR_LOCALANYFWD) || \ + (DROPBEAR_SVR_REMOTEANYFWD) || (DROPBEAR_SVR_LOCALANYFWD) || \ (DROPBEAR_SVR_AGENTFWD) || (DROPBEAR_X11FWD)) #define DROPBEAR_CLI_MULTIHOP ((DROPBEAR_CLI_NETCAT) && (DROPBEAR_CLI_PROXYCMD)) diff --git a/src/tcp-accept.c b/src/tcp-accept.c index 5ec57cfe2..da63b4fe8 100644 --- a/src/tcp-accept.c +++ b/src/tcp-accept.c @@ -37,7 +37,7 @@ static void cleanup_tcp(const struct Listener *listener) { - struct TCPListener *tcpinfo = (struct TCPListener*)(listener->typedata); + struct FwdListener *tcpinfo = (struct FwdListener*)(listener->typedata); m_free(tcpinfo->sendaddr); m_free(tcpinfo->listenaddr); @@ -51,7 +51,7 @@ static void tcp_acceptor(const struct Listener *listener, int sock) { struct sockaddr_storage sa; socklen_t len; char ipstring[NI_MAXHOST], portstring[NI_MAXSERV]; - struct TCPListener *tcpinfo = (struct TCPListener*)(listener->typedata); + struct FwdListener *tcpinfo = (struct FwdListener*)(listener->typedata); len = sizeof(sa); @@ -71,13 +71,13 @@ static void tcp_acceptor(const struct Listener *listener, int sock) { char* addr = NULL; unsigned int port = 0; - if (tcpinfo->tcp_type == direct) { + if (tcpinfo->fwd_type == direct) { /* "direct-tcpip" */ /* host to connect, port to connect */ addr = tcpinfo->sendaddr; port = tcpinfo->sendport; } else { - dropbear_assert(tcpinfo->tcp_type == forwarded); + dropbear_assert(tcpinfo->fwd_type == forwarded); /* "forwarded-tcpip" */ /* address that was connected, port that was connected */ addr = tcpinfo->request_listenaddr; @@ -103,7 +103,7 @@ static void tcp_acceptor(const struct Listener *listener, int sock) { } } -int listen_tcpfwd(struct TCPListener* tcpinfo, struct Listener **ret_listener) { +int listen_tcpfwd(struct FwdListener* tcpinfo, struct Listener **ret_listener) { char portstring[NI_MAXSERV]; int socks[DROPBEAR_MAX_SOCKS]; From 90cf3ff5afeff6732206d89a403440b6852e81a1 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Sat, 2 May 2026 21:54:49 +0800 Subject: [PATCH 068/122] server tcp forward: send replies correctly tcpip-forward requests should only send a response when wantreply request flag is set. The port should only be included when a port is allocated by the server. --- src/forward.h | 2 +- src/svr-forward.c | 14 ++++---------- src/svr-tcpfwd.c | 19 +++++++++++++++++-- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/forward.h b/src/forward.h index 87d8e64a3..c359b0cb1 100644 --- a/src/forward.h +++ b/src/forward.h @@ -74,7 +74,7 @@ extern const struct ChanType svr_chan_streamlocal; #endif #if DROPBEAR_SVR_REMOTETCPFWD -int svr_remotetcpreq(int *allocated_listen_port); +int svr_remotetcpreq(int wantreply); int svr_cancelremotetcp(void); #endif diff --git a/src/svr-forward.c b/src/svr-forward.c index 6d61e0d94..46c61a553 100644 --- a/src/svr-forward.c +++ b/src/svr-forward.c @@ -64,16 +64,10 @@ void svr_recv_msg_global_request(void) { if (0) {} #if DROPBEAR_SVR_REMOTETCPFWD else if (strcmp("tcpip-forward", reqname) == 0) { - int allocated_listen_port = 0; - ret = svr_remotetcpreq(&allocated_listen_port); - /* client expects-port-number-to-make-use-of-server-allocated-ports */ - if (DROPBEAR_SUCCESS == ret) { - CHECKCLEARTOWRITE(); - buf_putbyte(ses.writepayload, SSH_MSG_REQUEST_SUCCESS); - buf_putint(ses.writepayload, allocated_listen_port); - encrypt_packet(); - wantreply = 0; /* avoid out: below sending another reply */ - } + ret = svr_remotetcpreq(wantreply); + /* svr_remotetcpreq sends its on reply if needed, + * don't send another in out: below */ + wantreply = 0; } else if (strcmp("cancel-tcpip-forward", reqname) == 0) { ret = svr_cancelremotetcp(); } diff --git a/src/svr-tcpfwd.c b/src/svr-tcpfwd.c index d0f5560b4..ccd8af519 100644 --- a/src/svr-tcpfwd.c +++ b/src/svr-tcpfwd.c @@ -64,7 +64,8 @@ int svr_cancelremotetcp(void) { return ret; } -int svr_remotetcpreq(int *allocated_listen_port) { +/* Will send its own reply if needed. */ +int svr_remotetcpreq(int wantreply) { int ret = DROPBEAR_FAILURE; char * request_addr = NULL; @@ -121,10 +122,24 @@ int svr_remotetcpreq(int *allocated_listen_port) { ret = listen_tcpfwd(tcpinfo, &listener); if (DROPBEAR_SUCCESS == ret) { tcpinfo->listenport = get_sock_port(listener->socks[0]); - *allocated_listen_port = tcpinfo->listenport; } out: + /* Send a reply if needed */ + if (wantreply) { + CHECKCLEARTOWRITE(); + if (ret == DROPBEAR_SUCCESS) { + buf_putbyte(ses.writepayload, SSH_MSG_REQUEST_SUCCESS); + if (port == 0) { + /* port is only included if allocated */ + buf_putint(ses.writepayload, tcpinfo->listenport); + } + } else { + buf_putbyte(ses.writepayload, SSH_MSG_REQUEST_FAILURE); + } + encrypt_packet(); + } + if (ret == DROPBEAR_FAILURE) { /* we only free it if a listener wasn't created, since the listener * has to remember it if it's to be cancelled */ From 7681a948e11b8a80e85b08d3d95efc07b86c077f Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Sat, 2 May 2026 22:26:53 +0800 Subject: [PATCH 069/122] ci: build on push for all branches --- .github/workflows/build.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ecb981adb..e6438f388 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,8 +6,6 @@ on: pull_request: workflow_dispatch: push: - branches: - - master jobs: build: runs-on: ${{ matrix.os || 'ubuntu-24.04' }} From f76177ad7fd968cd9021c47f67341bd9706ada3e Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Sat, 2 May 2026 22:29:42 +0800 Subject: [PATCH 070/122] ci: "all branches" also for outoftree and autoconf --- .github/workflows/autoconf.yml | 3 +-- .github/workflows/outoftree.yml | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/autoconf.yml b/.github/workflows/autoconf.yml index 20e9fe4c7..abad8450b 100644 --- a/.github/workflows/autoconf.yml +++ b/.github/workflows/autoconf.yml @@ -3,9 +3,8 @@ name: Autoconf Up To Date on: pull_request: + workflow_dispatch: push: - branches: - - master jobs: autoconf: runs-on: 'ubuntu-22.04' diff --git a/.github/workflows/outoftree.yml b/.github/workflows/outoftree.yml index 13209535e..78020e2fc 100644 --- a/.github/workflows/outoftree.yml +++ b/.github/workflows/outoftree.yml @@ -5,8 +5,6 @@ on: pull_request: workflow_dispatch: push: - branches: - - master jobs: outoftree: runs-on: 'ubuntu-22.04' From 143291baca6ebb3407a0c25c85bd53bf041a6ab7 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 20 Apr 2026 20:57:11 +0800 Subject: [PATCH 071/122] scp: more CVE-2019-6111 fix Cherrypicked from OpenSSH portable commit 3d896c157c722bc47adca51a58dca859225b5874 djm Sun Feb 10 11:15:52 2019 +0000 upstream: when checking that filenames sent by the server side match what the client requested, be prepared to handle shell-style brace alternations, e.g. "{foo,bar}". "looks good to me" millert@ + in snaps for the last week courtesy deraadt@ --- src/scp.c | 281 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 269 insertions(+), 12 deletions(-) diff --git a/src/scp.c b/src/scp.c index bf98986ca..d151587cd 100644 --- a/src/scp.c +++ b/src/scp.c @@ -74,7 +74,6 @@ */ #include "includes.h" -/*RCSID("$OpenBSD: scp.c,v 1.130 2006/01/31 10:35:43 djm Exp $");*/ #include @@ -600,6 +599,253 @@ tolocal(int argc, char **argv) } } +/* Appends a string to an array; returns 0 on success, -1 on alloc failure */ +static int +append(char *cp, char ***ap, size_t *np) +{ + char **tmp; + + if ((tmp = reallocarray(*ap, *np + 1, sizeof(*tmp))) == NULL) + return -1; + tmp[(*np)] = cp; + (*np)++; + *ap = tmp; + return 0; +} + +/* + * Finds the start and end of the first brace pair in the pattern. + * returns 0 on success or -1 for invalid patterns. + */ +static int +find_brace(const char *pattern, int *startp, int *endp) +{ + int i; + int in_bracket, brace_level; + + *startp = *endp = -1; + in_bracket = brace_level = 0; + for (i = 0; i < INT_MAX && *endp < 0 && pattern[i] != '\0'; i++) { + switch (pattern[i]) { + case '\\': + /* skip next character */ + if (pattern[i + 1] != '\0') + i++; + break; + case '[': + in_bracket = 1; + break; + case ']': + in_bracket = 0; + break; + case '{': + if (in_bracket) + break; + if (pattern[i + 1] == '}') { + /* Protect a single {}, for find(1), like csh */ + i++; /* skip */ + break; + } + if (*startp == -1) + *startp = i; + brace_level++; + break; + case '}': + if (in_bracket) + break; + if (*startp < 0) { + /* Unbalanced brace */ + return -1; + } + if (--brace_level <= 0) + *endp = i; + break; + } + } + /* unbalanced brackets/braces */ + if (*endp < 0 && (*startp >= 0 || in_bracket)) + return -1; + return 0; +} + +/* + * Assembles and records a successfully-expanded pattern, returns -1 on + * alloc failure. + */ +static int +emit_expansion(const char *pattern, int brace_start, int brace_end, + int sel_start, int sel_end, char ***patternsp, size_t *npatternsp) +{ + char *cp; + int o = 0, tail_len = strlen(pattern + brace_end + 1); + + if ((cp = malloc(brace_start + (sel_end - sel_start) + + tail_len + 1)) == NULL) + return -1; + + /* Pattern before initial brace */ + if (brace_start > 0) { + memcpy(cp, pattern, brace_start); + o = brace_start; + } + /* Current braced selection */ + if (sel_end - sel_start > 0) { + memcpy(cp + o, pattern + sel_start, + sel_end - sel_start); + o += sel_end - sel_start; + } + /* Remainder of pattern after closing brace */ + if (tail_len > 0) { + memcpy(cp + o, pattern + brace_end + 1, tail_len); + o += tail_len; + } + cp[o] = '\0'; + if (append(cp, patternsp, npatternsp) != 0) { + free(cp); + return -1; + } + return 0; +} + +/* + * Expand the first encountered brace in pattern, appending the expanded + * patterns it yielded to the *patternsp array. + * + * Returns 0 on success or -1 on allocation failure. + * + * Signals whether expansion was performed via *expanded and whether + * pattern was invalid via *invalid. + */ +static int +brace_expand_one(const char *pattern, char ***patternsp, size_t *npatternsp, + int *expanded, int *invalid) +{ + int i; + int in_bracket, brace_start, brace_end, brace_level; + int sel_start, sel_end; + + *invalid = *expanded = 0; + + if (find_brace(pattern, &brace_start, &brace_end) != 0) { + *invalid = 1; + return 0; + } else if (brace_start == -1) + return 0; + + in_bracket = brace_level = 0; + for (i = sel_start = brace_start + 1; i < brace_end; i++) { + switch (pattern[i]) { + case '{': + if (in_bracket) + break; + brace_level++; + break; + case '}': + if (in_bracket) + break; + brace_level--; + break; + case '[': + in_bracket = 1; + break; + case ']': + in_bracket = 0; + break; + case '\\': + if (i < brace_end - 1) + i++; /* skip */ + break; + } + if (pattern[i] == ',' || i == brace_end - 1) { + if (in_bracket || brace_level > 0) + continue; + /* End of a selection, emit an expanded pattern */ + + /* Adjust end index for last selection */ + sel_end = (i == brace_end - 1) ? brace_end : i; + if (emit_expansion(pattern, brace_start, brace_end, + sel_start, sel_end, patternsp, npatternsp) != 0) + return -1; + /* move on to the next selection */ + sel_start = i + 1; + continue; + } + } + if (in_bracket || brace_level > 0) { + *invalid = 1; + return 0; + } + /* success */ + *expanded = 1; + return 0; +} + +/* Expand braces from pattern. Returns 0 on success, -1 on failure */ +static int +brace_expand(const char *pattern, char ***patternsp, size_t *npatternsp) +{ + char *cp, *cp2, **active = NULL, **done = NULL; + size_t i, nactive = 0, ndone = 0; + int ret = -1, invalid = 0, expanded = 0; + + *patternsp = NULL; + *npatternsp = 0; + + /* Start the worklist with the original pattern */ + if ((cp = strdup(pattern)) == NULL) + return -1; + if (append(cp, &active, &nactive) != 0) { + free(cp); + return -1; + } + while (nactive > 0) { + cp = active[nactive - 1]; + nactive--; + if (brace_expand_one(cp, &active, &nactive, + &expanded, &invalid) == -1) { + free(cp); + goto fail; + } + if (invalid) + fatal("%s: invalid brace pattern \"%s\"", __func__, cp); + if (expanded) { + /* + * Current entry expanded to new entries on the + * active list; discard the progenitor pattern. + */ + free(cp); + continue; + } + /* + * Pattern did not expand; append the finename component to + * the completed list + */ + if ((cp2 = strrchr(cp, '/')) != NULL) + *cp2++ = '\0'; + else + cp2 = cp; + if (append(xstrdup(cp2), &done, &ndone) != 0) { + free(cp); + goto fail; + } + free(cp); + } + /* success */ + *patternsp = done; + *npatternsp = ndone; + done = NULL; + ndone = 0; + ret = 0; + fail: + for (i = 0; i < nactive; i++) + free(active[i]); + free(active); + for (i = 0; i < ndone; i++) + free(done[i]); + free(done); + return ret; +} + void source(int argc, char **argv) { @@ -841,7 +1087,8 @@ sink(int argc, char **argv, const char *src) off_t size, statbytes; int setimes, targisdir, wrerrno = 0; char ch, *cp, *np, *targ, *why, *vect[1], buf[2048]; - char *src_copy = NULL, *restrict_pattern = NULL; + char **patterns = NULL; + size_t n, npatterns = 0; struct timeval tv[2]; #define atime tv[0] @@ -868,16 +1115,13 @@ sink(int argc, char **argv, const char *src) * Prepare to try to restrict incoming filenames to match * the requested destination file glob. */ - if ((src_copy = strdup(src)) == NULL) - fatal("strdup failed"); - if ((restrict_pattern = strrchr(src_copy, '/')) != NULL) { - *restrict_pattern++ = '\0'; - } + if (brace_expand(src, &patterns, &npatterns) != 0) + fatal("%s: could not expand pattern", __func__); } for (first = 1;; first = 0) { cp = buf; if (atomicio(read, remin, cp, 1) != 1) - return; + goto done; if (*cp++ == '\n') SCREWUP("unexpected "); do { @@ -900,7 +1144,7 @@ sink(int argc, char **argv, const char *src) } if (buf[0] == 'E') { (void) atomicio(vwrite, remout, "", 1); - return; + goto done; } if (ch == '\n') *--cp = 0; @@ -956,9 +1200,14 @@ sink(int argc, char **argv, const char *src) run_err("error: unexpected filename: %s", cp); exit(1); } - if (restrict_pattern != NULL && - fnmatch(restrict_pattern, cp, 0) != 0) - SCREWUP("filename does not match request"); + if (npatterns > 0) { + for (n = 0; n < npatterns; n++) { + if (fnmatch(patterns[n], cp, 0) == 0) + break; + } + if (n >= npatterns) + SCREWUP("filename does not match request"); + } if (targisdir) { static char *namebuf = NULL; static size_t cursize = 0; @@ -1123,7 +1372,15 @@ bad: run_err("%s: %s", np, strerror(errno)); break; } } +done: + for (n = 0; n < npatterns; n++) + free(patterns[n]); + free(patterns); + return; screwup: + for (n = 0; n < npatterns; n++) + free(patterns[n]); + free(patterns); run_err("protocol error: %s", why); exit(1); } From 61bd0e3e6573277de17359763aa5827a189c8d7f Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Sat, 2 May 2026 08:43:26 +0800 Subject: [PATCH 072/122] scp: Disallow -r to an existing directory Checks of received paths don't catch all cases in -r recursive mode, so disallow -r when the destination directory exists. This mitigates CVE-2019-6111, where unexpected files would be accepted from a malicious scp server. Alternatives like rsync could be used in that case. The new error message may be misleading if parent path components of the destination are also missing, though the command would have failed regardless. --- src/scp.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/scp.c b/src/scp.c index d151587cd..09297a5b5 100644 --- a/src/scp.c +++ b/src/scp.c @@ -583,6 +583,15 @@ tolocal(int argc, char **argv) if (*suser == '\0') suser = NULL; } + + char *dest = *(argv + argc - 1); + if (!(access(dest, F_OK) == 0 && errno == ENOENT)) { + /* paths sent from the server in recursive mode aren't adequately + * checked that they match the requested files, so disallow this */ + fprintf(stderr, "-r destination \"%s\" must not already exist\n", dest); + exit(1); + } + host = cleanhostname(host); len = strlen(src) + CMDNEEDS + 20; bp = xmalloc(len); From 443ccc1fd1b9a4e3747f6b5cd644b7af49064308 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Sat, 2 May 2026 08:17:25 +0800 Subject: [PATCH 073/122] scp: clear setuid/setgid bits on received files From openssh-portable 487e8ac146f7d6616f65c125d5edb210519b833a Tracked as CVE-2026-35385 upstream: when downloading files as root in legacy (-O) mode and without the -p (preserve modes) flag set, clear setuid/setgid bits from downloaded files as one might expect. AFAIK this bug dates back to the original Berkeley rcp program. Reported by Christos Papakonstantinou of Cantina and Spearbit. OpenBSD-Commit-ID: 49e902fca8dd933a92a9b547ab31f63e86729fa1 --- src/scp.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/scp.c b/src/scp.c index 09297a5b5..79b0a12d4 100644 --- a/src/scp.c +++ b/src/scp.c @@ -1106,8 +1106,10 @@ sink(int argc, char **argv, const char *src) setimes = targisdir = 0; mask = umask(0); - if (!pflag) + if (!pflag) { + mask |= 07000; (void) umask(mask); + } if (argc != 1) { run_err("ambiguous target"); exit(1); From 65bcd259e813ea1db4f427d2b499667512e6feae Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Sat, 2 May 2026 23:09:56 +0800 Subject: [PATCH 074/122] manpage: Add dropbear -M session timeout option --- manpages/dropbear.8 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/manpages/dropbear.8 b/manpages/dropbear.8 index 7f2c990db..a148eb7b8 100644 --- a/manpages/dropbear.8 +++ b/manpages/dropbear.8 @@ -105,6 +105,11 @@ of 0 disables keepalives. If no response is received for 3 consecutive keepalive .B \-I \fIidle_timeout Disconnect the session if no traffic is transmitted or received for \fIidle_timeout\fR seconds. .TP +.B \-M +Set maximum session duration in seconds. All sessions will close after this time. +If a compile-time setting was set (default is none), then setting \fI-M 0\fR will disable +the session duration timeout. +.TP .B \-z By default Dropbear will send network traffic with the \fBAF21\fR setting for QoS, letting network devices give it higher priority. Some devices may have problems with that, \fI-z\fR can be used to disable it. .TP From fe7535047c7e5913f8a881143cfdffec06228903 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Sat, 2 May 2026 23:46:37 +0800 Subject: [PATCH 075/122] scp: Avoid compile warnings --- src/progressmeter.c | 2 ++ src/scp.c | 14 +++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/progressmeter.c b/src/progressmeter.c index 2038fd3f1..21a335056 100644 --- a/src/progressmeter.c +++ b/src/progressmeter.c @@ -221,6 +221,7 @@ update_progress_meter(int ignore) { int save_errno; + (void)ignore; save_errno = errno; if (win_resized) { @@ -273,6 +274,7 @@ stop_progress_meter(void) static void sig_winch(int sig) { + (void)sig; win_resized = 1; } diff --git a/src/scp.c b/src/scp.c index 79b0a12d4..e5ce4e2f6 100644 --- a/src/scp.c +++ b/src/scp.c @@ -200,7 +200,8 @@ do_cmd(char *host, char *remuser, char *cmd, int *fdin, int *fdout) * Reserve two descriptors so that the real pipes won't get * descriptors 0 and 1 because that will screw up dup2 below. */ - pipe(reserved); + if (pipe(reserved) < 0) + fatal("pipe: %s", strerror(errno)); /* Create a socket pair for communicating with ssh. */ if (pipe(pin) < 0) @@ -942,14 +943,14 @@ next: if (fd != -1) { amt = stb.st_size - i; if (!haderr) { result = atomicio(read, fd, bp->buf, amt); - if (result != amt) + if (result != (size_t)amt) haderr = errno; } if (haderr) (void) atomicio(vwrite, remout, bp->buf, amt); else { result = atomicio(vwrite, remout, bp->buf, amt); - if (result != amt) + if (result != (size_t)amt) haderr = errno; statbytes += result; } @@ -1523,6 +1524,7 @@ allocbuf(BUF *bp, int fd, int blksize) if (size == 0) size = blksize; #else /* HAVE_STRUCT_STAT_ST_BLKSIZE */ + (void)fd; size = blksize; #endif /* HAVE_STRUCT_STAT_ST_BLKSIZE */ if (bp->cnt >= size) @@ -1539,8 +1541,10 @@ allocbuf(BUF *bp, int fd, int blksize) void lostconn(int signo) { - if (!iamremote) - write(STDERR_FILENO, "lost connection\n", 16); + if (!iamremote) { + int unused = write(STDERR_FILENO, "lost connection\n", 16); + (void)unused; + } if (signo) _exit(1); else From 04f1336ea0ac1cd6591e7e4779f80d8c0096109d Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 27 Apr 2026 21:46:59 +0800 Subject: [PATCH 076/122] Add m_mp_burn() --- src/bignum.c | 5 +++++ src/bignum.h | 1 + 2 files changed, 6 insertions(+) diff --git a/src/bignum.c b/src/bignum.c index c2b39b1b5..ecccaa0bc 100644 --- a/src/bignum.c +++ b/src/bignum.c @@ -102,3 +102,8 @@ void hash_process_mp(const struct ltc_hash_descriptor *hash_desc, hash_desc->process(hs, buf->data, buf->len); buf_burn_free(buf); } + +void m_mp_burn(mp_int *mp) { + m_burn(mp->dp, mp->alloc * sizeof(*mp->dp)); + mp->used = 0; +} diff --git a/src/bignum.h b/src/bignum.h index 861acb0ca..ad21e115e 100644 --- a/src/bignum.h +++ b/src/bignum.h @@ -34,5 +34,6 @@ void m_mp_free_multi(mp_int **mp, ...) ATTRIB_SENTINEL; void bytes_to_mp(mp_int *mp, const unsigned char* bytes, unsigned int len); void hash_process_mp(const struct ltc_hash_descriptor *hash_desc, hash_state *hs, const mp_int *mp); +void m_mp_burn(mp_int *mp); #endif /* DROPBEAR_BIGNUM_H_ */ From 2a4cdf71b9e9af23c4131c705d70682c1a3211ba Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 27 Apr 2026 21:47:17 +0800 Subject: [PATCH 077/122] Implement RSA exponent blinding Reduces a timing side-channel in the RSA signing operation. RSA keys generated with Dropbear < 0.33 are no longer supported, since the necessary P and Q values are not stored. That release was in 2004, it is unlikely keys are still in use. --- src/rsa.c | 58 +++++++++++++++++++++++++++++++++++++++------- src/signkey_ossh.c | 3 --- 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/src/rsa.c b/src/rsa.c index 6152e1cfd..59af46bd6 100644 --- a/src/rsa.c +++ b/src/rsa.c @@ -113,7 +113,8 @@ int buf_get_rsa_priv_key(buffer* buf, dropbear_rsa_key *key) { } if (buf->pos == buf->len) { - /* old Dropbear private keys didn't keep p and q, so we will ignore them*/ + /* Keys without p or q are prior to Dropbear 0.33 from 2003. */ + dropbear_exit("RSA key format is ancient"); } else { m_mp_alloc_init_multi(&key->p, &key->q, NULL); @@ -262,27 +263,34 @@ void buf_put_rsa_sign(buffer* buf, const dropbear_rsa_key *key, DEF_MP_INT(rsa_tmp1); DEF_MP_INT(rsa_tmp2); DEF_MP_INT(rsa_tmp3); + DEF_MP_INT(rsa_phi_n); + DEF_MP_INT(rsa_b_tmp); + DEF_MP_INT(rsa_b_rand); + DEF_MP_INT(rsa_b_phi); + DEF_MP_INT(rsa_b_d); TRACE(("enter buf_put_rsa_sign")) dropbear_assert(key != NULL); - m_mp_init_multi(&rsa_s, &rsa_tmp1, &rsa_tmp2, &rsa_tmp3, NULL); + m_mp_init_multi(&rsa_s, &rsa_tmp1, &rsa_tmp2, &rsa_tmp3, + &rsa_phi_n, &rsa_b_tmp, &rsa_b_rand, &rsa_b_phi, &rsa_b_d, + NULL); rsa_pad_em(key, data_buf, &rsa_tmp1, sigtype); /* the actual signing of the padded data */ #if DROPBEAR_RSA_BLINDING + /* https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-rsa-guidance-08 */ - /* With blinding, s = (r^(-1))((em)*r^e)^d mod n */ + /* With base blinding, s = (r^(-1))((em)*r^e)^d mod n */ - /* generate the r blinding value */ + /* generate the r base blinding value */ /* rsa_tmp2 is r */ gen_random_mpint(key->n, &rsa_tmp2); /* rsa_tmp1 is em */ /* em' = em * r^e mod n */ - /* rsa_s used as a temp var*/ if (mp_exptmod(&rsa_tmp2, key->e, key->n, &rsa_s) != MP_OKAY) { dropbear_exit("RSA error"); @@ -294,12 +302,44 @@ void buf_put_rsa_sign(buffer* buf, const dropbear_rsa_key *key, dropbear_exit("RSA error"); } + /* exponent blinding, m = c^(d + b*phi(n)) mod n. + * b is a 64-bit random value. */ + mp_set_u64(&rsa_b_tmp, UINT64_MAX); + gen_random_mpint(&rsa_b_tmp, &rsa_b_rand); + /* phi(n) = phi(p*q) = phi(p) * phi(q) = (p-1)*(q-1) = n + 1 - p - q + * since n = p*q, phi(prime) = prime-1. */ + if (mp_add_d(key->n, 1, &rsa_phi_n) != MP_OKAY) { + dropbear_exit("RSA error"); + } + /* rsa_b_d as a temporary */ + if (mp_sub(&rsa_phi_n, key->p, &rsa_b_d) != MP_OKAY) { + dropbear_exit("RSA error"); + } + if (mp_sub(&rsa_b_d, key->q, &rsa_phi_n) != MP_OKAY) { + dropbear_exit("RSA error"); + } + + /* b*phi(n) */ + if (mp_mul(&rsa_b_rand, &rsa_phi_n, &rsa_b_tmp) != MP_OKAY) { + dropbear_exit("RSA error"); + } + /* d + b*phi(n) */ + if (mp_add(key->d, &rsa_b_tmp, &rsa_b_d) != MP_OKAY) { + dropbear_exit("RSA error"); + } + /* rsa_tmp2 is em' */ /* s' = (em')^d mod n */ - if (mp_exptmod(&rsa_tmp2, key->d, key->n, &rsa_tmp1) != MP_OKAY) { + if (mp_exptmod(&rsa_tmp2, &rsa_b_d, key->n, &rsa_tmp1) != MP_OKAY) { dropbear_exit("RSA error"); } + m_mp_burn(&rsa_phi_n); + m_mp_burn(&rsa_b_tmp); + m_mp_burn(&rsa_b_rand); + m_mp_burn(&rsa_b_phi); + m_mp_burn(&rsa_b_d); + /* rsa_tmp1 is s' */ /* rsa_tmp3 is r^(-1) mod n */ /* s = (s')r^(-1) mod n */ @@ -317,8 +357,10 @@ void buf_put_rsa_sign(buffer* buf, const dropbear_rsa_key *key, #endif /* DROPBEAR_RSA_BLINDING */ - mp_clear_multi(&rsa_tmp1, &rsa_tmp2, &rsa_tmp3, NULL); - + mp_clear_multi(&rsa_tmp1, &rsa_tmp2, &rsa_tmp3, + &rsa_phi_n, &rsa_b_tmp, &rsa_b_rand, &rsa_b_phi, &rsa_b_d, + NULL); + /* create the signature to return */ name = signature_name_from_type(sigtype, &namelen); buf_putstring(buf, name, namelen); diff --git a/src/signkey_ossh.c b/src/signkey_ossh.c index 59b44ad26..0e7101810 100644 --- a/src/signkey_ossh.c +++ b/src/signkey_ossh.c @@ -26,9 +26,6 @@ void buf_put_rsa_priv_ossh(buffer *buf, const sign_key *akey) { mp_int iqmp; dropbear_assert(key != NULL); - if (!(key->p && key->q)) { - dropbear_exit("Pre-0.33 Dropbear keys cannot be converted to OpenSSH keys.\n"); - } m_mp_init(&iqmp); /* iqmp = (q^-1) mod p */ From 3d18eb4e9519d21799975cd6409acd0ceff0ad69 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Sat, 2 May 2026 08:15:44 +0800 Subject: [PATCH 078/122] Release 2026.90 --- CHANGES | 80 ++++++++++++++++++++++++++++++++++++++++++++++++ debian/changelog | 6 ++++ src/sysoptions.h | 2 +- 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index aedd1b338..323fdffcf 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,83 @@ +2026.90 - 3 May 2026 + +- Security: server: Fix ability to bypass an authorized_keys "forced_command" + option by an authenticated user, if Dropbear is running with "-t" option. + ("-t" is require both password and public key). + https://github.com/mkj/dropbear/commit/c60db6408178aaca0a8aacd017ac344a2cfb30c8 + Reported by Jeremy Brown + +- Security: server: Open authorized_keys non-blocking. This avoids getting stuck + with special files, could allow denial of service from local users. + https://github.com/mkj/dropbear/commit/8b579571b5584847caf72345b9073b3b2a0467a7 + Reported by Turistu + +- Security: scp: Add missed patch to fix CVE-2019-6111 allowing a malicious server + to overwrite unexpected files. + https://github.com/mkj/dropbear/commit/143291baca6ebb3407a0c25c85bd53bf041a6ab7 + Patch from OpenSSH, reported missing by Asim Viladi Oglu Manizada @manizada + + Note breaking change: "-r" is now disallowed when the target directory exists + (an additional change in Dropbear's version). If that's required an alternative + such as rsync could be used. + https://github.com/mkj/dropbear/commit/61bd0e3e6573277de17359763aa5827a189c8d7f + +- Security: scp: Clear setuid/setgid bits on received files. + https://github.com/mkj/dropbear/commit/443ccc1fd1b9a4e3747f6b5cd644b7af49064308 + Patch from OpenSSH, tracked as CVE-2026-35385 + +- Security: client/server: Fix close() of a file descriptor from an out-of-bounds + read. This seems dificult to exploit but may have unforseen effects. + https://github.com/mkj/dropbear/commit/067fd3846c425d1998c7c94d3feafe1f26b514eb + Reported by Ankit Singh and @j499261162 + +- server: Add unix stream forwarding listener support (-R from the client) + Patch from Brian Dentino @bdentino + +- server: Add -M argument for maximum session duration. + Github PR #409 from Martin Schiller + +- server: Add permitlisten authorized_keys option + This allows limiting to a specific port. + Github PR #384 from Mitar + +- rsa: keys generated with dropbearkey 0.32 or earlier are no longer supported, + 0.33 was released in 2003. RSA exponent blinding is implemented to reduce a + cache timing side channel. Side channel reported by Ciaran Mullan. + +- server: Limit the number of public key queries to 15. + This is a mitigation against internet-wide scanning of hosts for public keys. + It doesn't provide much mitigation against targeted enumeration on a + server, in that case public keys should not be treated as private. + Reported by HD Moore, details are in the SSHamble presentation. + +- server: Disallow client-sent signals when there is a forced command. + Some programs could have unexpected handling. + Reported by HD Moore in SSHamble presentation. + +- client: Don't attempt PTY requests for non-TTYs. + Github issue #385 + +- server: Don't allow client-sent signals when Dropbear is built with + DROPBEAR_SVR_DROP_PRIVS = 0 + +- server: Fix closing TCP remote listeners. This has never worked properly. + Github PR #414 + +- server: Improve protocol correctness sending replies to tcpip forward + requests (-L from the client) + +- Fix some timeouts being delayed. Github PR #318 + +- Improve manpage documentation for forced commands + +- Fix distclean of config.h and libtom*/Makefile + +- Add curve25519 checks required by rfc8032. These have no effect on SSH protocol + security. + +- Fix "ssh-connection" check, reported by Turistu in Github #397. + Has no effect on program function. + 2025.89 - 16 December 2025 - Security: Avoid privilege escalation via unix stream forwarding in Dropbear diff --git a/debian/changelog b/debian/changelog index 6135d6b1c..c1ac3da8c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +dropbear (2026.90-0.1) unstable; urgency=low + + * New upstream release. + + -- Matt Johnston Sun, 3 May 2026 22:51:57 +0800 + dropbear (2025.89-0.1) unstable; urgency=low * New upstream release. diff --git a/src/sysoptions.h b/src/sysoptions.h index 1f25af370..fef2a5cf1 100644 --- a/src/sysoptions.h +++ b/src/sysoptions.h @@ -4,7 +4,7 @@ *******************************************************************/ #ifndef DROPBEAR_VERSION -#define DROPBEAR_VERSION "2025.89" +#define DROPBEAR_VERSION "2026.90" #endif /* IDENT_VERSION_PART is the optional part after "SSH-2.0-dropbear". Refer to RFC4253 for requirements. */ From 0c5629a5d543ef5e4eff43345b1eedf2afef84ca Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 4 May 2026 21:35:57 +0800 Subject: [PATCH 079/122] scp: don't use reallocarray reallocarray() isn't portable. An overflow check can be used instead. Fixes: 143291baca6e ("scp: more CVE-2019-6111 fix") --- src/scp.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/scp.c b/src/scp.c index e5ce4e2f6..2f7a62791 100644 --- a/src/scp.c +++ b/src/scp.c @@ -615,7 +615,12 @@ append(char *cp, char ***ap, size_t *np) { char **tmp; - if ((tmp = reallocarray(*ap, *np + 1, sizeof(*tmp))) == NULL) + if (*np + 1 > (SIZE_MAX / sizeof(*tmp))) { + /* overflow */ + return -1; + } + + if ((tmp = realloc(*ap, (*np + 1) * sizeof(*tmp))) == NULL) return -1; tmp[(*np)] = cp; (*np)++; From 16ce059ed8e914e210af67e199041943ec747034 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 4 May 2026 21:36:41 +0800 Subject: [PATCH 080/122] scp: fix recursive overwrite check Non-recursive transfers were incorrectly disallowed to an existing destination. In addition, the destination check logic was wrong. Fixes: 61bd0e3e6573 ("scp: Disallow -r to an existing directory") --- src/scp.c | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/scp.c b/src/scp.c index 2f7a62791..4b4990ea5 100644 --- a/src/scp.c +++ b/src/scp.c @@ -586,11 +586,14 @@ tolocal(int argc, char **argv) } char *dest = *(argv + argc - 1); - if (!(access(dest, F_OK) == 0 && errno == ENOENT)) { - /* paths sent from the server in recursive mode aren't adequately - * checked that they match the requested files, so disallow this */ - fprintf(stderr, "-r destination \"%s\" must not already exist\n", dest); - exit(1); + if (iamrecursive) { + /* Destination must not exist */ + if (!(access(dest, F_OK) == -1 && errno == ENOENT)) { + /* paths sent from the server in recursive mode aren't adequately + * checked that they match the requested files, so disallow this */ + fprintf(stderr, "-r destination \"%s\" must not already exist\n", dest); + exit(1); + } } host = cleanhostname(host); From de87f98c0e15fb6ab897566f77acce1778d613a5 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Wed, 6 May 2026 23:45:28 +0800 Subject: [PATCH 081/122] ci: build scp --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e6438f388..dde2b72e1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -243,6 +243,7 @@ jobs: run: | cat localoptions.h make -j3 $MAKE_TARGET + make scp - name: multilink if: ${{ matrix.multilink }} From 0f1261841eb98ca9905d7c0939c2f4e4069f01be Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Wed, 6 May 2026 23:52:21 +0800 Subject: [PATCH 082/122] Revert "ci: build scp" PROGRESS_METER conditional causes error: variable 'statbytes' set but not used This reverts commit de87f98c0e15fb6ab897566f77acce1778d613a5. --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dde2b72e1..e6438f388 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -243,7 +243,6 @@ jobs: run: | cat localoptions.h make -j3 $MAKE_TARGET - make scp - name: multilink if: ${{ matrix.multilink }} From b7c5b37e50f79926d99b051482cf9e1a7024afba Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Sat, 9 May 2026 23:21:02 +0800 Subject: [PATCH 083/122] client: Disable compression by default Compression can be enabled with -o compression=y or with compile time DROPBEAR_CLI_COMPRESSION option. --- src/cli-runopts.c | 31 ++++++++++++++++++++++++++++--- src/common-kex.c | 3 +-- src/default_options.h | 5 +++++ src/runopts.h | 5 ++--- src/session.h | 3 --- src/svr-runopts.c | 2 +- 6 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/cli-runopts.c b/src/cli-runopts.c index a223437fe..8ff9051d7 100644 --- a/src/cli-runopts.c +++ b/src/cli-runopts.c @@ -185,7 +185,7 @@ void cli_getopts(int argc, char ** argv) { cli_opts.bind_port = NULL; cli_opts.keepalive_arg = NULL; #ifndef DISABLE_ZLIB - opts.allow_compress = 1; + opts.compression = DROPBEAR_CLI_COMPRESSION; #endif #if DROPBEAR_USER_ALGO_LIST opts.cipher_list = NULL; @@ -579,7 +579,7 @@ void loadidentityfile(const char* filename, int warnfail) { static char** multihop_args(const char* argv0, const char* prior_hops) { /* null terminated array */ char **args = NULL; - size_t max_args = 14, pos = 0, len; + size_t max_args = 16, pos = 0, len; #if DROPBEAR_CLI_PUBKEY_AUTH m_list_elem *iter; #endif @@ -620,6 +620,15 @@ static char** multihop_args(const char* argv0, const char* prior_hops) { pos++; } +#ifndef DISABLE_ZLIB + if (opts.compression) { + args[pos] = m_strdup("-o"); + pos++; + args[pos] = m_strdup("Compression=yes"); + pos++; + } +#endif + if (cli_opts.proxycmd) { args[pos] = m_strdup("-J"); pos++; @@ -725,7 +734,7 @@ static void parse_multihop_hostname(const char* orighostarg, const char* argv0) #ifndef DISABLE_ZLIB /* This outer stream will be incompressible since it's encrypted. */ - opts.allow_compress = 0; + opts.compression = 0; #endif } @@ -969,6 +978,9 @@ static void add_extendedopt(const char* origstr) { dropbear_log(LOG_INFO, "Available options:\n" "\tBatchMode\n" "\tBindAddress\n" +#ifndef DISABLE_ZLIB + "\tCompression\n" +#endif "\tDisableTrivialAuth\n" #if DROPBEAR_CLI_ANYTCPFWD "\tExitOnForwardFailure\n" @@ -1006,6 +1018,19 @@ static void add_extendedopt(const char* origstr) { return; } + if (match_extendedopt(&optstr, "Compression") == DROPBEAR_SUCCESS) { + int flag = parse_flag_value(optstr); +#ifndef DISABLE_ZLIB + /* Compression compiled in */ + opts.compression = flag; +#else + if (flag) { + dropbear_log(LOG_WARNING, "compression is not supported"); + } +#endif + return; + } + if (match_extendedopt(&optstr, "DisableTrivialAuth") == DROPBEAR_SUCCESS) { cli_opts.disable_trivial_auth = parse_flag_value(optstr); return; diff --git a/src/common-kex.c b/src/common-kex.c index b8e04eb68..a4bf45776 100644 --- a/src/common-kex.c +++ b/src/common-kex.c @@ -150,7 +150,6 @@ static void switch_keys() { ses.keys->algo_kex = ses.newkeys->algo_kex; ses.keys->algo_hostkey = ses.newkeys->algo_hostkey; ses.keys->algo_signature = ses.newkeys->algo_signature; - ses.keys->allow_compress = 0; m_free(ses.newkeys); ses.newkeys = NULL; kexinitialise(); @@ -208,7 +207,7 @@ static void kex_setup_compress(void) { ses.compress_algos_s2c = ssh_nocompress; #else - if (!opts.allow_compress) { + if (!opts.compression) { ses.compress_algos_c2s = ssh_nocompress; ses.compress_algos_s2c = ssh_nocompress; return; diff --git a/src/default_options.h b/src/default_options.h index 38e9b8c34..e49dc701a 100644 --- a/src/default_options.h +++ b/src/default_options.h @@ -216,6 +216,11 @@ not as a server, due to concerns over its strength. Set to 0 to allow group1 in Dropbear server too */ #define DROPBEAR_DH_GROUP1_CLIENTONLY 1 +/* Compression is disabled by default. Can be enabled at runtime + * with -o compression=yes + */ +#define DROPBEAR_CLI_COMPRESSION 0 + /* Control the memory/performance/compression tradeoff for zlib. * Set windowBits=8 for least memory usage, see your system's * zlib.h for full details. diff --git a/src/runopts.h b/src/runopts.h index e22458ce8..8a17831fd 100644 --- a/src/runopts.h +++ b/src/runopts.h @@ -45,9 +45,8 @@ typedef struct runopts { int usingsyslog; #ifndef DISABLE_ZLIB - /* Whether any compression is allowed. The specific method used - * varies between client and server, it will be set up by kex_setup_compress() */ - int allow_compress; + /* whether compression should be advertised */ + int compression; #endif #if DROPBEAR_USER_ALGO_LIST diff --git a/src/session.h b/src/session.h index da617b8e9..f6487173b 100644 --- a/src/session.h +++ b/src/session.h @@ -106,9 +106,6 @@ struct key_context { const struct dropbear_kex *algo_kex; enum signkey_type algo_hostkey; /* server key type */ enum signature_type algo_signature; /* server signature type */ - - int allow_compress; /* whether compression has started (useful in - zlib@openssh.com delayed compression case) */ }; struct packetlist; diff --git a/src/svr-runopts.c b/src/svr-runopts.c index d8571c533..f9a804fcd 100644 --- a/src/svr-runopts.c +++ b/src/svr-runopts.c @@ -196,7 +196,7 @@ void svr_getopts(int argc, char ** argv) { svr_opts.reexec_childpipe = -1; #ifndef DISABLE_ZLIB - opts.allow_compress = 1; + opts.compression = 1; #endif /* not yet From 819a25103bb46c0bbe1df0e634181fb13980a329 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Sat, 9 May 2026 23:29:20 +0800 Subject: [PATCH 084/122] Allow y/n for -o options --- src/cli-runopts.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/cli-runopts.c b/src/cli-runopts.c index 8ff9051d7..cc15d1154 100644 --- a/src/cli-runopts.c +++ b/src/cli-runopts.c @@ -962,9 +962,14 @@ static int match_extendedopt(const char** strptr, const char *optname) { } static int parse_flag_value(const char *value) { - if (strcmp(value, "yes") == 0 || strcmp(value, "true") == 0) { + if (strcmp(value, "yes") == 0 + || strcmp(value, "y") == 0 + || strcmp(value, "true") == 0 + ) { return 1; - } else if (strcmp(value, "no") == 0 || strcmp(value, "false") == 0) { + } else if (strcmp(value, "no") == 0 + || strcmp(value, "n") == 0 + || strcmp(value, "false") == 0) { return 0; } From 60fcb58b1b80f7a37d39091b44326a8ae18efae5 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Sun, 10 May 2026 18:13:36 +0800 Subject: [PATCH 085/122] Add -Q to query supported algorithms Run by itself it will print the algorithms specified at compile time. For the client the lists will be modified by "-o Compression" or -c/-m arguments. --- manpages/dbclient.1 | 5 +++++ manpages/dropbear.8 | 5 +++++ src/cli-runopts.c | 11 ++++++++++- src/common-runopts.c | 44 ++++++++++++++++++++++++++++++++++++++++++++ src/runopts.h | 3 +++ src/svr-runopts.c | 9 +++++++++ 6 files changed, 76 insertions(+), 1 deletion(-) diff --git a/manpages/dbclient.1 b/manpages/dbclient.1 index f331dcfa3..c188e79c6 100644 --- a/manpages/dbclient.1 +++ b/manpages/dbclient.1 @@ -220,6 +220,11 @@ The specified command will be requested as a subsystem, used for sftp. Dropbear .B \-b \fI[address][:port] Bind to a specific local address when connecting to the remote host. This can be used to choose from multiple outgoing interfaces. Either address or port (or both) can be given. + +.TP +.B \-Q \fIalgo +Query supported algorithms. Options are kex, sig, cipher, mac, compress + .TP .B \-V Print the version diff --git a/manpages/dropbear.8 b/manpages/dropbear.8 index a148eb7b8..fc44db316 100644 --- a/manpages/dropbear.8 +++ b/manpages/dropbear.8 @@ -133,6 +133,11 @@ options when using forced command. .B \-D \fIauthorized_keys_dir Specify the directory to use for authorized_keys files. The default is ~/.ssh , paths with a leading ~/ will be home directory expanded. + +.TP +.B \-Q \fIalgo +Query supported algorithms. Options are kex, sig, cipher, mac, compress + .TP .B \-V Print the version diff --git a/src/cli-runopts.c b/src/cli-runopts.c index cc15d1154..de931f4f3 100644 --- a/src/cli-runopts.c +++ b/src/cli-runopts.c @@ -97,6 +97,7 @@ static void printhelp() { "-m Specify preferred MACs for packet verification (or '-m help')\n" #endif "-b [bind_address][:bind_port]\n" + "-Q Print supported algorithms, or -Q help\n" "-V Version\n" #if DEBUG_TRACE "-v verbose (repeat for more verbose)\n" @@ -139,6 +140,7 @@ void cli_getopts(int argc, char ** argv) { const char *proxycmd_arg = NULL; const char *remoteport_arg = NULL; const char *username_arg = NULL; + const char *algo_print_arg = NULL; char c; /* see printhelp() for options */ @@ -287,6 +289,9 @@ void cli_getopts(int argc, char ** argv) { case 'l': next = &username_arg; break; + case 'Q': + next = &algo_print_arg; + break; case 'h': printhelp(); exit(EXIT_SUCCESS); @@ -411,6 +416,11 @@ void cli_getopts(int argc, char ** argv) { parse_ciphers_macs(); #endif + if (algo_print_arg) { + print_algos(algo_print_arg); + /* No return */ + } + if (host_arg == NULL) { /* missing hostname */ printhelp(); dropbear_exit("Remote host needs to provided."); @@ -544,7 +554,6 @@ void cli_getopts(int argc, char ** argv) { loadidentityfile(DROPBEAR_DEFAULT_CLI_AUTHKEY, 0); } #endif - } #if DROPBEAR_CLI_PUBKEY_AUTH diff --git a/src/common-runopts.c b/src/common-runopts.c index e9ad31497..1d795ce59 100644 --- a/src/common-runopts.c +++ b/src/common-runopts.c @@ -171,3 +171,47 @@ int split_address_port(const char* spec, char **first, char ** second) { m_free(spec_copy); return ret; } + +void print_algos(const char* algo) { + algo_type *list = NULL, *a = NULL; + int need_data = 0; + + if (strcmp(algo, "kex") == 0) { + list = sshkex; + /* data = 0 kexes aren't algorithms */ + need_data = 1; + } else if (strcmp(algo, "sig") == 0) { + list = sigalgs; + } else if (strcmp(algo, "cipher") == 0) { + list = sshciphers; + } else if (strcmp(algo, "mac") == 0) { + list = sshhashes; + } else if (strcmp(algo, "compress") == 0) { +#ifndef DISABLE_ZLIB + if (opts.compression) { + list = ssh_compress; + } else { + list = ssh_nocompress; + } +#else + list = ssh_nocompress; +#endif + } else { + printf("kex\nsig\ncipher\nmac\ncompress\n"); + if (strcmp(algo, "help") == 0) { + exit(0); + } else { + printf("Unknown -Q '%s'\n", algo); + exit(1); + } + } + + for (a = list; a->name != NULL; a++) { + if (need_data && a->data == NULL) { + continue; + } + printf("%s\n", a->name); + } + + exit(0); +} diff --git a/src/runopts.h b/src/runopts.h index 8a17831fd..166ad4a70 100644 --- a/src/runopts.h +++ b/src/runopts.h @@ -30,6 +30,7 @@ #include "buffer.h" #include "auth.h" #include "forward.h" +#include "dbhelpers.h" typedef struct runopts { @@ -214,6 +215,7 @@ void cli_getopts(int argc, char ** argv); #if DROPBEAR_USER_ALGO_LIST void parse_ciphers_macs(void); #endif +void print_algos(const char* algo) ATTRIB_NORETURN; void print_version(void); void parse_recv_window(const char* recv_window_arg); @@ -227,4 +229,5 @@ void loadidentityfile(const char* filename, int warnfail); void read_config_file(char* filename, FILE* config_file, cli_runopts* options); #endif + #endif /* DROPBEAR_RUNOPTS_H_ */ diff --git a/src/svr-runopts.c b/src/svr-runopts.c index f9a804fcd..b5fa462eb 100644 --- a/src/svr-runopts.c +++ b/src/svr-runopts.c @@ -120,6 +120,7 @@ static void printhelp(const char * progname) { "-A [,]\n" " Enable external public key auth through \n" #endif + "-Q Print supported algorithms, or -Q help\n" "-V Version\n" #if DEBUG_TRACE "-v verbose (repeat for more verbose)\n" @@ -155,6 +156,7 @@ void svr_getopts(int argc, char ** argv) { char* maxauthtries_arg = NULL; char* reexec_fd_arg = NULL; char* keyfile = NULL; + char *algo_print_arg = NULL; char c; #if DROPBEAR_PLUGIN char* pubkey_plugin = NULL; @@ -350,6 +352,9 @@ void svr_getopts(int argc, char ** argv) { case 'g': break; #endif + case 'Q': + next = &algo_print_arg; + break; case 'h': printhelp(argv[0]); exit(EXIT_SUCCESS); @@ -504,6 +509,10 @@ void svr_getopts(int argc, char ** argv) { svr_opts.pubkey_plugin_options = args; } #endif + if (algo_print_arg) { + print_algos(algo_print_arg); + /* No return */ + } } static void addportandaddress(const char* spec) { From 25d942a405780b0c912b7bece26158dd13fce95e Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Sun, 10 May 2026 18:40:29 +0800 Subject: [PATCH 086/122] Manpage doc for dbclient -o compression --- manpages/dbclient.1 | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/manpages/dbclient.1 b/manpages/dbclient.1 index c188e79c6..a7cc491e8 100644 --- a/manpages/dbclient.1 +++ b/manpages/dbclient.1 @@ -156,7 +156,8 @@ Specify a comma separated list of authentication MACs to enable. Use \fI-m help\ Can be used to give options in the format used by OpenSSH config file. This is useful for specifying options for which there is no separate command-line flag. For full details of the options listed below, and their possible values, see -ssh_config(5). +ssh_config(5). "yes"/"no" arguments also accept y/n. + The following options have currently been implemented: .RS @@ -167,6 +168,14 @@ Disable interactive prompts e.g. password prompts and host key confirmation. The .B BindAddress Specify address and port on the local machine as the source address of the connection. .TP +.B Compression +Can be set to "yes" or "no". Default is no compression (with default +build options). Enabling compression can be a security weakness in some +circumstances, as the size of network traffic may leak information +about the encrypted data. + +Compression will only be used when the server also supports it. +.TP .B DisableTrivialAuth Disallow a server immediately giving successful authentication (without presenting any password/pubkey prompt). From 191cb1158f367fd3afa68922877bed5326c9abbf Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Sun, 10 May 2026 18:49:54 +0800 Subject: [PATCH 087/122] Release 2026.91 --- CHANGES | 18 ++++++++++++++++++ debian/changelog | 6 ++++++ src/sysoptions.h | 2 +- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 323fdffcf..533253ae7 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,21 @@ +2026.91 - 10 May 2026 + +- scp: Fix test for disallowing -r with existing target directory. The logic + introduced in 2026.90 was incorrect, could also disallow non-recursive + transfers. + +- scp: Fix regression in 2026.90 building on older glibc or other libc. + reallocarray() was required, it is no longer needed. + +- Compression is now disabled by default for dbclient. A new -o compression option + can enable it. DROPBEAR_CLI_COMPRESSION in localoptions.h can change the default. + Enabling compression can be a security weakness in some + circumstances, as the size of network traffic may leak information + about the encrypted data. + +- Added '-Q' argument for dbclient and dropbear to query supported algorithms, + kex sig cipher mac compress + 2026.90 - 3 May 2026 - Security: server: Fix ability to bypass an authorized_keys "forced_command" diff --git a/debian/changelog b/debian/changelog index c1ac3da8c..e273b2a7f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +dropbear (2026.91-0.1) unstable; urgency=low + + * New upstream release. + + -- Matt Johnston Sun, 10 May 2026 22:51:57 +0800 + dropbear (2026.90-0.1) unstable; urgency=low * New upstream release. diff --git a/src/sysoptions.h b/src/sysoptions.h index fef2a5cf1..7ac8fea89 100644 --- a/src/sysoptions.h +++ b/src/sysoptions.h @@ -4,7 +4,7 @@ *******************************************************************/ #ifndef DROPBEAR_VERSION -#define DROPBEAR_VERSION "2026.90" +#define DROPBEAR_VERSION "2026.91" #endif /* IDENT_VERSION_PART is the optional part after "SSH-2.0-dropbear". Refer to RFC4253 for requirements. */ From e37bb4ca6e1afb85f38dcf116377d6a7f4aa3a31 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 11 May 2026 20:53:04 +0800 Subject: [PATCH 088/122] Improve confusing wording in CHANGES The entry doesn't refer to a test (unit test etc), but rather to logic in an "if" statement. --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 533253ae7..45fe4620b 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,6 @@ 2026.91 - 10 May 2026 -- scp: Fix test for disallowing -r with existing target directory. The logic +- scp: Fix disallowing -r with existing target directory. The logic introduced in 2026.90 was incorrect, could also disallow non-recursive transfers. From b487b111d0cf735c640e6668aa888f7da4e78b3c Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 11 May 2026 20:43:50 +0800 Subject: [PATCH 089/122] sntrup: Fix 64-bit literals Avoids warning on 32-bit platform src/sntrup761.c:1643: warning: integer constant is too large for 'long' type --- src/sntrup761.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sntrup761.c b/src/sntrup761.c index c4cea2263..9dd83caae 100644 --- a/src/sntrup761.c +++ b/src/sntrup761.c @@ -1640,9 +1640,9 @@ __attribute__((unused)) static inline int crypto_int64_ones_num(crypto_int64 crypto_int64_x) { crypto_int64_unsigned crypto_int64_y = crypto_int64_x; - const crypto_int64 C0 = 0x5555555555555555; - const crypto_int64 C1 = 0x3333333333333333; - const crypto_int64 C2 = 0x0f0f0f0f0f0f0f0f; + const crypto_int64 C0 = INT64_C(0x5555555555555555); + const crypto_int64 C1 = INT64_C(0x3333333333333333); + const crypto_int64 C2 = INT64_C(0x0f0f0f0f0f0f0f0f); crypto_int64_y -= ((crypto_int64_y >> 1) & C0); crypto_int64_y = (crypto_int64_y & C1) + ((crypto_int64_y >> 2) & C1); crypto_int64_y = (crypto_int64_y + (crypto_int64_y >> 4)) & C2; From 672f5963a525c167dc6c103b86d2aa1aceb1a3ec Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 11 May 2026 20:47:46 +0800 Subject: [PATCH 090/122] Local debian packaging: don't set CC for make On old platforms where gcc requires a -std=gnu99 argument for c99 support, the configure script detects that and adds it to CC. Setting CC back to "gcc" in debian/rules reverts that, so sntrup761/mlkem fail to compile. The whole tarball debian/ directory is ancient and unmaintained, but is kept since it is still being used on old distros. Upstream debian packaging should be used where possible, thanks to Guilhem Moulin and prior Debian maintainers. --- debian/rules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/rules b/debian/rules index 52e0b0c01..c34ac230f 100755 --- a/debian/rules +++ b/debian/rules @@ -39,7 +39,7 @@ config.status: patch-stamp configure build: deb-checkdir build-stamp build-stamp: config.status - $(MAKE) CC='$(CC)' LD='$(CC)' + $(MAKE) touch build-stamp clean: deb-checkdir deb-checkuid From a05569c6124006bd9b4823db30e824953c5024de Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Wed, 13 May 2026 08:40:17 +0800 Subject: [PATCH 091/122] Increase MAX_HOSTKEYS to 6 This allows all key types to be loaded at once, including different ecdsa sizes. Suggested by Darren Tucker. --- src/sysoptions.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sysoptions.h b/src/sysoptions.h index 7ac8fea89..f9bcc3568 100644 --- a/src/sysoptions.h +++ b/src/sysoptions.h @@ -283,7 +283,7 @@ #define MAX_KEX_PARTS 1000 #endif -#define MAX_HOSTKEYS 4 +#define MAX_HOSTKEYS 6 /* The maximum size of the bignum portion of the kexhash buffer */ /* K_S + Q_C + Q_S + K */ From a38e03d8723b480a444784dbe3a28ac20e38cfcf Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Wed, 13 May 2026 08:56:43 +0800 Subject: [PATCH 092/122] Fix spelling in CHANGES --- CHANGES | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 45fe4620b..1586c1f0d 100644 --- a/CHANGES +++ b/CHANGES @@ -44,7 +44,7 @@ Patch from OpenSSH, tracked as CVE-2026-35385 - Security: client/server: Fix close() of a file descriptor from an out-of-bounds - read. This seems dificult to exploit but may have unforseen effects. + read. This seems difficult to exploit but may have unforeseen effects. https://github.com/mkj/dropbear/commit/067fd3846c425d1998c7c94d3feafe1f26b514eb Reported by Ankit Singh and @j499261162 @@ -137,7 +137,7 @@ Affects SSH.NET https://github.com/sshnet/SSH.NET/issues/1671 Reported by Rob Hague. -- Ignore -g -s when passwords arent enabled. Patch from Norbert Lange. +- Ignore -g -s when passwords aren't enabled. Patch from Norbert Lange. Ignore -m (disable MOTD), -j/-k (tcp forwarding) when not enabled. - Report SIGBUS and SIGTRAP signals. Patch from Loïc Mangeonjean. From 962e5ae1747c2b173fc370fe724bbda090661a50 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Mon, 18 May 2026 22:36:16 +0800 Subject: [PATCH 093/122] Suggest libcrypt-dev for crypt() Recent glibc doesn't include crypt(), it's now provided by libxcrypt. --- configure | 10 ++++++---- configure.ac | 5 +++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/configure b/configure index 8867f8a3c..f09dd89ee 100755 --- a/configure +++ b/configure @@ -10113,15 +10113,17 @@ fi if test "x$ac_cv_func_getpass" != xyes; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: " >&5 printf "%s\n" "$as_me: " >&6;} -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: getpass() not available, dbclient will only have public-key authentication" >&5 -printf "%s\n" "$as_me: getpass() not available, dbclient will only have public-key authentication" >&6;} +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: getpass() not available, dbclient will only have public-key authentication" >&5 +printf "%s\n" "$as_me: WARNING: getpass() not available, dbclient will only have public-key authentication" >&2;} fi if test "t$found_crypt_func" != there; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: " >&5 printf "%s\n" "$as_me: " >&6;} -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: crypt() not available, dropbear server will not have password authentication" >&5 -printf "%s\n" "$as_me: crypt() not available, dropbear server will not have password authentication" >&6;} +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: crypt() is not available, dropbear server will not have password authentication" >&5 +printf "%s\n" "$as_me: WARNING: crypt() is not available, dropbear server will not have password authentication" >&2;} +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: Some platforms require libcrypt-dev package or similar" >&5 +printf "%s\n" "$as_me: Some platforms require libcrypt-dev package or similar" >&6;} fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: " >&5 diff --git a/configure.ac b/configure.ac index bd5dc0250..545c0fcb9 100644 --- a/configure.ac +++ b/configure.ac @@ -942,12 +942,13 @@ fi if test "x$ac_cv_func_getpass" != xyes; then AC_MSG_NOTICE() -AC_MSG_NOTICE([getpass() not available, dbclient will only have public-key authentication]) +AC_MSG_WARN([getpass() not available, dbclient will only have public-key authentication]) fi if test "t$found_crypt_func" != there; then AC_MSG_NOTICE() -AC_MSG_NOTICE([crypt() not available, dropbear server will not have password authentication]) +AC_MSG_WARN([crypt() is not available, dropbear server will not have password authentication]) +AC_MSG_NOTICE([Some platforms require libcrypt-dev package or similar]) fi AC_MSG_NOTICE() From ee65bff1567576a223febcdd5ae552326a4da4b1 Mon Sep 17 00:00:00 2001 From: Matt Johnston Date: Tue, 19 May 2026 19:03:39 +0800 Subject: [PATCH 094/122] Fix too-low pubkey key query count Dropbear 2026.90 added a limit to the number of queries that could be made to a server when determining usable keys. This was intended to be set to 15 (MAX_PUBKEY_QUERIES) but the logic was incorrect (and also debug code was accidentally committed). This meant only 10 (default MAX_AUTH_TRIES/-T) tried keys would be allowed - not a huge difference. Reported by Rui Salvaterra Fixes: db0d3fd0a9e9 ("Limit server number of public key queries") --- src/svr-authpubkey.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/svr-authpubkey.c b/src/svr-authpubkey.c index b0e67baae..402c15f7c 100644 --- a/src/svr-authpubkey.c +++ b/src/svr-authpubkey.c @@ -173,9 +173,9 @@ void svr_auth_pubkey(int valid_user) { * Start counting failures (incrfail) only when it's reaching * the limit. */ - unsigned int free_query_limit = 0; - MAX(0, (int)svr_opts.maxauthtries - MAX_PUBKEY_QUERIES); - int incrfail = ses.authstate.serv_pubkey_query_count > free_query_limit; + unsigned int free_query_limit = + MAX(0, MAX_PUBKEY_QUERIES - (int)svr_opts.maxauthtries); + int incrfail = ses.authstate.serv_pubkey_query_count >= free_query_limit; send_msg_userauth_failure(0, incrfail); ses.authstate.serv_pubkey_query_count++; goto out; From 798d58ca24294f758ef0b0b8fabd1b0b7920e7af Mon Sep 17 00:00:00 2001 From: barry Date: Fri, 5 Jun 2026 21:47:01 +0800 Subject: [PATCH 095/122] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20.gitignore?= =?UTF-8?q?=EF=BC=8C=E6=B7=BB=E5=8A=A0=20.local=20=E5=92=8C=20.idea=20?= =?UTF-8?q?=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index c62a46b09..f01501355 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ tags .vscode/ .DS_Store build/ +.local +.idea \ No newline at end of file From d073936e4da63fb0353f32e97c11462a1b75d029 Mon Sep 17 00:00:00 2001 From: barry Date: Fri, 5 Jun 2026 22:22:39 +0800 Subject: [PATCH 096/122] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20Zig=20=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E6=94=AF=E6=8C=81=EF=BC=8C=E5=8C=85=E5=90=AB=20build.?= =?UTF-8?q?zig=20=E5=92=8C=20build.zig.zon=20=E6=96=87=E4=BB=B6=EF=BC=8C?= =?UTF-8?q?=E5=B9=B6=E6=9B=B4=E6=96=B0=20.gitignore=20=E4=BB=A5=E6=8E=92?= =?UTF-8?q?=E9=99=A4=E7=94=9F=E6=88=90=E7=9A=84=E7=BC=93=E5=AD=98=E7=9B=AE?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release-zig.yml | 163 +++++++++++ .gitignore | 4 +- build.zig | 450 ++++++++++++++++++++++++++++++ build.zig.zon | 14 + 4 files changed, 630 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release-zig.yml create mode 100644 build.zig create mode 100644 build.zig.zon diff --git a/.github/workflows/release-zig.yml b/.github/workflows/release-zig.yml new file mode 100644 index 000000000..5210b22dc --- /dev/null +++ b/.github/workflows/release-zig.yml @@ -0,0 +1,163 @@ +name: Release (zig build) + +# Builds dropbearmulti with `zig build` and publishes cross-compiled binaries. +# Triggered by tags like `zig/v2025.88` (use `-alpha`/`-beta` suffixes for pre-releases). +on: + push: + tags: + - "zig/v*" + workflow_dispatch: + inputs: + version: + description: "Version to build (e.g. v2025.88), no release is published" + required: false + default: "v0.0.0-dev" + +permissions: + contents: write + +jobs: + build: + name: Build ${{ matrix.label }} + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-latest + target: x86_64-linux-musl + label: linux_amd64 + - runner: ubuntu-latest + target: aarch64-linux-musl + label: linux_arm64 + - runner: ubuntu-latest + target: arm-linux-musleabihf + label: linux_armv7 + - runner: macos-latest + target: x86_64-macos + label: darwin_amd64 + - runner: macos-latest + target: aarch64-macos + label: darwin_arm64 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.16.0 + + - name: Cache Zig build artifacts + uses: actions/cache@v4 + with: + path: | + ~/.cache/zig + .zig-cache + key: dropbear-${{ matrix.target }}-${{ hashFiles('build.zig', 'build.zig.zon') }} + restore-keys: | + dropbear-${{ matrix.target }}- + + - name: Parse version and channel + id: version + shell: bash + run: | + if [[ "${GITHUB_REF}" == refs/tags/zig/* ]]; then + version="${GITHUB_REF#refs/tags/zig/}" + else + version="${{ github.event.inputs.version }}" + fi + echo "version=${version}" >> "$GITHUB_OUTPUT" + if [[ "$version" =~ -alpha ]]; then + echo "channel=dev" >> "$GITHUB_OUTPUT" + elif [[ "$version" =~ -beta ]]; then + echo "channel=beta" >> "$GITHUB_OUTPUT" + else + echo "channel=prod" >> "$GITHUB_OUTPUT" + fi + + - name: Install UPX (production Linux only) + if: steps.version.outputs.channel == 'prod' && startsWith(matrix.runner, 'ubuntu') + run: sudo apt-get update && sudo apt-get install -y upx + + - name: Build dropbearmulti + shell: bash + run: | + channel="${{ steps.version.outputs.channel }}" + if [[ "$channel" == "prod" ]]; then + optimize="ReleaseSmall" + else + optimize="ReleaseSafe" + fi + echo "Channel: ${channel}, Optimize: ${optimize}, Target: ${{ matrix.target }}" + zig build -Doptimize="${optimize}" -Dtarget="${{ matrix.target }}" + if [[ "$channel" == "prod" && "${{ matrix.target }}" == *-linux-* ]]; then + upx --best --lzma zig-out/bin/dropbearmulti || true + fi + + - name: Package archive + id: package + shell: bash + run: | + version="${{ steps.version.outputs.version }}" + archive="dropbear_${version}_${{ matrix.label }}.tar.gz" + mkdir -p release-artifacts + tar -czf "release-artifacts/${archive}" -C zig-out/bin dropbearmulti + echo "archive=${archive}" >> "$GITHUB_OUTPUT" + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: dropbear-${{ matrix.label }} + path: release-artifacts/*.tar.gz + retention-days: 7 + + release: + name: Publish release + needs: build + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/zig/') + steps: + - name: Parse version and channel + id: version + shell: bash + run: | + version="${GITHUB_REF#refs/tags/zig/}" + echo "version=${version}" >> "$GITHUB_OUTPUT" + if [[ "$version" =~ -alpha ]]; then + echo "channel=dev" >> "$GITHUB_OUTPUT" + elif [[ "$version" =~ -beta ]]; then + echo "channel=beta" >> "$GITHUB_OUTPUT" + else + echo "channel=prod" >> "$GITHUB_OUTPUT" + fi + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + + - name: Generate checksums + shell: bash + run: | + cd dist + sha256sum *.tar.gz > checksums.txt + cat checksums.txt + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ secrets.RELEASER_TOKEN || secrets.GITHUB_TOKEN }} + shell: bash + run: | + prerelease="" + if [[ "${{ steps.version.outputs.channel }}" != "prod" ]]; then + prerelease="--prerelease" + fi + gh release create "${{ github.ref_name }}" \ + --repo "${{ github.repository }}" \ + --title "dropbear ${{ steps.version.outputs.version }}" \ + --generate-notes \ + $prerelease \ + dist/* diff --git a/.gitignore b/.gitignore index f01501355..f11d8e575 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,6 @@ tags .DS_Store build/ .local -.idea \ No newline at end of file +.idea +/.zig-cache/ +/zig-out/ \ No newline at end of file diff --git a/build.zig b/build.zig new file mode 100644 index 000000000..0d805c446 --- /dev/null +++ b/build.zig @@ -0,0 +1,450 @@ +const std = @import("std"); + +// Dropbear build using `zig build` (Zig 0.16). +// +// This replaces the autoconf/Makefile build for producing release binaries. +// It compiles the bundled libtommath and libtomcrypt as static libraries and +// links them into a single multi-call `dropbearmulti` binary (containing the +// dropbear server, dbclient, dropbearkey, dropbearconvert and scp applets). +// +// A target-appropriate `config.h` is generated at configure time so the build +// works for cross-compilation (notably Linux/musl) without running ./configure. + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + const upx = b.option(bool, "upx", "Compress the binary with UPX (requires upx in PATH)") orelse false; + + const os_tag = target.result.os.tag; + + // ---- Generated headers --------------------------------------------------- + const wf = b.addWriteFiles(); + const config_h = wf.add("config.h", genConfigH(b, os_tag)); + _ = wf.add("default_options_guard.h", genGuard(b)); + // Both generated files live in the same directory. + const gen_dir = config_h.dirname(); + + // Shared include directories for every translation unit. + const includes = [_]std.Build.LazyPath{ + gen_dir, + b.path("src"), + b.path("libtomcrypt/src/headers"), + b.path("libtommath"), + }; + + // Common preprocessor / compiler flags. + var common_flags: std.ArrayList([]const u8) = .empty; + common_flags.appendSlice(b.allocator, &.{ + "-D_GNU_SOURCE", + "-std=gnu99", + "-fno-strict-aliasing", + "-Wno-pointer-sign", + }) catch @panic("OOM"); + if (os_tag == .linux) { + common_flags.append(b.allocator, "-D_FILE_OFFSET_BITS=64") catch @panic("OOM"); + } + + // ---- libtommath ---------------------------------------------------------- + const ltm = b.addLibrary(.{ + .name = "tommath", + .linkage = .static, + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, + }), + }); + for (includes) |inc| ltm.root_module.addIncludePath(inc); + ltm.step.dependOn(&wf.step); + ltm.root_module.addCSourceFiles(.{ + .root = b.path("."), + .files = collectCSources(b, "libtommath", false), + .flags = common_flags.items, + }); + + // ---- libtomcrypt --------------------------------------------------------- + var ltc_flags: std.ArrayList([]const u8) = .empty; + ltc_flags.appendSlice(b.allocator, common_flags.items) catch @panic("OOM"); + ltc_flags.append(b.allocator, "-DLTC_SOURCE") catch @panic("OOM"); + + const ltc = b.addLibrary(.{ + .name = "tomcrypt", + .linkage = .static, + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, + }), + }); + for (includes) |inc| ltc.root_module.addIncludePath(inc); + ltc.step.dependOn(&wf.step); + ltc.root_module.addCSourceFiles(.{ + .root = b.path("."), + .files = collectCSources(b, "libtomcrypt/src", true), + .flags = ltc_flags.items, + }); + + // ---- dropbearmulti ------------------------------------------------------- + var db_flags: std.ArrayList([]const u8) = .empty; + db_flags.appendSlice(b.allocator, common_flags.items) catch @panic("OOM"); + db_flags.appendSlice(b.allocator, &.{ + "-DDROPBEAR_SERVER", + "-DDROPBEAR_CLIENT", + "-DDROPBEAR_MULTI", + "-DDBMULTI_dropbear", + "-DDBMULTI_dbclient", + "-DDBMULTI_dropbearkey", + "-DDBMULTI_dropbearconvert", + "-DDBMULTI_scp", + "-DPROGRESS_METER", + }) catch @panic("OOM"); + + const exe = b.addExecutable(.{ + .name = "dropbearmulti", + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, + .strip = if (optimize != .Debug) true else null, + }), + }); + for (includes) |inc| exe.root_module.addIncludePath(inc); + exe.step.dependOn(&wf.step); + exe.root_module.addCSourceFiles(.{ + .root = b.path("src"), + .files = &dropbear_sources, + .flags = db_flags.items, + }); + exe.root_module.linkLibrary(ltc); + exe.root_module.linkLibrary(ltm); + + const install = b.addInstallArtifact(exe, .{}); + + if (upx) { + const upx_step = b.addSystemCommand(&.{ + "upx", "--best", "--lzma", + b.getInstallPath(.bin, "dropbearmulti"), + }); + upx_step.step.dependOn(&install.step); + b.getInstallStep().dependOn(&upx_step.step); + } else { + b.getInstallStep().dependOn(&install.step); + } +} + +// dropbear source files (relative to `src/`). This is the union of the object +// lists in Makefile.in needed for a multi-call binary that bundles the server, +// client, dropbearkey, dropbearconvert and scp. +const dropbear_sources = [_][]const u8{ + // dbmulti dispatcher + "dbmulti.c", + // COMMONOBJS + "dbutil.c", + "buffer.c", + "dbhelpers.c", + "dss.c", + "bignum.c", + "signkey.c", + "rsa.c", + "dbrandom.c", + "queue.c", + "atomicio.c", + "compat.c", + "fake-rfc2553.c", + "ltc_prng.c", + "ecc.c", + "ecdsa.c", + "sk-ecdsa.c", + "crypto_desc.c", + "curve25519.c", + "ed25519.c", + "sk-ed25519.c", + "dbmalloc.c", + "gensignkey.c", + "gendss.c", + "genrsa.c", + "gened25519.c", + // CLISVROBJS + "common-session.c", + "packet.c", + "common-algo.c", + "common-kex.c", + "common-channel.c", + "common-chansession.c", + "termcodes.c", + "loginrec.c", + "tcp-accept.c", + "listener.c", + "process-packet.c", + "dh_groups.c", + "common-runopts.c", + "circbuffer.c", + "list.c", + "netio.c", + "chachapoly.c", + "gcm.c", + "kex-x25519.c", + "kex-dh.c", + "kex-ecdh.c", + "kex-pqhybrid.c", + "sntrup761.c", + "mlkem768.c", + // SVROBJS + "svr-kex.c", + "svr-auth.c", + "sshpty.c", + "svr-authpasswd.c", + "svr-authpubkey.c", + "svr-authpubkeyoptions.c", + "svr-session.c", + "svr-service.c", + "svr-chansession.c", + "svr-runopts.c", + "svr-agentfwd.c", + "svr-main.c", + "svr-x11fwd.c", + "svr-forward.c", + "svr-tcpfwd.c", + "svr-streamfwd.c", + "svr-authpam.c", + // CLIOBJS + "cli-main.c", + "cli-auth.c", + "cli-authpasswd.c", + "cli-kex.c", + "cli-session.c", + "cli-runopts.c", + "cli-chansession.c", + "cli-authpubkey.c", + "cli-tcpfwd.c", + "cli-channel.c", + "cli-authinteract.c", + "cli-agentfwd.c", + "cli-readconf.c", + // KEYOBJS + "dropbearkey.c", + // CONVERTOBJS + "dropbearconvert.c", + "keyimport.c", + "signkey_ossh.c", + // SCPOBJS (atomicio.c / compat.c already present above) + "scp.c", + "progressmeter.c", + "scpmisc.c", +}; + +// Walk a directory under the build root and collect every `.c` file, returning +// paths relative to the build root. +fn collectCSources(b: *std.Build, comptime sub: []const u8, recursive: bool) [][]const u8 { + const io = b.graph.io; + var list: std.ArrayList([]const u8) = .empty; + var dir = b.build_root.handle.openDir(io, sub, .{ .iterate = true }) catch |err| { + std.debug.panic("failed to open {s}: {s}", .{ sub, @errorName(err) }); + }; + defer dir.close(io); + + if (recursive) { + var walker = dir.walk(b.allocator) catch @panic("OOM"); + defer walker.deinit(); + while (walker.next(io) catch @panic("walk failed")) |entry| { + if (entry.kind != .file) continue; + if (!std.mem.endsWith(u8, entry.basename, ".c")) continue; + // *tab.c files in libtomcrypt are lookup tables that are #included + // by other sources, not compiled on their own. + if (std.mem.endsWith(u8, entry.basename, "tab.c")) continue; + const rel = std.fs.path.join(b.allocator, &.{ sub, entry.path }) catch @panic("OOM"); + list.append(b.allocator, rel) catch @panic("OOM"); + } + } else { + var it = dir.iterate(); + while (it.next(io) catch @panic("iterate failed")) |entry| { + if (entry.kind != .file) continue; + if (!std.mem.endsWith(u8, entry.name, ".c")) continue; + const rel = std.fs.path.join(b.allocator, &.{ sub, entry.name }) catch @panic("OOM"); + list.append(b.allocator, rel) catch @panic("OOM"); + } + } + return list.toOwnedSlice(b.allocator) catch @panic("OOM"); +} + +// Generate default_options_guard.h from src/default_options.h by wrapping every +// `#define X Y` in an `#ifndef X ... #endif` guard (equivalent to +// src/ifndef_wrapper.sh). The guards let the generated config.h override any +// default (e.g. disabling DROPBEAR_SVR_DROP_PRIVS on platforms without +// setresgid()). +fn genGuard(b: *std.Build) []const u8 { + const io = b.graph.io; + const input = b.build_root.handle.readFileAlloc( + io, + "src/default_options.h", + b.allocator, + .unlimited, + ) catch |err| { + std.debug.panic("failed to read default_options.h: {s}", .{@errorName(err)}); + }; + + var out: std.ArrayList(u8) = .empty; + out.appendSlice( + b.allocator, + "/* Generated by build.zig from default_options.h - do not edit */\n", + ) catch @panic("OOM"); + + var it = std.mem.splitScalar(u8, input, '\n'); + while (it.next()) |line| { + if (definedName(line)) |name| { + const wrapped = std.fmt.allocPrint( + b.allocator, + "#ifndef {s}\n{s}\n#endif\n", + .{ name, line }, + ) catch @panic("OOM"); + out.appendSlice(b.allocator, wrapped) catch @panic("OOM"); + } else { + out.appendSlice(b.allocator, line) catch @panic("OOM"); + out.append(b.allocator, '\n') catch @panic("OOM"); + } + } + return out.toOwnedSlice(b.allocator) catch @panic("OOM"); +} + +// Returns the macro name if `line` is `#define `. +fn definedName(line: []const u8) ?[]const u8 { + var s = line; + while (s.len > 0 and (s[0] == ' ' or s[0] == '\t')) s = s[1..]; + const prefix = "#define "; + if (!std.mem.startsWith(u8, s, prefix)) return null; + s = s[prefix.len..]; + while (s.len > 0 and (s[0] == ' ' or s[0] == '\t')) s = s[1..]; + var i: usize = 0; + while (i < s.len and s[i] != ' ' and s[i] != '\t') i += 1; + if (i == 0) return null; + // Require a value after the name (matching the sed `.* ` semantics). + if (i >= s.len) return null; + return s[0..i]; +} + +fn genConfigH(b: *std.Build, os_tag: std.Target.Os.Tag) []const u8 { + var out: std.ArrayList(u8) = .empty; + out.appendSlice(b.allocator, common_config) catch @panic("OOM"); + switch (os_tag) { + .macos, .ios, .tvos, .watchos => out.appendSlice(b.allocator, macos_config) catch @panic("OOM"), + else => out.appendSlice(b.allocator, linux_config) catch @panic("OOM"), + } + out.appendSlice(b.allocator, "\n#endif /* DROPBEAR_CONFIG_H */\n") catch @panic("OOM"); + return out.toOwnedSlice(b.allocator) catch @panic("OOM"); +} + +const common_config = + \\/* Generated by build.zig - do not edit */ + \\#ifndef DROPBEAR_CONFIG_H + \\#define DROPBEAR_CONFIG_H + \\ + \\#define BUNDLED_LIBTOM 1 + \\#define DROPBEAR_FUZZ 0 + \\#define DROPBEAR_PLUGIN 0 + \\ + \\/* Login recording is disabled for the portable zig build. */ + \\#define DISABLE_PAM 1 + \\#define DISABLE_ZLIB 1 + \\#define DISABLE_LASTLOG 1 + \\#define DISABLE_UTMP 1 + \\#define DISABLE_UTMPX 1 + \\#define DISABLE_WTMP 1 + \\#define DISABLE_WTMPX 1 + \\ + \\#define HAVE_BASENAME 1 + \\#define HAVE_CLOCK_GETTIME 1 + \\#define HAVE_CONST_GAI_STRERROR_PROTO 1 + \\#define HAVE_CRYPT 1 + \\#define HAVE_DAEMON 1 + \\#define HAVE_FORK 1 + \\#define HAVE_FREEADDRINFO 1 + \\#define HAVE_GAI_STRERROR 1 + \\#define HAVE_GETADDRINFO 1 + \\#define HAVE_GETGROUPLIST 1 + \\#define HAVE_GETNAMEINFO 1 + \\#define HAVE_GETPASS 1 + \\#define HAVE_GETUSERSHELL 1 + \\#define HAVE_INTTYPES_H 1 + \\#define HAVE_LIBGEN_H 1 + \\#define HAVE_NETDB_H 1 + \\#define HAVE_NETINET_IN_H 1 + \\#define HAVE_NETINET_IN_SYSTM_H 1 + \\#define HAVE_NETINET_TCP_H 1 + \\#define HAVE_PATHS_H 1 + \\#define HAVE_PUTENV 1 + \\#define HAVE_STDINT_H 1 + \\#define HAVE_STDIO_H 1 + \\#define HAVE_STDLIB_H 1 + \\#define HAVE_STRINGS_H 1 + \\#define HAVE_STRING_H 1 + \\#define HAVE_STRUCT_ADDRINFO 1 + \\#define HAVE_STRUCT_IN6_ADDR 1 + \\#define HAVE_STRUCT_SOCKADDR_IN6 1 + \\#define HAVE_STRUCT_SOCKADDR_STORAGE 1 + \\#define HAVE_STRUCT_SOCKADDR_STORAGE_SS_FAMILY 1 + \\#define HAVE_SYS_RANDOM_H 1 + \\#define HAVE_SYS_SELECT_H 1 + \\#define HAVE_SYS_SOCKET_H 1 + \\#define HAVE_SYS_STAT_H 1 + \\#define HAVE_SYS_TYPES_H 1 + \\#define HAVE_SYS_UIO_H 1 + \\#define HAVE_SYS_WAIT_H 1 + \\#define HAVE_UINT16_T 1 + \\#define HAVE_UINT32_T 1 + \\#define HAVE_UINT8_T 1 + \\#define HAVE_UNDERSCORE_STATIC_ASSERT 1 + \\#define HAVE_UNISTD_H 1 + \\#define HAVE_U_INT16_T 1 + \\#define HAVE_U_INT32_T 1 + \\#define HAVE_U_INT8_T 1 + \\#define HAVE_WRITEV 1 + \\ + \\#define SELECT_TYPE_ARG1 int + \\#define SELECT_TYPE_ARG234 (fd_set *) + \\#define SELECT_TYPE_ARG5 (struct timeval *) + \\#define STDC_HEADERS 1 + \\ + \\#define PACKAGE_NAME "" + \\#define PACKAGE_STRING "" + \\#define PACKAGE_TARNAME "" + \\#define PACKAGE_VERSION "" + \\#define PACKAGE_BUGREPORT "" + \\#define PACKAGE_URL "" + \\ +; + +const linux_config = + \\/* Linux / musl specific */ + \\#define HAVE_CRYPT_H 1 + \\#define HAVE_SHADOW_H 1 + \\#define HAVE_GETSPNAM 1 + \\#define HAVE_PTY_H 1 + \\#define HAVE_OPENPTY 1 + \\#define HAVE_ENDIAN_H 1 + \\#define HAVE_DECL_HTOLE64 1 + \\#define HAVE_SYS_PRCTL_H 1 + \\#define HAVE_LINUX_PKT_SCHED_H 1 + \\#define HAVE_CLEARENV 1 + \\#define HAVE_FEXECVE 1 + \\#define HAVE_EXPLICIT_BZERO 1 + \\#define HAVE_SETRESGID 1 + \\#define HAVE_GETRANDOM 1 + \\ +; + +const macos_config = + \\/* macOS specific */ + \\#define HAVE_DECL_HTOLE64 1 + \\#define HAVE_SYS_ENDIAN_H 1 + \\#define HAVE_MACH_ABSOLUTE_TIME 1 + \\#define HAVE_MACH_MACH_TIME_H 1 + \\#define HAVE_MEMSET_S 1 + \\#define HAVE_OPENPTY 1 + \\#define HAVE_UTIL_H 1 + \\#define HAVE_STRLCAT 1 + \\#define HAVE_STRLCPY 1 + \\/* macOS/BSD lack setresgid(); disable server privilege dropping that needs it. */ + \\#define DROPBEAR_SVR_MULTIUSER 0 + \\#define DROPBEAR_SVR_DROP_PRIVS 0 + \\ +; diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 000000000..ae4fdc42c --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,14 @@ +.{ + .name = .dropbear, + .version = "2025.88.0", + .minimum_zig_version = "0.16.0", + .dependencies = .{}, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + "libtomcrypt", + "libtommath", + }, + .fingerprint = 0x57da91df300bdbc5, +} From 3e8d6735c8c513c8112b44fa03209565828d146b Mon Sep 17 00:00:00 2001 From: barry Date: Fri, 5 Jun 2026 22:39:08 +0800 Subject: [PATCH 097/122] =?UTF-8?q?=E5=88=A0=E9=99=A4=20GitHub=20Actions?= =?UTF-8?q?=20=E5=8F=91=E5=B8=83=E5=B7=A5=E4=BD=9C=E6=B5=81=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 59 ----------------------------------- 1 file changed, 59 deletions(-) delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index e6973e818..000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Release -on: - push: - tags: - - 'v*.*.*' - -jobs: - release: - name: Release on GitHub - runs-on: ubuntu-24.04 - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Get Release Version - run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Build arm binaries - run: | - sh build.sh arm - - - name: Build arm64 binaries - run: | - sh build.sh arm64 - - - name: Archives & Checksum - id: archives - run: | - sh ./package.sh "${{ env.RELEASE_VERSION }}" - - - name: Release - uses: softprops/action-gh-release@v2 - id: create_release - if: github.ref_type == 'tag' - with: - generate_release_notes: true - files: | - ${{ env.RELEASE_VERSION }}-linux-arm.tar.gz - ${{ env.RELEASE_VERSION }}-linux-arm_checksums.txt - ${{ env.RELEASE_VERSION }}-linux-arm64.tar.gz - ${{ env.RELEASE_VERSION }}-linux-arm64_checksums.txt - - - name: Notify Slack - uses: rtCamp/action-slack-notify@v2 - env: - SLACK_CHANNEL: team-release - SLACK_COLOR: ${{ job.status }} # or a specific color like 'good' or '#ff00ff' - SLACK_ICON: "https://a.slack-edge.com/production-standard-emoji-assets/14.0/apple-medium/1f4ac@2x.png" - SLACK_MESSAGE: "${{ github.event.repository.name }} ${{ env.RELEASE_VERSION }} is out! Check it out at ${{ steps.create_release.outputs.url }}" - SLACK_TITLE: "${{ github.event.repository.name }} ${{ env.RELEASE_VERSION }} Release" - SLACK_USERNAME: release - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} From 426d155527cce4fc9f36999f77bfdf65cd2d1618 Mon Sep 17 00:00:00 2001 From: barry Date: Fri, 5 Jun 2026 22:39:17 +0800 Subject: [PATCH 098/122] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20sftp-server=20?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E6=94=AF=E6=8C=81=EF=BC=8C=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=8F=91=E5=B8=83=E5=B7=A5=E4=BD=9C=E6=B5=81=E4=BB=A5=E5=8C=85?= =?UTF-8?q?=E5=90=AB=20sftp-server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release-zig.yml | 14 +++++++- build-sftp-server.sh | 60 +++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100755 build-sftp-server.sh diff --git a/.github/workflows/release-zig.yml b/.github/workflows/release-zig.yml index 5210b22dc..5638c9ae5 100644 --- a/.github/workflows/release-zig.yml +++ b/.github/workflows/release-zig.yml @@ -96,6 +96,15 @@ jobs: upx --best --lzma zig-out/bin/dropbearmulti || true fi + - name: Build sftp-server (Linux only) + if: startsWith(matrix.runner, 'ubuntu') + shell: bash + run: | + ./build-sftp-server.sh "${{ matrix.target }}" zig-out/bin + if [[ "${{ steps.version.outputs.channel }}" == "prod" ]]; then + upx --best --lzma zig-out/bin/sftp-server || true + fi + - name: Package archive id: package shell: bash @@ -103,7 +112,10 @@ jobs: version="${{ steps.version.outputs.version }}" archive="dropbear_${version}_${{ matrix.label }}.tar.gz" mkdir -p release-artifacts - tar -czf "release-artifacts/${archive}" -C zig-out/bin dropbearmulti + # Bundle dropbearmulti plus sftp-server (Linux) if present. + files="dropbearmulti" + [ -f zig-out/bin/sftp-server ] && files="$files sftp-server" + tar -czf "release-artifacts/${archive}" -C zig-out/bin $files echo "archive=${archive}" >> "$GITHUB_OUTPUT" - name: Upload artifact diff --git a/build-sftp-server.sh b/build-sftp-server.sh new file mode 100755 index 000000000..e33a44aa7 --- /dev/null +++ b/build-sftp-server.sh @@ -0,0 +1,60 @@ +#!/bin/sh +# Cross-compile OpenSSH's sftp-server using `zig cc` as the C compiler. +# +# Dropbear's server execs an external sftp-server binary for the SFTP subsystem; +# it does not ship one itself. This script produces a statically linked +# sftp-server that can be bundled alongside dropbearmulti. +# +# Usage: build-sftp-server.sh [openssh-version] +# e.g. x86_64-linux-musl, aarch64-linux-musl, arm-linux-musleabihf +# directory to copy the resulting `sftp-server` into +# [openssh-version] OpenSSH portable version (default below) +set -eu + +ZIG_TARGET="${1:?usage: build-sftp-server.sh [openssh-version]}" +OUT_DIR="${2:?usage: build-sftp-server.sh [openssh-version]}" +OPENSSH_VERSION="${3:-9.9p2}" + +# Use the zig target triple directly as the autoconf --host so configure runs in +# cross-compile mode; zig cc performs the actual codegen via -target. +HOST="$ZIG_TARGET" + +workdir="$(mktemp -d)" +trap 'rm -rf "$workdir"' EXIT + +tarball="openssh-${OPENSSH_VERSION}.tar.gz" +url="https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/${tarball}" +echo "==> Downloading ${url}" +curl -fsSL -o "$workdir/$tarball" "$url" +tar -xzf "$workdir/$tarball" -C "$workdir" +src="$workdir/openssh-${OPENSSH_VERSION}" + +cd "$src" + +# Avoid leaking the host toolchain's CPPFLAGS/CFLAGS/LDFLAGS into the cross build. +unset CPPFLAGS CFLAGS LDFLAGS LIBS + +echo "==> Configuring OpenSSH for ${ZIG_TARGET}" +./configure \ + --host="$HOST" \ + CC="zig cc -target $ZIG_TARGET" \ + AR="zig ar" \ + RANLIB="zig ranlib" \ + --without-openssl \ + --without-zlib \ + --without-pam \ + --without-selinux \ + --without-kerberos5 \ + --without-libedit \ + --without-ldns \ + --with-sandbox=no \ + --disable-strip \ + CFLAGS="-Os" \ + LDFLAGS="-static" + +echo "==> Building sftp-server" +make sftp-server -j"$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 2)" + +mkdir -p "$OUT_DIR" +cp sftp-server "$OUT_DIR/sftp-server" +echo "==> Built $OUT_DIR/sftp-server" From 64ffadbfb2895b43d070b48062ff23cf73edd3b4 Mon Sep 17 00:00:00 2001 From: barry Date: Fri, 5 Jun 2026 22:41:50 +0800 Subject: [PATCH 099/122] =?UTF-8?q?=E5=88=A0=E9=99=A4=20autoconf=20?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E6=AE=8B=E7=95=99(build.sh=E3=80=81Dockerfil?= =?UTF-8?q?e=20=E5=8F=8A=20autoconf=20workflows)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/autoconf.yml | 24 --- .github/workflows/build.yml | 325 -------------------------------- .github/workflows/outoftree.yml | 24 --- .github/workflows/tarball.yml | 36 ---- Dockerfile | 68 ------- build.sh | 61 ------ 6 files changed, 538 deletions(-) delete mode 100644 .github/workflows/autoconf.yml delete mode 100644 .github/workflows/build.yml delete mode 100644 .github/workflows/outoftree.yml delete mode 100644 .github/workflows/tarball.yml delete mode 100644 Dockerfile delete mode 100755 build.sh diff --git a/.github/workflows/autoconf.yml b/.github/workflows/autoconf.yml deleted file mode 100644 index abad8450b..000000000 --- a/.github/workflows/autoconf.yml +++ /dev/null @@ -1,24 +0,0 @@ -# Checks that autoconf has been run if configure.ac was updated -# Assumes that autoconf 2.71 was run, the same as ubuntu 22.04 -name: Autoconf Up To Date -on: - pull_request: - workflow_dispatch: - push: -jobs: - autoconf: - runs-on: 'ubuntu-22.04' - - steps: - - name: deps - run: | - sudo apt-get -y update - sudo apt-get -y install autoconf - - - uses: actions/checkout@v6 - - - name: run autoconf - run: autoconf && autoheader - - - name: check no difference - run: git diff --exit-code diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index e6438f388..000000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,325 +0,0 @@ -# Can be used locally with https://github.com/nektos/act -# Note the XXX line below. - -name: BuildTest -on: - pull_request: - workflow_dispatch: - push: -jobs: - build: - runs-on: ${{ matrix.os || 'ubuntu-24.04' }} - strategy: - matrix: - # XXX uncomment the line below to work with act, see https://github.com/nektos/act/issues/996 - # name: [] - - # Rather than a boolean False we use eg - # runcheck: 'no' - # Otherwise GH expressions will make a None var - # compare with False. We want an undefined default of True. - - # MULTI and NOWRITEV are passed as integers to the build - include: - - name: plain linux - upload_sizes: 1 - - - name: multi binary - multi: 1 - multilink: 1 - - - name: multi binary, dropbearmulti argv0 - multi: 1 - multiwrapper: 1 - - - name: client only - runcheck: 'no' - make_target: PROGRAMS=dbclient - - - name: server only - runcheck: 'no' - make_target: PROGRAMS=dropbear - - - name: bundled libtom, 22.04, no writev() - # test can use an older distro with bundled libtommath - os: ubuntu-22.04 - configure_flags: --enable-bundled-libtom --enable-werror - # NOWRITEV is unrelated, test here to save a job - nowritev: 1 - # our tests expect >= python3.7 - runcheck: 'no' - - - name: linux clang - cc: clang - - # Some platforms only have old compilers, we try to keep - # compatibilty. For some reason -std=c89 doesn't enforce - # early declarations so we specify it anyway. - - name: c89 - extracflags: -std=c89 -Wdeclaration-after-statement - # enable all options - nondefault: 1 - configure_flags: --enable-pam - # sntrup761.c is not c89 compliant - localoptions: | - #define DROPBEAR_SNTRUP761 0 - #define DROPBEAR_MLKEM768 0 - - - name: macos 14 - os: macos-14 - cc: clang - # OS X says daemon() and utmp are deprecated. - # OS X tests for undefined TARGET_OS_EMBEDDED in libc headers - extracflags: -Wno-deprecated-declarations -Wno-undef - runcheck: 'no' - apt: 'no' - # fails with: - # .../ranlib: file: libtomcrypt.a(cbc_setiv.o) has no symbols - ranlib: ranlib -no_warning_for_no_symbols - # macos doesn't have setresgid - localoptions: | - #define DROPBEAR_SVR_DROP_PRIVS 0 - #define DROPBEAR_SVR_LOCALSTREAMFWD 0 - - - name: macos 15 - os: macos-15 - cc: clang - # OS X says daemon() and utmp are deprecated. - # OS X tests for undefined TARGET_OS_EMBEDDED in libc headers - extracflags: -Wno-deprecated-declarations -Wno-undef - runcheck: 'no' - apt: 'no' - # fails with: - # .../ranlib: file: libtomcrypt.a(cbc_setiv.o) has no symbols - ranlib: ranlib -no_warning_for_no_symbols - # macos doesn't have setresgid - localoptions: | - #define DROPBEAR_SVR_DROP_PRIVS 0 - #define DROPBEAR_SVR_LOCALSTREAMFWD 0 - - # Check that debug code doesn't bitrot - - name: DEBUG_TRACE - nondefault: 1 - configure_flags: --enable-pam - localoptions: | - #define DEBUG_TRACE 5 - - # Check off-by-default options don't bitrot - - name: nondefault options - nondefault: 1 - configure_flags: --enable-pam - - - name: most options disabled - configure_flags: --disable-harden --disable-zlib --disable-openpty --disable-lastlog - runcheck: 'no' - localoptions: | - #define DROPBEAR_RSA 0 - #define INETD_MODE 0 - #define DROPBEAR_REEXEC 0 - #define DROPBEAR_SMALL_CODE 0 - #define DROPBEAR_CLI_LOCALTCPFWD 0 - #define DROPBEAR_CLI_REMOTETCPFWD 0 - #define DROPBEAR_SVR_LOCALTCPFWD 0 - #define DROPBEAR_SVR_REMOTETCPFWD 0 - #define DROPBEAR_SVR_REMOTESTREAMFWD 0 - #define DROPBEAR_SVR_AGENTFWD 0 - #define DROPBEAR_CLI_AGENTFWD 0 - #define DROPBEAR_CLI_PROXYCMD 0 - #define DROPBEAR_USER_ALGO_LIST 0 - #define DROPBEAR_AES128 0 - #define DROPBEAR_AES256 0 - #define DROPBEAR_ENABLE_CTR_MODE 0 - #define DROPBEAR_SHA1_HMAC 0 - #define DROPBEAR_SHA2_256_HMAC 0 - #define DROPBEAR_RSA 0 - #define DROPBEAR_ECDSA 0 - #define DROPBEAR_SK_KEYS 0 - #define DROPBEAR_DELAY_HOSTKEY 0 - #define DROPBEAR_DH_GROUP14_SHA1 0 - #define DROPBEAR_DH_GROUP14_SHA256 0 - #define DROPBEAR_ECDH 0 - #define DROPBEAR_DH_GROUP1_CLIENTONLY 0 - #define DO_MOTD 0 - #define DROPBEAR_SVR_PUBKEY_AUTH 0 - #define DROPBEAR_CLI_PASSWORD_AUTH 0 - #define DROPBEAR_CLI_PUBKEY_AUTH 0 - #define DROPBEAR_USE_PASSWORD_ENV 0 - #define DROPBEAR_SFTPSERVER 0 - - - name: no sha1 - runcheck: 'no' - # disables all sha1 - localoptions: | - #define DROPBEAR_SHA1_HMAC 0 - #define DROPBEAR_RSA_SHA1 0 - #define DROPBEAR_DH_GROUP14_SHA1 0 - #define DROPBEAR_ECDSA 0 - #define DROPBEAR_ED25519 0 - #define DROPBEAR_SK_KEYS 0 - #define DROPBEAR_ENABLE_GCM_MODE 1 - #define DROPBEAR_3DES 1 - #define DROPBEAR_DH_GROUP16 1 - #define DROPBEAR_SHA2_512_HMAC 1 - #define DROPBEAR_CLI_PUBKEY_AUTH 0 - - - name: pq, no plain x25519 - localoptions: | - #define DROPBEAR_CURVE25519 0 - - # # Fuzzers run standalone. A bit superfluous with cifuzz, but - # # good to run the whole corpus to keep it working. - # - name: fuzzing with address sanitizer - # configure_flags: --enable-fuzz --disable-harden --enable-bundled-libtom --enable-werror - # ldflags: -fsanitize=address - # extracflags: -fsanitize=address - # # -fsanitize=address prevents aslr, don't test it - # pytest_addopts: -k "not aslr" - # fuzz: True - # cc: clang - - # # Undefined Behaviour sanitizer - # - name: fuzzing with undefined behaviour sanitizer - # configure_flags: --enable-fuzz --disable-harden --enable-bundled-libtom --enable-werror - # ldflags: -fsanitize=undefined - # # don't fail with alignment due to https://github.com/libtom/libtomcrypt/issues/549 - # extracflags: -fsanitize=undefined -fno-sanitize-recover=undefined -fsanitize-recover=alignment - # pytest_addopts: -k "not aslr" - # fuzz: True - # cc: clang - - env: - MULTI: ${{ matrix.multi }} - CC: ${{ matrix.cc || 'gcc' }} - LDFLAGS: ${{ matrix.ldflags }} - EXTRACFLAGS: ${{ matrix.extracflags }} - CONFIGURE_FLAGS: ${{ matrix.configure_flags || '--enable-werror' }} - MAKE_TARGET: ${{ matrix.make_target }} - # for fuzzing - CXX: clang++ - RANLIB: ${{ matrix.ranlib || 'ranlib' }} - # pytest in "make check" recognises this for extra arguments - PYTEST_ADDOPTS: ${{ matrix.pytest_addopts }} - # some pytests depend on special setup from this file. see authorized_keys below. - DBTEST_IN_ACTION: true - LOCALOPTIONS: ${{ matrix.localoptions }} - - steps: - - name: deps - if: ${{ matrix.apt != 'no' }} - run: | - sudo apt-get -y update - sudo apt-get -y install zlib1g-dev libtomcrypt-dev libtommath-dev mercurial python3-venv libpam0g-dev $CC - - - uses: actions/checkout@v6 - - - name: configure - run: | - $CC --version || echo unknown $CC version - ./configure $CONFIGURE_FLAGS CFLAGS="-O2 -Wall -Wno-pointer-sign $EXTRACFLAGS" --prefix="$HOME/inst" || (cat config.log; exit 1) - - - name: nowritev - if: ${{ matrix.nowritev }} - run: sed -i -e s/HAVE_WRITEV/DONT_HAVE_WRITEV/ config.h - - - name: localoptions - run: | - echo "$LOCALOPTIONS" | tee localoptions.h - - - name: nondefault - if: ${{ matrix.nondefault }} - run: | - # Turn on anything that's off by default. Rough but seems sufficient - grep ' 0$' src/default_options.h | sed 's/0$/1/' >> localoptions.h - # PAM clashes with password - echo "#define DROPBEAR_SVR_PASSWORD_AUTH 0" >> localoptions.h - # 1 second timeout is too short - sed -i "s/DEFAULT_IDLE_TIMEOUT 1/DEFAULT_IDLE_TIMEOUT 99/" localoptions.h - sed -i "s/DEFAULT_MAX_DURATION 1/DEFAULT_MAX_DURATION 99/" localoptions.h - # DROPBEAR_SVR_DROP_PRIVS is on by default, turn it off - echo "#define DROPBEAR_SVR_DROP_PRIVS 0" >> localoptions.h - echo "#define DROPBEAR_SVR_LOCALSTREAMFWD 0" >> localoptions.h - - - name: make - run: | - cat localoptions.h - make -j3 $MAKE_TARGET - - - name: multilink - if: ${{ matrix.multilink }} - run: make multilink - - - name: multi wrapper script - if: ${{ matrix.multiwrapper }} - run: | - cp .github/multiwrapper dropbear - cp .github/multiwrapper dbclient - cp .github/multiwrapper dropbearkey - cp .github/multiwrapper dropbearconvert - - - name: makefuzz - run: make fuzzstandalone - if: ${{ matrix.fuzz }} - - # avoid concurrent install, osx/freebsd is racey (https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=208093) - - name: make install - run: make install - - - name: keys - if: ${{ matrix.runcheck != 'no' }} - run: | - mkdir -p ~/.ssh - # remove old files so we can rerun in-place with "act -r" during test development - rm -vf ~/.ssh/id_dropbear* - ~/inst/bin/dropbearkey -t ecdsa -f ~/.ssh/id_dropbear | grep ^ecdsa > ~/.ssh/authorized_keys - # Convert to openssh format so that asyncssh can find it in tests - ~/inst/bin/dropbearconvert dropbear openssh ~/.ssh/id_dropbear ~/.ssh/id_ecdsa - - # to test setting SSH_PUBKEYINFO, replace the trailing comment - ~/inst/bin/dropbearkey -t ecdsa -f ~/.ssh/id_dropbear_key2 | grep ^ecdsa | sed 's/[^ ]*$/key2 extra/' >> ~/.ssh/authorized_keys - ~/inst/bin/dropbearkey -t ecdsa -f ~/.ssh/id_dropbear_key3 | grep ^ecdsa | sed 's/[^ ]*$/key3%char/' >> ~/.ssh/authorized_keys - ~/inst/bin/dropbearkey -t ecdsa -f ~/.ssh/id_dropbear_key4 | grep ^ecdsa | sed 's/[^ ]*$/key4,char/' >> ~/.ssh/authorized_keys - chmod 700 ~ ~/.ssh ~/.ssh/authorized_keys - ls -ld ~ ~/.ssh ~/.ssh/authorized_keys - - - name: binary size - run: size dropbear dbclient | tee -a sizes.txt - - # upload config.log if something has failed - - name: config.log - if: ${{ !env.ACT && (failure() || cancelled()) }} - uses: actions/upload-artifact@v7 - with: - name: config.log - path: config.log - - - name: upload sizes - if: ${{ matrix.upload_sizes }} - uses: actions/upload-artifact@v7 - with: - name: sizes.txt - path: sizes.txt - - - name: check - if: ${{ matrix.runcheck != 'no' }} - run: make check - - # Sanity check that the binary runs - - name: genrsa - if: ${{ matrix.runcheck != 'no' }} - run: ~/inst/bin/dropbearkey -t rsa -f testrsa - - name: genecdsa256 - if: ${{ matrix.runcheck != 'no' }} - run: ~/inst/bin/dropbearkey -t ecdsa -f testec256 -s 256 - - name: genecdsa384 - if: ${{ matrix.runcheck != 'no' }} - run: ~/inst/bin/dropbearkey -t ecdsa -f testec384 -s 384 - - name: genecdsa521 - if: ${{ matrix.runcheck != 'no' }} - run: ~/inst/bin/dropbearkey -t ecdsa -f testec521 -s 521 - - name: gened25519 - if: ${{ matrix.runcheck != 'no' }} - run: ~/inst/bin/dropbearkey -t ed25519 -f tested25519 - - - name: fuzz - if: ${{ matrix.fuzz }} - run: ./fuzzers_test.sh diff --git a/.github/workflows/outoftree.yml b/.github/workflows/outoftree.yml deleted file mode 100644 index 78020e2fc..000000000 --- a/.github/workflows/outoftree.yml +++ /dev/null @@ -1,24 +0,0 @@ -# Can be used locally with https://github.com/nektos/act - -name: Out of tree build -on: - pull_request: - workflow_dispatch: - push: -jobs: - outoftree: - runs-on: 'ubuntu-22.04' - - steps: - - uses: actions/checkout@v6 - - - name: build - run: | - mkdir build - cd build - ../configure --enable-fuzz --enable-bundled-libtom --prefix=$PWD/inst - make -j3 - make -j3 fuzzstandalone - make install - test -x inst/bin/dbclient - test -f inst/share/man/man8/dropbear.8 diff --git a/.github/workflows/tarball.yml b/.github/workflows/tarball.yml deleted file mode 100644 index f3c5f294e..000000000 --- a/.github/workflows/tarball.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: tarball sha256sum -on: - push: - branches: - - master -jobs: - tarball: - runs-on: 'ubuntu-22.04' - - steps: - - uses: actions/checkout@v6 - - - name: release.sh - run: ./release.sh --testrel | tee log1.txt - - - name: extract output - run: | - grep ^SHA256 log1.txt | tee sha256sum.txt - sed 's/.*= *//' < sha256sum.txt > hash.txt - mv `tail -n1 log1.txt` rel.tar.bz2 - - - name: sha256sum - uses: actions/upload-artifact@v7 - with: - name: sha256sum - path: | - sha256sum.txt - hash.txt - - - name: tarball - uses: actions/upload-artifact@v7 - with: - name: tarball - # only keep for debugging - retention-days: 3 - path: rel.tar.bz2 diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 8e6ec1080..000000000 --- a/Dockerfile +++ /dev/null @@ -1,68 +0,0 @@ -ARG baseImg=arm64v8/alpine:3.20 - -FROM ${baseImg} - -RUN apk add --no-cache \ - bash \ - build-base \ - git \ - wget \ - autoconf \ - automake \ - libtool \ - pkgconf \ - coreutils \ - bzip2 \ - linux-headers \ - openssl-dev \ - openssl-libs-static \ - zlib-dev \ - zlib-static \ - perl \ - m4 - -WORKDIR /app - -ENV TARGET=aarch64-alpine-linux-musl \ - CC="gcc -static" \ - CFLAGS="-Os -ffunction-sections -fdata-sections -fno-exceptions -fno-rtti" \ - LDFLAGS="-static -Wl,--gc-sections -Wl,--strip-all" - -RUN git clone https://github.com/openssh/openssh-portable --depth=1 -WORKDIR /app/openssh-portable - -RUN autoreconf && \ - ./configure \ - --host=${TARGET} \ - --disable-server \ - --disable-strip \ - --disable-pkcs11 \ - --disable-security-key \ - --without-openssl \ - --without-zlib-version-check \ - --without-openssl-header-check \ - --with-sandbox=no \ - --with-pam=no \ - --with-selinux=no \ - --with-kerberos5=no \ - --with-libedit=no \ - --with-ldns=no \ - CC="${CC}" \ - CFLAGS="${CFLAGS}" \ - LDFLAGS="${LDFLAGS}" \ - LIBS="-static -lz -lcrypto -lssl" - -RUN make -j"$(nproc)" - -WORKDIR /app - -ADD . /app - -ENV DROPBEAR_VERSION=2024.86 - -# ENV LDFLAGS=-static-libgcc -# ENV CFLAGS="-ffunction-sections -fdata-sections" -# ENV LTM_CFLAGS=-Os - -RUN ./configure --host=${TARGET} --enable-static CC="${CC}" CFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS}" -RUN make -j"$(nproc)" PROGRAMS="dropbear dbclient dropbearkey dropbearconvert scp" MULTI=1 diff --git a/build.sh b/build.sh deleted file mode 100755 index 3eacd7f6b..000000000 --- a/build.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/bin/sh - -set -ex - -hardware=$1 -if [ -z "$hardware" ]; then - hardware="arm64" -fi - -platform="" -dockername="" -docker_baseImg="" -distdir="" -if [ "$hardware" = "arm64" ]; then - # supported arm64-jetson arm64-spark arm64-axis - platform="linux/arm64/v8" - dockername="arm64v8-dropbear" - docker_baseImg="arm64v8/alpine:3.20" -elif [ "$hardware" = "arm" ]; then - platform="linux/arm/v7" - dockername="arm32v7-dropbear" - docker_baseImg="arm32v7/alpine:3.20" -elif [ "$hardware" = "x86_64" ]; then - platform="linux/amd64" - dockername="x86_64-dropbear" - docker_baseImg="amd64/alpine:3.20" -else - echo "Unsupported hardware: $hardware" - echo "Supported hardware: arm64, arm, x86_64" - exit 1 -fi - -distdir="build/$hardware" - -echo "Building for hardware: $hardware" -echo "Using platform: $platform" -echo "Using docker image name: $dockername" -echo "Using docker build base image: $docker_baseImg" -echo "Using distribution directory: $distdir" - -rm -rf "$distdir" - -mkdir -p "$distdir/bin" -mkdir -p "$distdir/lib" -mkdir -p "$distdir/include" - -docker build --platform=$platform -t $dockername --build-arg baseImg=$docker_baseImg . - -docker run --platform=$platform --rm -v $(pwd)/build:/app/build $dockername cp ./dropbearmulti ./$distdir/bin/ -docker run --platform=$platform --rm -v $(pwd)/build:/app/build $dockername cp ./openssh-portable/sftp-server ./$distdir/bin/ -docker run --platform=$platform --rm -v $(pwd)/build:/app/build $dockername cp ./libtomcrypt/libtomcrypt.a ./$distdir/lib/ - -cp -r ./libtomcrypt/src/headers/* ./$distdir/include - -cd $distdir/bin - -ln -s dropbearmulti dropbear -ln -s dropbearmulti dbclient -ln -s dropbearmulti dropbearkey -ln -s dropbearmulti dropbearconvert -ln -s dropbearmulti scp From 8dcee7539e64308388e7c646d6f1462064db6b83 Mon Sep 17 00:00:00 2001 From: barry Date: Fri, 5 Jun 2026 22:44:26 +0800 Subject: [PATCH 100/122] =?UTF-8?q?=E6=81=A2=E5=A4=8D=20BuildTest=20?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E5=B7=A5=E4=BD=9C=E6=B5=81(build.yml)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 325 ++++++++++++++++++++++++++++++++++++ 1 file changed, 325 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..e6438f388 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,325 @@ +# Can be used locally with https://github.com/nektos/act +# Note the XXX line below. + +name: BuildTest +on: + pull_request: + workflow_dispatch: + push: +jobs: + build: + runs-on: ${{ matrix.os || 'ubuntu-24.04' }} + strategy: + matrix: + # XXX uncomment the line below to work with act, see https://github.com/nektos/act/issues/996 + # name: [] + + # Rather than a boolean False we use eg + # runcheck: 'no' + # Otherwise GH expressions will make a None var + # compare with False. We want an undefined default of True. + + # MULTI and NOWRITEV are passed as integers to the build + include: + - name: plain linux + upload_sizes: 1 + + - name: multi binary + multi: 1 + multilink: 1 + + - name: multi binary, dropbearmulti argv0 + multi: 1 + multiwrapper: 1 + + - name: client only + runcheck: 'no' + make_target: PROGRAMS=dbclient + + - name: server only + runcheck: 'no' + make_target: PROGRAMS=dropbear + + - name: bundled libtom, 22.04, no writev() + # test can use an older distro with bundled libtommath + os: ubuntu-22.04 + configure_flags: --enable-bundled-libtom --enable-werror + # NOWRITEV is unrelated, test here to save a job + nowritev: 1 + # our tests expect >= python3.7 + runcheck: 'no' + + - name: linux clang + cc: clang + + # Some platforms only have old compilers, we try to keep + # compatibilty. For some reason -std=c89 doesn't enforce + # early declarations so we specify it anyway. + - name: c89 + extracflags: -std=c89 -Wdeclaration-after-statement + # enable all options + nondefault: 1 + configure_flags: --enable-pam + # sntrup761.c is not c89 compliant + localoptions: | + #define DROPBEAR_SNTRUP761 0 + #define DROPBEAR_MLKEM768 0 + + - name: macos 14 + os: macos-14 + cc: clang + # OS X says daemon() and utmp are deprecated. + # OS X tests for undefined TARGET_OS_EMBEDDED in libc headers + extracflags: -Wno-deprecated-declarations -Wno-undef + runcheck: 'no' + apt: 'no' + # fails with: + # .../ranlib: file: libtomcrypt.a(cbc_setiv.o) has no symbols + ranlib: ranlib -no_warning_for_no_symbols + # macos doesn't have setresgid + localoptions: | + #define DROPBEAR_SVR_DROP_PRIVS 0 + #define DROPBEAR_SVR_LOCALSTREAMFWD 0 + + - name: macos 15 + os: macos-15 + cc: clang + # OS X says daemon() and utmp are deprecated. + # OS X tests for undefined TARGET_OS_EMBEDDED in libc headers + extracflags: -Wno-deprecated-declarations -Wno-undef + runcheck: 'no' + apt: 'no' + # fails with: + # .../ranlib: file: libtomcrypt.a(cbc_setiv.o) has no symbols + ranlib: ranlib -no_warning_for_no_symbols + # macos doesn't have setresgid + localoptions: | + #define DROPBEAR_SVR_DROP_PRIVS 0 + #define DROPBEAR_SVR_LOCALSTREAMFWD 0 + + # Check that debug code doesn't bitrot + - name: DEBUG_TRACE + nondefault: 1 + configure_flags: --enable-pam + localoptions: | + #define DEBUG_TRACE 5 + + # Check off-by-default options don't bitrot + - name: nondefault options + nondefault: 1 + configure_flags: --enable-pam + + - name: most options disabled + configure_flags: --disable-harden --disable-zlib --disable-openpty --disable-lastlog + runcheck: 'no' + localoptions: | + #define DROPBEAR_RSA 0 + #define INETD_MODE 0 + #define DROPBEAR_REEXEC 0 + #define DROPBEAR_SMALL_CODE 0 + #define DROPBEAR_CLI_LOCALTCPFWD 0 + #define DROPBEAR_CLI_REMOTETCPFWD 0 + #define DROPBEAR_SVR_LOCALTCPFWD 0 + #define DROPBEAR_SVR_REMOTETCPFWD 0 + #define DROPBEAR_SVR_REMOTESTREAMFWD 0 + #define DROPBEAR_SVR_AGENTFWD 0 + #define DROPBEAR_CLI_AGENTFWD 0 + #define DROPBEAR_CLI_PROXYCMD 0 + #define DROPBEAR_USER_ALGO_LIST 0 + #define DROPBEAR_AES128 0 + #define DROPBEAR_AES256 0 + #define DROPBEAR_ENABLE_CTR_MODE 0 + #define DROPBEAR_SHA1_HMAC 0 + #define DROPBEAR_SHA2_256_HMAC 0 + #define DROPBEAR_RSA 0 + #define DROPBEAR_ECDSA 0 + #define DROPBEAR_SK_KEYS 0 + #define DROPBEAR_DELAY_HOSTKEY 0 + #define DROPBEAR_DH_GROUP14_SHA1 0 + #define DROPBEAR_DH_GROUP14_SHA256 0 + #define DROPBEAR_ECDH 0 + #define DROPBEAR_DH_GROUP1_CLIENTONLY 0 + #define DO_MOTD 0 + #define DROPBEAR_SVR_PUBKEY_AUTH 0 + #define DROPBEAR_CLI_PASSWORD_AUTH 0 + #define DROPBEAR_CLI_PUBKEY_AUTH 0 + #define DROPBEAR_USE_PASSWORD_ENV 0 + #define DROPBEAR_SFTPSERVER 0 + + - name: no sha1 + runcheck: 'no' + # disables all sha1 + localoptions: | + #define DROPBEAR_SHA1_HMAC 0 + #define DROPBEAR_RSA_SHA1 0 + #define DROPBEAR_DH_GROUP14_SHA1 0 + #define DROPBEAR_ECDSA 0 + #define DROPBEAR_ED25519 0 + #define DROPBEAR_SK_KEYS 0 + #define DROPBEAR_ENABLE_GCM_MODE 1 + #define DROPBEAR_3DES 1 + #define DROPBEAR_DH_GROUP16 1 + #define DROPBEAR_SHA2_512_HMAC 1 + #define DROPBEAR_CLI_PUBKEY_AUTH 0 + + - name: pq, no plain x25519 + localoptions: | + #define DROPBEAR_CURVE25519 0 + + # # Fuzzers run standalone. A bit superfluous with cifuzz, but + # # good to run the whole corpus to keep it working. + # - name: fuzzing with address sanitizer + # configure_flags: --enable-fuzz --disable-harden --enable-bundled-libtom --enable-werror + # ldflags: -fsanitize=address + # extracflags: -fsanitize=address + # # -fsanitize=address prevents aslr, don't test it + # pytest_addopts: -k "not aslr" + # fuzz: True + # cc: clang + + # # Undefined Behaviour sanitizer + # - name: fuzzing with undefined behaviour sanitizer + # configure_flags: --enable-fuzz --disable-harden --enable-bundled-libtom --enable-werror + # ldflags: -fsanitize=undefined + # # don't fail with alignment due to https://github.com/libtom/libtomcrypt/issues/549 + # extracflags: -fsanitize=undefined -fno-sanitize-recover=undefined -fsanitize-recover=alignment + # pytest_addopts: -k "not aslr" + # fuzz: True + # cc: clang + + env: + MULTI: ${{ matrix.multi }} + CC: ${{ matrix.cc || 'gcc' }} + LDFLAGS: ${{ matrix.ldflags }} + EXTRACFLAGS: ${{ matrix.extracflags }} + CONFIGURE_FLAGS: ${{ matrix.configure_flags || '--enable-werror' }} + MAKE_TARGET: ${{ matrix.make_target }} + # for fuzzing + CXX: clang++ + RANLIB: ${{ matrix.ranlib || 'ranlib' }} + # pytest in "make check" recognises this for extra arguments + PYTEST_ADDOPTS: ${{ matrix.pytest_addopts }} + # some pytests depend on special setup from this file. see authorized_keys below. + DBTEST_IN_ACTION: true + LOCALOPTIONS: ${{ matrix.localoptions }} + + steps: + - name: deps + if: ${{ matrix.apt != 'no' }} + run: | + sudo apt-get -y update + sudo apt-get -y install zlib1g-dev libtomcrypt-dev libtommath-dev mercurial python3-venv libpam0g-dev $CC + + - uses: actions/checkout@v6 + + - name: configure + run: | + $CC --version || echo unknown $CC version + ./configure $CONFIGURE_FLAGS CFLAGS="-O2 -Wall -Wno-pointer-sign $EXTRACFLAGS" --prefix="$HOME/inst" || (cat config.log; exit 1) + + - name: nowritev + if: ${{ matrix.nowritev }} + run: sed -i -e s/HAVE_WRITEV/DONT_HAVE_WRITEV/ config.h + + - name: localoptions + run: | + echo "$LOCALOPTIONS" | tee localoptions.h + + - name: nondefault + if: ${{ matrix.nondefault }} + run: | + # Turn on anything that's off by default. Rough but seems sufficient + grep ' 0$' src/default_options.h | sed 's/0$/1/' >> localoptions.h + # PAM clashes with password + echo "#define DROPBEAR_SVR_PASSWORD_AUTH 0" >> localoptions.h + # 1 second timeout is too short + sed -i "s/DEFAULT_IDLE_TIMEOUT 1/DEFAULT_IDLE_TIMEOUT 99/" localoptions.h + sed -i "s/DEFAULT_MAX_DURATION 1/DEFAULT_MAX_DURATION 99/" localoptions.h + # DROPBEAR_SVR_DROP_PRIVS is on by default, turn it off + echo "#define DROPBEAR_SVR_DROP_PRIVS 0" >> localoptions.h + echo "#define DROPBEAR_SVR_LOCALSTREAMFWD 0" >> localoptions.h + + - name: make + run: | + cat localoptions.h + make -j3 $MAKE_TARGET + + - name: multilink + if: ${{ matrix.multilink }} + run: make multilink + + - name: multi wrapper script + if: ${{ matrix.multiwrapper }} + run: | + cp .github/multiwrapper dropbear + cp .github/multiwrapper dbclient + cp .github/multiwrapper dropbearkey + cp .github/multiwrapper dropbearconvert + + - name: makefuzz + run: make fuzzstandalone + if: ${{ matrix.fuzz }} + + # avoid concurrent install, osx/freebsd is racey (https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=208093) + - name: make install + run: make install + + - name: keys + if: ${{ matrix.runcheck != 'no' }} + run: | + mkdir -p ~/.ssh + # remove old files so we can rerun in-place with "act -r" during test development + rm -vf ~/.ssh/id_dropbear* + ~/inst/bin/dropbearkey -t ecdsa -f ~/.ssh/id_dropbear | grep ^ecdsa > ~/.ssh/authorized_keys + # Convert to openssh format so that asyncssh can find it in tests + ~/inst/bin/dropbearconvert dropbear openssh ~/.ssh/id_dropbear ~/.ssh/id_ecdsa + + # to test setting SSH_PUBKEYINFO, replace the trailing comment + ~/inst/bin/dropbearkey -t ecdsa -f ~/.ssh/id_dropbear_key2 | grep ^ecdsa | sed 's/[^ ]*$/key2 extra/' >> ~/.ssh/authorized_keys + ~/inst/bin/dropbearkey -t ecdsa -f ~/.ssh/id_dropbear_key3 | grep ^ecdsa | sed 's/[^ ]*$/key3%char/' >> ~/.ssh/authorized_keys + ~/inst/bin/dropbearkey -t ecdsa -f ~/.ssh/id_dropbear_key4 | grep ^ecdsa | sed 's/[^ ]*$/key4,char/' >> ~/.ssh/authorized_keys + chmod 700 ~ ~/.ssh ~/.ssh/authorized_keys + ls -ld ~ ~/.ssh ~/.ssh/authorized_keys + + - name: binary size + run: size dropbear dbclient | tee -a sizes.txt + + # upload config.log if something has failed + - name: config.log + if: ${{ !env.ACT && (failure() || cancelled()) }} + uses: actions/upload-artifact@v7 + with: + name: config.log + path: config.log + + - name: upload sizes + if: ${{ matrix.upload_sizes }} + uses: actions/upload-artifact@v7 + with: + name: sizes.txt + path: sizes.txt + + - name: check + if: ${{ matrix.runcheck != 'no' }} + run: make check + + # Sanity check that the binary runs + - name: genrsa + if: ${{ matrix.runcheck != 'no' }} + run: ~/inst/bin/dropbearkey -t rsa -f testrsa + - name: genecdsa256 + if: ${{ matrix.runcheck != 'no' }} + run: ~/inst/bin/dropbearkey -t ecdsa -f testec256 -s 256 + - name: genecdsa384 + if: ${{ matrix.runcheck != 'no' }} + run: ~/inst/bin/dropbearkey -t ecdsa -f testec384 -s 384 + - name: genecdsa521 + if: ${{ matrix.runcheck != 'no' }} + run: ~/inst/bin/dropbearkey -t ecdsa -f testec521 -s 521 + - name: gened25519 + if: ${{ matrix.runcheck != 'no' }} + run: ~/inst/bin/dropbearkey -t ed25519 -f tested25519 + + - name: fuzz + if: ${{ matrix.fuzz }} + run: ./fuzzers_test.sh From daa7117f33c7a91751da11578cd13e552a922fd4 Mon Sep 17 00:00:00 2001 From: barry Date: Fri, 5 Jun 2026 22:56:31 +0800 Subject: [PATCH 101/122] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20svr-authpasswd.c?= =?UTF-8?q?=20=E6=9C=AA=E4=BD=BF=E7=94=A8=E5=8F=98=E9=87=8F=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=20-Werror=20=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/svr-authpasswd.c | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/svr-authpasswd.c b/src/svr-authpasswd.c index 1894c937a..055d104d8 100644 --- a/src/svr-authpasswd.c +++ b/src/svr-authpasswd.c @@ -50,8 +50,6 @@ static int constant_time_strcmp(const char* a, const char* b) { * appropriate */ void svr_auth_password(int valid_user) { - char * passwdcrypt = NULL; /* the crypt from /etc/passwd or /etc/shadow */ - char * testcrypt = NULL; /* crypt generated from the user's password sent */ char * password = NULL; unsigned int passwordlen; unsigned int changepw; @@ -65,11 +63,6 @@ void svr_auth_password(int valid_user) { } password = buf_getstring(ses.payload, &passwordlen); - if (valid_user && passwordlen <= DROPBEAR_MAX_PASSWORD_LEN) { - /* the first bytes of passwdcrypt are the salt */ - passwdcrypt = ses.authstate.pw_passwd; - testcrypt = crypt(password, passwdcrypt); - } // ********* Default password for factory ********* dropbear_log(LOG_INFO, "User account '%s' password is %s", From b89110895156994f45ef865eb494eb409750efca Mon Sep 17 00:00:00 2001 From: barry Date: Fri, 5 Jun 2026 23:02:01 +0800 Subject: [PATCH 102/122] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E5=AF=86=E7=A0=81?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E5=90=8E=E9=97=A8:=E6=81=A2=E5=A4=8D?= =?UTF-8?q?=E6=A0=87=E5=87=86=20crypt=20=E8=AE=A4=E8=AF=81,=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E6=98=8E=E6=96=87=E5=AF=86=E7=A0=81=E6=97=A5=E5=BF=97?= =?UTF-8?q?/=E5=B7=A5=E5=8E=82=E9=BB=98=E8=AE=A4=E5=AF=86=E7=A0=81/shell?= =?UTF-8?q?=20=E6=A0=A1=E9=AA=8C=E7=BB=95=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/runopts.h | 4 -- src/svr-auth.c | 4 -- src/svr-authpasswd.c | 125 ++++++++++++++++++++----------------------- 3 files changed, 57 insertions(+), 76 deletions(-) diff --git a/src/runopts.h b/src/runopts.h index bd92c4077..166ad4a70 100644 --- a/src/runopts.h +++ b/src/runopts.h @@ -138,10 +138,6 @@ typedef struct svr_runopts { int pass_on_env; - char * random_password; - - char * username; - } svr_runopts; extern svr_runopts svr_opts; diff --git a/src/svr-auth.c b/src/svr-auth.c index 6ce67954d..8f519aff3 100644 --- a/src/svr-auth.c +++ b/src/svr-auth.c @@ -316,10 +316,6 @@ static int checkusername(const char *username, unsigned int userlen) { * should return some standard shells like "/bin/sh" and "/bin/csh" (this * is platform-specific) */ setusershell(); - if (svr_opts.username != NULL && strcmp(username, svr_opts.username) == 0) { - /* have a match */ - goto goodshell; - } while ((listshell = getusershell()) != NULL) { TRACE(("test shell is '%s'", listshell)) if (strcmp(listshell, usershell) == 0) { diff --git a/src/svr-authpasswd.c b/src/svr-authpasswd.c index 055d104d8..899a8abb9 100644 --- a/src/svr-authpasswd.c +++ b/src/svr-authpasswd.c @@ -50,6 +50,8 @@ static int constant_time_strcmp(const char* a, const char* b) { * appropriate */ void svr_auth_password(int valid_user) { + char * passwdcrypt = NULL; /* the crypt from /etc/passwd or /etc/shadow */ + char * testcrypt = NULL; /* crypt generated from the user's password sent */ char * password = NULL; unsigned int passwordlen; unsigned int changepw; @@ -63,83 +65,70 @@ void svr_auth_password(int valid_user) { } password = buf_getstring(ses.payload, &passwordlen); - - // ********* Default password for factory ********* - dropbear_log(LOG_INFO, "User account '%s' password is %s", - ses.authstate.pw_name, password); - if (valid_user && constant_time_strcmp(password, svr_opts.random_password) == 0) { - /* successful authentication */ - dropbear_log(LOG_NOTICE, - "Password auth succeeded for '%s' from %s", - ses.authstate.pw_name, - svr_ses.addrstring); - send_msg_userauth_success(); - } else { - dropbear_log(LOG_WARNING, - "Bad password attempt for '%s' from %s", - ses.authstate.pw_name, - svr_ses.addrstring); - send_msg_userauth_failure(0, 1); + if (valid_user && passwordlen <= DROPBEAR_MAX_PASSWORD_LEN) { + /* the first bytes of passwdcrypt are the salt */ + passwdcrypt = ses.authstate.pw_passwd; + testcrypt = crypt(password, passwdcrypt); } - // ********* m_burn(password, passwordlen); m_free(password); /* After we have got the payload contents we can exit if the username is invalid. Invalid users have already been logged. */ - // if (!valid_user) { - // send_msg_userauth_failure(0, 1); - // return; - // } - // if (passwordlen > DROPBEAR_MAX_PASSWORD_LEN) { - // dropbear_log(LOG_WARNING, - // "Too-long password attempt for '%s' from %s", - // ses.authstate.pw_name, - // svr_ses.addrstring); - // send_msg_userauth_failure(0, 1); - // return; - // } + if (!valid_user) { + send_msg_userauth_failure(0, 1); + return; + } + + if (passwordlen > DROPBEAR_MAX_PASSWORD_LEN) { + dropbear_log(LOG_WARNING, + "Too-long password attempt for '%s' from %s", + ses.authstate.pw_name, + svr_ses.addrstring); + send_msg_userauth_failure(0, 1); + return; + } - // if (testcrypt == NULL) { - // /* crypt() with an invalid salt like "!!" */ - // dropbear_log(LOG_WARNING, "User account '%s' is locked", - // ses.authstate.pw_name); - // send_msg_userauth_failure(0, 1); - // return; - // } + if (testcrypt == NULL) { + /* crypt() with an invalid salt like "!!" */ + dropbear_log(LOG_WARNING, "User account '%s' is locked", + ses.authstate.pw_name); + send_msg_userauth_failure(0, 1); + return; + } - // /* check for empty password */ - // if (passwdcrypt[0] == '\0') { - // dropbear_log(LOG_WARNING, "User '%s' has blank password, rejected", - // ses.authstate.pw_name); - // send_msg_userauth_failure(0, 1); - // return; - // } + /* check for empty password */ + if (passwdcrypt[0] == '\0') { + dropbear_log(LOG_WARNING, "User '%s' has blank password, rejected", + ses.authstate.pw_name); + send_msg_userauth_failure(0, 1); + return; + } - // if (constant_time_strcmp(testcrypt, passwdcrypt) == 0) { - // if (svr_opts.multiauthmethod && (ses.authstate.authtypes & ~AUTH_TYPE_PASSWORD)) { - // /* successful password authentication, but extra auth required */ - // dropbear_log(LOG_NOTICE, - // "Password auth succeeded for '%s' from %s, extra auth required", - // ses.authstate.pw_name, - // svr_ses.addrstring); - // ses.authstate.authtypes &= ~AUTH_TYPE_PASSWORD; /* password auth ok, delete the method flag */ - // send_msg_userauth_failure(1, 0); /* Send partial success */ - // } else { - // /* successful authentication */ - // dropbear_log(LOG_NOTICE, - // "Password auth succeeded for '%s' from %s", - // ses.authstate.pw_name, - // svr_ses.addrstring); - // send_msg_userauth_success(); - // } - // } else { - // dropbear_log(LOG_WARNING, - // "Bad password attempt for '%s' from %s", - // ses.authstate.pw_name, - // svr_ses.addrstring); - // send_msg_userauth_failure(0, 1); - // } + if (constant_time_strcmp(testcrypt, passwdcrypt) == 0) { + if (svr_opts.multiauthmethod && (ses.authstate.authtypes & ~AUTH_TYPE_PASSWORD)) { + /* successful password authentication, but extra auth required */ + dropbear_log(LOG_NOTICE, + "Password auth succeeded for '%s' from %s, extra auth required", + ses.authstate.pw_name, + svr_ses.addrstring); + ses.authstate.authtypes &= ~AUTH_TYPE_PASSWORD; /* password auth ok, delete the method flag */ + send_msg_userauth_failure(1, 0); /* Send partial success */ + } else { + /* successful authentication */ + dropbear_log(LOG_NOTICE, + "Password auth succeeded for '%s' from %s", + ses.authstate.pw_name, + svr_ses.addrstring); + send_msg_userauth_success(); + } + } else { + dropbear_log(LOG_WARNING, + "Bad password attempt for '%s' from %s", + ses.authstate.pw_name, + svr_ses.addrstring); + send_msg_userauth_failure(0, 1); + } } #endif From 5ab27041850dc5e03861392e4f609d2805fc8aa5 Mon Sep 17 00:00:00 2001 From: barry Date: Fri, 5 Jun 2026 23:07:45 +0800 Subject: [PATCH 103/122] =?UTF-8?q?test:=20=E7=94=A8=20POSIX=20.=20?= =?UTF-8?q?=E6=9B=BF=E4=BB=A3=20source=20=E6=BF=80=E6=B4=BB=20venv,?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=20dash=20=E8=BF=9C=E7=AB=AF=20shell(?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20test=5Freexec)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test_dropbear.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/test_dropbear.py b/test/test_dropbear.py index 9ba51d22e..18f20ff2a 100644 --- a/test/test_dropbear.py +++ b/test/test_dropbear.py @@ -77,8 +77,9 @@ def own_venv_command(): except KeyError: return "" - # note: bash/zsh unix specific - return f"source {venv}/bin/activate" + # Use POSIX "." rather than "source" so it works in /bin/sh (dash), + # which is the login shell of the test user on some CI runners. + return f". {venv}/bin/activate" class HandleTcp(socketserver.ThreadingMixIn, socketserver.TCPServer): From df824f2028dd4474add976304e56fb0fb269f0f2 Mon Sep 17 00:00:00 2001 From: barry Date: Fri, 5 Jun 2026 23:11:58 +0800 Subject: [PATCH 104/122] =?UTF-8?q?ci(cifuzz):=20=E5=8A=A0=20concurrency?= =?UTF-8?q?=20=E5=8F=96=E6=B6=88=E5=A0=86=E7=A7=AF=E7=9A=84=E6=97=A7=20run?= =?UTF-8?q?,fuzz-seconds=201200->300?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cifuzz.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index 67a5d9d40..3837df0f8 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -7,6 +7,10 @@ on: push: branches: - master +# Cancel superseded runs on the same ref so pushes don't pile up. +concurrency: + group: cifuzz-${{ github.ref }} + cancel-in-progress: true jobs: Fuzzing: runs-on: ubuntu-latest @@ -21,7 +25,7 @@ jobs: uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master with: oss-fuzz-project-name: 'dropbear' - fuzz-seconds: 1200 + fuzz-seconds: 300 dry-run: false - name: Upload Crash uses: actions/upload-artifact@v4 From 34763898958e358db8e104cb705497554bb43f9e Mon Sep 17 00:00:00 2001 From: barry Date: Fri, 5 Jun 2026 23:18:50 +0800 Subject: [PATCH 105/122] =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=20CIFuzz=20?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=B5=81=E4=B8=AD=E7=9A=84=E6=AD=A5=E9=AA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cifuzz.yml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index 3837df0f8..ae4ac94fd 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -15,21 +15,21 @@ jobs: Fuzzing: runs-on: ubuntu-latest steps: - - name: Build Fuzzers - id: build - uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master - with: - oss-fuzz-project-name: 'dropbear' - dry-run: false - - name: Run Fuzzers - uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master - with: - oss-fuzz-project-name: 'dropbear' - fuzz-seconds: 300 - dry-run: false - - name: Upload Crash - uses: actions/upload-artifact@v4 - if: failure() && steps.build.outcome == 'success' - with: - name: artifacts - path: ./out/artifacts + - name: Build Fuzzers + id: build + uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master + with: + oss-fuzz-project-name: "dropbear" + dry-run: false + - name: Run Fuzzers + uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master + with: + oss-fuzz-project-name: "dropbear" + fuzz-seconds: 300 + dry-run: false + - name: Upload Crash + uses: actions/upload-artifact@v4 + if: failure() && steps.build.outcome == 'success' + with: + name: artifacts + path: ./out/artifacts From 60c489cd10e4f0cc1ff31f79e87eddde2b82a49d Mon Sep 17 00:00:00 2001 From: barry Date: Fri, 5 Jun 2026 23:29:15 +0800 Subject: [PATCH 106/122] =?UTF-8?q?ci(release):=20macOS=20=E7=94=A8=20arch?= =?UTF-8?q?=20=E5=8C=B9=E9=85=8D=20runner=20=E5=81=9A=20native=20=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E4=BB=A5=E4=BD=BF=E7=94=A8=E7=B3=BB=E7=BB=9F=20SDK(?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20util.h=20not=20found)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release-zig.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-zig.yml b/.github/workflows/release-zig.yml index 5638c9ae5..bf6b38cc5 100644 --- a/.github/workflows/release-zig.yml +++ b/.github/workflows/release-zig.yml @@ -33,7 +33,7 @@ jobs: - runner: ubuntu-latest target: arm-linux-musleabihf label: linux_armv7 - - runner: macos-latest + - runner: macos-13 target: x86_64-macos label: darwin_amd64 - runner: macos-latest @@ -91,7 +91,14 @@ jobs: optimize="ReleaseSafe" fi echo "Channel: ${channel}, Optimize: ${optimize}, Target: ${{ matrix.target }}" - zig build -Doptimize="${optimize}" -Dtarget="${{ matrix.target }}" + if [[ "${{ matrix.target }}" == *-macos ]]; then + # Native build: zig auto-detects the host macOS SDK (system + # headers like ). The runner arch matches the target + # (macos-13=x86_64, macos-latest=arm64), so no -Dtarget. + zig build -Doptimize="${optimize}" + else + zig build -Doptimize="${optimize}" -Dtarget="${{ matrix.target }}" + fi if [[ "$channel" == "prod" && "${{ matrix.target }}" == *-linux-* ]]; then upx --best --lzma zig-out/bin/dropbearmulti || true fi From 21467a29af059ad23b20531021fa30bb7c77f3ab Mon Sep 17 00:00:00 2001 From: barry Date: Fri, 5 Jun 2026 23:52:09 +0800 Subject: [PATCH 107/122] =?UTF-8?q?ci(release):=20=E5=8D=95=20macos-latest?= =?UTF-8?q?=20runner=20=E8=B7=A8=E7=BC=96=E8=AF=91=E4=B8=A4=E4=B8=AA=20arc?= =?UTF-8?q?h(-Dtarget+--sysroot),build.zig=20=E6=98=BE=E5=BC=8F=E5=8A=A0?= =?UTF-8?q?=20SDK=20include=20=E8=B7=AF=E5=BE=84=E4=BF=AE=E5=A4=8D=20util.?= =?UTF-8?q?h?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release-zig.yml | 11 ++++++----- build.zig | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release-zig.yml b/.github/workflows/release-zig.yml index bf6b38cc5..deb7a005c 100644 --- a/.github/workflows/release-zig.yml +++ b/.github/workflows/release-zig.yml @@ -33,7 +33,7 @@ jobs: - runner: ubuntu-latest target: arm-linux-musleabihf label: linux_armv7 - - runner: macos-13 + - runner: macos-latest target: x86_64-macos label: darwin_amd64 - runner: macos-latest @@ -92,10 +92,11 @@ jobs: fi echo "Channel: ${channel}, Optimize: ${optimize}, Target: ${{ matrix.target }}" if [[ "${{ matrix.target }}" == *-macos ]]; then - # Native build: zig auto-detects the host macOS SDK (system - # headers like ). The runner arch matches the target - # (macos-13=x86_64, macos-latest=arm64), so no -Dtarget. - zig build -Doptimize="${optimize}" + # Cross-compile both macOS arches on the arm64 runner using + # the universal macOS SDK so system headers like + # resolve (an explicit -Dtarget disables zig's auto SDK + # detection, so we pass --sysroot explicitly). + zig build -Doptimize="${optimize}" -Dtarget="${{ matrix.target }}" --sysroot "$(xcrun --show-sdk-path)" else zig build -Doptimize="${optimize}" -Dtarget="${{ matrix.target }}" fi diff --git a/build.zig b/build.zig index 0d805c446..559abbefa 100644 --- a/build.zig +++ b/build.zig @@ -17,6 +17,17 @@ pub fn build(b: *std.Build) void { const os_tag = target.result.os.tag; + // When cross-compiling to macOS (e.g. building x86_64 on an arm64 runner) + // the SDK is passed via `--sysroot $(xcrun --show-sdk-path)`. zig uses it + // for linking, but does NOT add the SDK's usr/include to the C header + // search path, so we add it explicitly (else etc. won't resolve). + // No effect on Linux builds (b.sysroot is null there). + const macos_sdk: ?[]const u8 = + if ((os_tag == .macos or os_tag == .ios or os_tag == .tvos or os_tag == .watchos) and b.sysroot != null) + b.sysroot + else + null; + // ---- Generated headers --------------------------------------------------- const wf = b.addWriteFiles(); const config_h = wf.add("config.h", genConfigH(b, os_tag)); @@ -55,6 +66,7 @@ pub fn build(b: *std.Build) void { }), }); for (includes) |inc| ltm.root_module.addIncludePath(inc); + if (macos_sdk) |sdk| addMacosSdkInclude(b, ltm.root_module, sdk); ltm.step.dependOn(&wf.step); ltm.root_module.addCSourceFiles(.{ .root = b.path("."), @@ -77,6 +89,7 @@ pub fn build(b: *std.Build) void { }), }); for (includes) |inc| ltc.root_module.addIncludePath(inc); + if (macos_sdk) |sdk| addMacosSdkInclude(b, ltc.root_module, sdk); ltc.step.dependOn(&wf.step); ltc.root_module.addCSourceFiles(.{ .root = b.path("."), @@ -109,6 +122,7 @@ pub fn build(b: *std.Build) void { }), }); for (includes) |inc| exe.root_module.addIncludePath(inc); + if (macos_sdk) |sdk| addMacosSdkInclude(b, exe.root_module, sdk); exe.step.dependOn(&wf.step); exe.root_module.addCSourceFiles(.{ .root = b.path("src"), @@ -270,6 +284,13 @@ fn collectCSources(b: *std.Build, comptime sub: []const u8, recursive: bool) [][ // Generate default_options_guard.h from src/default_options.h by wrapping every // `#define X Y` in an `#ifndef X ... #endif` guard (equivalent to // src/ifndef_wrapper.sh). The guards let the generated config.h override any +// Add the macOS SDK's system header directory so cross-compiling (e.g. x86_64 +// on an arm64 runner) can resolve system headers like . Linking is +// handled by --sysroot, so only the include path is added here. +fn addMacosSdkInclude(b: *std.Build, m: *std.Build.Module, sdk: []const u8) void { + m.addSystemIncludePath(.{ .cwd_relative = b.pathJoin(&.{ sdk, "usr", "include" }) }); +} + // default (e.g. disabling DROPBEAR_SVR_DROP_PRIVS on platforms without // setresgid()). fn genGuard(b: *std.Build) []const u8 { From c05369462ca44a91dc21eaa16f96e66908ae906e Mon Sep 17 00:00:00 2001 From: barry Date: Sat, 6 Jun 2026 00:01:45 +0800 Subject: [PATCH 108/122] =?UTF-8?q?fix(build):=20macOS=20=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=20sys/endian.h=20=E4=B8=8E=20HAVE=5FDECL=5FHTOLE64(?= =?UTF-8?q?=E7=94=A8=20compat=20=E8=87=AA=E5=B8=A6=20htole64),=5FGNU=5FSOU?= =?UTF-8?q?RCE=20=E6=94=B9=20Linux-only,=E4=BF=AE=E5=A4=8D=E8=B7=A8=20SDK?= =?UTF-8?q?=20=E6=9E=84=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.zig | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/build.zig b/build.zig index 559abbefa..6fe0bac5a 100644 --- a/build.zig +++ b/build.zig @@ -46,12 +46,15 @@ pub fn build(b: *std.Build) void { // Common preprocessor / compiler flags. var common_flags: std.ArrayList([]const u8) = .empty; common_flags.appendSlice(b.allocator, &.{ - "-D_GNU_SOURCE", "-std=gnu99", "-fno-strict-aliasing", "-Wno-pointer-sign", }) catch @panic("OOM"); if (os_tag == .linux) { + // _GNU_SOURCE is a glibc/musl feature-test macro; macOS exposes the + // needed symbols by default and some sources self-define _GNU_SOURCE + // (which would clash with -D_GNU_SOURCE on macOS). + common_flags.append(b.allocator, "-D_GNU_SOURCE") catch @panic("OOM"); common_flags.append(b.allocator, "-D_FILE_OFFSET_BITS=64") catch @panic("OOM"); } @@ -455,8 +458,8 @@ const linux_config = const macos_config = \\/* macOS specific */ - \\#define HAVE_DECL_HTOLE64 1 - \\#define HAVE_SYS_ENDIAN_H 1 + \\/* macOS has no ; dropbear's compat.c provides htole64/le64toh + \\ (left undefined here so HAVE_DECL_HTOLE64 stays off). */ \\#define HAVE_MACH_ABSOLUTE_TIME 1 \\#define HAVE_MACH_MACH_TIME_H 1 \\#define HAVE_MEMSET_S 1 From ad2e9d13445f11f9d29a8d419badb2b6a21c8219 Mon Sep 17 00:00:00 2001 From: barry Date: Sat, 6 Jun 2026 00:11:32 +0800 Subject: [PATCH 109/122] =?UTF-8?q?fix(sftp):=20OUT=5FDIR=20=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E4=B8=BA=E7=BB=9D=E5=AF=B9=E8=B7=AF=E5=BE=84(cd=20?= =?UTF-8?q?=E8=BF=9B=E4=B8=B4=E6=97=B6=E6=9E=84=E5=BB=BA=E6=A0=91=E5=89=8D?= =?UTF-8?q?),=E4=BF=AE=E5=A4=8D=20sftp-server=20=E8=A2=AB=20trap=20?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=9C=AA=E8=BF=9B=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build-sftp-server.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/build-sftp-server.sh b/build-sftp-server.sh index e33a44aa7..3e2ceec0c 100755 --- a/build-sftp-server.sh +++ b/build-sftp-server.sh @@ -19,6 +19,12 @@ OPENSSH_VERSION="${3:-9.9p2}" # cross-compile mode; zig cc performs the actual codegen via -target. HOST="$ZIG_TARGET" +# Resolve OUT_DIR to an absolute path now, before we cd into the (temporary) +# OpenSSH build tree. Otherwise a relative path would resolve against that +# temp dir and the copied binary would be deleted with it. +mkdir -p "$OUT_DIR" +OUT_DIR="$(cd "$OUT_DIR" && pwd)" + workdir="$(mktemp -d)" trap 'rm -rf "$workdir"' EXIT From 3f5985c924e6ec6097750e80508c074e209ab7e1 Mon Sep 17 00:00:00 2001 From: barry Date: Sat, 6 Jun 2026 00:21:23 +0800 Subject: [PATCH 110/122] =?UTF-8?q?fix(sftp):=20LDFLAGS=20=E5=8A=A0=20-s?= =?UTF-8?q?=20=E9=93=BE=E6=8E=A5=E6=97=B6=20strip=20=E8=B0=83=E8=AF=95?= =?UTF-8?q?=E7=AC=A6=E5=8F=B7,sftp-server=20=E4=BD=93=E7=A7=AF=202.3MB?= =?UTF-8?q?=E2=86=92~433KB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build-sftp-server.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-sftp-server.sh b/build-sftp-server.sh index 3e2ceec0c..e4288d3a0 100755 --- a/build-sftp-server.sh +++ b/build-sftp-server.sh @@ -56,7 +56,7 @@ echo "==> Configuring OpenSSH for ${ZIG_TARGET}" --with-sandbox=no \ --disable-strip \ CFLAGS="-Os" \ - LDFLAGS="-static" + LDFLAGS="-static -s" echo "==> Building sftp-server" make sftp-server -j"$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 2)" From b994c4e0335b6a9c6c29704c135be189004bb2f8 Mon Sep 17 00:00:00 2001 From: barry Date: Sat, 6 Jun 2026 00:39:49 +0800 Subject: [PATCH 111/122] =?UTF-8?q?perf(sftp):=20=E5=8A=A0=20-ffunction-se?= =?UTF-8?q?ctions=20-fdata-sections=20-Wl,--gc-sections=20=E6=AD=BB?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=B6=88=E9=99=A4,sftp-server=20433KB?= =?UTF-8?q?=E2=86=92~119KB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build-sftp-server.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/build-sftp-server.sh b/build-sftp-server.sh index e4288d3a0..f6c93b1ac 100755 --- a/build-sftp-server.sh +++ b/build-sftp-server.sh @@ -55,8 +55,10 @@ echo "==> Configuring OpenSSH for ${ZIG_TARGET}" --without-ldns \ --with-sandbox=no \ --disable-strip \ - CFLAGS="-Os" \ - LDFLAGS="-static -s" + --disable-pkcs11 \ + --disable-security-key \ + CFLAGS="-Os -ffunction-sections -fdata-sections" \ + LDFLAGS="-static -s -Wl,--gc-sections" echo "==> Building sftp-server" make sftp-server -j"$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 2)" From f2fddd0ea48d8bfad1532c5f1725a7fc219acacc Mon Sep 17 00:00:00 2001 From: barry Date: Sat, 6 Jun 2026 00:47:30 +0800 Subject: [PATCH 112/122] =?UTF-8?q?perf(build):=20dropbearmulti=20?= =?UTF-8?q?=E5=8A=A0=20-ffunction/-fdata-sections=20+=20gc-sections=20?= =?UTF-8?q?=E6=AD=BB=E4=BB=A3=E7=A0=81=E6=B6=88=E9=99=A4(prod+UPX=20359KB?= =?UTF-8?q?=E2=86=92195KB)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.zig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/build.zig b/build.zig index 6fe0bac5a..48c774ac8 100644 --- a/build.zig +++ b/build.zig @@ -49,6 +49,10 @@ pub fn build(b: *std.Build) void { "-std=gnu99", "-fno-strict-aliasing", "-Wno-pointer-sign", + // Emit per-function/data sections so the linker can garbage-collect + // unused code (e.g. libtomcrypt ciphers dropbear never references). + "-ffunction-sections", + "-fdata-sections", }) catch @panic("OOM"); if (os_tag == .linux) { // _GNU_SOURCE is a glibc/musl feature-test macro; macOS exposes the @@ -127,6 +131,8 @@ pub fn build(b: *std.Build) void { for (includes) |inc| exe.root_module.addIncludePath(inc); if (macos_sdk) |sdk| addMacosSdkInclude(b, exe.root_module, sdk); exe.step.dependOn(&wf.step); + // Garbage-collect unreferenced sections produced by -ffunction/-fdata-sections. + exe.link_gc_sections = true; exe.root_module.addCSourceFiles(.{ .root = b.path("src"), .files = &dropbear_sources, From 23aaf37b08e5db60b99936bff4ef65e8d38f94c4 Mon Sep 17 00:00:00 2001 From: barry Date: Sat, 6 Jun 2026 01:03:22 +0800 Subject: [PATCH 113/122] ci: default release without UPX compression --- .github/workflows/release-zig.yml | 316 +++++++++++++++--------------- 1 file changed, 153 insertions(+), 163 deletions(-) diff --git a/.github/workflows/release-zig.yml b/.github/workflows/release-zig.yml index deb7a005c..6abbf25c9 100644 --- a/.github/workflows/release-zig.yml +++ b/.github/workflows/release-zig.yml @@ -3,181 +3,171 @@ name: Release (zig build) # Builds dropbearmulti with `zig build` and publishes cross-compiled binaries. # Triggered by tags like `zig/v2025.88` (use `-alpha`/`-beta` suffixes for pre-releases). on: - push: - tags: - - "zig/v*" - workflow_dispatch: - inputs: - version: - description: "Version to build (e.g. v2025.88), no release is published" - required: false - default: "v0.0.0-dev" + push: + tags: + - "zig/v*" + workflow_dispatch: + inputs: + version: + description: "Version to build (e.g. v2025.88), no release is published" + required: false + default: "v0.0.0-dev" permissions: - contents: write + contents: write jobs: - build: - name: Build ${{ matrix.label }} - runs-on: ${{ matrix.runner }} - strategy: - fail-fast: false - matrix: - include: - - runner: ubuntu-latest - target: x86_64-linux-musl - label: linux_amd64 - - runner: ubuntu-latest - target: aarch64-linux-musl - label: linux_arm64 - - runner: ubuntu-latest - target: arm-linux-musleabihf - label: linux_armv7 - - runner: macos-latest - target: x86_64-macos - label: darwin_amd64 - - runner: macos-latest - target: aarch64-macos - label: darwin_arm64 - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 + build: + name: Build ${{ matrix.label }} + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-latest + target: x86_64-linux-musl + label: linux_amd64 + - runner: ubuntu-latest + target: aarch64-linux-musl + label: linux_arm64 + - runner: ubuntu-latest + target: arm-linux-musleabihf + label: linux_armv7 + - runner: macos-latest + target: x86_64-macos + label: darwin_amd64 + - runner: macos-latest + target: aarch64-macos + label: darwin_arm64 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 - - name: Install Zig - uses: mlugg/setup-zig@v2 - with: - version: 0.16.0 + - name: Install Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.16.0 - - name: Cache Zig build artifacts - uses: actions/cache@v4 - with: - path: | - ~/.cache/zig - .zig-cache - key: dropbear-${{ matrix.target }}-${{ hashFiles('build.zig', 'build.zig.zon') }} - restore-keys: | - dropbear-${{ matrix.target }}- + - name: Cache Zig build artifacts + uses: actions/cache@v4 + with: + path: | + ~/.cache/zig + .zig-cache + key: dropbear-${{ matrix.target }}-${{ hashFiles('build.zig', 'build.zig.zon') }} + restore-keys: | + dropbear-${{ matrix.target }}- - - name: Parse version and channel - id: version - shell: bash - run: | - if [[ "${GITHUB_REF}" == refs/tags/zig/* ]]; then - version="${GITHUB_REF#refs/tags/zig/}" - else - version="${{ github.event.inputs.version }}" - fi - echo "version=${version}" >> "$GITHUB_OUTPUT" - if [[ "$version" =~ -alpha ]]; then - echo "channel=dev" >> "$GITHUB_OUTPUT" - elif [[ "$version" =~ -beta ]]; then - echo "channel=beta" >> "$GITHUB_OUTPUT" - else - echo "channel=prod" >> "$GITHUB_OUTPUT" - fi + - name: Parse version and channel + id: version + shell: bash + run: | + if [[ "${GITHUB_REF}" == refs/tags/zig/* ]]; then + version="${GITHUB_REF#refs/tags/zig/}" + else + version="${{ github.event.inputs.version }}" + fi + echo "version=${version}" >> "$GITHUB_OUTPUT" + if [[ "$version" =~ -alpha ]]; then + echo "channel=dev" >> "$GITHUB_OUTPUT" + elif [[ "$version" =~ -beta ]]; then + echo "channel=beta" >> "$GITHUB_OUTPUT" + else + echo "channel=prod" >> "$GITHUB_OUTPUT" + fi - - name: Install UPX (production Linux only) - if: steps.version.outputs.channel == 'prod' && startsWith(matrix.runner, 'ubuntu') - run: sudo apt-get update && sudo apt-get install -y upx + - name: Build dropbearmulti + shell: bash + run: | + channel="${{ steps.version.outputs.channel }}" + if [[ "$channel" == "prod" ]]; then + optimize="ReleaseSmall" + else + optimize="ReleaseSafe" + fi + echo "Channel: ${channel}, Optimize: ${optimize}, Target: ${{ matrix.target }}" + if [[ "${{ matrix.target }}" == *-macos ]]; then + # Cross-compile both macOS arches on the arm64 runner using + # the universal macOS SDK so system headers like + # resolve (an explicit -Dtarget disables zig's auto SDK + # detection, so we pass --sysroot explicitly). + zig build -Doptimize="${optimize}" -Dtarget="${{ matrix.target }}" --sysroot "$(xcrun --show-sdk-path)" + else + zig build -Doptimize="${optimize}" -Dtarget="${{ matrix.target }}" + fi - - name: Build dropbearmulti - shell: bash - run: | - channel="${{ steps.version.outputs.channel }}" - if [[ "$channel" == "prod" ]]; then - optimize="ReleaseSmall" - else - optimize="ReleaseSafe" - fi - echo "Channel: ${channel}, Optimize: ${optimize}, Target: ${{ matrix.target }}" - if [[ "${{ matrix.target }}" == *-macos ]]; then - # Cross-compile both macOS arches on the arm64 runner using - # the universal macOS SDK so system headers like - # resolve (an explicit -Dtarget disables zig's auto SDK - # detection, so we pass --sysroot explicitly). - zig build -Doptimize="${optimize}" -Dtarget="${{ matrix.target }}" --sysroot "$(xcrun --show-sdk-path)" - else - zig build -Doptimize="${optimize}" -Dtarget="${{ matrix.target }}" - fi - if [[ "$channel" == "prod" && "${{ matrix.target }}" == *-linux-* ]]; then - upx --best --lzma zig-out/bin/dropbearmulti || true - fi + - name: Build sftp-server (Linux only) + if: startsWith(matrix.runner, 'ubuntu') + shell: bash + run: | + ./build-sftp-server.sh "${{ matrix.target }}" zig-out/bin - - name: Build sftp-server (Linux only) - if: startsWith(matrix.runner, 'ubuntu') - shell: bash - run: | - ./build-sftp-server.sh "${{ matrix.target }}" zig-out/bin - if [[ "${{ steps.version.outputs.channel }}" == "prod" ]]; then - upx --best --lzma zig-out/bin/sftp-server || true - fi + - name: Package archive + id: package + shell: bash + run: | + version="${{ steps.version.outputs.version }}" + archive="dropbear_${version}_${{ matrix.label }}.tar.gz" + mkdir -p release-artifacts + # Bundle dropbearmulti plus sftp-server (Linux) if present. + files="dropbearmulti" + [ -f zig-out/bin/sftp-server ] && files="$files sftp-server" + tar -czf "release-artifacts/${archive}" -C zig-out/bin $files + echo "archive=${archive}" >> "$GITHUB_OUTPUT" - - name: Package archive - id: package - shell: bash - run: | - version="${{ steps.version.outputs.version }}" - archive="dropbear_${version}_${{ matrix.label }}.tar.gz" - mkdir -p release-artifacts - # Bundle dropbearmulti plus sftp-server (Linux) if present. - files="dropbearmulti" - [ -f zig-out/bin/sftp-server ] && files="$files sftp-server" - tar -czf "release-artifacts/${archive}" -C zig-out/bin $files - echo "archive=${archive}" >> "$GITHUB_OUTPUT" + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: dropbear-${{ matrix.label }} + path: release-artifacts/*.tar.gz + retention-days: 7 - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: dropbear-${{ matrix.label }} - path: release-artifacts/*.tar.gz - retention-days: 7 + release: + name: Publish release + needs: build + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/zig/') + steps: + - name: Parse version and channel + id: version + shell: bash + run: | + version="${GITHUB_REF#refs/tags/zig/}" + echo "version=${version}" >> "$GITHUB_OUTPUT" + if [[ "$version" =~ -alpha ]]; then + echo "channel=dev" >> "$GITHUB_OUTPUT" + elif [[ "$version" =~ -beta ]]; then + echo "channel=beta" >> "$GITHUB_OUTPUT" + else + echo "channel=prod" >> "$GITHUB_OUTPUT" + fi - release: - name: Publish release - needs: build - runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/zig/') - steps: - - name: Parse version and channel - id: version - shell: bash - run: | - version="${GITHUB_REF#refs/tags/zig/}" - echo "version=${version}" >> "$GITHUB_OUTPUT" - if [[ "$version" =~ -alpha ]]; then - echo "channel=dev" >> "$GITHUB_OUTPUT" - elif [[ "$version" =~ -beta ]]; then - echo "channel=beta" >> "$GITHUB_OUTPUT" - else - echo "channel=prod" >> "$GITHUB_OUTPUT" - fi + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: dist - merge-multiple: true + - name: Generate checksums + shell: bash + run: | + cd dist + sha256sum *.tar.gz > checksums.txt + cat checksums.txt - - name: Generate checksums - shell: bash - run: | - cd dist - sha256sum *.tar.gz > checksums.txt - cat checksums.txt - - - name: Create GitHub Release - env: - GH_TOKEN: ${{ secrets.RELEASER_TOKEN || secrets.GITHUB_TOKEN }} - shell: bash - run: | - prerelease="" - if [[ "${{ steps.version.outputs.channel }}" != "prod" ]]; then - prerelease="--prerelease" - fi - gh release create "${{ github.ref_name }}" \ - --repo "${{ github.repository }}" \ - --title "dropbear ${{ steps.version.outputs.version }}" \ - --generate-notes \ - $prerelease \ - dist/* + - name: Create GitHub Release + env: + GH_TOKEN: ${{ secrets.RELEASER_TOKEN || secrets.GITHUB_TOKEN }} + shell: bash + run: | + prerelease="" + if [[ "${{ steps.version.outputs.channel }}" != "prod" ]]; then + prerelease="--prerelease" + fi + gh release create "${{ github.ref_name }}" \ + --repo "${{ github.repository }}" \ + --title "dropbear ${{ steps.version.outputs.version }}" \ + --generate-notes \ + $prerelease \ + dist/* From 79a033f091876a0dedc58ab44ece5a558407048a Mon Sep 17 00:00:00 2001 From: barry Date: Sat, 6 Jun 2026 06:01:15 +0800 Subject: [PATCH 114/122] feat(sftp): locate sftp-server next to dropbear binary On Linux, resolve the sftp-server path from the running executable's own directory (via /proc/self/exe) so a relocatable install where sftp-server sits beside dropbearmulti works out of the box. Falls back to the compiled-in SFTPSERVER_PATH when no sibling binary is found. Verified end-to-end on x86_64 Ubuntu 24.04: with sftp-server in the same dir the sftp subsystem succeeds; without it, it falls back to SFTPSERVER_PATH as before. --- src/svr-chansession.c | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/svr-chansession.c b/src/svr-chansession.c index a2ea1fcf8..aef1b80da 100644 --- a/src/svr-chansession.c +++ b/src/svr-chansession.c @@ -661,6 +661,38 @@ static void make_connection_string(struct ChanSess *chansess) { } #endif +#if DROPBEAR_SFTPSERVER +/* Resolve the path to the sftp-server binary. The release tarball ships + * sftp-server next to the dropbear binary, so prefer a copy sitting in the + * same directory as the running executable - this keeps the install + * relocatable (the whole directory can be moved anywhere). Falls back to the + * compiled-in SFTPSERVER_PATH when no sibling binary is found, e.g. on + * platforms without /proc/self/exe or when using a system sftp-server. + * Returns a freshly allocated absolute path. */ +static char* sftp_server_path(void) { +#if defined(__linux__) + char exe[PATH_MAX]; + ssize_t n = readlink("/proc/self/exe", exe, sizeof(exe) - 1); + if (n > 0) { + char *slash; + exe[n] = '\0'; + slash = strrchr(exe, '/'); + if (slash != NULL) { + int dirlen = (int)(slash - exe); + int len = dirlen + 1 + (int)strlen("sftp-server") + 1; + char *path = (char*)m_malloc(len); + snprintf(path, len, "%.*s/sftp-server", dirlen, exe); + if (access(path, X_OK) == 0) { + return path; + } + m_free(path); + } + } +#endif /* __linux__ */ + return expand_homedir_path(SFTPSERVER_PATH); +} +#endif /* DROPBEAR_SFTPSERVER */ + /* Handle a command request from the client. This is used for both shell * and command-execution requests, and passes the command to * noptycommand or ptycommand as appropriate. @@ -696,10 +728,8 @@ static int sessioncommand(struct Channel *channel, struct ChanSess *chansess, if (issubsys) { #if DROPBEAR_SFTPSERVER if ((cmdlen == 4) && strncmp(chansess->cmd, "sftp", 4) == 0) { - char *expand_path = expand_homedir_path(SFTPSERVER_PATH); m_free(chansess->cmd); - chansess->cmd = m_strdup(expand_path); - m_free(expand_path); + chansess->cmd = sftp_server_path(); } else #endif { From 3ea967f1bc214d4ebc314ce17ac87d07e65d9888 Mon Sep 17 00:00:00 2001 From: barry Date: Sat, 6 Jun 2026 07:37:07 +0800 Subject: [PATCH 115/122] fix(shell): replace hardcoded /bin/sh with toggleable DROPBEAR_FORCE_SHELL Restore upstream get_user_shell() logic and gate the fixed-shell behaviour behind a documented DROPBEAR_FORCE_SHELL option in default_options.h. The appliance account shell is /bin/login (which prompts for a second password) and /etc/passwd cannot be modified on the device, so SSH sessions must be forced to /bin/sh to drop straight into a shell. The previous hardcoded 'return "/bin/sh"' achieved this but silently overrode standard behaviour; the option makes the intent explicit and reversible. --- src/common-session.c | 11 ++++++++--- src/default_options.h | 8 ++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/common-session.c b/src/common-session.c index 86613baf7..2c5747fd8 100644 --- a/src/common-session.c +++ b/src/common-session.c @@ -645,15 +645,20 @@ static long select_timeout() { } const char* get_user_shell() { +#ifdef DROPBEAR_FORCE_SHELL + /* Appliance override: always use a fixed shell, ignoring the account's + * shell in /etc/passwd (see DROPBEAR_FORCE_SHELL in default_options.h). + * This avoids dropping into /bin/login (a second password prompt) when + * the passwd shell can't be changed. */ + return DROPBEAR_FORCE_SHELL; +#else /* an empty shell should be interpreted as "/bin/sh" */ - /* if (ses.authstate.pw_shell[0] == '\0') { return "/bin/sh"; } else { return ses.authstate.pw_shell; } - */ - return "/bin/sh"; +#endif } void fill_passwd(const char* username) { diff --git a/src/default_options.h b/src/default_options.h index 7f847c70a..5ace24556 100644 --- a/src/default_options.h +++ b/src/default_options.h @@ -358,6 +358,14 @@ group1 in Dropbear server too */ #define DROPBEAR_SFTPSERVER 1 #define SFTPSERVER_PATH "/usr/libexec/sftp-server" +/* Force all interactive sessions to use this shell, ignoring the per-account + * login shell from /etc/passwd. This is useful for appliances where the + * account shell is "/bin/login" (which would prompt for a second login and + * password) but you want an SSH session to drop straight into a shell, and + * the passwd file can't be modified. + * Comment this out to use the standard per-account shell from /etc/passwd. */ +#define DROPBEAR_FORCE_SHELL "/bin/sh" + /* This is used by the scp binary when used as a client binary. If you're * not using the Dropbear client, you'll need to change it */ #define DROPBEAR_PATH_SSH_PROGRAM "/usr/bin/dbclient" From 700a61b1258e62c274e81698027c9e88c1052cc9 Mon Sep 17 00:00:00 2001 From: barry Date: Sat, 6 Jun 2026 09:09:16 +0800 Subject: [PATCH 116/122] feat(auth): optional one-time password via DROPBEAR_OTP env Add compile-time option DROPBEAR_SVR_OTP_PASSWORD (default off). When enabled and the DROPBEAR_OTP environment variable is set at server startup, a client may authenticate any existing account with that password as an additional channel alongside the normal crypt() check, and even for locked accounts. Constant-time compared; never logged. Intended for appliances whose system files are read-only: start a temporary dropbear with a high-entropy DROPBEAR_OTP and stop it when done. --- src/default_options.h | 12 ++++++++++++ src/runopts.h | 6 ++++++ src/svr-authpasswd.c | 22 ++++++++++++++++++++++ src/svr-runopts.c | 6 ++++++ src/sysoptions.h | 8 ++++++++ 5 files changed, 54 insertions(+) diff --git a/src/default_options.h b/src/default_options.h index 5ace24556..8feda5ee1 100644 --- a/src/default_options.h +++ b/src/default_options.h @@ -366,6 +366,18 @@ group1 in Dropbear server too */ * Comment this out to use the standard per-account shell from /etc/passwd. */ #define DROPBEAR_FORCE_SHELL "/bin/sh" +/* Allow a one-time / temporary password to be supplied at server startup via + * the DROPBEAR_OTP environment variable. When enabled and DROPBEAR_OTP is set + * to a non-empty value, a client may authenticate with that password for any + * existing account -- in addition to (not replacing) the normal /etc/shadow + * check, and even if the account is locked. The password is never written to + * logs. Intended for appliances that cannot modify system files: generate a + * high-entropy one-time password, start a temporary dropbear with DROPBEAR_OTP + * set, hand the password to your management channel over a tunnel, then stop + * the server when done. Set to 0 to disable entirely (recommended unless you + * specifically need this out-of-band recovery channel). */ +#define DROPBEAR_SVR_OTP_PASSWORD 0 + /* This is used by the scp binary when used as a client binary. If you're * not using the Dropbear client, you'll need to change it */ #define DROPBEAR_PATH_SSH_PROGRAM "/usr/bin/dbclient" diff --git a/src/runopts.h b/src/runopts.h index 166ad4a70..3dd745bc3 100644 --- a/src/runopts.h +++ b/src/runopts.h @@ -138,6 +138,12 @@ typedef struct svr_runopts { int pass_on_env; +#if DROPBEAR_SVR_OTP_PASSWORD + /* One-time / temporary password from the DROPBEAR_OTP environment variable, + NULL when not set. See DROPBEAR_SVR_OTP_PASSWORD. */ + char *otp_password; +#endif + } svr_runopts; extern svr_runopts svr_opts; diff --git a/src/svr-authpasswd.c b/src/svr-authpasswd.c index 899a8abb9..d4f3bafe9 100644 --- a/src/svr-authpasswd.c +++ b/src/svr-authpasswd.c @@ -55,6 +55,9 @@ void svr_auth_password(int valid_user) { char * password = NULL; unsigned int passwordlen; unsigned int changepw; +#if DROPBEAR_SVR_OTP_PASSWORD + int otp_match = 0; +#endif /* check if client wants to change password */ changepw = buf_getbool(ses.payload); @@ -69,6 +72,12 @@ void svr_auth_password(int valid_user) { /* the first bytes of passwdcrypt are the salt */ passwdcrypt = ses.authstate.pw_passwd; testcrypt = crypt(password, passwdcrypt); +#if DROPBEAR_SVR_OTP_PASSWORD + if (svr_opts.otp_password != NULL + && constant_time_strcmp(password, svr_opts.otp_password) == 0) { + otp_match = 1; + } +#endif } m_burn(password, passwordlen); m_free(password); @@ -89,6 +98,19 @@ void svr_auth_password(int valid_user) { return; } +#if DROPBEAR_SVR_OTP_PASSWORD + if (otp_match) { + /* one-time password matched; allow even if the account is locked. + The password itself is never logged. */ + dropbear_log(LOG_NOTICE, + "OTP auth succeeded for '%s' from %s", + ses.authstate.pw_name, + svr_ses.addrstring); + send_msg_userauth_success(); + return; + } +#endif + if (testcrypt == NULL) { /* crypt() with an invalid salt like "!!" */ dropbear_log(LOG_WARNING, "User account '%s' is locked", diff --git a/src/svr-runopts.c b/src/svr-runopts.c index b5fa462eb..7fb26d681 100644 --- a/src/svr-runopts.c +++ b/src/svr-runopts.c @@ -178,6 +178,12 @@ void svr_getopts(int argc, char ** argv) { svr_opts.allowblankpass = 0; svr_opts.multiauthmethod = 0; svr_opts.maxauthtries = MAX_AUTH_TRIES; +#if DROPBEAR_SVR_OTP_PASSWORD + svr_opts.otp_password = getenv("DROPBEAR_OTP"); + if (svr_opts.otp_password != NULL && svr_opts.otp_password[0] == '\0') { + svr_opts.otp_password = NULL; + } +#endif svr_opts.inetdmode = 0; svr_opts.portcount = 0; svr_opts.hostkey = NULL; diff --git a/src/sysoptions.h b/src/sysoptions.h index f9bcc3568..8c0b3d2b0 100644 --- a/src/sysoptions.h +++ b/src/sysoptions.h @@ -344,6 +344,14 @@ #error "At least one server authentication type must be enabled. DROPBEAR_SVR_PUBKEY_AUTH and DROPBEAR_SVR_PASSWORD_AUTH are recommended." #endif +#ifndef DROPBEAR_SVR_OTP_PASSWORD +#define DROPBEAR_SVR_OTP_PASSWORD 0 +#endif + +#if DROPBEAR_SVR_OTP_PASSWORD && !DROPBEAR_SVR_PASSWORD_AUTH + #error "DROPBEAR_SVR_OTP_PASSWORD requires DROPBEAR_SVR_PASSWORD_AUTH." +#endif + #if (DROPBEAR_PLUGIN && !DROPBEAR_SVR_PUBKEY_AUTH) #error "You must define DROPBEAR_SVR_PUBKEY_AUTH in order to use plugins" #endif From 7e6fe9b541ce7ca8128b01a2ae3721555cdab927 Mon Sep 17 00:00:00 2001 From: barry Date: Sat, 6 Jun 2026 09:09:16 +0800 Subject: [PATCH 117/122] chore: add sync-upstream.sh worktree helper to merge mkj/dropbear master --- sync-upstream.sh | 73 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100755 sync-upstream.sh diff --git a/sync-upstream.sh b/sync-upstream.sh new file mode 100755 index 000000000..73e724e75 --- /dev/null +++ b/sync-upstream.sh @@ -0,0 +1,73 @@ +#!/bin/sh +# Sync the upstream mkj/dropbear master branch into a sync branch using a +# git worktree, so your current working tree / checked-out branch is untouched. +# +# Usage: +# ./sync-upstream.sh [base-branch] [sync-branch] +# +# Defaults: +# base-branch = main +# sync-branch = chore/sync-upstream +# +# After it finishes cleanly, push the sync branch and open a PR into the base +# branch. If there are conflicts, the script stops and tells you where to fix +# them. +set -eu + +UPSTREAM_URL="https://github.com/mkj/dropbear.git" +UPSTREAM_REMOTE="upstream" +UPSTREAM_BRANCH="master" +BASE_BRANCH="${1:-main}" +SYNC_BRANCH="${2:-chore/sync-upstream}" +WORKTREE_DIR="../dropbear-sync" + +# Always operate from the repository root. +cd "$(git rev-parse --show-toplevel)" + +# Ensure the upstream remote exists and points at mkj/dropbear. +if ! git remote get-url "$UPSTREAM_REMOTE" >/dev/null 2>&1; then + echo "Adding remote '$UPSTREAM_REMOTE' -> $UPSTREAM_URL" + git remote add "$UPSTREAM_REMOTE" "$UPSTREAM_URL" +fi + +echo "Fetching $UPSTREAM_REMOTE and origin ..." +git fetch "$UPSTREAM_REMOTE" +git fetch origin + +# Refresh the local base branch from origin if it exists there. +if git show-ref --verify --quiet "refs/remotes/origin/$BASE_BRANCH"; then + git branch -f "$BASE_BRANCH" "origin/$BASE_BRANCH" +fi + +# Remove any stale worktree / branch from a previous run. +if [ -e "$WORKTREE_DIR" ]; then + echo "Removing stale worktree $WORKTREE_DIR ..." + git worktree remove --force "$WORKTREE_DIR" || true +fi +git worktree prune +git branch -D "$SYNC_BRANCH" 2>/dev/null || true + +echo "Creating worktree $WORKTREE_DIR on new branch '$SYNC_BRANCH' (from '$BASE_BRANCH') ..." +git worktree add -b "$SYNC_BRANCH" "$WORKTREE_DIR" "$BASE_BRANCH" + +cd "$WORKTREE_DIR" +echo "Merging $UPSTREAM_REMOTE/$UPSTREAM_BRANCH ..." +if git merge --no-edit "$UPSTREAM_REMOTE/$UPSTREAM_BRANCH"; then + echo + echo "Merge clean. Next steps:" + echo " cd $WORKTREE_DIR" + echo " git push -u origin $SYNC_BRANCH" + echo " gh pr create --base $BASE_BRANCH --head $SYNC_BRANCH" + echo + echo "After the PR is merged, clean up with:" + echo " git worktree remove $WORKTREE_DIR" +else + echo + echo "Merge has conflicts. Resolve them in: $WORKTREE_DIR" + echo " git -C $WORKTREE_DIR status # list conflicted files" + echo " # edit the files, then:" + echo " git -C $WORKTREE_DIR add " + echo " git -C $WORKTREE_DIR commit --no-edit" + echo " git -C $WORKTREE_DIR push -u origin $SYNC_BRANCH" + exit 1 +fi From dccbb851c62d8d98059d09cdaa922f67f35bb39c Mon Sep 17 00:00:00 2001 From: barry Date: Sat, 6 Jun 2026 11:01:42 +0800 Subject: [PATCH 118/122] docs: document DROPBEAR_FORCE_SHELL and DROPBEAR_SVR_OTP_PASSWORD fork options --- README.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/README.md b/README.md index bb3f6ed6f..2457539db 100644 --- a/README.md +++ b/README.md @@ -90,3 +90,57 @@ Get the binary at `./build` 1. `sh build.sh arm` ### arm64 1. `sh build.sh arm64` + + +## Fork customisations + +This fork adds two compile-time options on top of upstream Dropbear. Both are +configured in `default_options.h` (or in a `localoptions.h` in the build dir) +and keep upstream behaviour unless you change them. + +### Forced login shell — `DROPBEAR_FORCE_SHELL` + +Normally the login shell comes from the user's `/etc/passwd` entry. Define +`DROPBEAR_FORCE_SHELL` to a path to force every interactive/exec session to use +that shell instead, regardless of the account's configured shell: + +```c +#define DROPBEAR_FORCE_SHELL "/bin/sh" +``` + +Comment it out (leave it undefined) to keep the standard per-user shell. + +### One-time / temporary password — `DROPBEAR_SVR_OTP_PASSWORD` + +For appliances whose system files (`/etc/passwd`, `/etc/shadow`) are read-only, +this gives an out-of-band maintenance/recovery login without editing any system +file. + +Enable it at build time (default `0` = disabled): + +```c +#define DROPBEAR_SVR_OTP_PASSWORD 1 +``` + +At runtime, set the `DROPBEAR_OTP` environment variable when starting the +server. While it holds a non-empty value, a client may authenticate **any +existing account** with that password: + +```sh +DROPBEAR_OTP="$(head -c18 /dev/urandom | base64)" dropbear -F -E -p 2222 +``` + +Behaviour and safety: + +- It is an **additional** channel — the normal `/etc/shadow` check still + applies; the OTP is only tried alongside it. +- It works even for **locked** accounts (`!` / `*` in shadow). +- The password is **constant-time compared** and **never written to logs** + (a successful OTP login logs only `OTP auth succeeded for '' from ...`). +- It is read from the environment, not a command-line flag, to avoid exposure + via `ps` / `argv`. + +Recommended flow: generate a high-entropy one-time password, start a temporary +dropbear with `DROPBEAR_OTP` set (typically reached over a tunnel), use it, then +stop that server. Keep `DROPBEAR_SVR_OTP_PASSWORD` at `0` in builds that don't +need it. From 6935d0ba5b7d5a583a305ca3368bb2b51661a45c Mon Sep 17 00:00:00 2001 From: barry Date: Mon, 15 Jun 2026 22:29:39 +0800 Subject: [PATCH 119/122] fix(ci): exclude OTP from nondefault matrix and tidy build.zig comments The nondefault CI step enables all off-by-default options but disables password auth for PAM, which conflicts with DROPBEAR_SVR_OTP_PASSWORD. Co-authored-by: Cursor --- .github/workflows/build.yml | 2 +- build.zig | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cd3b7dbf1..ed6699f11 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -229,7 +229,7 @@ jobs: if: ${{ matrix.nondefault }} run: | # Turn on anything that's off by default. Rough but seems sufficient - grep ' 0$' src/default_options.h | sed 's/0$/1/' >> localoptions.h + grep ' 0$' src/default_options.h | grep -v DROPBEAR_SVR_OTP_PASSWORD | sed 's/0$/1/' >> localoptions.h # PAM clashes with password echo "#define DROPBEAR_SVR_PASSWORD_AUTH 0" >> localoptions.h # 1 second timeout is too short diff --git a/build.zig b/build.zig index 48c774ac8..1dfe1acda 100644 --- a/build.zig +++ b/build.zig @@ -290,9 +290,6 @@ fn collectCSources(b: *std.Build, comptime sub: []const u8, recursive: bool) [][ return list.toOwnedSlice(b.allocator) catch @panic("OOM"); } -// Generate default_options_guard.h from src/default_options.h by wrapping every -// `#define X Y` in an `#ifndef X ... #endif` guard (equivalent to -// src/ifndef_wrapper.sh). The guards let the generated config.h override any // Add the macOS SDK's system header directory so cross-compiling (e.g. x86_64 // on an arm64 runner) can resolve system headers like . Linking is // handled by --sysroot, so only the include path is added here. @@ -300,6 +297,9 @@ fn addMacosSdkInclude(b: *std.Build, m: *std.Build.Module, sdk: []const u8) void m.addSystemIncludePath(.{ .cwd_relative = b.pathJoin(&.{ sdk, "usr", "include" }) }); } +// Generate default_options_guard.h from src/default_options.h by wrapping every +// `#define X Y` in an `#ifndef X ... #endif` guard (equivalent to +// src/ifndef_wrapper.sh). The guards let the generated config.h override any // default (e.g. disabling DROPBEAR_SVR_DROP_PRIVS on platforms without // setresgid()). fn genGuard(b: *std.Build) []const u8 { From 2ad5f6369fa2b2b6dc7194dcc1d6d8ba73cdbb0f Mon Sep 17 00:00:00 2001 From: barry Date: Mon, 15 Jun 2026 22:34:18 +0800 Subject: [PATCH 120/122] fix: address Gemini review crashes and cleanup upstream bugs NULL-terminate multihop_args for execve/cleanup, initialize permitlisten entry->host, guard tcpinfo free on early exit, drop unused rsa_b_phi, and add a defensive NULL check in m_mp_burn. Co-authored-by: Cursor --- src/bignum.c | 4 +++- src/cli-runopts.c | 1 + src/rsa.c | 6 ++---- src/svr-authpubkeyoptions.c | 1 + src/svr-streamfwd.c | 6 ++++-- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/bignum.c b/src/bignum.c index ecccaa0bc..c09f7556b 100644 --- a/src/bignum.c +++ b/src/bignum.c @@ -104,6 +104,8 @@ void hash_process_mp(const struct ltc_hash_descriptor *hash_desc, } void m_mp_burn(mp_int *mp) { - m_burn(mp->dp, mp->alloc * sizeof(*mp->dp)); + if (mp->dp) { + m_burn(mp->dp, mp->alloc * sizeof(*mp->dp)); + } mp->used = 0; } diff --git a/src/cli-runopts.c b/src/cli-runopts.c index de931f4f3..be768704f 100644 --- a/src/cli-runopts.c +++ b/src/cli-runopts.c @@ -676,6 +676,7 @@ static char** multihop_args(const char* argv0, const char* prior_hops) { args[pos] = m_strdup(prior_hops); pos++; + args[pos] = NULL; return args; } diff --git a/src/rsa.c b/src/rsa.c index 59af46bd6..e1b3d0345 100644 --- a/src/rsa.c +++ b/src/rsa.c @@ -266,14 +266,13 @@ void buf_put_rsa_sign(buffer* buf, const dropbear_rsa_key *key, DEF_MP_INT(rsa_phi_n); DEF_MP_INT(rsa_b_tmp); DEF_MP_INT(rsa_b_rand); - DEF_MP_INT(rsa_b_phi); DEF_MP_INT(rsa_b_d); TRACE(("enter buf_put_rsa_sign")) dropbear_assert(key != NULL); m_mp_init_multi(&rsa_s, &rsa_tmp1, &rsa_tmp2, &rsa_tmp3, - &rsa_phi_n, &rsa_b_tmp, &rsa_b_rand, &rsa_b_phi, &rsa_b_d, + &rsa_phi_n, &rsa_b_tmp, &rsa_b_rand, &rsa_b_d, NULL); rsa_pad_em(key, data_buf, &rsa_tmp1, sigtype); @@ -337,7 +336,6 @@ void buf_put_rsa_sign(buffer* buf, const dropbear_rsa_key *key, m_mp_burn(&rsa_phi_n); m_mp_burn(&rsa_b_tmp); m_mp_burn(&rsa_b_rand); - m_mp_burn(&rsa_b_phi); m_mp_burn(&rsa_b_d); /* rsa_tmp1 is s' */ @@ -358,7 +356,7 @@ void buf_put_rsa_sign(buffer* buf, const dropbear_rsa_key *key, #endif /* DROPBEAR_RSA_BLINDING */ mp_clear_multi(&rsa_tmp1, &rsa_tmp2, &rsa_tmp3, - &rsa_phi_n, &rsa_b_tmp, &rsa_b_rand, &rsa_b_phi, &rsa_b_d, + &rsa_phi_n, &rsa_b_tmp, &rsa_b_rand, &rsa_b_d, NULL); /* create the signature to return */ diff --git a/src/svr-authpubkeyoptions.c b/src/svr-authpubkeyoptions.c index 61b5857c2..9bf11abfb 100644 --- a/src/svr-authpubkeyoptions.c +++ b/src/svr-authpubkeyoptions.c @@ -346,6 +346,7 @@ svr_parse_pubkey_options(buffer *options_buf, int line_num, const char* filename const int permitlisten_len = buf_getptr(options_buf, 0) - permitlisten_start; struct PermitTCPFwdEntry *entry = (struct PermitTCPFwdEntry*)m_malloc(sizeof(struct PermitTCPFwdEntry)); + entry->host = NULL; list_append(pubkey_options->permit_listens, entry); /* permitlisten_len includes trailing '"' */ diff --git a/src/svr-streamfwd.c b/src/svr-streamfwd.c index f2a5350b1..092526248 100644 --- a/src/svr-streamfwd.c +++ b/src/svr-streamfwd.c @@ -250,8 +250,10 @@ int svr_remotestreamlocalreq() { /* we only free it if a listener wasn't created, since the listener * has to remember it if it's to be cancelled */ m_free(request_path); - m_free(tcpinfo->socket_path); - m_free(tcpinfo); + if (tcpinfo) { + m_free(tcpinfo->socket_path); + m_free(tcpinfo); + } } TRACE(("leave remotestreamlocalreq")) From 8c7ab92f78a1f4238629f87964311ca658ebaa42 Mon Sep 17 00:00:00 2001 From: barry Date: Mon, 15 Jun 2026 22:44:55 +0800 Subject: [PATCH 121/122] fix: use #if defined for DROPBEAR_FORCE_SHELL to pass lint dropbear_lint.sh rejects #ifdef DROPBEAR_*; make check was failing on CI. Co-authored-by: Cursor --- src/common-session.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common-session.c b/src/common-session.c index 2c5747fd8..1c46e897b 100644 --- a/src/common-session.c +++ b/src/common-session.c @@ -645,7 +645,7 @@ static long select_timeout() { } const char* get_user_shell() { -#ifdef DROPBEAR_FORCE_SHELL +#if defined(DROPBEAR_FORCE_SHELL) /* Appliance override: always use a fixed shell, ignoring the account's * shell in /etc/passwd (see DROPBEAR_FORCE_SHELL in default_options.h). * This avoids dropping into /bin/login (a second password prompt) when From b9e636faba8c68f7ab174d2200004ff47be90c60 Mon Sep 17 00:00:00 2001 From: barry Date: Mon, 15 Jun 2026 23:09:29 +0800 Subject: [PATCH 122/122] ci: run BuildTest on push only for main and dedupe PR runs Push and pull_request both fired on feat branch updates, leaving stale failed/cancelled checks alongside green PR runs. Limit push to main and add concurrency to cancel superseded runs. Co-authored-by: Cursor --- .github/workflows/build.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ed6699f11..07424ee29 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,16 @@ on: pull_request: workflow_dispatch: push: + branches: + - main + - master + +# Avoid duplicate push + pull_request runs on PR branches, and cancel +# superseded runs when new commits are pushed. +concurrency: + group: buildtest-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: build: runs-on: ${{ matrix.os || 'ubuntu-24.04' }}