File Coverage

blib/lib/App/ReslirpTunnel/Butler.pm
Criterion Covered Total %
statement 27 115 23.4
branch 0 26 0.0
condition 0 10 0.0
subroutine 9 27 33.3
pod 0 13 0.0
total 36 191 18.8


line stmt bran cond sub pod time code
1             package App::ReslirpTunnel::Butler;
2              
3 1     1   7 use strict;
  1         2  
  1         44  
4 1     1   6 use warnings;
  1         2  
  1         75  
5              
6 1     1   8 use POSIX;
  1         2  
  1         10  
7 1     1   2668 use Socket;
  1         3  
  1         701  
8 1     1   634 use IO::Socket::UNIX;
  1         22296  
  1         7  
9 1     1   1052 use Path::Tiny;
  1         4  
  1         98  
10 1     1   716 use App::ReslirpTunnel::RPC;
  1         5  
  1         48  
11              
12 1     1   10 use parent 'App::ReslirpTunnel::Logger';
  1         3  
  1         10  
13 1     1   120 use Carp;
  1         2  
  1         1828  
14              
15             sub new {
16 0     0 0   my ($class, %args) = @_;
17 0           my $self = { dont_close_stdio => delete $args{dont_close_stdio} };
18 0           bless $self, $class;
19 0           $self->_init_logger(%args, log_prefix => "ReslirpTunnel::Butler");
20 0           return $self;
21             }
22              
23             sub start {
24 0     0 0   shift->_start_slave();
25             }
26              
27             sub _my_lib_path {
28 0     0     my $self = shift;
29 0           my $lib_path = Path::Tiny->new($INC{'App/ReslirpTunnel.pm'})->realpath->parent->parent;
30 0           $self->_log(debug => 'lib_path', $lib_path);
31 0           return $lib_path;
32             }
33              
34             sub _arg2hex {
35 0     0     my ($self, $arg) = @_;
36 0 0         defined $arg or confess "arg2hex: arg is not defined";
37              
38 0           utf8::encode($arg);
39 0           return join('', map { sprintf("%02x", ord($_)) } split(//, $arg));
  0            
40             }
41              
42             sub _start_slave {
43 0     0     my $self = shift;
44              
45 0 0         socketpair(my $parent_socket, my $child_socket, AF_UNIX, SOCK_STREAM, 0)
46             or $self->_die("socketpair failed", $!);
47              
48 0           my $pid = fork();
49 0 0 0       if (defined $pid and $pid == 0) {
    0          
50 0           close($parent_socket);
51 0 0         POSIX::dup2(fileno($child_socket), 1)
52             or $self->_die("dup2 failed", $!);
53 0           my $lib_path = $self->_my_lib_path;
54 0           my @sudo_cmd = ("sudo", $^X, "-I".$lib_path, "-MApp::ReslirpTunnel::ElevatedSlave", "-e", "App::ReslirpTunnel::ElevatedSlave::start");
55 0           push @sudo_cmd, $self->_arg2hex($self->{dont_close_stdio});
56 0           push @sudo_cmd, $self->_arg2hex($self->{log_level});
57 0           push @sudo_cmd, $self->_arg2hex($self->{log_to_stderr});
58 0 0         if (not $self->{log_to_stderr}) {
59 0           push @sudo_cmd, $self->_arg2hex($self->{log_file});
60 0           push @sudo_cmd, $self->_arg2hex($>);
61             }
62              
63             # $self->_log(debug => 'Running sudo cmd', "|".join("| |",@sudo_cmd)."|");
64 0           $self->_log(debug => 'Running sudo cmd', "@sudo_cmd");
65 0 0         exec { $sudo_cmd[0] } @sudo_cmd
  0            
66             or $self->_log(error => "Exec failed", $!);
67 0           POSIX::_exit(1);
68             }
69             elsif (not defined $pid) {
70 0           $self->_die("Fork failed", $!);
71             }
72 0           close($child_socket);
73              
74 0           $self->_log(debug => "waiting for sudo process to return");
75 0           while (1) {
76 0           my $out_pid = waitpid($pid, 0);
77 0 0         if ($out_pid == -1) {
    0          
78 0           $self->_log(debug => "waitpid failed, retrying", $!);
79 0           sleep 1;
80             }
81             elsif ($?) {
82 0           $self->_die("sudo failed", $?);
83             }
84             else {
85 0           $self->_log(debug => "sudo exited with code 0");
86 0           $self->{rpc} = App::ReslirpTunnel::RPC->new($parent_socket);
87 0           return 1;
88             }
89             }
90             }
91              
92             sub _request {
93 0     0     my ($self, $cmd, %args) = @_;
94 0           $self->{rpc}->send_packet({cmd => $cmd, args => \%args});
95 0           my $r = $self->{rpc}->recv_packet();
96 0 0         if ($r->{status} eq 'bye') {
97 0           $self->{rpc} = undef;
98             }
99 0           return $r
100             }
101              
102 0     0 0   sub hello { shift->_request('hello') }
103              
104             sub _request_check_ok {
105 0     0     my ($self, $request, %args) = @_;
106 0   0       my $expected_status = delete($args{expected_status}) // 'ok';
107 0           my $r = $self->_request($request, %args);
108 0 0         if ($r->{status} ne $expected_status) {
109 0           $self->_log(debug => "request failed", "status=$r->{status}, expected=$expected_status", $r->{error});
110 0           return;
111             }
112 0           return $r;
113             }
114              
115             sub create_tap {
116 0     0 0   my ($self, $device) = @_;
117 0           $self->_log(debug => "Forwarding request for creating tap device $device");
118 0   0       my $r = $self->_request_check_ok('create_tap', device => $device) // return;
119 0 0         unless ($r->{fd_follows}) {
120 0           $self->_log("protocol error", "fd expected");
121 0           return;
122             }
123 0           my $fd = $self->{rpc}->recv_fd();
124              
125             # reopen $tap_fd as a Perl file handle
126 0 0         my $tap_fh = IO::Socket::UNIX->new_from_fd($fd, "r+")
127             or $self->_die("Failed to create tap file handle", $!);
128 0           return ($tap_fh);
129             }
130              
131             sub device_up {
132 0     0 0   my ($self, $device) = @_;
133 0           return $self->_request_check_ok('device_up', device => $device)
134             }
135              
136             sub device_addr_add {
137 0     0 0   my ($self, $device, $addr, $mask) = @_;
138 0           return $self->_request_check_ok('device_addr_add', device => $device, addr => $addr, mask => $mask);
139             }
140              
141             sub bye {
142 0     0 0   my $self = shift;
143 0           return $self->_request_check_ok('bye', expected_status => 'bye');
144             }
145              
146             sub add_a_record {
147 0     0 0   my($self, $name, $addr) = @_;
148 0           return $self->_request_check_ok('add_a_record', name => $name, addr => $addr);
149             }
150              
151             sub start_dnsmasq {
152 0     0 0   my ($self, %args) = @_;
153 0           my $r = $self->_request_check_ok(start_dnsmasq => %args);
154 0   0       my $pid = $r->{pid} // $self->_log(warn => "Failed to get dnsmasq PID");
155 0           return $pid;
156             }
157              
158             sub resolvectl_dns {
159 0     0 0   my ($self, %args) = @_;
160 0           return $self->_request_check_ok(resolvectl_dns => %args);
161             }
162              
163             sub resolvectl_domain {
164 0     0 0   my ($self, %args) = @_;
165 0           return $self->_request_check_ok(resolvectl_domain => %args)
166             }
167              
168             sub route_add {
169 0     0 0   my ($self, %args) = @_;
170 0           return $self->_request_check_ok(route_add => %args);
171             }
172              
173 0     0 0   sub screen_reset { shift->_request('screen_reset'); 1 }
  0            
174              
175             1;