1/*2Maddy Mail Server - Composable all-in-one email server.3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors45This program is free software: you can redistribute it and/or modify6it under the terms of the GNU General Public License as published by7the Free Software Foundation, either version 3 of the License, or8(at your option) any later version.910This program is distributed in the hope that it will be useful,11but WITHOUT ANY WARRANTY; without even the implied warranty of12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13GNU General Public License for more details.1415You should have received a copy of the GNU General Public License16along with this program. If not, see <https://www.gnu.org/licenses/>.17*/1819package dns2021import (22 "context"23 "net"24 "strconv"25 "strings"26 "time"2728 "github.com/foxcpp/maddy/framework/log"29 "github.com/miekg/dns"30)3132type TLSA = dns.TLSA3334// ExtResolver is a convenience wrapper for miekg/dns library that provides35// 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.Client39 Cfg *dns.ClientConfig40}4142// RCodeError is returned by ExtResolver when the RCODE in response is not43// NOERROR.44type RCodeError struct {45 Name string46 Code int47}4849func (err RCodeError) Temporary() bool {50 return err.Code == dns.RcodeServerFailure51}5253func (err RCodeError) Error() string {54 switch err.Code {55 case dns.RcodeFormatError:56 return "dns: rcode FORMERR when looking up " + err.Name57 case dns.RcodeServerFailure:58 return "dns: rcode SERVFAIL when looking up " + err.Name59 case dns.RcodeNameError:60 return "dns: rcode NXDOMAIN when looking up " + err.Name61 case dns.RcodeNotImplemented:62 return "dns: rcode NOTIMP when looking up " + err.Name63 case dns.RcodeRefused:64 return "dns: rcode REFUSED when looking up " + err.Name65 }66 return "dns: non-success rcode: " + strconv.Itoa(err.Code) + " when looking up " + err.Name67}6869func IsNotFound(err error) bool {70 if dnsErr, ok := err.(*net.DNSError); ok {71 return dnsErr.IsNotFound72 }73 if rcodeErr, ok := err.(RCodeError); ok {74 return rcodeErr.Code == dns.RcodeNameError75 }76 return false77}7879func isLoopback(addr string) bool {80 ip := net.ParseIP(addr)81 if ip == nil {82 return false83 }84 return ip.IsLoopback()85}8687func (e ExtResolver) exchange(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {88 var resp *dns.Msg89 var lastErr error90 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 continue94 }9596 if resp.Rcode != dns.RcodeSuccess {97 lastErr = RCodeError{msg.Question[0].Name, resp.Rcode}98 continue99 }100101 // Diregard AD flags from non-local resolvers, likely they are102 // communicated with using an insecure channel and so flags can be103 // tampered with.104 if !isLoopback(srv) {105 resp.AuthenticatedData = false106 }107108 break109 }110 return resp, lastErr111}112113func (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, err117 }118119 msg := new(dns.Msg)120 msg.SetQuestion(revAddr, dns.TypePTR)121 msg.SetEdns0(4096, false)122 msg.AuthenticatedData = true123124 resp, err := e.exchange(ctx, msg)125 if err != nil {126 return false, nil, err127 }128129 ad = resp.AuthenticatedData130 names = make([]string, 0, len(resp.Answer))131 for _, rr := range resp.Answer {132 ptrRR, ok := rr.(*dns.PTR)133 if !ok {134 continue135 }136137 names = append(names, ptrRR.Ptr)138 }139 return140}141142func (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, err146 }147148 addrs = make([]string, 0, len(addrParsed))149 for _, addr := range addrParsed {150 addrs = append(addrs, addr.String())151 }152 return ad, addrs, nil153}154155func (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 = true160161 resp, err := e.exchange(ctx, msg)162 if err != nil {163 return false, nil, err164 }165166 ad = resp.AuthenticatedData167 mxs = make([]*net.MX, 0, len(resp.Answer))168 for _, rr := range resp.Answer {169 mxRR, ok := rr.(*dns.MX)170 if !ok {171 continue172 }173174 mxs = append(mxs, &net.MX{175 Host: mxRR.Mx,176 Pref: mxRR.Preference,177 })178 }179 return180}181182func (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 = true187188 resp, err := e.exchange(ctx, msg)189 if err != nil {190 return false, nil, err191 }192193 ad = resp.AuthenticatedData194 recs = make([]string, 0, len(resp.Answer))195 for _, rr := range resp.Answer {196 txtRR, ok := rr.(*dns.TXT)197 if !ok {198 continue199 }200201 recs = append(recs, strings.Join(txtRR.Txt, ""))202 }203 return204}205206// CheckCNAMEAD is a special function for use in DANE lookups. It attempts to determine final207// (canonical) name of the host and also reports whether the whole chain of CNAME's and final zone208// 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 = true216 resp, err := e.exchange(ctx, msg)217 if err != nil {218 return false, "", err219 }220221 for _, r := range resp.Answer {222 switch r := r.(type) {223 case *dns.A:224 rname = r.Hdr.Name225 ad = resp.AuthenticatedData // Use AD flag from response we used to determine rname226 }227 }228229 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 = true235 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.Name241 ad = resp.AuthenticatedData242 }243 }244 }245 }246247 return ad, rname, nil248}249250func (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 = true255 resp, err := e.exchange(ctx, msg)256 if err != nil {257 return false, "", err258 }259260 for _, r := range resp.Answer {261 cnameR, ok := r.(*dns.CNAME)262 if !ok {263 continue264 }265 return resp.AuthenticatedData, cnameR.Target, nil266 }267268 return resp.AuthenticatedData, "", nil269}270271func (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 = true277278 resp, err := e.exchange(ctx, msg)279 aaaaFailed := false280 var (281 v6ad bool282 v6addrs []net.IPAddr283 )284 if err != nil {285 // Disregard the error for AAAA lookups.286 aaaaFailed = true287 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.AuthenticatedData291 for _, rr := range resp.Answer {292 aaaaRR, ok := rr.(*dns.AAAA)293 if !ok {294 continue295 }296 v6addrs = append(v6addrs, net.IPAddr{IP: aaaaRR.AAAA})297 }298 }299300 // 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 = true305306 resp, err = e.exchange(ctx, msg)307 var (308 v4ad bool309 v4addrs []net.IPAddr310 )311 if err != nil {312 if aaaaFailed {313 return false, nil, err314 }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.AuthenticatedData319 v4addrs = make([]net.IPAddr, 0, len(resp.Answer))320 for _, rr := range resp.Answer {321 aRR, ok := rr.(*dns.A)322 if !ok {323 continue324 }325 v4addrs = append(v4addrs, net.IPAddr{IP: aRR.A})326 }327 }328329 // A little bit of careful handling is required if AD is inconsistent330 // for A and AAAA queries. This unfortunatenly happens in practice. For331 // purposes of DANE handling (A/AAAA check) we disregard AAAA records332 // if they are not authenctiated and return only A records with AD=true.333334 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, nil345}346347func (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, err351 }352353 msg := new(dns.Msg)354 msg.SetQuestion(dns.Fqdn(name), dns.TypeTLSA)355 msg.SetEdns0(4096, false)356 msg.AuthenticatedData = true357358 resp, err := e.exchange(ctx, msg)359 if err != nil {360 return false, nil, err361 }362363 ad = resp.AuthenticatedData364 recs = make([]dns.TLSA, 0, len(resp.Answer))365 for _, rr := range resp.Answer {366 rr, ok := rr.(*dns.TLSA)367 if !ok {368 continue369 }370371 recs = append(recs, *rr)372 }373 return374}375376func NewExtResolver() (*ExtResolver, error) {377 cfg, err := dns.ClientConfigFromFile("/etc/resolv.conf")378 if err != nil {379 return nil, err380 }381382 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 = port389 }390391 if len(cfg.Servers) == 0 {392 cfg.Servers = []string{"127.0.0.1"}393 }394395 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 }, nil403}