Golang 正向代理对于 Host 的处理 (RFC 7230)
缘由
起因是我们为用户提供了一个 httproxy
正向代理服务,用户必须通过这个以访问互联网,不然只能访问集群内资源。
然后有用户反馈说这个 httpproxy
无法正常指定 Host
。比如:
curl -H 'Host: github.com' httpbin.org/headers
正常情况
服务器将收到包含 Host: github.com
header 的报文,通常我们通过这种方式手动指定 ip 来访问网站。
用户遭遇
服务器看起来接收到了 Host: httpbin.org
的 header。
于是开始排查问题。
排查
我们的httpproxy
是proxyproto
这个包来做转换的,然后套的 http.Server
-> httputil.ReverseProxy
发现在 http.Server
ServeHTTP
这边的 request
就已经没有Host
的header
了,此时github.com
这个自定义Host
信息完全丢失,然后还去看了proxyproto
实现,也没问题,后来用gop
这个库把http.Request
整个结构体+指针全部打印出来,发现buf
里面是存在Host
的,那么目光转向 net/http
……
由于Interface
包裹的太好,这个基础库的使用量由极高,所以大大增加的逻辑链路查找。最后通过文件结构和关键字的方法找到了请求处理。过程略
1 |
|
这里是本次问题的核心。在正向代理的情况下,URI
一定为absolute-form
的形式,也就是代码注释中的second case
。
req.Host
在此处被赋值为 req.URL.Host
。header
中自定义的Host
字段被忽略。
然后Header
中的Host
在readRequest
完成后的src/net/http/request.go:1026
被删除。所以在用户侧就直接丢失信息了。
修复
这下子问题修复方案很清晰,只需要调整一下逻辑顺序即可,优先使用Header
中的Host
1 |
|
我参考注释,翻阅了 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 |
|
然后使用localhost:8080
作为proxy
就可以了。