Repository: lima-vm/lima

Issue:
UDP port forwarding not working unless the port number is explicitly specified
Just some context:

- **macOS Version:** `15.6.1`
- **Lima Version:** `limactl version 1.2.1`

Hi everyone, I'm unsure if I'm doing something wrong or if there’s an issue with UDP functionality in my setup.

I’m trying to run the project available at [sparkfabrik/http-proxy](https://github.com/sparkfabrik/http-proxy), which includes a basic DNS resolver that resolves all `.loc` domains to `127.0.0.1`. This tool is meant for development, and you can find the source code for the DNS server [here](https://github.com/sparkfabrik/http-proxy/blob/main/cmd/dns-server/main.go).

The DNS server exposes a single UDP port at `:19322`. You can test it using the following Docker command:

```shell
docker run --name=dns-server -d -p 19322:19322/udp ghcr.io/sparkfabrik/http-proxy-services /usr/local/bin/dns-server
```

When using Docker Desktop, I typically test the server with:

```shell
dig @127.0.0.1 -p19322 test.spark.loc +short
```

However, when I try the same command on Lima, it doesn’t work. I even attempted to set `export LIMA_SSH_PORT_FORWARDER=false` before starting the VM, but that didn’t resolve the issue. If I understand correctly, gRPC is now the default protocol.

The Lima machine was created with the following command:

```shell
limactl create --name="test-udp" template://docker
```

You can find here a simple script to replicate the issue:

```
#!/usr/bin/env bash

set -euo pipefail

VM_NAME="test-udp"
ORIGINAL_CONTEXT=""

setup_lima_vm() {
    local vm_name="${1:-${VM_NAME}}"

    echo "--- Lima Version ---"
    limactl --version

    echo "--- Creating Lima VM: ${vm_name} ---"
    if ! limactl list | grep -q "^${vm_name}"; then
        limactl create -y --name="${vm_name}" template://docker
    fi

    echo "--- Starting Lima VM with LIMA_SSH_PORT_FORWARDER=false ---"
    export LIMA_SSH_PORT_FORWARDER=false
    limactl start "${vm_name}"
    docker context create "${vm_name}" --docker "host=$(limactl list docker "{$vm_name}" --format 'unix://{{.Dir}}/sock/docker.sock')" || true
    docker context use "${vm_name}"
    docker run hello-world
    echo "✅ Lima VM '${vm_name}' is ready"
}

cleanup_lima_vm() {
    echo "Cleaning up Lima VM: ${VM_NAME}..."
    limactl stop "${VM_NAME}" >/dev/null 2>&1 || true
    limactl delete "${VM_NAME}" >/dev/null 2>&1 || true
    docker context rm "${VM_NAME}" >/dev/null 2>&1 || true
}

cleanup() {
    echo "Cleaning up dns-server container..."
    docker rm -f dns-server >/dev/null 2>&1 || true
    cleanup_lima_vm

    if [[ -n "${ORIGINAL_CONTEXT}" ]]; then
        echo "Restoring original Docker context: ${ORIGINAL_CONTEXT}"
        docker context use "${ORIGINAL_CONTEXT}" >/dev/null 2>&1 || true
    fi
}

trap cleanup EXIT

run_test() {
    local context_name="$1"
    echo "---"
    echo "Switching to Docker context: ${context_name}"
    if ! docker context use "${context_name}"; then
        echo "Failed to switch to context '${context_name}'. Skipping test."
        return
    fi

    echo "Starting dns-server container..."
    docker run --name=dns-server -d -p 19322:19322/udp ghcr.io/sparkfabrik/http-proxy-services /usr/local/bin/dns-server

    sleep 2

    echo "Querying DNS server..."
    if dig @localhost -p 19322 test.loc +short | grep -q "127.0.0.1"; then
        echo "✅ DNS test PASSED for context: ${context_name}"
    else
        echo "❌ DNS test FAILED for context: ${context_name}"
    fi

    docker rm -f dns-server >/dev/null 2>&1 || true
    echo "---"
    echo
}

cleanup

echo "--- Saving original Docker context ---"
ORIGINAL_CONTEXT=$(docker context show)
echo "Original context: ${ORIGINAL_CONTEXT}"

setup_lima_vm
docker context use "${VM_NAME}"

run_test "desktop-linux"
run_test "${VM_NAME}"

echo "--- Restoring original Docker context ---"
docker context use "${ORIGINAL_CONTEXT}"
echo "Restored to context: ${ORIGINAL_CONTEXT}"

echo "All tests completed."
```

That in my case is:

```shell
❯ ./testcase-udp-ports.sh
Cleaning up dns-server container...
---
Switching to Docker context: desktop-linux
desktop-linux
Current context is now "desktop-linux"
Starting dns-server container...
9c5813ede7a3d4181c07ec0ea718a49eacb137528ff0eb9d42f6b74e413e5873
Querying DNS server...
✅ DNS test PASSED for context: desktop-linux
Cleaning up dns-server container...
---

---
Switching to Docker context: lima-sf-docker-dev
lima-sf-docker-dev
Current context is now "lima-sf-docker-dev"
Starting dns-server container...
abde38128be7994db452b82cf77a5f9b31f57d5b4b3d48576143bdf1136e78e7
Querying DNS server...
❌ DNS test FAILED for context: lima-sf-docker-dev
Cleaning up dns-server container...
---

All tests completed.
Cleaning up dns-server container...
```

Thanks!


Discussion:
@paolomainardi: Just tried the `v.1.2.0` release which didn't contain an UDP port-forwarding fix, but the issue is still there.

I tried a way simpler test case reported here: https://github.com/lima-vm/lima/pull/3724

Basically:

* `docker run --rm -p 5053:53/udp -p 5053:53/tcp coredns/coredns:latest`
* `dig @127.0.0.1 -p 5053 example.com` **KO**
* `dig @127.0.0.1 -p 5053 example.com +tcp`: **OK**@paolomainardi: I did some progress watching the logs:

```shell
==> /Users/paolomainardi/.lima/sf-docker-dev/ha.stderr.log <==
{"level":"debug","msg":"guest agent event: time:{seconds:1758181963  nanos:531357366}  local_ports_added:{protocol:\"tcp\"  ip:\"0.0.0.0\"  port:5053}  local_ports_added:{protocol:\"tcp\"  ip:\"::\"  port:5053}  local_ports_added:{protocol:\"udp\"  ip:\"0.0.0.0\"  port:5053}  local_ports_added:{protocol:\"udp\"  ip:\"::\"  port:5053}","time":"2025-09-18T09:52:43+02:00"}
{"level":"info","msg":"Forwarding TCP from 0.0.0.0:5053 to 127.0.0.1:5053","time":"2025-09-18T09:52:43+02:00"}
{"level":"info","msg":"Forwarding TCP from [::]:5053 to 127.0.0.1:5053","time":"2025-09-18T09:52:43+02:00"}
{"level":"info","msg":"Not forwarding UDP 0.0.0.0:5053","time":"2025-09-18T09:52:43+02:00"}
{"level":"info","msg":"Not forwarding UDP [::]:5053","time":"2025-09-18T09:52:43+02:00"}
{"level":"debug","msg":"guest agent event: time:{seconds:1758181975  nanos:541742183}  local_ports_added:{protocol:\"udp\"  ip:\"0.0.0.0\"  port:57971}","time":"2025-09-18T09:52:55+02:00"}
{"level":"info","msg":"Not forwarding UDP 0.0.0.0:57971","time":"2025-09-18T09:52:55+02:00"}
{"level":"debug","msg":"guest agent event: time:{seconds:1758181993  nanos:536052105}  local_ports_removed:{protocol:\"udp\"  ip:\"0.0.0.0\"  port:57971}","time":"2025-09-18T09:53:13+02:00"}
```
UDP ports are not being forwarded from the host agent; the next step is to understand why.@paolomainardi: Ok I fixed it, I didn't understand that UDP port forwarding was enabled by default but ports must be specified, I started from a template and badly assumed that port forwarding was already in place, as the TCP one.

I fixed by creating my lima VM with this template:

```yaml
    base:
    - template://docker-rootful

    # Add UDP port forwarding for container networking
    portForwards:
    - guestIP: "127.0.0.1"
      guestPort: 19322
      hostIP: "127.0.0.1"
      hostPort: 19322
      proto: "udp"
    EOF
```

As I need this port for now, I'm not yet sure if it's better to forward a range, but at least now it works as expected for the use case.

I suggest improving the documentation or startup logs to help users like me avoid a deep dive into the port forwarding section, even though it was quite interesting.@AkihiroSuda: > ports must be specified

This still seems a bug. There might be a condition where `/proc/net/udp*` watcher in the guestagent fails to detect the UDP port

@AkihiroSuda: > UDP port forwarding not working on MacOS

Does this issue title imply that the feature works fine on Linux hosts?
@paolomainardi: > > UDP port forwarding not working on MacOS
> 
> Does this issue title imply that the feature works fine on Linux hosts?

The title may be somewhat misleading; I have only tested it on macOS.@paolomainardi: > > ports must be specified
> 
> This still seems a bug. There might be a condition where `/proc/net/udp*` watcher in the guestagent fails to detect the UDP port

Take my consideration with a grain of salt, just trying to understand the codebase and the data structures.

Isn't the default configuration here: https://github.com/lima-vm/lima/blob/master/pkg/limayaml/defaults.go#L904 to forward TCP unless specified otherwise?  @AkihiroSuda: The guestagent seems able to detect the port, but the hostagent seems rejecting the port with "Not forwarding UDP" in `ha.stderr.log`.

The culprit is around here

https://github.com/lima-vm/lima/blob/cc46912dde67e12146c3a817cfde46f7c3e693f7/pkg/portfwd/forward.go#L47-L55

https://github.com/lima-vm/lima/blob/cc46912dde67e12146c3a817cfde46f7c3e693f7/pkg/portfwd/forward.go#L70-L101@AkihiroSuda: > Isn't the default configuration here: https://github.com/lima-vm/lima/blob/master/pkg/limayaml/defaults.go#L904 to forward TCP unless specified otherwise?


Right, submitted a PR:
- https://github.com/lima-vm/lima/pull/4054


Curious if this was a recent regression, or it had been disabled on purpose, or just broken since the initial implementation@paolomainardi: Thank you! I'm glad I noticed it, though.

Solution patch:
diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go
index 1c96ff8e..c0d95eff 100644
--- a/pkg/limayaml/defaults.go
+++ b/pkg/limayaml/defaults.go
@@ -901,7 +901,7 @@ func executeHostTemplate(format, instDir string, param map[string]string) (bytes
 
 func FillPortForwardDefaults(rule *limatype.PortForward, instDir string, user limatype.User, param map[string]string) {
 	if rule.Proto == "" {
-		rule.Proto = limatype.ProtoTCP
+		rule.Proto = limatype.ProtoAny
 	}
 	if rule.GuestIP == nil {
 		if rule.GuestIPMustBeZero {
diff --git a/templates/default.yaml b/templates/default.yaml
index d1a00616..0e28c09d 100644
--- a/templates/default.yaml
+++ b/templates/default.yaml
@@ -508,6 +508,7 @@ networks:
 #
 # # Lima internally appends this fallback rule at the end:
 # - guestIP: "127.0.0.1"
+#   proto: "any"
 #   guestPortRange: [1, 65535]
 #   hostIP: "127.0.0.1"
 #   hostPortRange: [1, 65535]



Test patch:
diff --git a/pkg/limayaml/defaults_test.go b/pkg/limayaml/defaults_test.go
index 83857f50..aa9a8253 100644
--- a/pkg/limayaml/defaults_test.go
+++ b/pkg/limayaml/defaults_test.go
@@ -128,7 +128,7 @@ func TestFillDefault(t *testing.T) {
 		GuestPortRange: [2]int{1, 65535},
 		HostIP:         IPv4loopback1,
 		HostPortRange:  [2]int{1, 65535},
-		Proto:          limatype.ProtoTCP,
+		Proto:          limatype.ProtoAny,
 		Reverse:        false,
 	}