diff options
author | TJ Kolev <tjkolev@gmail.com> | 2023-02-02 15:10:37 -0600 |
---|---|---|
committer | TJ Kolev <tjkolev@gmail.com> | 2023-02-02 15:10:37 -0600 |
commit | fdcadd906c21dcf2c09a965a6046f7bf9de84a69 (patch) | |
tree | f3ca129cb8f9d058a963fafadb707fee26b8a872 /src/common-session.c | |
parent | 9defeb477aebf0eb575885eb1fd4a4330ce52531 (diff) | |
download | dropbear-fdcadd906c21dcf2c09a965a6046f7bf9de84a69.tar.gz |
Dropbear SSH - file structure reorg
Separating source and binaries.
* Dropbear source files (.c, .h) were moved under
a new ./src folder.
* Object binaries get generated into the ./obj folder.
This helps to keep less cluttered project.
tjk :)
Diffstat (limited to 'src/common-session.c')
-rw-r--r-- | src/common-session.c | 705 |
1 files changed, 705 insertions, 0 deletions
diff --git a/src/common-session.c b/src/common-session.c new file mode 100644 index 0000000..6991f57 --- /dev/null +++ b/src/common-session.c @@ -0,0 +1,705 @@ +/* + * Dropbear - a SSH2 server + * + * Copyright (c) Matt Johnston + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * 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. */ + +#include "includes.h" +#include "session.h" +#include "dbutil.h" +#include "packet.h" +#include "algo.h" +#include "buffer.h" +#include "dss.h" +#include "ssh.h" +#include "dbrandom.h" +#include "kex.h" +#include "channel.h" +#include "runopts.h" +#include "netio.h" + +static void checktimeouts(void); +static long select_timeout(void); +static int ident_readln(int fd, char* buf, int count); +static void read_session_identification(void); + +struct sshsession ses; /* GLOBAL */ + +/* called only at the start of a session, set up initial state */ +void common_session_init(int sock_in, int sock_out) { + time_t now; + +#if DEBUG_TRACE + debug_start_net(); +#endif + + TRACE(("enter session_init")) + + ses.sock_in = sock_in; + ses.sock_out = sock_out; + ses.maxfd = MAX(sock_in, sock_out); + + if (sock_in >= 0) { + setnonblocking(sock_in); + } + if (sock_out >= 0) { + setnonblocking(sock_out); + } + + ses.socket_prio = DROPBEAR_PRIO_NORMAL; + /* Sets it to lowdelay */ + update_channel_prio(); + +#if !DROPBEAR_SVR_MULTIUSER + /* A sanity check to prevent an accidental configuration option + leaving multiuser systems exposed */ + errno = 0; + getuid(); + if (errno != ENOSYS) { + dropbear_exit("Non-multiuser Dropbear requires a non-multiuser kernel"); + } +#endif + + now = monotonic_now(); + ses.connect_time = now; + ses.last_packet_time_keepalive_recv = now; + ses.last_packet_time_idle = now; + ses.last_packet_time_any_sent = 0; + ses.last_packet_time_keepalive_sent = 0; + +#if DROPBEAR_FUZZ + if (!fuzz.fuzzing) +#endif + { + if (pipe(ses.signal_pipe) < 0) { + dropbear_exit("Signal pipe failed"); + } + setnonblocking(ses.signal_pipe[0]); + setnonblocking(ses.signal_pipe[1]); + ses.maxfd = MAX(ses.maxfd, ses.signal_pipe[0]); + ses.maxfd = MAX(ses.maxfd, ses.signal_pipe[1]); + } + + ses.writepayload = buf_new(TRANS_MAX_PAYLOAD_LEN); + ses.transseq = 0; + + ses.readbuf = NULL; + ses.payload = NULL; + ses.recvseq = 0; + + initqueue(&ses.writequeue); + + ses.requirenext = SSH_MSG_KEXINIT; + ses.dataallowed = 1; /* we can send data until we actually + send the SSH_MSG_KEXINIT */ + ses.ignorenext = 0; + ses.lastpacket = 0; + ses.reply_queue_head = NULL; + ses.reply_queue_tail = NULL; + + /* set all the algos to none */ + ses.keys = (struct key_context*)m_malloc(sizeof(struct key_context)); + ses.newkeys = NULL; + ses.keys->recv.algo_crypt = &dropbear_nocipher; + ses.keys->trans.algo_crypt = &dropbear_nocipher; + ses.keys->recv.crypt_mode = &dropbear_mode_none; + ses.keys->trans.crypt_mode = &dropbear_mode_none; + + ses.keys->recv.algo_mac = &dropbear_nohash; + ses.keys->trans.algo_mac = &dropbear_nohash; + + ses.keys->algo_kex = NULL; + ses.keys->algo_hostkey = -1; + ses.keys->recv.algo_comp = DROPBEAR_COMP_NONE; + ses.keys->trans.algo_comp = DROPBEAR_COMP_NONE; + +#ifndef DISABLE_ZLIB + ses.keys->recv.zstream = NULL; + ses.keys->trans.zstream = NULL; +#endif + + /* key exchange buffers */ + ses.session_id = NULL; + ses.kexhashbuf = NULL; + ses.transkexinit = NULL; + ses.dh_K = NULL; + ses.remoteident = NULL; + + ses.chantypes = NULL; + + ses.allowprivport = 0; + +#if DROPBEAR_PLUGIN + ses.plugin_session = NULL; +#endif + + TRACE(("leave session_init")) +} + +void session_loop(void(*loophandler)(void)) { + + fd_set readfd, writefd; + struct timeval timeout; + int val; + + /* main loop, select()s for all sockets in use */ + for(;;) { + const int writequeue_has_space = (ses.writequeue_len <= 2*TRANS_MAX_PAYLOAD_LEN); + + timeout.tv_sec = select_timeout(); + timeout.tv_usec = 0; + DROPBEAR_FD_ZERO(&writefd); + DROPBEAR_FD_ZERO(&readfd); + + dropbear_assert(ses.payload == NULL); + + /* We get woken up when signal handlers write to this pipe. + SIGCHLD in svr-chansession is the only one currently. */ +#if DROPBEAR_FUZZ + if (!fuzz.fuzzing) +#endif + { + FD_SET(ses.signal_pipe[0], &readfd); + } + + /* set up for channels which can be read/written */ + setchannelfds(&readfd, &writefd, writequeue_has_space); + + /* Pending connections to test */ + set_connect_fds(&writefd); + + /* We delay reading from the input socket during initial setup until + after we have written out our initial KEXINIT packet (empty writequeue). + This means our initial packet can be in-flight while we're doing a blocking + read for the remote ident. + We also avoid reading from the socket if the writequeue is full, that avoids + replies backing up */ + if (ses.sock_in != -1 + && (ses.remoteident || isempty(&ses.writequeue)) + && writequeue_has_space) { + FD_SET(ses.sock_in, &readfd); + } + + /* Ordering is important, this test must occur after any other function + might have queued packets (such as connection handlers) */ + if (ses.sock_out != -1 && !isempty(&ses.writequeue)) { + FD_SET(ses.sock_out, &writefd); + } + + val = select(ses.maxfd+1, &readfd, &writefd, NULL, &timeout); + + if (ses.exitflag) { + dropbear_exit("Terminated by signal"); + } + + if (val < 0 && errno != EINTR) { + dropbear_exit("Error in select"); + } + + if (val <= 0) { + /* If we were interrupted or the select timed out, we still + * want to iterate over channels etc for reading, to handle + * server processes exiting etc. + * We don't want to read/write FDs. */ + DROPBEAR_FD_ZERO(&writefd); + DROPBEAR_FD_ZERO(&readfd); + } + + /* We'll just empty out the pipe if required. We don't do + any thing with the data, since the pipe's purpose is purely to + wake up the select() above. */ + ses.channel_signal_pending = 0; + if (FD_ISSET(ses.signal_pipe[0], &readfd)) { + char x; + TRACE(("signal pipe set")) + while (read(ses.signal_pipe[0], &x, 1) > 0) {} + ses.channel_signal_pending = 1; + } + + /* check for auth timeout, rekeying required etc */ + checktimeouts(); + + /* process session socket's incoming data */ + if (ses.sock_in != -1) { + if (FD_ISSET(ses.sock_in, &readfd)) { + if (!ses.remoteident) { + /* blocking read of the version string */ + read_session_identification(); + } else { + read_packet(); + } + } + + /* Process the decrypted packet. After this, the read buffer + * will be ready for a new packet */ + if (ses.payload != NULL) { + process_packet(); + } + } + + /* if required, flush out any queued reply packets that + were being held up during a KEX */ + maybe_flush_reply_queue(); + + handle_connect_fds(&writefd); + + /* loop handler prior to channelio, in case the server loophandler closes + channels on process exit */ + loophandler(); + + /* process pipes etc for the channels, ses.dataallowed == 0 + * during rekeying ) */ + channelio(&readfd, &writefd); + + /* process session socket's outgoing data */ + if (ses.sock_out != -1) { + if (!isempty(&ses.writequeue)) { + write_packet(); + } + } + + } /* for(;;) */ + + /* Not reached */ +} + +static void cleanup_buf(buffer **buf) { + if (!*buf) { + return; + } + buf_burn_free(*buf); + *buf = NULL; +} + +/* clean up a session on exit */ +void session_cleanup() { + + TRACE(("enter session_cleanup")) + + /* we can't cleanup if we don't know the session state */ + if (!ses.init_done) { + TRACE(("leave session_cleanup: !ses.init_done")) + return; + } + + /* BEWARE of changing order of functions here. */ + + /* Must be before extra_session_cleanup() */ + chancleanup(); + + if (ses.extra_session_cleanup) { + ses.extra_session_cleanup(); + } + + /* After these are freed most functions will fail */ +#if DROPBEAR_CLEANUP + /* listeners call cleanup functions, this should occur before + other session state is freed. */ + remove_all_listeners(); + + remove_connect_pending(); + + while (!isempty(&ses.writequeue)) { + buf_free(dequeue(&ses.writequeue)); + } + + m_free(ses.newkeys); +#ifndef DISABLE_ZLIB + if (ses.keys->recv.zstream != NULL) { + if (inflateEnd(ses.keys->recv.zstream) == Z_STREAM_ERROR) { + dropbear_exit("Crypto error"); + } + m_free(ses.keys->recv.zstream); + } +#endif + + m_free(ses.remoteident); + m_free(ses.authstate.pw_dir); + m_free(ses.authstate.pw_name); + m_free(ses.authstate.pw_shell); + m_free(ses.authstate.pw_passwd); + m_free(ses.authstate.username); +#endif + + cleanup_buf(&ses.session_id); + cleanup_buf(&ses.hash); + cleanup_buf(&ses.payload); + cleanup_buf(&ses.readbuf); + cleanup_buf(&ses.writepayload); + cleanup_buf(&ses.kexhashbuf); + cleanup_buf(&ses.transkexinit); + if (ses.dh_K) { + mp_clear(ses.dh_K); + } + m_free(ses.dh_K); + + m_burn(ses.keys, sizeof(struct key_context)); + m_free(ses.keys); + + TRACE(("leave session_cleanup")) +} + +void send_session_identification() { + buffer *writebuf = buf_new(strlen(LOCAL_IDENT "\r\n") + 1); + buf_putbytes(writebuf, (const unsigned char *) LOCAL_IDENT "\r\n", strlen(LOCAL_IDENT "\r\n")); + writebuf_enqueue(writebuf); +} + +static void read_session_identification() { + /* max length of 255 chars */ + char linebuf[256]; + int len = 0; + char done = 0; + int i; + + /* Servers may send other lines of data before sending the + * version string, client must be able to process such lines. + * If they send more than 50 lines, something is wrong */ + for (i = IS_DROPBEAR_CLIENT ? 50 : 1; i > 0; i--) { + len = ident_readln(ses.sock_in, linebuf, sizeof(linebuf)); + + if (len < 0 && errno != EINTR) { + /* It failed */ + break; + } + + if (len >= 4 && memcmp(linebuf, "SSH-", 4) == 0) { + /* start of line matches */ + done = 1; + break; + } + } + + if (!done) { + TRACE(("error reading remote ident: %s\n", strerror(errno))) + ses.remoteclosed(); + } else { + /* linebuf is already null terminated */ + ses.remoteident = m_malloc(len); + memcpy(ses.remoteident, linebuf, len); + } + + /* Shall assume that 2.x will be backwards compatible. */ + if (strncmp(ses.remoteident, "SSH-2.", 6) != 0 + && strncmp(ses.remoteident, "SSH-1.99-", 9) != 0) { + dropbear_exit("Incompatible remote version '%s'", ses.remoteident); + } + + DEBUG1(("remoteident: %s", ses.remoteident)) + +} + +/* returns the length including null-terminating zero on success, + * or -1 on failure */ +static int ident_readln(int fd, char* buf, int count) { + + char in; + int pos = 0; + int num = 0; + fd_set fds; + struct timeval timeout; + + TRACE(("enter ident_readln")) + + if (count < 1) { + return -1; + } + + DROPBEAR_FD_ZERO(&fds); + + /* select since it's a non-blocking fd */ + + /* leave space to null-terminate */ + while (pos < count-1) { + + FD_SET(fd, &fds); + + timeout.tv_sec = 1; + timeout.tv_usec = 0; + if (select(fd+1, &fds, NULL, NULL, &timeout) < 0) { + if (errno == EINTR) { + continue; + } + TRACE(("leave ident_readln: select error")) + return -1; + } + + checktimeouts(); + + /* Have to go one byte at a time, since we don't want to read past + * the end, and have to somehow shove bytes back into the normal + * packet reader */ + if (FD_ISSET(fd, &fds)) { + num = read(fd, &in, 1); + /* a "\n" is a newline, "\r" we want to read in and keep going + * so that it won't be read as part of the next line */ + if (num < 0) { + /* error */ + if (errno == EINTR) { + continue; /* not a real error */ + } + TRACE(("leave ident_readln: read error")) + return -1; + } + if (num == 0) { + /* EOF */ + TRACE(("leave ident_readln: EOF")) + return -1; + } + +#if DROPBEAR_FUZZ + fuzz_dump(&in, 1); +#endif + + if (in == '\n') { + /* end of ident string */ + break; + } + /* we don't want to include '\r's */ + if (in != '\r') { + buf[pos] = in; + pos++; + } + } + } + + buf[pos] = '\0'; + TRACE(("leave ident_readln: return %d", pos+1)) + return pos+1; +} + +void ignore_recv_response() { + /* Do nothing */ + TRACE(("Ignored msg_request_response")) +} + +static void send_msg_keepalive() { + time_t old_time_idle = ses.last_packet_time_idle; + struct Channel *chan = get_any_ready_channel(); + + CHECKCLEARTOWRITE(); + + if (chan) { + /* Channel requests are preferable, more implementations + handle them than SSH_MSG_GLOBAL_REQUEST */ + TRACE(("keepalive channel request %d", chan->index)) + start_send_channel_request(chan, DROPBEAR_KEEPALIVE_STRING); + } else { + TRACE(("keepalive global request")) + /* Some peers will reply with SSH_MSG_REQUEST_FAILURE, + some will reply with SSH_MSG_UNIMPLEMENTED, some will exit. */ + buf_putbyte(ses.writepayload, SSH_MSG_GLOBAL_REQUEST); + buf_putstring(ses.writepayload, DROPBEAR_KEEPALIVE_STRING, + strlen(DROPBEAR_KEEPALIVE_STRING)); + } + buf_putbyte(ses.writepayload, 1); /* want_reply */ + encrypt_packet(); + + ses.last_packet_time_keepalive_sent = monotonic_now(); + + /* keepalives shouldn't update idle timeout, reset it back */ + ses.last_packet_time_idle = old_time_idle; +} + +/* Returns the difference in seconds, clamped to LONG_MAX */ +static long elapsed(time_t now, time_t prev) { + time_t del = now - prev; + if (del > LONG_MAX) { + return LONG_MAX; + } + return (long)del; +} + +/* Check all timeouts which are required. Currently these are the time for + * user authentication, and the automatic rekeying. */ +static void checktimeouts() { + + time_t now; + now = monotonic_now(); + + if (IS_DROPBEAR_SERVER && ses.connect_time != 0 + && elapsed(now, ses.connect_time) >= AUTH_TIMEOUT) { + dropbear_close("Timeout before auth"); + } + + /* we can't rekey if we haven't done remote ident exchange yet */ + if (ses.remoteident == NULL) { + return; + } + + if (!ses.kexstate.sentkexinit + && (elapsed(now, ses.kexstate.lastkextime) >= KEX_REKEY_TIMEOUT + || ses.kexstate.datarecv+ses.kexstate.datatrans >= KEX_REKEY_DATA)) { + TRACE(("rekeying after timeout or max data reached")) + send_msg_kexinit(); + } + + if (opts.keepalive_secs > 0 && ses.authstate.authdone) { + /* Avoid sending keepalives prior to auth - those are + not valid pre-auth packet types */ + + /* Send keepalives if we've been idle */ + if (elapsed(now, ses.last_packet_time_any_sent) >= opts.keepalive_secs) { + send_msg_keepalive(); + } + + /* Also send an explicit keepalive message to trigger a response + if the remote end hasn't sent us anything */ + if (elapsed(now, ses.last_packet_time_keepalive_recv) >= opts.keepalive_secs + && elapsed(now, ses.last_packet_time_keepalive_sent) >= opts.keepalive_secs) { + send_msg_keepalive(); + } + + if (elapsed(now, ses.last_packet_time_keepalive_recv) + >= opts.keepalive_secs * DEFAULT_KEEPALIVE_LIMIT) { + dropbear_exit("Keepalive timeout"); + } + } + + if (opts.idle_timeout_secs > 0 + && elapsed(now, ses.last_packet_time_idle) >= opts.idle_timeout_secs) { + dropbear_close("Idle timeout"); + } +} + +static void update_timeout(long limit, time_t now, time_t last_event, long * timeout) { + TRACE2(("update_timeout limit %ld, now %llu, last %llu, timeout %ld", + limit, + (unsigned long long)now, + (unsigned long long)last_event, *timeout)) + if (last_event > 0 && limit > 0) { + *timeout = MIN(*timeout, elapsed(now, last_event) + limit); + TRACE2(("new timeout %ld", *timeout)) + } +} + +static long select_timeout() { + /* determine the minimum timeout that might be required, so + as to avoid waking when unneccessary */ + long timeout = KEX_REKEY_TIMEOUT; + time_t now = monotonic_now(); + + if (!ses.kexstate.sentkexinit) { + update_timeout(KEX_REKEY_TIMEOUT, now, ses.kexstate.lastkextime, &timeout); + } + + if (ses.authstate.authdone != 1 && IS_DROPBEAR_SERVER) { + /* AUTH_TIMEOUT is only relevant before authdone */ + update_timeout(AUTH_TIMEOUT, now, ses.connect_time, &timeout); + } + + if (ses.authstate.authdone) { + update_timeout(opts.keepalive_secs, now, + MAX(ses.last_packet_time_keepalive_recv, ses.last_packet_time_keepalive_sent), + &timeout); + } + + update_timeout(opts.idle_timeout_secs, now, ses.last_packet_time_idle, + &timeout); + + /* clamp negative timeouts to zero - event has already triggered */ + return MAX(timeout, 0); +} + +const char* get_user_shell() { + /* 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; + } +} +void fill_passwd(const char* username) { + struct passwd *pw = NULL; + if (ses.authstate.pw_name) + m_free(ses.authstate.pw_name); + if (ses.authstate.pw_dir) + m_free(ses.authstate.pw_dir); + if (ses.authstate.pw_shell) + m_free(ses.authstate.pw_shell); + if (ses.authstate.pw_passwd) + m_free(ses.authstate.pw_passwd); + + pw = getpwnam(username); + if (!pw) { + return; + } + ses.authstate.pw_uid = pw->pw_uid; + ses.authstate.pw_gid = pw->pw_gid; + ses.authstate.pw_name = m_strdup(pw->pw_name); + ses.authstate.pw_dir = m_strdup(pw->pw_dir); + ses.authstate.pw_shell = m_strdup(pw->pw_shell); + { + char *passwd_crypt = pw->pw_passwd; +#ifdef HAVE_SHADOW_H + /* get the shadow password if possible */ + struct spwd *spasswd = getspnam(ses.authstate.pw_name); + if (spasswd && spasswd->sp_pwdp) { + passwd_crypt = spasswd->sp_pwdp; + } +#endif + if (!passwd_crypt) { + /* android supposedly returns NULL */ + passwd_crypt = "!!"; + } + ses.authstate.pw_passwd = m_strdup(passwd_crypt); + } +} + +/* Called when channels are modified */ +void update_channel_prio() { + enum dropbear_prio new_prio; + int any = 0; + unsigned int i; + + TRACE(("update_channel_prio")) + + if (ses.sock_out < 0) { + TRACE(("leave update_channel_prio: no socket")) + return; + } + + new_prio = DROPBEAR_PRIO_NORMAL; + for (i = 0; i < ses.chansize; i++) { + struct Channel *channel = ses.channels[i]; + if (!channel) { + continue; + } + any = 1; + if (channel->prio == DROPBEAR_PRIO_LOWDELAY) { + new_prio = DROPBEAR_PRIO_LOWDELAY; + break; + } + } + + if (any == 0) { + /* lowdelay during setup */ + TRACE(("update_channel_prio: not any")) + new_prio = DROPBEAR_PRIO_LOWDELAY; + } + + if (new_prio != ses.socket_prio) { + TRACE(("Dropbear priority transitioning %d -> %d", ses.socket_prio, new_prio)) + set_sock_priority(ses.sock_out, new_prio); + ses.socket_prio = new_prio; + } +} + |