HTTP标头如何导致time.gov偏离UTC时间

Lobsters Hottest 新闻

摘要

本文详细介绍了一项技术调查,解释了HTTP标头如何导致time.gov网站出现时间偏差,并阐述了NTP与HTTP时间同步之间的差异。

<p><a href="https://lobste.rs/s/3i1ulr/how_http_header_caused_time_gov_skew_from">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/05/08 09:45

# 一个 HTTP 标头如何让 time.gov 偏离 UTC 来源:https://alexsci.com/blog/how-time-gov-works/ 构建于《硅片之上》(https://alexsci.com/blog/) --- 在美国,国家标准与技术研究院(NIST)(https://www.nist.gov/)维护着官方的美国时间基准。NIST 分发该基准,以支持从气象学到 GPS 卫星的各种应用。程序员最熟悉的可能是通过网络时间协议(NTP)分发时间,NIST 通过运行多个 NTP 服务器来支持这一功能。NIST 还运营着美观的 time.gov 网站(https://time.gov/),通过网页提供官方时间基准。如果你不信任电脑任务栏上的时钟,这是一个检查时间的简便方法。 在最近的一个项目中,我需要一个可信的时钟,time.gov 是一个方便的选择。为了验证提供的时间基准是否准确,我并排打开了两个浏览器窗口,但发现给出的时钟偏移估算值差异超出了我可容忍的范围。当我与另一个来源(一个 NTP 客户端)进行比较时,发现了更大的差异。 Time.gov 估算值为 +0.019s 和 +0.209s,而 ntpdig time.cloudflare.com 估算值为 -0.008098 ± 0.004975。(https://alexsci.com/blog/how-time-gov-works/three-way-compare.png)#### 并排浏览器窗口暗示 time.gov 存在问题 我放弃了 time.gov 作为备选,但估算值的不一致一直困扰着我。直觉告诉我,应该能达到更高的精度,所以我必须回头找出问题所在。 这篇博文解释了问题所在以及 NIST 如何修复它。 *为清晰起见:本文描述的问题仅影响 time.gov 网站上显示的时钟,不影响 NIST 的任何其他时间服务。* ## NTP 与半往返时间 在深入探讨 time.gov 的实现之前,我先简要概述一下 NTP 的工作原理。为简洁起见,我会简化一些细节;如果你想深入了解,其他地方有很好的解释。 在 NTP 协议中,服务器用当前时间戳进行响应。这个时间戳在生成时是准确的,但客户端不会立即看到它。响应到达客户端需要时间,这导致时间戳逐渐过时。NTP 客户端需要估算自时间戳生成以来经过了多少时间。它通过测量请求的往返时间(RTT)来做到这一点,RTT 包括网络延迟和服务器处理时间。使用这些指标调整服务器提供的时间戳,可以产生一个对当前时间的非常好的估算。 虽然请求和响应在网络传输中可能耗时不同,但合理的预测是网络延迟在双向相同。因此,NTP 响应所经历的网络延迟可以估算为往返时间的一半。 客户端在上午 10:09:49 请求时间。服务器在上午 10:10:00 响应。客户端在上午 10:09:51 收到响应,即请求开始后 2 秒。客户端得出结论:它比服务器的时间基准慢了 10 秒(https://alexsci.com/blog/how-time-gov-works/Simplified-NTP.drawio.png)#### NTP 协议的简化版本 关于 NTP 还有很多需要了解:它使用 UDP,响应包含多个时间戳,会使用多个请求,并执行统计分析。time.gov 的漏洞足够显著,我们可以忽略这些细节,使用这个更简单的模型。 ## 通过 HTTP 分发时间 time.gov 网站通过 HTTP 同步时间,使用 JavaScript 执行请求(https://web.archive.org/web/20260308233044/https://time.gov/scripts/zzz__5c7ff74f782289d35dd3f9a4744ff31c221b34e9.js)。核心功能是执行一个 HTTP 请求,并使用“new Date()”收集时间信息。 ``` d = new Date(); var xmlHttp = new XMLHttpRequest(); ... xmlHttp.send(null); ... var currentTimeObj = new Date(); ``` 像 NTP 一样,time.gov 发送多个时间戳,但这些时间戳之间的差值四舍五入为零毫秒,不影响计算。因此我们继续忽略这个细节。 计算出的偏移量显示给用户,网站显示通过用本地时间加上偏移量来更新。 JavaScript 看起来像是 NTP 的合理近似,并且有可能产生正确的时间。time.gov 遇到的问题是发生在网络层面的。 ## 网络上发生了什么? 打开 time.gov 会加载来自 time.gov 域的大量资源,包括 HTML、JavaScript 和 CSS。获取时间戳的网络请求也使用 time.gov 域。出于性能原因,网络浏览器通常会在单个连接上发送多个 HTTP 请求。然而,来自 time.gov 的 HTTP 响应都包含 `Connection: close` 标头,这告诉网络浏览器立即关闭每个连接。每次网络浏览器从 time.gov 请求资源时,都会通过一个新的网络连接进行,这需要 TCP 握手和 TLS 设置。 JavaScript 代码假设,像 NTP 一样,只发生一次网络往返。不幸的是,`Connection: close` 标头强制发生了三次往返。这个错误的假设是 time.gov 问题的根本原因。 图表显示三次网络往返。服务器在 2.5RTT 时生成时间戳,但预测猜为 1.5RTT(https://alexsci.com/blog/how-time-gov-works/simple-3rtt.drawio.png)#### 简化的网络时间线 如图所示,服务器在两次半往返后生成时间戳,而客户端的猜测接近一次半往返。这种错误计算导致了一个完整的往返时间误差,比他们根本不调整网络延迟还要糟糕!有趣的是,估算发生在网络请求的 HTTP 部分开始之前。我们知道服务器不可能在我们询问之前就告诉我们时间,所以这肯定不可能是正确的。 在实践中,情况可能更糟,因为每次往返可能耗费不同的时间。在下面的例子中,TCP 握手和 TLS 建立花费的时间比 HTTP 往返长得多。这导致 time.gov 估算出服务器时间戳是在 TLS 会话建立之前生成的。 一个浏览器调试窗口显示 TCP 握手花费 107ms,TLS 建立花费 92ms,等待 19ms。(https://alexsci.com/blog/how-time-gov-works/400ms-network-timing.png)#### 显示网络时间细节 网络往返通常足够快,以至于误差难以察觉。仔细检查网络行为会揭示问题。 ## 如何修复? 我认为有两种方法可以帮助修复这个问题。 第一种是将连接标头改为 `keep-alive`。这将允许浏览器保持连接打开更长时间,在*大多数*情况下将时间同步的 HTTP 请求减少到一次网络往返。 这是加拿大国家研究委员会(NRC)的网页时钟(https://nrc.canada.ca/en/web-clock/)所采用的方法。 一个网页时钟显示...(https://nrc.canada.ca/en/web-clock/)#### 加拿大政府的网页时钟 但这不是一个完美的解决方案。在初始同步期间,NRC 网页时钟应用能够使用现有连接。然而,这个网页时钟会定期重新同步。后续同步延迟足够长,以至于之前的连接已经关闭,这意味着必须创建新的网络连接。你可以通过观察时钟看到这种行为:一两分钟后,偏移量估算和估算的网络延迟会发生明显变化。因此,NIST 和 NRC 的网页时钟都受此漏洞影响。 如果我们能只测量网络请求的 HTTP 部分,额外的网络往返就不会成为问题。“new Date()”方法非常粗糙,测量的范围远超预期。另一种方法使用 PerformanceResourceTiming(https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming)来收集精确的网络时间信息。这个接口旨在提供网络请求每个部分的准确测量,使我们能够只测量应用层;排除 DNS 查找、连接握手、TLS 初始化等。我构建了一个基于 CDN 的实验性网页时钟(https://clock.alexsci.com/)来验证该过程,结果令人满意。 我向 NIST(3 月 17 日)和 NRC(一个月后)报告了这个问题,并给出了这些建议。 大约在 4 月 24 日,time.gov 更改了他们的 HTTP 标头,启用了 keep-alive。 开发工具响应标头的截图。红色标注了 'Connection: Keep-Alive' 和 'Keep-Alive: timeout=5, max=95'(https://alexsci.com/blog/how-time-gov-works/fixed-headers.png)#### 添加了连接 keep-alive 标头 我认为核心问题已经修复,因为“往返”测量不再测量多次往返。在我的测试中,time.gov 的估算现在更接近 NTP 的估算。 ## 剩余的精度问题 不幸的是,time.gov 的“new Date()”方法中仍然测量了太多东西。比较一下该网站估算网络请求持续时间的方式,与 Firefox 的网络调试选项卡和 PerformanceResourceTiming 的测量结果(下一节会有更多内容)。 截图显示 'timeDotGov.data.responseTime - timeDotGov.data.requestTime = 63' 而网络选项卡显示 49(https://alexsci.com/blog/how-time-gov-works/new-date-vs-network-tab.drawio.png)#### timeDotGov.data.responseTime - timeDotGov.data.requestTime 仍然太长 在这个例子中,原始测量值 63 毫秒仍然大于 Firefox 网络时间面板中显示的 49 毫秒以及从 PerformanceResourceTiming 计算出的值。 ## 进一步改进 这是一个客户端补丁,将 time.gov 改为使用 PerformanceResourceTiming 来计算时钟偏移。将此 JavaScript 粘贴到开发者工具控制台将基于更精确的测量重新计算往返时间。它还添加了误差范围,生成估算时应包含这些范围。注意:十分钟后页面会硬刷新,此补丁将被移除。 ``` var requestTiming = performance.getEntries().filter((x) => x.name.includes("cgi?disablecache"))[0]; const serverDelay = timeDotGov.data.RThalf * 2 - timeDotGov.data.responseTime + timeDotGov.data.requestTime; timeDotGov.data.RThalf = ((requestTiming.responseEnd - requestTiming.requestStart) - serverDelay) / 2; const localTimeResponseEnd = new Date().getTime() - (performance.now()-requestTiming.responseEnd); timeDotGov.data.realTimeDif = timeDotGov.data.serverTime - localTimeResponseEnd; // time.gov 在这里也忘了 RThalf var diff = ((timeDotGov.data.realTimeDif + timeDotGov.data.RThalf)/1000) * -1; var diffDisplay = diff.toFixed(3); if (diffDisplay > 0 ) { diffDisplay = "+" + diffDisplay; } // 添加误差范围 diffDisplay = diffDisplay + " ± " + (timeDotGov.data.RThalf/1000).toFixed(3); document.getElementById("realTimeDif").innerHTML = diffDisplay; ``` 效果如下: 截图显示 Firefox 打开了开发者工具。修正后的显示显示偏移量为 -0.011±0.008 s。终端窗口显示 ntpdig 输出为 +0.013108 +/- 0.005543。(https://alexsci.com/blog/how-time-gov-works/fixed-example.png)#### 修改后的 time.gov 与 ntpdig 高度一致 *注意:time.gov 和 ntpdig 对计算出的偏移量符号取反。* 这个例子表明修改后的 time.gov 可以提供更好的精度。话虽如此,请不要将我的补丁视为更好的“官方时间”,因为我不提供任何担保。NIST 应考虑调整 time.gov 测量网络时间的方式。 ## 结尾思考 这类难以诊断的问题潜伏在每个复杂的计算机系统中。你知道你的软件表现不如预期,但找不到问题所在。面对如此多的架构层,可能会感到不知从何处下手。作为一名 DevOps 顾问,我喜欢引导客户更深入地理解他们的软件。有时候,一个标头就是阻碍你的全部因素。 time.gov 的漏洞是一个很好的例子,说明事情可能变得多么具有挑战性。这个 Web 应用对网络行为做出的假设在生产环境中并不成立。本地测试不太可能发现这个问题,因为开发环境可能使用了不同的标头。似乎有可能 HTTP Connection 标头是在 time.gov 上线后的某个时间被修改的,也许是由网络团队更改负载均衡器或 Web 应用防火墙导致的。甚至有可能这个问题在 NIST 网络上没有复现,因为网络路径可能经过不同的中间盒,它们提供了不同的 HTTP 标头。 这是一场需要回归第一性原理的完美风暴。 --- 如果你想表示赞赏并支持我的写作,可以通过此链接进行。 所有陈述均为个人观点,不代表雇主的立场或意见。

相似文章

时间黑客:利用音频谐波欺骗原子钟

Lobsters Hottest

本文探讨了消费级原子钟如何通过音频谐波信号被欺骗的漏洞,并详细说明了由于电离层传播问题,在美国东海岸接收 NIST 的 WWVB 无线电信号所面临的技术挑战。

WiFi 时间

Hacker News Top

一篇关于一个已废弃项目的详细记录,该项目旨在构建一个“假的”GPS模块,通过WiFi使用NTP和ESP8266获取时间,目标达到亚毫秒精度,但最终因硬件限制未能实现。

Wi-Wi:1纳秒级无线时间同步

Jeff Geerling

Wi-Wi STAMP是日本NICT开发的无线时间同步协议和硬件,使用900 MHz频段实现纳秒级同步和毫米级精度,已在NAB 2026上展示。

我们如何在 hyper HTTP 库中发现了一个 bug

Lobsters Hottest

Cloudflare 工程师在调试其 Images 服务间歇性故障时,发现 hyper HTTP 库中存在一个竞态条件 bug,该故障导致大图像转换返回截断的响应。修复只需要四行代码。