ブログ エンジニアリング no_proxyを標準化する方法:お客様事例で徹底解説
更新日:March 17, 2025
12分で読めます

no_proxyを標準化する方法:お客様事例で徹底解説

環境変数“no proxy”が原因で問題発生したことは?お客様事例を取り上げ、標準化の方法を考えてみました。

question-mark-pile.jpg

ウェブプロキシサーバーを使用した経験がある方なら、環境変数http_proxyHTTP_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.cominternal2.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_proxyno_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"

つまり、元々「小文字表記のみ」で始まったのですが、その後新しいクライアントであるwgetcurlの登場により、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がcurlv5.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設定の先頭に「.」がある場合、動作が異なります。

たとえば、curlwgetは動作が異なります。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に以下の問題の報告がありました。

  1. コマンドラインからのgit pushが起動した
  2. ウェブ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_proxyNO_PROXY、両者の定義が一致していないため警告が出ました。定義を一致させるか/誤ったエントリを削除することで、この問題を解決できます。

この古いエントリの何が原因で問題が起きたのか、もう少し詳しく見てみることにします。先ほど「no proxyの場合」で述べたことをここで思い出してみましょう。

  1. Rubyはまず小文字形式を試す
  2. Goはまず大文字形式を試す

その結果、GitLab WorkhorseなどのGoで記述されたサービスには正しいプロキシ構文となりました。Goサービスが主にこのアクティビティを処理したため、コマンドラインからのgit pushは正常に機能しました。

ClientWorkhorseGitaly1. git push2. gRPC: PostReceivePack3. OK4. OKClientWorkhorseGitaly

gRPC呼び出しでは、no_proxyがGitalyに直接接続するように適切に構成されていたため、プロキシの使用が試行されませんでした。

ただし、ユーザーがUIを変更すると、GitalyはリクエストをRubyで記述されたgitaly-rubyサービスに転送します。gitaly-rubyはリポジトリに変更を加え、gRPCコールバックを介して親プロセスにレポートを返します(英語)。ただし、以下の手順4に示すように、レポート手順は実行されませんでした。

ClientRailsGitalygitaly-rubyProxy1. Change file in UI2. gRPC: UserCommitFiles3. gRPC: UserCommitFiles4. CONNECT5. FAILClientRailsGitalygitaly-rubyProxy

gRPCは基盤となるトランスポートとしてHTTP/2を使用するため、gitaly-rubyは間違ったno_proxy設定で構成されたプロキシへのCONNECTを試行しました。プロキシはこのHTTP要求を即座に拒否し、ウェブUIプッシュケースで失敗を引き起こしました。

環境から小文字のno_proxyを削除すると、UIからのプッシュが期待どおりに機能し、gitaly-rubyが親のGitalyプロセスに直接接続されました。以下の図のステップ4は適切に機能しました。

ClientRailsGitalygitaly-rubyProxy1. Change file in UI2. gRPC: UserCommitFiles3. gRPC: UserCommitFiles4. OK5. OK6. OKClientRailsGitalygitaly-rubyProxy

もう一つの原因はgRPCにあった

https://ではなくhttp://が使用されています。セキュリティの観点からは理想的ではありませんが、TLS証明書の検証の問題によりクライアントが失敗するという面倒を避けるために行う場合もあります。

しかしこの場合、HTTPSプロキシが指定されていれば、この問題は発生しなかったでしょう。HTTPSプロキシが使用されている場合、gRPCはHTTPSプロキシをサポートしていないため、この設定を無視するからです。

解決策:最小限の共通項で設定する

小文字と大文字のプロキシ設定で矛盾した値を定義すべきではないことは、誰もが同意すると思います。ただし、複数の言語で記述されたスタックを管理する必要がある場合は、HTTPプロキシ構文を最も共通する設定で行うよう検討することをおすすめします。

http_proxyhttps_proxy

  • 小文字形式を使用する。 HTTP_PROXY は常にサポートまたは推奨されるわけではない。
    • どうしても大文字形式も使用する必要がある場合は、必ず 同じ値を共有する。

no_proxy

  1. 小文字形式を使用する。
  2. カンマ区切りのhostname:port値を使用する。
  3. IPアドレスは問題ないが、ホスト名は解決されない。
  4. サフィックスは常にマッチングされる(例:example.comtest.example.comと一致)。
  5. トップレベルドメインを一致させる必要がある場合は、先頭のドット(.)を使用しない。
  6. GoとRubyのみがCIDRマッチングをサポートしているため、CIDRマッチングの使用は避ける。

解決策:no_proxyの標準化チェックリスト

最小公分母を知っておくと、定義が異なるウェブクライアントにコピーされた場合に、問題を回避する上で役立ちます。しかし、no_proxyやその他のプロキシ設定には、間に合わせの標準よりも、文書化された標準が必要かもしれません。以下のリストを出発点としてお役立てください。

  1. 大文字の変数よりも小文字の変数を優先する (例 http_proxyHTTP_PROXYの前に検索すべき)。
  2. カンマ区切りの hostname:port 値を使用する。
    • 各値にはオプションの空白を含めることができる。
  3. DNSルックアップの実行や、正規表現の使用を行わない。
  4. すべての ホストに一致させるには*を使用する。
  5. 先頭のドット (.) を削除し、ドメインサフィックスに対してマッチングさせる。
  6. CIDRブロックマッチングをサポートする。
  7. 特別なIPアドレスを想定しない(たとえばno_proxyのループバックアドレス)。

まとめ

最初のウェブプロキシがリリースされてから25年以上経ちました。環境変数を介してウェブクライアントを構成する基本的な仕組みはあまり変わっていませんが、さまざまな実装で微妙な違いが生じています。

今回、GitLabのあるお客様の具体的な事例をご紹介しました。このお客様の状況は以下のとおりでした。

  • 競合するno_proxy変数とNO_PROXY変数を誤って定義
  • RubyとGoはこれらの設定を処理する方法が異なるため、トラブルシューティングに何時間も費やす

このブログではこの2つの違いに焦点を当て、解説しました。皆様の本番スタックで将来の問題発生回避にお役立ていただけると幸いです。また、設定標準チェックリストを参照して、ウェブクライアントの保守担当者様が動作を標準化し、このような問題を根本的に回避することを願っています。

Gitの利便性を生かしつつ、一元化されたプラットフォームでデベロッパー、セキュリティ担当者、運用チームをサポートするGitLabでは、AIによるコード提案機能があるため、効率性を高められます。導入検討中の方は、ぜひ無料でのトライアルをお試しください。

無料トライアルを開始してみる

画像出展: PixaBay




監修:小松原 つかさ  @tkomatsubara
(GitLab合同会社 ソリューションアーキテクト本部 シニアパートナーソリューションアーキテクト)

ご意見をお寄せください

このブログ記事を楽しんでいただけましたか?ご質問やフィードバックがあればお知らせください。GitLabコミュニティフォーラムで新しいトピックを作成して、ご意見をお聞かせください。 フィードバックをお寄せください

始めてみましょう

統合されたDevSecOpsプラットフォームによって、チームで何が実現できるかご確認ください。

無料トライアルを開始する

チームに最適なプランを見つけましょう

価格設定を見る

GitLabがチームにもたらすメリットをご覧ください

お問い合わせ