下载本文的PDF版本 PDF

稳健性原则再思考

寻求中间立场

Eric Allman,Sendmail


“对你所做的要保守,对你从别人那里接受的要开放。” (RFC 793)


1981年,Jon Postel制定了稳健性原则,也称为Postel定律,作为当时新生的TCP的基本实现指南。稳健性原则的目的是最大化网络服务实现之间的互操作性,尤其是在面对模糊或不完整的规范时。如果每个生成协议片段的服务实现都使用对规范最保守的解释,并且每个接受该协议片段的实现都使用最慷慨的解释来解释它,那么这两个服务能够相互通信的机会将最大化。Arpanet的经验表明,让独立开发的实现能够互操作是困难的,并且由于预计互联网将比Arpanet大得多,因此需要加强旧的临时方法。

尽管稳健性原则是专门为TCP的实现而描述的,但它很快被接受为实现通用网络协议的一个良好主张。有些人已将其应用于API的设计,甚至编程语言的设计。它简单、易于理解且直观明了。但它是正确的吗?

多年来,稳健性原则被认为是公理,更多的时候是在被忽视而不是实践时失败。然而,近年来,这一原则受到了挑战。这不是因为实施者变得更愚蠢了,而是因为世界变得更加充满敌意。稳健性原则影响两个一般问题领域:有序互操作性和安全性。


标准和互操作性

网络协议实现中的互操作性是一个难题™。这有很多原因,所有原因都归结为计算机是无情的这一基本真理。例如,规范可能是模棱两可的:两位工程师构建了符合规范的实现,但这些实现仍然无法相互通信。规范实际上可能是明确的,但措辞方式导致某些人误解。可以说,一些最重要的规范属于此类,因为它们是用一种对大多数工程师来说不自然的法律术语编写的。规范可能没有考虑到某些情况(例如,硬件故障),这可能导致在实际世界中使实现工作实际上需要违反规范。

类似地,规范可能对环境做出隐含的假设(例如,硬件支持的最大网络数据包大小或相关协议的工作方式),并且这些假设可能是不正确的,或者环境可能会发生变化。最后,也是非常普遍的情况,一些实施者可能会发现需要增强协议以添加规范未定义的新功能。

编写标准(即定义不同实现之间互操作性的任何规范)是一门艺术。标准本质上是合同,在法律意义上是如此,但法律具有悠久的定义、重新定义和定义细化的历史优势(或可能是劣势),通常是在判例法中。标准的目的是使互操作性成为可能。这既需要精确性(以避免歧义),也需要清晰性(以避免误解)。两者中任何一个的失败都会导致缺乏互操作性。不幸的是,正如刚刚指出的那样,这两个目标有时是相互冲突的。

我们正常的人类语言常常是模棱两可的;在现实生活中,我们毫不费力地处理这些歧义(或将它们用作笑话的基础),但在技术世界中,它们可能会引起问题。然而,极其精确的语言对我们来说非常不自然,以至于很难体会到其中的微妙之处。标准通常使用形式语法、数学方程式和有限状态机,以便简洁地传达精确的信息,这当然有所帮助,但这些通常不能独立存在——例如,语法描述语法但不描述语义,方程式必须转换为代码,而有限状态机对于人类来说是出了名的难以理解。

标准通常包括图表和示例以帮助理解,但这些实际上可能会产生问题。考虑一下图表与描述性文本不匹配的可能性。哪个是正确的?就此而言,任何时候在两个地方描述同一件事,都存在两种描述可能说出细微不同的事情的危险。例如,RFC 821和RFC 822都描述了电子邮件地址的语法,但不幸的是,它们在细微之处有所不同(这些标准后来已更新以修复此问题和其他问题)。常见的解决方案始终是“通过引用”包含必要的重复语言(即,包括对另一文档的引用,而不是实际描述)。当然,如果走极端,这可能会导致标准的鼠窝。例如,OSI关于消息处理(电子邮件)的建议(即标准)包含在大约20个不同的文件中,这些文件充满了交叉引用。

即使使用示例也可能引起争议。示例永远不是规范性的(标准的流行语,意为权威);也就是说,如果示例与正文之间存在冲突,则以正文为准。此外,示例很少是完整的。它们可能演示了协议的某些部分,但并非所有细节。从理论上讲,如果您从标准中删除所有示例,那么该标准的含义根本不会改变——唯一的raison d'�tre是帮助理解。问题在于,一些实施者阅读示例(通常比标准的实际文本更容易理解)并从中实现,因此错过了标准的重要细节。这导致一些标准作者完全避免使用示例。

一些(通常由供应商驱动的)标准使用“参考实现”方法——也就是说,将单个实现定义为正确的;当且仅当所有其他实现针对参考实现工作时,它们才是正确的。这种方法充满了危险。首先,没有哪个实现是完全没有错误的,因此在参考实现中查找和修复错误本质上会改变标准。

同样,标准通常具有各种“未定义”或“保留”元素——例如,同时指定了具有重叠语义的多个选项。其他实现将找到这些未定义元素的工作方式,然后依赖于这种意外的行为。当扩展参考实现以添加功能时,这会产生问题;这些未定义和保留元素通常用于提供新功能。此外,可能有两个独立的实现,它们各自针对参考实现工作,但不相互工作。尽管如此,参考实现方法可以与书面规范结合使用,尤其是在规范正在完善时,这可能是有用的。

最初的InterOp会议旨在允许具有NFS(网络文件系统)实现的供应商测试互操作性,并最终公开演示他们可以互操作。最初的11天仅限于少数工程师,以便他们可以在一个房间里聚在一起,真正使他们的东西协同工作。当他们走进房间时,供应商主要针对他们自己的系统,也可能针对Sun的系统(因为作为NFS的原始开发者,Sun当时拥有参考实现)。漫长的夜晚致力于解决规范中的歧义。在这11天结束时,大门向客户敞开,此时大多数(但不是全部)系统都针对其他每个系统工作。在那次会议结束时,NFS协议得到了更好的理解,许多错误得到了修复,并且标准得到了改进。这是实现驱动的标准不可避免的道路。

标准的另一种方法是让一群聪明人在一个房间里集思广益,讨论标准应该做什么,只有在标准编写完成后才应该实现代码。这最符合传统的软件工程,即在编写代码之前先编写规范。在极端情况下,这就是瀑布模型。以这种方式生成标准的问题与瀑布模型中出现的问题相同:规范(标准)有时会强制执行一些用处不大但难以或不可能实现的事情,并且返回修改规范的成本会随着时间呈指数增长。

也许最好的情况是标准和实现并行开发。当SMTP(简单邮件传输协议)正在开发时,我处于不寻常的地位,与标准本身同时开发Sendmail软件。当提出对标准草案的更新时,我能够立即实施它们,通常是通宵达旦,这使得标准和实现能够一起发展。标准中的歧义以及不必要的难以实现的善意功能很快被暴露出来。不幸的是,这种情况在今天很少见,至少部分原因是世界已经变得足够复杂,以至于标准的如此快速更新不再容易。


标准中的歧义和可扩展性

作为歧义的一个例子,请考虑以下来自(虚构的)标准的摘录


如果在数据包中指定了A选项,则字段X包含参数的值。


这假设协议具有固定大小的标头。A可能是一些标志字段中的一位,X是数据包中的某个字段。从表面上看,此描述似乎很清楚,但它没有指定如果指定A选项,则字段X的含义。更好的措辞可能是


如果在数据包中指定了A选项,则字段X包含参数的值;否则,字段X必须为零。


Figure 1 An example protocol diagram

您可能认为这种措辞应该是没有必要的——当然X应该为零,那么为什么要如此明确呢?但是,如果没有这个细节,它也可能意味着:“如果数据包中未指定A选项,则字段X将被忽略”——或者,也许,“字段X是未定义的。” 这两者都与“必须为零”的解释有很大不同。此外,这两种措辞之间的差异是微不足道的,但意义重大。在前一种情况下,“忽略”可能意味着“必须被忽略”(即,在任何情况下,如果未指定选项A,则不应使用字段X)。但后一种情况允许字段X可能被重用于其他目的的可能性。

这(最终)将我们带回了稳健性原则。给定“必须为零”规范,为了最稳健,任何实现都应确保在发送数据包之前将X字段归零(对自己发送的内容要保守),但在接收时不对X字段进行检查(对自己接受的内容要开放)。

现在假设我们的标准已修订(版本2)以添加也使用X字段的B选项(不能与选项A结合使用)。稳健性原则来拯救我们了:由于“稳健”的版本1实现不应检查X字段的值,除非已指定选项A,因此添加选项B不会有问题。当然,版本1接收器将无法提供选项B功能,但当他们收到版本2数据包时也不会崩溃。这是一件好事:它允许我们扩展协议而不会破坏旧的实现。

这也阐明了在传递数据包时该怎么做——实现不应清除字段X,即使那是“最保守”的做法,因为那会破坏在两个版本2实现之间转发数据包的版本1实现的情况。在这种情况下,稳健性原则必须包括一个推论:实现应默默地忽略并传递它们不理解的任何内容。换句话说,关于“保守”有两种定义,它们是直接冲突的。

现在让我们假设我们虚构的标准有另一个字段Y,旨在供将来使用——即,在协议扩展中。有很多方法可以描述此类字段,但常见的示例是将其标记为“保留”或“必须为零”。前者没有说明合规实现应使用什么值来初始化保留字段,而后者则说明了,但通常假设零是一个好的初始化程序。应用稳健性原则很容易看出,当使用字段Y的协议版本3发布时,不会有问题,因为所有旧的实现都将在该字段中发送零。


阴暗面

但是,如果有实现没有将字段Y设置为零会发生什么?该字段从未初始化,因此它保留了之前内存中碰巧存在的任何垃圾。在这种情况下,即使不正确(不够保守)的实现也可能很高兴地生存下来并与其他实现互操作,即使它在技术上不符合规范。(这样的实现也可能正在泄漏信息——一个安全问题。)或者,实现可能会征用Y字段用于其他用途(“毕竟,它没有被使用,所以我最好使用它”)。效果是相同的。

这里发生的情况是,由于所有其他实现的开放性,坏的实现得以幸存。这永远不会被检测到,直到协议的版本3出现——或者某些实现违反了稳健性原则的“接受”方面,并开始进行更仔细的检查。实际上,一些协议实现具有特殊的测试模块,这些模块“对自己接受的内容保守”,以便找出这些问题,但许多协议实现没有。

最终的侮辱可能发生在版本3正在开发时,我们发现太多的实现(或一个非常流行的实现)对自己生成的内容不够保守。免得您认为这一定是罕见的,有无数的案例表明,供应商找到了一个方便的“保留”字段并将其征用为己用。

我将此示例框定为好像它涉及低级网络数据包(类似于传输控制协议RFC 793中的图3),2但这可以很容易地应用于通用协议,例如XML。相同的困难(征用稍后使用的标签、删除无法识别的属性等)都适用于此处。(有关为什么稳健性原则不应应用于XML的充分论据,请参见Tim Bray的“再次谈Postel。”1


安全性

稳健性原则是在一个合作者的互联网中制定的。自那时以来,世界发生了很大变化。一切,即使是您可能认为自己控制的服务,也值得怀疑。不仅需要检查用户输入——攻击者还可能在DNS(域名系统)结果、数据库查询结果、HTTP回复代码中包含任意数据,不胜枚举。每个人都知道要检查缓冲区溢出,但检查传入数据远不止于此。

• 您可能会很想信任您自己的公司数据库,但请考虑数据最初是如何添加的。您是否信任可能更新该数据库的每件软件都进行严格的检查?如果不是,您应该进行自己的检查。

• 您是否通过防火墙的TCP连接获取数据?您是否考虑过连接劫持的可能性?安全关键数据应仅通过加密、签名的连接接受。其他数据应仔细检查。

• 您是否信任来自防火墙内部计算机的连接?您听说过病毒吗?即使您认为在您控制之下的机器也可能已被破坏。

• 您是否信任命令行标志和环境变量?如果有人设法在您的系统上获得帐户,则这些可能用于特权升级。

互联网的气氛已经发生了很大变化,以至于稳健性原则必须被严重地重新解释。对自己接受的内容开放可能会导致安全问题。有时,互操作性和安全性是相互矛盾的。在今天的气候下,两者都是必不可少的。必须取得某种平衡。


过度的开放

在稳健性原则的两方面都可能犯错。之前的“将X字段归零”示例是一个对自己生成的内容过于保守的案例,但大多数重新评估都来自“对自己接受的内容开放”方面。

问题出现在走多远。不验证您不必解释的“必须为零”字段实际上是否为零可能是合理的——也就是说,您可以将它们视为未定义的。作为一个真实世界的例子,SMTP规范说实现必须允许最多998个字符的行,但许多实现允许任意长度;接受更长的行可能是可以的(事实上,更长的行经常发生,因为很多软件将段落作为单行传输)。同样,尽管SMTP被定义为七位协议,但许多实现可以毫无问题地处理八位字符,并且世界上的大部分地区已经开始依赖这一点(现在有一个SMTP扩展来指定八位字符,从而使此行为正式化)。

另一方面,必须处理数字签名的实现可能应该非常严格地解释公钥证书。这些通常编码为BASE64,它使用65个字符进行编码(大小写字符、数字、“+”、“/”和“=”,所有这些都可以用七位US-ASCII表示)。如果证书中出现其他字符,则可能是安全攻击造成的,因此应该拒绝。在这种情况下,过于开放的实现可能只会忽略无法识别的字符。

事实上,软件可靠性和安全性的原则之一始终是检查您的输入。有些人将此解释为意味着用户输入,但在许多情况下,这意味着检查所有内容,包括来自本地“合作”服务甚至函数参数的结果。这个听起来合理的原则可以概括为“对自己接受的内容保守”。


通用性

我已经描述了这个问题,就好像我正在查看像TCP这样的协议,数据包中有固定位字段。事实上,它比这更常见。考虑一些真实世界的例子。

• 即使未包含MIME-Version标头字段,某些MIME实现也会解释MIME标头字段,例如Content-Type或Content-Transfer-Encoding。这些实现中的不一致是导致接收到损坏邮件的原因之一。

• 太多Web服务器接受来自用户和其他服务的任意数据,并在未首先检查数据的合理性的情况下对其进行处理。这是SQL注入攻击的原因。请注意,这也可能走向另一个极端:许多Web服务器不允许您在电子邮件地址中指定“+”,即使这是完全合法的(并且有用)。这是一个对自己接受的内容过于保守的明显例子。

• 较旧的微型计算机在指令包含本应是非法(或至少是未定义)的位模式时,通常会做一些有用的事情。硬件不会抛出故障(这很昂贵),而是会做对硬件设计人员来说方便的事情。过于狡猾的软件工程师有时会发现这些怪癖并使用它们,这导致程序在您升级硬件时无法工作。DEC PDP-8就是这样一台机器的例子。事实证明,可以要求机器在一个指令中左移和右移。在某些型号上,这具有清除高位和低位的效果,但在其他型号上,它会执行其他操作。

• Sendmail因对其接受的内容过于开放而受到批评。例如,Sendmail接受了From标头字段值中不包含域名的地址,通过添加本地域名来“修复”它们。这在大多数公司环境中都很好用,但在托管环境中却不行。这种开放性对邮件提交者的作者修复问题几乎没有压力。另一方面,有人说Sendmail应该接受伪造的地址,但不尝试纠正它(即,它太保守了,而不是太开放了)。

• 许多Web浏览器通常愿意接受不正确的HTML(特别是,许多浏览器接受缺少结束标记的页面)。这可能会导致呈现歧义(那个结束标记到底属于哪里?),但非常常见,以至于不正确的形式已成为事实上的标准——这使得构建任何重要的Web页面都成为一场噩梦。这被称为“规范腐烂”。

所有这些,在某种程度上,都是对自己接受的内容过于开放的例子。这反过来又允许实现永久存在,而这些实现对自己生成的内容不够保守。


现在怎么办?

那么,该怎么办呢?稳健性原则错了吗?从长远来看,对自己生成的内容保守,甚至对自己接受的内容更加保守,也许更稳健。也许有一个中间立场。或者,稳健性原则只是一堆糟糕选择中最不糟糕的选择。

嗯,不尽然。请记住,对自己接受的内容开放是允许协议扩展的一部分原因。如果每个实现都坚持字段Y实际上必须为零,那么推出我们协议的版本3几乎是不可能的。几乎每个成功的协议都需要在某个时候或另一个时候进行扩展,要么是因为原始设计没有考虑到某些事情,要么是因为世界在其下方发生了变化。变化是永恒的,世界是一个充满敌意的地方。标准——以及这些标准的实现——需要将变化和危险考虑在内。并且像其他一切一样,稳健性原则必须适度应用。


参考文献

1. Bray, T. 2004. 再次谈Postel; http://www.tbray.org/ongoing/When/200x/2004/01/11/PostelPilgrim.

2. 传输控制协议 RFC 793,图3. 1981; http://datatracker.ietf.org/doc/rfc793/.


致谢

感谢Kirk McKusick向我介绍了第一次InterOp的详细信息,并感谢George Neville-Neil、Stu Feldman和Terry Coatta的有用建议。


喜欢它,讨厌它?请告诉我们

[email protected]


Eric Allman 是Sendmail的联合创始人兼首席科学官,Sendmail是最早的基于开源的公司之一。Allman之前是加州大学伯克利分校Mammoth项目的首席程序员。这是他在伯克利的第二次任职,因为他是INGRES数据库管理项目的首席程序员。他还参与了伯克利的早期Unix工作,多年来编写了许多实用程序,这些实用程序出现在BSD的各种版本中,包括troff -me宏、tset、trek、syslog、vacation,当然还有sendmail。Allman在伯克利的两个任职期间在Britton Lee(后来的Sharebase)从事数据库用户和应用程序界面工作,并在国际计算机科学研究所为用于基于神经网络的语音识别的Ring Array Processor项目做出了贡献。他还与他人合著了Unix Review的“C Advisor”专栏多年。他是Usenix协会的董事会成员,也是Queue编辑顾问委员会的创始成员。


© 2011 1542-7730/11/0600 $10.00

acmqueue

最初发表于Queue vol. 9, no. 6
数字图书馆中评论本文





更多相关文章

David Collier-Brown - 你根本不了解带宽
当您的员工或客户说他们的互联网性能很差时,带宽可能不是问题。一旦他们拥有大约50到100 Mbps的带宽,问题就是延迟,即ISP的路由器处理他们的流量需要多长时间。如果您是ISP并且您的所有客户都讨厌您,请振作起来。这现在是一个可以解决的问题,这要归功于一群敬业的人,他们找到了它,消灭了它,然后在家庭路由器中验证了他们的解决方案。


Geoffrey H. Cooper - 使用FDO和不受信任的安装程序模型的设备入职
设备的自动入职是处理正在安装的越来越多的“边缘”和物联网设备的重要技术。设备的入职与大多数设备管理功能不同,因为设备的信任从工厂和供应链转移到目标应用程序。为了通过自动入职加快流程,供应链中的信任关系必须在设备中正式化,以允许自动化过渡。


Brian Eaton, Jeff Stewart, Jon Tedesco, N. Cihan Tas - 通过关键路径追踪进行分布式延迟分析
低延迟是许多Google应用程序(如搜索)的重要功能,延迟分析工具在维持大规模低延迟方面发挥着关键作用。对于包括功能和数据不断演变的服务的复杂分布式系统,将总体延迟保持在最低水平是一项具有挑战性的任务。在大型、真实世界的分布式系统中,现有的工具(如RPC遥测、CPU分析和分布式跟踪)对于理解整个系统的子组件很有价值,但在实践中不足以执行端到端延迟分析。


David Crawshaw - 一切VPN再次焕然一新
VPN(虚拟专用网络)已有24年的历史。这个概念是为与我们今天所知的互联网截然不同的互联网而创建的。随着互联网的增长和变化,VPN用户和应用程序也在增长和变化。在2000年代的互联网中,VPN经历了尴尬的青春期,与其他广泛流行的抽象概念交互不良。在过去的十年中,互联网再次发生了变化,这个新的互联网为VPN提供了新的用途。一种全新的协议WireGuard的开发为构建这些新的VPN提供了技术基础。





© 保留所有权利。

© . All rights reserved.