如何用 Go 語言實現正向代理

正向代理是處理一組內網客戶端發往外部機器的網絡請求的一種代理方式。

實際上,正向代理是你的應用和你所要連接的服務器之間的中間人。它在 HTTP(S) 協議上起作用,並且被部署在網絡設施的邊緣。

你通常可以在大型組織或大學中見到正向代理,它被用來進行授權管理或網絡安全方面的控制。

我發現在使用容器或者動態的雲環境工作時,正向代理很有用,因爲你會面臨一組服務器和外部網絡的通信問題。

如果你在 AWS、AZure 之類的動態環境下工作,你會擁有一批數量不定的服務器和一批數量不定的公網 IP。你把應用運行在 Kubernetes 集羣上時也是一樣,容器可能遍佈四處。

現在假設有客戶讓你提供一個公網 IP 的範圍,因爲他需要設置防火牆。你如何提供這個特性呢?這個問題有些情況下很簡單,有些情況下可能非常複雜。

2015 年 12 月 1 日,有一位用戶在 **CircleCI 論壇**上提了這個問題,並且問題還未關閉。當然,CircleCI 很棒。我只是舉個例子,並非要埋怨他們。

解決這個問題的一種可行方法是使用正向代理。你可以讓一組節點以同一靜態 IP 運轉,然後把清單提供給客戶即可。

幾乎所有云服務提供商都是這樣做的,比如 DigitalOcean 的浮動 IP(floating IP)、AWS 的彈性 IP(elastic IP)等。

你可以通過配置自己的應用來把請求轉發到這個(代理)池中。這樣,終點的服務所取得的 IP 就是正向代理節點的 IP,而不是內部 IP。

正向代理可以成爲你的網絡設施的又一安全層,因爲你可以在一箇中心化的地方極其方便地掃描和控制內部網絡發出來的數據包。

正向代理不會帶來單點故障,因爲你可以運行多個正向代理服務,他們具有很好的伸縮性。

在底層,HTTP 的 CONNECT 方法就是一種正向代理。

CONNECT 方法將請求連接轉化爲透明 TCP/IP 通道,通常用於在未加密的 HTTP 代理上進行 SSL 加密的通信(HTTPS)。 很多用各種語言寫成的 HTTP 客戶端已經以透明的方式支持這個功能了。在此,我以一個使用 Go 語言和 privoxy 的小例子來告訴大家,這很簡單。

首先,我們創建一個名爲 whoyare 的應用。它是一個 HTTP 服務器,功能是返回你的遠程地址。

package main
import (
 "encoding/json"
 "net/http"
)
func main() {
 http.HandleFunc("/whoyare", func(w http.ResponseWriter, r *http.Request) {
  w.Header().Set("Content-Type", "application/json")
  body, _ := json.Marshal(map[string]string{
   "addr": r.RemoteAddr,
  })
  w.Write(body)
 })
 http.ListenAndServe(":8080", nil)
}

如果用 GET 方法訪問路徑 /whoyare,你會得到一個類似下面的 JSON 格式的響應:{"addr": "34.35.23.54"},其中 34.35.23.54 就是你的公網地址。如果你使用的是筆記本電腦,那麼在終端上發出請求後,你應該會得到 localhost的結果。可以用 curl 來試一下:

18:36 $ curl -v http://localhost:8080/whoyare
* TCP_NODELAY set
> GET /whoyare HTTP/1.1
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< Date: Sun, 18 Mar 2018 17:36:40 GMT
< Content-Length: 31
<
* Connection #0 to host localhost left intact
{"addr":"localhost:38606"}

我寫了另外一個程序,它用 http.Client 在標準輸出上打印響應。你可以在已經運行了 whoyare 服務的前提下運行這個程序:

package main
import (
 "io/ioutil"
 "log"
 "net/http"
 "os"
)
type whoiam struct {
 Addr string
}
func main() {
 url := "http://localhost:8080"
 if "" != os.Getenv("URL") {
  url = os.Getenv("URL")
 }
 log.Printf("Target %s.", url)
 resp, err := http.Get(url + "/whoyare")
 if err != nil {
  log.Fatal(err.Error())
 }
 defer resp.Body.Close()
 body, err := ioutil.ReadAll(resp.Body)
 if err != nil {
  log.Fatal(err.Error())
 }
 println("You are " + string(body))
}

這是個很簡單的例子,但是你可以將其運用在很多複雜場合。

爲了讓例子更清楚,我在 DigitalOcean 上創建了兩臺虛擬機:一臺運行 privoxy,另一臺運行 whoyare

Privoxy 是一個易用的正向代理。相比而言,Nginx 和 Haproxy 都不太適合在這種場景下使用,因爲它們不支持CONNECT方法。

我在 Docker Hub 上創建了一個 docker 鏡像,你可以直接運行它,默認使用端口 8118。

core@coreos-s-1vcpu-1gb-ams3-01 ~ $ docker run -it --rm -p 8118:8118
gianarb/privoxy:latest
2018-03-18 17:28:05.589 7fbbf41dab88 Info: Privoxy version 3.0.26
2018-03-18 17:28:05.589 7fbbf41dab88 Info: Program name: privoxy
2018-03-18 17:28:05.591 7fbbf41dab88 Info: Loading filter file:
/etc/privoxy/default.filter
2018-03-18 17:28:05.599 7fbbf41dab88 Info: Loading filter file:
/etc/privoxy/user.filter
2018-03-18 17:28:05.599 7fbbf41dab88 Info: Loading actions file:
/etc/privoxy/match-all.action
2018-03-18 17:28:05.600 7fbbf41dab88 Info: Loading actions file:
/etc/privoxy/default.action
2018-03-18 17:28:05.607 7fbbf41dab88 Info: Loading actions file:
/etc/privoxy/user.action
2018-03-18 17:28:05.611 7fbbf41dab88 Info: Listening on port 8118 on IP address
0.0.0.0

第二步,編譯whoyare並且把可執行文件用 scp 傳送到服務器,可使用以下命令:

$ CGO_ENABLED=0 GOOS=linux go build -o bin/server_linux -a ./whoyare

應用運行起來之後,我們就可以用 cURL 來直接或者通過 privoxy 發送請求了。

直接發送請求如下:

$ curl -v http://your-ip:8080/whoyare

cURL 使用環境變量http_proxy來配置代理進行請求轉發:

$ http_proxy=http://167.99.41.79:8118 curl -v http://188.166.17.88:8080/whoyare
*   Trying 167.99.41.79...
* TCP_NODELAY set
* Connected to 167.99.41.79 (167.99.41.79) port 8118 (#0)
> GET http://188.166.17.88:8080/whoyare HTTP/1.1
> Host: 188.166.17.88:8080
> User-Agent: curl/7.58.0
> Accept: */*
> Proxy-Connection: Keep-Alive
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< Date: Sun, 18 Mar 2018 17:37:02 GMT
< Content-Length: 29
< Proxy-Connection: keep-alive
<
* Connection #0 to host 167.99.41.79 left intact
{"addr":"167.99.41.79:58920"}

如你所見,我設置了 http_proxy=http://167.99.41.79:8118 之後,響應不再包含我的公網 IP 了,而是代理的 IP。

privoxy 處應該會留下如下的請求日誌:

2018-03-18 17:28:22.886 7fbbf41d5ae8 Request: 188.166.17.88:8080/whoyare
2018-03-18 17:32:29.495 7fbbf41d5ae8 Request: 188.166.17.88:8080/whoyare

你之前運行的客戶端默認連接到 localhost:8080,但可以通過設置環境變量 URL=http://188.166.17.88:8080 來覆蓋目標地址。運行以下命令可以直接到達 whoyare

$ URL=http://188.166.17.88:8080 ./bin/client_linux
2018/03/18 18:37:59 Target http://188.166.17.88:8080.
You are {"addr":"95.248.202.252:38620"}

Go 語言的 HTTP.Client 包支持一組和代理相關的環境變量,設置這些環境變量可以對運行期間的服務立刻生效,十分靈活。

export HTTP_PROXY=http://http_proxy:port/
export HTTPS_PROXY=http://https_proxy:port/
export NO_PROXY=127.0.0.1, localhost

前兩個環境變量很簡單,一個是 HTTP 代理,一個是 HTTPS 代理。NO_PROXY 排除了一組主機名,當要訪問的主機在這個清單裏的時候,請求不經過代理。我這裏配置的是 localhost 和 127.0.0.1。

HTT_PROXY=http://forwardproxy:8118
     +--------------+           +---+         +---+
     |              |           |                |         |                |
     |   client     +----------^+ forward proxy  +--------^+    whoyare     |
     |              |           |                |         |                |
     +--------------+           +---+         +----^-----------+
                                                                |
                                                                |
    +---------------+                                           |
    |               |                                           |
    |   client      +-----------------+
    |               |
    +---------------+
   HTTP_PROXY not configured

配置了環境變量的客戶端將會通過代理訪問,其他客戶端將直接訪問。

這個控制粒度很重要。你不僅可以按進程去控制是否經過代理,還可以按請求去控制,十分靈活。

$ HTTP_PROXY=http://167.99.41.79:8118 URL=http://188.166.17.88:8080
./bin/client_linux
2018/03/18 18:39:18 Target http://188.166.17.88:8080.
You are {"addr":"167.99.41.79:58922"}

可以看到,我們通過代理到達了 whoyare,響應中的 addr 是代理的地址。

最後一個命令有些怪異,但它只是爲了展示 NO_PROXY 是如何工作的。我們在設置訪問代理的同時,排除了 whoyare 的 URL。正如我們期望的那樣,請求沒有經過代理:

$ HTTP_PROXY=http://167.99.41.79:8118 URL=http://188.166.17.88:8080 NO_PROXY=188.166.17.88 ./bin/client_linux
2018/03/18 18:42:03 Target http://188.166.17.88:8080.
You are {"addr":"95.248.202.252:38712"}

本文應作爲 Go 語言和正向代理的實用介紹來閱讀。你可以訂閱我的 rss,或者在 twitter 上關注我。興許我以後還會介紹如何用 Go 替代 privoxy 以及如何在 Kubernetes 集羣上部署。所以,快告訴我先寫哪部分吧!

via: https://gianarb.it/blog/golang-forwarding-proxy

作者:gianarb

譯者:vincent08

校對:Unknwon

本文由 GCTT 原創編譯,Go 中文網 榮譽推出

更多資訊歡迎關注公衆號:Go 語言中文網

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://zhuanlan.zhihu.com/p/145159685