当前位置

电子邮件

电子邮件相关

如何用 nginx 做 postfix 的 SMTP 反向代理,以及 XCLIENT 的支持

作为 lighttpd 的竞争者,轻量级web服务器 nginx 最近才开始崭露头角,知道它还可以用来做 pop3/imap4 反向代理的估计就比较少了,至于用 nginx 做 smtp 的反向代理,估计全中国现在和我一样想到这个需求的人一只手就能数过来。

需要 smtp 反向代理是因为我们的 vip 邮箱是可以免费试用的,希望在策略上对已交费用户和免费试用用户(其中有相当部分是 spammer)做出不同处理。前面用 nginx 把不同的用户请求代理到后台不同的 postfix 上,然后 postfix 再各自配置不同的 Milter Server 做过滤。另外需要前台能支持 XCLIENT,这样 postfix/milter 可以得到客户端的 IP,对于 anti-spam 来说是很有意义的。

nginx 在大约一年前增加了对 XCLIENT 的支持,对于 webmail 服务来说,nginx 可以说是再完美不过的反向代理前台了。

nginx 配置 smtp 反向代理需要在配置文件里加这么一段:

mail {
    auth_http http://127.0.0.1/auth;
    server {
        listen 26;
        protocol smtp;
        proxy on;
        smtp_auth login plain;
    }
}

用 python 写一个简单的 auth 服务来做测试:

  1. import SimpleHTTPServer
  2.  
  3. class handler(SimpleHTTPServer.SimpleHTTPRequestHandler):
  4.     def do_GET(self):
  5.         if (self.path == '/auth'):
  6.             # verify 'Auth-User', 'Auth-Pass', 'Client-IP'
  7.             if self.headers.get('Auth-Protocol') == 'smtp':
  8.                 self.send_response(200)
  9.                 self.send_header("Auth-Status", "OK");
  10.                 self.send_header("Auth-Server", "127.0.0.1");
  11.                 self.send_header("Auth-Port", "25");
  12.                 self.end_headers()
  13.                 return
  14.  
  15. addr = ('', 80)
  16. httpd = SimpleHTTPServer.BaseHTTPServer.HTTPServer(addr, handler)
  17. httpd.serve_forever()

修改 postfix 的 main.cf 配置,允许 nginx 代理服务器发送 XCLIENT 命令。

smtpd_authorized_xclient_hosts = 127.0.0.0/8

理论来说,到这里就应该就都配置好了。但是 nginx 会带上一个 LOGIN=foobar 的属性发给后台,而 postfix 是不支持该属性的,这将导致 postfix 报告一个 Bad XCLIENT attribute name: LOGIN 的 501 错误。很疑惑当初贡献这段代码的人是用什么 smtp server 做后台的,总之和 postfix 配合的话,必须修改 nginx 的程序(比修改 postfix 要简单些): 找到 src/mail/ngx_mail_proxy_module.c 里 "case ngx_smtp_helo:" 的那一部分,把和 "LOGIN" 相关的代码去掉就好了

附:最后决定利用周末时间写一个 patch,希望会被 nginx 接纳.
nginx-0.5.35-xclient.patch

Milter 协议(2)

先贴一段 twisted 框架下实现 Milter 协议解析的核心函数 :)

  1. class MilterServer(Protocol):
  2.     def init_dataReceived(self):
  3.         self.lastdata = ""
  4.         self.len = 0
  5.  
  6.     def __init__(self):
  7.         self.init_dataReceived()
  8.  
  9.     # 注意:实践中发现一次 recv 中包括了两个完整的 packet; 而且理论上存在
  10.     # 一个 packet 需要两次 recv 才能取出来的可能
  11.     def dataReceived(self, data):
  12.         if len(data): print len(data)
  13.         if self.lastdata != "":
  14.             data = self.lastdata + data
  15.         if self.len != 0:
  16.             if self.len <= len(data):
  17.                 next = data[self.len:]
  18.                 self.proc(data[:self.len])
  19.                 self.init_dataReceived()
  20.                 if len(next) > 0:
  21.                     self.dataReceived(next)
  22.             else: #继续等待输入
  23.                 self.lastdata = data
  24.         else:
  25.             if len(data) < 4:
  26.                 self.lastdata = data
  27.             else:
  28.                 self.len = struct.unpack(">i", data[:4])[0]
  29.                 self.lastdata = data[4:]
  30.                 print self.len, len(self.lastdata)
  31.                 self.dataReceived("")
  32.  
  33.     def proc(self, data):
  34.         ...
  35.         ...

上述代码在线上曾短时间的跑了跑,协议解析的逻辑部分应该是没有问题滴(稳妥起见,目前我们实际用的还是 libmilter 的 python binding)

实现 Milter Server 需要注意的一个事情就是:和 SMTP 一样,是要能支持在一次
connection 中完成多次 transaction 的!!还有 abort 命令,实际上就是一次 RSET 请求。

由于相当然的以为 eom (EndOfMessage) 事件后会话就应该结束了,结果碰到了问题怎么也想不出头绪,绕了一个多星期的弯路才找到程序的毛病所在。

另外在 Postfix 实现里面,自定义的 replymsg 的格式要求严格遵循 RFC,格式是:
"%s %s %s" % (code, Enhanced-Status-Code, msg)
E-S-C 的第一个数字要和 code 的第一个数字保持一致,见 postfix 源代码 milter8.c 里 SMFIR_REPLYCODE 的处理

仔细阅读了一下 sendmail 的 Milter Technical Overview,原来在 DATA 阶段,MTA 是需要把信件整个接受下来以后,再依次发给各个 Milter 的,而且是给一个 Milter 完整的传送完一个 message 后,再接着向下一个 Milter 发送。以前一直理解有误,觉得应该是 on-the-fly 的把数据依次传递给 Milter,这样效率最高.

Milter 协议

milter 是个好东东,但不知道出于什么样阴暗的心理,sendmail 的家伙们居然一直没有把这么重要的协议整理出正式的文档。开发者要么去使用 libmilter 函数库,要么老老实实去啃代码吧。

libmilter 应该说也实现的不错,而且还有 python binding,但它自己实现了一个事件循环,让现在酷爱 Twisted 的俺稍稍有些不爽。

感谢为 Milter 写 Perl 模块的程序员,也随代码同时提供了一封非官方的 milter protocol 说明。在这份文档的帮助下,很快就在 Twisted 框架下实现了 Milter Server 的骨架,大概 200 行代码吧。

Milter 协议最核心的就是:所有的 packet 都是首先 4 个字节表示接下来数据的长度,然后分析数据即可。另外就是在实践中发现一次 recv 中出现了两个甚至更多的 packet,协议分析需要注意。

另: twisted 2.5 的 epoll 似乎 memory leak 的厉害,本来想建议用 gc module 来试着解决的,结果换回缺省的 selectreactor 就好了

--Update--
milter protocol 说明文档目前搜索到的地址在:http://cpansearch.perl.org/src/AVAR/Sendmail-PMilter-1.00/doc/milter-pro...

Postfix 之 Milter

在整三年前,我负责亿邮反垃圾邮件网关的开发,在辛苦 Patch MTA 的同时,感到处理 SMTP 业务的代码和实现过滤的部分耦合的太紧密,而同时别的小组也在根据客户的需求时不时的修改 MTA 程序,这样长期下去势必会造成代码的不统一和高昂的维护成本。当时就开始设想一种 MTA 和 AntiSpam Engine 之间的通信机制,这样我们两个团队可以并行的高效开发,可惜在此方面一直没有琢磨太清楚,即使知道了 Milter 这个名次也没能把两个东西想到一起去,很快到了 04 年 9 月份我就离开了这个项目,也没有机会继续深入,实在是很遗憾。

Milter 就是一个 MTA 和 Filter 通信的协议。MTA 在会话的各个阶段不断把从客户端来的信息(IP、并发连接数)和数据传给 Filter,然后再根据 Filter 返回的结果返回给客户端,甚至修改信件内容!

Sendmail 不但定义了 Milter 协议,而且提供了 libmilter,它封装了一个多线程 Milter Daemon 的常规流程,帮助开发人员把更多精力放在实现过滤上。

使用 Milter 还有一个好处,就是能实现 before-queue 的过滤。这意味着我们能在 SMTP 会话处理阶段就把垃圾邮件拒绝,而不是接受下来后再弹回。

Postfix 2.4 对 Milter 已经支持的很完善,而且它还可以配置多个 Milter 的过滤,我们可以把网上那些开源的 Milter 和我们自己的 Milter 混合在一起实现更全面的防护。

这里透露一个信息:如果反垃圾邮件厂商希望在搜狐这里做测试,那提供一个 Milter 而不是 Gateway 我可是会给加分的,:)
因为要实现自定义白名单功能的话,Gateway 显然有潜在的问题。

另一方面我们也正在计划开发 SMTP-Milter,就是把防垃圾邮件网关封装成一个标准的 Milter Daemon(很奇怪,我满世界找了一圈,没有找到有这样的开源项目)。但这样也给 Gateway 提出了要求——首先是要能做到 before-queue 的过滤,另外就是最好能实现 Postfix 的 XCLIENT 扩展,如果你有自己的一套 IP 黑名单或者 HELO/EHLO 过滤机制的话。

加上 Postfix 之 tcp_tablePostfix 之 Dovecot Authencication Protocol,我们针对 Postfix 的开发就只包括这三方面,这是不是简单的有些难以置信?

Postfix 之 Dovecot Authencication Protocol

接前文 Postfix 之 tcp_table

和用户账户查询类似的一个应用就是用户身份认证。当然如果只是为了部署 MX 的话可以不考虑此点,但很多情况下我们还是需要给 SMTP 客户端用户一个连接上来认证后发信的方案

Postfix 没有内置任何形式的认证支持,必须通过第三方 SASL 服务才能达到这个目的。Postfix 2.4 支持两种认证服务,一类是链接 Cyrus-SASL 库来使用,另一类则是透过 Dovecot Authentication Protocol,连接一个 UNIX Socket 服务去认证。

虽然 Cyrus 在邮件领域更加有名,但我认为采取 Dovecot Authentication Protocol 去连接一个 Daemon 是一种更清晰的方案,编译部署也免去很多麻烦。

和 tcp_table server 一样,这个 Daemon 我们也是用 Python 完成的,为了方便描述,以后在团队内还是称之为 DAP server 吧。

Postfix 之 tcp_table

目前运行的 MTA 是在 Postfix 的一个很古老的版本上修改而来,再加上长期以来断断续续打了不少补丁,原始代码和修改代码纠缠太深,以至于很难把这些修改合并到最新的 Postfix 版本上;这次 MTA 改造的目标就是不修改 Postfix 任何代码,完全通过 Postfix 所支持的接口或协议实现需要的特性,这样以后可以跟着 Postfix 进行同步升级。

首先要解决的就是收件人地址查询的机制。

Postfix 支持一大票查询表(lookup table)格式,包括什么 unix、nis、bdb、cdb、ldap、mysql、pqsql 等等,但是对搜狐来说,能用的只有 tcp_table

简单地说,tcp_table 只是一个面向网络连接的协议描述,可以用它来封装任何形式、任何目标的用户查询。只可惜 Postfix 一直没有对它提供正式的支持,需要打开 SNAPSHOT 定义才能获得这个功能。

为了提高性能,可以在实现 tcp_table 的 daemon 里面加入用户名查询的缓存,以及连接用户名数据库的连接池。还要说的是,这个 daemon 很适合用 Python 来实现,:)

订阅 RSS - 电子邮件