File Coverage

blib/lib/App/ReslirpTunnel.pm
Criterion Covered Total %
statement 39 461 8.4
branch 0 146 0.0
condition 0 81 0.0
subroutine 13 57 22.8
pod 0 2 0.0
total 52 747 6.9


line stmt bran cond sub pod time code
1             package App::ReslirpTunnel;
2              
3             our $VERSION = '0.06';
4              
5 1     1   135272 use strict;
  1         2  
  1         42  
6 1     1   6 use warnings;
  1         10  
  1         62  
7              
8 1     1   799 use Socket;
  1         5222  
  1         634  
9 1     1   584 use Data::Validate::Domain qw(is_hostname);
  1         22307  
  1         88  
10 1     1   739 use Data::Validate::IP qw(is_ipv4);
  1         50060  
  1         154  
11 1     1   1436 use Path::Tiny;
  1         17975  
  1         92  
12 1     1   757 use File::XDG;
  1         6906  
  1         42  
13 1     1   674 use POSIX;
  1         8633  
  1         7  
14 1     1   5256 use Net::OpenSSH;
  1         37024  
  1         72  
15 1     1   972 use JSON::PP;
  1         36576  
  1         82  
16              
17 1     1   422 use parent 'App::ReslirpTunnel::Logger';
  1         249  
  1         7  
18              
19 1     1   793 use App::ReslirpTunnel::Butler;
  1         5  
  1         48  
20 1     1   643 use App::ReslirpTunnel::Loop;
  1         4  
  1         8061  
21              
22             sub new {
23 0     0 0   my ($class, %args) = @_;
24 0           my $self = { args => \%args };
25 0           bless $self, $class;
26 0           return $self;
27             }
28              
29             sub go {
30 0     0 0   my $self = shift;
31              
32 0           eval {
33 0           $self->_init_xdg;
34 0           $self->_init_time;
35 0           $self->_init_logger;
36 0           $self->_log(info => "Starting ReslirpTunnel");
37 0           $self->_set_signal_handlers;
38 0           $self->_init_config;
39 0           $self->_init_butler;
40 0           $self->_init_ssh;
41 0           $self->_send_to_background;
42 0           $self->_init_tap_device;
43 0           $self->_init_reslirp;
44 0           $self->_init_loop;
45 0           $self->_config_forward_dns;
46 0           $self->_config_net_mappings;
47 0           $self->_init_dnsmasq;
48 0           $self->_init_resolver_rules;
49 0           $self->_init_routes;
50 0           $self->_wait_for_something;
51 0           $self->_log(info => "Terminating ReslirpTunnel");
52             };
53 0 0         if ($@) {
54 0           die "Something went wrong: $@\n";
55             }
56 0           $self->_kill_everything;
57             }
58              
59             sub _init_xdg {
60 0     0     my $self = shift;
61 0 0         my $app_name = $self->{args}{app_name} or die "App name missing, unable to initialize XDG helper";
62 0           $self->{xdg} = File::XDG->new(name => $app_name, path_class => 'Path::Tiny');
63             }
64              
65             sub _init_time {
66 0     0     my $self = shift;
67 0           $self->{timestamp} = POSIX::strftime("%Y%m%dT%H%M%SZ", gmtime);
68             }
69              
70             sub _init_logger {
71 0     0     my $self = shift;
72 0           my $level = $self->{args}{log_level};
73 0           my $log_to_stderr = $self->{args}{log_to_stderr};
74 0           my $fn = $self->{args}{log_file};
75 0 0         unless (defined $fn) {
76 0           my $parent_dir = $self->{xdg}->state_home->child('logs')->mkdir;
77 0           $fn = $parent_dir->child($self->{timestamp}.".reslirp-tunnel.log");
78 0           eval {
79 0           my $sl = $parent_dir->child('latest.reslirp-tunnel.log');
80 0 0         unlink $sl if -l $sl;
81 0           symlink $fn, $sl;
82             };
83             }
84 0           $self->SUPER::_init_logger(log_level => $level, log_to_stderr => $log_to_stderr, log_file => $fn);
85             }
86              
87             sub _set_signal_handlers {
88 0     0     my $self = shift;
89 0           my $signal_count = 0;
90 0           $self->{signal_count_ref} = \$signal_count;
91             $self->{signal_handler} = sub {
92 0     0     $signal_count++;
93 0           $self->_log(info => "Signal received, count: $signal_count");
94 0           };
95              
96 0           $SIG{INT} = $self->{signal_handler};
97 0           $SIG{TERM} = $self->{signal_handler};
98             }
99              
100             sub _init_config {
101 0     0     my $self = shift;
102 0           my $args = $self->{args};
103              
104 0   0       $self->{run_in_foreground} = $args->{run_in_foreground} // 0;
105 0   0       $self->{dont_close_stdio} = $args->{dont_close_stdio} // 0;
106              
107 0           $self->{ssh_host} = $args->{ssh_host};
108 0           $self->{ssh_port} = $args->{ssh_port};
109 0           $self->{ssh_user} = $args->{ssh_user};
110              
111 0   0       $self->{remote_network} = $args->{remote_network} // '10.0.2.0';
112 0 0         is_ipv4($self->{remote_network}) or $self->_die("Invalid remote network address, $self->{remote_network}");
113              
114 0   0       $self->{remote_netmask} = $self->_parse_netmask($args->{remote_netmask} // 24);
115 0           $self->{remote_dns} = $self->_parse_ip($args->{remote_dns});
116 0           $self->{remote_gw} = $self->_parse_ip($args->{remote_gw});
117 0           $self->{local_ip} = $self->_parse_ip($args->{local_ip});
118             }
119              
120             sub _parse_netmask {
121 0     0     my ($self, $netmask) = @_;
122 0 0 0       ($netmask =~ /^\d+$/ && $netmask >= 1 && $netmask <= 31) or $self->_die("Invalid netmask", $netmask);
      0        
123 0           return $netmask;
124             }
125              
126             sub _parse_ip {
127 0     0     my ($self, $ip) = @_;
128 0           my $network = $self->{remote_network};
129 0           my $netmask = $self->{remote_netmask};
130 0 0         if ($ip =~ /^\d+$/) {
131 0           $ip = $network =~ s/\d+$/$ip/r;
132             }
133 0 0         is_ipv4($ip) or $self->_die("Invalid IP address", $ip);
134              
135 0           my $ip_int = __ip_to_int($ip);
136 0           my $net_int = __ip_to_int($network);
137 0           my $bitmask = ~0 << (32 - $netmask);
138              
139 0 0         unless (($ip_int & $bitmask) == ($ip_int & $bitmask)) {
140 0           $self->_die("IP address $ip is not inside remote network $network/$netmask");
141             }
142              
143 0           return $ip;
144             }
145              
146             sub __ip_to_int {
147 0     0     my $ip = shift;
148 0           return unpack("N", pack("C*", split(/\./, $ip)));
149             }
150              
151             sub _init_butler {
152 0     0     my $self = shift;
153             my $butler = $self->{butler} = App::ReslirpTunnel::Butler->new(dont_close_stdio => $self->{dont_close_stdio},
154             log_level => $self->{log_level},
155             log_to_stderr => $self->{log_to_stderr},
156 0           log_file => $self->{log_file});
157              
158 0 0         $butler->start or $self->_die("Failed to start butler");
159 0 0         $butler->hello
160             or $self->_die("Failed to say hello to butler");
161 0           $self->_log(info => "Elevated slave process started and ready");
162             }
163              
164             sub _send_to_background {
165 0     0     my $self = shift;
166 0 0         return if $self->{run_in_foreground};
167              
168 0           $self->_log(info => "Moving to background");
169 0           POSIX::setsid();
170              
171 0   0       my $pid = fork // $self->_die("Unable to move process into the background", $!);
172 0 0         if ($pid == 0) {
173 0           $SIG{INT} = $self->{signal_handler};
174 0           $SIG{TERM} = $self->{signal_handler};
175              
176 0 0         unless ($self->{dont_close_stdio}) {
177 0           open STDIN, '<', '/dev/null';
178 0           open STDOUT, '>', '/dev/null';
179 0 0         open STDERR, '>', '/dev/null' unless $self->{log_to_stderr};
180             }
181              
182 0           $self->{log_prefix} = "ReslirpTunnel::Child";
183              
184 0           return 1; # Return in the child!!!
185             }
186             else {
187 0           eval {
188 0           syswrite STDERR, "$0 moved to background, PID: $pid\n";
189 0           $self->_log(debug => "First process exiting");
190             };
191              
192 0           POSIX::_exit(0);
193             }
194             }
195              
196             sub _init_ssh {
197 0     0     my $self = shift;
198 0   0       my $host = $self->{ssh_host} // $self->_die("No remote host specified");
199 0           my $port = $self->{ssh_port};
200 0           my $user = $self->{ssh_user};
201 0           my $cmd = $self->{args}{ssh_command};
202 0           my $more_args = $self->{args}{more_ssh_args};
203 0           my @args = (host => $host);
204 0 0         push @args, (port => $port) if defined $port;
205 0 0         push @args, (user => $user) if defined $user;
206 0 0         push @args, (ssh_cmd => $cmd) if defined $cmd;
207 0 0         push @args, (master_opts => $more_args) if defined $more_args;
208 0           $self->{ssh} = my $ssh = Net::OpenSSH->new(@args);
209 0 0         $ssh->error and
210             $self->_die("Unable to connect to remote host", $ssh->error);
211 0   0       $self->{remote_os} = $self->{args}{remote_os} // $self->_autodetect_remote_os //
      0        
212             $self->_die("No remote OS specified and unable to autodetect it");
213 0   0       $self->{remote_shell} = $self->{args}{remote_shell} // $self->_autodetect_remote_shell //
      0        
214             $self->_die("No remote shell specified and unable to autodetect it");
215              
216 0 0         $self->{quoting_backend} = (($self->{remote_shell} eq 'windows') ? 'MSWin' : 'ksh');
217 0           my $ssh_master_pid = $self->{ssh}->get_master_pid;
218 0           $self->_log(debug => "SSH master PID", $ssh_master_pid);
219 0           $self->{ssh_master_pid} = $ssh_master_pid;
220             }
221              
222             sub _autodetect_remote_os {
223 0     0     my $self = shift;
224 0           my $ssh = $self->{ssh};
225 0           my $out = $ssh->capture('echo %COMSPEC%');
226 0           my $looks_like_unix = $out =~ /^\%COMSPEC\%$/m;
227 0 0         if ($looks_like_unix) {
228 0           $self->_log(debug => "Looks like a Unix-like system, let's check it further...");
229 0           my $uname = lc $ssh->capture('uname -s');
230 0 0         if ($uname =~ /^(Linux|Darwin|FreeBSD|OpenBSD|NetBSD|DragonFly|MidnightBSD|AIX|HP-UX|SunOS|IRIX|OSF1|SCO_SV|QNX)$/i) {
231 0           $self->_log(info => "Remote OS identified as Linux/UNIX ($1)");
232 0           return 'unix';
233             }
234             }
235             else {
236 0           $self->_log(debug => "Looks like Windows, let's check it further...");
237 0           my $ver = $ssh->capture('ver');
238 0 0         if ($ver =~ /^(Microsoft Windows \[Version.*\])/m) {
239 0           $self->_log(info => "Remote OS identified as Windows ($1)");
240 0           return 'windows';
241             }
242             }
243 0           $self->_warn("Unable to autodetect remote OS");
244 0           return;
245             }
246              
247             sub _autodetect_remote_shell {
248 0     0     my $self = shift;
249 0 0         if ($self->{remote_os} eq 'windows') {
250 0           return $self->{remote_shell} = 'windows';
251             }
252 0           my $ssh = $self->{ssh};
253 0 0         my $out = $ssh->capture('echo $SHELL') or return "sh";
254 0           chomp($out);
255 0           return Path::Tiny->new($out)->basename;
256             }
257              
258             sub _init_tap_device {
259 0     0     my $self = shift;
260 0           my $butler = $self->{butler};
261 0   0       my $device = $self->{tap_device} = $self->{args}{device} // $self->_find_unused_tap_device;
262 0           $self->{tap_fh} = $butler->create_tap($device);
263 0 0         $butler->device_up($device)
264             or $self->_die("Failed to bring up tap device $device");
265              
266 0           my $host = $self->{local_ip};
267 0           my $mask = $self->{remote_netmask};
268 0 0         $butler->device_addr_add($device, $host, $mask)
269             or $self->_die("Failed to add address $host/$mask to tap device $device");
270 0           $self->_log(info => "Tap device $device created and configured");
271 0           1;
272             }
273              
274             sub _init_reslirp {
275 0     0     my $self = shift;
276 0           my $ssh = $self->{ssh};
277 0   0       my $cmd = $self->{reslirp_command} = $self->{args}{reslirp_command} // $self->_autodetect_reslirp_command;
278 0           my @args = @{$self->{args}{more_reslirp_args}};
  0            
279 0           $self->_log(info => "Starting remote reSLIRP process");
280 0           $self->_log(debug => "Remote command: $cmd @args");
281             my ($socket, undef, $stderr, $pid) = $ssh->open_ex({stderr_pipe => 1,
282             stdinout_socket => 1,
283             quote_args => 1,
284             remote_shell => $self->{quoting_backend}},
285 0           $cmd, @args);
286 0           $self->{reslirp_socket} = $socket;
287 0           $self->{reslirp_stderr} = $stderr;
288 0           $self->{reslirp_pid} = $pid;
289 0 0         $pid or $self->_die("Failed to start reSLIRP process");
290 0           $self->_log(info => "reSLIRP process started");
291             }
292              
293             sub _autodetect_reslirp_command {
294 0     0     my $self = shift;
295 0 0         if ($self->{remote_os} eq 'windows') {
296 0           return 'C:\Program Files\reSLIRP\reslirp.exe';
297             }
298 0           return "reslirp";
299             }
300              
301             sub _find_unused_tap_device {
302 0     0     my $self = shift;
303 0           my $n = 0;
304 0           for my $n (0..100) {
305 0           my $device = "tap$n";
306 0 0         -e "/dev/$device" or return $device;
307             }
308 0           $self->_die("Unable to find an unused tap device");
309             }
310              
311             sub _config_forward_dns {
312 0     0     my $self = shift;
313 0   0       $self->{forward_dns} //= {};
314 0   0       $self->{forward_ipv4} //= {};
315 0           $self->_config_forward_dns_ssh;
316             }
317              
318             sub _config_forward_dns_ssh {
319 0     0     my $self = shift;
320 0           for my $record (@{$self->{args}{forward_dns_ssh}}) {
  0            
321 0           $self->_log(debug => "Retrieving iface DNS servers using remote shell");
322 0           my $domain = $record->{domain};
323 0           my $iface = $record->{iface};
324 0 0         my $method = "_resolve_remote_iface_dns__". (($self->{remote_os} eq 'windows') ? 'windows' : 'unix');
325 0 0         if (my @addrs = $self->$method($iface)) {
326 0           $self->_log(debug => "DNS servers for remote iface $iface", join(", ", @addrs));
327 0           for my $addr (@addrs) {
328 0   0       push @{$self->{forward_dns}{$domain} //= []}, $addr;
  0            
329 0           $self->{forward_ipv4}{"$addr/32"} = 1;
330             }
331             }
332             else {
333 0           $self->_warn("Failed to retrieve DNS servers using remote shell, ignoring domain", $record->{domain});
334             }
335             }
336             }
337              
338             sub _resolve_remote_iface_dns__unix {
339 0     0     my $self = shift;
340 0           $self->_warn('Retrieving by iface DNS servers using the shell on remote Unix hosts is not implemented yet');
341             ()
342 0           }
343              
344             sub _resolve_remote_iface_dns__windows {
345 0     0     my ($self, $iface) = @_;
346 0           my $ssh = $self->{ssh};
347              
348 0           my $out = $ssh->capture({remote_shell=> 'MSWin'}, 'powershell', '-Command', "Get-DnsClientServerAddress | ConvertTo-Json");
349 0           my @addrs;
350 0           eval {
351 0           for my $record (@{JSON::PP::decode_json($out)}) {
  0            
352 0 0 0       if ($record->{InterfaceAlias} eq $iface and
353             $record->{AddressFamily} eq '2') {
354 0           push @addrs, @{$record->{ServerAddresses}};
  0            
355             }
356             }
357             };
358 0 0         unless (@addrs) {
359 0           $self->_warn("Failed to parse JSON output from DnsClientServerAddress", $@);
360 0           $self->_log(debug => "Output was", $out);
361             }
362 0           return @addrs;
363             }
364              
365              
366             sub _config_net_mappings {
367 0     0     my $self = shift;
368 0   0       $self->{net_mapping} //= {};
369 0   0       $self->{forward_ipv4} //= {};
370 0           $self->_config_net_mappings_net;
371 0           $self->_config_net_mappings_direct;
372 0           $self->_config_net_mappings_local;
373 0           $self->_config_net_mappings_dns;
374 0           $self->_config_net_mappings_ssh;
375             }
376              
377             sub _config_net_mappings_net {
378 0     0     my $self = shift;
379 0           for my $record (@{$self->{args}{route_nets}}) {
  0            
380 0           my $addr = $record->{addr};
381 0           my $mask = $record->{mask};
382 0 0 0       if ($self->_validate_ipv4($addr) and $self->_validate_netmask($mask)) {
383 0           $self->{forward_ipv4}{"$addr/$mask"} = 1;
384             }
385             else {
386 0           $self->_warn("Ignoring invalid network", "$addr/$mask");
387             }
388             }
389             }
390              
391             sub _config_net_mappings_direct {
392 0     0     my $self = shift;
393 0           for my $record (@{$self->{args}{route_hosts}}) {
  0            
394 0   0       my $addrs = $record->{addrs} // [];
395 0           $self->{forward_ipv4}{"$_/32"} = 1 for @$addrs;
396 0 0         if (defined (my $host = $record->{host})) {
397 0 0         if ($self->_validate_domain_name($host)) {
398 0   0       push @{$self->{net_mapping}{$host} //= []}, @$addrs;
  0            
399             }
400             else {
401 0           $self->_warn("Ignoring host with invalid name", $host);
402             }
403             }
404             }
405             }
406              
407             sub _config_net_mappings_local {
408 0     0     my $self = shift;
409 0           for my $host (@{$self->{args}{route_hosts_local}}) {
  0            
410 0           my $addr;
411 0 0         if (is_ipv4($host)) {
    0          
412 0           $self->{forward_ipv4}{"$host/32"} = 1;
413             }
414             elsif ($self->_validate_domain_name($host)) {
415 0           my $good;
416 0           my ($err, @records) = Socket::getaddrinfo($host);
417 0 0         unless ($err) {
418 0           for my $record (@records) {
419 0 0         if ($record->{family} == AF_INET) {
420 0           my (undef, $packed_ip) = sockaddr_in($record->{addr});
421 0           my $addr = inet_ntoa($packed_ip);
422 0   0       push @{$self->{net_mapping}{$host} //= []}, $addr;
  0            
423 0           $self->{forward_ipv4}{"$addr/32"} = 1;
424 0           $good = 1;
425             }
426             }
427             }
428 0 0         $good or $self->_warn("Failed to resolve host, ignoring it", $host);
429             }
430             else {
431 0           $self->_warn("Ignoring host with invalid name", $host);
432             }
433             }
434             }
435              
436             sub _validate_ipv4 {
437 0     0     my ($self, $ipv4) = @_;
438 0 0         is_ipv4($ipv4) and return 1;
439 0           $self->_log(debug => "Bad IPv4", $ipv4);
440 0           return undef;
441             }
442              
443             sub _validate_netmask {
444 0     0     my ($self, $mask) = @_;
445 0 0 0       $mask =~ /\d+/ and $mask >= 1 and $mask <= 32 and return 1;
      0        
446 0           $self->_log(debug => "Bad netmask", $mask);
447 0           return undef;
448             }
449              
450             sub _validate_domain_name {
451 0     0     my ($self, $domain) = @_;
452 0 0         is_hostname($domain, {'domain_private_tld' => 1}) and return 1;
453 0           $self->_log(debug => "Bad domain", $domain);
454 0           return undef;
455             }
456              
457             sub _config_net_mappings_dns {
458 0     0     my $self = shift;
459 0           my $route_hosts = $self->{args}{route_hosts_dns};
460 0 0         if (@$route_hosts) {
461 0           my $dns = Net::DNS::Resolver->new(nameservers => [$self->{remote_dns}], recurse => 1);
462 0           for my $host (@$route_hosts) {
463 0 0         if ($self->_validate_domain_name($host)) {
464 0           my $good;
465 0           $self->_log(debug => "Resolving $host using remote DNS");
466 0           my $query = $dns->query($host, 'A');
467 0 0         if ($query) {
468 0           for my $rr ($query->answer) {
469 0 0         if ($rr->type eq 'A') {
470 0           my $addr = $rr->address;
471 0   0       push @{$self->{net_mapping}{$host} //= []}, $addr;
  0            
472 0           $self->{forward_ipv4}{"$addr/32"} = 1;
473 0           $good = 1;
474             }
475             }
476             }
477 0 0         $good or $self->_warn("Failed to resolve host using remote DNS, ignoring it", $host);
478             }
479             else {
480 0           $self->_warn("Ignoring host with invalid name", $host);
481             }
482             }
483             }
484             }
485              
486             sub _config_net_mappings_ssh {
487 0     0     my $self = shift;
488 0           my $route_hosts = $self->{args}{route_hosts_ssh};
489 0           for my $host (@$route_hosts) {
490 0 0         if ($self->_validate_domain_name($host)) {
491 0           $self->_log(debug => "Resolving $host using remote shell");
492 0 0         my $method = "_resolve_remote_host_with_shell__" . (($self->{remote_os} eq 'windows') ? 'windows' : 'unix');
493 0           my @addrs = $self->$method($host);
494 0           for my $addr (@addrs) {
495 0   0       push @{$self->{net_mapping}{$host} //= []}, $addr;
  0            
496 0           $self->{forward_ipv4}{"$addr/32"} = 1;
497             }
498 0 0         @addrs or $self->_warn("Failed to resolve host using remote DNS, ignoring it", $host);
499             }
500             else {
501 0           $self->_warn("Ignoring host with invalid name", $host);
502             }
503             }
504             }
505              
506             sub _resolve_remote_host_with_shell__unix {
507 0     0     my $self = shift;
508 0           $self->_warn('Resolving using the shell on remote Unix hosts is not implemented yet');
509             ()
510 0           }
511              
512             sub _resolve_remote_host_with_shell__windows {
513 0     0     my ($self, $host) = @_;
514 0           my $ssh = $self->{ssh};
515              
516 0           my $out = $ssh->capture({remote_shell=> 'MSWin'}, 'powershell', '-Command', "Resolve-DnsName $host | ConvertTo-Json");
517 0           my @addrs;
518 0           eval {
519 0           my $records = JSON::PP::decode_json($out);
520 0           my @names = $host;
521 0           for my $r (@$records) {
522 0 0         push @names, $r->{NameHost} if $r->{Type} == 5;
523             }
524 0           for my $r (@$records) {
525 0 0         push @addrs, $r->{IP4Address} if $r->{Type} == 1
526             }
527             return @addrs
528 0           };
529 0 0         unless (@addrs) {
530 0           $self->_warn("Failed to parse JSON output from Resolve-DnsName", $@);
531 0           $self->_log(debug => "Output was", $out);
532             }
533 0           return @addrs;
534             }
535              
536             sub _init_dnsmasq {
537 0     0     my $self = shift;
538 0           my $net_mapping = $self->{net_mapping};
539 0           my $forward_dns = $self->{forward_dns};
540              
541 0 0 0       if (%$net_mapping or %$forward_dns) {
542 0           $self->_log(info => "Starting dnsmasq");
543              
544 0           my $pid_parent_dir = $self->{xdg}->state_home->child('dnsmasq')->mkdir;
545 0           my $pid_fn = $pid_parent_dir->child($self->{timestamp}.".dnsmasq.pid");
546 0           my $latest_fn = $pid_parent_dir->child("latest.dnsmasq.pid");
547 0 0         unlink $latest_fn if -e $latest_fn;
548 0           symlink $pid_fn, $latest_fn;
549 0           my $log_fn = $self->{xdg}->state_home->child('logs')->mkdir->child($self->{timestamp}.".dnsmasq.log");
550 0           my $butler = $self->{butler};
551 0           my $user_name = $self->_get_user_name;
552 0           my $group_name = $self->_get_group_name;
553             my $pid = $self->{dnsmasq_pid} = $butler->start_dnsmasq(device => $self->{tap_device},
554 0           net_mapping => $net_mapping,
555             forward_dns => $forward_dns,
556             user => $user_name,
557             group => $group_name,
558             pid_fn => "$pid_fn",
559             log_fn => "$log_fn");
560 0 0         if ($pid) {
561 0           $self->_log(debug => "dnsmasq PID", $pid);
562             }
563             else {
564 0           $self->_warn("dnsmasq failed to start correctly, no PID found");
565             }
566             }
567             else {
568 0           $self->_log(debug => 'dnsmasq not required');
569             }
570             }
571              
572             sub _init_resolver_rules {
573 0     0     my $self = shift;
574 0           my @domains = ( keys(%{$self->{net_mapping}}),
575 0           keys(%{$self->{forward_dns}}) );
  0            
576 0 0         if(@domains) {
577 0           $self->_log(info => "Setting up resolver rules");
578 0           my $butler = $self->{butler};
579 0           my $device = $self->{tap_device};
580 0           my $local_ip = $self->{local_ip};
581 0           $butler->resolvectl_dns(device => $device, dns => $local_ip);
582 0           for my $domain (@domains) {
583 0           $butler->resolvectl_domain(device => $device, domain => $domain);
584             }
585             }
586             }
587              
588             sub _init_routes {
589 0     0     my $self = shift;
590 0           my $forward = $self->{forward_ipv4};
591 0 0         if (%$forward) {
592 0           $self->_log(info => "Setting up routes");
593 0           my $butler = $self->{butler};
594 0           for my $net (keys %$forward) {
595 0           $butler->route_add(net => $net, gw => $self->{remote_gw}, device => $self->{tap_device});
596             }
597             }
598             }
599              
600             sub _get_user_name {
601 0     0     my $self = shift;
602 0           my $user = getpwuid($<);
603 0 0         return $user if $user;
604              
605 0           $self->_warn("Failed to get user name, using 'nobody'");
606 0           return 'nobody';
607             }
608              
609             sub _get_group_name {
610 0     0     my $self = shift;
611 0           my $group = getgrgid($();
612 0 0         return $group if $group;
613              
614 0           $self->_warn("Failed to get group name, using 'nogroup'");
615 0           return 'nogroup'
616             }
617              
618             sub _init_loop {
619 0     0     my $self = shift;
620              
621             my $loop = App::ReslirpTunnel::Loop->new(log_level => $self->{log_level},
622             log_to_stderr => $self->{log_to_stderr},
623 0           log_file => $self->{log_file});
624              
625             my $pid = $loop->run($self->{tap_fh}, $self->{reslirp_socket}, $self->{reslirp_stderr})
626 0   0       //$self->_die("Failed to start IO loop process");
627              
628 0           $self->_log(info => "IO loop process started, PID: $pid");
629 0           $self->{loop_pid} = $pid;
630             }
631              
632             sub _find_process_by_pid {
633 0     0     my ($self, $pid) = @_;
634 0           for my $process (qw(reslirp loop dnsmasq)) {
635 0           my $process_pid = $self->{"${process}_pid"};
636 0 0         if (defined $process_pid) {
637 0 0         return $process if $self->{"${process}_pid"} == $pid;
638             }
639             }
640 0           return;
641             }
642              
643             sub _wait_for_something {
644 0     0     my $self = shift;
645 0           $self->_log(debug => "Waiting for some child to exit");
646 0           while (not ${$self->{signal_count_ref}}) {
  0            
647 0           my $kid = waitpid(-1, WNOHANG);
648 0 0         if ($kid <= 0) {
649             # $self->_log(debug => "waitpid", $kid);
650 0 0         $self->_log(debug => "waitpid failed", $!) if $kid < 0;
651 0           select undef, undef, undef, 5;
652             }
653             else {
654 0           $self->_log(debug => "process $kid exited, rc", $? >> 8);
655 0           for my $proc (qw(reslirp loop ssh_master)) {
656 0           my $proc_pid = $self->{"${proc}_pid"};
657 0 0 0       if (defined $proc_pid and $kid == $proc_pid) {
658 0           $self->_log(info => "Process $proc (PID: $kid) finished");
659 0           delete $self->{"${proc}_pid"};
660              
661 0 0         $self->{ssh}->master_exited if $proc eq 'ssh_master';
662 0           return;
663             }
664             }
665 0           $self->_warn("Unknown process with PID $kid finished");
666             }
667             }
668             }
669              
670             sub _kill_everything {
671 0     0     my $self = shift;
672 0           $self->_log(debug => "killing everything!");
673 0           my @signals = (0, 0, 15, 15, 15, 9, 9, 9);
674              
675 0 0         if (defined(my $ssh = $self->{ssh})) {
676 0           $ssh->disconnect;
677 0           delete $self->{ssh_master_pid};
678             }
679              
680 0           for my $process (qw(loop reslirp dnsmasq)) {
681 0   0       my $pid = $self->{"${process}_pid"} // next;
682 0           $self->_log(debug => "Waiting for process $process (PID: $pid) to finish");
683 0 0         if (kill(0 => $pid) > 0) {
684 0           for my $signal (@signals) {
685 0           my $kid = waitpid($pid, WNOHANG);
686 0 0         if ($kid == $pid) {
687 0           $self->_log(debug => "Process $process exited and captured", $?);
688 0           last;
689             }
690 0           sleep 1;
691 0           $self->_log(debug => "Sending signal $signal to process $pid");
692 0           kill $signal => $pid;
693             }
694             }
695             else {
696 0           $self->_log(debug => "Cannot send signals to process $pid");
697 0           last;
698             }
699             }
700 0           $self->_log(info => "All processes finished");
701             }
702              
703             1;
704             __END__