Better, more flexible handling for IPv4/IPv6.
Add --family=auto/all. Attempt connects to all available addresses. Working error handling. Fixes #629.
This commit is contained in:
+30
-6
@@ -64,6 +64,11 @@ pass between client and server. By default, \fBmosh\fP uses the ports
|
|||||||
between 60000 and 61000, but allows the user to request a particular
|
between 60000 and 61000, but allows the user to request a particular
|
||||||
UDP port instead.
|
UDP port instead.
|
||||||
|
|
||||||
|
Currently, \fBmosh\fP has limited support for IPv6, dual-stack
|
||||||
|
networks, and servers with multiple addresses. At session start, it
|
||||||
|
will select a single IPv4 or IPv6 server address to connect to for the
|
||||||
|
lifetime of the session.
|
||||||
|
|
||||||
\fBmosh\fP will do its best to arrange a UTF-8 character set locale on
|
\fBmosh\fP will do its best to arrange a UTF-8 character set locale on
|
||||||
the client and server. The client must have locale-related environment
|
the client and server. The client must have locale-related environment
|
||||||
variables that specify UTF-8. \fBmosh\fP will pass these client
|
variables that specify UTF-8. \fBmosh\fP will pass these client
|
||||||
@@ -128,17 +133,36 @@ Synonym for \-\-predict=always
|
|||||||
Synonym for \-\-predict=never
|
Synonym for \-\-predict=never
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B --family=\fIFAMILY\fP
|
.B \-\-family=inet
|
||||||
Force the use of a particular address family, which defaults to `inet'
|
Only use IPv4 for the SSH connection and Mosh session.
|
||||||
(IPv4), and can also be `inet6' (IPv6; requires IO::Socket::IP or
|
|
||||||
IO::Socket::INET6).
|
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B -4
|
.B \-\-family=inet6
|
||||||
|
Only use IPv6 for the SSH connection and Mosh session. This and the
|
||||||
|
following two dual-stack modes require Perl's IO::Socket::IP or
|
||||||
|
IO::Socket::INET6 modules.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-\-family=all
|
||||||
|
Choose an address from all available IPv4 or IPv6 address, even for
|
||||||
|
dual-stack hosts. This is the most convenient option, but requires
|
||||||
|
dual-stack connectivity when roaming with dual-stack servers. This is
|
||||||
|
the default.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-\-family=auto
|
||||||
|
Autodetect IPv4 or IPv6 for hosts that only have addresses
|
||||||
|
in a single family. Hosts with both IPv4 and IPv6 addresses will
|
||||||
|
raise an error, and require re-invocation of \fBmosh\fP with another
|
||||||
|
.B \-\-family
|
||||||
|
option.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-4
|
||||||
Synonym for \-\-family=inet
|
Synonym for \-\-family=inet
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B -6
|
.B \-6
|
||||||
Synonym for \-\-family=inet6
|
Synonym for \-\-family=inet6
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
|
|||||||
+88
-25
@@ -35,6 +35,19 @@ use strict;
|
|||||||
use Getopt::Long;
|
use Getopt::Long;
|
||||||
use IO::Socket;
|
use IO::Socket;
|
||||||
use Text::ParseWords;
|
use Text::ParseWords;
|
||||||
|
use Socket qw( :addrinfo IPPROTO_IP IPPROTO_IPV6 IPPROTO_TCP IPPROTO_UDP );
|
||||||
|
use Errno qw(EINTR);
|
||||||
|
use POSIX qw(_exit);
|
||||||
|
|
||||||
|
|
||||||
|
my $have_ipv6 = eval {
|
||||||
|
require IO::Socket::IP;
|
||||||
|
IO::Socket::IP->import('-register');
|
||||||
|
1;
|
||||||
|
} || eval {
|
||||||
|
require IO::Socket::INET6;
|
||||||
|
1;
|
||||||
|
};
|
||||||
|
|
||||||
$|=1;
|
$|=1;
|
||||||
|
|
||||||
@@ -45,7 +58,7 @@ my $predict = undef;
|
|||||||
|
|
||||||
my $bind_ip = undef;
|
my $bind_ip = undef;
|
||||||
|
|
||||||
my $family = 'inet';
|
my $family = 'all';
|
||||||
my $port_request = undef;
|
my $port_request = undef;
|
||||||
|
|
||||||
my @ssh = ('ssh');
|
my @ssh = ('ssh');
|
||||||
@@ -71,8 +84,10 @@ qq{Usage: $0 [options] [--] [user@]host [command...]
|
|||||||
-n --predict=never never use local echo
|
-n --predict=never never use local echo
|
||||||
--predict=experimental aggressively echo even when incorrect
|
--predict=experimental aggressively echo even when incorrect
|
||||||
|
|
||||||
-4 --family=inet use IPv4 only [default]
|
-4 --family=inet use IPv4 only
|
||||||
-6 --family=inet6 use IPv6 only
|
-6 --family=inet6 use IPv6 only
|
||||||
|
--family=auto autodetect network type for single-family hosts
|
||||||
|
--family=all try all network types [default]
|
||||||
-p PORT[:PORT2]
|
-p PORT[:PORT2]
|
||||||
--port=PORT[:PORT2] server-side UDP port or range
|
--port=PORT[:PORT2] server-side UDP port or range
|
||||||
--bind-server={ssh|any|IP} ask the server to reply from an IP address
|
--bind-server={ssh|any|IP} ask the server to reply from an IP address
|
||||||
@@ -190,36 +205,83 @@ if ( ! defined $fake_proxy ) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
my ( $host, $port ) = @ARGV;
|
my ( $host, $port ) = @ARGV;
|
||||||
|
my $err;
|
||||||
|
my @res;
|
||||||
|
my $af;
|
||||||
|
|
||||||
use Errno qw(EINTR);
|
$family = lc( $family );
|
||||||
my $have_ipv6 = eval {
|
# Handle IPv4-only Perl installs.
|
||||||
require IO::Socket::IP;
|
|
||||||
IO::Socket::IP->import('-register');
|
|
||||||
1;
|
|
||||||
} || eval {
|
|
||||||
require IO::Socket::INET6;
|
|
||||||
1;
|
|
||||||
};
|
|
||||||
use POSIX qw(_exit);
|
|
||||||
|
|
||||||
# Report failure if IPv6 needed and not available.
|
|
||||||
if (lc($family) eq "inet6") {
|
|
||||||
if (!$have_ipv6) {
|
if (!$have_ipv6) {
|
||||||
|
# Report failure if IPv6 needed and not available.
|
||||||
|
if (defined($family) && $family eq "inet6") {
|
||||||
die "$0: IPv6 sockets not available in this Perl install\n";
|
die "$0: IPv6 sockets not available in this Perl install\n";
|
||||||
}
|
}
|
||||||
|
# Force IPv4.
|
||||||
|
$family = "inet";
|
||||||
}
|
}
|
||||||
|
|
||||||
# Resolve hostname and connect
|
# If the user selected a specific family, parse it.
|
||||||
|
if ( defined( $family ) && ( $family ne "auto" && $family ne "all" )) {
|
||||||
|
# Choose an address family, or cause pain.
|
||||||
my $afstr = 'AF_' . uc( $family );
|
my $afstr = 'AF_' . uc( $family );
|
||||||
my $af = eval { IO::Socket->$afstr } or die "$0: Invalid family $family\n";
|
$af = eval { IO::Socket->$afstr } or die "$0: Invalid family $family\n";
|
||||||
my $sock = IO::Socket->new( Domain => $af,
|
}
|
||||||
Family => $af,
|
|
||||||
PeerHost => $host,
|
|
||||||
PeerPort => $port,
|
|
||||||
Proto => "tcp" )
|
|
||||||
or die "$0: Could not connect to $host: $@\n";
|
|
||||||
|
|
||||||
|
# First try the address as a numeric.
|
||||||
|
my %hints = ( flags => AI_NUMERICHOST,
|
||||||
|
socktype => SOCK_STREAM,
|
||||||
|
protocol => IPPROTO_TCP );
|
||||||
|
if ( defined( $af )) {
|
||||||
|
$hints{family} = $af;
|
||||||
|
}
|
||||||
|
( $err, @res ) = getaddrinfo( $host, $port, \%hints );
|
||||||
|
if ( $err ) {
|
||||||
|
# Get canonical name for this host.
|
||||||
|
$hints{flags} = AI_CANONNAME;
|
||||||
|
( $err, @res ) = getaddrinfo( $host, $port, \%hints );
|
||||||
|
die "$0: could not get canonical name for $host: ${err}\n" if $err;
|
||||||
|
# Then get final resolution of the canonical name.
|
||||||
|
$hints{flags} = undef;
|
||||||
|
my @newres;
|
||||||
|
( $err, @newres ) = getaddrinfo( $res[0]{canonname}, $port, \%hints );
|
||||||
|
die "$0: could not resolve canonical name ${res[0]{canonname}} for ${host}: ${err}\n" if $err;
|
||||||
|
@res = @newres;
|
||||||
|
}
|
||||||
|
|
||||||
|
# If v4 or v6 was specified, reduce the host list.
|
||||||
|
if ( defined( $af )) {
|
||||||
|
@res = grep {$_->{family} == $af} @res;
|
||||||
|
} elsif ( $family ne "all" ) {
|
||||||
|
# If v4/v6/all were not specified, verify that this host only has one address family available.
|
||||||
|
for my $ai ( @res ) {
|
||||||
|
if ( !defined( $af )) {
|
||||||
|
$af = $ai->{family};
|
||||||
|
} else {
|
||||||
|
die "$0: host has both IPv4 and IPv6 addresses, use --family to specify behavior\n"
|
||||||
|
if $af != $ai->{family};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Now try and connect to something.
|
||||||
|
my $sock;
|
||||||
|
my $addr_string;
|
||||||
|
my $service;
|
||||||
|
for my $ai ( @res ) {
|
||||||
|
( $err, $addr_string, $service ) = getnameinfo( $ai->{addr}, NI_NUMERICHOST );
|
||||||
|
next if $err;
|
||||||
|
if ( $sock = IO::Socket->new( Domain => $ai->{family},
|
||||||
|
Family => $ai->{family},
|
||||||
|
PeerHost => $addr_string,
|
||||||
|
PeerPort => $port,
|
||||||
|
Proto => "tcp" )) {
|
||||||
print STDERR "MOSH IP ", $sock->peerhost, "\n";
|
print STDERR "MOSH IP ", $sock->peerhost, "\n";
|
||||||
|
last;
|
||||||
|
} else {
|
||||||
|
$err = $@;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
die "$0: Could not connect to ${host}, last tried ${addr_string}: ${err}\n" if !$sock;
|
||||||
|
|
||||||
# Act like netcat
|
# Act like netcat
|
||||||
binmode($sock);
|
binmode($sock);
|
||||||
@@ -298,8 +360,9 @@ if ( $pid == 0 ) { # child
|
|||||||
exec( $server, @server );
|
exec( $server, @server );
|
||||||
die "Cannot exec $server: $!\n";
|
die "Cannot exec $server: $!\n";
|
||||||
}
|
}
|
||||||
my $quoted_self = shell_quote( $0, "--family=$family" );
|
|
||||||
exec @ssh, '-S', 'none', '-o', "ProxyCommand=$quoted_self --fake-proxy -- %h %p", '-n', '-tt', $userhost, '--', "$server " . shell_quote( @server );
|
my $quoted_proxy_command = shell_quote( $0, "--family=$family" );
|
||||||
|
exec @ssh, '-S', 'none', '-o', "ProxyCommand=$quoted_proxy_command --fake-proxy -- %h %p", '-n', '-tt', $userhost, '--', "$server " . shell_quote( @server );
|
||||||
die "Cannot exec ssh: $!\n";
|
die "Cannot exec ssh: $!\n";
|
||||||
} else { # parent
|
} else { # parent
|
||||||
my ( $ip, $port, $key );
|
my ( $ip, $port, $key );
|
||||||
|
|||||||
Reference in New Issue
Block a user