写点什么

自古以来,代理程序都是兵家折戟之地

  • 2022 年 10 月 07 日
    四川
  • 本文字数:1791 字

    阅读完需:约 6 分钟

正向代理的血案

前几天打算使用 golang 做一个代理程序,golang 标准库net/http/httputil已经提供了这样的能力。



一把梭之后发现必然返回403 Forbidden, 我直接在target里面填上游服务实例 ip 就可以正确返回。


给一个向代理百度官网的简化示例,大家可以体会一下:


package main
import ( "fmt" "log" "net/http" "net/http/httputil")
func ReverseProxyHandler(w http.ResponseWriter, r *http.Request) { fmt.Println("receive a request from:", r.RemoteAddr, r.Header)
target := "www.baidu.com" director := func(req *http.Request) { req.URL.Scheme = "https" req.URL.Host = target // req.Host = target } proxy := &httputil.ReverseProxy{Director: director} proxy.ServeHTTP(w, r)}
func main() { fmt.Printf("Starting server at port 8080\n") if err := http.ListenAndServe(":8080", http.HandlerFunc(ReverseProxyHandler)); err != nil { log.Fatal(err) }}
复制代码


郁闷了很久,wireshark 抓包也看不出端倪(其实是知识有漏洞,那肯定找不到原因)。

头脑风暴

调试 httputil 的源代码:


  • 在代理后 url 中的 host 已经变成指定域名,但 header 中的 host 值没有发生变化还是 localhost:8000;

  • 此时我并没有发现问题,因为我笃定 url 中的 host 应该决定了请求的具体地址,抱着死马当活马医的态度,我重写了 header 中的 host 为目标百度域名


req.Host = target // 上面被注释


竟然真的成功了


小板凳好好摆一摆

知识漏洞的关键点在于 :


  • url 中已经有 host 了,为什么 header 中还要有 host?

  • url 中的 host 与 request.header 中的 host 到底什么关系?


rfc规范(这是个宝藏站点)


  1. Host请求头是在 http1.1 作为必选被引入,如果请求头没有 Host 或有多个 Host 请求头, 将会返回 400 错误。

  2. 请求中的“Host”提供了目标 URI 的主机和端口信息。


最关键的第三点:


  1. 设计Host请求头的动机: 在请求(为多个网站服务的)共享主机时,使初始服务器能够区分目标资源。


The "Host" header field in a request provides the host and port information from the target URI, enabling the origin server to distinguish among resources while servicing requests for multiple host names


什么意思呢?


在微服务架构下,请求在打到业务应用之前都会流经负载均衡器,例如 nginx/网关,这些负载均衡器提供了单负载节点配置多个域名的能力。但是请求打到负载主机,需要有信息能区分目标服务域名,这就依赖请求头中的 Host。



上图来自 阿里云应用型负载均衡


我们来看在nginx配置基于名字的多虚拟主机的写法:



 server_name  name ...      // 用来设置虚拟主机名,其实也就是域名匹配; 匹配成功,走下面的location逻辑。
复制代码


在这个配置中,nginx 仅仅检查请求的 Host 头以决定该请求应由哪个虚拟主机来处理。


如果 Host 头没有匹配任意一个虚拟主机,或者请求中根本没有包含 Host 头,那 nginx 会将请求分发到定义在此端口上的默认虚拟主机。


在以上配置中,第一个被列出的虚拟主机即 nginx 的默认虚拟主机——这是 nginx 的默认行为。而且,可以显式地设置某个主机为默认虚拟主机,即在"listen"指令中设置"default_server"参数:


server {    listen      80 default_server;    server_name example.net www.example.net;    ...}
复制代码


回到最开始的问题,我们写的反向代理程序其实是客户端,虽然重写了 url Host, 但是请求打到虚拟主机的时候,请求头中的 Host 还是最开始的 localhost:8080, 这个 Host 根本无法在虚拟主机中被识别, 所以我们还需要重写请求头中的 Host 为目标域名。


进一步, 难道 golang 的 httputil 标准库没有考虑到这一点,我又看了一次 ReverseProxy 源码,其实这个错误姿势在源码注释中已经提醒了。


NewSingleHostReverseProxy returns a new ReverseProxy that routes URLs to the scheme, host, and base path provided in target. If the target's path is "/base" and the incoming request was for "/dir",the target request will be for /base/dir. NewSingleHostReverseProxy does not rewrite the Host header. To rewrite Host headers, use ReverseProxy directly with a custom Director policy.

结束语

本文通过一个简单的正向代理程序的错误姿势,引出了 Host 请求头的作用,更进一步认识了主流负载均衡服务器在请求链路中的行为。


Host 请求头用于在单负载节点支撑多域名。

发布于: 刚刚阅读数: 3
用户头像

急性子,入戏慢。 2018.06.17 加入

阿里云社区专家博主,同程旅行基础架构 ; 热衷分享,执着于阅读写作,佛系不水文,有态度公众号:《精益码农》; 持续输出高价值Go、.NET、云原生原创文章。

评论

发布
暂无评论
自古以来,代理程序都是兵家折戟之地_有态度的马甲_InfoQ写作社区