Repository: lima-vm/lima
Pull request #667 (2022-02-19 04:49:15)

iptables rule without an ip range applies to all interfaces
The current logic pretends it only applies to `127.0.0.1`, which means the new `guestIPMustBeZero` rule does not detect it properly.

This is a problem with `nerdctl` and `containerd` on Alpine. It works "by accident" on Ubuntu because the port was also bound to `[::]`, which had an entry in `/proc/net/tcp6`.

This issue does not affect moby because moby doesn't rely on `iptables` rules but has proper entries in `/proc/net/tcp`.



Patch:
diff --git a/.cirrus.yml b/.cirrus.yml
index 1ebdb91e..00bdaf82 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -30,7 +30,7 @@ task:
   - cat /proc/cpuinfo
   install_deps_script:
   - apt-get update
-  - apt-get install -y --no-install-recommends ca-certificates curl git golang openssh-client make netcat ovmf sudo qemu-system-x86 qemu-utils
+  - apt-get install -y --no-install-recommends ca-certificates curl git golang jq openssh-client make netcat ovmf sudo qemu-system-x86 qemu-utils
   go_cache:
     fingerprint_script: uname -s ; cat go.sum
     folder: $GOPATH/pkg/mod
diff --git a/pkg/guestagent/iptables/iptables.go b/pkg/guestagent/iptables/iptables.go
index e2ba72d4..fbd03f69 100644
--- a/pkg/guestagent/iptables/iptables.go
+++ b/pkg/guestagent/iptables/iptables.go
@@ -75,12 +75,10 @@ func parsePortsFromRules(rules []string) ([]Entry, error) {
 					istcp = true
 				}
 
-				// if the IP is blank the port forwarding the portforwarding,
-				// which gets information from this, will skip it. When no IP
-				// is present localhost will work.
+				// When no IP is present the rule applies to all interfaces.
 				ip := found[1]
 				if ip == "" {
-					ip = "127.0.0.1"
+					ip = "0.0.0.0"
 				}
 				ent := Entry{
 					IP:   net.ParseIP(ip),


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index bd31e07b..6f55c380 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -121,6 +121,9 @@ jobs:
       # QEMU:      required by Lima itself
       # bash:      required by test-example.sh (OS version of bash is too old)
       # coreutils: required by test-example.sh for the "timeout" command
+      # These would need to be added to run the alpine.yaml config on the macOS runner:
+      # curl:      required by test-example.sh to download nerdctl for alpine
+      # jq:        required by test-example.sh to determine download URL for nerdctl
       run: |
         set -x
         # Github runners seem to symlink to python2.7 version of 2to3,
diff --git a/hack/test-example.sh b/hack/test-example.sh
index 8b4c9b45..f6c1b63b 100755
--- a/hack/test-example.sh
+++ b/hack/test-example.sh
@@ -168,12 +168,14 @@ if [[ -n ${CHECKS["mount-home"]} ]]; then
 	fi
 fi
 
+# Use GHCR to avoid hitting Docker Hub rate limit
+nginx_image="ghcr.io/stargz-containers/nginx:1.19-alpine-org"
+alpine_image="ghcr.io/containerd/alpine:3.14.0"
+
 if [[ -n ${CHECKS["containerd-user"]} ]]; then
 	INFO "Run a nginx container with port forwarding 127.0.0.1:8080"
 	set -x
 	limactl shell "$NAME" nerdctl info
-	# Use GHCR to avoid hitting Docker Hub rate limit
-	nginx_image="ghcr.io/stargz-containers/nginx:1.19-alpine-org"
 	limactl shell "$NAME" nerdctl pull --quiet ${nginx_image}
 	limactl shell "$NAME" nerdctl run -d --name nginx -p 127.0.0.1:8080:80 ${nginx_image}
 
@@ -189,7 +191,6 @@ if [[ -n ${CHECKS["containerd-user"]} ]]; then
 		mkdir -p "$hometmp"
 		defer "rm -rf \"$hometmp\""
 		set -x
-		alpine_image="ghcr.io/containerd/alpine:3.14.0"
 		limactl shell "$NAME" nerdctl pull --quiet ${alpine_image}
 		echo "random-content-${RANDOM}" >"$hometmp/random"
 		expected="$(cat "$hometmp/random")"
@@ -219,6 +220,34 @@ if [[ -n ${CHECKS["port-forwards"]} ]]; then
 		limactl shell "$NAME" sudo zypper in -y netcat-openbsd
 	fi
 	"${scriptdir}/test-port-forwarding.pl" "${NAME}"
+
+	if [[ -n ${CHECKS["containerd-user"]} || ${NAME} == "alpine" ]]; then
+		INFO "Testing that 'nerdctl run' binds to 0.0.0.0 by default and is forwarded to the host"
+		if [ "$(uname)" = "Darwin" ]; then
+			# macOS runners seem to use `localhost` as the hostname, so the perl lookup just returns `127.0.0.1`
+			hostip=$(system_profiler SPNetworkDataType -json | jq -r 'first(.SPNetworkDataType[] | select(.ip_address) | .ip_address) | first')
+		else
+			hostip=$(perl -MSocket -MSys::Hostname -E 'say inet_ntoa(scalar gethostbyname(hostname()))')
+		fi
+		if [ -n "${hostip}" ]; then
+			sudo=""
+			if [ "${NAME}" = "alpine" ]; then
+				arch=$(limactl info | jq -r .defaultTemplate.arch)
+				nerdctl=$(limactl info | jq -r ".defaultTemplate.containerd.archives[] | select(.arch==\"$arch\").location")
+				curl -Lso nerdctl-full.tgz "${nerdctl}"
+				limactl shell "$NAME" sudo apk add containerd
+				limactl shell "$NAME" sudo rc-service containerd start
+				limactl shell "$NAME" sudo tar xzf "${PWD}/nerdctl-full.tgz" -C /usr/local
+				rm nerdctl-full.tgz
+				sudo="sudo"
+			fi
+			limactl shell "$NAME" $sudo nerdctl info
+			limactl shell "$NAME" $sudo nerdctl pull --quiet ${nginx_image}
+			limactl shell "$NAME" $sudo nerdctl run -d --name nginx -p 8888:80 ${nginx_image}
+
+			timeout 3m bash -euxc "until curl -f --retry 30 --retry-connrefused http://${hostip}:8888; do sleep 3; done"
+		fi
+	fi
 	set +x
 fi
 
diff --git a/hack/test-port-forwarding.pl b/hack/test-port-forwarding.pl
index e84e0254..980e7bd6 100755
--- a/hack/test-port-forwarding.pl
+++ b/hack/test-port-forwarding.pl
@@ -304,3 +304,10 @@ portForwards:
   # forward: ::             4041 → 127.0.0.1 4041
   # ignore:  127.0.0.1      4043 → 127.0.0.1 4043
   # ignore:  192.168.5.15   4044 → 127.0.0.1 4044
+
+# This rule exist to test `nerdctl run` binding to 0.0.0.0 by default,
+# and making sure it gets forwarded to the external host IP.
+# The actual test code is in test-example.sh in the "port-forwarding" block.
+- guestIPMustBeZero: true
+  guestPort: 8888
+  hostIP: 0.0.0.0
diff --git a/pkg/guestagent/iptables/iptables_test.go b/pkg/guestagent/iptables/iptables_test.go
index eb58d447..7d9bf9be 100644
--- a/pkg/guestagent/iptables/iptables_test.go
+++ b/pkg/guestagent/iptables/iptables_test.go
@@ -84,8 +84,8 @@ func TestParsePortsFromRules(t *testing.T) {
 		t.Fatalf("expected 2 ports parsed from iptables but parsed %d", l)
 	}
 
-	if res[0].IP.String() != "127.0.0.1" || res[0].Port != 8082 || res[0].TCP != true {
-		t.Errorf("expected port 8082 on IP 127.0.0.1 with TCP true but go port %d on IP %s with TCP %t", res[0].Port, res[0].IP.String(), res[0].TCP)
+	if res[0].IP.String() != "0.0.0.0" || res[0].Port != 8082 || res[0].TCP != true {
+		t.Errorf("expected port 8082 on IP 0.0.0.0 with TCP true but got port %d on IP %s with TCP %t", res[0].Port, res[0].IP.String(), res[0].TCP)
 	}
 	if res[1].IP.String() != "127.0.0.1" || res[1].Port != 8081 || res[1].TCP != true {
 		t.Errorf("expected port 8081 on IP 127.0.0.1 with TCP true but go port %d on IP %s with TCP %t", res[1].Port, res[1].IP.String(), res[1].TCP)