Add integration test for ALPN

This commit is contained in:
s0ph0s 2024-12-22 15:18:01 -05:00
parent 2db7323921
commit 5dd88e7d19
4 changed files with 155 additions and 7 deletions

View file

@ -26,6 +26,8 @@ services:
condition: service_healthy
localserve:
condition: service_healthy
moxacmepebblealpn:
condition: service_healthy
networks:
mailnet1:
ipv4_address: 172.28.1.50
@ -83,6 +85,31 @@ services:
mailnet1:
ipv4_address: 172.28.1.20
# Third mox instance that uses ACME with pebble and has ALPN enabled.
moxacmepebblealpn:
hostname: moxacmepebblealpn.mox1.example
domainname: mox3.example
image: mox_integration_moxmail
environment:
MOX_UID: "${MOX_UID}"
command: ["sh", "-c", "/integration/moxacmepebblealpn.sh"]
volumes:
- ./testdata/integration/resolv.conf:/etc/resolv.conf
- ./testdata/integration:/integration
healthcheck:
test: netstat -nlt | grep ':25 '
interval: 1s
timeout: 1s
retries: 10
depends_on:
dns:
condition: service_healthy
acmepebble:
condition: service_healthy
networks:
mailnet1:
ipv4_address: 172.28.1.80
localserve:
hostname: localserve.mox1.example
domainname: mox1.example

View file

@ -5,8 +5,10 @@
package main
import (
"bufio"
"crypto/tls"
"fmt"
"net/http"
"log/slog"
"net"
"os"
@ -190,3 +192,77 @@ a message.
})
log.Print("success", slog.Any("duration", time.Since(t0)))
}
func expectReadAfter2s(t *testing.T, hostport string, nextproto string, expected string) {
tlsConfig := &tls.Config{
NextProtos: []string{
nextproto,
},
}
conn, err := tls.Dial("tcp", hostport, tlsConfig)
if err != nil {
t.Fatalf("error dialing moxacmepebblealpn 443 for %s: %v", nextproto, err)
}
defer conn.Close()
rdr := bufio.NewReader(conn)
conn.SetReadDeadline(time.Now().Add(2 * time.Second))
line, err := rdr.ReadString('\n')
if err != nil {
t.Fatalf("error reading from %s connection: %v", nextproto, err)
}
if !strings.HasPrefix(line, expected) {
t.Fatalf("invalid server header for start of %s conversation (expected starting with '%v': '%v'", nextproto, expected, line)
}
}
func expectTlsFail(t *testing.T, hostport string, nextproto string) {
tlsConfig := &tls.Config{
NextProtos: []string{
nextproto,
},
}
conn, err := tls.Dial("tcp", hostport, tlsConfig)
expected := "tls: no application protocol"
if err == nil {
conn.Close()
t.Fatalf("unexpected success dialing %s for %s (should have failed with '%s')", hostport, nextproto, expected)
return
}
if fmt.Sprintf("%v", err) == expected {
t.Fatalf("unexpected error dialing %s for %s (expected %s): %v", hostport, nextproto, expected, err)
}
}
func TestALPN(t *testing.T) {
known_available_http_file := "https://%s/.well-known/mta-sts.txt"
log := mlog.New("integration", nil)
mlog.Logfmt = true
// ALPN should work when enabled.
alpnhost := "moxacmepebblealpn.mox1.example:443"
log.Info("trying IMAP via ALPN (should succeed)", slog.String("host", alpnhost))
expectReadAfter2s(t, alpnhost, "imap", "* OK ")
log.Info("trying SMTP via ALPN (should succeed)", slog.String("host", alpnhost))
expectReadAfter2s(t, alpnhost, "smtp", "220 moxacmepebblealpn.mox1.example ESMTP ")
log.Info("trying HTTP (should succeed)", slog.String("host", alpnhost))
_, err := http.Get(fmt.Sprintf(known_available_http_file, alpnhost))
if err != nil {
t.Fatalf("error checking for HTTP response on ALPN host (expected nil): %v", err)
}
// ALPN should not work when not enabled.
nonalpnhost := "moxacmepebble.mox1.example:443"
log.Info("trying IMAP via ALPN (should fail)", slog.String("host", nonalpnhost))
expectTlsFail(t, nonalpnhost, "imap")
log.Info("trying SMTP via ALPN (should fail)", slog.String("host", nonalpnhost))
expectTlsFail(t, nonalpnhost, "smtp")
log.Info("trying HTTP (should succeed)", slog.String("host", nonalpnhost))
_, err = http.Get(fmt.Sprintf(known_available_http_file, nonalpnhost))
if err != nil {
t.Fatalf("error checking for HTTP response on non-ALPN host (expected nil): %v", err)
}
}

View file

@ -5,13 +5,14 @@ $TTL 5m
@ NS dns.example.
moxacmepebble.mox1 A 172.28.1.10
moxmail2.mox2 A 172.28.1.20
dns A 172.28.1.30
acmepebble A 172.28.1.40
test A 172.28.1.50
localserve.mox1 A 172.28.1.60
postfixmail.postfix A 172.28.1.70
moxacmepebble.mox1 A 172.28.1.10
moxmail2.mox2 A 172.28.1.20
dns A 172.28.1.30
acmepebble A 172.28.1.40
test A 172.28.1.50
localserve.mox1 A 172.28.1.60
postfixmail.postfix A 172.28.1.70
moxacmepebblealpn.mox1 A 172.28.1.80
postfix MX 10 postfixmail.postfix.example.
postfixdkim0._domainkey.postfix TXT "v=DKIM1;h=sha256;t=s;k=ed25519;p=a4IsBTuMsSQjU+xVyx8KEd8eObis4FrCiV72OaEkvDY="

44
testdata/integration/moxacmepebblealpn.sh vendored Executable file
View file

@ -0,0 +1,44 @@
#!/bin/sh
set -x # print commands
set -e # exit on failed command
apk add unbound curl
(rm -r /tmp/mox 2>/dev/null || exit 0) # clean slate
mkdir /tmp/mox
cd /tmp/mox
mox quickstart -skipdial moxtest1@mox1.example "$MOX_UID" > output.txt
cp config/mox.conf config/mox.conf.orig
sed -i -e 's/letsencrypt:/pebble:/g' -e 's/: letsencrypt/: pebble/g' -e 's,DirectoryURL: https://acme-v02.api.letsencrypt.org/directory,DirectoryURL: https://acmepebble.example:14000/dir,' -e 's/SMTP:$/SMTP:\n\t\t\tFirstTimeSenderDelay: 1s/' -e 's/Submissions:$/Submissions:\n\t\t\tEnableOnHTTPS: true/' -e 's/IMAPS:$/IMAPS:\n\t\t\tEnableOnHTTPS: true/' config/mox.conf
cat <<EOF >>config/mox.conf
TLS:
CA:
CertFiles:
# So certificates from moxmail2 are trusted, and pebble's certificate is trusted.
- /integration/tls/ca.pem
EOF
# Recognize postfix@mox1.example as destination, and that it is a forwarding destination.
# Postfix seems to keep the mailfrom when forwarding, so we match on that verifieddomain (but using DKIM).
sed -i -e 's/moxtest1@mox1.example: nil/moxtest1@mox1.example: nil\n\t\t\tpostfix@mox1.example:\n\t\t\t\tRulesets:\n\t\t\t\t\t-\n\t\t\t\t\t\tSMTPMailFromRegexp: .*\n\t\t\t\t\t\tVerifiedDomain: mox1.example\n\t\t\t\t\t\tIsForward: true\n\t\t\t\t\t\tMailbox: Inbox/' config/domains.conf
(
cat /integration/example.zone;
sed -n '/^;/,/will be suggested/p' output.txt |
# allow sending from postfix for mox1.example.
sed 's/mox1.example. *TXT "v=spf1 ip4:172.28.1.10 mx ~all"/mox1.example. TXT "v=spf1 ip4:172.28.1.10 ip4:172.28.1.70 mx ~all"/'
) >/integration/example-integration.zone
unbound-control -s 172.28.1.30 reload # reload unbound with zone file changes
CURL_CA_BUNDLE=/integration/tls/ca.pem curl -o /integration/tmp-pebble-ca.pem https://acmepebble.example:15000/roots/0
mox -checkconsistency serve &
while true; do
if test -e data/ctl; then
echo -n accountpass1234 | mox setaccountpassword moxtest1
break
fi
sleep 0.1
done
wait