自古以來,代理程式都是兵家折戟之地

2022-08-30 15:03:50

正向代理的血案

前幾天打算使用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設定基於名字的多虛擬主機的寫法:

在這個設定中,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請求頭用於在單負載節點支撐多域名。