From 6d086d2afdc4e3e1b19721a6dc1a5a600c2f146e Mon Sep 17 00:00:00 2001 From: Jan Dubois Date: Mon, 14 Feb 2022 23:19:01 -0800 Subject: [PATCH 1/3] Make sure to send an empty reply to AAAA queries when IPv6 is disabled Forwarding the request to the defaultHandler could result in returning NXDOMAIN, which can break resolution of IPv4 addresses. Signed-off-by: Jan Dubois --- pkg/hostagent/dns/dns.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/hostagent/dns/dns.go b/pkg/hostagent/dns/dns.go index cf30bce8868..9f9baf9cd29 100644 --- a/pkg/hostagent/dns/dns.go +++ b/pkg/hostagent/dns/dns.go @@ -82,6 +82,7 @@ func (h *Handler) handleQuery(w dns.ResponseWriter, req *dns.Msg) { switch q.Qtype { case dns.TypeAAAA: if !h.IPv6 { + handled = true break } fallthrough From e3459c6de78ff7a7584189d32f93fc70e95cbaad Mon Sep 17 00:00:00 2001 From: Jan Dubois Date: Mon, 14 Feb 2022 23:28:53 -0800 Subject: [PATCH 2/3] Add mechanism to define static hostnames in the hostResolver This is an alternative to adding them globally to /etc/hosts on the host (not the guest). It also allows aliasing them to host.lima.internal. This allows names to be resolved not just inside the guest, but also inside containers inside the guest, which only have access to /etc/resolv.conf, and not the full resolver inside the guest. Signed-off-by: Jan Dubois --- cmd/limactl/debug.go | 2 +- examples/docker.yaml | 9 ++++ .../cidata.TEMPLATE.d/boot/06-etc-hosts.sh | 3 ++ pkg/hostagent/dns/dns.go | 47 ++++++++++++++++--- pkg/hostagent/hostagent.go | 5 +- pkg/limayaml/default.yaml | 6 +++ pkg/limayaml/defaults.go | 22 +++++++++ pkg/limayaml/defaults_test.go | 23 +++++++++ pkg/limayaml/limayaml.go | 5 +- 9 files changed, 112 insertions(+), 10 deletions(-) diff --git a/cmd/limactl/debug.go b/cmd/limactl/debug.go index 0ce89bc2b7d..e12babdef13 100644 --- a/cmd/limactl/debug.go +++ b/cmd/limactl/debug.go @@ -47,7 +47,7 @@ func debugDNSAction(cmd *cobra.Command, args []string) error { return err } } - srv, err := dns.Start(udpLocalPort, tcpLocalPort, ipv6) + srv, err := dns.Start(udpLocalPort, tcpLocalPort, ipv6, map[string]string{}) if err != nil { return err } diff --git a/examples/docker.yaml b/examples/docker.yaml index aa49bc9f2c2..43b26f2a748 100644 --- a/examples/docker.yaml +++ b/examples/docker.yaml @@ -23,6 +23,10 @@ containerd: user: false provision: - mode: system + # This script defines the host.docker.internal hostname when hostResolver is disabled. + # It is also needed for lima 0.8.2 and earlier, which does not support hostResolver.hosts. + # Names defined in /etc/hosts inside the VM are not resolved inside containers when + # using the hostResolver; use hostResolver.hosts instead (requires lima 0.8.3 or later). script: | #!/bin/sh sed -i 's/host.lima.internal.*/host.lima.internal host.docker.internal/' /etc/hosts @@ -56,6 +60,11 @@ probes: exit 1 fi hint: See "/var/log/cloud-init-output.log". in the guest +hostResolver: + # hostResolver.hosts requires lima 0.8.3 or later. Names defined here will also + # resolve inside containers, and not just inside the VM itself. + hosts: + host.docker.internal: host.lima.internal portForwards: - guestSocket: "/run/user/{{.UID}}/docker.sock" hostSocket: "{{.Dir}}/sock/docker.sock" diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/06-etc-hosts.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/06-etc-hosts.sh index 19feaf46346..97b5cdfdd79 100644 --- a/pkg/cidata/cidata.TEMPLATE.d/boot/06-etc-hosts.sh +++ b/pkg/cidata/cidata.TEMPLATE.d/boot/06-etc-hosts.sh @@ -1,5 +1,8 @@ #!/bin/bash set -eux -o pipefail +# Define host.lima.internal in case the hostResolver is disabled. When using +# the hostResolver, the name is provided by the lima resolver itself because +# it doesn't have access to /etc/hosts inside the VM. sed -i '/host.lima.internal/d' /etc/hosts echo -e "${LIMA_CIDATA_SLIRP_GATEWAY}\thost.lima.internal" >>/etc/hosts diff --git a/pkg/hostagent/dns/dns.go b/pkg/hostagent/dns/dns.go index 9f9baf9cd29..ef7eae90191 100644 --- a/pkg/hostagent/dns/dns.go +++ b/pkg/hostagent/dns/dns.go @@ -7,6 +7,7 @@ import ( "net" "strings" + "github.com/lima-vm/lima/pkg/limayaml" "github.com/miekg/dns" "github.com/sirupsen/logrus" ) @@ -19,6 +20,8 @@ type Handler struct { clientConfig *dns.ClientConfig clients []*dns.Client IPv6 bool + cname map[string]string + ip map[string]net.IP } type Server struct { @@ -44,7 +47,7 @@ func newStaticClientConfig(ips []net.IP) (*dns.ClientConfig, error) { return dns.ClientConfigFromReader(r) } -func newHandler(IPv6 bool) (dns.Handler, error) { +func newHandler(IPv6 bool, hosts map[string]string) (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")} @@ -62,6 +65,15 @@ func newHandler(IPv6 bool) (dns.Handler, error) { clientConfig: cc, clients: clients, IPv6: IPv6, + cname: make(map[string]string), + ip: make(map[string]net.IP), + } + for host, address := range hosts { + if ip := net.ParseIP(address); ip != nil { + h.ip[host] = ip + } else { + h.cname[host] = limayaml.Cname(address) + } } return h, nil } @@ -87,10 +99,27 @@ func (h *Handler) handleQuery(w dns.ResponseWriter, req *dns.Msg) { } fallthrough case dns.TypeCNAME, dns.TypeA: - cname, err := net.LookupCNAME(q.Name) - if err != nil { + cname := q.Name + seen := make(map[string]bool) + for { + // break cyclic definition + if seen[cname] { + break + } + if _, ok := h.cname[cname]; ok { + seen[cname] = true + cname = h.cname[cname] + continue + } break } + var err error + if _, ok := h.ip[cname]; !ok { + cname, err = net.LookupCNAME(cname) + if err != nil { + break + } + } if cname != "" && cname != q.Name { hdr.Rrtype = dns.TypeCNAME a := &dns.CNAME{ @@ -104,7 +133,13 @@ func (h *Handler) handleQuery(w dns.ResponseWriter, req *dns.Msg) { break } hdr.Name = cname - addrs, err := net.LookupIP(q.Name) + var addrs []net.IP + if _, ok := h.ip[cname]; ok { + addrs = []net.IP{h.ip[cname]} + err = nil + } else { + addrs, err = net.LookupIP(cname) + } if err == nil && len(addrs) > 0 { for _, ip := range addrs { var a dns.RR @@ -220,8 +255,8 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, req *dns.Msg) { } } -func Start(udpLocalPort, tcpLocalPort int, IPv6 bool) (*Server, error) { - h, err := newHandler(IPv6) +func Start(udpLocalPort, tcpLocalPort int, IPv6 bool, hosts map[string]string) (*Server, error) { + h, err := newHandler(IPv6, hosts) if err != nil { return nil, err } diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index 19d00b1ae19..82883a2eb00 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -27,6 +27,7 @@ import ( "github.com/lima-vm/lima/pkg/hostagent/events" "github.com/lima-vm/lima/pkg/limayaml" "github.com/lima-vm/lima/pkg/qemu" + qemuconst "github.com/lima-vm/lima/pkg/qemu/const" "github.com/lima-vm/lima/pkg/sshutil" "github.com/lima-vm/lima/pkg/store" "github.com/lima-vm/lima/pkg/store/filenames" @@ -249,7 +250,9 @@ func (a *HostAgent) Run(ctx context.Context) error { }() if *a.y.HostResolver.Enabled { - dnsServer, err := dns.Start(a.udpDNSLocalPort, a.tcpDNSLocalPort, *a.y.HostResolver.IPv6) + hosts := a.y.HostResolver.Hosts + hosts["host.lima.internal."] = qemuconst.SlirpGateway + dnsServer, err := dns.Start(a.udpDNSLocalPort, a.tcpDNSLocalPort, *a.y.HostResolver.IPv6, hosts) 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 abd92130a08..4ea43c6e86e 100644 --- a/pkg/limayaml/default.yaml +++ b/pkg/limayaml/default.yaml @@ -256,6 +256,12 @@ hostResolver: enabled: null # Default: false ipv6: null + # Static names can be defined here as an alternative to adding them to the hosts /etc/hosts. + # Values can be either other hostnames, or IP addresses. The host.lima.internal name is + # predefined to specify the gateway address to the host. + hosts: + # guest.name: 127.1.1.1 + # host.name: host.lima.internal # 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 32f53cb512c..6d3f029c20b 100644 --- a/pkg/limayaml/defaults.go +++ b/pkg/limayaml/defaults.go @@ -10,6 +10,7 @@ import ( "path/filepath" "runtime" "strconv" + "strings" "text/template" "github.com/lima-vm/lima/pkg/guestagent/api" @@ -180,6 +181,19 @@ func FillDefault(y, d, o *LimaYAML, filePath string) { y.SSH.ForwardAgent = pointer.Bool(false) } + hosts := make(map[string]string) + // Values can be either names or IP addresses. Name values are canonicalized in the hostResolver. + for k, v := range d.HostResolver.Hosts { + hosts[Cname(k)] = v + } + for k, v := range y.HostResolver.Hosts { + hosts[Cname(k)] = v + } + for k, v := range o.HostResolver.Hosts { + hosts[Cname(k)] = v + } + y.HostResolver.Hosts = hosts + y.Provision = append(append(o.Provision, y.Provision...), d.Provision...) for i := range y.Provision { provision := &y.Provision[i] @@ -486,3 +500,11 @@ func IsNativeArch(arch Arch) bool { nativeAARCH64 := arch == AARCH64 && runtime.GOARCH == "arm64" return nativeX8664 || nativeAARCH64 } + +func Cname(host string) string { + host = strings.ToLower(host) + if !strings.HasSuffix(host, ".") { + host += "." + } + return host +} diff --git a/pkg/limayaml/defaults_test.go b/pkg/limayaml/defaults_test.go index b8ae142365b..64bbf8ec48a 100644 --- a/pkg/limayaml/defaults_test.go +++ b/pkg/limayaml/defaults_test.go @@ -89,6 +89,11 @@ func TestFillDefault(t *testing.T) { // All these slices and maps are empty in "builtin". Add minimal entries here to see that // their values are retained and defaults for their fields are applied correctly. y = LimaYAML{ + HostResolver: HostResolver{ + Hosts: map[string]string{ + "MY.Host": "host.lima.internal", + }, + }, Mounts: []Mount{ {Location: "/tmp"}, }, @@ -119,6 +124,10 @@ func TestFillDefault(t *testing.T) { } expect := builtin + expect.HostResolver.Hosts = map[string]string{ + "my.host.": "host.lima.internal", + } + expect.Mounts = y.Mounts expect.Mounts[0].Writable = pointer.Bool(false) expect.Mounts[0].SSHFS.Cache = pointer.Bool(true) @@ -195,6 +204,9 @@ func TestFillDefault(t *testing.T) { HostResolver: HostResolver{ Enabled: pointer.Bool(false), IPv6: pointer.Bool(true), + Hosts: map[string]string{ + "default": "localhost", + }, }, PropagateProxyEnv: pointer.Bool(false), @@ -248,6 +260,9 @@ func TestFillDefault(t *testing.T) { expect.Containerd.Archives[0].Arch = *d.Arch expect.Mounts[0].SSHFS.Cache = pointer.Bool(true) expect.Mounts[0].SSHFS.FollowSymlinks = pointer.Bool(false) + expect.HostResolver.Hosts = map[string]string{ + "default.": d.HostResolver.Hosts["default"], + } y = LimaYAML{} FillDefault(&y, &d, &LimaYAML{}, filePath) @@ -270,6 +285,8 @@ func TestFillDefault(t *testing.T) { expect.Mounts = append(d.Mounts, y.Mounts...) expect.Networks = append(d.Networks, y.Networks...) + expect.HostResolver.Hosts["default."] = d.HostResolver.Hosts["default"] + // d.DNS will be ignored, and not appended to y.DNS // "TWO" does not exist in filledDefaults.Env, so is set from d.Env @@ -312,6 +329,9 @@ func TestFillDefault(t *testing.T) { HostResolver: HostResolver{ Enabled: pointer.Bool(false), IPv6: pointer.Bool(false), + Hosts: map[string]string{ + "override.": "underflow", + }, }, PropagateProxyEnv: pointer.Bool(false), @@ -376,6 +396,9 @@ func TestFillDefault(t *testing.T) { expect.PortForwards = append(append(o.PortForwards, y.PortForwards...), d.PortForwards...) expect.Containerd.Archives = append(append(o.Containerd.Archives, y.Containerd.Archives...), d.Containerd.Archives...) + expect.HostResolver.Hosts["default."] = d.HostResolver.Hosts["default"] + expect.HostResolver.Hosts["my.host."] = d.HostResolver.Hosts["host.lima.internal"] + // o.Mounts just makes d.Mounts[0] writable because the Location matches expect.Mounts = append(d.Mounts, y.Mounts...) expect.Mounts[0].Writable = pointer.Bool(true) diff --git a/pkg/limayaml/limayaml.go b/pkg/limayaml/limayaml.go index 374dd0a7cd1..474369e46ee 100644 --- a/pkg/limayaml/limayaml.go +++ b/pkg/limayaml/limayaml.go @@ -136,8 +136,9 @@ type Network struct { } type HostResolver struct { - Enabled *bool `yaml:"enabled,omitempty" json:"enabled,omitempty"` - IPv6 *bool `yaml:"ipv6,omitempty" json:"ipv6,omitempty"` + Enabled *bool `yaml:"enabled,omitempty" json:"enabled,omitempty"` + IPv6 *bool `yaml:"ipv6,omitempty" json:"ipv6,omitempty"` + Hosts map[string]string `yaml:"hosts,omitempty" json:"hosts,omitempty"` } // DEPRECATED types below From 014fbc3eb3679f7a406501a17e4bb6fb6dad4fbf Mon Sep 17 00:00:00 2001 From: Jan Dubois Date: Tue, 15 Feb 2022 01:11:39 -0800 Subject: [PATCH 3/3] Add a static entry for the hostname to the slirp IP This is done automatically by systemd-resolved, in which case this change is ignored, but on e.g. Alpine this is required to connect to the VM using its own hostname. Signed-off-by: Jan Dubois --- pkg/hostagent/hostagent.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index 82883a2eb00..b5f4d8f3ed6 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -41,6 +41,7 @@ type HostAgent struct { udpDNSLocalPort int tcpDNSLocalPort int instDir string + instName string sshConfig *ssh.SSHConfig portForwarder *portForwarder onClose []func() error // LIFO @@ -146,6 +147,7 @@ func New(instName string, stdout io.Writer, sigintCh chan os.Signal, opts ...Opt udpDNSLocalPort: udpDNSLocalPort, tcpDNSLocalPort: tcpDNSLocalPort, instDir: inst.Dir, + instName: instName, sshConfig: sshConfig, portForwarder: newPortForwarder(sshConfig, sshLocalPort, rules), qExe: qExe, @@ -252,6 +254,7 @@ func (a *HostAgent) Run(ctx context.Context) error { if *a.y.HostResolver.Enabled { hosts := a.y.HostResolver.Hosts hosts["host.lima.internal."] = qemuconst.SlirpGateway + hosts[fmt.Sprintf("lima-%s.", a.instName)] = qemuconst.SlirpIPAddress dnsServer, err := dns.Start(a.udpDNSLocalPort, a.tcpDNSLocalPort, *a.y.HostResolver.IPv6, hosts) if err != nil { return fmt.Errorf("cannot start DNS server: %w", err)