golang 正向代理对于 Host 的处理 (RFC 7230)

缘由

起因是我们为用户提供了一个 httproxy 正向代理服务,用户必须通过这个以访问互联网,不然只能访问集群内资源。

然后有用户反馈说这个 httpproxy 无法正常指定 Host。比如:

curl -H 'Host: github.com' httpbin.org/headers

正常情况

2022-11-30T14:12:08.png

服务器将收到包含 Host: github.com header 的报文,通常我们通过这种方式手动指定 ip 来访问网站。

用户遭遇

服务器看起来接收到了 Host: httpbin.org 的 header。

于是开始排查问题。

排查

我们的httpproxyproxyproto这个包来做转换的,然后套的 http.Server -> httputil.ReverseProxy

发现在 http.Server ServeHTTP 这边的 request 就已经没有Hostheader了,此时github.com这个自定义Host信息完全丢失,然后还去看了proxyproto实现,也没问题,后来用gop这个库把http.Request整个结构体+指针全部打印出来,发现buf里面是存在Host的,那么目光转向 net/http ……

由于Interface包裹的太好,这个基础库的使用量由极高,所以大大增加的逻辑链路查找。最后通过文件结构和关键字的方法找到了请求处理。过程略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// github.com/golang/go  tag:go1.19

// src/net/http/request.go:1030

func readRequest(b *bufio.Reader) (req *Request, err error) {

// ...

// RFC 7230, section 5.3: Must treat

// GET /index.html HTTP/1.1

// Host: www.google.com

// and

// GET http://www.google.com/index.html HTTP/1.1

// Host: doesntmatter

// the same. In the second case, any Host line is ignored.

req.Host = req.URL.Host

if req.Host == "" {

req.Host = req.Header.get("Host")

}

}

这里是本次问题的核心。在正向代理的情况下,URI一定为absolute-form的形式,也就是代码注释中的second case

req.Host 在此处被赋值为 req.URL.Hostheader中自定义的Host字段被忽略。

然后Header中的HostreadRequest完成后的src/net/http/request.go:1026被删除。所以在用户侧就直接丢失信息了。

修复

这下子问题修复方案很清晰,只需要调整一下逻辑顺序即可,优先使用Header中的Host

1
2
3
4
5
6
7
req.Host = req.Header.get("Host")

if req.Host == "" {

req.Host = req.URL.Host

}

我参考注释,翻阅了 RFC 7230 section 5.3,其中这么写到

Once an inbound connection is obtained, the client sends an HTTP request message (Section 3) with a request-target derived from the target URI. There are four distinct formats for the request-target, depending on both the method being requested and whether the request is to a proxy.

我原先以为这是对于RFC的这段描述有分歧,所以导致出现了这样的问题,后来等到我想要New Issue的时候,找到了这个 https://github.com/golang/go/issues/16265

发现RFC 7230 section 5.4中提到

When a proxy receives a request with an absolute-form of request-target, the proxy MUST ignore the received Host header field (if any) and instead replace it with the host information of the request-target. A proxy that forwards such a request MUST generate a new Host field-value based on the received request-target rather than forward the received Host field-value.

不是很能理解该处 RFC 的用意,其中明确提到在forward proxy正向代理职能下,必须要忽略Host字段,根据URI重新生成。

由于不符合RFC,官方 close 了这个 issue,所以如果需要处理这个场景,只能往 go 打 patch 了

奇技淫巧

上班时与 leader 进行了一番讨论,发现还有些方法

  • 根据gop打印出来的信息来看,原请求的 buf 依然存在于http.Request里面,在Director可以重新解析请求内容,把 Host 重新找回来,不过这个方法过于 hack 了,怎么想怎么奇怪

  • 重新定义一个比如 X-Host 的 header,但是需要改用户代码,体验比较糟糕

  • 利用一个不遵守 RFC 的前置代理服务器把Host放到X-Host,但是这比改 go 标准库更奇怪了

  • 可以利用 http 的 CONNECT 方法!

http 的CONNECT方法有点像利用 http 长连接的外壳进行一个 tcp 转发。

那么怎么做呢,curl 有个参数叫--connect-to,就是把请求包在 connect 内完成。

对于 python,我找了几个实现,都不行,看了 stackoverflow 的评论区说 urllib3 不支持这个操作。

但是最终还是找到了方法。

既然CONNECT可以用于建立 tcp 连接,那么我们就可以把这部分操作放在外部程序完成。httpproxy to socks 的解决方案,找到了pporxy

1
2
3
pip3 install pproxy

python3 -m pproxy -r http://proxy.dst:3128

然后使用localhost:8080作为proxy就可以了。


golang 正向代理对于 Host 的处理 (RFC 7230)
https://hunsh.net/archives/138/
发布于
2022年11月30日
许可协议