#!/usr/bin/perl -w use strict; use IO::Socket; use IO::Select; use LWP::UserAgent; use Getopt::Long; use vars qw( %CONF $VERSION ); my %listen; # listening sockets my %socket; # tunnel sockets $VERSION = 0.03; # default configuration %CONF = ( 'user-agent' => "connect-tunnel/$VERSION", ); =head1 NAME connect-tunnel - Create CONNECT tunnels through HTTP proxies =head1 SYNOPSIS B S<[ B<-v> ]> S<[ B<-A> I ]> S<[ B<-P> I ]> B<-T> I S<[ B<-T> I ]> =head1 DESCRIPTION B sets up tunneled connections to external hosts by redirecting connections to local ports towards thoses hosts/ports through a HTTP proxy. B makes use of the HTTP C method to ask the proxy to create a tunnel to an outside server. Be aware that some proxies are set up to deny some outside tunnels (either to ports other than 443 or outside a specified set of outside hosts). =head1 OPTIONS The program follows the usual GNU command line syntax, with long options starting with two dashes. =over 4 =item B<-A>, B<--proxy-authentication> I Proxy authentication information. Please note that all the authentication schemes supported by LWP::UserAgent are supported (we use an LWP::UserAgent). This means we also support NTLM, since it is supported as from libwww-perl 5.66. =item B<-L>, B<--local-only> Create the tunnels so that they will only listen on C. Thus, only connections originating from the machine that runs B will be accepted. That was the default behaviour in B version 0.02. =item B<-P>, B<--proxy> I[I<:port>] The proxy is required to connect the tunnels. If no port is given, 8080 is used by default. See also L. =item B<-T>, B<--tunnel> I Specifies that the given I on the local host is to be forwarded to the given I and I on the remote side. This works by allocating a socket to listen to I on the local side, and whenever a connection is made to this I, the connection is forwarded through the proxy, and a connection is made to the remote I at port I. On Unix systems, only root can forward privileged ports. Note that you can setup tunnels to multiple destinations, by using the B<--tunnel> option several times. =item B<-U>, B<--user-agent> I Specify User-Agent value to send in HTTP requests. The default is to send C>. =item B<-v>, B<--verbose> Verbose output. This option can be used several times for more verbose output. =back =cut # # get and check the options # GetOptions( \%CONF, "verbose|v+", "tunnel|T=s@", "proxy|P=s", "proxy-authentication|A=s", "local-only|L", "user-agent|U=s" ); # create a homebrewed user agent class { my ( $user, $pass ); ( $user, $pass ) = split ':', $CONF{'proxy-authentication'} if defined $CONF{'proxy-authentication'}; package TunnelAgent; use vars qw( @ISA ); @ISA = qw(LWP::UserAgent); sub get_basic_credentials { return ( $user, $pass ); } } my $ua = TunnelAgent->new( agent => $CONF{'user-agent'}, env_proxy => 1, ); # check for a proxy if ( $CONF{proxy} ) { $CONF{proxy} .= ":8080" if not $CONF{proxy} =~ /:/; $ua->proxy( http => "http://$CONF{proxy}/" ); } die "--proxy option required$/" unless $ua->proxy( 'http' ); # create the tunnels entrances die "--tunnel option required$/" unless exists $CONF{tunnel}; for ( @{ $CONF{tunnel} } ) { die "--tunnel format required$/" unless /^\d+:[\w.]+:\d+$/; my ( $port, $host, $hostport ) = split ':'; my $socket = IO::Socket::INET->new( $CONF{'local-only'} ? ( LocalAddr => 'localhost' ) : (), Listen => 1, LocalPort => $port, Proto => 'tcp', ReuseAddr => 1, ); die "Tunnel error: $! for $_\n" if not defined $socket; @{ $listen{$socket} }{qw( self port host hostport dest )} = ( $socket, $port, $host, $hostport, "$host:$hostport" ); } =head1 EXAMPLES To connect to a SSH server running beyond the proxy on port 443, through the proxy proxy.company.com, running on port 8080, use the following command: connect-tunnel -P proxy.company.com:8080 -T 22:ssh.example.com:443 And now point your favorite ssh client to the machine running B. You can also emulate a "standard" user-agent: connect-tunnel -U "Mozilla/4.03 [en] (X11; I; Linux 2.1.89 i586)" -P proxy.company.com:8080 -T 22:ssh.example.com:443 B can easily use your proxy credentials to connect outside: connect-tunnel -U "Mozilla/4.03 [en] (X11; I; Linux 2.1.89 i586)" -P proxy.company.com:8080 -T 22:ssh.example.com:443 -A book:s3kr3t But if you don't want anybody else to connect to your tunnels and through the proxy with I credentials, use the B<--local-only> option: connect-tunnel -U "Mozilla/4.03 [en] (X11; I; Linux 2.1.89 i586)" -P proxy.company.com:8080 -T 22:ssh.example.com:443 -A book:s3kr3t -L If you have several destinations, there is no need to run several instances of B: connect-tunnel -U "Mozilla/4.03 [en] (X11; I; Linux 2.1.89 i586)" -P proxy.company.com:8080 -A book:s3kr3t -L -T 22:ssh.example.com:443 -T 222:ssh2.example.com:443 But naturally, you will need to correctly set up the ports in your clients. Mmm, such a long command line would perfectly fit in an alias or a F<.BAT> file. C<;-)> =head1 ENVIRONMENT VARIABLES The LWP::UserAgent that is used to connect to the proxy accept the usual C environment variable to define the proxy. The environment variable is overriden by the B<--proxy> option, if passed to B. =cut # listen to the tunnels entrances my $select = IO::Select->new; $select->add( map { $listen{$_}{self} } keys %listen ); # the loop while ( my @ready = $select->can_read ) { for (@ready) { # a new connection to the listening ports if ( exists $listen{$_} ) { # Create a new socket my $client = $listen{$_}{self}->accept; my ( $port, $addr ) = unpack_sockaddr_in( $client->peername ); $addr = join '.', unpack( 'C4', $addr ); print "$addr:$port -> $listen{$_}{dest} : connection request$/" if $CONF{verbose} > 0; # connect to the proxy my $req = HTTP::Request->new( CONNECT => 'http://' . $listen{$_}{dest} . '/' ); my $res = $ua->request($req); # authentication failed if ( not $res->is_success ) { warn "$addr:$port -> $listen{$_}{dest} : " . "connection failed with status " . $res->status_line . $/; $client->close; next; } # the proxy socket my $proxy = $res->{client_socket}; my ( $pport, $paddr ) = unpack_sockaddr_in( $proxy->peername ); $paddr = join '.', unpack( 'C4', $paddr ); print "$addr:$port -> $listen{$_}{dest} : ", "connection established via $paddr:$pport$/" if $CONF{verbose} > 0; # set up peers data structures @{ $socket{$client} }{qw( self peer host port host_port )} = ( $client, $proxy, $addr, $port, "$addr:$port" ); @{ $socket{$proxy} }{qw( self peer host port host_port dest )} = ( $proxy, $client, $paddr, $pport, "$paddr:$pport", $listen{$_}{dest} ); for ( $client, $proxy ) { $_->autoflush(1); $_->blocking(0); $select->add($_); } } else { # data on established connections # useful data my $sock = $socket{$_}{self}; my $peer = $socket{$_}{peer}; my ( $data, $src, $dst ); if ( $CONF{verbose} > 0 ) { $src = $socket{$sock}{host_port} . ( exists $socket{$sock}{dest} ? ' (' . $socket{$sock}{dest} . ')' : '' ); $dst = $socket{$peer}{host_port} . ( exists $socket{$peer}{dest} ? ' (' . $socket{$peer}{dest} . ')' : '' ); } # read the data my $read = $sock->sysread( $data, 4096 ); # check for errors or end of connection if ( not defined $read ) { warn "Unable to read from " . $socket{$sock}{host_port} . $/; $read = 0; } # end of connection if ( $read == 0 ) { for ( $sock, $peer ) { $_->close; delete $socket{$_}; # reset the IO::Select object, since $select->remove # removes more than we want to $select = IO::Select->new; $select->add( map { $listen{$_}{self} } keys %listen ); $select->add( map { $socket{$_}{self} } keys %socket ); } print "$src -> $dst : connection closed$/" if $CONF{verbose} > 0; next; } # proxy the data $peer->syswrite($data); print "$src -> $dst : ", length($data), " bytes sent$/" if ( $CONF{verbose} > 1 ); } } } =head1 TODO Next version should have an option to create a control port, to which one could connect to interact with B and add/remove tunnels, close connections, change the User-Agent string, and so on. =head1 AUTHOR Philippe "BooK" Bruhat Ebook@cpan.orgE I seem to have re-invented a well-known wheel with that script, but at least hope I have added a few interesting options to it. Bits of the documentation wording is stolen from OpenSSH documentation about options B<-L> and B<-R>. =head1 COPYRIGHT This module is free software; you can redistribute it or modify it under the same terms as Perl itself. =cut