如何阅读一篇RFC
Mark Nottingham | QUIC Working Group Co-Chair
9 Sep 2018
原文链接:https://www.mnot.net/blog/2018/07/31/read_rfc
好坏暂且不论,我们通过RFC详细规定许多互联网协议。这些文档被部分开发者视作圣经,他们钻研RFC试图找寻隐含的意义,但随后又将其当做无关紧要的废纸弃置一边,因为它们实在难以理解。RFC难以理解,这通常令人感到挫败,但更重要的是,这可能引发互操作性(Interoperability )和安全性上的问题。然而,借助一些“RFC是如何写成并出版的?”相关的知识,能让理解一篇RFC更容易些。
以下是我从HTTP和其他一些事情中的经验得出的看法。
从何处开始?
查找RFC文档的官方途径是RFC Editor网站。然而,如我们接下来将看到的,RFC Editor上缺失一些关键信息,因此绝大多数人使用tools.ietf.org。
由于数量太多(现在已接近9000!),想找到我们想要的(原文right)RFC很困难。显然我们可以用网络搜索引擎来查找,此外RFC Editor的搜索工具也非常便利。
除上述两个网站外,也可以使用rfc.fyi,它可以通过标题和关键词搜索RFC,并按标签检索。
众所周知纯文本RFC文档由于排版丑陋而难以阅读,但事情正在好转,RFC Editor正在封装一种新的RFC格式,该格式有更好的展示形式且支持自定义选项。与此同时,如果你想要更加可用的RFC文档,也可以使用将特定主题的RFC进行打包的第三方仓库,比如,greenbytes维护的与WebDAV相关的RFC列表、HTTP工作组维护的与HTTP相关的那些RFC。
这一篇是哪种RFC?
所有RFC的顶部都有类似下图的横幅:
第一行写着"Internet Engineering Task Force (IETF)",表示本文由IETF出品。尽管并非广为人知,但确实有一些途径不需要IETF达成共识就可以发表RFC,例如independent stream。
事实上,发表一篇文档可以通过多种"stream"(此处作者用打引号的stream,应该是承接上一段说的"independent stream"的"stream",其实就是”方式“)。只有带"IETF stream"(也即文档顶部横幅第一行写着IETF的)的文档,表示IETF全体已审阅该文档所述协议的详细规定并声明共识。
较早期的文档(RFC5705及更早)第一行写的是"Network Woking Group",所以需要再深究一些来判断该文档是否代表IETF共识。可以在RFC Editor网站中查看该文档"Status of this Memo"这一节作为深究之始。
第二行是"Request for Comments"数字。如果取而代之写的是"Internet-Draft",就代表这篇文档并非RFC,只是一篇草案,并且任何人都可以提交草案。仅仅是草案的话并不一定会被IETF采用。
“Catagory"那一行冒号后面可能是"Standards Track”, “Informational”,"Experimental"或"Best Current Practice"之一。这几者之间的区别有时是模糊的,但如果是IETF发布的,就代表该文档已经过合理审查。然而,要记住"Informational"和"Experimental"不是标准,即使文档是IETF达成共识后发布的。
最后,文档的作者列在横幅右侧。跟学术界不同,此处列出的并非完整的“谁对这篇文档做出了贡献”列表,贡献列表通常列在靠近末尾的“致谢”小节。在RFC中,横幅右侧所列就是字面意思的“谁写了这篇文档”。通常会看到"Ed"跟在人名后面,这表示他们作为文档编辑写成文档,因为通常情况下文本*(译注:原文text,区别于document)*是已经写好的(就像修订一篇RFC时待修订文档肯定已经是存在的)。
这篇是最新的吗?
RFC是一系列归档文件,即使只是一个字母的区别也不能修改。(见diff between RFC7158 and RFC7159,这是个极端例子,7158的年份错了)
因此,确保你正在阅读的是正确的文档,这一点很重要。每篇文档的头部横幅包含一些有帮助的信息:
"Obsoletes"列出本篇文档完全替代的RFC文档编号,即,你应该使用当前这篇,而非Obsoletes列出的。注意,当某个协议出现新版本时,旧版本并不一定已被完全取代,例如HTTP/2并未完全取代HTTP/1.1,因为旧版本仍然是合法且必需的。但RFC7230确实取代了RFC2616。(译注:此处原文还有半句" because it’s the reference for that protocol. "因it指代和reference意义没有理解清楚,且删去之后对全文理解基本无影响,所以未做翻译)
"Updates"列出了当前文档对哪些文档做出了实质性改动,换句话说,如果你正在阅读Updates列出的文档,你可能也得看一下当前文档。
不幸的是,ASCII文本版RFC(比如RFC Editor网站上的)并不会告诉你你现在正在看的RFC文档有没有被其他哪篇文档更新或取代。这就是为什么绝大多数人用tools.ietf.org中的RFC仓库,该仓库中的文档会把这类信息标在像这样的横幅中:
横幅中列出的每个编号都是一个超链接,因此可以很方便地找到最新版的文档。
即使是最新的RFC通常也存在问题,再工具横幅右侧,你可以看到“勘误表存在”(Errata Exist)的警告以及上方的勘误表链接。
勘误表是对文档的更正和说明,这些内容不值得发布一版新的RFC(因此用勘误表的形式做更正)。有时这些更正、说明对RFC的实现有实质影响(例如,如果某个规范中的一个bug导致重大误解),这时就值得重新发布RFC。
举个例子,这是RFC7230的勘误表。当阅读勘误时,要时刻注意它们的状态;许多“勘误”由于只是某人误解规范意义而被拒绝采用。
理解上下文
开发者们阅读一篇RFC的内容,实现他们看到的,但却与作者的意图恰好相反,这种情况比你想象的更常见。
这是因为,要把规范写得让读者在选择性阅读(某一部分)时不出现误解是极其困难的。(与任何传世经典都一样)
因此,除了阅读直接相关的内容外,至少还要阅读这些内容引用的内容,无论它们在同一篇文档还是另一篇文档,这一点很有必要。紧要关头下,如果你无法阅读整篇文档的话,阅读任何可能相关的小节也能带来很大帮助。
例如,HTTP消息头在定义中用CRLF分隔,但如果你跳到这里,你会看到“收件人可能会将单个LF识别为行结束符,并忽略前面所有的CR”。
还有一点同同样重要,需要记住,许多协议通过设立IANA登记处来管理它们的扩展点;这些不是规范,而是真理的来源*(译注:可能是说部分规范出自这些登记处?)*。例如,HTTP方法的规范列表在这个登记处,而不在任何HTTP规范中。
理解要求关键词
几乎所有RFC都会在开始处有这么一节:
这些RFC2119关键词帮助定义互操作性,但他们也时常令开发者困惑。类似下面这句话在规范中十分常见:
这个要求是针对"Foo message"这个协议消息类型的*(译注:原文protocol artefact,通过上下文理解为“协议消息类型”)。如果你要发送一个Foo message,规范中已明确说明它不能包含Bar header(Bar header是什么可能要看具体文档)*,如果你发送的Foo message中包含了Bar header,它就不是一条符合格式的Foo message。
然而,(在规范中)很少明确接收方应该采取的行动,如果你收到一条带Bar header的Foo message,你会怎么做?
一些开发者会拒绝这种消息,尽管规范中没有写明要这么做;另一些则会继续处理这种消息,但会剥除或忽略Bar header,尽管规范中明确指出所有标头都需要处理。
所有这些都会有意无意地导致互操作性问题。正确做法是遵从正常流程处理头部,除非有明确要求(进行相反的处理流程)。
这是因为通常来说,规范的编写是为了公开规定行为;换句话说,凡是没有明确禁止的行为都是允许的。因此,对规范的过度解读无形中会有坏处,因为你会引入新的行为,而其他人则不得不围绕这些行为展开工作。
理想情况下,规范应该从信息处理者的角度定义行为,像这样:
如果做不到这个程度,最好在规范的其他地方找找关于错误处理的一般性建议(例:HTTP的一致性和错误处理部分)。
另外,记住要求的目标;绝大多数规范有一系列高度成熟的术语,用于区分协议中不同的角色。
例如,HTTP有一种名为“代理”的中间人,同时充当客户端和服务器(并非User-Agent或origin server),代理需要注意针对所有角色*(译注:此处原文all of those roles应该就是指代客户端、服务器)*的要求。
类似的,HTTP的一些要求会基于特定情况,区分“生成一条消息”和仅仅“转发一条消息”二者间的区别。留意这类特定协议的特定术语能省去许多猜测。
"SHOULD"关键词
对,没错,"SHOULD"关键词值得单开一小节。尽管已在努力根除,但这个虚实不定的词仍使许多RFC令人困扰。RFC2119这样描述这个词:
实践中,作者经常使用"SHOULD"和"SHOULD NOT"表示”我们希望你这样做,但我们知道我们不可能一直要求你这样做“。
例如,在overview of HTTP methods,我们可以看到:
这些 "SHOULD"不是 “MUST”,因为服务器可能合理地决定采取另一种行动:当某个来自client的请求可以确定为是攻击者发送的,服务器可能会切断连接;或者要获取服务器上的某个资源需要经过HTTP授权,服务器在发送405(方法不允许)前或许会先发送401(未授权)向客户端要求HTTP授权。
SHOULD并不意味着服务器可以随意忽略一项要求,因为这种行为是对规范要求的不尊重。
有时我们会看到SHOULD的形式是这样的:
注意那个"unless"——它指出了SHOULD允许的“特定情形”(译注:在该特定情形下可以不按SHOULD所要求的来做)。可以说这里的SHOULD可以被替换为MUST,因为unless子句依然生效,但这种风格的规范很常见。(译注:此处为作者个人观点)
示例阅读
另一个非常常见的误区是略过规范中的例子,就去实现规范。
不幸的是,示例通常是作者最不关心的部分,因为它需要随协议的每一处变动而更新。
因此,它们通常是整篇规范中最不可靠的部分。是的,尽管作者无疑应该在发布前复核示例,但确实有错误被漏过。
此外,即使是完美的例子,也有可能不是为了说明你想找的那方面;它们往往为图简洁而被截断,或者展示在解码步骤之后。
尽管会花费更多时间,但最好还是阅读文档的实际内容。示例不是规范。
On ABNF(增强型巴科斯范式)
Augmented BNF常被用于定义协议字段。例如:
一旦你习惯了ABNF,它会提供一种易于理解的格式指明协议字段看起来应该是什么样子的。
然而,ABNF是“理想情况”——它指出一则消息理想情况下的格式,而你生成的各种消息应该去匹配这种格式。但它没有说明当你收到不符合格式的消息时应该如何处理。事实上,很多规范完全没有说明ABNF和处理要求*(译注:应该是前文要求关键词部分,“SHOULD”,"MUST"等等)*的关系。
如果你试图严格执行协议规范中的ABNF,大多数协议会出现严重失败,但有时严格执行很重要。在上面的例子中,分号前后不允许出现空白,但你可以打赌有人会留出空白,并且有的协议实现会接受这种做法。
因此,你需要阅读ABNF周边的其他内容,了解额外的要求或上下文,并意识到没有直接(明确)要求,你可能不得不调整解析器使之能处理比ABNF要求的格式更宽松的输入。
部分规范开始认可ABNF格式理想性的特点,并明确规定解析语法,纳入了错误处理。(译注:也就是在RFC中写明不符合ABNF规定格式时如何处理)。为确保互操作性,当规范中有明确规定时,应该照规定实现。
安全考虑
从RFC3552开始,RFC模板加入了“安全考虑”小节。
因此,很少有RFC在发布时没有专门的安全相关的小节;评审流程不允许一篇草案说“本协议没有安全考虑”。
所以,无论是实现还是部署协议,你都需要阅读并确保理解安全考虑部分;否则会存在各种安全隐患。
关注它的参考文献(若有)也是好主意。如果没有任何参考文献,尝试找一些文档中的关键词来了解正在讨论中的问题。
延伸阅读(Finding out more)
如果一篇RFC没有解答你的问题,或者你对文章所述存有疑惑,最好找到最相关的工作组然后提问发送至他们的联系邮箱。如果没有现有的工作组涵盖有关主题,请尝试合适的领域。
提出勘误通常不是第一件应该做的事——先和其他人讨论一下。
现在许多工作组用Github管理他们的规范,如果你对某个正在实行的规范抱有疑问,可以先到Gihub提个issue。如果它(规范)已经是RFC,最好的方法还是使用邮件列表,除非有其他相反的指示。
域](https://ietf.org/topics/areas/)。
提出勘误通常不是第一件应该做的事——先和其他人讨论一下。
现在许多工作组用Github管理他们的规范,如果你对某个正在实行的规范抱有疑问,可以先到Gihub提个issue。如果它(规范)已经是RFC,最好的方法还是使用邮件列表,除非有其他相反的指示。
我肯定关于“如何阅读RFC”还有更多可写内容,也会有一些人对我写的这些内容提出异议,但这是我对于如何阅读的看法。我希望它能给读者带来帮助。