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 dnsbl
 20
 21import (
 22	"context"
 23	"net"
 24	"strconv"
 25	"strings"
 26
 27	"github.com/foxcpp/maddy/framework/dns"
 28	"github.com/foxcpp/maddy/framework/exterrors"
 29)
 30
 31type ListedErr struct {
 32	Identity string
 33	List     string
 34	Reason   string
 35}
 36
 37func (le ListedErr) Fields() map[string]interface{} {
 38	return map[string]interface{}{
 39		"check":           "dnsbl",
 40		"list":            le.List,
 41		"listed_identity": le.Identity,
 42		"reason":          le.Reason,
 43		"smtp_code":       554,
 44		"smtp_enchcode":   exterrors.EnhancedCode{5, 7, 0},
 45		"smtp_msg":        "Client identity listed in the used DNSBL",
 46	}
 47}
 48
 49func (le ListedErr) Error() string {
 50	return le.Identity + " is listed in the used DNSBL"
 51}
 52
 53func checkDomain(ctx context.Context, resolver dns.Resolver, cfg List, domain string) error {
 54	query := domain + "." + cfg.Zone
 55
 56	addrs, err := resolver.LookupHost(ctx, query)
 57	if err != nil {
 58		if dnsErr, ok := err.(*net.DNSError); ok && dnsErr.IsNotFound {
 59			return nil
 60		}
 61
 62		return err
 63	}
 64
 65	if len(addrs) == 0 {
 66		return nil
 67	}
 68
 69	// Attempt to extract explanation string.
 70	txts, err := resolver.LookupTXT(context.Background(), query)
 71	if err != nil || len(txts) == 0 {
 72		// Not significant, include addresses as reason. Usually they are
 73		// mapped to some predefined 'reasons' by BL.
 74		return ListedErr{
 75			Identity: domain,
 76			List:     cfg.Zone,
 77			Reason:   strings.Join(addrs, "; "),
 78		}
 79	}
 80
 81	// Some BLs provide multiple reasons (meta-BLs such as Spamhaus Zen) so
 82	// don't mangle them by joining with "", instead join with "; ".
 83
 84	return ListedErr{
 85		Identity: domain,
 86		List:     cfg.Zone,
 87		Reason:   strings.Join(txts, "; "),
 88	}
 89}
 90
 91func checkIP(ctx context.Context, resolver dns.Resolver, cfg List, ip net.IP) error {
 92	ipv6 := true
 93	if ipv4 := ip.To4(); ipv4 != nil {
 94		ip = ipv4
 95		ipv6 = false
 96	}
 97
 98	if ipv6 && !cfg.ClientIPv6 {
 99		return nil
100	}
101	if !ipv6 && !cfg.ClientIPv4 {
102		return nil
103	}
104
105	query := queryString(ip) + "." + cfg.Zone
106
107	addrs, err := resolver.LookupIPAddr(ctx, query)
108	if err != nil {
109		if dnsErr, ok := err.(*net.DNSError); ok && dnsErr.IsNotFound {
110			return nil
111		}
112
113		return err
114	}
115
116	filteredAddrs := make([]net.IPAddr, 0, len(addrs))
117addrsLoop:
118	for _, addr := range addrs {
119		// No responses whitelist configured - permit all.
120		if len(cfg.Responses) == 0 {
121			filteredAddrs = append(filteredAddrs, addr)
122			continue
123		}
124
125		for _, respNet := range cfg.Responses {
126			if respNet.Contains(addr.IP) {
127				filteredAddrs = append(filteredAddrs, addr)
128				continue addrsLoop
129			}
130		}
131	}
132
133	if len(filteredAddrs) == 0 {
134		return nil
135	}
136
137	// Attempt to extract explanation string.
138	txts, err := resolver.LookupTXT(ctx, query)
139	if err != nil || len(txts) == 0 {
140		// Not significant, include addresses as reason. Usually they are
141		// mapped to some predefined 'reasons' by BL.
142
143		reasonParts := make([]string, 0, len(filteredAddrs))
144		for _, addr := range filteredAddrs {
145			reasonParts = append(reasonParts, addr.IP.String())
146		}
147
148		return ListedErr{
149			Identity: ip.String(),
150			List:     cfg.Zone,
151			Reason:   strings.Join(reasonParts, "; "),
152		}
153	}
154
155	// Some BLs provide multiple reasons (meta-BLs such as Spamhaus Zen) so
156	// don't mangle them by joining with "", instead join with "; ".
157
158	return ListedErr{
159		Identity: ip.String(),
160		List:     cfg.Zone,
161		Reason:   strings.Join(txts, "; "),
162	}
163}
164
165func queryString(ip net.IP) string {
166	ipv6 := true
167	if ipv4 := ip.To4(); ipv4 != nil {
168		ip = ipv4
169		ipv6 = false
170	}
171
172	res := strings.Builder{}
173	if ipv6 {
174		res.Grow(63) // 0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0
175	} else {
176		res.Grow(15) // 000.000.000.000
177	}
178
179	for i := len(ip) - 1; i >= 0; i-- {
180		octet := ip[i]
181
182		if ipv6 {
183			// X.X
184			res.WriteString(strconv.FormatInt(int64(octet&0xf), 16))
185			res.WriteRune('.')
186			res.WriteString(strconv.FormatInt(int64((octet&0xf0)>>4), 16))
187		} else {
188			// X
189			res.WriteString(strconv.Itoa(int(octet)))
190		}
191
192		if i != 0 {
193			res.WriteRune('.')
194		}
195	}
196	return res.String()
197}