maddy

Fork https://github.com/foxcpp/maddy

git clone git://git.lin.moe/go/maddy.git

  1/*
  2Maddy Mail Server - Composable all-in-one email server.
  3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  4
  5This program is free software: you can redistribute it and/or modify
  6it under the terms of the GNU General Public License as published by
  7the Free Software Foundation, either version 3 of the License, or
  8(at your option) any later version.
  9
 10This program is distributed in the hope that it will be useful,
 11but WITHOUT ANY WARRANTY; without even the implied warranty of
 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13GNU General Public License for more details.
 14
 15You should have received a copy of the GNU General Public License
 16along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17*/
 18
 19package dns
 20
 21import (
 22	"context"
 23	"net"
 24	"strconv"
 25	"strings"
 26	"time"
 27
 28	"github.com/foxcpp/maddy/framework/log"
 29	"github.com/miekg/dns"
 30)
 31
 32type TLSA = dns.TLSA
 33
 34// ExtResolver is a convenience wrapper for miekg/dns library that provides
 35// access to certain low-level functionality (notably, AD flag in responses,
 36// indicating whether DNSSEC verification was performed by the server).
 37type ExtResolver struct {
 38	cl  *dns.Client
 39	Cfg *dns.ClientConfig
 40}
 41
 42// RCodeError is returned by ExtResolver when the RCODE in response is not
 43// NOERROR.
 44type RCodeError struct {
 45	Name string
 46	Code int
 47}
 48
 49func (err RCodeError) Temporary() bool {
 50	return err.Code == dns.RcodeServerFailure
 51}
 52
 53func (err RCodeError) Error() string {
 54	switch err.Code {
 55	case dns.RcodeFormatError:
 56		return "dns: rcode FORMERR when looking up " + err.Name
 57	case dns.RcodeServerFailure:
 58		return "dns: rcode SERVFAIL when looking up " + err.Name
 59	case dns.RcodeNameError:
 60		return "dns: rcode NXDOMAIN when looking up " + err.Name
 61	case dns.RcodeNotImplemented:
 62		return "dns: rcode NOTIMP when looking up " + err.Name
 63	case dns.RcodeRefused:
 64		return "dns: rcode REFUSED when looking up " + err.Name
 65	}
 66	return "dns: non-success rcode: " + strconv.Itoa(err.Code) + " when looking up " + err.Name
 67}
 68
 69func IsNotFound(err error) bool {
 70	if dnsErr, ok := err.(*net.DNSError); ok {
 71		return dnsErr.IsNotFound
 72	}
 73	if rcodeErr, ok := err.(RCodeError); ok {
 74		return rcodeErr.Code == dns.RcodeNameError
 75	}
 76	return false
 77}
 78
 79func isLoopback(addr string) bool {
 80	ip := net.ParseIP(addr)
 81	if ip == nil {
 82		return false
 83	}
 84	return ip.IsLoopback()
 85}
 86
 87func (e ExtResolver) exchange(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
 88	var resp *dns.Msg
 89	var lastErr error
 90	for _, srv := range e.Cfg.Servers {
 91		resp, _, lastErr = e.cl.ExchangeContext(ctx, msg, net.JoinHostPort(srv, e.Cfg.Port))
 92		if lastErr != nil {
 93			continue
 94		}
 95
 96		if resp.Rcode != dns.RcodeSuccess {
 97			lastErr = RCodeError{msg.Question[0].Name, resp.Rcode}
 98			continue
 99		}
100
101		// Diregard AD flags from non-local resolvers, likely they are
102		// communicated with using an insecure channel and so flags can be
103		// tampered with.
104		if !isLoopback(srv) {
105			resp.AuthenticatedData = false
106		}
107
108		break
109	}
110	return resp, lastErr
111}
112
113func (e ExtResolver) AuthLookupAddr(ctx context.Context, addr string) (ad bool, names []string, err error) {
114	revAddr, err := dns.ReverseAddr(addr)
115	if err != nil {
116		return false, nil, err
117	}
118
119	msg := new(dns.Msg)
120	msg.SetQuestion(revAddr, dns.TypePTR)
121	msg.SetEdns0(4096, false)
122	msg.AuthenticatedData = true
123
124	resp, err := e.exchange(ctx, msg)
125	if err != nil {
126		return false, nil, err
127	}
128
129	ad = resp.AuthenticatedData
130	names = make([]string, 0, len(resp.Answer))
131	for _, rr := range resp.Answer {
132		ptrRR, ok := rr.(*dns.PTR)
133		if !ok {
134			continue
135		}
136
137		names = append(names, ptrRR.Ptr)
138	}
139	return
140}
141
142func (e ExtResolver) AuthLookupHost(ctx context.Context, host string) (ad bool, addrs []string, err error) {
143	ad, addrParsed, err := e.AuthLookupIPAddr(ctx, host)
144	if err != nil {
145		return false, nil, err
146	}
147
148	addrs = make([]string, 0, len(addrParsed))
149	for _, addr := range addrParsed {
150		addrs = append(addrs, addr.String())
151	}
152	return ad, addrs, nil
153}
154
155func (e ExtResolver) AuthLookupMX(ctx context.Context, name string) (ad bool, mxs []*net.MX, err error) {
156	msg := new(dns.Msg)
157	msg.SetQuestion(dns.Fqdn(name), dns.TypeMX)
158	msg.SetEdns0(4096, false)
159	msg.AuthenticatedData = true
160
161	resp, err := e.exchange(ctx, msg)
162	if err != nil {
163		return false, nil, err
164	}
165
166	ad = resp.AuthenticatedData
167	mxs = make([]*net.MX, 0, len(resp.Answer))
168	for _, rr := range resp.Answer {
169		mxRR, ok := rr.(*dns.MX)
170		if !ok {
171			continue
172		}
173
174		mxs = append(mxs, &net.MX{
175			Host: mxRR.Mx,
176			Pref: mxRR.Preference,
177		})
178	}
179	return
180}
181
182func (e ExtResolver) AuthLookupTXT(ctx context.Context, name string) (ad bool, recs []string, err error) {
183	msg := new(dns.Msg)
184	msg.SetQuestion(dns.Fqdn(name), dns.TypeTXT)
185	msg.SetEdns0(4096, false)
186	msg.AuthenticatedData = true
187
188	resp, err := e.exchange(ctx, msg)
189	if err != nil {
190		return false, nil, err
191	}
192
193	ad = resp.AuthenticatedData
194	recs = make([]string, 0, len(resp.Answer))
195	for _, rr := range resp.Answer {
196		txtRR, ok := rr.(*dns.TXT)
197		if !ok {
198			continue
199		}
200
201		recs = append(recs, strings.Join(txtRR.Txt, ""))
202	}
203	return
204}
205
206// CheckCNAMEAD is a special function for use in DANE lookups. It attempts to determine final
207// (canonical) name of the host and also reports whether the whole chain of CNAME's and final zone
208// are "secure".
209//
210// If there are no A or AAAA records for host, rname = "" is returned.
211func (e ExtResolver) CheckCNAMEAD(ctx context.Context, host string) (ad bool, rname string, err error) {
212	msg := new(dns.Msg)
213	msg.SetQuestion(dns.Fqdn(host), dns.TypeA)
214	msg.SetEdns0(4096, false)
215	msg.AuthenticatedData = true
216	resp, err := e.exchange(ctx, msg)
217	if err != nil {
218		return false, "", err
219	}
220
221	for _, r := range resp.Answer {
222		switch r := r.(type) {
223		case *dns.A:
224			rname = r.Hdr.Name
225			ad = resp.AuthenticatedData // Use AD flag from response we used to determine rname
226		}
227	}
228
229	if rname == "" {
230		// IPv6-only host? Try to find out rname using AAAA lookup.
231		msg := new(dns.Msg)
232		msg.SetQuestion(dns.Fqdn(host), dns.TypeAAAA)
233		msg.SetEdns0(4096, false)
234		msg.AuthenticatedData = true
235		resp, err := e.exchange(ctx, msg)
236		if err == nil {
237			for _, r := range resp.Answer {
238				switch r := r.(type) {
239				case *dns.AAAA:
240					rname = r.Hdr.Name
241					ad = resp.AuthenticatedData
242				}
243			}
244		}
245	}
246
247	return ad, rname, nil
248}
249
250func (e ExtResolver) AuthLookupCNAME(ctx context.Context, host string) (ad bool, cname string, err error) {
251	msg := new(dns.Msg)
252	msg.SetQuestion(dns.Fqdn(host), dns.TypeCNAME)
253	msg.SetEdns0(4096, false)
254	msg.AuthenticatedData = true
255	resp, err := e.exchange(ctx, msg)
256	if err != nil {
257		return false, "", err
258	}
259
260	for _, r := range resp.Answer {
261		cnameR, ok := r.(*dns.CNAME)
262		if !ok {
263			continue
264		}
265		return resp.AuthenticatedData, cnameR.Target, nil
266	}
267
268	return resp.AuthenticatedData, "", nil
269}
270
271func (e ExtResolver) AuthLookupIPAddr(ctx context.Context, host string) (ad bool, addrs []net.IPAddr, err error) {
272	// First, query IPv6.
273	msg := new(dns.Msg)
274	msg.SetQuestion(dns.Fqdn(host), dns.TypeAAAA)
275	msg.SetEdns0(4096, false)
276	msg.AuthenticatedData = true
277
278	resp, err := e.exchange(ctx, msg)
279	aaaaFailed := false
280	var (
281		v6ad    bool
282		v6addrs []net.IPAddr
283	)
284	if err != nil {
285		// Disregard the error for AAAA lookups.
286		aaaaFailed = true
287		log.DefaultLogger.Error("Network I/O error during AAAA lookup", err, "host", host)
288	} else {
289		v6addrs = make([]net.IPAddr, 0, len(resp.Answer))
290		v6ad = resp.AuthenticatedData
291		for _, rr := range resp.Answer {
292			aaaaRR, ok := rr.(*dns.AAAA)
293			if !ok {
294				continue
295			}
296			v6addrs = append(v6addrs, net.IPAddr{IP: aaaaRR.AAAA})
297		}
298	}
299
300	// Then repeat query with IPv4.
301	msg = new(dns.Msg)
302	msg.SetQuestion(dns.Fqdn(host), dns.TypeA)
303	msg.SetEdns0(4096, false)
304	msg.AuthenticatedData = true
305
306	resp, err = e.exchange(ctx, msg)
307	var (
308		v4ad    bool
309		v4addrs []net.IPAddr
310	)
311	if err != nil {
312		if aaaaFailed {
313			return false, nil, err
314		}
315		// Disregard A lookup error if AAAA succeeded.
316		log.DefaultLogger.Error("Network I/O error during A lookup, using AAAA records", err, "host", host)
317	} else {
318		v4ad = resp.AuthenticatedData
319		v4addrs = make([]net.IPAddr, 0, len(resp.Answer))
320		for _, rr := range resp.Answer {
321			aRR, ok := rr.(*dns.A)
322			if !ok {
323				continue
324			}
325			v4addrs = append(v4addrs, net.IPAddr{IP: aRR.A})
326		}
327	}
328
329	// A little bit of careful handling is required if AD is inconsistent
330	// for A and AAAA queries. This unfortunatenly happens in practice. For
331	// purposes of DANE handling (A/AAAA check) we disregard AAAA records
332	// if they are not authenctiated and return only A records with AD=true.
333
334	addrs = make([]net.IPAddr, 0, len(v4addrs)+len(v6addrs))
335	if !v6ad && !v4ad {
336		addrs = append(addrs, v6addrs...)
337		addrs = append(addrs, v4addrs...)
338	} else {
339		if v6ad {
340			addrs = append(addrs, v6addrs...)
341		}
342		addrs = append(addrs, v4addrs...)
343	}
344	return v4ad, addrs, nil
345}
346
347func (e ExtResolver) AuthLookupTLSA(ctx context.Context, service, network, domain string) (ad bool, recs []TLSA, err error) {
348	name, err := dns.TLSAName(domain, service, network)
349	if err != nil {
350		return false, nil, err
351	}
352
353	msg := new(dns.Msg)
354	msg.SetQuestion(dns.Fqdn(name), dns.TypeTLSA)
355	msg.SetEdns0(4096, false)
356	msg.AuthenticatedData = true
357
358	resp, err := e.exchange(ctx, msg)
359	if err != nil {
360		return false, nil, err
361	}
362
363	ad = resp.AuthenticatedData
364	recs = make([]dns.TLSA, 0, len(resp.Answer))
365	for _, rr := range resp.Answer {
366		rr, ok := rr.(*dns.TLSA)
367		if !ok {
368			continue
369		}
370
371		recs = append(recs, *rr)
372	}
373	return
374}
375
376func NewExtResolver() (*ExtResolver, error) {
377	cfg, err := dns.ClientConfigFromFile("/etc/resolv.conf")
378	if err != nil {
379		return nil, err
380	}
381
382	if overrideServ != "" && overrideServ != "system-default" {
383		host, port, err := net.SplitHostPort(overrideServ)
384		if err != nil {
385			panic(err)
386		}
387		cfg.Servers = []string{host}
388		cfg.Port = port
389	}
390
391	if len(cfg.Servers) == 0 {
392		cfg.Servers = []string{"127.0.0.1"}
393	}
394
395	cl := new(dns.Client)
396	cl.Dialer = &net.Dialer{
397		Timeout: time.Duration(cfg.Timeout) * time.Second,
398	}
399	return &ExtResolver{
400		cl:  cl,
401		Cfg: cfg,
402	}, nil
403}