Skip to content

Commit

Permalink
mosh-server: Support timeouts on lost connectivity to network client.
Browse files Browse the repository at this point in the history
Closes #690.
  • Loading branch information
cgull committed Nov 24, 2015
1 parent 4b84449 commit b742e95
Show file tree
Hide file tree
Showing 10 changed files with 253 additions and 18 deletions.
37 changes: 37 additions & 0 deletions man/mosh-server.1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions man/mosh.1
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 2 additions & 0 deletions scripts/mosh.pl
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ sub predict_check {
$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
Expand Down
81 changes: 73 additions & 8 deletions src/frontend/mosh-server.cc
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
#include <netdb.h>
#include <time.h>
#include <sys/stat.h>
#include <inttypes.h>

#ifdef HAVE_UTMPX_H
#include <utmpx.h>
Expand Down Expand Up @@ -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[],
Expand Down Expand Up @@ -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 ||
Expand Down Expand Up @@ -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() );
Expand All @@ -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();

Expand All @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
}
Expand Down
6 changes: 4 additions & 2 deletions src/tests/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions src/tests/e2e-test-server
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
# 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
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
Expand Down
114 changes: 114 additions & 0 deletions src/tests/server-network-timeout.test
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions src/tests/server-signal-timeout.test
1 change: 0 additions & 1 deletion src/util/select.cc
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,4 @@ void Select::handle_signal( int signum )

Select &sel = get_instance();
sel.got_signal[ signum ] = 1;
sel.got_any_signal = 1;
}
Loading

0 comments on commit b742e95

Please sign in to comment.