File Coverage

blib/lib/App/ReslirpTunnel/Loop.pm
Criterion Covered Total %
statement 12 138 8.7
branch 0 66 0.0
condition 0 19 0.0
subroutine 4 11 36.3
pod 0 3 0.0
total 16 237 6.7


line stmt bran cond sub pod time code
1             package App::ReslirpTunnel::Loop;
2              
3 1     1   8 use strict;
  1         3  
  1         43  
4 1     1   5 use warnings;
  1         2  
  1         74  
5 1     1   7 use POSIX;
  1         2  
  1         8  
6              
7 1     1   2356 use parent 'App::ReslirpTunnel::Logger';
  1         4  
  1         8  
8              
9             sub new {
10 0     0 0   my ($class, %logger_args) = @_;
11 0           my $self = bless {}, $class;
12 0           $self->_init_logger(%logger_args,
13             log_prefix => 'ReslirpTunnel::Loop');
14 0           return $self;
15             }
16              
17 0     0 0   sub hexdump { unpack "H*", $_[0] }
18              
19             sub run {
20 0     0 0   my ($self, $tap_handle, $ssh_handle, $ssh_err_handle) = @_;
21 0           $self->_log(debug => "tap_handle: $tap_handle, ssh_handle: $ssh_handle, ssh_err_handle: $ssh_err_handle");
22              
23 0           my $pid = fork();
24 0 0 0       if (defined $pid and $pid == 0) {
    0          
25 0           eval {
26             # We use a double eval because we want to catch any
27             # errors, even those due to a failed logging call!
28 0           eval {
29             # We close everything but the TAP, SSH, SSH error and log handles
30 0           my @keep_fhs = ($tap_handle, $ssh_handle, $ssh_err_handle);
31 0           my $log_fh = eval { $self->{log}{adapter}{fh} };
  0            
32 0 0         push @keep_fhs, $log_fh if defined $log_fh;
33              
34 0           $self->_close_fds_but(@keep_fhs);
35 0           $self->_init_signal_handlers();
36 0           eval {
37 0           $self->_log(debug => 'looping...');
38 0           $self->_loop($tap_handle, $ssh_handle, $ssh_err_handle);
39             };
40 0 0         if ($@) {
41 0           $self->_log(error => "IO loop failed", $@);
42             }
43             };
44 0 0         if ($@) {
45 0           $self->_log(error => "Error setting up loop", $@);
46             };
47             };
48 0           POSIX::_exit(0);
49             }
50             elsif (not defined $pid) {
51 0           $self->_log(error => "Fork failed", $!);
52 0           return;
53             }
54 0           $self->{pid} = $pid;
55 0           return $pid;
56             }
57              
58             sub _init_signal_handlers {
59 0     0     my $self = shift;
60 0           my $signal_count = 0;
61 0   0 0     $self->{signal_handler} //= sub { $signal_count++ };
  0            
62 0   0       $self->{signal_count_ref} //= \$signal_count;
63 0           $SIG{INT} = $self->{signal_handler};
64 0           $SIG{TERM} = $self->{signal_handler};
65             }
66              
67             sub _close_fds_but {
68 0     0     my ($self, @keep_fhs) = @_;
69 0           my @keep_fds = map fileno($_), @keep_fhs;
70              
71 0           $self->_log(debug => "Keeping fds: @keep_fds");
72 0   0       my $max_fd = POSIX::sysconf(POSIX::_SC_OPEN_MAX) || 1024;
73 0           for my $fd (3 .. $max_fd) {
74             POSIX::close($fd)
75 0 0         unless grep { $fd == $_ } @keep_fds;
  0            
76             }
77             }
78              
79             sub _loop {
80 0     0     my ($self, $tap_handle, $ssh_handle, $ssh_err_handle) = @_;
81              
82 0           my $tap2ssh_buff = '';
83 0           my $ssh2tap_buff = '';
84 0           my $err_buff = '';
85 0           my $pkt_buff;
86 0           my $max_buff_size = 65*1025; # 10KB buffer
87              
88 0           my $tap_fd = fileno($tap_handle);
89 0           my $ssh_fd = fileno($ssh_handle);
90 0           my $err_fd = fileno($ssh_err_handle);
91              
92 0           my $err_open = 1;
93 0           my $tunnel_closed;
94             my $close_later;
95 0           my $err_close_time_limit;
96              
97 0   0       while ($err_open and not ${$self->{signal_count_ref}}) {
  0            
98 0           my $ssh2tap_pkt_len;
99 0           my $rfds = '';
100 0           my $wfds = '';
101 0           my $efds = $rfds;
102              
103 0           vec($rfds, $err_fd, 1) = 1;
104 0 0         unless ($close_later) {
105 0 0         vec($rfds, $ssh_fd, 1) = 1 if length($ssh2tap_buff) < $max_buff_size;
106 0 0         vec($rfds, $tap_fd, 1) = 1 if length($tap2ssh_buff) < $max_buff_size;
107              
108 0 0         if (length($ssh2tap_buff) >= 2) {
109 0           $ssh2tap_pkt_len = unpack("n", $ssh2tap_buff);
110 0 0         if (length($ssh2tap_buff) >= $ssh2tap_pkt_len + 2) {
111 0           vec($wfds, $tap_fd, 1) = 1;
112             }
113             }
114 0 0         if (length($tap2ssh_buff) > 0) {
115 0           vec($wfds, $ssh_fd, 1) = 1;
116             }
117             }
118              
119 0           my $nfound = select($rfds, $wfds, $efds, 15);
120 0 0         next if $nfound <= 0;
121              
122 0 0         if (vec($rfds, $err_fd, 1)) {
123 0           my $n = sysread($ssh_err_handle, $err_buff, $max_buff_size, length($err_buff));
124 0 0         if (!defined $n) {
    0          
125 0           $self->_warn("Read from SSH error channel failed", $!);
126             }
127             elsif ($n == 0) {
128 0           $self->_log(info => "SSH error channel closed");
129 0           $close_later++;
130             }
131             else {
132 0           while ($err_buff =~ s/^(.*)\n//) {
133 0 0         next if $1 =~ /^\s*$/;
134 0           $self->_log(info => "Remote stderr", $1);
135             }
136 0           while (length($err_buff) >= 1500) {
137 0           $self->_log(ingo => "Remote stderr", substr($err_buff, 0, 1500)." (truncated)");
138 0           substr($err_buff, 0, 1500) = '';
139             }
140             }
141             }
142              
143 0 0         if (vec($rfds, $ssh_fd, 1)) {
144 0           my $n = sysread($ssh_handle, $ssh2tap_buff, $max_buff_size, length($ssh2tap_buff));
145 0 0         if (!defined $n) {
    0          
146 0           $self->_warn("Read from SSH failed", $!);
147             }
148             elsif ($n == 0) {
149 0           $self->_warn("SSH closed connection");
150 0           $close_later++;
151             }
152             }
153              
154 0 0         if (vec($rfds, $tap_fd, 1)) {
155 0           my $n = sysread($tap_handle, $pkt_buff, $max_buff_size);
156 0 0         if (!defined $n) {
    0          
157 0           $self->_warn("Read from TAP failed", $!);
158             }
159             elsif ($n == 0) {
160 0           $self->_warn("TAP closed connection");
161 0           $close_later++;
162             }
163             else {
164 0           $tap2ssh_buff .= pack("n", $n) . $pkt_buff;
165             }
166             }
167              
168 0 0         if (vec($wfds, $ssh_fd, 1)) {
169 0           my $n = syswrite($ssh_handle, $tap2ssh_buff, length($tap2ssh_buff));
170 0 0         if (!defined $n) {
    0          
171 0           $self->_warn("Write to SSH failed", $!);
172             }
173             elsif ($n == 0) {
174 0           $self->_warn("SSH closed connection");
175 0           $close_later++;
176             }
177             else {
178 0           substr($tap2ssh_buff, 0, $n) = '';
179             }
180             }
181              
182 0 0         if(vec($wfds, $tap_fd, 1)) {
183 0 0 0       if (not defined $ssh2tap_pkt_len or length($ssh2tap_buff) < $ssh2tap_pkt_len + 2) {
184 0           $self->_log(warn => "Unexpected write flag for TAP");
185             }
186             else {
187 0           my $n = syswrite($tap_handle, substr($ssh2tap_buff, 2, $ssh2tap_pkt_len));
188             # In any case, we remove the packet from the buffer. The TCP/IP magic!
189 0           substr($ssh2tap_buff, 0, $ssh2tap_pkt_len + 2) = '';
190 0 0         if (!defined $n) {
    0          
191 0           $self->_warn("Write to TAP failed", $!);
192             }
193             elsif ($n == 0) {
194 0           $self->_warn("TAP closed", $!);
195 0           $close_later++;
196             }
197             }
198             }
199              
200 0 0         if ($close_later) {
201 0           $self->_log(debug => "Closing tap and ssh sockets");
202 0           close($tap_handle);
203 0           close($ssh_handle);
204 0           $tunnel_closed++;
205 0           undef $close_later;
206 0           $err_close_time_limit = time + 10;
207             }
208              
209 0 0 0       if ($tunnel_closed and time > $err_close_time_limit) {
210 0           $self->_log(debug => "Closing error socket");
211 0           close($ssh_err_handle);
212 0           $err_open = 0;
213             }
214 0 0         $err_open-- if $tunnel_closed;
215             }
216             }
217              
218             1;