diff --git a/Makefile b/Makefile index 11bcf51..a71610f 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,15 @@ WAIT_TIMEOUT = 300 TEST_DNS ?= DNS_FLAG = $(if $(TEST_DNS),--dns $(TEST_DNS),) +# Log which resolver the container uses and what dbl.spamhaus.org answers it, +# before any mail is sent (test/share/tests/dnsbl-probe.sh). reverse and ldap2 +# inherit the host resolver, so a run that fails on a refused resolver looks +# like a delivery bug unless this is in the log, and a run that passed says +# nothing about whether the blocklist answered at all. +define dnsbl_probe + -@docker exec $(1) sh /tmp/tests/dnsbl-probe.sh +endef + all: build-no-cache default reverse ldap ldap2 sieve ecdsa traefik_acmev1 traefik_acmev2 clean no-build: default reverse ldap ldap2 sieve ecdsa traefik_acmev1 traefik_acmev2 clean default: init_default fixtures_default run_default stop_default @@ -232,6 +241,7 @@ fixtures_ldap2: docker exec mailserver_ldap2 /bin/sh -c "while ! echo PING | nc -z 0.0.0.0 11332 ; do sleep 1 ; done" # rspamd docker exec mailserver_ldap2 /bin/sh -c "while ! echo PING | nc -z 0.0.0.0 993 ; do sleep 1 ; done" # dovecot imaps docker exec mailserver_ldap2 /bin/sh -c "while ! echo PING | nc -z 0.0.0.0 587 ; do sleep 1 ; done" # submission + $(call dnsbl_probe,mailserver_ldap2) docker exec mailserver_ldap2 /bin/sh -c "python3 /tmp/tests/smtp-send.py 0.0.0.0 25 /tmp/tests/email-templates/external-to-existing-user.txt" docker exec mailserver_ldap2 /bin/sh -c "python3 /tmp/tests/smtp-send.py 0.0.0.0 25 /tmp/tests/email-templates/external-to-valid-user-subaddress.txt" docker exec mailserver_ldap2 /bin/sh -c "python3 /tmp/tests/smtp-send.py 0.0.0.0 25 /tmp/tests/email-templates/external-to-non-existing-user.txt" @@ -321,6 +331,7 @@ fixtures_reverse: docker exec mailserver_reverse /bin/sh -c "while ! echo PING | nc -z 0.0.0.0 11332 ; do sleep 1 ; done" # rspamd docker exec mailserver_reverse /bin/sh -c "while ! echo PING | nc -z 0.0.0.0 993 ; do sleep 1 ; done" # dovecot imaps docker exec mailserver_reverse /bin/sh -c "while ! echo PING | nc -z 0.0.0.0 587 ; do sleep 1 ; done" # submission + $(call dnsbl_probe,mailserver_reverse) docker exec mailserver_reverse /bin/sh -c "python3 /tmp/tests/smtp-send.py 0.0.0.0 25 /tmp/tests/email-templates/external-to-existing-user.txt" docker exec mailserver_reverse /bin/sh -c "python3 /tmp/tests/smtp-send.py 0.0.0.0 25 /tmp/tests/email-templates/external-to-valid-user-subaddress-with-default-separator.txt" docker exec mailserver_reverse /bin/sh -c "python3 /tmp/tests/smtp-send.py 0.0.0.0 25 /tmp/tests/email-templates/external-to-non-existing-user.txt" @@ -423,6 +434,7 @@ fixtures_default: # Wait for rspamd to start (default) docker exec mailserver_default /bin/sh -c "while ! echo PING | nc -z 0.0.0.0 11332 ; do sleep 1 ; done" + $(call dnsbl_probe,mailserver_default) docker exec mailserver_default /bin/sh -c "python3 /tmp/tests/smtp-send.py 0.0.0.0 25 /tmp/tests/email-templates/external-to-existing-user.txt" docker exec mailserver_default /bin/sh -c "python3 /tmp/tests/smtp-send.py 0.0.0.0 25 /tmp/tests/email-templates/external-to-existing-user-spam-learning.txt" docker exec mailserver_default /bin/sh -c "python3 /tmp/tests/smtp-send.py 0.0.0.0 25 /tmp/tests/email-templates/external-to-valid-user-subaddress.txt" diff --git a/rootfs/etc/postfix/main.cf b/rootfs/etc/postfix/main.cf index eb425c9..e50a0f1 100644 --- a/rootfs/etc/postfix/main.cf +++ b/rootfs/etc/postfix/main.cf @@ -181,14 +181,18 @@ smtpd_relay_restrictions= # * reject_non_fqdn_sender : Reject when the MAIL FROM address is not in fully-qualified domain form # * reject_unknown_sender_domain : Reject when the MAIL FROM domain has no DNS MX, no DNS A record or a malformed MX record # * reject_sender_login_mismatch: Reject when the client is not (SASL) logged in as that MAIL FROM address owner or when the client is (SASL) logged in, but the client login name doesn't own the MAIL FROM address -# * reject_rhsbl_sender : Reject when the MAIL FROM domain is blacklisted in dbl.spamhaus.org +# * reject_rhsbl_sender : Reject when the MAIL FROM domain is blacklisted in dbl.spamhaus.org. +# 127.0.1.[2..99] are the listing codes; 127.255.255.0/24 reports a problem with the query +# itself (refused resolver, too many queries) and must not be read as a listing, so it is +# only logged. Without the code filter postfix matches any answer and rejects every sender. smtpd_sender_restrictions= reject_non_fqdn_sender, reject_unknown_sender_domain, reject_sender_login_mismatch, reject_unlisted_sender, - reject_rhsbl_sender dbl.spamhaus.org, + reject_rhsbl_sender dbl.spamhaus.org=127.0.1.[2..99], + warn_if_reject reject_rhsbl_sender dbl.spamhaus.org=127.255.255.[1..255], check_sender_access hash:/etc/postfix/sender_access ## @@ -201,7 +205,9 @@ smtpd_sender_restrictions= # * reject_unknown_recipient_domain : Reject when the RCPT TO domain has no DNS MX or no DNS A record or a malformed MX record # * reject_non_fqdn_recipient : Reject when the RCPT TO address is not in fully-qualified domain form # * reject_unlisted_recipient : Reject when the RCPT TO address is not listed in the list of valid recipients for its domain -# * reject_rbl_client : Reject connections from IP addresses blacklisted in zen.spamhaus.org +# * reject_rbl_client : Reject connections from IP addresses blacklisted in zen.spamhaus.org. +# 127.0.0.[2..11] are the listing codes; the 127.255.255.0/24 query errors are only logged, +# as for dbl.spamhaus.org above. smtpd_recipient_restrictions= permit_mynetworks, @@ -210,7 +216,8 @@ smtpd_recipient_restrictions= reject_non_fqdn_recipient, reject_unlisted_recipient, check_client_access hash:/etc/postfix/rbl_override, - reject_rbl_client zen.spamhaus.org + reject_rbl_client zen.spamhaus.org=127.0.0.[2..11], + warn_if_reject reject_rbl_client zen.spamhaus.org=127.255.255.[1..255] ## # Restrictions that the Postfix SMTP server applies in the context of a client HELO command diff --git a/test/share/tests/dnsbl-probe.sh b/test/share/tests/dnsbl-probe.sh new file mode 100755 index 0000000..77ac84a --- /dev/null +++ b/test/share/tests/dnsbl-probe.sh @@ -0,0 +1,30 @@ +#!/bin/sh +# Print what this container's resolver answers for dbl.spamhaus.org, and which +# resolver that is. Runs inside the mailserver container (needs dig). +# +# dbltest.com -> 127.0.1.2 the permanent test listing: the blocklist works +# dbltest.com -> 127.255.255.x this resolver is refused: every sender gets rejected +# dbltest.com -> (nothing) answered nothing: the blocklist is inert +# gmail.com -> (nothing) the fixtures' sender domain, the name postfix looks up +# example.com -> (nothing) expected, it is not listed +# +# The resolver line gives the nameserver the container is configured with and +# the resolver's egress address as seen by two whoami services, because a +# healthy Spamhaus answer does not name it (only a refusal does). ttl is the +# remaining TTL: a full value is a fresh answer, a lower one came from cache. + +q() { dig +time=3 +tries=1 "$@" 2>/dev/null | grep -v '^;'; } + +ns=$(awk '/^nameserver/{print $2; exit}' /etc/resolv.conf) +ak=$(q +short A whoami.akamai.net | head -1) +gg=$(q +short TXT o-o.myaddr.l.google.com | grep -v edns0 | head -1 | tr -d '"') +printf '[dnsbl] resolver: nameserver=%s egress-seen-by-akamai=%s egress-seen-by-google=%s\n' "${ns:--}" "${ak:--}" "${gg:--}" + +for name in dbltest.com.dbl.spamhaus.org gmail.com.dbl.spamhaus.org example.com.dbl.spamhaus.org; do + out=$(dig +time=3 +tries=1 +noall +comments +answer +authority A "$name" 2>&1) + st=$(printf '%s\n' "$out" | sed -n 's/.*status: \([A-Z]*\).*/\1/p' | head -1); [ -z "$st" ] && st=unreachable + a=$(printf '%s\n' "$out" | grep '[[:space:]]A[[:space:]]' | sed 's/.*[[:space:]]//' | tr '\n' ',' | sed 's/,$//'); [ -z "$a" ] && a=- + ttl=$(printf '%s\n' "$out" | grep -v '^;' | awk 'NF>=5{print $2; exit}'); [ -z "$ttl" ] && ttl=- + t=$(q +short TXT "$name" | grep '^"' | head -1); [ -z "$t" ] && t=- + printf '[dnsbl] %-32s status=%-11s A=%-16s ttl=%-5s %s\n' "$name" "$st" "$a" "$ttl" "$t" +done