mirror of
https://github.com/mjl-/mox.git
synced 2025-06-27 19:08:15 +03:00
add "mox smtp dial" subcommand, for debugging connectivity issues
with various options for the tls connection.
This commit is contained in:
parent
70bbfc8f10
commit
4a14abc254
46
doc.go
46
doc.go
@ -114,6 +114,7 @@ any parameters. Followed by the help and usage information for each command.
|
||||
mox rdap domainage domain
|
||||
mox retrain [accountname]
|
||||
mox sendmail [-Fname] [ignoredflags] [-t] [<message]
|
||||
mox smtp dial host[:port]
|
||||
mox spf check domain ip
|
||||
mox spf lookup domain
|
||||
mox spf parse txtrecord
|
||||
@ -1527,6 +1528,51 @@ binary should be setgid that group:
|
||||
|
||||
usage: mox sendmail [-Fname] [ignoredflags] [-t] [<message]
|
||||
|
||||
# mox smtp dial
|
||||
|
||||
Dial the address, initialize the SMTP session, including using STARTTLS to enable TLS if the server supports it.
|
||||
|
||||
If no port is specified, SMTP port 25 is used.
|
||||
|
||||
Data is copied between connection and stdin/stdout until either side closes the
|
||||
connection.
|
||||
|
||||
The flags influence the TLS configuration, useful for debugging interoperability
|
||||
issues.
|
||||
|
||||
No MTA-STS or DANE verification is done.
|
||||
|
||||
Hint: Use "mox -loglevel trace smtp dial ..." to see the protocol messages
|
||||
exchanged during connection set up.
|
||||
|
||||
usage: mox smtp dial host[:port]
|
||||
-ehlohostname string
|
||||
our hostname to use during the SMTP EHLO command
|
||||
-forcetls
|
||||
use TLS, even if remote SMTP server does not announce STARTTLS extension
|
||||
-notls
|
||||
do not use TLS
|
||||
-remotehostname string
|
||||
remote hostname to use for TLS verification, if enabled; the hostname from the parameter is used by default
|
||||
-tlscerts string
|
||||
path to root ca certificates in pem form, for verification
|
||||
-tlsciphersuites string
|
||||
ciphersuites to allow, comma-separated, order is ignored, only for TLS 1.2 and earlier, empty value uses TLS stack defaults; values: tls_ecdhe_ecdsa_with_aes_128_cbc_sha, tls_ecdhe_ecdsa_with_aes_128_gcm_sha256, tls_ecdhe_ecdsa_with_aes_256_cbc_sha, tls_ecdhe_ecdsa_with_aes_256_gcm_sha384, tls_ecdhe_ecdsa_with_chacha20_poly1305_sha256, tls_ecdhe_rsa_with_aes_128_cbc_sha, tls_ecdhe_rsa_with_aes_128_gcm_sha256, tls_ecdhe_rsa_with_aes_256_cbc_sha, tls_ecdhe_rsa_with_aes_256_gcm_sha384, tls_ecdhe_rsa_with_chacha20_poly1305_sha256, and insecure: tls_ecdhe_ecdsa_with_aes_128_cbc_sha256, tls_ecdhe_ecdsa_with_rc4_128_sha, tls_ecdhe_rsa_with_3des_ede_cbc_sha, tls_ecdhe_rsa_with_aes_128_cbc_sha256, tls_ecdhe_rsa_with_rc4_128_sha, tls_rsa_with_3des_ede_cbc_sha, tls_rsa_with_aes_128_cbc_sha, tls_rsa_with_aes_128_cbc_sha256, tls_rsa_with_aes_128_gcm_sha256, tls_rsa_with_aes_256_cbc_sha, tls_rsa_with_aes_256_gcm_sha384, tls_rsa_with_rc4_128_sha
|
||||
-tlscurves string
|
||||
tls ecc key exchange mechanisms to allow, comma-separated, order is ignored, empty value uses TLS stack defaults; values: curvep256, curvep384, curvep521, x25519, x25519mlkem768
|
||||
-tlsnodynamicrecordsizing
|
||||
disable TLS dynamic record sizing
|
||||
-tlsnosessiontickets
|
||||
disable TLS session tickets
|
||||
-tlsrenegotiation string
|
||||
when to allow renegotiation; only applies to tls1.2 and earlier, not tls1.3; values: never, once, always (default "never")
|
||||
-tlsverify
|
||||
verify remote hostname during TLS
|
||||
-tlsversionmax string
|
||||
maximum TLS version, empty value uses TLS stack default; values: tls1.2, etc.
|
||||
-tlsversionmin string
|
||||
minimum TLS version, empty value uses TLS stack default; values: tls1.2, etc.
|
||||
|
||||
# mox spf check
|
||||
|
||||
Check the status of IP for the policy published in DNS for the domain.
|
||||
|
224
main.go
224
main.go
@ -12,6 +12,7 @@ import (
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
@ -23,6 +24,7 @@ import (
|
||||
"io/fs"
|
||||
"log"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@ -201,6 +203,7 @@ var commands = []struct {
|
||||
{"rdap domainage", cmdRDAPDomainage},
|
||||
{"retrain", cmdRetrain},
|
||||
{"sendmail", cmdSendmail},
|
||||
{"smtp dial", cmdSMTPDial},
|
||||
{"spf check", cmdSPFCheck},
|
||||
{"spf lookup", cmdSPFLookup},
|
||||
{"spf parse", cmdSPFParse},
|
||||
@ -1897,6 +1900,227 @@ with DKIM, by mox.
|
||||
xcheckf(err, "writing rsa private key")
|
||||
}
|
||||
|
||||
// todo: options for specifying the domain this is the mx host of, and enabling dane and/or mta-sts verification
|
||||
func cmdSMTPDial(c *cmd) {
|
||||
c.params = "host[:port]"
|
||||
|
||||
var tlsCerts, tlsCiphersuites, tlsCurves, tlsVersionMin, tlsVersionMax, tlsRenegotiation string
|
||||
var tlsVerify, noTLS, forceTLS, tlsNoSessionTickets, tlsNoDynamicRecordSizing bool
|
||||
var ehloHostnameStr, remoteHostnameStr string
|
||||
|
||||
ciphersuites := map[string]*tls.CipherSuite{}
|
||||
ciphersuitesInsecure := map[string]*tls.CipherSuite{}
|
||||
for _, v := range tls.CipherSuites() {
|
||||
if slices.Contains(v.SupportedVersions, tls.VersionTLS10) || slices.Contains(v.SupportedVersions, tls.VersionTLS11) || slices.Contains(v.SupportedVersions, tls.VersionTLS12) {
|
||||
ciphersuites[strings.ToLower(v.Name)] = v
|
||||
}
|
||||
}
|
||||
for _, v := range tls.InsecureCipherSuites() {
|
||||
if slices.Contains(v.SupportedVersions, tls.VersionTLS10) || slices.Contains(v.SupportedVersions, tls.VersionTLS11) || slices.Contains(v.SupportedVersions, tls.VersionTLS12) {
|
||||
ciphersuitesInsecure[strings.ToLower(v.Name)] = v
|
||||
}
|
||||
}
|
||||
|
||||
curvesList := []tls.CurveID{
|
||||
tls.CurveP256,
|
||||
tls.CurveP384,
|
||||
tls.CurveP521,
|
||||
tls.X25519,
|
||||
tls.X25519MLKEM768,
|
||||
}
|
||||
curves := map[string]tls.CurveID{}
|
||||
for _, a := range curvesList {
|
||||
curves[strings.ToLower(a.String())] = a
|
||||
}
|
||||
|
||||
c.flag.StringVar(&tlsCiphersuites, "tlsciphersuites", "", "ciphersuites to allow, comma-separated, order is ignored, only for TLS 1.2 and earlier, empty value uses TLS stack defaults; values: "+strings.Join(slices.Sorted(maps.Keys(ciphersuites)), ", ")+", and insecure: "+strings.Join(slices.Sorted(maps.Keys(ciphersuitesInsecure)), ", "))
|
||||
c.flag.StringVar(&tlsCurves, "tlscurves", "", "tls ecc key exchange mechanisms to allow, comma-separated, order is ignored, empty value uses TLS stack defaults; values: "+strings.Join(slices.Sorted(maps.Keys(curves)), ", "))
|
||||
c.flag.StringVar(&tlsCerts, "tlscerts", "", "path to root ca certificates in pem form, for verification")
|
||||
c.flag.StringVar(&tlsVersionMin, "tlsversionmin", "", "minimum TLS version, empty value uses TLS stack default; values: tls1.2, etc.")
|
||||
c.flag.StringVar(&tlsVersionMax, "tlsversionmax", "", "maximum TLS version, empty value uses TLS stack default; values: tls1.2, etc.")
|
||||
c.flag.BoolVar(&tlsVerify, "tlsverify", false, "verify remote hostname during TLS")
|
||||
c.flag.BoolVar(&tlsNoSessionTickets, "tlsnosessiontickets", false, "disable TLS session tickets")
|
||||
c.flag.BoolVar(&tlsNoDynamicRecordSizing, "tlsnodynamicrecordsizing", false, "disable TLS dynamic record sizing")
|
||||
c.flag.BoolVar(&noTLS, "notls", false, "do not use TLS")
|
||||
c.flag.BoolVar(&forceTLS, "forcetls", false, "use TLS, even if remote SMTP server does not announce STARTTLS extension")
|
||||
c.flag.StringVar(&tlsRenegotiation, "tlsrenegotiation", "never", "when to allow renegotiation; only applies to tls1.2 and earlier, not tls1.3; values: never, once, always")
|
||||
c.flag.StringVar(&ehloHostnameStr, "ehlohostname", "", "our hostname to use during the SMTP EHLO command")
|
||||
c.flag.StringVar(&remoteHostnameStr, "remotehostname", "", "remote hostname to use for TLS verification, if enabled; the hostname from the parameter is used by default")
|
||||
|
||||
c.help = `Dial the address, initialize the SMTP session, including using STARTTLS to enable TLS if the server supports it.
|
||||
|
||||
If no port is specified, SMTP port 25 is used.
|
||||
|
||||
Data is copied between connection and stdin/stdout until either side closes the
|
||||
connection.
|
||||
|
||||
The flags influence the TLS configuration, useful for debugging interoperability
|
||||
issues.
|
||||
|
||||
No MTA-STS or DANE verification is done.
|
||||
|
||||
Hint: Use "mox -loglevel trace smtp dial ..." to see the protocol messages
|
||||
exchanged during connection set up.
|
||||
`
|
||||
args := c.Parse()
|
||||
if len(args) != 1 {
|
||||
c.Usage()
|
||||
}
|
||||
|
||||
if noTLS && forceTLS {
|
||||
log.Fatalf("cannot have both -notls and -forcetls")
|
||||
}
|
||||
|
||||
parseTLSVersion := func(s string) uint16 {
|
||||
switch s {
|
||||
case "tls1.0":
|
||||
return tls.VersionTLS10
|
||||
case "tls1.1":
|
||||
return tls.VersionTLS11
|
||||
case "tls1.2":
|
||||
return tls.VersionTLS12
|
||||
case "tls1.3":
|
||||
return tls.VersionTLS13
|
||||
case "":
|
||||
return 0
|
||||
default:
|
||||
log.Fatalf("invalid tls version %q", s)
|
||||
panic("not reached")
|
||||
}
|
||||
}
|
||||
tlsConfig := tls.Config{
|
||||
MinVersion: parseTLSVersion(tlsVersionMin),
|
||||
MaxVersion: parseTLSVersion(tlsVersionMax),
|
||||
InsecureSkipVerify: !tlsVerify,
|
||||
SessionTicketsDisabled: tlsNoSessionTickets,
|
||||
DynamicRecordSizingDisabled: tlsNoDynamicRecordSizing,
|
||||
}
|
||||
|
||||
switch tlsRenegotiation {
|
||||
case "never":
|
||||
tlsConfig.Renegotiation = tls.RenegotiateNever
|
||||
case "once":
|
||||
tlsConfig.Renegotiation = tls.RenegotiateOnceAsClient
|
||||
case "always":
|
||||
tlsConfig.Renegotiation = tls.RenegotiateFreelyAsClient
|
||||
default:
|
||||
log.Fatalf("invalid value %q for -tlsrenegotation", tlsRenegotiation)
|
||||
}
|
||||
if tlsCerts != "" {
|
||||
pool := x509.NewCertPool()
|
||||
pembuf, err := os.ReadFile(tlsCerts)
|
||||
xcheckf(err, "reading tls certificates")
|
||||
ok := pool.AppendCertsFromPEM(pembuf)
|
||||
if !ok {
|
||||
c.log.Warn("no tls certificates found", slog.String("path", tlsCerts))
|
||||
}
|
||||
tlsConfig.RootCAs = pool
|
||||
}
|
||||
if tlsCiphersuites != "" {
|
||||
for s := range strings.SplitSeq(tlsCiphersuites, ",") {
|
||||
s = strings.TrimSpace(s)
|
||||
c, ok := ciphersuites[s]
|
||||
if !ok {
|
||||
c, ok = ciphersuitesInsecure[s]
|
||||
}
|
||||
if !ok {
|
||||
log.Fatalf("unknown ciphersuite %q", s)
|
||||
}
|
||||
tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, c.ID)
|
||||
}
|
||||
}
|
||||
if tlsCurves != "" {
|
||||
for s := range strings.SplitSeq(tlsCurves, ",") {
|
||||
s = strings.TrimSpace(s)
|
||||
if c, ok := curves[s]; !ok {
|
||||
log.Fatalf("unknown ecc key exchange algorithm %q", s)
|
||||
} else {
|
||||
tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var host, portStr string
|
||||
var err error
|
||||
host, portStr, err = net.SplitHostPort(args[0])
|
||||
if err != nil {
|
||||
host = args[0]
|
||||
portStr = "25"
|
||||
}
|
||||
port, err := strconv.ParseInt(portStr, 10, 64)
|
||||
xcheckf(err, "parsing port %q", portStr)
|
||||
|
||||
if remoteHostnameStr == "" {
|
||||
remoteHostnameStr = host
|
||||
}
|
||||
remoteHostname, err := dns.ParseDomain(remoteHostnameStr)
|
||||
xcheckf(err, "parsing remote host")
|
||||
tlsConfig.ServerName = remoteHostname.Name()
|
||||
|
||||
resolver := dns.StrictResolver{Pkg: "smtpdial"}
|
||||
_, _, _, ips, _, err := smtpclient.GatherIPs(context.Background(), c.log.Logger, resolver, "ip", dns.IPDomain{Domain: remoteHostname}, nil)
|
||||
xcheckf(err, "resolve host")
|
||||
c.log.Info("resolved remote address", slog.Any("ips", ips))
|
||||
|
||||
dialer := &net.Dialer{Timeout: 5 * time.Second}
|
||||
dialedIPs := map[string][]net.IP{}
|
||||
conn, ip, err := smtpclient.Dial(context.Background(), c.log.Logger, dialer, dns.IPDomain{Domain: remoteHostname}, ips, int(port), dialedIPs, nil)
|
||||
xcheckf(err, "dial")
|
||||
c.log.Info("connected to remote host", slog.Any("ip", ip))
|
||||
|
||||
tlsMode := smtpclient.TLSOpportunistic
|
||||
if forceTLS {
|
||||
tlsMode = smtpclient.TLSRequiredStartTLS
|
||||
} else if noTLS {
|
||||
tlsMode = smtpclient.TLSSkip
|
||||
}
|
||||
var ehloHostname dns.Domain
|
||||
if ehloHostnameStr == "" {
|
||||
name, err := os.Hostname()
|
||||
xcheckf(err, "get hostname")
|
||||
ehloHostnameStr = name
|
||||
}
|
||||
ehloHostname, err = dns.ParseDomain(ehloHostnameStr)
|
||||
xcheckf(err, "parse hostname")
|
||||
|
||||
opts := smtpclient.Opts{
|
||||
TLSConfig: &tlsConfig,
|
||||
}
|
||||
client, err := smtpclient.New(context.Background(), c.log.Logger, conn, tlsMode, false, ehloHostname, dns.Domain{}, opts)
|
||||
xcheckf(err, "new smtp client")
|
||||
|
||||
cs := client.TLSConnectionState()
|
||||
if cs == nil {
|
||||
c.log.Info("smtp initialized without tls")
|
||||
} else {
|
||||
c.log.Info("smtp initialized with tls",
|
||||
slog.String("version", tls.VersionName(cs.Version)),
|
||||
slog.String("ciphersuite", strings.ToLower(tls.CipherSuiteName(cs.CipherSuite))),
|
||||
slog.String("sni", cs.ServerName),
|
||||
)
|
||||
for _, chain := range cs.VerifiedChains {
|
||||
var l []string
|
||||
for _, cert := range chain {
|
||||
s := fmt.Sprintf("dns names %q, common name %q, %s - %s, issuer %q)", strings.Join(cert.DNSNames, ","), cert.Subject.CommonName, cert.NotBefore.Format("2006-01-02T15:04:05"), cert.NotAfter.Format("2006-01-02T15:04:05"), cert.Issuer.CommonName)
|
||||
l = append(l, s)
|
||||
}
|
||||
c.log.Info("tls certificate verification chain", slog.String("chain", strings.Join(l, "; ")))
|
||||
}
|
||||
}
|
||||
|
||||
conn, err = client.Conn()
|
||||
xcheckf(err, "get smtp session connection")
|
||||
|
||||
go func() {
|
||||
_, err := io.Copy(os.Stdout, conn)
|
||||
xcheckf(err, "copy from connection to stdout")
|
||||
err = conn.Close()
|
||||
c.log.Check(err, "closing connection")
|
||||
}()
|
||||
_, err = io.Copy(conn, os.Stdin)
|
||||
xcheckf(err, "copy from stdin to connection")
|
||||
}
|
||||
|
||||
func cmdDANEDial(c *cmd) {
|
||||
c.params = "host:port"
|
||||
var usages string
|
||||
|
@ -114,6 +114,7 @@ type Client struct {
|
||||
daneMoreHostnames []dns.Domain // Additional allowed names in TLS certificate for DANE-TA.
|
||||
daneVerifiedRecord *adns.TLSA // If non-nil, then will be set to verified DANE record if any.
|
||||
clientCert *tls.Certificate // If non-nil, tls client authentication is done.
|
||||
tlsConfigOpts *tls.Config // If non-nil, tls config to use.
|
||||
|
||||
// TLS connection success/failure are added. These are always non-nil, regardless
|
||||
// of what was passed in opts. It lets us unconditionally dereference them.
|
||||
@ -235,6 +236,12 @@ type Opts struct {
|
||||
// tracked.
|
||||
RecipientDomainResult *tlsrpt.Result // MTA-STS or no policy.
|
||||
HostResult *tlsrpt.Result // DANE or no policy.
|
||||
|
||||
// If not nil, the TLS config to use instead of the default. Useful for custom
|
||||
// certificate verification or TLS parameters. The other DANE/TLS/certificate
|
||||
// fields in [Opts], and the tlsVerifyPKIX and remoteHostname parameters to [New]
|
||||
// have no effect when TLSConfig is set.
|
||||
TLSConfig *tls.Config
|
||||
}
|
||||
|
||||
// New initializes an SMTP session on the given connection, returning a client that
|
||||
@ -290,6 +297,7 @@ func New(ctx context.Context, elog *slog.Logger, conn net.Conn, tlsMode TLSMode,
|
||||
cmds: []string{"(none)"},
|
||||
recipientDomainResult: ensureResult(opts.RecipientDomainResult),
|
||||
hostResult: ensureResult(opts.HostResult),
|
||||
tlsConfigOpts: opts.TLSConfig,
|
||||
}
|
||||
c.log = mlog.New("smtpclient", elog).WithFunc(func() []slog.Attr {
|
||||
now := time.Now()
|
||||
@ -354,6 +362,10 @@ func (c *Client) tlsConfig() *tls.Config {
|
||||
// failures. And we may have to verify both PKIX and DANE, record errors for
|
||||
// each, and possibly ignore the errors.
|
||||
|
||||
if c.tlsConfigOpts != nil {
|
||||
return c.tlsConfigOpts
|
||||
}
|
||||
|
||||
verifyConnection := func(cs tls.ConnectionState) error {
|
||||
// Collect verification errors. If there are none at the end, TLS validation
|
||||
// succeeded. We may find validation problems below, record them for a TLS report
|
||||
@ -1453,9 +1465,10 @@ func (c *Client) Close() (rerr error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Conn returns the connection with initialized SMTP session. Once the caller uses
|
||||
// this connection it is in control, and responsible for closing the connection,
|
||||
// and other functions on the client must not be called anymore.
|
||||
// Conn returns the connection with the initialized SMTP session, possibly wrapping
|
||||
// a TLS connection, and handling protocol trace logging. Once the caller uses this
|
||||
// connection it is in control, and responsible for closing the connection, and
|
||||
// other functions on the client must not be called anymore.
|
||||
func (c *Client) Conn() (net.Conn, error) {
|
||||
if err := c.conn.SetDeadline(time.Time{}); err != nil {
|
||||
return nil, fmt.Errorf("clearing io deadlines: %w", err)
|
||||
|
Loading…
x
Reference in New Issue
Block a user