先重温一下什么叫反向代理,正向代理。
鹅厂二面,nginx回忆录

所谓正向,反向代理取决于代理的是出站请求,还是入站请求

正向代理: 代理的出站请求, 客户端能感知到代理程序,架构上距离客户端更近。
反向代理: 代理的是入站请求,客户端认为代理程序就是服务器,客户端感知不到代理逻辑,架构上距离服务端更近。

反向代理的血案

前几天打算使用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的主机和端口信息。

Host = uri-host [ ":" port ]
host: the domain name of the server (for virtual hosting).
port[option]: TCP port number on which the server is listening.

最关键的第三点:

  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。
The exact resource identified by an Internet request is determined by
examining both the Request-URI and the Host header field.

什么意思呢?

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

Host请求头在虚拟主机服务多网域服务中的关键作用-小白菜博客
上图来自 阿里云应用型负载均衡

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

在这个配置中,nginx会检查请求的Host头与server_name指令的相等来决定该请求应由哪个虚拟主机来处理。

如果Host头没有匹配任意一个虚拟主机,或者请求中根本没有包含Host头,那nginx会将请求分发到定义在此端口上的默认虚拟主机。
在以上配置中,第一个被列出的虚拟主机即nginx的默认虚拟主机——这是nginx的默认行为。而且,可以显式地设置某个主机为默认虚拟主机,即在"listen"指令中设置"default_server"参数:

server {
    listen      80 default_server;
    server_name example.net www.example.net;  //  server_name name... 设置虚拟主机名,其实就是域名,可设置多个。
    ...
}

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


httputil内置的NewSingleHostReverseProxy 是一个【反向代理到固定地址】的实现,他也没有重写Host请求头, 而我上面的写法其实就是一个自定义实现。

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请求头用于在单负载节点支撑多域名。