惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

小众软件
小众软件
A
About on SuperTechFans
博客园 - Franky
Engineering at Meta
Engineering at Meta
Recent Announcements
Recent Announcements
云风的 BLOG
云风的 BLOG
B
Blog
Microsoft Security Blog
Microsoft Security Blog
L
LangChain Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
U
Unit 42
Martin Fowler
Martin Fowler
Y
Y Combinator Blog
Stack Overflow Blog
Stack Overflow Blog
博客园 - 叶小钗
Vercel News
Vercel News
Apple Machine Learning Research
Apple Machine Learning Research
The Cloudflare Blog
Last Week in AI
Last Week in AI
腾讯CDC
Microsoft Azure Blog
Microsoft Azure Blog
爱范儿
爱范儿
V
V2EX
G
Google Developers Blog

Hunsh's Blog

生成个人的 Github PR List 如何选择合适的加密手段 Nginx SNI Proxy 如何优雅导入 k8s.io/kubernetes Make Go mod and Git to use specify .netrc 多架构镜像的体积优化 使用 buildkit 进行多架构构建并提取产物 如何修改镜像 layer(以 sourcemap-less grafana 为例) acme.sh 自动化 Google CA 申请证书 微信小程序嵌入任意公众号文章 k8s && bazel 项目从 go1.20 升级 go1.21 go serve http and https on same port typecho 迁移到 hexo 实现 hexo 文章和资源在同一目录下 golang mitm 遇到的问题 流式数据专用的 mine-type 分享 MacOS 的一些系统fix脚本(dns、sleep) prometheus rate/increase/delta/sum等函数不符合预期或出现小数的原理 [油猴脚本] USTB Everywhere 为校外访问添加访问任意网站的能力
Golang 正向代理对于 Host 的处理 (RFC 7230)
2022-11-30 · via Hunsh's Blog

缘由

起因是我们为用户提供了一个 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就可以了。