我的反垃圾邮件生活
或
我早在1999年10月为LinuxPlanet网站写了本系列的第一个部分,但编辑决定不发表系列的其余部分。从那时起,许多人要求续篇。这篇文章是第二部分。
本系列的第一个部分讨论了我早期与垃圾邮件问题的经历,首先是Usenet,然后是我的电子邮件收件箱。我谈到了如何设置邮件过滤器程序以及如何让它解析收到的消息。我讨论了将标题分割成逻辑行以及为什么这样做是必要的。关于详细信息和Perl代码,您可以阅读原始文章。
我还谈到了我的过滤理念,即将发送大量垃圾邮件的域名列入黑名单,并拒绝来自这些域的邮件。
域名模式匹配
处理域的一种方法可能是从消息中的“收件人”地址中剥离主机名。然而,这是不可能的,因为主机名本身就是一个域名。《code>perl.com既是主机名又是域名;《code>www.perl.com既是主机名又是域名,chocolaty-goodness.www.perl.com
也是如此。在实践中,虽然使用一个简单的启发式方法很容易
- 将主机名拆分成组件。
- 如果最后一个组件是
com
、edu
、gov
、net
或org
,则域名是最后两个组件。 - 否则,域名是最后三个组件
理论上是这样的:如果最后一个组件不是像com
这样的通用顶级域名,那么它可能是一个两字母的国家代码。大多数国家在其域名的第二级模仿通用空间。例如,英国有ac.uk
、co.uk
和org.uk
,分别对应于edu
、com
和org
,因此当我收到来自[email protected]
的邮件时,我希望识别域名是ox.ac.uk
(牛津大学),而不是ac.uk
。
当然,这是一个启发式方法,也就是一种复杂的方式来说明它不起作用。许多顶级域名并没有按照我假设的方式在第三级分割。例如,to
域名根本没有任何组织,就像com
域名一样。如果我收到来自hot.spama.to
的邮件,我的程序只会将该域名列入黑名单,而不会意识到它属于同一个所有人拥有的更大的spama.to
域名。然而,到目前为止,这种情况从未发生。
我没有将mil
列入例外名单。然而,我从未收到过来自mil
的垃圾邮件。
最终,域名注册人员将引入一批新的通用顶级域名,如.firm
和.web
。但他们从1996年开始就为它做准备;他们还在为此而努力;我可能等到老死也等不到它发生。(更多信息请见http://www.gtld-mou.org/。)
尽管有这些问题,这种方法已经很好地工作了很长时间,因为几乎没有问题真正出现。这里有一个教训:世界上充满了过度设计的软件。有时候,你可能会浪费大量时间试图找到一个完美解决方案,而这个问题只需要部分解决。或者,像我朋友在麻省理工学院经常说的那样,“足够好,可以用于政府工作!”
以下是提取域名的代码
1 my ($user, $site) = $s =~ /(.*)@(.*)/;
2 next unless $site;
3 my @components = split(/\./, $site);
4 my $n_comp = ($components[-1] =~ /^edu|com|net|org|gov$/) ? 2 : 3;
5 my $domain = lc(join '.', @components[-$n_comp .. -1]);
6 $domain =~ s/^\.//; # Remove leading . if there is one.
发送者的地址位于 $s
中。我通过简单的模式匹配从地址中提取网站名称,这也是“足够好”原则的一个绝佳例子。每周都会有人在 comp.lang.perl.misc
新闻组中提问,寻求匹配电子邮件地址的模式。发送者收到了很多非常长且复杂的答案,或者被告知无法完成。然而,它就在那里。当然,它不起作用。当然,如果你收到了地址为 ``@“@plover.com”的邮件,它将会失败。当然,如果你收到了地址如 <@send.here.first:[email protected],> 的源路由邮件,它也不会起作用。
但是,你知道吗?这些事情从未发生过。一个生产邮件服务器必须处理这些繁琐的细节,但如果我的程序因为对邮件地址格式的过度简单假设而没有拒绝某些消息作为垃圾邮件,那么就没有伤害。
在第2行,如果没有网站名称,我们就直接跳到下一个地址,因为现在看来这根本不是地址。第3行将网站名称分解成组件;thelonious.new.ox.ac.uk
被分解为 thelonious
、new
、ox
、ac
和 uk
。
第4行是一个令人讨厌的经验法则:它查看最后一个组件,在这种情况下是 uk
,如果它是那五个魔法中的一个(edu
、com
、net
、org
或 gov
),则将 $n_comp
设置为2;否则为3。《$n_comp》将是域名中的组件数量,因此 saul.cis.upenn.edu
的域名为 upenn.edu
,而 thelonious.new.ox.ac.uk
的域名为 ox.ac.uk
。
为了获取最后一个组件,我们使用 -1
对组件数组 @components
进行下标访问。作为数组下标,-1
表示获取数组的最后一个元素。类似地,-2 表示获取倒数第二个元素。在这种情况下,最后一个组件是 uk
,它与模式不匹配,所以 $n_comp
是3。
在第5行,-$n_comp .. -1
实际上是 -3 .. -1
,这是一个列表 -3, -2, -1
。我们使用 Perl 的一种称为“列表切片”的功能来从 @components
数组中提取仅包含 -3、-2 和 -1 的元素。语法
@components[(some list)]
触发了这个功能。列表被认为是下标的列表,具有这些下标的 @components
的元素按顺序提取。这就是为什么你可以这样写
($year, $month, $day) = (localtime)[5,4,3];
从 localtime
中按顺序提取年份、月份和日的原因——这几乎是一个相同的功能。这里我们正在提取 -3(倒数第三个)、-2(倒数第二个)和 -1(最后一个)的元素,并将它们再次连接起来。如果 $n_comp
是2,我们只会得到 -2 和 -1 的元素。
最后,第6行处理了一个常见的经验法则不起作用的情况。如果我们从 alcatel.at
收到邮件,并尝试选择最后三个组件,我们将会得到一个未定义的组件——因为实际上只有两个——连接的结果将是 .alcatel.at
,前面有一个虚假的空组件。第6行检查域名的开头是否有额外的点,如果有,就将其删除。
现在你已经有了它,你打算用它做什么?
我已经提取了域名。显然的事情是,有一个包含每个不良域名的巨大散列,并且在这个散列中查找这个域名以查看它是否在那里。在散列中查找事物非常快。
然而,我并没有决定这样做。相反,我有一个包含每个不良域名的正则表达式的大文件,我将要匹配的域名与文件中的所有模式进行匹配。这要慢得多。慢得多。而不是瞬间查找域名,匹配模式需要0.24秒。
有些人可能会看到这一点,并抱怨它的运行时间比应有的时间长了千倍。也许这是真的。但模式更加灵活,多或少0.25秒又有什么关系呢?邮件过滤器在1月份处理了2211条消息。每条消息耗时0.24秒,模式匹配每月花费我不到9分钟。
关于缺点就这么多。优点是什么呢?我可以使用模式。这是一个很大的优点。
在我的模式文件中有一个模式,拒绝来自任何声称他们的域名全部为数字的人的邮件,例如12345.com。用哈希是不可能做到这一点的。我还有一个拒绝包含“casino”域名的邮件的模式。在我听说过这些地方之前,这个模式就解决了Planetrockcasino.com和Romancasino.com的垃圾邮件。记住,我只在域名上做模式匹配,所以如果有人从casino.ox.ac.uk
发邮件给我,它就会通过。
正则表达式实际上确实有一个潜在问题:模式在文件中,每个模式一行。假设我在文件中添加模式时,不小心留下了一行空行。然后收到了一些邮件。过滤器提取了发件人的域名,并开始逐行检查模式文件。0.24秒后,它到达了空白行。
当字符串与空模式匹配时会发生什么?它会匹配,这就是答案。每个字符串都匹配空模式。由于假设这些模式描述的是我不想要的邮件,所以这封信被拒绝。下一封信也被拒绝。所有信件都被拒绝。哎呀。
说应该检查空白模式,并在它们存在时跳过它们,很有诱惑力,但这不能保护我不受只有一个点而没有其他内容的行的侵害——这也会匹配任何字符串。
因此,我这样做
$MATCHLESS = "qjdhqhd1!&@^#^*&!@#";
if ($MATCHLESS =~ /$badsite_pat/i) {
&defer("The bad site pattern matched `$MATCHLESS',
so I assume it would match anything. Deferring...\n");
}
由于模式旨在识别不良域名,它们中没有一个应该匹配qjdhqhd1!&@^#^*&!@#
。如果一个模式确实匹配这个字符串,那么它可能还会匹配很多它不应该匹配的东西。在这种情况下,程序假设模式文件已损坏,并推迟了邮件的投递。这意味着它告诉qmail
它现在还没有准备好投递邮件,而qmail
应该稍后再次尝试。 qmail
将一直尝试,直到它成功投递或五天后放弃,届时它将给发件人退回邮件。在五天之前,我可能会注意到我没有收到邮件,查看过滤器日志文件,并修复模式文件。随着qmail
再次尝试投递,推迟的邮件最终会到达。
当你的邮件传输代理是qmail
时,推迟邮件很容易。以下是defer
子例程的全部内容
sub defer {
my $msg = shift;
carp $msg;
exit 111;
}
当qmail
看到过滤器程序返回的111退出状态时,它将其解释为请求推迟投递。(同样,100告诉qmail
发生了永久性故障,应立即将邮件退回给发件人。正常状态为0表示投递成功。)
如果我将com
作为模式安装到模式文件中,我仍然会遇到麻烦,因为它匹配了更多的域名,但MATCHLESS
测试没有捕捉到它。但与空白行问题不同,它从未出现过,所以我决定在它出现时处理它。
“收到:”行
除了过滤From:
、Reply-To:
和信封发件人地址外,我还检查转发主机列表中的恶意域名。From:
和Reply-To:
标头很容易伪造:发件人可以在这些字段中放入任何他们想放的内容,垃圾邮件发送者通常也是这样做。但是Received:
字段略有不同。当计算机A向计算机B发送消息时,接收计算机B会向消息添加一个Received:
标头,记录它是谁、何时收到消息以及来自何方。如果消息经过多台计算机,将会有多个接收行,最早的行在标头的底部,后续的行在上面添加。以下是一组典型的Received:
行。
1 Received: (qmail 7131 invoked by uid 119); 22 Feb 1999 22:01:59 -0000
2 Received: (qmail 7124 invoked by uid 119); 22 Feb 1999 22:01:58 -0000
3 Received: (qmail 7119 invoked from network); 22 Feb 1999 22:01:53 -0000
4 Received: from renoir.op.net ([email protected])
5 by plover.com with SMTP; 22 Feb 1999 22:01:53 -0000
6 Received: from pisarro.op.net ([email protected] [209.152.193.22])
by renoir.op.net (o1/$Revision:1.18 $) with ESMTP id RAA24909
for <[email protected]>; Mon, 22 Feb 1999 17:01:48 -0500 (EST)
7 Received: from linc.cis.upenn.edu (LINC.CIS.UPENN.EDU [158.130.12.3])
by pisarro.op.net
(o2/$Revision: 1.1 $) with ESMTP id RAA12310 for
<[email protected]>; Mon, 22 Feb 1999 17:01:45 -0500(EST)
8 Received: from saul.cis.upenn.edu (SAUL.CIS.UPENN.EDU [158.130.12.4])
9 by linc.cis.upenn.edu (8.8.5/8.8.5) with ESMTP id QAA15020
10 for <[email protected]>; Mon, 22 Feb 1999 16:56:20 -0500 (EST)
11 Received: from mail.cucs.org ([email protected] [207.25.43.252])
12 by saul.cis.upenn.edu (8.8.5/8.8.5) with ESMTP id QAA09203
13 for <[email protected]>; Mon, 22 Feb 1999 16:56:19 -0500 (EST)
14 Received: from localhost.cucs.org ([192.168.1.223])
15 by mail.cucs.org (8.8.5/8.8.5) with SMTP id QAA06332
16 for <[email protected]>; Mon, 22 Feb 1999 16:54:11 -0500
这是某人发送到[email protected]
的消息,这是我以前的一个地址。显然,发件人的邮件客户端在localhost.cucs.org
,最初将消息传递给组织的邮件服务器mail.cucs.org
。邮件服务器随后将14-16行添加到消息标头中。
邮件服务器然后将消息通过互联网发送到saul.cis.upenn.edu
。saul
添加了11-13行。注意,第13行的时间比第13行的时间晚128秒。这可能意味着消息在发送到saul
之前在mail.cucs.org
上停留了128秒,或者可能意味着两台计算机的时钟没有正确同步。
当邮件到达saul
时,那里的邮件发送器发现我有一个指向[email protected]
的.forward
文件。saul
需要将消息转发到[email protected]
。然而,宾夕法尼亚大学CIS系的大多数机器不自行处理互联网邮件。相反,它们将所有邮件转发到部门邮件中心linc
,该中心负责将所有邮件发送到组织外部。当邮件由saul
转发到它时,linc
添加了8-10行。
linc
在域名服务中查找了op.net
,发现机器pisarro.op.net
正在接收op.net
域的邮件。当pisarro
收到来自linc
的邮件时,它添加了第7行。
我不知道为什么pisarro
然后将消息发送到renoir
,但我们知道它确实这样做了,因为第6行这样说了。
当邮件从renoir
发送出来时,plover.com
上的qmail
添加了4-5行。然后,在将邮件发送到mjd
、然后是mjd-filter
(运行我的垃圾邮件过滤器),最后是mjd-filter-deliver
(这实际上是我的邮箱地址)的过程中,添加了最后三行1-3。
我们能从这些学到什么?Received:
行记录了消息在投递过程中经过的每一台计算机。而且与From:
和Reply-To:
行不同,它确实记录了消息曾经在哪里。
假设原始发件人,在localhost.cucs.org
,想要隐藏消息的来源。让我们称他为Bob。Bob无法阻止cucs.org
出现在消息标头中。为什么?因为第11行就是如此。第11行是由saul.cis.upenn.edu
添加的,而不是Bob,Bob无法控制upenn.edu
域中的计算机。
Bob可以通过添加虚假的Received:
行来试图混淆问题,但他无法阻止其他计算机添加正确的行。
现在,当垃圾邮件发送者发送垃圾邮件时,他们通常会伪造From:
和Reply-To:
行,这样人们就不知道他们是谁,无法来杀死他们。但他们无法伪造Received:
行,因为那是另一台计算机放入的。因此,当我们正在搜索要检查的恶意域名模式列表时,我们也应该查看Received:
行。
那个问题的困难在于,没有标准规定Received:
行应该是什么样的,或者里面应该包含什么内容,每个不同的邮件发送器都有自己的做法。你可以在上面的例子中看到这一点。我们需要一种方法来遍历Received:
行,并寻找可能属于域的内容。这正是Perl正则表达式设计的目的。
1 子函数 forwarders { 2 return @forwarders if @forwarders; 3 4 @forwarders = 5 grep { /[A-Za-z]/ } ($H{'Received'} =~ m/(?:[\w-]+\.)+[\w-]+/g); 6 7 @forwarders = grep { !/(\bplover\.com|\bcis\.upenn\.edu|\bpobox\.com|\bop\.net)$/i } @forwarders; 8 9 foreach $r (@forwarders) { 10 $r{lc $r} = 1; 11 } 12 13 @forwarders = keys %r; 14 15 return @forwarders; 16 }
消息头已经解析并放置在%H
哈希中。$H{Received}
包含整个消息中所有Received
行的拼接。forwarders()
函数的目的是检查$H{Received}
,提取它找到的所有域名,并将它们放入数组@forwarders
中。
第4-5行是这个过程的精髓。让我们更仔细地看看。
$H{'Received'} =~ m/(?:[\w-]+\.)+[\w-]+/g
这在对Received:
行进行模式匹配。[\w-]
查找单个字母、数字、下划线或连字符,而[\w-]+
查找此类字符的序列,例如saul
或apple-gunkies
。这是一个域名组成部分。[\w-]+\.
查找域名组成部分后面跟着一个点,如saul.
或apple-gunkies.
。
暂时忽略?:
。如果没有它,模式是([\w-]+\.)+[\w-]+
,这意味着域名组成部分后面跟着一个点,然后是另一个域名组成部分后面跟着另一个点,依此类推,并以域名组成部分结尾且没有点。所以这是一个匹配类似域的模式。
/g
修饰符在匹配中指示Perl找到所有匹配的子字符串,并返回一个列表。Perl会遍历Received:
头,提取所有看起来像域的内容,将它们制作成一个列表,并返回域名的列表。
这个特性的另一个例子
$s = "Timmy is 7 years old and he lives at 350 Beacon St.
Boston, MA 02134" @numbers = ($s =~ m/\d+/g);
现在@numbers
包含(7, 350, 02134)。
我还没有解释那个?:
。我必须承认我在说谎。Perl只有在模式中不包含括号时才返回匹配子字符串的列表。如果模式包含括号,括号会导致字符串的一部分被捕获到特殊变量$1
中,并且匹配返回一个$1
的列表而不是整个匹配子字符串的列表。如果我是
"saul.cis.upenn.edu plover.com" =~ m/([\w-]+\.)+[\w-]+/g
而不是这样,我就会得到列表("saul.cis.upenn.", "plover.")
,这些都是$1
,因为com
部分匹配最后的[\w-]+
,它不在括号内。真实模式中的?:
不过是一个告诉Perl不要使用$1
的开关。由于$1
没有被使用,我们得到默认行为,匹配返回所有匹配的内容。
4 @forwarders =
5 grep { /[A-Za-z]/ } ($H{'Received'} =~ m/(?:[\w-]+\.)+[\w-]+/g);
模式匹配生成一个可能属于域的项目的列表。列表最初看起来像
renoir.op.net 209.152.193.4
plover.com
pisarro.op.net pisarro.op.net 209.152.193.22 renoir.op.net 1.18 mail.op.net
linc.cis.upenn.edu LINC.CIS.UPENN.EDU 158.130.12.3 pisarro.op.net 1.1 op.net
saul.cis.upenn.edu SAUL.CIS.UPENN.EDU 158.130.12.4
linc.cis.upenn.edu 8.8.5 8.8.5
op.net
mail.cucs.org cucs-a252.cucs.org 207.25.43.252
saul.cis.upenn.edu 8.8.5 8.8.5
saul.cis.upenn.edu
localhost.cucs.org 192.168.1.223
mail.cucs.org 8.8.5 8.8.5
saul.cis.upenn.edu
如你所见,它包含了很多垃圾。最值得注意的是,它包含几个8.8.5
的实例,因为upenn.edu
的邮件发送器是Sendmail版本8.8.5。还有一些我们无法过滤的IP地址,以及一些看起来像版本号的其他内容。grep
过滤这个项目列表,只传递包含至少一个字母的项目,丢弃完全由数字组成的项。
@forwarders
现在是
renoir.op.net
plover.com
pisarro.op.net pisarro.op.net renoir.op.net mail.op.net
linc.cis.upenn.edu LINC.CIS.UPENN.EDU pisarro.op.net op.net
saul.cis.upenn.edu SAUL.CIS.UPENN.EDU
linc.cis.upenn.edu
op.net
mail.cucs.org cucs-a252.cucs.org
saul.cis.upenn.edu
saul.cis.upenn.edu
localhost.cucs.org
mail.cucs.org
saul.cis.upenn.edu
函数的其余部分只是进行一些清理。第7行丢弃了一些出现频率很高的、不值得查看的域名
``
7 @forwarders = grep { !/(\bplover\.com|\bcis\.upenn\.edu|\bpobox\.com|\bop\.net)$/i }
@forwarders;
Plover.com 是我的域名,它将出现在我所有的邮件中,所以检查它没有意义。我在宾夕法尼亚大学工作了四年半,那里转发了大量的邮件给我,所以检查 cis.upenn.edu
域名也没有意义。同样,我订阅了 Pobox.com 的终身电子邮件转发服务,通过它转发了大量的邮件。op.net
是我的 ISP 域名,当 Plover 不可用时,它会为我处理邮件。第 7 行从 @forwarders
列表中删除了所有这些域名,只留下以下内容
mail.cucs.org cucs-a252.cucs.org
localhost.cucs.org
mail.cucs.org
第 9-13 行现在使用常见的 Perl 习语删除重复项
9 foreach $r (@forwarders) {
10 $r{lc $r} = 1;
11 }
12
13 @forwarders = keys %r;
我们将剩余的项用作散列表的键。由于散列表不能有两个相同的键,所以重复的 mail.cucs.org
对散列表没有影响,最终得到的键有,mail.cucs.org
,cucs-a252.cucs.org
和 localhost.cucs.org
。与这些键关联的值都是“1”,这并不重要。当我们询问第 13 行的 Perl 键列表时,我们得到每个键正好一次。
最后,第 15 行将转发者列表返回给需要它的人。
还有一件小事情我没有讨论
2 return @forwarders if @forwarders;
该函数首先检查是否已经处理了 Received:
行和计算机 @forwarders
。如果是这样,它将返回列表而不再次计算。这样我就可以在我程序中任何需要转发者列表的地方调用 forwarders()
,而不用担心我会做重复的工作;forwarders()
函数保证在第一次调用后就立即返回。
更多内容即将到来
由于延迟时间较长,我将重复第一篇文章中的测验:这个报头行有什么问题?
Received: from login_2961.sayme2.net (mail.sayme2.net[103.12.210.92])
by sayme2.net (8.8.5/8.7.3) with SMTP id XAA02040
for [email protected]; Thu, 28 August 1997 15:51:23 -0700 (EDT)
故事还没有结束。在下一篇文章中,我将谈论我使用的其他一些用于过滤垃圾邮件的规则;其中之一会在看到像上面那样的行时丢弃消息。另一个规则会在消息中没有 To:
行时丢弃邮件——这是大量邮件的可能迹象。
我还会讲述一个警示故事,讲述我可能因为我的系统工作得太好而损失了大量金钱,以及我是如何发现有时,你 确实 希望收到未经请求的大量邮件的。
标签
反馈
这篇文章有什么问题吗?通过在 GitHub 上打开问题或拉取请求来帮助我们。