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 dnsbl2021import (22 "context"23 "net"24 "strconv"25 "strings"2627 "github.com/foxcpp/maddy/framework/dns"28 "github.com/foxcpp/maddy/framework/exterrors"29)3031type ListedErr struct {32 Identity string33 List string34 Reason string35}3637func (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}4849func (le ListedErr) Error() string {50 return le.Identity + " is listed in the used DNSBL"51}5253func checkDomain(ctx context.Context, resolver dns.Resolver, cfg List, domain string) error {54 query := domain + "." + cfg.Zone5556 addrs, err := resolver.LookupHost(ctx, query)57 if err != nil {58 if dnsErr, ok := err.(*net.DNSError); ok && dnsErr.IsNotFound {59 return nil60 }6162 return err63 }6465 if len(addrs) == 0 {66 return nil67 }6869 // 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 are73 // mapped to some predefined 'reasons' by BL.74 return ListedErr{75 Identity: domain,76 List: cfg.Zone,77 Reason: strings.Join(addrs, "; "),78 }79 }8081 // Some BLs provide multiple reasons (meta-BLs such as Spamhaus Zen) so82 // don't mangle them by joining with "", instead join with "; ".8384 return ListedErr{85 Identity: domain,86 List: cfg.Zone,87 Reason: strings.Join(txts, "; "),88 }89}9091func checkIP(ctx context.Context, resolver dns.Resolver, cfg List, ip net.IP) error {92 ipv6 := true93 if ipv4 := ip.To4(); ipv4 != nil {94 ip = ipv495 ipv6 = false96 }9798 if ipv6 && !cfg.ClientIPv6 {99 return nil100 }101 if !ipv6 && !cfg.ClientIPv4 {102 return nil103 }104105 query := queryString(ip) + "." + cfg.Zone106107 addrs, err := resolver.LookupIPAddr(ctx, query)108 if err != nil {109 if dnsErr, ok := err.(*net.DNSError); ok && dnsErr.IsNotFound {110 return nil111 }112113 return err114 }115116 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 continue123 }124125 for _, respNet := range cfg.Responses {126 if respNet.Contains(addr.IP) {127 filteredAddrs = append(filteredAddrs, addr)128 continue addrsLoop129 }130 }131 }132133 if len(filteredAddrs) == 0 {134 return nil135 }136137 // 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 are141 // mapped to some predefined 'reasons' by BL.142143 reasonParts := make([]string, 0, len(filteredAddrs))144 for _, addr := range filteredAddrs {145 reasonParts = append(reasonParts, addr.IP.String())146 }147148 return ListedErr{149 Identity: ip.String(),150 List: cfg.Zone,151 Reason: strings.Join(reasonParts, "; "),152 }153 }154155 // Some BLs provide multiple reasons (meta-BLs such as Spamhaus Zen) so156 // don't mangle them by joining with "", instead join with "; ".157158 return ListedErr{159 Identity: ip.String(),160 List: cfg.Zone,161 Reason: strings.Join(txts, "; "),162 }163}164165func queryString(ip net.IP) string {166 ipv6 := true167 if ipv4 := ip.To4(); ipv4 != nil {168 ip = ipv4169 ipv6 = false170 }171172 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.0175 } else {176 res.Grow(15) // 000.000.000.000177 }178179 for i := len(ip) - 1; i >= 0; i-- {180 octet := ip[i]181182 if ipv6 {183 // X.X184 res.WriteString(strconv.FormatInt(int64(octet&0xf), 16))185 res.WriteRune('.')186 res.WriteString(strconv.FormatInt(int64((octet&0xf0)>>4), 16))187 } else {188 // X189 res.WriteString(strconv.Itoa(int(octet)))190 }191192 if i != 0 {193 res.WriteRune('.')194 }195 }196 return res.String()197}