Repository: lima-vm/lima
Pull request #547 (2022-01-13 00:30:22)

HostResolver shouldn't return IPv6 addresses by default
They don't work over the SLIRP interface, and sometimes don't even work over a bridged vmnet interface.

See also https://github.com/rancher-sandbox/rancher-desktop/issues/1193

This PR deprecates the `useHostResolver` setting and replaces it with `hostResolver.enabled`. The old setting can still be used, but if the new property is set, any old setting in the same file is being ignored. Generating proper warnings gets pretty complex when the interaction between `default.yaml`, `lima.yaml`, and `override.yaml` have to be considered.

It could be argued that `HostResolver.LookupIPv6` should default to `true` for compatibility with previous releases. However, given that it seems to be broken in some setups, disabling IPv6 by default seems like a safer choice to me.

Also includes one bug fix for `limactl debug dns 5533 5533`, which previously ignored the TCP port.

Aside: I needed to include the TCP port during testing because `limactl debug dns 5533` (without `-6`) option resulted in:

```console
$ dig @127.0.0.1 -p 5533 AAAA google.com
;; Truncated, retrying in TCP mode.
;; Connection to 127.0.0.1#5533(127.0.0.1) for google.com failed: connection refused.
```


Patch:
diff --git a/cmd/limactl/debug.go b/cmd/limactl/debug.go
index 2554cee9..0ce89bc2 100644
--- a/cmd/limactl/debug.go
+++ b/cmd/limactl/debug.go
@@ -27,22 +27,27 @@ func newDebugDNSCommand() *cobra.Command {
 		Args:  cobra.RangeArgs(1, 2),
 		RunE:  debugDNSAction,
 	}
+	cmd.Flags().BoolP("ipv6", "6", false, "lookup IPv6 addresses too")
 	return cmd
 }
 
 func debugDNSAction(cmd *cobra.Command, args []string) error {
+	ipv6, err := cmd.Flags().GetBool("ipv6")
+	if err != nil {
+		return err
+	}
 	udpLocalPort, err := strconv.Atoi(args[0])
 	if err != nil {
 		return err
 	}
 	tcpLocalPort := 0
-	if len(args) > 2 {
+	if len(args) > 1 {
 		tcpLocalPort, err = strconv.Atoi(args[1])
 		if err != nil {
 			return err
 		}
 	}
-	srv, err := dns.Start(udpLocalPort, tcpLocalPort)
+	srv, err := dns.Start(udpLocalPort, tcpLocalPort, ipv6)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/cidata/cidata.go b/pkg/cidata/cidata.go
index 1a045565..a4049c93 100644
--- a/pkg/cidata/cidata.go
+++ b/pkg/cidata/cidata.go
@@ -138,7 +138,7 @@ func GenerateISO9660(instDir, name string, y *limayaml.LimaYAML, udpDNSLocalPort
 	if err != nil {
 		return err
 	}
-	if *y.UseHostResolver {
+	if *y.HostResolver.Enabled {
 		args.UDPDNSLocalPort = udpDNSLocalPort
 		args.TCPDNSLocalPort = tcpDNSLocalPort
 		args.DNSAddresses = append(args.DNSAddresses, qemu.SlirpDNS)
diff --git a/pkg/hostagent/dns/dns.go b/pkg/hostagent/dns/dns.go
index 6b3887c9..cf30bce8 100644
--- a/pkg/hostagent/dns/dns.go
+++ b/pkg/hostagent/dns/dns.go
@@ -18,6 +18,7 @@ const truncateSize = 512
 type Handler struct {
 	clientConfig *dns.ClientConfig
 	clients      []*dns.Client
+	IPv6         bool
 }
 
 type Server struct {
@@ -43,7 +44,7 @@ func newStaticClientConfig(ips []net.IP) (*dns.ClientConfig, error) {
 	return dns.ClientConfigFromReader(r)
 }
 
-func newHandler() (dns.Handler, error) {
+func newHandler(IPv6 bool) (dns.Handler, error) {
 	cc, err := dns.ClientConfigFromFile("/etc/resolv.conf")
 	if err != nil {
 		fallbackIPs := []net.IP{net.ParseIP("8.8.8.8"), net.ParseIP("1.1.1.1")}
@@ -60,6 +61,7 @@ func newHandler() (dns.Handler, error) {
 	h := &Handler{
 		clientConfig: cc,
 		clients:      clients,
+		IPv6:         IPv6,
 	}
 	return h, nil
 }
@@ -78,7 +80,12 @@ func (h *Handler) handleQuery(w dns.ResponseWriter, req *dns.Msg) {
 			Ttl:    5,
 		}
 		switch q.Qtype {
-		case dns.TypeCNAME, dns.TypeA, dns.TypeAAAA:
+		case dns.TypeAAAA:
+			if !h.IPv6 {
+				break
+			}
+			fallthrough
+		case dns.TypeCNAME, dns.TypeA:
 			cname, err := net.LookupCNAME(q.Name)
 			if err != nil {
 				break
@@ -212,8 +219,8 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, req *dns.Msg) {
 	}
 }
 
-func Start(udpLocalPort, tcpLocalPort int) (*Server, error) {
-	h, err := newHandler()
+func Start(udpLocalPort, tcpLocalPort int, IPv6 bool) (*Server, error) {
+	h, err := newHandler(IPv6)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go
index c0706b1e..19d00b1a 100644
--- a/pkg/hostagent/hostagent.go
+++ b/pkg/hostagent/hostagent.go
@@ -92,7 +92,7 @@ func New(instName string, stdout io.Writer, sigintCh chan os.Signal, opts ...Opt
 	}
 
 	var udpDNSLocalPort, tcpDNSLocalPort int
-	if *y.UseHostResolver {
+	if *y.HostResolver.Enabled {
 		udpDNSLocalPort, err = findFreeUDPLocalPort()
 		if err != nil {
 			return nil, err
@@ -248,8 +248,8 @@ func (a *HostAgent) Run(ctx context.Context) error {
 		a.emitEvent(ctx, exitingEv)
 	}()
 
-	if *a.y.UseHostResolver {
-		dnsServer, err := dns.Start(a.udpDNSLocalPort, a.tcpDNSLocalPort)
+	if *a.y.HostResolver.Enabled {
+		dnsServer, err := dns.Start(a.udpDNSLocalPort, a.tcpDNSLocalPort, *a.y.HostResolver.IPv6)
 		if err != nil {
 			return fmt.Errorf("cannot start DNS server: %w", err)
 		}
diff --git a/pkg/limayaml/default.yaml b/pkg/limayaml/default.yaml
index 9577f1fd..144241d8 100644
--- a/pkg/limayaml/default.yaml
+++ b/pkg/limayaml/default.yaml
@@ -236,9 +236,14 @@ propagateProxyEnv: null
 # The host agent implements a DNS server that looks up host names on the host
 # using the local system resolver. This means changing VPN and network settings
 # are reflected automatically into the guest, including conditional forward,
-# and mDNS lookup:
-# Default: true
-useHostResolver: null
+# and mDNS lookup. By default only IPv4 addresses will be returned. IPv6 addresses
+# can only work when using a vmnet network interface and the host has working
+# IPv6 configured as well.
+hostResolver:
+  # Default: true
+  enabled: null
+  # Default: false
+  ipv6: null
 
 # If useHostResolver is false, then the following rules apply for configuring dns:
 # Explicitly set DNS addresses for qemu user-mode networking. By default qemu picks *one*
diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go
index 74aba85f..728d02d0 100644
--- a/pkg/limayaml/defaults.go
+++ b/pkg/limayaml/defaults.go
@@ -217,14 +217,36 @@ func FillDefault(y, d, o *LimaYAML, filePath string) {
 		// After defaults processing the singular HostPort and GuestPort values should not be used again.
 	}
 
-	if y.UseHostResolver == nil {
-		y.UseHostResolver = d.UseHostResolver
+	// If both `useHostResolved` and `HostResolver.Enabled` are defined in the same config,
+	// then the deprecated `useHostResolved` setting is silently ignored.
+	if y.HostResolver.Enabled == nil {
+		y.HostResolver.Enabled = y.UseHostResolver
 	}
-	if o.UseHostResolver != nil {
-		y.UseHostResolver = o.UseHostResolver
+	if d.HostResolver.Enabled == nil {
+		d.HostResolver.Enabled = d.UseHostResolver
 	}
-	if y.UseHostResolver == nil {
-		y.UseHostResolver = pointer.Bool(true)
+	if o.HostResolver.Enabled == nil {
+		o.HostResolver.Enabled = o.UseHostResolver
+	}
+
+	if y.HostResolver.Enabled == nil {
+		y.HostResolver.Enabled = d.HostResolver.Enabled
+	}
+	if o.HostResolver.Enabled != nil {
+		y.HostResolver.Enabled = o.HostResolver.Enabled
+	}
+	if y.HostResolver.Enabled == nil {
+		y.HostResolver.Enabled = pointer.Bool(true)
+	}
+
+	if y.HostResolver.IPv6 == nil {
+		y.HostResolver.IPv6 = d.HostResolver.IPv6
+	}
+	if o.HostResolver.IPv6 != nil {
+		y.HostResolver.IPv6 = o.HostResolver.IPv6
+	}
+	if y.HostResolver.IPv6 == nil {
+		y.HostResolver.IPv6 = pointer.Bool(false)
 	}
 
 	if y.PropagateProxyEnv == nil {
diff --git a/pkg/limayaml/limayaml.go b/pkg/limayaml/limayaml.go
index 20e33d36..053c5469 100644
--- a/pkg/limayaml/limayaml.go
+++ b/pkg/limayaml/limayaml.go
@@ -25,7 +25,8 @@ type LimaYAML struct {
 	Network           NetworkDeprecated `yaml:"network,omitempty" json:"network,omitempty"` // DEPRECATED, use `networks` instead
 	Env               map[string]string `yaml:"env,omitempty" json:"env,omitempty"`
 	DNS               []net.IP          `yaml:"dns,omitempty" json:"dns,omitempty"`
-	UseHostResolver   *bool             `yaml:"useHostResolver,omitempty" json:"useHostResolver,omitempty"`
+	HostResolver      HostResolver      `yaml:"hostResolver,omitempty" json:"hostResolver,omitempty"`
+	UseHostResolver   *bool             `yaml:"useHostResolver,omitempty" json:"useHostResolver,omitempty"` // DEPRECATED, use `HostResolver.Enabled` instead
 	PropagateProxyEnv *bool             `yaml:"propagateProxyEnv,omitempty" json:"propagateProxyEnv,omitempty"`
 }
 
@@ -132,6 +133,11 @@ type Network struct {
 	Interface  string `yaml:"interface,omitempty" json:"interface,omitempty"`
 }
 
+type HostResolver struct {
+	Enabled *bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
+	IPv6    *bool `yaml:"ipv6,omitempty" json:"ipv6,omitempty"`
+}
+
 // DEPRECATED types below
 
 // Types have been renamed to turn all references to the old names into compiler errors,
diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go
index 14b7f0ad..7ec8f721 100644
--- a/pkg/limayaml/validate.go
+++ b/pkg/limayaml/validate.go
@@ -194,8 +194,8 @@ func Validate(y LimaYAML, warn bool) error {
 		// processed sequentially and the first matching rule for a guest port determines forwarding behavior.
 	}
 
-	if y.UseHostResolver != nil && *y.UseHostResolver && len(y.DNS) > 0 {
-		return fmt.Errorf("field `dns` must be empty when field `useHostResolver` is true")
+	if y.HostResolver.Enabled != nil && *y.HostResolver.Enabled && len(y.DNS) > 0 {
+		return fmt.Errorf("field `dns` must be empty when field `HostResolver.Enabled` is true")
 	}
 
 	if err := validateNetwork(y, warn); err != nil {


Test patch:
diff --git a/pkg/limayaml/defaults_test.go b/pkg/limayaml/defaults_test.go
index f6ca06b6..bfe08289 100644
--- a/pkg/limayaml/defaults_test.go
+++ b/pkg/limayaml/defaults_test.go
@@ -67,7 +67,10 @@ func TestFillDefault(t *testing.T) {
 		Video: Video{
 			Display: pointer.String("none"),
 		},
-		UseHostResolver:   pointer.Bool(true),
+		HostResolver: HostResolver{
+			Enabled: pointer.Bool(true),
+			IPv6:    pointer.Bool(false),
+		},
 		PropagateProxyEnv: pointer.Bool(true),
 	}
 
@@ -186,7 +189,10 @@ func TestFillDefault(t *testing.T) {
 		Video: Video{
 			Display: pointer.String("cocoa"),
 		},
-		UseHostResolver:   pointer.Bool(false),
+		HostResolver: HostResolver{
+			Enabled: pointer.Bool(false),
+			IPv6:    pointer.Bool(true),
+		},
 		PropagateProxyEnv: pointer.Bool(false),
 
 		Mounts: []Mount{
@@ -298,7 +304,10 @@ func TestFillDefault(t *testing.T) {
 		Video: Video{
 			Display: pointer.String("cocoa"),
 		},
-		UseHostResolver:   pointer.Bool(false),
+		HostResolver: HostResolver{
+			Enabled: pointer.Bool(false),
+			IPv6:    pointer.Bool(false),
+		},
 		PropagateProxyEnv: pointer.Bool(false),
 
 		Mounts: []Mount{