电子邮件系统中的 DNS 记录

2023-10-29 ⏳8.7分钟(3.5千字)

最近要搭建个人电子邮件系统,正在系统学习相关的知识。今天分享与 DNS 相关的内容。

我根据功能或者场景把内容大致分了三块:发信、收信和看信。发信就是使用邮件系统向外面的服务器发送邮件;收信则是接收外部服务器发来的电子邮件;看信则针对于使用客户端查看或者下载服务器上的邮件。先说发信。

发信

PTR

每个电子邮箱都有自己的服务器。发信的时候是发送方的服务器连接到接收方服务器的 25 号端口进行通信。理论上互联网上的任意设备都能充当发送服务器的角色。但很快就产生了大规模的垃圾邮件问题。发垃圾邮件的人使用提供的动态 IP 地址,打一枪换一个地方,让人防不胜防。最后导致两个结果:

  1. 禁止用户设备访问外网服务器的 25 端口。现在很多云服务厂商依然执行该策略。
  2. 收件服务器通过 DNS 的 PTR 记录反向查询 IP 地址的域名,如果跟发件人的域名不匹配则拒绝接收。

通过以上两种策略,垃圾邮件问题有所缓解。所以早期设置邮件服务器都需要给对应的 IP 地址添加 PTR 记录。

比如谷歌的 IP 地址 8.8.4.4,它的 PTR 域名是 4.4.8.8.in-addr.arpa,对应的 PTR 值为:

drill PTR 4.4.8.8.in-addr.arpa
# or
drill -x 8.8.4.4
;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 3489
;; flags: qr rd ra ; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;; 4.4.8.8.in-addr.arpa.        IN      PTR

;; ANSWER SECTION:
4.4.8.8.in-addr.arpa.   60908   IN      PTR     dns.google.

这里指向了 dns.google 这个域名。

PTR 记录使用专用的域名空间 in-addr.arpa/ip6.arpa,查询 PTR 记录会对 DNS 系统造成较大的压力,而且会减缓邮件收取的速度。所以 IETF 又设计了新的 SPF1 记录来取待 PTR。

SPF

所谓 SPF,就是 Sender Policy Framework,简单来说就是用一种特殊的 TXT 记录保存某个域名允许哪些服务器发送该域下的电子邮件。

以 QQ 邮箱为例,我们查询它的 TXT 记录:

drill txt qq.com
;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 13765
;; flags: qr rd ra ; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;; qq.com.      IN      TXT

;; ANSWER SECTION:
qq.com. 450     IN      TXT     "v=spf1 include:spf.mail.qq.com -all"

这里首先看到 SPF 前缀 v=spf1,然后是 include:spf.mail.qq.com,它表示真正的服务器列表需要再查spf.mail.qq.com的 TXT 记录,而且只允许该列表中的服务器发送 @qq.com 域的邮件。最后的-all表示如果有其他服务器尝试发送 @qq.com 域邮件,请接收方务必拒收。

对于最后的-all,有时候还会看到类似~all取值,这种的意思是如果邮件来自 SPF 列表之外的服务器,则需要接收后放到垃圾邮件中。这种策略比拒收要稳托一些,主要用于避免 SPF 没有及时更新而导致邮件被完全拒收的情况。

我们继续查询 spf.mail.qq.com 对应的 TXT 记录:

drill txt spf.mail.qq.com
;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 27877
;; flags: qr rd ra ; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;; spf.mail.qq.com.     IN      TXT

;; ANSWER SECTION:
spf.mail.qq.com.        600     IN      TXT     "v=spf1 include:qq-a.mail.qq.com include:qq-b.mail.qq.com include:qq-c.mail.qq.com include:biz-a.mail.qq.com include:biz-b.mail.qq.com include:biz-c.mail.qq.com include:biz-d.mail.qq.com -all"

显然,这是一个更长的 SPF 列表,我们取第一个 qq-a.mail.qq.com 继续查询:

drill txt qq-a.mail.qq.com
;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 51025
;; flags: qr rd ra ; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;; qq-a.mail.qq.com.    IN      TXT

;; ANSWER SECTION:
qq-a.mail.qq.com.       600     IN      TXT     "v=spf1 ip4:101.226.139.0/25 ip4:101.91.43.0/25 ip4:101.91.44.128/25 ip4:112.64.237.128/25 ip4:116.128.173.0/25 ip4:121.51.40.128/25 ip4:121.51.6.0/25 ip4:162.62.52.214 ip4:162.62.55.67 ip4:162.62.57.0/24 ip4:162.62.58.211 ip4:162.62.58.216 -all"

终于到头了,这次不再有 include 字样,而是代之以 ip4 开头的 IP 网段列表,这里可以是网段,比如 ip4:101.226.139.0/25,也可以是单独的地址,比如 ip4:162.62.58.216。如果需要允许 IPv6 网络地址,则需要添加 ip6: 前缀,同样也支持网段处单个地址。

除了 include/ip4/ip6 外,SPF 记录还支持以下类型的数据:

从上面的的分析我们可以看出,收信方在收信前需要递归查询 DNS 中的 SPF 记录,来确认发信方是否被允许发信。这个过程可能会触发很多次 DNS 解析。为了避免引起 DDos 攻击,SPF 规定所有 DNS 查询不能超过 10 次,超过了就拒收!

对于个人邮件服务而言,一般不会有很多服务器,完全可以都写在一条 SPF 记录中,不需要递归查询。

假设我们的邮件域名是 example.org,对应的邮件服务器是 ex1.example.com,它的 IP 地址为 A:10.2.3.4/AAAA:2001:beef::1。

最推荐的写法是

mx1.example.org. TXT   "v=spf1 ip4:10.2.3.4 ip6:2001:beef::1 ~all"

如果你的 IP 地址不固定,有时会变,则可以使用 a 类型:

mx1.example.org. TXT   "v=spf1 a:ex1.example.com ~all"

如果 example.com 的发件服务器域名经常变,也就是 MX 会改,改么只能写成如下形式了:

mx1.example.org. TXT   "v=spf1 mx ~all"

但无论是 mx 型还是 a 型,都会让收信方产生额外的 DNS 查询。所以建议还是使用第一种。

不管用哪种,大家一定要注意 SPF 记录的格式和取值,如果搞错可能就发不出邮件。大家设置好之后可以通过 MailHardener2 检查,非常方便。

SPF 记录本质上是通过 IP 白名单限制发送服务器的范围。IETF 还设计了一种通过数字签名来限制发送范围的方案,这就是 DKIM。

DKIM

DKIM 全称 DomainKeys Identified Mail Signatures,由 RFC63763 定义。

DKIM 的本质是生成一对非对称密钥对。把公钥通过 DNS 的 DKIM 记录发布出来。然后服务器发送邮件前使用自己的私钥将内容签名后再发送。收信方接收后查询对应的 DKIM 记录,找到公钥后验证签名。验证通过方可接收,否则就拒收。

这里的密钥对通常是由发送服务器自动生成的,常见的就是 RSA-SHA256 签名。一个 DKIM 示例如下:

drill txt default._domainkey.example.org
;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 50686
;; flags: qr rd ra ; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;; default._domainkey.lvht.net. IN      TXT

;; ANSWER SECTION:
default._domainkey.example.org.    300     IN      TXT     "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyM+Hi+2mk4cvxtTiEdFtXHuRiU0h40yEkPF1hJMQcoPp+SGM5Ikor52X7lNqgXE8zFkjMUMlykltwqdOGBa5ZEG1fRZx2sPRi3q5dXrL6kfUsNSn5V8ZBZFm4FtcIV9c7bPAvfp2ZRnLrkPSQzJ1f6R7XgfxP5JSFzNfpBFanToRXVGlftWyrw6Uro3C366mE" "Bf/n1100JpAecAsiiL1GnLtHAilbo/QRhyI3nxx5Jt0rSFf69S3mrydDWFsmxUTrw9/YGQT7xEL2tk08FTdNj9r9TRXmNpaQnSJxJJWmZ33V+VLOsEyBmiFbjuzD6U6wtSOW2PexKEOPmV0pHwHhQIDAQAB"

这里是在 _domainkey 子域下指定了 default 子域。其实这个 default 可以随便填,只要跟邮件签名里的值对得上就行。

下面是一个 DKIM 签名示例:

DKIM-Signature: a=rsa-sha256; bh=iQ/2DHA4xYuzNkuty4mmtgXpGyW0lOv+tDU9bZ5y9zA=; c=relaxed/relaxed; d=lvht.net; h=Subject:Subject:Sender:To:To:Cc:From:From:Date:Date:MIME-Version:Content-Type:Content-Transfer-Encoding:Reply-To:In-Reply-To:Message-Id:Message-Id:References:Autocrypt:Openpgp; i=@example.org; s=default; t=1698495966; v=1; x=1698927966; b=fsRwrjAXpqNbPhBLPLL/cQKe1RNnAcGyBm4QSzs3NNixS1MxWwza0rUs4BAvXfRlDvSoDi6i akpFOMqeZ0PzO8TP0VSJW62BallAAwUO4iEapRsDqh7hj22KY5K2OHvHum75B0UyQTCGEJM4mzr waliehLExSCUQ8dh0C2tNzcwnuNZ54nH5thIF2yKR6M9Ty9lEIn3xnNnUFAjnZ8yn4s+M3iXQid G/R9PZ6k5/QDm9aTMvfTjIJxs8ZM0hwz/uyHsUEU7Z0eYlWd0DjVOaSr/NP/dbNKuCSTut3Vqhr B9PnVKESBY0mSw1QaMKkONpZnvE6xo9JsbUHdf9G7ZMvw==

签名的格式和记算方法本文不展开讨论。大家注意签名中的 s=default 就可以了。收信方会根据它查询 default._domainkey.example.org 对应的 DKIM 记录来读取公钥。

无论是 SPF 还是 DKIM,如果全部验证失败,收信方就会拒信。这有两种可能,一种是有人尝试冒充你发送邮件,另一种可能是你的邮箱系统配置有误。但无论是哪种,最好应该及时通知该域名的系统管理员。于是就诞生了 DMARC。

DMARC

DMARC 全称 Domain-based Message Authentication, Reporting, and Conformance4

先给出一个例子:

_dmarc.example.org.   TXT    "v=DMARC1; p=quarantine; ruf=mailto:postmaster@example.org"

这里的 p 表示 policy。管理员可以通过 DMARC 影响收信方针对 SPF 或者 DKIM 的验证行为。上例中的 quarantine 表示隔离,通常就是让收信方将未通过验证的邮件移到垃圾箱。还可以指定为 p=reject 表示拒收。而如果改为 p=none,则表示不干预,由收信方自行处置。后面的 ruf=mailto:postmaster@example.org 则是收求收信方定期发送异常处理报告,这样发信域的管理员就能及时收到报警了。

以下是 GMail 发的一封报告邮件:

This is the mail delivery system at mx1.lehu.in.

Unfortunately, your message could not be delivered to one or more
recipients. The usual cause of this problem is invalid
recipient address or maintenance at the recipient side.

Contact the postmaster for further assistance, provide the Message ID (below):

Message ID: XXXXXXXX
Arrival: 2023-10-28 10:54:44 +0000 UTC
Last delivery attempt: 2023-10-28 10:54:44 +0000 UTC

Delivery to XXXX@gmail.com failed with error: gmail-smtp-in.l.google.com.
said: This mail has been blocked because the sender is unauthenticated.
Gmail requires all senders to authenticate with either SPF or DKIM.

Authentication results:
DKIM = did not pass
SPF [example.org] with ip: [XXXX:XXXX:XXXX:XXXX::] = did not pass

...

以上是发信的部分,下面继续说收信部分。

收信

MX

当外域用户需要给某一邮箱发信时,它会先通过 DNS 查询 MX 记录。假设邮箱地址是 demo@gmail.com。发信方首先会查询 gmail.org 的 MX 记录:

drill mx gmail.com
;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 393
;; flags: qr rd ra ; QUERY: 1, ANSWER: 5, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;; gmail.com.   IN      MX

;; ANSWER SECTION:
gmail.com.      3467    IN      MX      5 gmail-smtp-in.l.google.com.
gmail.com.      3467    IN      MX      30 alt3.gmail-smtp-in.l.google.com.
gmail.com.      3467    IN      MX      10 alt1.gmail-smtp-in.l.google.com.
gmail.com.      3467    IN      MX      20 alt2.gmail-smtp-in.l.google.com.
gmail.com.      3467    IN      MX      40 alt4.gmail-smtp-in.l.google.com.

一个域名可以有多个 MX 记录,每条对应一个域名,每个域名对应一台或者一组服务器,它们有不同的权重,数值越小权重越大。上面的 gmail.org 示例中,权重最高的是 gmail-smtp-in.l.google.com 这台,权重是 5。外域服务器要给 gmail.com 域发邮件会优先连接这台服务器。如果连不上,则会根据权重依次连接其他服务器。

其实收信所需要的最核心的 DNS 记录就说完了🤷但为了内容的完整性和系统性,我在这里额外讲一下 MTA-STS5

MTA-STS

MTA-STS 全称 SMTP MTA Strict Transport Security。这里之所以叫 Strict,是因为之前的安全措施很松散。电子邮件系统使用 SMTP 协议,通过 TCP 25 号端口通信。一开始都是明文,不加密。后来需要加密了,但需要保持向前兼容,只能默认使用明文协议,然后通过 STARTTLS 升级到 TLS 加密通信上。但这种升级可能受到中间人降级攻击,让收发双方以为对方不支持加密,从而只能使用明文通信。

为了解决这个问题,MTA-STS 应运而生。

首先为邮件域名添加如下 TXT 记录:

_mta-sts.example.org.   TXT    "v=STSv1; id=1"

这里的 _mta-sts 是固定前缀,v=STSv1 表示版本,后面的 id=1 表示内容版本。他域服务器在向 example.org 域发信之前,会先查询对应的 MTA-STS 记录。如果有,则会进一步请求其对应的 MTA-STS 策略数据,这一步是通过 HTTPS 实现的,为的是避免数据被中间人篡改。

MTA-STS 对应的 HTTPS 链接也是标准化的,取值为

https://mta-sts.example.org/.well-known/mta-sts.txt

该 URL 的反回内容格式如下:

version: STSv1
mode: enforce
max_age: 604800
mx: mx1.example.org
mx: mx2.example.org

前面的 TXT 记录中的 id=1 表示的就是这个 mta-sts.txt 内容版本。如果发布了新的策略,应该更新 DNS 中的 id 取值。只要 DNS 刷新了,发信方就会更新自己的本地缓存。

策略中的 mode: enforce 要求发信方必须升级到 TLS 加密传输。这样就避免了中间人的降级攻击。

细心的读者可能会问,为什么这里要同时使用 DNS + HTTPS 来发布 mta-sts.txt 呢?其实也有纯 DNS 方案,由 RFC76726 定义,基于 DNSSEC 和 DANE 技术。但目前 DNSSEC 技术还没有普及,所以只能鉴时用 HTTPS 先过渡一下。

TLSRPT

跟 DMARC 机制类似,IETF 还定义了 SMTP TLS Reporting7,用来给发信方向收信方报告 TLS 加密升级情况。它的 DNS 记录格式如下:

_smtp._tls.example.org. TXT    "v=TLSRPTv1;rua=mailto:postmaster@example.org"

这里的 _smtp._tls 是固定子域。后面的rua=mailto:postmaster@example.org 指定接收报告的邮箱地址。以下是一份报造样例:

{
  "report-id":"2020-01-01T00:00:00Z_mailhardener.com",  
  "date-range":{
    "start-datetime":"2020-01-01T00:00:00Z",
    "end-datetime":"2020-01-07T23:59:59Z"
  },
  "organization-name":"Google Inc.",
  "contact-info":"smtp-tls-reporting@google.com",
  "policies":[
    {
      "policy": {
        "policy-type":"sts",
        "policy-string":[
           "version: STSv1",
           "mode: enforce",
           "mx: demo.mailhardener.com",
           "max_age: 604800"
        ],
        "policy-domain":"mailhardener.com"
      },
      "summary":{
        "total-successful-session-count":23,
        "total-failure-session-count": 1
      },
      "failure-details": [
        {
          "result-type": "certificate-host-mismatch",
          "sending-mta-ip": "123.123.123.123",
          "receiving-ip": "234.234.234.234",
          "receiving-mx-hostname": "demo.mailhardener.com",
          "failed-session-count": 1
        }
      ]
    }
  ]
}

这里面只有整体统计数据,不包含私人邮件内容。同样的,管理员可以根据 TLSRPT 报告确定本域系统配置是否正常。

但是 MTA-STS 是一种增强型配置,不加也不会影响邮件系统的功能,也不会影响收信。唯一的隐患是用户的通信内容可能被别人监测到。

以一两节说的都是不同的邮件服务器(MTA)之间通信所需要的 DNS 记录。其实客户端(MUA) 跟服务器之间通信也可以设置一些 DNS 记录。下面我们就介绍一下。

看信

客户端连接服务器无外乎两个功能,寄信和取信。寄信叫 SMTP Submission,使用 SMTP 的 587 端口。取信分两种,POP3 和 IMAP。POP3 的端口是 110 和 995,IMAP 使用的端口是 143 和 993。如果读者不了解这些端口,可以参考我的另一篇文章

这些端口(如果有的话)都可以通过为 example.org 添加 SRV 记录发布:

_submission._tcp    SRV 0 1 587 mail.example.com.
_imap._tcp          SRV 0 1 143 imap.example.com.
_imaps._tcp         SRV 0 1 993 imap.example.com.
_pop3._tcp          SRV 0 1 110 pop3.example.com.
_pop3s._tcp         SRV 0 1 995 pop3.example.com.

我们在配置邮件时只要输入电子邮箱地址,客户端就会尝试查询对应的 SRV 记录并自动完成配置。如果某项服务不存在,可以将其对应的服务器域名改为 .。比如没有 POP3 服务:

_pop3._tcp          SRV 0 1 110 .
_pop3s._tcp         SRV 0 1 995 .

总结

以上就是本文的主要内容了。显然,邮件系统光 DNS 记录就足以让人眼花缭乱了。那为什么会这么复杂呢?这是因为邮件协议并非一开始就设计了这么多功能,而是伴随着互联网的发展一点一点长出了这么多功能。因为要确保兼容已有的老系统,不得不一遍一遍地打补丁。虽然如此,但电子邮件协议仍然是最成功的联邦化通信协议,它是去中心化互联网最后的堡垒。如果有一天它取代了,那互联网的去中心化梦想真就破灭了。另外,SMTP 协议作为一种匿名投递协议,构成了互联网最基本的信任模型,即首先假设对方是好人,然后再根据行为来做进一步判断。我认为这是开放式互联网发展的大前提。所以大家都搞起来呀,为去中心化的互联网尽一份力量。


  1. https://datatracker.ietf.org/doc/html/rfc7208↩︎

  2. https://www.mailhardener.com/tools/spf-validator↩︎

  3. https://datatracker.ietf.org/doc/html/rfc6376↩︎

  4. https://datatracker.ietf.org/doc/html/rfc7489↩︎

  5. https://datatracker.ietf.org/doc/html/rfc8461↩︎

  6. https://datatracker.ietf.org/doc/html/rfc7672↩︎

  7. https://www.rfc-editor.org/rfc/rfc8460.html↩︎