Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/hack/test-port-forwarding.pl
1637 views
1
#!/usr/bin/env perl
2
3
# This script tests the port forwarding settings of lima. It has to be run
4
# twice: once to update the instance yaml file with the port forwarding
5
# rules (before the instance is started). And once when the instance is
6
# running to perform the tests:
7
#
8
# ./hack/test-port-forwarding.pl templates/default.yaml
9
# limactl --tty=false start templates/default.yaml
10
# git restore templates/default.yaml
11
# ./hack/test-port-forwarding.pl default
12
#
13
# TODO: support for ipv6 host addresses
14
15
use strict;
16
use warnings;
17
18
use Config qw(%Config);
19
use IO::Handle qw();
20
use Socket qw(inet_ntoa);
21
use Sys::Hostname qw(hostname);
22
23
my $instance = shift;
24
25
my $addr = scalar gethostbyname(hostname());
26
# If hostname address cannot be determines, use localhost to trigger fallback to system_profiler lookup
27
my $ipv4 = length $addr ? inet_ntoa($addr) : "127.0.0.1";
28
my $ipv6 = ""; # todo
29
30
# macOS GitHub runners seem to use "localhost" as the hostname
31
if ($ipv4 eq "127.0.0.1" && $Config{osname} eq "darwin") {
32
$ipv4 = qx(system_profiler SPNetworkDataType -json | jq -r 'first(.SPNetworkDataType[] | select(.ip_address) | .ip_address) | first');
33
chomp $ipv4;
34
}
35
36
# If $instance is a filename, add our portForwards to it to enable testing
37
if (-f $instance) {
38
open(my $fh, "+< $instance") or die "Can't open $instance for read/write: $!";
39
my @yaml;
40
while (<$fh>) {
41
# Remove existing "portForwards:" section from the config file
42
my $seq = /^portForwards:/ ... /^[a-z]/;
43
next if $seq && $seq !~ /E0$/;
44
push @yaml, $_;
45
}
46
seek($fh, 0, 0);
47
truncate($fh, 0);
48
print $fh $_ for @yaml;
49
while (<DATA>) {
50
s/ipv4/$ipv4/g;
51
s/ipv6/$ipv6/g;
52
print $fh $_;
53
}
54
exit;
55
}
56
57
# Check if netcat is available before running tests
58
my $nc_path = `command -v nc 2>/dev/null`;
59
chomp $nc_path;
60
unless ($nc_path) {
61
die "Error: 'nc' (netcat) is not installed on the host system.\n" .
62
"Please install netcat to run this test script:\n" .
63
" - On macOS: brew install netcat\n" .
64
" - On Ubuntu/Debian: sudo apt-get install netcat\n" .
65
" - On RHEL/CentOS: sudo yum install nmap-ncat\n";
66
}
67
68
# Otherwise $instance must be the name of an already running instance that has been
69
# configured with our portForwards settings.
70
71
my $instanceType = qx(limactl ls --json "$instance" | jq -r '.vmType' | sed s/x/x/);
72
chomp $instanceType;
73
74
# Get sshLocalPort for lima instance
75
my $sshLocalPort;
76
open(my $ls, "limactl ls --json |") or die;
77
while (<$ls>) {
78
next unless /"name":"$instance"/;
79
($sshLocalPort) = /"sshLocalPort":(\d+)/ or die;
80
last;
81
}
82
die "Cannot determine sshLocalPort" unless $sshLocalPort;
83
84
# Extract forwarding tests from the "portForwards" section
85
my @test;
86
while (<DATA>) {
87
chomp;
88
s/^\s+#\s*//;
89
next unless /^(forward|ignore)/;
90
if (/ipv6/ && !$ipv6) {
91
printf " Not yet: # $_\n";
92
next;
93
}
94
s/sshLocalPort/$sshLocalPort/g;
95
s/ipv4/$ipv4/g;
96
s/ipv6/$ipv6/g;
97
# forward: 127.0.0.1 899 → 127.0.0.1 799
98
# ignore: 127.0.0.2 8888
99
/^(forward|ignore):\s+([0-9.:]+)\s+(\d+)(?:\s+)?(?:\s+([0-9.:]+)(?:\s+(\d+))?)?/;
100
die "Cannot parse test '$_'" unless $1;
101
my %test; @test{qw(mode guest_ip guest_port host_ip host_port)} = ($1, $2, $3, $4, $5);
102
$test{host_ip} ||= "127.0.0.1";
103
$test{host_port} ||= $test{guest_port};
104
if ($test{mode} eq "forward" && $test{host_port} < 1024 && $Config{osname} ne "darwin") {
105
printf "🚧 Not supported on $Config{osname}: # $_\n";
106
next;
107
}
108
if ($test{mode} eq "ignore" && ($test{guest_ip} eq "0.0.0.0" || $test{guest_ip} eq "127.0.0.1") && "$instanceType" eq "wsl2") {
109
printf "🚧 Not supported for $instanceType machines: # $_\n";
110
next;
111
}
112
if ($test{guest_ip} eq "192.168.5.15" && "$instanceType" eq "wsl2") {
113
printf "🚧 Not supported for $instanceType machines: # $_\n";
114
next;
115
}
116
117
my $remote = JoinHostPort($test{guest_ip},$test{guest_port});
118
my $local = JoinHostPort($test{host_ip},$test{host_port});
119
if ($test{mode} eq "ignore") {
120
$test{log_msg} = "Not forwarding TCP $remote";
121
}
122
else {
123
$test{log_msg} = "Forwarding TCP from $remote to $local";
124
}
125
push @test, \%test;
126
}
127
128
open(my $lima, "| limactl shell --workdir / $instance")
129
or die "Can't run lima shell on $instance: $!";
130
$lima->autoflush;
131
132
print $lima <<'EOF';
133
set -e
134
cd $HOME
135
sudo pkill -x nc || true
136
rm -f nc.*
137
EOF
138
139
# Give the hostagent some time to remove any port forwards from a previous (crashed?) test run
140
sleep 5;
141
142
# Record current log size, so we can skip prior output
143
$ENV{HOME_HOST} ||= "$ENV{HOME}";
144
$ENV{LIMA_HOME} ||= "$ENV{HOME_HOST}/.lima";
145
my $ha_log = "$ENV{LIMA_HOME}/$instance/ha.stderr.log";
146
my $ha_log_size = -s $ha_log or die;
147
148
# Setup a netcat listener on the guest for each test
149
foreach my $id (0..@test-1) {
150
my $test = $test[$id];
151
my $nc = "nc -l $test->{guest_ip} $test->{guest_port}";
152
if ($instance =~ /^alpine/) {
153
$nc = "nc -l -s $test->{guest_ip} -p $test->{guest_port}";
154
}
155
156
my $sudo = $test->{guest_port} < 1024 ? "sudo " : "";
157
print $lima "${sudo}${nc} >nc.${id} 2>/dev/null &\n";
158
}
159
160
# Make sure the guest- and hostagents had enough time to set up the forwards
161
sleep 5;
162
163
# Try to reach each listener from the host
164
foreach my $test (@test) {
165
next if $test->{host_port} == $sshLocalPort;
166
my $nc = "nc -w 1 $test->{host_ip} $test->{host_port}";
167
open(my $netcat, "| $nc") or die "Can't run '$nc': $!";
168
print $netcat "$test->{log_msg}\n";
169
# Don't check for errors on close; macOS nc seems to return non-zero exit code even on success
170
close($netcat);
171
}
172
173
# Extract forwarding log messages from hostagent log
174
open(my $log, "< $ha_log") or die "Can't read $ha_log: $!";
175
seek($log, $ha_log_size, 0) or die "Can't seek $ha_log to $ha_log_size: $!";
176
my %seen;
177
while (<$log>) {
178
$seen{$1}++ if /(Forwarding TCP from .*? to (\d.*?|\[.*?\]):\d+)/;
179
$seen{$1}++ if /(Not forwarding TCP .*?:\d+)/;
180
}
181
close $log or die;
182
183
my $rc = 0;
184
my %expected;
185
foreach my $id (0..@test-1) {
186
my $test = $test[$id];
187
my $err = "";
188
$expected{$test->{log_msg}}++;
189
unless ($seen{$test->{log_msg}}) {
190
$err .= "\n Message missing from ha.stderr.log";
191
}
192
my $log = qx(limactl shell --workdir / $instance sh -c "cd; cat nc.$id");
193
chomp $log;
194
if ($test->{mode} eq "forward" && $test->{log_msg} ne $log) {
195
$err .= "\n Guest received: '$log'";
196
}
197
if ($test->{mode} eq "ignore" && $log) {
198
$err .= "\n Guest received: '$log' (instead of nothing)";
199
}
200
printf "%s %s%s\n", ($err ? "❌" : "✅"), $test->{log_msg}, $err;
201
$rc = 1 if $err;
202
}
203
204
foreach (keys %seen) {
205
next if $expected{$_};
206
# Should this be an error? Really should only happen if something else failed as well.
207
print "😕 Unexpected log message: $_\n";
208
}
209
210
# Cleanup remaining netcat instances (and port forwards)
211
print $lima "sudo pkill -x nc";
212
213
exit $rc;
214
215
sub JoinHostPort {
216
my($host,$port) = @_;
217
$host = "[$host]" if $host =~ /:/;
218
return "$host:$port";
219
}
220
221
# This YAML section includes port forwarding `rules` for the guest- and hostagents,
222
# with interleaved `tests` (in comments) that are executed by this script. The strings
223
# "ipv4" and "ipv6" will be replaced by the actual host ipv4 and ipv6 addresses.
224
__DATA__
225
portForwards:
226
# We can't test that port 22 will be blocked because the guestagent has
227
# been ignoring it since startup, so the log message is in the part of
228
# the log we skipped.
229
# skip: 127.0.0.1 22 → 127.0.0.1 2222
230
# ignore: 127.0.0.1 sshLocalPort
231
232
- guestIP: 127.0.0.2
233
guestPortRange: [3000, 3009]
234
hostPortRange: [2000, 2009]
235
ignore: true
236
237
- guestIP: 0.0.0.0
238
guestPortRange: [3010, 3019]
239
hostPortRange: [2010, 2019]
240
ignore: true
241
242
- guestIP: 0.0.0.0
243
guestPortRange: [3000, 3029]
244
hostPortRange: [2000, 2029]
245
246
# The following rule is completely shadowed by the previous one and has no effect
247
- guestIP: 0.0.0.0
248
guestPortRange: [3020, 3029]
249
hostPortRange: [2020, 2029]
250
ignore: true
251
252
# ignore: 127.0.0.2 3000
253
# forward: 127.0.0.3 3001 → 127.0.0.1 2001
254
255
# Blocking 127.0.0.2 cannot block forwarding from 0.0.0.0
256
# forward: 0.0.0.0 3002 → 127.0.0.1 2002
257
258
# Blocking 0.0.0.0 will block forwarding from any interface
259
# ignore: 0.0.0.0 3010
260
# ignore: 127.0.0.1 3011
261
262
# Forwarding from 0.0.0.0 works for any interface (including IPv6)
263
# The "ignore" rule above has no effect because the previous rule already matched.
264
# forward: 127.0.0.2 3020 → 127.0.0.1 2020
265
# forward: 127.0.0.1 3021 → 127.0.0.1 2021
266
# forward: 0.0.0.0 3022 → 127.0.0.1 2022
267
# forward: :: 3023 → 127.0.0.1 2023
268
# forward: ::1 3024 → 127.0.0.1 2024
269
270
- guestPortRange: [3030, 3039]
271
hostPortRange: [2030, 2039]
272
hostIP: ipv4
273
274
# forward: 127.0.0.1 3030 → ipv4 2030
275
# forward: 0.0.0.0 3031 → ipv4 2031
276
# forward: :: 3032 → ipv4 2032
277
# forward: ::1 3033 → ipv4 2033
278
279
- guestPortRange: [300, 309]
280
281
# forward: 127.0.0.1 300 → 127.0.0.1 300
282
283
- guestPortRange: [310, 319]
284
hostIP: 0.0.0.0
285
286
# forward: 127.0.0.1 310 → 0.0.0.0 310
287
288
# Things we can't test:
289
# - Accessing a forward from a different interface (e.g. connect to ipv4 to connect to 0.0.0.0)
290
# - failed forward to privileged port
291
292
293
- guestIP: "192.168.5.15"
294
guestPortRange: [4000, 4009]
295
hostIP: "ipv4"
296
297
# forward: 192.168.5.15 4000 → ipv4 4000
298
299
- guestIP: "::1"
300
guestPortRange: [4010, 4019]
301
hostIP: "::"
302
303
# forward: ::1 4010 → :: 4010
304
305
- guestIP: "::"
306
guestPortRange: [4020, 4029]
307
hostIP: "ipv4"
308
309
# forward: 127.0.0.1 4020 → ipv4 4020
310
# forward: 127.0.0.2 4021 → ipv4 4021
311
# forward: 192.168.5.15 4022 → ipv4 4022
312
# forward: 0.0.0.0 4023 → ipv4 4023
313
# forward: :: 4024 → ipv4 4024
314
# forward: ::1 4025 → ipv4 4025
315
316
- guestIP: "0.0.0.0"
317
guestPortRange: [4030, 4039]
318
hostIP: "ipv4"
319
320
# forward: 127.0.0.1 4030 → ipv4 4030
321
# forward: 127.0.0.2 4031 → ipv4 4031
322
# forward: 192.168.5.15 4032 → ipv4 4032
323
# forward: 0.0.0.0 4033 → ipv4 4033
324
# forward: :: 4034 → ipv4 4034
325
# forward: ::1 4035 → ipv4 4035
326
327
- guestIPMustBeZero: true
328
guestPortRange: [4040, 4049]
329
330
- guestIP: "0.0.0.0"
331
guestPortRange: [4040, 4049]
332
ignore: true
333
334
# forward: 0.0.0.0 4040 → 127.0.0.1 4040
335
# forward: :: 4041 → 127.0.0.1 4041
336
# ignore: 127.0.0.1 4043 → 127.0.0.1 4043
337
# ignore: 192.168.5.15 4044 → 127.0.0.1 4044
338
339
# This rule exist to test `nerdctl run` binding to 0.0.0.0 by default,
340
# and making sure it gets forwarded to the external host IP.
341
# The actual test code is in test-example.sh in the "port-forwarding" block.
342
- guestIPMustBeZero: true
343
guestPort: 8888
344
hostIP: 0.0.0.0
345
346