mosh-server: Support timeouts on lost connectivity to network client.

Closes #690.
This commit is contained in:
John Hood
2015-10-18 16:27:31 -04:00
parent 4b8444988e
commit b742e958b6
10 changed files with 253 additions and 18 deletions
+37
View File
@@ -82,6 +82,43 @@ Locale-related environment variable to try as part of a fallback
environment, if the startup environment does not specify a character
set of UTF-8.
.SH ENVIRONMENT VARIABLES
These variables allow server-side configuration of Mosh's behavior.
They may be set by administrators in system login/rc files,
/etc/login.conf, or similar mechanisms, or users in their shell's
login/rc files. \fBmosh-server\fP passes these variables to the login
session and shell that it starts, but changing them there will have no
effect.
.TP
.B MOSH_SERVER_NETWORK_TMOUT
If this variable is set to a positive integer number, it specifies how
long (in seconds) \fBmosh-server\fP will wait to receive an update from the
client before exiting. Since \fPmosh\fP is very useful for mobile
clients with intermittent operation and connectivity, we suggest
setting this variable to a high value, such as 604800 (one week) or
2592000 (30 days). Otherwise, \fBmosh-server\fP will wait
indefinitely for a client to reappear. This variable is somewhat
similar to the \fBTMOUT\fP variable found in many Bourne shells.
However, it is not a login-session inactivity timeout; it only applies
to network connectivity.
.TP
.B MOSH_SERVER_SIGNAL_TMOUT
If this variable is set to a positive integer number, it specifies how
long (in seconds) \fBmosh-server\fP will ignore SIGUSR1 while waiting
to receive an update from the client. Otherwise, \fBSIGUSR1\fP will
always terminate \fBmosh-server\fP. Users and administrators may
implement scripts to clean up disconnected Mosh sessions. With this
variable set, a user or administrator can issue
.nf
$ pkill -SIGUSR1 mosh-server
.fi
to kill disconnected sessions without killing connected login
sessions.
.SH EXAMPLE
.nf
+7
View File
@@ -92,6 +92,13 @@ command to run server helper on remote machine (default: "mosh-server")
The server helper is unprivileged and can be installed in the user's
home directory.
This option can be used to set environment variables for the server by
using the
.BR env (1)
command to wrap the actual server command. See
.BR mosh-server (1)
for available environment variables.
.TP
.B \-\-ssh=\fICOMMAND\fP
OpenSSH command to remotely execute mosh-server on remote machine (default: "ssh")
+2
View File
@@ -266,6 +266,8 @@ if ( (not defined $colors)
$colors = 0;
}
$ENV{ 'MOSH_CLIENT_PID' } = $$; # We don't support this, but it's useful for test and debug.
my $pid = open(my $pipe, "-|");
die "$0: fork: $!\n" unless ( defined $pid );
if ( $pid == 0 ) { # child
+73 -8
View File
@@ -54,6 +54,7 @@
#include <netdb.h>
#include <time.h>
#include <sys/stat.h>
#include <inttypes.h>
#ifdef HAVE_UTMPX_H
#include <utmpx.h>
@@ -93,7 +94,9 @@ typedef Network::Transport< Terminal::Complete, Network::UserStream > ServerConn
static void serve( int host_fd,
Terminal::Complete &terminal,
ServerConnection &network );
ServerConnection &network,
long network_timeout,
long network_signaled_timeout );
static int run_server( const char *desired_ip, const char *desired_port,
const string &command_path, char *command_argv[],
@@ -340,6 +343,34 @@ 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, bool verbose, bool with_motd ) {
/* get network idle timeout */
long network_timeout = 0;
char *timeout_envar = getenv( "MOSH_SERVER_NETWORK_TMOUT" );
if ( timeout_envar && *timeout_envar ) {
errno = 0;
char *endptr;
network_timeout = strtol( timeout_envar, &endptr, 10 );
if ( *endptr != '\0' || ( network_timeout == 0 && errno == EINVAL ) ) {
fprintf( stderr, "MOSH_SERVER_NETWORK_TMOUT not a valid integer, ignoring\n" );
} else if ( network_timeout < 0 ) {
fprintf( stderr, "MOSH_SERVER_NETWORK_TMOUT is negative, ignoring\n" );
network_timeout = 0;
}
}
/* get network signaled idle timeout */
long network_signaled_timeout = 0;
char *signal_envar = getenv( "MOSH_SERVER_SIGNAL_TMOUT" );
if ( signal_envar && *signal_envar ) {
errno = 0;
char *endptr;
network_signaled_timeout = strtol( signal_envar, &endptr, 10 );
if ( *endptr != '\0' || ( network_signaled_timeout == 0 && errno == EINVAL ) ) {
fprintf( stderr, "MOSH_SERVER_SIGNAL_TMOUT not a valid integer, ignoring\n" );
} else if ( network_signaled_timeout < 0 ) {
fprintf( stderr, "MOSH_SERVER_SIGNAL_TMOUT is negative, ignoring\n" );
network_signaled_timeout = 0;
}
}
/* get initial window size */
struct winsize window_size;
if ( ioctl( STDIN_FILENO, TIOCGWINSZ, &window_size ) < 0 ||
@@ -505,7 +536,7 @@ static int run_server( const char *desired_ip, const char *desired_port,
#endif
try {
serve( master, terminal, *network );
serve( master, terminal, *network, network_timeout, network_signaled_timeout );
} catch ( const Network::NetworkException &e ) {
fprintf( stderr, "Network exception: %s\n",
e.what() );
@@ -531,12 +562,16 @@ 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 )
static void serve( int host_fd, Terminal::Complete &terminal, ServerConnection &network, long network_timeout, long network_signaled_timeout )
{
/* scale timeouts */
const uint64_t network_timeout_ms = static_cast<uint64_t>( network_timeout ) * 1000;
const uint64_t network_signaled_timeout_ms = static_cast<uint64_t>( network_signaled_timeout ) * 1000;
/* prepare to poll for events */
Select &sel = Select::get_instance();
sel.add_signal( SIGTERM );
sel.add_signal( SIGINT );
sel.add_signal( SIGUSR1 );
uint64_t last_remote_num = network.get_remote_state_num();
@@ -549,14 +584,31 @@ static void serve( int host_fd, Terminal::Complete &terminal, ServerConnection &
while ( 1 ) {
try {
static const uint64_t timeout_if_no_client = 60000;
int timeout = INT_MAX;
uint64_t now = Network::timestamp();
const int timeout_if_no_client = 60000;
int timeout = min( network.wait_time(), terminal.wait_time( now ) );
timeout = min( timeout, network.wait_time() );
timeout = min( timeout, terminal.wait_time( now ) );
if ( (!network.get_remote_state_num())
|| network.shutdown_in_progress() ) {
timeout = min( timeout, 5000 );
}
/*
* The server goes completely asleep if it has no remote peer.
* We may want to wake up sooner.
*/
if ( network_timeout_ms ) {
int64_t network_sleep = network_timeout_ms -
( now - network.get_latest_remote_state().timestamp );
if ( network_sleep < 0 ) {
network_sleep = 0;
} else if ( network_sleep > INT_MAX ) {
/* 24 days might be too soon. That's OK. */
network_sleep = INT_MAX;
}
timeout = min( timeout, static_cast<int>(network_sleep) );
}
/* poll for events */
sel.clear_fds();
@@ -679,7 +731,20 @@ static void serve( int host_fd, Terminal::Complete &terminal, ServerConnection &
}
}
if ( sel.any_signal() ) {
bool idle_shutdown = false;
if ( network_timeout_ms &&
network_timeout_ms <= time_since_remote_state ) {
idle_shutdown = true;
fprintf( stderr, "Network idle for %" PRIu64 " seconds.\n", time_since_remote_state / 1000 );
}
if ( sel.signal( SIGUSR1 ) ) {
if ( !network_signaled_timeout_ms || network_signaled_timeout_ms <= time_since_remote_state ) {
idle_shutdown = true;
fprintf( stderr, "Network idle for %"PRIu64" seconds when SIGUSR1 received\n", time_since_remote_state / 1000 );
}
}
if ( sel.any_signal() || idle_shutdown ) {
/* shutdown signal */
if ( network.has_remote_addr() && (!network.shutdown_in_progress()) ) {
network.start_shutdown();
@@ -736,8 +801,8 @@ static void serve( int host_fd, Terminal::Complete &terminal, ServerConnection &
}
if ( !network.get_remote_state_num()
&& time_since_remote_state >= uint64_t( timeout_if_no_client ) ) {
fprintf( stderr, "No connection within %d seconds.\n",
&& time_since_remote_state >= timeout_if_no_client ) {
fprintf( stderr, "No connection within %" PRIu64 " seconds.\n",
timeout_if_no_client / 1000 );
break;
}
+4 -2
View File
@@ -12,9 +12,11 @@ displaytests = \
emulation-80th-column.test \
emulation-back-tab.test \
emulation-multiline-scroll.test \
window-resize.test \
server-network-timeout.test \
server-signal-timeout.test \
unicode-combine-fallback-assert.test \
unicode-later-combining.test
unicode-later-combining.test \
window-resize.test
check_PROGRAMS = ocb-aes encrypt-decrypt base64
TESTS = ocb-aes encrypt-decrypt base64 $(displaytests)
+3
View File
@@ -5,6 +5,8 @@
# then captures screen with `tmux capture-pane`. Captures exitstatus
# of both and returns appropriate errors.
#
export MOSH_SERVER_PID=$PPID
if [ $# -lt 2 ]; then
printf "not enough args\n" >&2
exit 99
@@ -12,6 +14,7 @@ fi
testname=$1
shift
rm -f $testname.capture $testname.exitstatus
trap ":" TERM HUP QUIT # If the session closes on us, let the test we're running drive.
trap 'rv=$?; echo $rv > $testname.exitstatus; exit $rv' EXIT
# check for tmux
if [ -z "$TMUX_PANE" ]; then
+114
View File
@@ -0,0 +1,114 @@
#!/bin/sh
#
# This test checks for operation of the MOSH_SERVER_NETWORK_TIMEOUT variable.
# It does this by
# * setting the variable
# * killing the client (and its network traffic)
# * waiting server-side, for the server to die
# If it is killed, the test is successful.
# If it survives that long and the server is still around, the test fails.
# The client waits a bit longer than the server so that status can be collected
# properly.
#
TIMEOUT=10
fail()
{
printf "$@" 2>&1
exit 99
}
PATH=$PATH:.:$srcdir
# Top-level wrapper.
if [ $# -eq 0 ]; then
e2e-test $0 client baseline
exit
fi
# OK, we have arguments, we're one of the test hooks.
client()
{
case "$myname" in
server-network-timeout)
export MOSH_SERVER_NETWORK_TMOUT=$TIMEOUT;;
server-signal-timeout)
export MOSH_SERVER_SIGNAL_TMOUT=$TIMEOUT;;
*)
fail "unexpected test name %s\n" "$myname"
esac
shift
eval "$@"
# The client may be murdered. We need to expect that...
retval=$?
case $retval in
0|1)
fail "mosh-client had a normal exit\n";; # test condition failed
137)
# Aha, signal 9. Wait.
sleep $(( $TIMEOUT + 12 ))
exit 0
;;
*)
fail "unknown client wrapper failure, retval=%d\n" $retval
;;
esac
fail "client wrapper shouldnt get here\n"
}
baseline()
{
# check for our wonderful variable
if [ -z "$MOSH_SERVER_NETWORK_TMOUT" -a -z "$MOSH_SERVER_SIGNAL_TMOUT" ]; then
env
fail "Variable unset\n"
fi
# check for our client
if [ -z "$MOSH_CLIENT_PID" ]; then
env
fail "Client pid unavailable\n"
fi
if ! kill -0 $MOSH_CLIENT_PID; then
fail "mosh client is unexpectedly missing\n"
fi
# Set up for good return and cleanup on being killed
trap "echo got killed >&2; sleep 1; exit 0" SIGHUP SIGTERM
sleep 1
# Kill the client, to stop network traffic.
kill -KILL $MOSH_CLIENT_PID
case "$myname" in
server-network-timeout)
# Just wait. This is the hardest part.
sleep $(( $TIMEOUT + 7 ))
;;
server-signal-timeout)
# Wait for the timeout to expire.
sleep $(( $TIMEOUT + 2 ))
# Tell the server to go away.
kill -USR1 $MOSH_SERVER_PID
sleep 5
;;
*)
fail "unexpected test name %s\n" "$myname"
esac
# If we're still alive and the server is too, the test failed.
# XXX the server is getting killed and we're getting here anyway.
# Exit with error only if server is still around.
! kill -0 $MOSH_SERVER_PID
exit
}
myname="$(basename $0 .test)"
case $1 in
baseline|variant)
baseline;;
client)
client "$@";;
*)
fail "unknown test argument %s\n" $1;;
esac
+1
View File
@@ -0,0 +1 @@
server-network-timeout.test
-1
View File
@@ -43,5 +43,4 @@ void Select::handle_signal( int signum )
Select &sel = get_instance();
sel.got_signal[ signum ] = 1;
sel.got_any_signal = 1;
}
+12 -7
View File
@@ -58,8 +58,6 @@ public:
private:
Select()
: max_fd( -1 )
, got_any_signal( 0 )
/* These initializations are not used; they are just
here to appease -Weffc++. */
, all_fds( dummy_fd_set )
@@ -124,7 +122,6 @@ public:
memcpy( &read_fds, &all_fds, sizeof( read_fds ) );
memcpy( &error_fds, &all_fds, sizeof( error_fds ) );
clear_got_signal();
got_any_signal = 0;
#ifdef HAVE_PSELECT
struct timespec ts;
@@ -185,16 +182,25 @@ public:
return FD_ISSET( fd, &error_fds );
}
bool signal( int signum ) const
/* This method consumes a signal notification. */
bool signal( int signum )
{
fatal_assert( signum >= 0 );
fatal_assert( signum <= MAX_SIGNAL_NUMBER );
return got_signal[ signum ];
/* XXX This requires a guard against concurrent signals. */
bool rv = got_signal[ signum ];
got_signal[ signum ] = 0;
return rv;
}
/* This method does not consume signal notifications. */
bool any_signal( void ) const
{
return got_any_signal;
bool rv = false;
for (int i = 0; i < MAX_SIGNAL_NUMBER; i++) {
rv |= got_signal[ i ];
}
return rv;
}
private:
@@ -206,7 +212,6 @@ private:
/* We assume writes to these ints are atomic, though we also try to mask out
concurrent signal handlers. */
int got_any_signal;
int got_signal[ MAX_SIGNAL_NUMBER + 1 ];
fd_set all_fds, read_fds, error_fds;