- 真野隼記
- 約 3,000 文字
- 2,100 View
はじめに
夏の自由研究連載2021の4日目で、TIG DXユニット真野です。
この記事では みんな 個人的に大好きなフォワードプロキシの概要と、Goでの既存のOSSライブラリを利用した実装例をまとめました。このテーマに決めた理由は以下です。
- Goのnet/httpのクライアントは、
https_proxyの環境変数で差し込める(標準のデフォルトクライアントをそのまま利用する前提です) - 差し込んだプロキシ側に、ロギングや認証やできればリトライを仕込めば色々役立つ事があるんじゃないかという調査
- Goだとhttp.Clientは、RoundTripperというインターフェースを実装したTransportでカスタマイズ可能なので、実用だとこちらを利用したほうが良いと思います。
- 概念的にはサービスメッシュの文脈でのサイドカープロキシに近いものをイメージしています
自由研究という趣旨なので、そんなに実用性は考えず、興味ドリブンで手を動かそうと思いテーマに選びました。
フォワードプロキシとは
大きな括りではWebプロキシとも呼ばれることあるフォワードプロキシ(単にプロキシと呼ぶことも多いです)ですが、クライアントとWebサーバの中間に位置し、クライアントの要求を代理(proxy)してWebサーバにアクセスする存在です。ProxyとDockerと新人社員と時々わたしの記事にも詳しく書かれていますが、メリットとしてはキャッシュや接続先の通信の制限、ウィルススキャンを行うと言った余地をもたせることができる点でしょうか。Webエンジニアとしてのデメリットがあるとすると、利用するツール群でのプロキシ設定が大変だということがでしょうか。
たまにプロキシと書いてリバースプロキシ(私の周囲ではリバプロと略す人が多い)を指すブログ記事なども見かけますが、リバースプロキシとの差は、プロキシサーバがクライアント側にあるか、サーバ側にあるかの違いでしょう。今回は掲題にある通り、フォワードプロキシについてです。
自由研究でやりたいこと
Goでローカル端末(同一プロセス)上にフォワードプロキシを立ち上げ、アプリ側には http_proxyやhttps_proxy の環境変数で先程のフォワードプロキシのFQDNを設定してアクセスさせること。
もし同一プロセス内に組み込む場合は、実現させるためにはローカルでフォワードプロキシのgoroutineを起動すること、フォワードプロキシのプロトコル(HTTPのCONNECTメソッドなど)を守って実装する必要があり、利用できるOSSなどを調査します。
リバースプロキシだと、net/http/httputil の ReverseProxyを利用すればかなり楽できそうなのですが、繰り返しますが今回構築するのはフォワードプロキシなので異なります。
実装
フォワードプロキシを実装するには、HTTP CONNECTメソッドなど所定のプロトコルを解釈させる必要があると思うのですが、elazarl/goproxyなど有名なプロダクトがすでに存在したのでそちらを利用します。
goproxyという名前はgo module側のプロキシサーバと勘違いしそうですが、それとは関係ないです。
goproxyはカスタマイズ可能なHTTPプロキシライブラリを提供するとREADMEに書いている通り、内部で利用するTransportなどが公開されているので自由度が高い印象を受けました。プロキシ自体は net/httpのハンドラーなので、コードもGoに慣れている人であれば比較的理解しやすいと思います。
goporxyをまずmain関数内で呼び出すミニマムな実装で試してみます。
package main |
IPアドレスを取得する、 https://httpbin.org/ip というサイトにアクセスで試してみます。IP部分はなんとなく書き換えておきます。
環境変数プロキシ設定 |
上記のように実行してみると、proxy.Verbose = true の設定をしていることもあり、goproxyでログ出力され、ローカルのフォワードプロキシを経由して通信されていることがわかります。
念の為、環境変数を外すると、直接外部に通信されることも確認します。
環境変数を外す |
goproxy側でログを出していないため、環境変数の有無で通信経路を変えられたようです。
goproxyで紹介されているユースケース
READMEにはgoproxyの利用例もいくつか書かれていて興味深かったです。例えば、午前8時から午後17時までの時間帯にはアクセスを禁じる処理が紹介されていました。これは冗談寄りのアイデアだと思いますが、実用に近づけたユースケースを考えると障害テスト寄りのことを実現するときにも使えそうだなと思います。
proxy.OnRequest(goproxy.DstHostIs("www.reddit.com")).DoFunc( |
もちろん、何かしらの認証やトレーサビリティに利用できそうな、リクエストヘッダへの差し込みも可能で、紹介されています。
proxy.OnRequest().DoFunc( |
拡張ポイント
前の章で説明しましたが、goproxyを利用する場合は以下のような拡張ポイントが用意されています。
// Add handlers to httpsHandlers |
実装例はexamplesフォルダに2021.08.26時点で14ほどの例があるので、大体何ができるかはここから追えると思います。
ミドルウェアでの拡張
goproxyもServeHTTPを実装されているため、よくあるmiddlewareでの拡張が可能です。
|
このmiddlewareを次のように呼び出します。
go http.ListenAndServe(":8000", exampleMiddleware(proxy)) |
この形式であれば、既存資産のライブラリを流用しやすいかもしれません。
ミドルウェア拡張の注意
例えば、レスポンスコードを取得するために、 http.ResponseWriter をラップしたいときはよくあると思います。
type StatusRecorder struct { |
これをそのまま使うと、panicが発生するので注意です。
go http.ListenAndServe(":8000", loggingMiddleware(proxy)) |
go run main.go https://httpbin.org/ip |
リクエストをラップするにはHijackインターフェースを実装する必要があるとのこと。そこで以下のレシーバーを追加します。
func (r *StatusRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) { |
そうするとステータスコードのロギングが成功します。
> go run main.go http://httpbin.org/ip |
注意ですが、HTTPS通信だとこの実装ではステータスが取れません。Man in The Middel Proxyの仕組みを構築する必要があるのでそこまでガンバるかどうかでしょうか(この制約が、圧倒的に使い勝手の面でhttp.Client側のTransportに比べて面倒だなと感じることができました)
>go run main.go https://httpbin.org/ip |
HTTPSの宛先では、★★★ 0 とステータスが取れていないことがわかります。
さいごに
環境変数(http_proxy, https_proxy, no_proxy)などで差し込めるフォワードプロキシをローカル(に近いところ)で利用して、何かしらの共通処理を用いられないかという自由研究でした。接続先のサーバレスポンスによって処理を切り替えたい(例えばリトライしたい)というときには、Man in The Middel Proxyの考慮が必要で(特別な証明書を準備し、クライアントに読み込ませる必要がある)、気軽に導入するにはハードルが高いです。
実用性に近い部分では、リクエスト側に何かしらエンリッチ(認証情報やトレース情報)するケースや、カオスエンジニアなどの障害テストを行うときには少し便利かもしれません。障害テストはプロキシという要素が1つ増えているので、どうしてもスタブを作るのが面倒な時にサポート用途に使えるかも? という具合でしょうか。