From 89a7a4e7973b2c2a565d22a662d1f8b020b64ed5 Mon Sep 17 00:00:00 2001 From: Dmytro Kryvenko Date: Fri, 15 Oct 2021 17:19:38 -0700 Subject: [PATCH] Host-agent DNS forwarder improvements: - Listen for TCP traffic - Add AAAA, CNAME, NS, MX, TXT and SRV forwarding - Fix `iptables` forward rules so hostagent is accessible from containers Co-authored-by: Jan Dubois Signed-off-by: Dmytro Kryvenko --- docs/internal.md | 1 + docs/network.md | 18 ++- .../boot/07-host-dns-setup.sh | 11 ++ .../boot/30-install-packages.sh | 9 +- pkg/cidata/cidata.TEMPLATE.d/lima.env | 1 + pkg/cidata/cidata.go | 3 +- pkg/cidata/template.go | 1 + pkg/hostagent/dns.go | 150 +++++++++++++++--- pkg/hostagent/hostagent.go | 12 +- 9 files changed, 180 insertions(+), 26 deletions(-) diff --git a/docs/internal.md b/docs/internal.md index fa33b5bfba1..d6cd3cc7c0f 100644 --- a/docs/internal.md +++ b/docs/internal.md @@ -112,3 +112,4 @@ The volume label is "cidata", as defined by [cloud-init NoCloud](https://cloudin - `LIMA_CIDATA_SLIRP_GATEWAY`: set to the IP address of the host on the SLIRP network. `192.168.5.2`. - `LIMA_CIDATA_SLIRP_DNS`: set to the IP address of the DNS on the SLIRP network. `192.168.5.3`. - `LIMA_CIDATA_UDP_DNS_LOCAL_PORT`: set to the udp port number of the hostagent dns server (or 0 when not enabled). +- `LIMA_CIDATA_TCP_DNS_LOCAL_PORT`: set to the tcp port number of the hostagent dns server (or 0 when not enabled). diff --git a/docs/network.md b/docs/network.md index 6aa238d0770..afe8e797f50 100644 --- a/docs/network.md +++ b/docs/network.md @@ -20,9 +20,23 @@ The loopback addresses of the host is `192.168.5.2` and is accessible from the g The DNS. -If `useHostResolver` in `lima.yaml` is true, then the hostagent is going to run a DNS server over udp, using the same socket number as `ssh.localPort`. This server does a local lookup using the native host resolver, so will deal correctly with VPN configurations and split-DNS setups, as well a mDNS (for this the hostagent has to be compiled with `CGO_ENABLED=1`). +If `useHostResolver` in `lima.yaml` is true, then the hostagent is going to run a DNS server over tcp and udp - each on a separate randomly selected free port. This server does a local lookup using the native host resolver, so it will deal correctly with VPN configurations and split-DNS setups, as well as mDNS, local `/etc/hosts` etc. For this the hostagent has to be compiled with `CGO_ENABLED=1` as default Go resolver is [broken](https://github.com/golang/go/issues/12524). -This udp port is then forwarded via iptables rules to `192.168.5.3:53`, overriding the DNS provided by QEMU via slirp. +These tcp and udp ports are then forwarded via iptables rules to `192.168.5.3:53`, overriding the DNS provided by QEMU via slirp. + +Currently following request types are supported: + +- A +- AAAA +- CNAME +- TXT +- NS +- MX +- SRV + +For all other queries hostagent will redirect the query to the nameservers specified in `/etc/resolv.conf` (or, if that fails - to `8.8.8.8` and `1.1.1.1`). + +DNS over tcp is rarely used. It is usually only used either when user explicitly requires it, or when request+response can't fit into a single UDP packet (most likely in case of DNSSEC), or in the case of certain management operations such as domain transfers. Neither DNSSEC nor management operations are currently supported by a hostagent, but on the off chance that the response may contain an unusually long list of records - hostagent will also listen for the tcp traffic. During initial cloud-init bootstrap, `iptables` may not yet be installed. In that case the repo server is determined using the slirp DNS. After `iptables` has been installed, the forwarding rule is applied, switching over to the hostagent DNS. diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/07-host-dns-setup.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/07-host-dns-setup.sh index ace7a476dac..7eed302a9bd 100644 --- a/pkg/cidata/cidata.TEMPLATE.d/boot/07-host-dns-setup.sh +++ b/pkg/cidata/cidata.TEMPLATE.d/boot/07-host-dns-setup.sh @@ -6,8 +6,19 @@ if command -v iptables >/dev/null 2>&1; then if [ -n "${LIMA_CIDATA_UDP_DNS_LOCAL_PORT}" ] && [ "${LIMA_CIDATA_UDP_DNS_LOCAL_PORT}" -ne 0 ]; then # Only add the rule once if ! iptables-save | grep "udp.*${LIMA_CIDATA_SLIRP_GATEWAY}:${LIMA_CIDATA_UDP_DNS_LOCAL_PORT}"; then + iptables -t nat -A PREROUTING -d "${LIMA_CIDATA_SLIRP_DNS}" -p udp --dport 53 -j DNAT \ + --to-destination "${LIMA_CIDATA_SLIRP_GATEWAY}:${LIMA_CIDATA_UDP_DNS_LOCAL_PORT}" iptables -t nat -A OUTPUT -d "${LIMA_CIDATA_SLIRP_DNS}" -p udp --dport 53 -j DNAT \ --to-destination "${LIMA_CIDATA_SLIRP_GATEWAY}:${LIMA_CIDATA_UDP_DNS_LOCAL_PORT}" fi fi + if [ -n "${LIMA_CIDATA_TCP_DNS_LOCAL_PORT}" ] && [ "${LIMA_CIDATA_TCP_DNS_LOCAL_PORT}" -ne 0 ]; then + # Only add the rule once + if ! iptables-save | grep "tcp.*${LIMA_CIDATA_SLIRP_GATEWAY}:${LIMA_CIDATA_TCP_DNS_LOCAL_PORT}"; then + iptables -t nat -A PREROUTING -d "${LIMA_CIDATA_SLIRP_DNS}" -p tcp --dport 53 -j DNAT \ + --to-destination "${LIMA_CIDATA_SLIRP_GATEWAY}:${LIMA_CIDATA_TCP_DNS_LOCAL_PORT}" + iptables -t nat -A OUTPUT -d "${LIMA_CIDATA_SLIRP_DNS}" -p tcp --dport 53 -j DNAT \ + --to-destination "${LIMA_CIDATA_SLIRP_GATEWAY}:${LIMA_CIDATA_TCP_DNS_LOCAL_PORT}" + fi + fi fi diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/30-install-packages.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/30-install-packages.sh index 2174746ca99..04e637eb727 100644 --- a/pkg/cidata/cidata.TEMPLATE.d/boot/30-install-packages.sh +++ b/pkg/cidata/cidata.TEMPLATE.d/boot/30-install-packages.sh @@ -14,7 +14,7 @@ INSTALL_IPTABLES=0 if [ "${LIMA_CIDATA_CONTAINERD_SYSTEM}" = 1 ] || [ "${LIMA_CIDATA_CONTAINERD_USER}" = 1 ]; then INSTALL_IPTABLES=1 fi -if [ "${LIMA_CIDATA_UDP_DNS_LOCAL_PORT}" -ne 0 ]; then +if [ "${LIMA_CIDATA_UDP_DNS_LOCAL_PORT}" -ne 0 ] || [ "${LIMA_CIDATA_TCP_DNS_LOCAL_PORT}" -ne 0 ]; then INSTALL_IPTABLES=1 fi @@ -96,7 +96,14 @@ elif command -v apk >/dev/null 2>&1; then fi fi +SETUP_DNS=0 if [ -n "${LIMA_CIDATA_UDP_DNS_LOCAL_PORT}" ] && [ "${LIMA_CIDATA_UDP_DNS_LOCAL_PORT}" -ne 0 ]; then + SETUP_DNS=1 +fi +if [ -n "${LIMA_CIDATA_TCP_DNS_LOCAL_PORT}" ] && [ "${LIMA_CIDATA_TCP_DNS_LOCAL_PORT}" -ne 0 ]; then + SETUP_DNS=1 +fi +if [ "${SETUP_DNS}" = 1 ]; then # Try to setup iptables rule again, in case we just installed iptables "${LIMA_CIDATA_MNT}/boot/07-host-dns-setup.sh" fi diff --git a/pkg/cidata/cidata.TEMPLATE.d/lima.env b/pkg/cidata/cidata.TEMPLATE.d/lima.env index 8a85fb81c2d..3478dd03f9e 100644 --- a/pkg/cidata/cidata.TEMPLATE.d/lima.env +++ b/pkg/cidata/cidata.TEMPLATE.d/lima.env @@ -17,3 +17,4 @@ LIMA_CIDATA_CONTAINERD_SYSTEM= LIMA_CIDATA_SLIRP_DNS={{.SlirpDNS}} LIMA_CIDATA_SLIRP_GATEWAY={{.SlirpGateway}} LIMA_CIDATA_UDP_DNS_LOCAL_PORT={{.UDPDNSLocalPort}} +LIMA_CIDATA_TCP_DNS_LOCAL_PORT={{.TCPDNSLocalPort}} diff --git a/pkg/cidata/cidata.go b/pkg/cidata/cidata.go index a9d9dd1afd1..1a3ce62ac99 100644 --- a/pkg/cidata/cidata.go +++ b/pkg/cidata/cidata.go @@ -63,7 +63,7 @@ func setupEnv(y *limayaml.LimaYAML) (map[string]string, error) { return env, nil } -func GenerateISO9660(instDir, name string, y *limayaml.LimaYAML, udpDNSLocalPort int, nerdctlArchive string) error { +func GenerateISO9660(instDir, name string, y *limayaml.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort int, nerdctlArchive string) error { if err := limayaml.Validate(*y, false); err != nil { return err } @@ -119,6 +119,7 @@ func GenerateISO9660(instDir, name string, y *limayaml.LimaYAML, udpDNSLocalPort } if *y.UseHostResolver { args.UDPDNSLocalPort = udpDNSLocalPort + args.TCPDNSLocalPort = tcpDNSLocalPort args.DNSAddresses = append(args.DNSAddresses, qemu.SlirpDNS) } else if len(y.DNS) > 0 { for _, addr := range y.DNS { diff --git a/pkg/cidata/template.go b/pkg/cidata/template.go index 0b1522a8047..1cde2d09378 100644 --- a/pkg/cidata/template.go +++ b/pkg/cidata/template.go @@ -40,6 +40,7 @@ type TemplateArgs struct { SlirpGateway string SlirpDNS string UDPDNSLocalPort int + TCPDNSLocalPort int Env map[string]string DNSAddresses []string } diff --git a/pkg/hostagent/dns.go b/pkg/hostagent/dns.go index 2033953cd52..b8ddb4d8f3d 100644 --- a/pkg/hostagent/dns.go +++ b/pkg/hostagent/dns.go @@ -16,6 +16,20 @@ type Handler struct { clients []*dns.Client } +type Server struct { + udp *dns.Server + tcp *dns.Server +} + +func (s *Server) Shutdown() { + if s.udp != nil { + _ = s.udp.Shutdown() + } + if s.tcp != nil { + _ = s.tcp.Shutdown() + } +} + func newStaticClientConfig(ips []net.IP) (*dns.ClientConfig, error) { s := `` for _, ip := range ips { @@ -52,27 +66,111 @@ func (h *Handler) handleQuery(w dns.ResponseWriter, req *dns.Msg) { handled bool ) reply.SetReply(req) - for _, q := range reply.Question { + for _, q := range req.Question { + hdr := dns.RR_Header{ + Name: q.Name, + Rrtype: q.Qtype, + Class: q.Qclass, + Ttl: 5, + } switch q.Qtype { - case dns.TypeA: + case dns.TypeCNAME, dns.TypeA, dns.TypeAAAA: + cname, err := net.LookupCNAME(q.Name) + if err != nil { + break + } + if cname != "" && cname != q.Name { + hdr.Rrtype = dns.TypeCNAME + a := &dns.CNAME{ + Hdr: hdr, + Target: cname, + } + reply.Answer = append(reply.Answer, a) + handled = true + } + if q.Qtype == dns.TypeCNAME { + break + } + hdr.Name = cname addrs, err := net.LookupIP(q.Name) if err == nil && len(addrs) > 0 { for _, ip := range addrs { - if ip.To4() != nil { - a := &dns.A{ - Hdr: dns.RR_Header{ - Name: q.Name, - Rrtype: dns.TypeA, - Class: dns.ClassINET, - }, - A: ip.To4(), + var a dns.RR + ipv6 := ip.To4() == nil + if q.Qtype == dns.TypeA && !ipv6 { + hdr.Rrtype = dns.TypeA + a = &dns.A{ + Hdr: hdr, + A: ip.To4(), + } + } else if q.Qtype == dns.TypeAAAA && ipv6 { + hdr.Rrtype = dns.TypeAAAA + a = &dns.AAAA{ + Hdr: hdr, + AAAA: ip.To16(), + } + } else { + continue + } + reply.Answer = append(reply.Answer, a) + handled = true + } + } + case dns.TypeTXT: + txt, err := net.LookupTXT(q.Name) + if err == nil && len(txt) > 0 { + a := &dns.TXT{ + Hdr: hdr, + Txt: txt, + } + reply.Answer = append(reply.Answer, a) + handled = true + } + case dns.TypeNS: + ns, err := net.LookupNS(q.Name) + if err == nil && len(ns) > 0 { + for _, s := range ns { + if s.Host != "" { + a := &dns.NS{ + Hdr: hdr, + Ns: s.Host, + } + reply.Answer = append(reply.Answer, a) + handled = true + } + } + } + case dns.TypeMX: + mx, err := net.LookupMX(q.Name) + if err == nil && len(mx) > 0 { + for _, s := range mx { + if s.Host != "" { + a := &dns.MX{ + Hdr: hdr, + Mx: s.Host, + Preference: s.Pref, } reply.Answer = append(reply.Answer, a) handled = true - break } } } + case dns.TypeSRV: + _, addrs, err := net.LookupSRV("", "", q.Name) + if err == nil { + hdr.Rrtype = dns.TypeSRV + for _, addr := range addrs { + a := &dns.SRV{ + Hdr: hdr, + Target: addr.Target, + Port: addr.Port, + Priority: addr.Priority, + Weight: addr.Weight, + } + reply.Answer = append(reply.Answer, a) + handled = true + } + } } } if handled { @@ -107,17 +205,31 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, req *dns.Msg) { } } -func (a *HostAgent) StartDNS() (*dns.Server, error) { +func (a *HostAgent) StartDNS() (*Server, error) { h, err := newHandler() if err != nil { panic(err) } - addr := fmt.Sprintf("127.0.0.1:%d", a.udpDNSLocalPort) - server := &dns.Server{Net: "udp", Addr: addr, Handler: h} - go func() { - if e := server.ListenAndServe(); e != nil { - panic(e) - } - }() + server := &Server{} + if a.udpDNSLocalPort > 0 { + addr := fmt.Sprintf("127.0.0.1:%d", a.udpDNSLocalPort) + s := &dns.Server{Net: "udp", Addr: addr, Handler: h} + server.udp = s + go func() { + if e := s.ListenAndServe(); e != nil { + panic(e) + } + }() + } + if a.tcpDNSLocalPort > 0 { + addr := fmt.Sprintf("127.0.0.1:%d", a.tcpDNSLocalPort) + s := &dns.Server{Net: "tcp", Addr: addr, Handler: h} + server.tcp = s + go func() { + if e := s.ListenAndServe(); e != nil { + panic(e) + } + }() + } return server, nil } diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index f41023d9488..817126de9bd 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -35,6 +35,7 @@ type HostAgent struct { y *limayaml.LimaYAML sshLocalPort int udpDNSLocalPort int + tcpDNSLocalPort int instDir string sshConfig *ssh.SSHConfig portForwarder *portForwarder @@ -87,15 +88,19 @@ func New(instName string, stdout io.Writer, sigintCh chan os.Signal, opts ...Opt return nil, err } - var udpDNSLocalPort int + var udpDNSLocalPort, tcpDNSLocalPort int if *y.UseHostResolver { udpDNSLocalPort, err = findFreeUDPLocalPort() if err != nil { return nil, err } + tcpDNSLocalPort, err = findFreeTCPLocalPort() + if err != nil { + return nil, err + } } - if err := cidata.GenerateISO9660(inst.Dir, instName, y, udpDNSLocalPort, o.nerdctlArchive); err != nil { + if err := cidata.GenerateISO9660(inst.Dir, instName, y, udpDNSLocalPort, tcpDNSLocalPort, o.nerdctlArchive); err != nil { return nil, err } @@ -135,6 +140,7 @@ func New(instName string, stdout io.Writer, sigintCh chan os.Signal, opts ...Opt y: y, sshLocalPort: sshLocalPort, udpDNSLocalPort: udpDNSLocalPort, + tcpDNSLocalPort: tcpDNSLocalPort, instDir: inst.Dir, sshConfig: sshConfig, portForwarder: newPortForwarder(sshConfig, sshLocalPort, rules), @@ -244,7 +250,7 @@ func (a *HostAgent) Run(ctx context.Context) error { if err != nil { return fmt.Errorf("cannot start DNS server: %w", err) } - defer func() { _ = dnsServer.Shutdown() }() + defer dnsServer.Shutdown() } qCmd := exec.CommandContext(ctx, a.qExe, a.qArgs...)