ウェブプロキシサーバーを使用した経験がある方なら、環境変数http_proxy
やHTTP_PROXY
をよくご存知でしょう。しかし、no_proxy
(ノープロキシ)に関しては、どうもわかりにくい、と感じていらっしゃる方も多いのではないでしょうか。
no proxyとは、あるホスト宛のトラフィックでプロキシを経由させないようにする環境変数です。世界基準が存在するHTTPと違い、ウェブクライアントがno proxyを処理する方法に「標準」は存在しません。その結果、ウェブクライアントは場合により異なる方法で処理を行います。
その違いが原因でサービスが通信を停止し、その原因を突き止めるために週末返上で作業する羽目になったGitLabのお客様もいらっしゃいます。
そこで、この記事ではGitLabのお客様が直面した問題について、具体例を挙げながら根本原因を探り、「no proxyを標準化する方法」というテーマを掘り下げてみます。
no proxyはなぜ「わかりにくい」のか
no proxyがなぜわかりにくいのか、具体例を挙げて説明します。
現在、ほとんどのウェブクライアントは環境変数を介してプロキシサーバーへの接続をサポートしています。環境変数には大文字表記と小文字表記があります。
http_proxy
/HTTP_PROXY
https_proxy
/HTTPS_PROXY
no_proxy
/NO_PROXY
これらの変数は、プロキシサーバーにアクセスするのにどのURLを使用するか、またどういった例外を作っているか、クライアントに指示するものです。
たとえば、ある企業で田中さんがhttp://tanaka.example.com:8080
でリッスンしているプロキシサーバーの場合、次のようになります。
export http_proxy=http://tanaka.example.com:8080
一方、同僚の斎藤さんも、次のように大文字バージョンのHTTP_PROXY
で定義していたとします。
export HTTP_PROXY=http://saito.example.com:8080
この場合、どちらのプロキシサーバーが使用されることになるのでしょうか?答えは「状況によって異なる」です。ある場合は田中さんのプロキシサーバーが有効になる場合もあれば、ある場合は斎藤さんのプロキシサーバーが有効になる場合があります。
この場合、どちらのプロキシサーバーが使用されることになるのでしょうか?答えは「状況によって異なる」です。ある場合は田中さんのプロキシサーバーが有効になる場合もあれば、ある場合は斎藤さんのプロキシサーバーが有効になる場合があります。
では、例外を設定したい場合はどうなるでしょうか。たとえば、internal.example.com
とinternal2.example.com
以外のすべてで、プロキシサーバーを経由したい場合です。このような場合がno_proxy
変数の出番です。no_proxy
を次のように定義します。
export no_proxy=internal.example.com,internal2.example.com
では、IPアドレスを除外したい場合はどうすればよいでしょうか?アスタリスクやワイルドカードは使用できるのでしょうか?CIDRブロック(例:192.168.1.1/32
)は?
これらの答えも、「状況によって異なる」です。つまり「使用言語やツールという”PC環境”によって、proxy変数の処理方法が異なる」のが、no proxyがわかりにくいとされている理由です。次の項では、proxy変数の処理方法の違いについてさらに掘り下げます。
なぜno proxyはこんなに複雑なのか?
この問題の理解を深めるため、no proxyを巡るこれまでの経緯を説明しておきます。
1994年においてほとんどのウェブクライアントは、http_proxy
とno_proxy
環境変数をサポートするCERNのlibwww
を使用していました。libwww
は、http_proxy
の小文字形式のみを使用し、no_proxy
構文は以下のようにシンプルでした。
no_proxy is a comma- or space-separated list of machine
or domain names, with optional :port part. If no :port
part is present, it applies to all ports on that domain.
Example:
no_proxy="cern.ch,some.domain:8001"
つまり、元々「小文字表記のみ」で始まったのですが、その後新しいクライアントであるwget
やcurl
の登場により、no proxy
の大文字が使用可になったり、不可とされたりと変遷しているのです。
1996年1月にHrvoje Niksicが、libwww
をリンクせずに独自のHTTP実装を追加する新しいクライアント、geturl
(現在のwget
の前身)をリリースしました。翌月にはgeturl
がバージョン1.1でhttp_proxyのサポートを追加され、同年5月にはgeturl
バージョン1.3でno_proxy
のサポートが追加されました。ここではlibwww
と同様に、geturl
では小文字形式no_proxy
のみのサポートでした。
1998年1月には、Daniel Stenbergがcurl
v5.1をリリースし、http_proxy
およびno_proxy
変数をサポート。また、大文字の形式のHTTP_PROXY
およびNO_PROXY
も許可されました。
2009年3月にはcurl v7.19.4がセキュリティ上の懸念から、大文字HTTP_PROXY
のサポートを廃止します。curl
ではHTTP_PROXY
は無視されますが、HTTPS_PROXY
は現在でも動作します。
一目でわかるproxy変数の処理方法の違い
GitLabのNourdinel Bachaが調査したところ、これらのプロキシサーバー変数の処理方法は、使用言語やツールによって異なることがわかりました。
http_proxyとhttps_proxyの場合
各行はサポートされている動作を表し、各列にはそれが適用されるツール(例:curl)または言語(例:Ruby)を表しています。
curl | wget | Ruby | Python | Go | |
---|---|---|---|---|---|
http_proxy |
はい | はい | はい | はい | はい |
HTTP_PROXY |
いいえ | いいえ | はい (警告) | はい (REQUEST_METHOD が環境にない場合) |
はい |
https_proxy |
はい | はい | はい | はい | はい |
HTTPS_PROXY |
はい | いいえ | はい | はい | はい |
大文字と小文字の優先順位 | 小文字 | 小文字のみ | 小文字 | 小文字 | 大文字 |
参照 | 出所 | 出所 | 出所 | 出所 | 出所 |
この表から以下のことがわかります。
- http_proxyとhttps_proxyは常に全面的にサポートされているが、HTTP_PROXYは必ずしもサポートされているわけではない。
- Python(urllib経由)では状況がさらに複雑となる。HTTP_PROXYが使用できるのは、REQUEST_METHODが環境で定義されていない場合に限られる。
- Goだけは他と異なり、小文字バージョンより大文字バージョンを優先する。
環境変数はすべて大文字だと思われがちですが、実は最初に登場したhttp_proxy
に倣い、小文字表記が事実上のスタンダードとなっています。よくわからない場合は、普遍的にサポートされている小文字形式の使用をおすすめします。
no_proxyの場合
さて、次はno_pproxy
について説明します。次の表は、さまざまな実装の状態を示しています。こちらの表はhttp_proxy
の場合に比べてもっと複雑です。例えば、no_proxy
設定が次の様に定義されているとします。
export no_proxy=example.com
これはドメインが完全一致である必要があるのか、それともsubdomain.example.comのようなサブドメインも含まれるのでしょうか。次の表は様々な実装状況を示しています。「サフィックス(接尾辞)と一致?」の行を見ると分かるように、実際にはすべての実装がサフィックス(ドメイン末尾)を適切に一致させることができます。
curl | wget | Ruby | Python | Go | |
---|---|---|---|---|---|
no_proxy |
はい | はい | はい | はい | はい |
NO_PROXY |
はい | いいえ | はい | いいえ | はい |
大文字と小文字の優先順位 | 小文字 | 小文字のみ | 小文字 | 小文字のみ | 大文字 |
サフィックス(接尾辞)と一致? | はい | はい | はい | はい | はい |
. でリーディング停止? |
はい | いいえ | はい | はい | いいえ |
* はすべてのホストに一致? |
はい | いいえ | いいえ | はい | はい |
正規表現をサポート? | いいえ | いいえ | いいえ | いいえ | いいえ |
CIDRブロックをサポート? | いいえ | いいえ | はい | いいえ | はい |
ループバックIPを検出する? | いいえ | いいえ | いいえ | いいえ | はい |
参考 | 出所 | 出所 | 出所 | 出所 | 出所 |
ただし、no_proxy
設定の先頭に「.」がある場合、動作が異なります。
たとえば、curl
とwget
は動作が異なります。curl
は常に先頭の「.」を削除し、ドメインサフィックスと照合します。次の呼び出しはプロキシをバイパスします。
$ env https_proxy=http://non.existent/ no_proxy=.gitlab.com curl https://gitlab.com
<html><body>You are being <a href="https://about.gitlab.com/">redirected</a>.</body></html>
ただし、wget
は先頭の「.
」を削除せず、ホスト名に対して正確な文字列一致を実行します。その結果、wget
はトップレベルドメインが使用されている場合にプロキシの使用を試みます。
$ env https_proxy=http://non.existent/ no_proxy=.gitlab.com wget https://gitlab.com
Resolving non.existent (non.existent)... failed: Name or service not known.
wget: unable to resolve host address 'non.existent'
すべての実装において、正規表現はサポートされません。
正規表現には独自の特徴(PCRE、POSIXなど)があるため、正規表現を使用すると問題がさらに複雑になると思われます。また、正規表現を使用すると、パフォーマンスとセキュリティの問題が発生する可能性があります。
no_proxy
を*
に設定するとプロキシが完全に無効になる場合もあるが、これはすべてに共通するルールではない。
プロキシを使用するかどうかを決定する際に、ホスト名をIPアドレスに解決するためのDNSルックアップを実行する実装はない。
クライアントによってIPアドレスが明示的に使用されることが予想される場合を除き、no_proxy
変数にIPアドレスを指定しないようにしましょう。
18.240.0.1/24
などのCIDRブロックは、リクエストが直接IPアドレスに対して行われた場合にのみ機能します。CIDRブロックが許可されるのはGoとRubyのみです。他の実装とは異なり、GoではループバックIPアドレスが検出されると、プロキシの使用が自動的に無効になります。
GitLabのお客様が抱えたno proxy問題
大文字小文字表記、言語とツールによるリアクションの違いに注意を払う必要があるのは、複数の言語で記述されたアプリケーションを、プロキシサーバーを備えた企業のファイアウォールの背後で動作させる場合です。GitLabもそのひとつであり、RubyとGoで記述された複数のサービスで構成されています。
ここでGitLabのあるお客様の例を挙げましょう。お客様はプロキシ構文を次のように設定しました。
HTTP_PROXY: http://proxy.company.com
HTTPS_PROXY: http://proxy.company.com
NO_PROXY: .correct-company.com
このお客様からGitLabに以下の問題の報告がありました。
- コマンドラインからの
git
pushが起動した - ウェブUI経由で行われたGitの変更が失敗した
連絡を受けたサポートエンジニアは、Kubernetesの構文の問題により、古い値が残っていることを発見しました。ポッドの環境は実際には次のようになっていました。
HTTP_PROXY: http://proxy.company.com
HTTPS_PROXY: http://proxy.company.com
NO_PROXY: .correct-company.com
no_proxy: .wrong-company.com
no_proxy
とNO_PROXY
、両者の定義が一致していないため警告が出ました。定義を一致させるか/誤ったエントリを削除することで、この問題を解決できます。
この古いエントリの何が原因で問題が起きたのか、もう少し詳しく見てみることにします。先ほど「no proxyの場合」で述べたことをここで思い出してみましょう。
- Rubyはまず小文字形式を試す
- Goはまず大文字形式を試す
その結果、GitLab WorkhorseなどのGoで記述されたサービスには正しいプロキシ構文となりました。Goサービスが主にこのアクティビティを処理したため、コマンドラインからのgit push
は正常に機能しました。
gRPC呼び出しでは、no_proxy
がGitalyに直接接続するように適切に構成されていたため、プロキシの使用が試行されませんでした。
ただし、ユーザーがUIを変更すると、GitalyはリクエストをRubyで記述されたgitaly-ruby
サービスに転送します。gitaly-ruby
はリポジトリに変更を加え、gRPCコールバックを介して親プロセスにレポートを返します(英語)。ただし、以下の手順4に示すように、レポート手順は実行されませんでした。
gRPCは基盤となるトランスポートとしてHTTP/2を使用するため、gitaly-ruby
は間違ったno_proxy
設定で構成されたプロキシへのCONNECTを試行しました。プロキシはこのHTTP要求を即座に拒否し、ウェブUIプッシュケースで失敗を引き起こしました。
環境から小文字のno_proxy
を削除すると、UIからのプッシュが期待どおりに機能し、gitaly-ruby
が親のGitalyプロセスに直接接続されました。以下の図のステップ4は適切に機能しました。
もう一つの原因はgRPCにあった
https://
ではなくhttp://
が使用されています。セキュリティの観点からは理想的ではありませんが、TLS証明書の検証の問題によりクライアントが失敗するという面倒を避けるために行う場合もあります。
しかしこの場合、HTTPSプロキシが指定されていれば、この問題は発生しなかったでしょう。HTTPSプロキシが使用されている場合、gRPCはHTTPSプロキシをサポートしていないため、この設定を無視するからです。
解決策:最小限の共通項で設定する
小文字と大文字のプロキシ設定で矛盾した値を定義すべきではないことは、誰もが同意すると思います。ただし、複数の言語で記述されたスタックを管理する必要がある場合は、HTTPプロキシ構文を最も共通する設定で行うよう検討することをおすすめします。
http_proxy
と https_proxy
- 小文字形式を使用する。
HTTP_PROXY
は常にサポートまたは推奨されるわけではない。- どうしても大文字形式も使用する必要がある場合は、必ず 同じ値を共有する。
no_proxy
- 小文字形式を使用する。
- カンマ区切りの
hostname:port
値を使用する。 - IPアドレスは問題ないが、ホスト名は解決されない。
- サフィックスは常にマッチングされる(例:
example.com
はtest.example.com
と一致)。 - トップレベルドメインを一致させる必要がある場合は、先頭のドット(
.
)を使用しない。 - GoとRubyのみがCIDRマッチングをサポートしているため、CIDRマッチングの使用は避ける。
解決策:no_proxy
の標準化チェックリスト
最小公分母を知っておくと、定義が異なるウェブクライアントにコピーされた場合に、問題を回避する上で役立ちます。しかし、no_proxy
やその他のプロキシ設定には、間に合わせの標準よりも、文書化された標準が必要かもしれません。以下のリストを出発点としてお役立てください。
- 大文字の変数よりも小文字の変数を優先する (例
http_proxy
はHTTP_PROXY
の前に検索すべき)。 - カンマ区切りの
hostname:port
値を使用する。- 各値にはオプションの空白を含めることができる。
- DNSルックアップの実行や、正規表現の使用を行わない。
- すべての ホストに一致させるには
*
を使用する。 - 先頭のドット (
.
) を削除し、ドメインサフィックスに対してマッチングさせる。 - CIDRブロックマッチングをサポートする。
- 特別なIPアドレスを想定しない(たとえば
no_proxy
のループバックアドレス)。
まとめ
最初のウェブプロキシがリリースされてから25年以上経ちました。環境変数を介してウェブクライアントを構成する基本的な仕組みはあまり変わっていませんが、さまざまな実装で微妙な違いが生じています。
今回、GitLabのあるお客様の具体的な事例をご紹介しました。このお客様の状況は以下のとおりでした。
- 競合する
no_proxy
変数とNO_PROXY
変数を誤って定義 - RubyとGoはこれらの設定を処理する方法が異なるため、トラブルシューティングに何時間も費やす
このブログではこの2つの違いに焦点を当て、解説しました。皆様の本番スタックで将来の問題発生回避にお役立ていただけると幸いです。また、設定標準チェックリストを参照して、ウェブクライアントの保守担当者様が動作を標準化し、このような問題を根本的に回避することを願っています。
Gitの利便性を生かしつつ、一元化されたプラットフォームでデベロッパー、セキュリティ担当者、運用チームをサポートするGitLabでは、AIによるコード提案機能があるため、効率性を高められます。導入検討中の方は、ぜひ無料でのトライアルをお試しください。
画像出展: PixaBay
監修:小松原 つかさ @tkomatsubara
(GitLab合同会社 ソリューションアーキテクト本部 シニアパートナーソリューションアーキテクト)