From 18af9d4d6f1a5a113d0a5852fe79e3f92c6c99d5 Mon Sep 17 00:00:00 2001 From: Kevin Riggle Date: Sat, 7 Feb 2015 04:37:57 -0500 Subject: [PATCH] out-of-band communication and ssh-agent forwarding Pluggable out-of-band communication mechanism over Mosh transport layer and SSH agent forwarding support on top of the out-of-band mechanism --- configure.ac | 16 + man/mosh.1 | 9 + scripts/mosh | 19 +- src/Makefile.am | 2 +- src/agent/Makefile.am | 7 + src/agent/agent.cc | 490 +++++++++++++++++++++++ src/agent/agent.h | 123 ++++++ src/frontend/Makefile.am | 4 +- src/frontend/mosh-client.cc | 9 +- src/frontend/mosh-server.cc | 54 ++- src/frontend/stmclient.cc | 24 +- src/frontend/stmclient.h | 6 +- src/network/Makefile.am | 2 +- src/network/networktransport.cc | 5 + src/network/networktransport.h | 3 + src/network/outofband.cc | 323 +++++++++++++++ src/network/outofband.h | 134 +++++++ src/network/transportsender.cc | 22 +- src/network/transportsender.h | 8 +- src/protobufs/Makefile.am | 2 +- src/protobufs/agent.proto | 8 + src/protobufs/oob.proto | 11 + src/protobufs/transportinstruction.proto | 2 + src/util/Makefile.am | 2 +- src/util/swrite.cc | 45 +++ src/util/swrite.h | 3 + 26 files changed, 1304 insertions(+), 29 deletions(-) create mode 100644 src/agent/Makefile.am create mode 100644 src/agent/agent.cc create mode 100644 src/agent/agent.h create mode 100644 src/network/outofband.cc create mode 100644 src/network/outofband.h create mode 100644 src/protobufs/agent.proto create mode 100644 src/protobufs/oob.proto diff --git a/configure.ac b/configure.ac index 83d6ceb9e..8d25ca6da 100644 --- a/configure.ac +++ b/configure.ac @@ -164,6 +164,13 @@ AS_IF([test x"$with_utempter" != xno], [AC_MSG_WARN([Unable to find libutempter; utmp entries will not be made.])], [AC_MSG_ERROR([--with-utempter was given but libutempter was not found.])])])]) +# Handle --disable-agent-forwarding +AC_ARG_ENABLE(agent-forwarding, + AS_HELP_STRING([--disable-agent-forwarding], + [disable ssh agent forwarding in compile time]), + , enable_agent_forwarding=yes) + + AC_SEARCH_LIBS([compress], [z], , [AC_MSG_ERROR([Unable to find zlib.])]) AC_SEARCH_LIBS([socket], [socket]) @@ -171,6 +178,7 @@ AC_SEARCH_LIBS([inet_addr], [nsl]) # Checks for header files. AC_CHECK_HEADERS([m4_normalize([ + errno.h fcntl.h langinfo.h limits.h @@ -198,6 +206,8 @@ AC_CHECK_HEADERS([endian.h sys/endian.h]) AC_CHECK_HEADERS([utmpx.h]) AC_CHECK_HEADERS([termio.h]) AC_CHECK_HEADERS([sys/uio.h]) +AC_CHECK_HEADERS([sys/un.h]) +AC_CHECK_HEADERS([sys/types.h]) # Checks for typedefs, structures, and compiler characteristics. AC_HEADER_STDBOOL @@ -366,6 +376,11 @@ AC_CHECK_DECL([IUTF8], [AC_MSG_WARN([No IUTF8 termios mode; character-erase of multibyte character sequence probably does not work properly in canonical mode on this platform.])], [[#include ]]) +if test "$enable_agent_forwarding" = "yes"; then + AC_DEFINE([SUPPORT_AGENT_FORWARDING], [], [ + Define to enable support for SSH agent forwarding]) +fi + # Checks for protobuf PKG_CHECK_MODULES([protobuf], [protobuf]) @@ -378,6 +393,7 @@ AC_CONFIG_FILES([ src/protobufs/Makefile src/statesync/Makefile src/terminal/Makefile + src/agent/Makefile src/util/Makefile scripts/Makefile src/examples/Makefile diff --git a/man/mosh.1 b/man/mosh.1 index 3a4fdec27..a49516f23 100644 --- a/man/mosh.1 +++ b/man/mosh.1 @@ -98,6 +98,11 @@ OpenSSH command to remotely execute mosh-server on remote machine (default: "ssh An alternate ssh port can be specified with, \fIe.g.\fP, \-\-ssh="ssh \-p 2222". +.TP +.B \-\-forward-agent +Enable ssh authentication agent forwarding. If you use this, please be +aware of the security implications. + .TP .B \-\-predict=\fIWHEN\fP Controls use of speculative local echo. WHEN defaults to `adaptive' @@ -112,6 +117,10 @@ confident. This generally means a previous prediction on the same row of the terminal has been confirmed by the server, without any intervening control character keystrokes. +.TP +.B \-A +Synonym for \-\-forward-agent + .TP .B \-a Synonym for \-\-predict=always diff --git a/scripts/mosh b/scripts/mosh index f13de812f..d47ef2a71 100755 --- a/scripts/mosh +++ b/scripts/mosh @@ -53,6 +53,8 @@ my $ssh = 'ssh'; my $term_init = 1; +my $forward_agent = 0; + my $help = undef; my $version = undef; @@ -81,6 +83,8 @@ qq{Usage: $0 [options] [--] [user@]host [command...] (example: "ssh -p 2222") (default: "ssh") +-A --forward-agent enable ssh agent forwarding + --no-init do not send terminal initialization string --help this message @@ -118,6 +122,8 @@ GetOptions( 'client=s' => \$client, '6' => sub { $family = 'inet6' }, 'p=s' => \$port_request, 'ssh=s' => \$ssh, + 'A' => \$forward_agent, + 'forward-agent!' => \$forward_agent, 'init!' => \$term_init, 'help' => \$help, 'version' => \$version, @@ -247,6 +253,10 @@ if ( $pid == 0 ) { # child my @server = ( 'new' ); + if ( $forward_agent ) { + push @server, ( '-A' ); + } + push @server, ( '-c', $colors ); push @server, @bind_arguments; @@ -307,7 +317,14 @@ if ( $pid == 0 ) { # child $ENV{ 'MOSH_KEY' } = $key; $ENV{ 'MOSH_PREDICTION_DISPLAY' } = $predict; $ENV{ 'MOSH_NO_TERM_INIT' } = '1' if !$term_init; - exec {$client} ("$client @cmdline |", $ip, $port); + + my @client_av = (); + if ( $forward_agent ) { + push @client_av, ( '-A' ); + } + push @client_av, ( $ip, $port ); + + exec {$client} ("$client @cmdline |", @client_av); } sub shell_quote { join ' ', map {(my $a = $_) =~ s/'/'\\''/g; "'$a'"} @_ } diff --git a/src/Makefile.am b/src/Makefile.am index 2390f7cd8..332bb2c5c 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1 +1 @@ -SUBDIRS = protobufs util crypto terminal network statesync frontend examples tests +SUBDIRS = protobufs util crypto terminal network statesync agent frontend examples tests diff --git a/src/agent/Makefile.am b/src/agent/Makefile.am new file mode 100644 index 000000000..b6dcef2f6 --- /dev/null +++ b/src/agent/Makefile.am @@ -0,0 +1,7 @@ +AM_CPPFLAGS = -I$(srcdir)/../util -I$(srcdir)/../network -I$(srcdir)/../protobufs $(protobuf_CFLAGS) -I$(srcdir)/../crypto $(TINFO_CFLAGS) +AM_CXXFLAGS = $(WARNING_CXXFLAGS) $(PICKY_CXXFLAGS) $(HARDEN_CFLAGS) $(MISC_CXXFLAGS) + +noinst_LIBRARIES = libmoshagent.a + +libmoshagent_a_SOURCES = agent.h agent.cc + diff --git a/src/agent/agent.cc b/src/agent/agent.cc new file mode 100644 index 000000000..b3ee6a603 --- /dev/null +++ b/src/agent/agent.cc @@ -0,0 +1,490 @@ +/* + Mosh: the mobile shell + Copyright 2012 Keith Winstein + + SSH Agent forwarding for Mosh + Copyright 2013 Timo J. Rinne + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + In addition, as a special exception, the copyright holders give + permission to link the code of portions of this program with the + OpenSSL library under certain conditions as described in each + individual source file, and distribute linked combinations including + the two. + + You must obey the GNU General Public License in all respects for all + of the code used other than OpenSSL. If you modify file(s) with this + exception, you may extend this exception to your version of the + file(s), but you are not obligated to do so. If you do not wish to do + so, delete this exception statement from your version. If you delete + this exception statement from all source files in the program, then + also delete it here. +*/ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#ifdef HAVE_SYS_TYPES_H +#include +#endif +#include + +#ifdef SUPPORT_AGENT_FORWARDING +#ifdef HAVE_SYS_UN_H +#include +#else +#undef SUPPORT_AGENT_FORWARDING +#endif +#endif + +#include "prng.h" +#include "network.h" +#include "swrite.h" +#include "select.h" +#include "outofband.h" +#include "agent.h" +#include "agent.pb.h" +#include "fatal_assert.h" + +using namespace Agent; +using std::string; +using std::map; +using Network::OutOfBand; +using Network::OutOfBandCommunicator; + +ProxyAgent::ProxyAgent( bool is_server, bool dummy ) + : comm( NULL ), + oob_ctl_ptr( NULL ), + server( is_server ), + ok( false ), + l_sock( -1 ), + l_dir( "" ), + l_path( "" ), + cnt( 0 ), + agent_sessions() +{ +#ifdef SUPPORT_AGENT_FORWARDING + if ( dummy ) { + return; + } + if (server) { + PRNG prng; + string dir("/tmp/ma-"); + string voc = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + int i; + for ( i = 0; i < 10; i++ ) { + dir += voc.substr( prng.uint32() % voc.length(), 1 ); + } + if ( mkdir( dir.c_str(), 0700 ) != 0 ) { + return; + } + string path(dir + "/"); + for ( i = 0; i < 12; i++ ) { + path += voc.substr( prng.uint32() % voc.length(), 1 ); + } + int sock = socket( AF_UNIX, SOCK_STREAM, 0 ); + if ( sock < 0 ) { + (void) rmdir( dir.c_str() ); + return; + } + if ( fcntl( sock, F_SETFD, FD_CLOEXEC ) != 0 ) { + (void) rmdir( dir.c_str() ); + return; + } + struct sockaddr_un sunaddr; + memset( &sunaddr, 0, sizeof (sunaddr) ); + sunaddr.sun_family = AF_UNIX; + if ( path.length() >= sizeof (sunaddr.sun_path) ) { + (void) close( sock ); + (void) rmdir( dir.c_str() ); + return; + } + strncpy( sunaddr.sun_path, path.c_str(), sizeof (sunaddr.sun_path) ); + if ( bind( sock, (struct sockaddr *) &sunaddr, sizeof (sunaddr) ) < 0 ) { + (void) close( sock ); + (void) rmdir( dir.c_str() ); + return; + } + if ( listen( sock, AGENT_PROXY_LISTEN_QUEUE_LENGTH ) < 0) { + (void) close( sock ); + (void) unlink( path.c_str() ); + (void) rmdir( dir.c_str() ); + return; + } + l_sock = sock; + l_path = path; + l_dir = dir; + } + ok = true; +#endif +} + +ProxyAgent::~ProxyAgent( void ) { +#ifdef SUPPORT_AGENT_FORWARDING + shutdown(); +#endif +} + +void ProxyAgent::close_sessions( void ) { +#ifdef SUPPORT_AGENT_FORWARDING + map< uint64_t, AgentConnection * >::iterator i = agent_sessions.begin(); + while ( i != agent_sessions.end() ) { + AgentConnection *ac = i->second; + agent_sessions.erase( i ); + delete ac; + i = agent_sessions.begin(); + } +#endif +} + +void ProxyAgent::shutdown( void ) { +#ifdef SUPPORT_AGENT_FORWARDING + detach_oob(); + if (ok) { + if ( server && l_sock >= 0 ) { + (void) close( l_sock ); + (void) unlink( l_path.c_str() ); + (void) rmdir( l_dir.c_str() ); + l_sock = -1; + l_path = ""; + l_dir = ""; + } + close_sessions(); + ok = false; + } +#endif +} + +void ProxyAgent::attach_oob(OutOfBand *oob_ctl) { + detach_oob(); + fatal_assert(oob_ctl != NULL); + oob_ctl_ptr = oob_ctl; + comm = oob_ctl_ptr->init(AGENT_FORWARD_OOB_NAME, Network::OOB_MODE_RELIABLE_DATAGRAM, this); + fatal_assert(comm != NULL); +} + +void ProxyAgent::detach_oob(void) { + if (oob_ctl_ptr != NULL) { + oob_ctl_ptr->uninit(AGENT_FORWARD_OOB_NAME); + } + oob_ctl_ptr = NULL; +} + +void ProxyAgent::pre_poll( void ) { +#ifdef SUPPORT_AGENT_FORWARDING + if ( ! ok ) { + return; + } + Select &sel = Select::get_instance(); + if ( server && l_sock >= 0 ) { + sel.add_fd( l_sock ); + } + for ( map< uint64_t, AgentConnection * >::iterator i = agent_sessions.begin(); i != agent_sessions.end(); i++ ) { + AgentConnection *ac = i->second; + if ( ac->sock() >= 0 ) { + sel.add_fd( ac->sock() ); + ac->mark_in_read_set(true); + } else { + ac->mark_in_read_set(false); + } + } +#endif +} + +void ProxyAgent::post_poll( void ) { +#ifdef SUPPORT_AGENT_FORWARDING + if ( ! ok ) { + return; + } + Select &sel = Select::get_instance(); + // First handle possible incoming data from local sockets + map< uint64_t, AgentConnection * >::iterator i = agent_sessions.begin(); + while ( ((! server) || (l_sock >= 0)) && i != agent_sessions.end() ) { + AgentConnection *ac = i->second; + if ( (comm == NULL) || (oob_ctl_ptr == NULL) || ac->eof() || (ac->idle_time() > AGENT_IDLE_TIMEOUT) ) { + agent_sessions.erase( i++ ); + delete ac; + continue; + } + + if ( ac->in_read_set() && sel.read( ac->sock() ) ) { + while ( true ) { + string packet = ac->recv_packet(); + if ( ! packet.empty() ) { + AgentBuffers::Instruction inst; + inst.set_agent_id(ac->s_id); + inst.set_agent_data(packet); + string pb_packet; + fatal_assert(inst.SerializeToString(&pb_packet)); + comm->send(pb_packet); + continue; + } + if ( ac->eof() ) { + notify_eof(ac->s_id); + agent_sessions.erase( i++ ); + delete ac; + break; + } + i++; + break; + } + } else { + i++; + } + } + if ( ! server ) { + return; + } + // Then see if we have mysteriously died in between. + if ( l_sock < 0 ) { + return; + } + // Then check for new incoming connections. + if ( sel.read( l_sock ) ) { + AgentConnection *new_as = get_session(); + if ( new_as != NULL ) { + agent_sessions[new_as->s_id] = new_as; + } + } +#endif +} + +void ProxyAgent::post_tick( void ) { +#ifdef SUPPORT_AGENT_FORWARDING + if ( (! ok) || (comm == NULL) ) { + return; + } + while (comm->readable()) { + string pb_packet = comm->recv(); + AgentBuffers::Instruction inst; + fatal_assert( inst.ParseFromString(pb_packet) ); + uint64_t agent_id = inst.agent_id(); + string agent_data = inst.has_agent_data() ? inst.agent_data() : ""; + if (agent_data.empty()) { + map < uint64_t, AgentConnection* >::iterator i = agent_sessions.find(agent_id); + if (i != agent_sessions.end()) { + AgentConnection *ac = i->second; + agent_sessions.erase( i ); + delete ac; + } + } else { + map < uint64_t, AgentConnection* >::iterator i = agent_sessions.find(agent_id); + if (i == agent_sessions.end()) { + AgentConnection *new_as = NULL; + if (! server) { + const char *ap = getenv( "SSH_AUTH_SOCK" ); + if ( ap != NULL ) { + string agent_path(ap); + if ( ! agent_path.empty() ) { + new_as = new AgentConnection ( agent_path, agent_id, this ); + } + } + } + if (new_as == NULL) { + notify_eof(agent_id); + } else { + agent_sessions[agent_id] = new_as; + } + i = agent_sessions.find(agent_id); + } + if (i != agent_sessions.end()) { + AgentConnection *ac = i->second; + uint64_t idle = ac->idle_time(); + uint64_t timeout = idle < AGENT_IDLE_TIMEOUT ? (AGENT_IDLE_TIMEOUT - idle) * 1000 : 1; + if ( swrite_timeout( ac->sock(), timeout, agent_data.c_str(), agent_data.length() ) != 0 ) { + agent_sessions.erase( i ); + delete ac; + notify_eof(agent_id); + } + } + } + } +#endif +} + +void ProxyAgent::notify_eof(uint64_t agent_id) { +#ifdef SUPPORT_AGENT_FORWARDING + if (comm == NULL) { + return; + } + AgentBuffers::Instruction inst; + inst.set_agent_id(agent_id); + string pb_packet; + fatal_assert(inst.SerializeToString(&pb_packet)); + comm->send(pb_packet); +#endif +} + + +AgentConnection *ProxyAgent::get_session() { +#ifdef SUPPORT_AGENT_FORWARDING + if ( (! server) || l_sock < 0) { + return NULL; + } + struct sockaddr_un sunaddr; + socklen_t slen = sizeof ( sunaddr ); + memset( &sunaddr, 0, slen ); + int sock = accept ( l_sock, (struct sockaddr *)&sunaddr, &slen ); + if ( sock < 0 ) { + return NULL; + } + + if ( (comm == NULL) || (oob_ctl_ptr == NULL) ) { + (void) close( sock ); + return NULL; + } + + /* Here we should check that peer effective uid matches with the + euid of this process. Skipping however and trusting the file + system to protect the socket. This would basically catch root + accessing the socket, but root can change its effective uid to + match the socket anyways, so it doesn't really help at all. */ + + /* If can't set the socket mode, discard it. */ + if ( fcntl( sock, F_SETFD, FD_CLOEXEC ) != 0 || fcntl( sock, F_SETFL, O_NONBLOCK ) != 0 ) { + (void) close( sock ); + return NULL; + } + return new AgentConnection ( sock, ++cnt, this ); +#else + return NULL; +#endif +} + +AgentConnection::AgentConnection(int sock, uint64_t id, ProxyAgent *s_agent_ptr) + : s_in_read_set( false ), + s_sock( sock ), + s_id( id ), + idle_start( Network::timestamp() ), + packet_buf( "" ), + packet_len( 0 ), + agent_ptr( s_agent_ptr ) +{ +#ifndef SUPPORT_AGENT_FORWARDING + if (sock >= 0) { + (void) close( sock ); + } + s_sock = -1; +#endif +} + +AgentConnection::AgentConnection(std::string agent_path, uint64_t id, ProxyAgent *s_agent_ptr) + : s_in_read_set( false ), + s_sock( -1 ), + s_id( id ), + idle_start( Network::timestamp() ), + packet_buf( "" ), + packet_len( 0 ), + agent_ptr( s_agent_ptr ) +{ +#ifdef SUPPORT_AGENT_FORWARDING + int sock = socket( AF_UNIX, SOCK_STREAM, 0 ); + struct sockaddr_un sunaddr; + memset( &sunaddr, 0, sizeof (sunaddr) ); + sunaddr.sun_family = AF_UNIX; + if ( agent_path.length() >= sizeof (sunaddr.sun_path) ) { + (void) close( sock ); + return; + } + if ( fcntl( sock, F_SETFD, FD_CLOEXEC ) != 0 ) { + (void) close( sock ); + return; + } + strncpy( sunaddr.sun_path, agent_path.c_str(), sizeof (sunaddr.sun_path) ); + if ( connect(sock, (struct sockaddr *)&sunaddr, sizeof (sunaddr)) < 0 ) { + (void) close( sock ); + return; + } + if ( fcntl( sock, F_SETFL, O_NONBLOCK ) != 0 ) { + (void) close( sock ); + return; + } + s_sock = sock; +#endif +} + +AgentConnection::~AgentConnection() { + if ( s_sock >= 0 ) { + (void) close ( s_sock ); + } +} + +uint64_t AgentConnection::idle_time() { + return (Network::timestamp() - idle_start) / 1000; +} + +string AgentConnection::recv_packet() { +#ifdef SUPPORT_AGENT_FORWARDING + if (eof()) { + return ""; + } + ssize_t rv; + if (packet_len < 1) { + unsigned char buf[4]; + rv = read( s_sock, buf, 4 ); + if ( (rv < 0) && ( errno == EAGAIN || errno == EWOULDBLOCK ) ) { + return ""; + } + if ( rv != 4 ) { + (void) close(s_sock); + s_sock = -1; + return ""; + } + if ( buf[0] != 0 ) { + (void) close(s_sock); + s_sock = -1; + return ""; + } + + packet_len = (((size_t)buf[1]) << 16) | (((size_t)buf[2]) << 8) | ((size_t)buf[3]); + if ( packet_len < 1 || packet_len > AGENT_MAXIMUM_PACKET_LENGTH ) { + (void) close(s_sock); + s_sock = -1; + return ""; + } + packet_buf.append((char *)buf, 4); + idle_start = Network::timestamp(); + } + /* read in loop until the entire packet is read or EAGAIN happens */ + do { + unsigned char buf[1024]; + size_t len = packet_len + 4 - packet_buf.length(); + if (len > sizeof (buf)) { + len = sizeof (buf); + } + rv = read(s_sock, buf, len); + if ( (rv < 0) && ( errno == EAGAIN || errno == EWOULDBLOCK ) ) { + return ""; + } + if ( rv < 1 ) { + (void) close(s_sock); + s_sock = -1; + return ""; + } + packet_buf.append((char *)buf, rv); + idle_start = Network::timestamp(); + } while (packet_buf.length() < (packet_len + 4)); + string packet(packet_buf); + packet_buf = ""; + packet_len = 0; + return packet; +#endif + return ""; +} diff --git a/src/agent/agent.h b/src/agent/agent.h new file mode 100644 index 000000000..4a19c6ffc --- /dev/null +++ b/src/agent/agent.h @@ -0,0 +1,123 @@ +/* + Mosh: the mobile shell + Copyright 2012 Keith Winstein + + SSH Agent forwarding for Mosh + Copyright 2013 Timo J. Rinne + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + In addition, as a special exception, the copyright holders give + permission to link the code of portions of this program with the + OpenSSL library under certain conditions as described in each + individual source file, and distribute linked combinations including + the two. + + You must obey the GNU General Public License in all respects for all + of the code used other than OpenSSL. If you modify file(s) with this + exception, you may extend this exception to your version of the + file(s), but you are not obligated to do so. If you do not wish to do + so, delete this exception statement from your version. If you delete + this exception statement from all source files in the program, then + also delete it here. +*/ + +#ifndef AGENT_HPP +#define AGENT_HPP + +#include +#include + +#include "outofband.h" + +#define AGENT_MAXIMUM_PACKET_LENGTH 32768 // Not counting the length field. +#define AGENT_MAXIMUM_OUTPUT_BUFFER_LENGTH (AGENT_MAXIMUM_PACKET_LENGTH * 4) // Counting all data +#define AGENT_IDLE_TIMEOUT 30 // In seconds. Must be enforced by the caller. +#define AGENT_PROXY_LISTEN_QUEUE_LENGTH 4 +#define AGENT_FORWARD_OOB_NAME "ssh-agent-forward" + +namespace Agent { + + class ProxyAgent; + + class AgentConnection + { + private: + bool s_in_read_set; + int s_sock; + uint64_t s_id; + uint64_t idle_start; + string packet_buf; + size_t packet_len; + ProxyAgent *agent_ptr; + + AgentConnection(int sock, uint64_t id, ProxyAgent *s_agent_ptr); + AgentConnection(std::string agent_path, uint64_t id, ProxyAgent *s_agent_ptr); + ~AgentConnection(); + AgentConnection(const AgentConnection&); // unimplemented + AgentConnection operator=(const AgentConnection&); // unimplemented + + int sock() { return s_sock; } + bool eof() { return (s_sock < 0); } + std::string recv_packet(); + uint64_t idle_time(); + void mark_in_read_set(bool val) { s_in_read_set = val; } + bool in_read_set( void ) { return s_in_read_set; } + + public: + friend class ProxyAgent; + }; + + class ProxyAgent : public Network::OutOfBandPlugin + { + private: + Network::OutOfBandCommunicator *comm; + Network::OutOfBand *oob_ctl_ptr; + bool server; + bool ok; + int l_sock; + string l_dir; + string l_path; + uint64_t cnt; + std::map< uint64_t, AgentConnection * > agent_sessions; + + void detach_oob(void); + Network::OutOfBand *oob( void ) { return oob_ctl_ptr; } + void notify_eof(uint64_t agent_id); + AgentConnection *get_session(); + + ProxyAgent(const ProxyAgent&); // unimplemented + ProxyAgent operator=(const ProxyAgent&); // unimplemented + + public: + // Required by parent class + bool active( void ) { return ok && ((! server) || (l_sock >= 0)); } + void pre_poll( void ); + void post_poll( void ); + void post_tick( void ); + void close_sessions( void ); + void shutdown( void ); + void attach_oob(Network::OutOfBand *oob_ctl); + + // Class specific stuff + ProxyAgent( bool is_server, bool dummy = false ); + ~ProxyAgent( void ); + std::string listener_path( void ) { if ( ok && server && l_sock >= 0 ) return l_path; return ""; } + + friend class AgentConnection; + }; + +} + +#endif diff --git a/src/frontend/Makefile.am b/src/frontend/Makefile.am index a0345ae02..91b5a7ba0 100644 --- a/src/frontend/Makefile.am +++ b/src/frontend/Makefile.am @@ -1,7 +1,7 @@ -AM_CPPFLAGS = -I$(srcdir)/../statesync -I$(srcdir)/../terminal -I$(srcdir)/../network -I$(srcdir)/../crypto -I../protobufs -I$(srcdir)/../util $(TINFO_CFLAGS) $(protobuf_CFLAGS) $(OPENSSL_CFLAGS) +AM_CPPFLAGS = -I$(srcdir)/../statesync -I$(srcdir)/../terminal -I$(srcdir)/../network -I$(srcdir)/../crypto -I$(srcdir)/../protobufs -I$(srcdir)/../agent -I$(srcdir)/../util $(TINFO_CFLAGS) $(protobuf_CFLAGS) $(OPENSSL_CFLAGS) AM_CXXFLAGS = $(WARNING_CXXFLAGS) $(PICKY_CXXFLAGS) $(HARDEN_CFLAGS) $(MISC_CXXFLAGS) AM_LDFLAGS = $(HARDEN_LDFLAGS) -LDADD = ../crypto/libmoshcrypto.a ../network/libmoshnetwork.a ../statesync/libmoshstatesync.a ../terminal/libmoshterminal.a ../util/libmoshutil.a ../protobufs/libmoshprotos.a -lm $(TINFO_LIBS) $(protobuf_LIBS) $(OPENSSL_LIBS) +LDADD = ../crypto/libmoshcrypto.a ../network/libmoshnetwork.a ../statesync/libmoshstatesync.a ../terminal/libmoshterminal.a ../agent/libmoshagent.a ../util/libmoshutil.a ../protobufs/libmoshprotos.a -lm $(TINFO_LIBS) $(protobuf_LIBS) $(OPENSSL_LIBS) mosh_server_LDADD = $(LDADD) $(LIBUTIL) diff --git a/src/frontend/mosh-client.cc b/src/frontend/mosh-client.cc index 83230a1ec..b483e0f14 100644 --- a/src/frontend/mosh-client.cc +++ b/src/frontend/mosh-client.cc @@ -101,10 +101,15 @@ int main( int argc, char *argv[] ) /* Detect edge case */ fatal_assert( argc > 0 ); + bool forward_agent = false; + /* Get arguments */ int opt; - while ( (opt = getopt( argc, argv, "c" )) != -1 ) { + while ( (opt = getopt( argc, argv, "cA" )) != -1 ) { switch ( opt ) { + case 'A': + forward_agent = true; + break; case 'c': print_colorcount(); exit( 0 ); @@ -160,7 +165,7 @@ int main( int argc, char *argv[] ) set_native_locale(); try { - STMClient client( ip, desired_port, key, predict_mode ); + STMClient client( ip, desired_port, key, predict_mode, forward_agent ); client.init(); try { diff --git a/src/frontend/mosh-server.cc b/src/frontend/mosh-server.cc index 84cf7b676..3d9d9b544 100644 --- a/src/frontend/mosh-server.cc +++ b/src/frontend/mosh-server.cc @@ -79,6 +79,7 @@ #include "locale_utils.h" #include "pty_compat.h" #include "select.h" +#include "agent.h" #include "timestamp.h" #include "fatal_assert.h" @@ -92,11 +93,13 @@ typedef Network::Transport< Terminal::Complete, Network::UserStream > ServerConn void serve( int host_fd, Terminal::Complete &terminal, - ServerConnection &network ); + ServerConnection &network, + Agent::ProxyAgent &agent ); int run_server( const char *desired_ip, const char *desired_port, const string &command_path, char *command_argv[], - const int colors, bool verbose, bool with_motd ); + const int colors, bool verbose, bool with_motd, + bool with_agent_fwd ); using namespace std; @@ -165,6 +168,7 @@ int main( int argc, char *argv[] ) string command_path; char **command_argv = NULL; int colors = 0; + bool with_agent_fwd = false; bool verbose = false; /* don't close stdin/stdout/stderr */ /* Will cause mosh-server not to correctly detach on old versions of sshd. */ list locale_vars; @@ -185,8 +189,11 @@ int main( int argc, char *argv[] ) && (strcmp( argv[ 1 ], "new" ) == 0) ) { /* new option syntax */ int opt; - while ( (opt = getopt( argc - 1, argv + 1, "i:p:c:svl:" )) != -1 ) { + while ( (opt = getopt( argc - 1, argv + 1, "i:p:c:svl:A" )) != -1 ) { switch ( opt ) { + case 'A': + with_agent_fwd = true; + break; case 'i': desired_ip = optarg; break; @@ -308,7 +315,7 @@ int main( int argc, char *argv[] ) } try { - return run_server( desired_ip, desired_port, command_path, command_argv, colors, verbose, with_motd ); + return run_server( desired_ip, desired_port, command_path, command_argv, colors, verbose, with_motd, with_agent_fwd ); } catch ( const Network::NetworkException& e ) { fprintf( stderr, "Network exception: %s: %s\n", e.function.c_str(), strerror( e.the_errno ) ); @@ -322,7 +329,8 @@ int main( int argc, char *argv[] ) int run_server( const char *desired_ip, const char *desired_port, const string &command_path, char *command_argv[], - const int colors, bool verbose, bool with_motd ) { + const int colors, bool verbose, bool with_motd, + bool with_agent_fwd ) { /* get initial window size */ struct winsize window_size; if ( ioctl( STDIN_FILENO, TIOCGWINSZ, &window_size ) < 0 || @@ -374,6 +382,13 @@ int run_server( const char *desired_ip, const char *desired_port, fprintf( stderr, "[mosh-server detached, pid = %d]\n", (int)getpid() ); + /* initialize agent listener if requested */ + Agent::ProxyAgent agent( true, ! with_agent_fwd ); + if ( with_agent_fwd && (! agent.active()) ) { + fprintf( stderr, "Warning: Agent listener initialization failed. Disabling agent forwarding.\n" ); + with_agent_fwd = false; + } + int master; #ifndef HAVE_IUTF8 @@ -454,6 +469,14 @@ int run_server( const char *desired_ip, const char *desired_port, exit( 1 ); } + /* set SSH_AUTH_SOCK */ + if ( agent.active() ) { + if ( setenv( "SSH_AUTH_SOCK", agent.listener_path().c_str(), true ) < 0 ) { + perror( "setenv" ); + exit( 1 ); + } + } + /* ask ncurses to send UTF-8 instead of ISO 2022 for line-drawing chars */ if ( setenv( "NCURSES_NO_UTF8_ACS", "1", true ) < 0 ) { perror( "setenv" ); @@ -488,7 +511,7 @@ int run_server( const char *desired_ip, const char *desired_port, #endif try { - serve( master, terminal, *network ); + serve( master, terminal, *network, agent ); } catch ( const Network::NetworkException& e ) { fprintf( stderr, "Network exception: %s: %s\n", e.function.c_str(), strerror( e.the_errno ) ); @@ -514,7 +537,7 @@ int run_server( const char *desired_ip, const char *desired_port, return 0; } -void serve( int host_fd, Terminal::Complete &terminal, ServerConnection &network ) +void serve( int host_fd, Terminal::Complete &terminal, ServerConnection &network, Agent::ProxyAgent &agent ) { /* prepare to poll for events */ Select &sel = Select::get_instance(); @@ -530,6 +553,8 @@ void serve( int host_fd, Terminal::Complete &terminal, ServerConnection &network socklen_t saved_addr_len = 0; #endif + agent.attach_oob(network.oob()); + while ( 1 ) { try { uint64_t now = Network::timestamp(); @@ -551,6 +576,8 @@ void serve( int host_fd, Terminal::Complete &terminal, ServerConnection &network sel.add_fd( host_fd ); } + network.oob()->pre_poll(); + int active_fds = sel.select( timeout ); if ( active_fds < 0 ) { perror( "select" ); @@ -647,6 +674,7 @@ void serve( int host_fd, Terminal::Complete &terminal, ServerConnection &network /* If the pty slave is closed, reading from the master can fail with EIO (see #264). So we treat errors on read() like EOF. */ if ( bytes_read <= 0 ) { + network.oob()->shutdown(); network.start_shutdown(); } else { string terminal_to_host = terminal.act( string( buf, bytes_read ) ); @@ -664,6 +692,7 @@ void serve( int host_fd, Terminal::Complete &terminal, ServerConnection &network if ( sel.any_signal() ) { /* shutdown signal */ if ( network.has_remote_addr() && (!network.shutdown_in_progress()) ) { + network.oob()->shutdown(); network.start_shutdown(); } else { break; @@ -677,6 +706,7 @@ void serve( int host_fd, Terminal::Complete &terminal, ServerConnection &network if ( (!network.shutdown_in_progress()) && sel.error( host_fd ) ) { /* host problem */ + network.oob()->shutdown(); network.start_shutdown(); } @@ -721,15 +751,25 @@ void serve( int host_fd, Terminal::Complete &terminal, ServerConnection &network && time_since_remote_state >= uint64_t( timeout_if_no_client ) ) { fprintf( stderr, "No connection within %d seconds.\n", timeout_if_no_client / 1000 ); + network.oob()->shutdown(); break; } + if ( time_since_remote_state > (AGENT_IDLE_TIMEOUT * 1000) || time_since_remote_state > 30000 ) { + network.oob()->close_sessions(); + } + network.oob()->post_poll(); + network.tick(); + + network.oob()->post_tick(); + } catch ( const Network::NetworkException& e ) { fprintf( stderr, "%s: %s\n", e.function.c_str(), strerror( e.the_errno ) ); spin(); } catch ( const Crypto::CryptoException& e ) { if ( e.fatal ) { + network.oob()->shutdown(); throw; } else { fprintf( stderr, "Crypto exception: %s\n", e.text.c_str() ); diff --git a/src/frontend/stmclient.cc b/src/frontend/stmclient.cc index cfb473a3f..c41cc52c6 100644 --- a/src/frontend/stmclient.cc +++ b/src/frontend/stmclient.cc @@ -59,6 +59,7 @@ #include "pty_compat.h" #include "select.h" #include "timestamp.h" +#include "agent.h" #include "networktransport.cc" @@ -414,6 +415,10 @@ void STMClient::main( void ) /* initialize signal handling and structures */ main_init(); + Agent::ProxyAgent agent( false, ! forward_agent ); + + agent.attach_oob(network->oob()); + /* prepare to poll for events */ Select &sel = Select::get_instance(); @@ -439,6 +444,8 @@ void STMClient::main( void ) } sel.add_fd( STDIN_FILENO ); + network->oob()->pre_poll(); + int active_fds = sel.select( wait_time ); if ( active_fds < 0 ) { perror( "select" ); @@ -458,6 +465,7 @@ void STMClient::main( void ) if ( sel.error( *it ) ) { /* network problem */ + network->oob()->shutdown(); break; } } @@ -471,9 +479,12 @@ void STMClient::main( void ) if ( !process_user_input( STDIN_FILENO ) ) { if ( !network->has_remote_addr() ) { break; - } else if ( !network->shutdown_in_progress() ) { - overlays.get_notification_engine().set_notification_string( wstring( L"Exiting..." ), true ); - network->start_shutdown(); + } else { + network->oob()->shutdown(); + if ( !network->shutdown_in_progress() ) { + overlays.get_notification_engine().set_notification_string( wstring( L"Exiting..." ), true ); + network->start_shutdown(); + } } } } @@ -496,6 +507,7 @@ void STMClient::main( void ) break; } else if ( !network->shutdown_in_progress() ) { overlays.get_notification_engine().set_notification_string( wstring( L"Signal received, shutting down..." ), true ); + network->oob()->shutdown(); network->start_shutdown(); } } @@ -506,6 +518,7 @@ void STMClient::main( void ) break; } else if ( !network->shutdown_in_progress() ) { overlays.get_notification_engine().set_notification_string( wstring( L"Exiting..." ), true ); + network->oob()->shutdown(); network->start_shutdown(); } } @@ -534,6 +547,7 @@ void STMClient::main( void ) if ( timestamp() - network->get_latest_remote_state().timestamp > 15000 ) { if ( !network->shutdown_in_progress() ) { overlays.get_notification_engine().set_notification_string( wstring( L"Timed out waiting for server..." ), true ); + network->oob()->shutdown(); network->start_shutdown(); } } else { @@ -545,8 +559,12 @@ void STMClient::main( void ) overlays.get_notification_engine().set_notification_string( L"" ); } + network->oob()->post_poll(); + network->tick(); + network->oob()->post_tick(); + const Network::NetworkException *exn = network->get_send_exception(); if ( exn ) { overlays.get_notification_engine().set_network_exception( *exn ); diff --git a/src/frontend/stmclient.h b/src/frontend/stmclient.h index 187b4f5c9..de51bca46 100644 --- a/src/frontend/stmclient.h +++ b/src/frontend/stmclient.h @@ -47,7 +47,7 @@ class STMClient { std::string ip; std::string port; std::string key; - + bool forward_agent; int escape_key; int escape_pass_key; int escape_pass_key2; @@ -83,8 +83,8 @@ class STMClient { void resume( void ); /* restore state after SIGCONT */ public: - STMClient( const char *s_ip, const char *s_port, const char *s_key, const char *predict_mode ) - : ip( s_ip ), port( s_port ), key( s_key ), + STMClient( const char *s_ip, const char *s_port, const char *s_key, const char *predict_mode, bool s_forward_agent ) + : ip( s_ip ), port( s_port ), key( s_key ), forward_agent( s_forward_agent ), escape_key( 0x1E ), escape_pass_key( '^' ), escape_pass_key2( '^' ), escape_requires_lf( false ), escape_key_help( L"?" ), saved_termios(), raw_termios(), diff --git a/src/network/Makefile.am b/src/network/Makefile.am index 3143cc400..fe8e3edcb 100644 --- a/src/network/Makefile.am +++ b/src/network/Makefile.am @@ -3,4 +3,4 @@ AM_CXXFLAGS = $(WARNING_CXXFLAGS) $(PICKY_CXXFLAGS) $(HARDEN_CFLAGS) $(MISC_CXXF noinst_LIBRARIES = libmoshnetwork.a -libmoshnetwork_a_SOURCES = network.cc network.h networktransport.cc networktransport.h transportfragment.cc transportfragment.h transportsender.cc transportsender.h transportstate.h compressor.cc compressor.h +libmoshnetwork_a_SOURCES = network.cc network.h networktransport.cc networktransport.h transportfragment.cc transportfragment.h transportsender.cc transportsender.h transportstate.h compressor.cc compressor.h outofband.h outofband.cc diff --git a/src/network/networktransport.cc b/src/network/networktransport.cc index 0e0bcf24a..12ca4ffee 100644 --- a/src/network/networktransport.cc +++ b/src/network/networktransport.cc @@ -130,6 +130,11 @@ void Transport::recv( void ) } } + /* Deliver out of band data */ + if (inst.has_oob()) { + oob()->input(inst.oob()); + } + /* apply diff to reference state */ TimestampedState new_state = *reference_state; new_state.timestamp = timestamp(); diff --git a/src/network/networktransport.h b/src/network/networktransport.h index 6f3efd590..d8b5e36de 100644 --- a/src/network/networktransport.h +++ b/src/network/networktransport.h @@ -83,6 +83,9 @@ namespace Network { /* Find diff between last receiver state and current remote state, then rationalize states. */ string get_remote_diff( void ); + /* Get refenrece to out of band control object */ + OutOfBand *oob( void ) { return sender.oob(); } + /* Shut down other side of connection. */ /* Illegal to change current_state after this. */ void start_shutdown( void ) { sender.start_shutdown(); } diff --git a/src/network/outofband.cc b/src/network/outofband.cc new file mode 100644 index 000000000..2f369245f --- /dev/null +++ b/src/network/outofband.cc @@ -0,0 +1,323 @@ +/* + Mosh: the mobile shell + Copyright 2012 Keith Winstein + + Out of band protocol extension for Mosh + Copyright 2013 Timo J. Rinne + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + In addition, as a special exception, the copyright holders give + permission to link the code of portions of this program with the + OpenSSL library under certain conditions as described in each + individual source file, and distribute linked combinations including + the two. + + You must obey the GNU General Public License in all respects for all + of the code used other than OpenSSL. If you modify file(s) with this + exception, you may extend this exception to your version of the + file(s), but you are not obligated to do so. If you do not wish to do + so, delete this exception statement from your version. If you delete + this exception statement from all source files in the program, then + also delete it here. +*/ + +#include +#include +#include +#include + +#include "fatal_assert.h" + +#include "outofband.h" +#include "oob.pb.h" + +#include + +using namespace Network; +using namespace OutOfBandBuffers; +using namespace std; + + +OutOfBand::OutOfBand() + : comms(), + datagram_instruction_out(), + reliable_instruction_out_sent(), + reliable_instruction_out_unsent(), + seq_num_out( 0 ), + ack_num_out( 0 ) +{ + seq_num_out = 0; + ack_num_out = 0; +} + +OutOfBandCommunicator *OutOfBand::init(string name, OutOfBandMode mode, OutOfBandPlugin *plugin) { + map < string, OutOfBandCommunicator * >::iterator i = comms.find(name); + if (i != comms.end()) { + return NULL; + } + OutOfBandCommunicator *comm = new OutOfBandCommunicator(mode, name, this, plugin); + comms[name] = comm; + return comm; +} + +void OutOfBand::pre_poll( void ) { + map < string, OutOfBandCommunicator * >::iterator i = comms.begin(); + while (i != comms.end()) { + OutOfBandCommunicator *comm = (i++)->second; + if (comm->plugin_ptr->active()) { + comm->plugin_ptr->pre_poll(); + } + } +} + +void OutOfBand::post_poll( void ) { + map < string, OutOfBandCommunicator * >::iterator i = comms.begin(); + while (i != comms.end()) { + OutOfBandCommunicator *comm = (i++)->second; + if (comm->plugin_ptr->active()) { + comm->plugin_ptr->post_poll(); + } + } +} + +void OutOfBand::post_tick( void ) { + map < string, OutOfBandCommunicator * >::iterator i = comms.begin(); + while (i != comms.end()) { + OutOfBandCommunicator *comm = (i++)->second; + if (comm->plugin_ptr->active()) { + comm->plugin_ptr->post_tick(); + } + } +} + +void OutOfBand::close_sessions( void ) { + map < string, OutOfBandCommunicator * >::iterator i = comms.begin(); + while (i != comms.end()) { + OutOfBandCommunicator *comm = (i++)->second; + comm->plugin_ptr->close_sessions(); + } +} + +void OutOfBand::shutdown( void ) { + map < string, OutOfBandCommunicator * >::iterator i = comms.begin(); + while (i != comms.end()) { + OutOfBandCommunicator *comm = (i++)->second; + comm->plugin_ptr->shutdown(); + } +} + +void OutOfBand::uninit(string name) { + map < string, OutOfBandCommunicator * >::iterator i = comms.find(name); + if (i == comms.end()) { + return; + } + OutOfBandCommunicator *comm = i->second; + comms.erase(i); + delete comm; +} + +void OutOfBand::uninit(OutOfBandCommunicator *comm) { + uninit(comm->name); +} + +void OutOfBand::uninit(void) { + map < string, OutOfBandCommunicator * >::iterator i; + while ((i = comms.begin()) != comms.end()) { + OutOfBandCommunicator *comm = i->second; + comms.erase(i); + delete comm; + } +} + +void OutOfBand::input(string data) { + Instruction inst; + fatal_assert( inst.ParseFromString(data) ); + if (inst.has_ack_num()) { + uint64_t ack_num = inst.ack_num(); + if (ack_num != 0) { + list < OutOfBandBuffers::Instruction >::iterator i = reliable_instruction_out_sent.begin(); + while (i != reliable_instruction_out_sent.end()) { + fatal_assert((*i).has_seq_num()); + if ((*i).seq_num() <= ack_num) { + i = reliable_instruction_out_sent.erase(i); + continue; + } + break; + } + } + } + + bool ack = false; + + if (inst.has_payload_type() && inst.has_payload_data()) { + string payload_type = inst.payload_type(); + string payload_data = inst.payload_data(); + uint64_t seq_num = inst.has_seq_num() ? inst.seq_num() : 0; + uint64_t oob_mode = inst.has_oob_mode() ? inst.oob_mode() : 0; + OutOfBandCommunicator *comm = NULL; + map < string, OutOfBandCommunicator * >::iterator i = comms.find(payload_type); + if (i != comms.end()) { + comm = i->second; + fatal_assert(oob_mode == (uint64_t)comm->mode); + } + if (seq_num == 0) { + fatal_assert(oob_mode == (uint64_t)OOB_MODE_DATAGRAM); + if (comm != NULL) { + comm->datagram_queue.push(payload_data); + } + } else { + fatal_assert(oob_mode == (uint64_t)OOB_MODE_STREAM || oob_mode == (uint64_t)OOB_MODE_RELIABLE_DATAGRAM); + if (seq_num == next_seq_num(ack_num_out)) { + if (comm != NULL) { + switch (comm->mode) { + case OOB_MODE_STREAM: + comm->stream_buf += payload_data; + break; + case OOB_MODE_RELIABLE_DATAGRAM: + comm->datagram_queue.push(payload_data); + break; + default: + //NOTREACHED + fatal_assert(comm->mode == OOB_MODE_STREAM || comm->mode == OOB_MODE_RELIABLE_DATAGRAM); + } + } + ack_num_out = seq_num; + } + ack = true; + } + } + + if (ack && (! has_unsent_output())) { + Instruction inst; + datagram_instruction_out.push(inst); + } +} + +bool OutOfBand::has_output(void) { + return (! (datagram_instruction_out.empty() && reliable_instruction_out_sent.empty() && reliable_instruction_out_unsent.empty())); +} + +bool OutOfBand::has_unsent_output(void) { + return (! (datagram_instruction_out.empty() && reliable_instruction_out_unsent.empty())); +} + +string OutOfBand::output(void) { + string rv(""); + if (! datagram_instruction_out.empty()) { + Instruction inst = datagram_instruction_out.front(); + if (ack_num_out != 0) { + inst.set_ack_num(ack_num_out); + } + fatal_assert(inst.SerializeToString(&rv)); + datagram_instruction_out.pop(); + return rv; + } + if (! reliable_instruction_out_sent.empty()) { + Instruction inst = reliable_instruction_out_sent.front(); + if (ack_num_out != 0) { + inst.set_ack_num(ack_num_out); + } + fatal_assert(inst.SerializeToString(&rv)); + return rv; + } + if (! reliable_instruction_out_unsent.empty()) { + Instruction inst = reliable_instruction_out_unsent.front(); + reliable_instruction_out_sent.push_back(inst); + reliable_instruction_out_unsent.pop_front(); + if (ack_num_out != 0) { + inst.set_ack_num(ack_num_out); + } + fatal_assert(inst.SerializeToString(&rv)); + return rv; + } + return ""; +} + +OutOfBandCommunicator::OutOfBandCommunicator( OutOfBandMode oob_mode, string oob_name, + OutOfBand *oob_ctl, OutOfBandPlugin *plugin) + : mode( oob_mode ), + name( oob_name ), + stream_buf( "" ), + datagram_queue(), + plugin_ptr( plugin ), + oob_ctl_ptr( oob_ctl ) +{ + /* */ +} + +void OutOfBandCommunicator::send(string data) { + Instruction inst; + if (oob()->ack_num_out != 0) { + inst.set_ack_num(oob()->ack_num_out); + } + inst.set_payload_type(name); + inst.set_payload_data(data); + inst.set_oob_mode((uint64_t)mode); + switch (mode) { + case OOB_MODE_STREAM: + case OOB_MODE_RELIABLE_DATAGRAM: + inst.set_seq_num(oob()->increment_seq_num_out()); + oob()->reliable_instruction_out_unsent.push_back(inst); + break; + //FALLTHROUGH + case OOB_MODE_DATAGRAM: + oob()->datagram_instruction_out.push(inst); + } +} + +bool OutOfBandCommunicator::readable(void) { + switch (mode) { + case OOB_MODE_STREAM: + return (! stream_buf.empty()); + case OOB_MODE_DATAGRAM: + case OOB_MODE_RELIABLE_DATAGRAM: + return (! datagram_queue.empty()); + } + //NOTREACHED + return false; +} + +string OutOfBandCommunicator::recv(void) { + string rv(""); + switch (mode) { + case OOB_MODE_STREAM: + if (stream_buf.empty()) { + return rv; + } + rv = stream_buf; + stream_buf = ""; + return rv; + case OOB_MODE_RELIABLE_DATAGRAM: + case OOB_MODE_DATAGRAM: + if (datagram_queue.empty()) { + return rv; + } + rv = datagram_queue.front(); + datagram_queue.pop(); + return rv; + } + //NOTREACHED + return ""; +} + +string OutOfBandCommunicator::read(size_t len) { + fatal_assert(mode == OOB_MODE_STREAM); + if (stream_buf.length() < len) { + return ""; + } + string rv = stream_buf.substr(0, len); + stream_buf = stream_buf.substr(len); + return rv; +} diff --git a/src/network/outofband.h b/src/network/outofband.h new file mode 100644 index 000000000..cd6d5c616 --- /dev/null +++ b/src/network/outofband.h @@ -0,0 +1,134 @@ +/* + Mosh: the mobile shell + Copyright 2012 Keith Winstein + + Out of band protocol extension for Mosh + Copyright 2013 Timo J. Rinne + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + In addition, as a special exception, the copyright holders give + permission to link the code of portions of this program with the + OpenSSL library under certain conditions as described in each + individual source file, and distribute linked combinations including + the two. + + You must obey the GNU General Public License in all respects for all + of the code used other than OpenSSL. If you modify file(s) with this + exception, you may extend this exception to your version of the + file(s), but you are not obligated to do so. If you do not wish to do + so, delete this exception statement from your version. If you delete + this exception statement from all source files in the program, then + also delete it here. +*/ + + +#ifndef OUT_OF_BAND_HPP +#define OUT_OF_BAND_HPP + +#include +#include +#include +#include + +#include "oob.pb.h" + +using std::string; +using std::queue; +using std::list; +using std::map; + +namespace Network { + + enum OutOfBandMode { OOB_MODE_STREAM = 1, OOB_MODE_DATAGRAM = 2, OOB_MODE_RELIABLE_DATAGRAM = 3 }; + + class OutOfBand; + class OutOfBandPlugin; + class OutOfBandCommunicator; + + class OutOfBandCommunicator + { + private: + OutOfBandMode mode; + string name; + string stream_buf; + queue < string > datagram_queue; + OutOfBandPlugin *plugin_ptr; + OutOfBand *oob_ctl_ptr; + OutOfBand *oob(void) { return oob_ctl_ptr; } + OutOfBandCommunicator(OutOfBandMode oob_mode, string oob_name, OutOfBand *oob_ctl, OutOfBandPlugin *plugin); + OutOfBandCommunicator(const OutOfBandCommunicator&); + OutOfBandCommunicator operator=(const OutOfBandCommunicator&); + + public: + void send(string data); + bool readable(void); + string recv(void); + string read(size_t len); + + friend class OutOfBand; + }; + + class OutOfBand + { + private: + map < string, OutOfBandCommunicator * > comms; + queue < OutOfBandBuffers::Instruction > datagram_instruction_out; + list < OutOfBandBuffers::Instruction > reliable_instruction_out_sent; + list < OutOfBandBuffers::Instruction > reliable_instruction_out_unsent; + uint64_t seq_num_out; + uint64_t ack_num_out; + uint64_t next_seq_num(uint64_t sn) { sn++; if (sn == 0) sn++; return sn; } + uint64_t increment_seq_num_out(void) { seq_num_out = next_seq_num(seq_num_out); return seq_num_out; } + + public: + OutOfBand(); + ~OutOfBand() { uninit(); } + + void pre_poll( void ); + void post_poll( void ); + void post_tick( void ); + void close_sessions( void ); + void shutdown( void ); + + OutOfBandCommunicator *init(string name, OutOfBandMode mode, OutOfBandPlugin *plugin); + void uninit(string name); + void uninit(OutOfBandCommunicator *comm); + void uninit(void); + bool has_output(void); + bool has_unsent_output(void); + // input and output are to be called from transport code only + void input(string data); + string output(void); + + friend class OutOfBandCommunicator; + }; + + class OutOfBandPlugin + { + public: + virtual bool active( void ) = 0; + virtual void pre_poll( void ) = 0; + virtual void post_poll( void ) = 0; + virtual void post_tick( void ) = 0; + virtual void close_sessions( void ) = 0; + virtual void shutdown( void ) = 0; + virtual void attach_oob(Network::OutOfBand *oob_ctl) = 0; + + friend class OutOfBand; + }; + +} + +#endif diff --git a/src/network/transportsender.cc b/src/network/transportsender.cc index e641655d9..7e3cfc181 100644 --- a/src/network/transportsender.cc +++ b/src/network/transportsender.cc @@ -61,6 +61,7 @@ TransportSender::TransportSender( Connection *s_connection, MyState &in pending_data_ack( false ), SEND_MINDELAY( 8 ), last_heard( 0 ), + oob_ctl(), prng(), mindelay_clock( -1 ) { @@ -96,14 +97,18 @@ void TransportSender::calculate_timers( void ) next_ack_time = now + ACK_DELAY; } - if ( !(current_state == sent_states.back().state) ) { + if ( oob()->has_unsent_output() ) { + next_send_time = sent_states.back().timestamp + send_interval(); + if ( mindelay_clock != uint64_t( -1 ) ) { + next_send_time = max( next_send_time, mindelay_clock + SEND_MINDELAY ); + } + } else if ( !(current_state == sent_states.back().state) ) { if ( mindelay_clock == uint64_t( -1 ) ) { mindelay_clock = now; } - next_send_time = max( mindelay_clock + SEND_MINDELAY, sent_states.back().timestamp + send_interval() ); - } else if ( !(current_state == assumed_receiver_state->state) + } else if ( ((!(current_state == assumed_receiver_state->state)) || (oob()->has_output())) && (last_heard + ACTIVE_RETRY_TIMEOUT > now) ) { next_send_time = sent_states.back().timestamp + send_interval(); if ( mindelay_clock != uint64_t( -1 ) ) { @@ -181,11 +186,12 @@ void TransportSender::tick( void ) if ( diff.empty() && (now >= next_ack_time) ) { send_empty_ack(); mindelay_clock = uint64_t( -1 ); - } else if ( !diff.empty() && ( (now >= next_send_time) - || (now >= next_ack_time) ) ) { + } else if ( !diff.empty() && ((now >= next_send_time) || (now >= next_ack_time)) ) { /* Send diffs or ack */ send_to_receiver( diff ); mindelay_clock = uint64_t( -1 ); + } else if ( oob()->has_output() && ((now >= next_send_time) || (now >= next_ack_time)) ) { + send_empty_ack(); } } @@ -194,7 +200,7 @@ void TransportSender::send_empty_ack( void ) { uint64_t now = timestamp(); - assert( now >= next_ack_time ); + assert( now >= next_ack_time || oob()->has_output() ); uint64_t new_num = sent_states.back().num + 1; @@ -316,6 +322,10 @@ void TransportSender::send_in_fragments( string diff, uint64_t new_num inst.set_diff( diff ); inst.set_chaff( make_chaff() ); + if (oob()->has_output()) { + inst.set_oob(oob()->output()); + } + if ( new_num == uint64_t(-1) ) { shutdown_tries++; } diff --git a/src/network/transportsender.h b/src/network/transportsender.h index 572c47f56..3d4285b13 100644 --- a/src/network/transportsender.h +++ b/src/network/transportsender.h @@ -42,6 +42,7 @@ #include "transportstate.h" #include "transportfragment.h" #include "prng.h" +#include "outofband.h" using std::list; using std::pair; @@ -104,6 +105,8 @@ namespace Network { uint64_t last_heard; /* last time received new state */ + OutOfBand oob_ctl; /* out of band protocol object */ + /* chaff to disguise instruction length */ PRNG prng; const string make_chaff( void ); @@ -133,7 +136,10 @@ namespace Network { void remote_heard( uint64_t ts ) { last_heard = ts; } /* Starts shutdown sequence */ - void start_shutdown( void ) { if ( !shutdown_in_progress ) { shutdown_start = timestamp(); shutdown_in_progress = true; } } + void start_shutdown( void ) { if ( !shutdown_in_progress ) { oob_ctl.uninit(); shutdown_start = timestamp(); shutdown_in_progress = true; } } + + /* Get refenrece to out of band control object */ + OutOfBand *oob( void ) { return &oob_ctl; } /* Misc. getters and setters */ /* Cannot modify current_state while shutdown in progress */ diff --git a/src/protobufs/Makefile.am b/src/protobufs/Makefile.am index 131ec4ea8..419525ac0 100644 --- a/src/protobufs/Makefile.am +++ b/src/protobufs/Makefile.am @@ -1,4 +1,4 @@ -source = userinput.proto hostinput.proto transportinstruction.proto +source = userinput.proto hostinput.proto transportinstruction.proto oob.proto agent.proto AM_CPPFLAGS = $(protobuf_CFLAGS) AM_CXXFLAGS = $(WARNING_CXXFLAGS) $(HARDEN_CFLAGS) $(MISC_CXXFLAGS) diff --git a/src/protobufs/agent.proto b/src/protobufs/agent.proto new file mode 100644 index 000000000..b35033171 --- /dev/null +++ b/src/protobufs/agent.proto @@ -0,0 +1,8 @@ +option optimize_for = LITE_RUNTIME; + +package AgentBuffers; + +message Instruction { + required uint64 agent_id = 1; + optional bytes agent_data = 2; +} diff --git a/src/protobufs/oob.proto b/src/protobufs/oob.proto new file mode 100644 index 000000000..561ca31ea --- /dev/null +++ b/src/protobufs/oob.proto @@ -0,0 +1,11 @@ +option optimize_for = LITE_RUNTIME; + +package OutOfBandBuffers; + +message Instruction { + optional uint64 seq_num = 1; + optional uint64 ack_num = 2; + optional uint64 oob_mode = 3; + optional bytes payload_type = 4; + optional bytes payload_data = 5; +} diff --git a/src/protobufs/transportinstruction.proto b/src/protobufs/transportinstruction.proto index d4a76f77d..bb22a8c24 100644 --- a/src/protobufs/transportinstruction.proto +++ b/src/protobufs/transportinstruction.proto @@ -13,4 +13,6 @@ message Instruction { optional bytes diff = 6; optional bytes chaff = 7; + + optional bytes oob = 8; } diff --git a/src/util/Makefile.am b/src/util/Makefile.am index 25dc3dd45..ea41058b8 100644 --- a/src/util/Makefile.am +++ b/src/util/Makefile.am @@ -2,4 +2,4 @@ AM_CXXFLAGS = $(WARNING_CXXFLAGS) $(PICKY_CXXFLAGS) $(HARDEN_CFLAGS) $(MISC_CXXF noinst_LIBRARIES = libmoshutil.a -libmoshutil_a_SOURCES = locale_utils.cc locale_utils.h swrite.cc swrite.h dos_assert.h fatal_assert.h select.h select.cc timestamp.h timestamp.cc pty_compat.cc pty_compat.h +libmoshutil_a_SOURCES = locale_utils.cc locale_utils.h swrite.cc swrite.h dos_assert.h fatal_assert.h select.h select.cc timestamp.h timestamp.cc pty_compat.cc pty_compat.h diff --git a/src/util/swrite.cc b/src/util/swrite.cc index 64a772c6f..458477c3c 100644 --- a/src/util/swrite.cc +++ b/src/util/swrite.cc @@ -30,11 +30,17 @@ also delete it here. */ +#include "config.h" + #include #include #include +#include +#include +#include "timestamp.h" #include "swrite.h" +#include "fatal_assert.h" int swrite( int fd, const char *str, ssize_t len ) { @@ -53,3 +59,42 @@ int swrite( int fd, const char *str, ssize_t len ) return 0; } + + +int swrite_timeout( int fd, uint64_t timeout_ms, const char *str, ssize_t len ) +{ + size_t total_bytes_written = 0; + size_t bytes_to_write = ( len >= 0 ) ? len : (ssize_t) strlen( str ); + uint64_t t0 = frozen_timestamp(); + uint64_t iteration = 0; + + while ( total_bytes_written < bytes_to_write ) { + iteration++; + ssize_t rv = write( fd, str + total_bytes_written, bytes_to_write - total_bytes_written); + if ( rv > 0 ) { + total_bytes_written += rv; + continue; + } else if ( rv < 0 && ( errno == EAGAIN || errno == EWOULDBLOCK ) ) { + uint64_t t1 = frozen_timestamp(); + fatal_assert( t1 >= t0 ); + uint64_t total_time_spent = t1 - t0; + if ( total_time_spent > timeout_ms ) { + perror( "write" ); + return -1; + } + uint64_t time_left = timeout_ms - total_time_spent; + uint64_t sleep_time = 999; + if ( time_left < sleep_time ) sleep_time = time_left; + if ( iteration * 50 < sleep_time ) sleep_time = iteration * 50; + fatal_assert( sleep_time > 0 ); + struct timespec req; + req.tv_sec = 0; + req.tv_nsec = sleep_time * 1000000; + nanosleep( &req, NULL ); + continue; + } else { + perror( "write" ); + } + } + return 0; +} diff --git a/src/util/swrite.h b/src/util/swrite.h index e75bf7ed3..2f72782ef 100644 --- a/src/util/swrite.h +++ b/src/util/swrite.h @@ -33,6 +33,9 @@ #ifndef SWRITE_HPP #define SWRITE_HPP +#include + int swrite( int fd, const char *str, ssize_t len = -1 ); +int swrite_timeout( int fd, uint64_t timeout_ms, const char *str, ssize_t len = -1 ); #endif