diff --git a/Makefile.am b/Makefile.am index fcd08da67..1b7fa0942 100644 --- a/Makefile.am +++ b/Makefile.am @@ -53,7 +53,7 @@ clean-local: cppcheck: cppcheck $(CPPCHECK_FLAGS) -include src/include/config.h -I src/include \ -I src/crypto -I src/frontend -I src/network -I src/protobufs \ - -I src/statesync -I src/terminal -I src/util \ + -I src/statesync -I src/terminal -I src/util -I src/agent \ -I /usr/include -I /usr/include/google/protobuf -I/usr/include/openssl \ src diff --git a/configure.ac b/configure.ac index ee70c7b86..d3b12c7d7 100644 --- a/configure.ac +++ b/configure.ac @@ -23,8 +23,16 @@ AS_IF([test x"$PROTOC" = x], m4_ifdef([AM_PROG_AR], [AM_PROG_AR]) # Protobuf 3.6+ requires C++11. -AS_IF([pkg-config --atleast-version 3.6.0 protobuf], - [AX_CXX_COMPILE_STDCXX([11])]) +# Protobuf 22+ requires C++14. +# Protobuf 23+ requires C++17. +AS_IF( + [pkg-config --atleast-version 23.0.0 protobuf], + [AX_CXX_COMPILE_STDCXX([17])], + [pkg-config --atleast-version 22.0.0 protobuf], + [AX_CXX_COMPILE_STDCXX([14])], + [pkg-config --atleast-version 3.6.0 protobuf], + [AX_CXX_COMPILE_STDCXX([11])] +) WARNING_CXXFLAGS="" PICKY_CXXFLAGS="" @@ -209,6 +217,13 @@ AS_IF([test x"$enable_syslog" != xno], [AC_MSG_WARN([Unable to find syslog.h.])], [AC_MSG_ERROR([--enable-syslog was given but syslog.h was not found.])])])]) +AC_ARG_ENABLE([agent-forwarding], + [AS_HELP_STRING([--disable-agent-forwarding], [Build support for SSH agent forwarding @<:@yes@:>@])], + [enable_agent_forwarding="$enableval"], + [enable_agent_forwarding="yes"]) +AS_IF([test x"$enable_agent_forwarding" != xno], + [AC_DEFINE([SUPPORT_AGENT_FORWARDING], [1], [Define to build support for SSH agent forwarding.])]) + # Checks for libraries. AC_ARG_ENABLE([static-libraries], [AS_HELP_STRING([--enable-static-libraries], [Enable all static linking options below @<:@no@:>@])]) @@ -260,6 +275,7 @@ AC_SEARCH_LIBS([clock_gettime], [rt]) # Checks for header files. AC_CHECK_HEADERS(m4_normalize([ + errno.h fcntl.h langinfo.h limits.h @@ -287,6 +303,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]) AC_CHECK_HEADERS([memory tr1/memory]) # Checks for typedefs, structures, and compiler characteristics. @@ -588,6 +606,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 a77a8d305..342caabd1 100644 --- a/man/mosh.1 +++ b/man/mosh.1 @@ -114,6 +114,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 \-\-ssh-pty\fP .B \-\-no-ssh-pty\fP @@ -134,6 +139,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.pl b/scripts/mosh.pl index f224ef348..48cb61cc4 100755 --- a/scripts/mosh.pl +++ b/scripts/mosh.pl @@ -79,6 +79,8 @@ BEGIN my $term_init = 1; +my $forward_agent = 0; + my $localhost = undef; my $ssh_pty = 1; @@ -118,6 +120,8 @@ BEGIN (example: "ssh -p 2222") (default: "ssh") +-A --forward-agent enable ssh agent forwarding + --no-ssh-pty do not allocate a pseudo tty on ssh connection --no-init do not send terminal initialization string @@ -152,6 +156,10 @@ sub predict_check { } } +# Make GetOptions behave more like traditional UNIX parsers. +# As a side effect, parse short options case-sensitively (which we need for -A). +Getopt::Long::Configure( "bundling" ); + GetOptions( 'client=s' => \$client, 'server=s' => \$server, 'predict=s' => \$predict, @@ -164,6 +172,8 @@ sub predict_check { '6' => sub { $family = 'inet6' }, 'p=s' => \$port_request, 'ssh=s' => sub { @ssh = shellwords($_[1]); }, + 'A' => \$forward_agent, + 'forward-agent!' => \$forward_agent, 'ssh-pty!' => \$ssh_pty, 'init!' => \$term_init, 'local' => \$localhost, @@ -376,6 +386,10 @@ sub predict_check { } my @server = ( 'new' ); + if ( $forward_agent ) { + push @server, ( '-A' ); + } + push @server, ( '-c', $colors ); push @server, @bind_arguments; @@ -462,7 +476,14 @@ sub predict_check { $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 0c2e9fe1a..64aa0a4b2 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1 +1 @@ -SUBDIRS = include protobufs util crypto terminal network statesync frontend examples tests fuzz +SUBDIRS = include protobufs util crypto terminal network statesync agent frontend examples tests fuzz diff --git a/src/agent/Makefile.am b/src/agent/Makefile.am new file mode 100644 index 000000000..6cda339b9 --- /dev/null +++ b/src/agent/Makefile.am @@ -0,0 +1,7 @@ +AM_CPPFLAGS = -I$(srcdir)/../util -I$(srcdir)/../crypto -I$(srcdir)/../network -I../protobufs $(TINFO_CFLAGS) $(protobuf_CFLAGS) +AM_CXXFLAGS = $(WARNING_CXXFLAGS) $(PICKY_CXXFLAGS) $(HARDEN_CFLAGS) $(MISC_CXXFLAGS) $(CODE_COVERAGE_CXXFLAGS) +AM_LDFLAGS = $(HARDEN_LDFLAGS) $(CODE_COVERAGE_LIBS) + +noinst_LIBRARIES = libmoshagent.a + +libmoshagent_a_SOURCES = agent.cc agent.h 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 1aa83fd3f..d1b4d54c3 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) $(CRYPTO_CFLAGS) +AM_CPPFLAGS = -I$(srcdir)/../statesync -I$(srcdir)/../terminal -I$(srcdir)/../agent -I$(srcdir)/../network -I$(srcdir)/../crypto -I../protobufs -I$(srcdir)/../util $(TINFO_CFLAGS) $(protobuf_CFLAGS) $(CRYPTO_CFLAGS) AM_CXXFLAGS = $(WARNING_CXXFLAGS) $(PICKY_CXXFLAGS) $(HARDEN_CFLAGS) $(MISC_CXXFLAGS) $(CODE_COVERAGE_CXXFLAGS) AM_LDFLAGS = $(HARDEN_LDFLAGS) $(CODE_COVERAGE_LIBS) -LDADD = ../crypto/libmoshcrypto.a ../network/libmoshnetwork.a ../statesync/libmoshstatesync.a ../terminal/libmoshterminal.a ../util/libmoshutil.a ../protobufs/libmoshprotos.a -lm $(TINFO_LIBS) $(protobuf_LIBS) $(CRYPTO_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) $(CRYPTO_LIBS) mosh_server_LDADD = $(LDADD) diff --git a/src/frontend/mosh-client.cc b/src/frontend/mosh-client.cc index 5b1a919e4..01dcc1066 100644 --- a/src/frontend/mosh-client.cc +++ b/src/frontend/mosh-client.cc @@ -114,6 +114,8 @@ int main( int argc, char *argv[] ) /* Detect edge case */ fatal_assert( argc > 0 ); + bool forward_agent = false; + /* Get arguments */ for ( int i = 1; i < argc; i++ ) { if ( 0 == strcmp( argv[ i ], "--help" ) ) { @@ -127,11 +129,14 @@ int main( int argc, char *argv[] ) } int opt; - while ( (opt = getopt( argc, argv, "#:cv" )) != -1 ) { + while ( (opt = getopt( argc, argv, "#:cvA" )) != -1 ) { switch ( opt ) { case '#': // Ignore the original arguments to mosh wrapper break; + case 'A': + forward_agent = true; + break; case 'c': print_colorcount(); exit( 0 ); @@ -191,7 +196,7 @@ int main( int argc, char *argv[] ) bool success = false; try { - STMClient client( ip, desired_port, key.c_str(), predict_mode, verbose, predict_overwrite ); + STMClient client( ip, desired_port, key.c_str(), predict_mode, verbose, predict_overwrite, forward_agent ); client.init(); try { diff --git a/src/frontend/mosh-server.cc b/src/frontend/mosh-server.cc index f6d2ff3bc..099c20303 100644 --- a/src/frontend/mosh-server.cc +++ b/src/frontend/mosh-server.cc @@ -86,6 +86,7 @@ #include "locale_utils.h" #include "pty_compat.h" #include "select.h" +#include "agent.h" #include "timestamp.h" #include "fatal_assert.h" @@ -101,11 +102,13 @@ static void serve( int host_fd, Terminal::Complete &terminal, ServerConnection &network, long network_timeout, - long network_signaled_timeout ); + long network_signaled_timeout, + Agent::ProxyAgent &agent ); static int run_server( const char *desired_ip, const char *desired_port, const string &command_path, char *command_argv[], - const int colors, unsigned int verbose, bool with_motd ); + const int colors, unsigned int verbose, bool with_motd, + bool with_agent_fwd ); static void print_version( FILE *file ) @@ -182,6 +185,7 @@ int main( int argc, char *argv[] ) string command_path; char **command_argv = NULL; int colors = 0; + bool with_agent_fwd = false; unsigned int verbose = 0; /* don't close stdin/stdout/stderr */ /* Will cause mosh-server not to correctly detach on old versions of sshd. */ list locale_vars; @@ -210,7 +214,7 @@ 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 ) { /* * This undocumented option does nothing but eat its argument. @@ -251,6 +255,9 @@ int main( int argc, char *argv[] ) case 'l': locale_vars.push_back( string( optarg ) ); break; + case 'A': + with_agent_fwd = true; + break; default: /* don't die on unknown options */ print_usage( stderr, argv[ 0 ] ); @@ -364,7 +371,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\n", e.what() ); @@ -378,7 +385,8 @@ int main( int argc, char *argv[] ) static int run_server( const char *desired_ip, const char *desired_port, const string &command_path, char *command_argv[], - const int colors, unsigned int verbose, bool with_motd ) { + const int colors, unsigned int verbose, bool with_motd, + bool with_agent_fwd ) { /* get network idle timeout */ long network_timeout = 0; char *timeout_envar = getenv( "MOSH_SERVER_NETWORK_TMOUT" ); @@ -478,6 +486,13 @@ static int run_server( const char *desired_ip, const char *desired_port, exit( 0 ); } + /* 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; /* close file descriptors */ @@ -558,6 +573,14 @@ static 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" ); @@ -621,7 +644,7 @@ static int run_server( const char *desired_ip, const char *desired_port, #endif try { - serve( master, terminal, *network, network_timeout, network_signaled_timeout ); + serve( master, terminal, *network, network_timeout, network_signaled_timeout, agent ); } catch ( const Network::NetworkException &e ) { fprintf( stderr, "Network exception: %s\n", e.what() ); @@ -645,7 +668,7 @@ static int run_server( const char *desired_ip, const char *desired_port, return 0; } -static void serve( int host_fd, Terminal::Complete &terminal, ServerConnection &network, long network_timeout, long network_signaled_timeout ) +static void serve( int host_fd, Terminal::Complete &terminal, ServerConnection &network, long network_timeout, long network_signaled_timeout, Agent::ProxyAgent &agent ) { /* scale timeouts */ const uint64_t network_timeout_ms = static_cast( network_timeout ) * 1000; @@ -677,6 +700,8 @@ static void serve( int host_fd, Terminal::Complete &terminal, ServerConnection & bool child_released = false; + agent.attach_oob(network.oob()); + while ( true ) { try { static const uint64_t timeout_if_no_client = 60000; @@ -715,6 +740,8 @@ static void serve( int host_fd, Terminal::Complete &terminal, ServerConnection & sel.add_fd( host_fd ); } + network.oob()->pre_poll(); + int active_fds = sel.select( timeout ); if ( active_fds < 0 ) { perror( "select" ); @@ -840,6 +867,7 @@ static void serve( int host_fd, Terminal::Complete &terminal, ServerConnection & /* 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 { terminal_to_host += terminal.act( string( buf, bytes_read ) ); @@ -871,6 +899,7 @@ static void serve( int host_fd, Terminal::Complete &terminal, ServerConnection & if ( sel.any_signal() || idle_shutdown ) { /* shutdown signal */ if ( network.has_remote_addr() && (!network.shutdown_in_progress()) ) { + network.oob()->shutdown(); network.start_shutdown(); } else { break; @@ -915,15 +944,25 @@ static void serve( int host_fd, Terminal::Complete &terminal, ServerConnection & && time_since_remote_state >= timeout_if_no_client ) { fprintf( stderr, "No connection within %llu seconds.\n", static_cast( 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\n", e.what() ); spin(); } catch ( const Crypto::CryptoException &e ) { if ( e.fatal ) { + network.oob()->shutdown(); throw; } else { fprintf( stderr, "Crypto exception: %s\n", e.what() ); diff --git a/src/frontend/stmclient.cc b/src/frontend/stmclient.cc index e6e9b3a31..1c3ecf0cd 100644 --- a/src/frontend/stmclient.cc +++ b/src/frontend/stmclient.cc @@ -60,6 +60,7 @@ #include "pty_compat.h" #include "select.h" #include "timestamp.h" +#include "agent.h" #include "networktransport-impl.h" @@ -434,6 +435,10 @@ bool STMClient::main( void ) } #endif + Agent::ProxyAgent agent( false, ! forward_agent ); + + agent.attach_oob(network->oob()); + /* prepare to poll for events */ Select &sel = Select::get_instance(); @@ -459,6 +464,8 @@ bool 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" ); @@ -486,7 +493,11 @@ bool 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(); + } else { + /* XXX: cannot be reached? preserved to avoid non-identity transformation during rebase */ + network->oob()->shutdown(); } } @@ -507,6 +518,7 @@ bool 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(); } } @@ -535,6 +547,7 @@ bool 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 { @@ -546,8 +559,12 @@ bool STMClient::main( void ) overlays.get_notification_engine().set_notification_string( L"" ); } + network->oob()->post_poll(); + network->tick(); + network->oob()->post_tick(); + string & send_error = network->get_send_error(); if ( !send_error.empty() ) { overlays.get_notification_engine().set_network_error( send_error ); diff --git a/src/frontend/stmclient.h b/src/frontend/stmclient.h index c1440c3ac..98fa89138 100644 --- a/src/frontend/stmclient.h +++ b/src/frontend/stmclient.h @@ -48,6 +48,7 @@ class STMClient { std::string ip; std::string port; std::string key; + bool forward_agent; int escape_key; int escape_pass_key; @@ -87,9 +88,9 @@ 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, unsigned int s_verbose, const char *predict_overwrite ) + STMClient( const char *s_ip, const char *s_port, const char *s_key, const char *predict_mode, unsigned int s_verbose, const char *predict_overwrite, bool s_forward_agent ) : ip( s_ip ? s_ip : "" ), port( s_port ? s_port : "" ), - key( s_key ? s_key : "" ), + key( s_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 021d2415c..cbb220060 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-impl.h networktransport.h transportfragment.cc transportfragment.h transportsender-impl.h transportsender.h transportstate.h compressor.cc compressor.h +libmoshnetwork_a_SOURCES = network.cc network.h networktransport-impl.h networktransport.h transportfragment.cc transportfragment.h transportsender-impl.h transportsender.h transportstate.h compressor.cc compressor.h outofband.h outofband.cc diff --git a/src/network/networktransport-impl.h b/src/network/networktransport-impl.h index e6325093f..dbd600b43 100644 --- a/src/network/networktransport-impl.h +++ b/src/network/networktransport-impl.h @@ -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 115ff37e1..1b1ac6071 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..b25d3d4d7 --- /dev/null +++ b/src/network/outofband.cc @@ -0,0 +1,325 @@ +/* + 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; +} + +OutOfBandPlugin::~OutOfBandPlugin() { } diff --git a/src/network/outofband.h b/src/network/outofband.h new file mode 100644 index 000000000..01c53acb8 --- /dev/null +++ b/src/network/outofband.h @@ -0,0 +1,135 @@ +/* + 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; + virtual ~OutOfBandPlugin() = 0; + + friend class OutOfBand; + }; + +} + +#endif diff --git a/src/network/transportsender-impl.h b/src/network/transportsender-impl.h index 518f1768b..10c761a25 100644 --- a/src/network/transportsender-impl.h +++ b/src/network/transportsender-impl.h @@ -63,6 +63,7 @@ TransportSender::TransportSender( Connection *s_connection, MyState &in pending_data_ack( false ), SEND_MINDELAY( 8 ), last_heard( 0 ), + oob_ctl(), prng(), mindelay_clock( -1 ) { @@ -98,14 +99,19 @@ 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 = std::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 = std::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 ) ) { @@ -187,7 +193,7 @@ void TransportSender::tick( void ) } if ( diff.empty() ) { - if ( (now >= next_ack_time) ) { + if ( (now >= next_ack_time) || ((now >= next_send_time) && oob()->has_output()) ) { send_empty_ack(); mindelay_clock = uint64_t( -1 ); } @@ -207,7 +213,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; @@ -329,6 +335,10 @@ void TransportSender::send_in_fragments( const string & diff, uint64_t 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 db55713fd..3f797b231 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" namespace Network { using std::list; @@ -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 e42ae65b7..6576da6b9 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) $(CODE_COVERAGE_CXXFLAGS) -Wno-error 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 b08e7b05a..9ac2236e8 100644 --- a/src/protobufs/transportinstruction.proto +++ b/src/protobufs/transportinstruction.proto @@ -15,4 +15,6 @@ message Instruction { optional bytes diff = 6; optional bytes chaff = 7; + + optional bytes oob = 8; } diff --git a/src/util/swrite.cc b/src/util/swrite.cc index ada96f59b..39c358988 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 ) { @@ -52,3 +58,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