📘 第六章 HTTP首部
📄 一句话概括

Via 记的是「这个报文经过了哪些代理」。格式是「协议版本 代理名」,每过一跳追加一项。 它是排查链路的第一手证据,但也可以被伪造或删掉。

📝 本节和前面几节的关系

第五章讲代理时已经用过它一次。 这一节把它当成一个字段来细看:格式有哪几种写法,以及怎么拿它做告警。

一、每一项的格式

每一项是「协议版本 代理名」,中间一个空格。代理名可以带一个括号注释,写明软件名和版本。 | 写法 | 含义 | |---|---| | 1.1 outer-proxy | HTTP/1.1,代理名叫 outer-proxy | | 1.1 squid (squid/4.15) | 名字后面用括号补软件版本 | | 1.0 gw | HTTP/1.0 的老网关 | | 2 inner | HTTP/2(注意写的是 2,不是 2.0) | 最后一行是个小陷阱:HTTP/2 在 Via 里就写 2,不写 2.0。 因为 Via 的协议版本沿用「主版本.次版本」的写法,而 HTTP/2 没有次版本,所以只有一位。

二、顺序:最左是最近的

Via: 1.1 p1, 1.1 p2 表示请求先过 p1 再过 p2,还是先过 p2 再过 p1? 答案是先 p1 后 p2。每过一跳往右追加一项,所以最左边的是第一个经过的,最右边的是最后一个。 客户端看到这个头时,如果想找「离自己最近的代理」,看最左边那一项。 源服务器看到的顺序是整个链路从入口到出口的顺序。

三、拿它做劫持告警

思路很简单:维护一份「已知代理名单」,把响应里 Via 的每一项解析出来,名单之外的就报警。 Demo 里名单是 outer-proxy 和 inner-proxy,实际收到的两项都在名单里,告警为空。 如果中间多插了一个不认识的代理,就会出现在「名单外的」那一栏。 但这个办法有两个明确的局限。 一是可以伪造:攻击者可以自己写一个 Via: 1.1 outer-proxy 来伪装,也可以直接删掉 Via。 二是可以改写:有些设备不是追加而是覆盖,看到的结果就不可靠了。 所以 Via 只能当辅助证据。真正要确认链路,得靠出口侧的主动探测,或者对比不同位置的日志。

四、动手 Demo:两级代理的 Via 与名单比对

源服务器自己回一个 Via: 1.1 inner-proxy,外层代理追加 1.1 outer-proxy,客户端收到两项。 然后把每一项解析出来,跟一份已知名单比对。

# 源服务器
return reply(u'HTTP/1.1 200 OK',
             [(u'Via', u'1.1 inner-proxy')], b'ok')

# 外层代理:追加自己
nv = hd.get(u'via', u'')
keep = [(u'Via', (nv + u', ' if nv else u'') + u'1.1 outer-proxy')]

KNOWN = {u'outer-proxy', u'inner-proxy'}
got = [x.strip().split(u' ')[-1].split(u'(')[0].strip()
       for x in via.split(u',') if x.strip()]
unknown = [g for g in got if g not in KNOWN]

实跑输出:

--------------------------------------------------------------------
Via 的格式,以及怎么拿它做劫持告警
--------------------------------------------------------------------
  客户端收到的 Via           1.1 inner-proxy
  项数                       1
  顺序                       最左是最先经过的,最右是最后经过的

  每一项的格式是「协议版本 代理名」,代理名可以带注释:
    1.1 outer-proxy          协议 1.1 | 名字 outer-proxy
    1.1 outer-proxy (squid/4.15) 协议 1.1 | 名字 outer-proxy (squid/4.15)
    1.0 gw                   协议 1.0 | 名字 gw
    2 inner                  协议 2 | 名字 inner

  注意                       2 表示 HTTP/2,不是「版本 2.0」

  用它做劫持告警的思路:维护一份「已知代理名单」,
  响应里的 Via 出现名单外的项,就说明中间多了一个不认识的东西。
    已知代理                 inner-proxy、outer-proxy
    这次出现的               inner-proxy
    名单外的                 没有

  Via 是排查链路的第一手证据。它有两个局限要知道:
  一是可以被伪造或干脆删掉,所以「没有 Via」不等于「没走代理」;
  二是有些设备会改写而不是追加,看到的结果就不可靠了。
  真正可靠的做法是在出口侧做主动探测,而不是只信报文里的自述。

客户端收到的 Via 是 1.1 inner-proxy, 1.1 outer-proxy,两项。 顺序值得注意:inner-proxy 在左,outer-proxy 在右。可客户端先碰到的是 outer,为什么它在右边? 因为源服务器先写了自己那一项,代理再往右追加——追加是从右往左「长出去」, 但读的时候从左往右是按经过顺序。 名单比对结果:已知 inner-proxy、outer-proxy,这次出现的也是这两个,名单外的为空。 输出里还试了四种写法:1.1 outer-proxy、1.1 squid (squid/4.15)、1.0 gw、 2 inner——最后那个 2 是 HTTP/2。

30 秒实验

  1. 在代理里把 Via 改成直接覆盖而不是追加,看客户端还能不能看出有两跳。
  2. 手工在请求里加一个 Via: 1.1 fake,看源服务器会不会信。
  3. 想一想:如果攻击者能删掉 Via,你还怎么证明请求经过了代理?

常见误区

  1. 「Via 最右是最近的代理」——最左是最近的,按经过顺序从左往右。
  2. 「HTTP/2 在 Via 里写 2.0」——写 2。
  3. 「没有 Via 就说明没走代理」——Via 可以被删掉,也可能是设备没实现。
自测三问(点开对答案) 1. `Via` 每一项的格式是什么? —— 「协议版本 代理名」,代理名后面可以带括号注释,写明软件和版本。 2. 多项 `Via` 的顺序怎么读? —— 从左往右按请求经过的顺序,最左是第一个经过的代理。 3. 用 `Via` 做劫持告警有什么局限? —— 可以伪造、可以删掉、有些设备会改写而不是追加,所以只能当辅助证据。

通用首部还剩最后一个:一个用来写「这份东西有问题」的字段。

相关笔记