App-ReslirpTunnel

 view release on metacpan or  search on metacpan

lib/App/ReslirpTunnel.pm  view on Meta::CPAN

package App::ReslirpTunnel;

our $VERSION = '0.06';

use strict;
use warnings;

use Socket;
use Data::Validate::Domain qw(is_hostname);
use Data::Validate::IP qw(is_ipv4);
use Path::Tiny;
use File::XDG;
use POSIX;
use Net::OpenSSH;
use JSON::PP;

use parent 'App::ReslirpTunnel::Logger';

use App::ReslirpTunnel::Butler;
use App::ReslirpTunnel::Loop;

sub new {
    my ($class, %args) = @_;
    my $self = { args => \%args };
    bless $self, $class;
    return $self;
}

sub go {
    my $self = shift;

    eval {
        $self->_init_xdg;
        $self->_init_time;
        $self->_init_logger;
        $self->_log(info => "Starting ReslirpTunnel");
        $self->_set_signal_handlers;
        $self->_init_config;
        $self->_init_butler;
        $self->_init_ssh;
        $self->_send_to_background;
        $self->_init_tap_device;
        $self->_init_reslirp;
        $self->_init_loop;
        $self->_config_forward_dns;
        $self->_config_net_mappings;
        $self->_init_dnsmasq;
        $self->_init_resolver_rules;
        $self->_init_routes;
        $self->_wait_for_something;
        $self->_log(info => "Terminating ReslirpTunnel");
    };
    if ($@) {
        die "Something went wrong: $@\n";
    }
    $self->_kill_everything;
}

sub _init_xdg {
    my $self = shift;
    my $app_name = $self->{args}{app_name} or die "App name missing, unable to initialize XDG helper";
    $self->{xdg} = File::XDG->new(name => $app_name, path_class => 'Path::Tiny');
}

sub _init_time {
    my $self = shift;
    $self->{timestamp} = POSIX::strftime("%Y%m%dT%H%M%SZ", gmtime);
}

sub _init_logger {
    my $self = shift;
    my $level = $self->{args}{log_level};
    my $log_to_stderr = $self->{args}{log_to_stderr};
    my $fn = $self->{args}{log_file};
    unless (defined $fn) {
        my $parent_dir = $self->{xdg}->state_home->child('logs')->mkdir;
        $fn = $parent_dir->child($self->{timestamp}.".reslirp-tunnel.log");
        eval {
            my $sl = $parent_dir->child('latest.reslirp-tunnel.log');
            unlink $sl if -l $sl;
            symlink $fn, $sl;
        };
    }
    $self->SUPER::_init_logger(log_level => $level, log_to_stderr => $log_to_stderr, log_file => $fn);
}

sub _set_signal_handlers {
    my $self = shift;
    my $signal_count = 0;
    $self->{signal_count_ref} = \$signal_count;
    $self->{signal_handler} = sub {
        $signal_count++;
        $self->_log(info => "Signal received, count: $signal_count");
    };

    $SIG{INT} = $self->{signal_handler};
    $SIG{TERM} = $self->{signal_handler};
}

sub _init_config {
    my $self = shift;

lib/App/ReslirpTunnel.pm  view on Meta::CPAN

    $self->{run_in_foreground} = $args->{run_in_foreground} // 0;
    $self->{dont_close_stdio} = $args->{dont_close_stdio} // 0;

    $self->{ssh_host} = $args->{ssh_host};
    $self->{ssh_port} = $args->{ssh_port};
    $self->{ssh_user} = $args->{ssh_user};

    $self->{remote_network} = $args->{remote_network} // '10.0.2.0';
    is_ipv4($self->{remote_network}) or $self->_die("Invalid remote network address, $self->{remote_network}");

    $self->{remote_netmask} = $self->_parse_netmask($args->{remote_netmask} // 24);
    $self->{remote_dns} = $self->_parse_ip($args->{remote_dns});
    $self->{remote_gw} = $self->_parse_ip($args->{remote_gw});
    $self->{local_ip} = $self->_parse_ip($args->{local_ip});
}

sub _parse_netmask {
    my ($self, $netmask) = @_;
    ($netmask =~ /^\d+$/ && $netmask >= 1 && $netmask <= 31) or $self->_die("Invalid netmask", $netmask);
    return $netmask;
}

sub _parse_ip {
    my ($self, $ip) = @_;
    my $network = $self->{remote_network};
    my $netmask = $self->{remote_netmask};
    if ($ip =~ /^\d+$/) {
        $ip = $network =~ s/\d+$/$ip/r;
    }
    is_ipv4($ip) or $self->_die("Invalid IP address", $ip);

    my $ip_int = __ip_to_int($ip);
    my $net_int = __ip_to_int($network);
    my $bitmask = ~0 << (32 - $netmask);

    unless (($ip_int & $bitmask) == ($ip_int & $bitmask)) {
        $self->_die("IP address $ip is not inside remote network $network/$netmask");
    }

    return $ip;
}

sub __ip_to_int {
    my $ip = shift;
    return unpack("N", pack("C*", split(/\./, $ip)));
}

sub _init_butler {
    my $self = shift;
    my $butler = $self->{butler} = App::ReslirpTunnel::Butler->new(dont_close_stdio => $self->{dont_close_stdio},
                                                                 log_level => $self->{log_level},
                                                                 log_to_stderr => $self->{log_to_stderr},
                                                                 log_file => $self->{log_file});

    $butler->start or $self->_die("Failed to start butler");
    $butler->hello
        or $self->_die("Failed to say hello to butler");
    $self->_log(info => "Elevated slave process started and ready");
}

sub _send_to_background {
    my $self = shift;
    return if $self->{run_in_foreground};

    $self->_log(info => "Moving to background");
    POSIX::setsid();

    my $pid = fork // $self->_die("Unable to move process into the background", $!);
    if ($pid == 0) {
        $SIG{INT} = $self->{signal_handler};
        $SIG{TERM} = $self->{signal_handler};

        unless ($self->{dont_close_stdio}) {
            open STDIN, '<', '/dev/null';
            open STDOUT, '>', '/dev/null';
            open STDERR, '>', '/dev/null' unless $self->{log_to_stderr};
        }

        $self->{log_prefix} = "ReslirpTunnel::Child";

        return 1; # Return in the child!!!
    }
    else {
        eval {
            syswrite STDERR, "$0 moved to background, PID: $pid\n";
            $self->_log(debug => "First process exiting");
        };

        POSIX::_exit(0);
    }
}

sub _init_ssh {
    my $self = shift;
    my $host = $self->{ssh_host} // $self->_die("No remote host specified");
    my $port = $self->{ssh_port};
    my $user = $self->{ssh_user};
    my $cmd = $self->{args}{ssh_command};
    my $more_args = $self->{args}{more_ssh_args};
    my @args = (host => $host);
    push @args, (port => $port) if defined $port;
    push @args, (user => $user) if defined $user;
    push @args, (ssh_cmd => $cmd) if defined $cmd;
    push @args, (master_opts => $more_args) if defined $more_args;
    $self->{ssh} = my $ssh = Net::OpenSSH->new(@args);
    $ssh->error and
         $self->_die("Unable to connect to remote host", $ssh->error);
    $self->{remote_os} = $self->{args}{remote_os} // $self->_autodetect_remote_os //
        $self->_die("No remote OS specified and unable to autodetect it");
    $self->{remote_shell} = $self->{args}{remote_shell} // $self->_autodetect_remote_shell //
        $self->_die("No remote shell specified and unable to autodetect it");

    $self->{quoting_backend} = (($self->{remote_shell} eq 'windows') ? 'MSWin' : 'ksh');
    my $ssh_master_pid = $self->{ssh}->get_master_pid;
    $self->_log(debug => "SSH master PID", $ssh_master_pid);
    $self->{ssh_master_pid} = $ssh_master_pid;
}

sub _autodetect_remote_os {
    my $self = shift;
    my $ssh = $self->{ssh};
    my $out = $ssh->capture('echo %COMSPEC%');
    my $looks_like_unix = $out =~ /^\%COMSPEC\%$/m;
    if ($looks_like_unix) {
        $self->_log(debug => "Looks like a Unix-like system, let's check it further...");
        my $uname = lc $ssh->capture('uname -s');
        if ($uname =~ /^(Linux|Darwin|FreeBSD|OpenBSD|NetBSD|DragonFly|MidnightBSD|AIX|HP-UX|SunOS|IRIX|OSF1|SCO_SV|QNX)$/i) {
            $self->_log(info => "Remote OS identified as Linux/UNIX ($1)");
            return 'unix';
        }
    }
    else {
        $self->_log(debug => "Looks like Windows, let's check it further...");
        my $ver = $ssh->capture('ver');
        if ($ver =~ /^(Microsoft Windows \[Version.*\])/m) {
            $self->_log(info => "Remote OS identified as Windows ($1)");
            return 'windows';
        }
    }
    $self->_warn("Unable to autodetect remote OS");
    return;
}

sub _autodetect_remote_shell {
    my $self = shift;



( run in 2.487 seconds using v1.01-cache-2.11-cpan-a49fcb8fa48 )