-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathnet-ssh
executable file
·163 lines (139 loc) · 8.07 KB
/
net-ssh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
#!/usr/bin/perl
#
# Join a remote LAN via SSH
# requirements (both client & server): perl, iproute2, iptables; root access
# run as "root". Ensure server is configured with "PermitTunnel yes".
# https://help.ubuntu.com/community/SSH_VPN/
# https://grox.net/sysadm/net/ssh.vpn.tun.howto
#
use strict;
use warnings;
use Getopt::Long qw/:config no_ignore_case bundling/;
$0 = join(' ', ($0 =~ s{^.*?([^/]+)$}{$1}sr), @ARGV);
Getopt::Long::GetOptions(
'server|s:s' => \(my $ssh_server), # --server=[user@]host[:port] # remote SSH gate-in server; default usr=root, default port=22
'tun-spec|t:s' => \(my $tun_spec = "1:1"), # e.g. --tun-spec=1:2 see 'man ssh' option -w
'tun-addr|T:s' => \(my $tun_addr = '10.190.190.250:251'), # tunnel ip address(es) (peer address is optional) e.g. --tun-addr=10.0.0.7[:10.0.0.8]
'net-remote|net|N=s@' => \(my $nets_remote), # remote network(s) you want to VPN into, --net-remote=172.30.208.0/20 --net-remote=172.31.111.0/24
'dry-run|dry' => \(my $dry_run), # don't execute local commands, just show them
'help' => \(my $help) # read comments for the options above
# the unrecognized options and everything after '--' will be passed as options to "ssh"
);
STDOUT->autoflush(1);
STDERR->autoflush(1);
my @ssh_extra = (@ARGV); # the remainder & the unknown arguments - just pass to ssh
shift @ssh_extra if scalar(@ssh_extra) && $ssh_extra[0] eq '--';
die("See source header for help") if $help;
die("Please specify at least one remote network to join") if !$nets_remote;
my $invalid_nets = join(', ', grep { !m{^(?:\d+\.){3}\d+/\d+$}s } @$nets_remote);
die("Invalid remote network spec(s): $invalid_nets") if $invalid_nets;
($ssh_server =~ m{^ (?:(\w+)\@)? ( \w[\w\-]+ | \d{1,3}(?:\.\d{1,3}){3} | \[[\w\:]+\] ) (?:\:(\d+))? $}sx) or die("Bad server spec");
my ($ssh_user, $ssh_host, $ssh_port) = ($1 // 'root', $2, $3);
my ($dev_tun_lo, $dev_tun_re) = ($tun_spec =~ m{^(\d+) \: (\d+)$}sx) ? ('tun'.$1, 'tun'.$2) : die("Bad tunnel sequence spec");
my ($addr_tun_lo, $tun_pfx, $tun_sfx_lo, $tun_sfx_re, $addr_tun_re) =
($tun_addr =~ m{^(((?:\d+\.){3}) (\d+)) (?:[,:] (?: (\d+) | ((?:\d+\.){3} \d+) ))? $}x) or die("Invalid '--tun-addr' spec");
$addr_tun_re //= $addr_tun_re // defined($tun_sfx_re) ? $tun_pfx.$tun_sfx_re : $tun_pfx.($tun_sfx_lo + 1);
die("Run me as root") if ($ENV{USER} // $ENV{LOGNAME} // qx{whoami}) !~ /^root$/m;
my $cmd_params = {
dev_tun_lo => $dev_tun_lo, # tunnel interface on the client, e.g. "tun1"
dev_tun_re => $dev_tun_re, # tunnel interface on the remote server, e.g. "tun1"
addr_tun_lo => $addr_tun_lo, # client tunnel interface IP
addr_tun_re => $addr_tun_re, # remote tunnel interface IP; +1 if not specified
nets_remote => join(',', @$nets_remote) # remote LAN[s] to join
};
# local cleanup
my $local_cleanup_cmd = <<'EOF' =~ s{^\h*\#\V*\n}{}gmr =~ s{^\h+}{}gmr =~ s{ (\$\{ (\w+) \}) }{ $cmd_params->{$2} // $1 }gexr;
/sbin/ip route | perl -ne 'm{^(\S+)(?=\h) .*? \h+ dev \h+ \Q${dev_tun_lo}\E}x and system("/sbin/ip route del $1")'
EOF
print("Local cleanup:\n$local_cleanup_cmd\n");
system($local_cleanup_cmd) if !$dry_run;
# initialize local routing
my $local_init_cmd = <<"EOF";
/sbin/ip link set $dev_tun_lo up
/sbin/ip addr add $addr_tun_lo peer $addr_tun_re/32 dev $dev_tun_lo
EOF
for my $rnet (@$nets_remote) { # route packets to the remote LAN[s] on the client via the new tunnel interface
$local_init_cmd .= "/sbin/ip route add $rnet metric 1000 via $addr_tun_lo" . "\n";
}
$local_init_cmd = $local_init_cmd =~ s{\t+}{}gsr =~ s{\n+}{; }gsr =~ s{^\h*\#\V*\n}{}gmr =~ s{\s*;*\s*$}{}sr;
# remote perl code to execute on the server upon connection
my $remote_cmd = <<'REMOTE_CODE' =~ s{ (\$\{ (\w+) \}) }{ $cmd_params->{$2} // $1 }gexr =~ s{^\h*\#\V*\n}{}gmr;
use strict;
use warnings;
STDOUT->autoflush(1);
STDERR->autoflush(1);
$ENV{PATH} .= ':/usr/sbin' if $ENV{PATH} !~ m{(?:^|:)/usr/sbin(?:$|:)}m && -d '/usr/sbin';
$ENV{PATH} .= ':/sbin' if $ENV{PATH} !~ m{(?:^|:)/sbin(?:$|:)}m && -d '/sbin';
my $ip_route = qx{/sbin/ip route; /sbin/ip a};
my $ifaces_re_map = {};
my $net_addr_re_map = {};
for my $net_re (split(/,/, '${nets_remote}')) {
my ($iface, $iface_addr_re) = ($ip_route =~ m{^\h* \Q$net_re\E \h+ dev \h+ (\S+) \V+? \h+ src \h+ ([\d\.]+) }mx)
or die("Failed to detect remote network interface for '$net_re'");
$ifaces_re_map->{$iface} = 1;
$net_addr_re_map->{$net_re} = $iface_addr_re;
}
die("Missing server tunnel interface: ${dev_tun_re}; ensure 'PermitTunnel yes'") if $ip_route !~ m{^ \h*\d+: \h+ \Q${dev_tun_re}\E: \h }mx;
die("Failed to detect server configuration") if scalar(%$ifaces_re_map) == 0;
my $rule_tag = "-m comment --comment ${dev_tun_re}".'x'.$$; # tag created iptables rules; it helps with purging them at the end
my $init_net_cmd = <<'SRV_INIT' =~ s{^\h+}{}gmr; # init network, shell script
# prepare / cleanup tunnel resources we need for this connection to work; likely redundant
iptables -t filter -S | perl -ne 'm{^-A (.*? -[oi] \Q${dev_tun_re}\E .*)} and system("iptables -t filter -D $1")'
iptables -t nat -S | perl -ne 'm{^-A (.*?\Q -s ${addr_tun_lo}/\E\d+.*)} and system("iptables -t nat -D $1")'
echo 1 > /proc/sys/net/ipv4/ip_forward
# prepare/setup tunnel device & IP
/sbin/ip link set ${dev_tun_re} up
/sbin/ip addr add ${addr_tun_re} peer ${addr_tun_lo}/32 dev ${dev_tun_re}
SRV_INIT
# the following FORWARDs are only necessary when the default filter FORWARD policy is DROP
for my $riface (keys %$ifaces_re_map) { # allow 2-way traffic between interfaces tunX and enoYY
$init_net_cmd .= "iptables -t filter -I FORWARD 1 -i $riface -o ${dev_tun_re} $rule_tag -m state --state ESTABLISHED,RELATED -j ACCEPT" . "\n";
$init_net_cmd .= "iptables -t filter -I FORWARD 2 -i ${dev_tun_re} -o $riface $rule_tag -j ACCEPT" . "\n";
}
# MASQUERADE client packets, use local server LAN IP address
while (my ($net_re, $iface_addr_re) = each (%$net_addr_re_map)) { # mask incoming client IP with local
$init_net_cmd .= "iptables -t nat -A POSTROUTING -s ${addr_tun_lo}/32 -d $net_re $rule_tag -j SNAT --to-source $iface_addr_re" . "\n";
}
print("Server init:\n$init_net_cmd\n");
system($init_net_cmd);
# commands to cleanup the tagged iptable rules created in this session
my $srv_cleanup_cmd = <<'SRV_CLR' =~ s{^\h+}{}gmr =~ s{(\$(\w+))\b}{{RULE_TAG=>$rule_tag}->{$2}//$1}ger; # cleanup after
iptables -t filter -S | perl -ne 'm{^-A (.*?\Q $RULE_TAG \E.*)} and system("iptables -t filter -D $1")'
iptables -t nat -S | perl -ne 'm{^-A (.*?\Q $RULE_TAG \E.*)} and system("iptables -t nat -D $1")'
SRV_CLR
print("This will run right after SSHD shuts down:\n$srv_cleanup_cmd");
my $sshd_pid = getppid(); # wait for this PID to vanish, then cleanup iptables rule created by this script
my $finalizer_cmdz = <<'FINALIZER' =~ s{^\h+}{}gmr =~ s{(\$(\w+))\b}{{SSHD_PID=>$sshd_pid,CLEAN_CMDZ=>$srv_cleanup_cmd}->{$2}//$1}ger;
$0 = "net-ssh finalizer ${dev_tun_re}-$SSHD_PID"; # change process title
my $sshd_stat_file = "/proc/$SSHD_PID/cmdline"; # SSHD process file
my $sshd_ctime = (-C $sshd_stat_file).''; # inode change timestamp
sleep(1) while $sshd_ctime eq -C $sshd_stat_file; # in memory '/proc' file stat is free (CPU use < 3sec/24hr)
exec(<<'CLEANUP_CMDZ');
$CLEAN_CMDZ
CLEANUP_CMDZ
FINALIZER
# install/launch the finalizer as a daemon
exec("( (
/usr/bin/perl <<'FINALIZER'
$finalizer_cmdz
FINALIZER
)& )&");
REMOTE_CODE
print("To be run on the server:\n$remote_cmd\n") if $dry_run;
# all the "VPN" action is here
push(@ssh_extra, '-p', $ssh_port) if $ssh_port;
my @ssh_cmd = ('ssh', '-o', 'PermitLocalCommand yes', '-o', "LocalCommand $local_init_cmd", '-w', $tun_spec, @ssh_extra, "$ssh_user\@$ssh_host", '--', '/usr/bin/perl');
print("SSH cmd:\n".join(' ', @ssh_cmd)."\n\n");
if (!$dry_run) {
$SIG{'INT'} = $SIG{'TERM'} = $SIG{'HUP'} = \&net_cleanup; # handle dying gracefully
open(my $remote_hcmd, '|-', @ssh_cmd); # run ssh
print $remote_hcmd $remote_cmd; # send perl code to be executed on the remote server
print("Remote initialization commands sent\n");
close($remote_hcmd); # this will stop and wait until connection terminates
print("SSH disconnected\n");
}
net_cleanup();
sub net_cleanup {
print("Client cleanup:\n$local_cleanup_cmd");
exec($local_cleanup_cmd) if !$dry_run;
}