順方向プロキシ

このページの目次

順方向プロキシは、クライアントに代わってターゲットにアクセスします。クライアントはまず、特定の接続またはリクエストをプロキシに委ねるかどうかを決定し、その後、プロキシがターゲットとポリシーに従って転送を行います。プロキシアドレスを設定しても、すべてのプログラム、DNS、UDP、またはシステムトラフィックが自動的にそのプロキシを経由するわけではありません。

プロキシのパスを理解するには、以下の3つの問題を同時に追跡する必要があります。誰がプロキシを選択するか、誰がターゲットドメイン名を解決するか、そして各セグメントが暗号化されているかどうかです。最後のターゲットIPやブラウザの「プロキシ接続済み」の表示だけを見ても、これらの質問に答えることはできません。

シナリオクライアントがプロキシに送信するものプロキシの主な作業
通常のHTTPプロキシリクエストabsolute-form 形式のターゲットを持つHTTPリクエストHTTPを解析し、転送する
HTTP CONNECTホストとポートバイトトンネルを確立する
SOCKS5 CONNECTバイナリ形式のターゲットアドレスとポートTCPリレーを確立する
SOCKS5 UDP ASSOCIATE制御関連付け、その後UDPカプセル化データグラムターゲットに応じてUDPをリレーする
PACローカルで選択ルールを実行するDIRECTまたはプロキシ候補を選択し、トラフィックは転送しない

HTTP リクエストプロキシと CONNECT

平文のサイトに通常のHTTPプロキシリクエストを送信する場合、リクエストターゲットは通常 GET http://example.com/index.html HTTP/1.1 のようになります。プロキシはHTTPを読み取り、処理できます。HTTPSターゲットにアクセスする場合、クライアントはまず CONNECT example.com:443 HTTP/1.1 と対応するHostを送信し、成功した2xxレスポンスを受け取ってから、トンネル内でターゲットとのTLSハンドシェイクを行います。

CONNECT はバイトトンネルを確立する。TLS の有無はトンネル内のプロトコルによって決まる クライアント まずプロキシに接続し、ターゲットを提出する プロキシ ポリシーに従ってターゲットに接続する ターゲットサーバー プロキシが開始した接続を受け入れる HTTPS シナリオ: クライアント ↔ ターゲットの TLS はトンネルを通過する プロキシは依然として CONNECT ターゲットとトラフィックのメタデータを確認できる クライアントからプロキシへ、プロキシからターゲットへの通信は通常2つのTCP接続であり、同じエンドツーエンドのTCPシーケンス空間ではありません。

CONNECTが成功した後、バイトは通常のHTTPメッセージボディとして解析されなくなります。通常、クライアントからプロキシへ、プロキシからターゲットへの通信は2つの独立したTCP接続です。プロキシが結合するのはバイト転送能力であり、1つのTCP接続をサーバーまでそのまま延長するわけではありません。

CONNECT自体は暗号化を提供せず、HTTPSに限定されません。プロキシはターゲットポートと宛先を制限できます。クライアントは、プロキシへの接続成功を、任意のアドレスへのアクセス許可と同一視してはいけません。プロキシ認証の失敗は通常407とProxy-Authenticateに対応し、ターゲットサービス認証の401は別の層です。RFC 9110: CONNECT

HTTPS プロキシと HTTPS ターゲットは別の問題

http://proxy.example:8080 と https://proxy.example:8443 は、クライアントからプロキシへの転送方法を記述しています。ターゲットURLの https:// は、ターゲットサービスがTLSを使用することを記述しています。HTTPSプロキシ上でHTTPSターゲットにCONNECTすると、外側のプロキシTLSと内側のターゲットTLSの両方が形成され、両者の認証対象は異なります。

内側のTLSを終了しないプロキシは、ターゲットのHTTPコンテンツを確認できませんが、明示的なCONNECTターゲット、時間、トラフィックの規模については依然として把握しています。ECHは内側のClientHelloの可視コンテンツを変更できますが、すでにプロキシに送信されたCONNECTターゲットを消去することはありません。クライアントがプロキシが発行した代替証明書を信頼し、TLS検査を許可する場合、実際の身元と機密性の境界はすでに変わっています。

HTTP/2、HTTP/3も対応するCONNECTメカニズムを担うことができます。すべてのプロキシ接続を1つのHTTP/1.1接続として仮定し、1つのトンネルしか構築しないことはできません。通常のCONNECTのTCPバイトトンネルとCONNECT-UDPは異なる能力です。後者には専用のUDPプロキシプロトコルの取り決めがあります。RFC 9298

SOCKS5: メソッド選択、認証、コマンドはTCPハンドシェイクと混同してはならない

まずプロキシへのTCP接続を確立し、利用可能な認証方法をネゴシエートします。ユーザー名/パスワードなどの方法を選択する場合、さらにサブネゴシエーションがあり、その後、CONNECT、BIND、またはUDP ASSOCIATEコマンドを送信します。これらの段階を固定の「3ウェイハンドシェイク」として一括りにすることはできません。

フィールドまたは値意味
VER=5SOCKSバージョン
METHOD=0x00 / 0x02認証なし / ユーザー名パスワード方法
METHOD=0xff受け入れ可能な方法なし、接続を閉じる
CMD=1 / 2 / 3CONNECT / BIND / UDP ASSOCIATE
ATYP=1 / 3 / 4IPv4 / ドメイン名 / IPv6
REP=0コマンド成功、その後対応するセマンティクスに従って転送する

ドメイン名アドレスには、1バイトの長さと名前のバイトが含まれ、末尾のNULはありません。ポートは2バイトのネットワークバイトオーダーです。CONNECTレスポンスのBND.ADDR/PORTは、プロキシがターゲット接続に使用するバインドエンドポイントであり、DSTをそのままエコーバックしたものではありません。RFC 1928

ユーザー名パスワードサブネゴシエーションには独自のバージョンフィールドがあり、1です。SOCKSメインプロトコルのバージョン5として記述してはいけません。この方法は資格情報バイトを直接転送し、暗号化を自動的に提供しません。セキュリティは外側の転送とデプロイメントの境界に依存します。RFC 1929

完全なアドレスの例: ドメイン名 CONNECT と IPv4 UDP ヘッダー

import ipaddress

name = b'example.com'
request = bytes([5, 1, 0, 3, len(name)]) + name + (443).to_bytes(2, 'big')
assert request.hex() == '050100030b6578616d706c652e636f6d01bb'
assert len(request) == 18
n = request[4]
assert request[5:5+n].decode('ascii') == 'example.com'
assert int.from_bytes(request[5+n:7+n], 'big') == 443
# SOCKS断片化なし、IPv4ターゲット。ここでDATAは教育的なバイトであり、実際のDNSパケットではありません。
udp_head = bytes([0, 0, 0, 1]) + ipaddress.ip_address('192.0.2.53').packed + (53).to_bytes(2, 'big')
packet = udp_head + b'DATA'
assert len(udp_head) == 10 and len(packet) == 14
assert udp_head.hex() == '00000001c00002350035'
assert int.from_bytes(packet[8:10], 'big') == 53
print('CONNECT リクエスト / UDPカプセル化ヘッダー:', len(request), len(udp_head))

この例はエンコーディングのみをチェックし、プロキシ接続、認証、またはリレーは実装しません。SOCKS5は汎用のIP層転送を提供しません。例えば、ICMPはCONNECT経由で送信されるTCPコンテンツとして直接通過しません。

UDP ASSOCIATE: 制御接続はUDP-over-TCPではない

SOCKS5 UDP: TCP 制御関連付けと UDP データパスが同時に存在する クライアント SOCKS5プロキシ UDPターゲット TCP: UDP ASSOCIATE / relayアドレスを返す UDP: SOCKSヘッダー + アプリケーションデータ UDP: アプリケーションデータ UDP: レスポンスデータ UDP: SOCKSヘッダー + レスポンスデータ 関連付けはTCP制御接続の存続に依存する。TCP接続があるからといって、UDPデータが信頼性の高い転送を得るわけではない。

クライアントはTCPコマンドを介して関連付けを確立します。コマンド内のアドレスとポートは、使用する予定のクライアントUDPエンドポイントを記述しており、不明な場合はゼロを埋め込むことができます。プロキシは利用可能なrelayエンドポイントを返信し、クライアントはその後、各UDPデータグラムにSOCKSヘッダーを付けてrelayに送信します。ヘッダー内で実際のターゲットを指定します。関連付けリクエストのゼロアドレスを、各データグラムの最終宛先として誤解しないでください。

返されるデータも同様に、relayがSOCKSヘッダーを付けてクライアントに送信し、リモートソースを識別します。関連付けはTCP制御接続が閉じられたときに終了します。UDPは依然として損失や再配置が発生する可能性があり、TCP制御チャネルはそれを再送信しません。FRAG=0は断片化されていないことを示し、SOCKS層の断片化サポートはオプションです。サポートしていない実装は、FRAGがゼロでないパケットを破棄します。これもIP断片化とは異なります。

IPv4ターゲットのSOCKS UDPヘッダーは10バイト、IPv6ターゲットは22バイト、ドメイン名ターゲットは7+名前のバイト数で、外側のUDP/IPオーバーヘッドが加算されます。TCPプロキシが接続可能だからといって、UDP relayが到達可能であることは証明されません。クライアントアプリケーションがUDP ASSOCIATEを実装していない可能性もあります。

DNS はどちら側にあるか:アドレスタイプとクライアントの実装が共同で決定する

クライアントがローカルで解決し、IPを送信する場合、プロキシはすでに発生したローカルDNSクエリを取り消すことはできません。ATYP=ドメイン名を送信する場合、プロキシ側で独自のポリシーに従って解決またはルーティングします。必ずしもプロキシが特定の固定パブリックDNSを使用するわけではありません。

curlは socks5:// のローカル解決と socks5h:// のプロキシ側解決を区別します。以下は、対応するローカルプロキシがある場合に手動で観察するためのものです。実行すると、実際のターゲットリクエストが発行されます。

curl --proxy socks5h://127.0.0.1:1080 --connect-timeout 5 https://example.com/

--connect-timeout はダウンロード全体の総時間制限ではありません。プロキシホスト自体の検出、アプリケーションの事前解決、DoH、およびこのプロキシを使用しない他のプロセスは、それぞれ個別に確認する必要があります。curlの成功1件だけで、システム全体にローカルDNSトラフィックがないと断言することはできません。curlプロキシオプション

ドメイン名をIPに変更すると、パケットの長さだけが変わるわけではない

2つのパスは最終的に同じターゲットにアクセスできますが、プロキシが受信する証拠は異なります。IPパスには元のドメイン名がなく、ドメイン名パスには長さバイトと名前自体があります。ここではコマンドの責任範囲を示していますが、これに基づいてシステムのすべてのDNSがプロキシを経由すると宣言することはできません。

図を準備しています
ドメイン名をIPに変更すると、パケットの長さだけが変わるわけではない

ATYP は、リクエストが名前またはアドレスのどちらを運ぶかを決定する。ローカルで既に実行されたDNSクエリは、その後暗号化トンネルに入っても消滅しない。

PAC: ターゲットに応じてルートを選択し、DIRECT は明示的なフォールバック

PACのFindProxyForURLは、プロキシ候補の文字列を返します。これは制限された環境内のJavaScriptであり、ブラウザのページ状態に自由にアクセスできる通常のスクリプトではありません。ドメインルールは、DNS補助関数を呼び出すよりも予測しやすいことが多いです。isInNet などはローカル解決を引き起こす可能性があり、ローカル解決を回避したいという意図と衝突します。

function FindProxyForURL(url, host) {
    // 明確に管理された内部ドメインのみをダイレクト接続に渡し、国のサフィックスでサーバーの場所を推測しない。
    host = host.toLowerCase();
    if (host === 'intranet.example' || host.slice(-17) === '.intranet.example') {
        return 'DIRECT';
    }
    return 'PROXY 127.0.0.1:7890; SOCKS5 127.0.0.1:1080';
}

実装がサポートするPAC構文と補助関数は、クライアントに応じて確認する必要があります。例の最後にDIRECTがないということは、明示的に「プロキシの失敗時に直接アクセスする」候補が許可されていないことを意味します。DIRECTを追加すると、失敗時の出口ポリシーが変更されるため、無害な固定テンプレートとして扱うことはできません。

PACは、各リクエストで完全に再実行されることを保証するものではなく、HTTPS URLが常にパスを含むと仮定してはいけません。Chromiumを例にとると、PACで可視化されるURLにはプライバシーの切り捨てがあり、プロキシ選択にはキャッシュと失敗処理があります。そのSOCKS5実装にも独自の能力範囲があります。ドメインサフィックス .cn はターゲットの物理位置を証明するものではなく、.local はローカル名前解決のセマンティクスも関与するため、すべてのイントラネットを粗雑に代表するために使用すべきではありません。Chromiumプロキシ説明

失敗の特定: 段階的に証拠を記録する

段階失敗のシグナル最初に確認すること
プロキシの選択期待されるプロキシを経由していないアプリケーションが設定、PAC/バイパスルールを読み取っているか
プロキシへの接続タイムアウト、接続拒否アドレス、リスニング、ネットワーク、外側のTLS
プロキシ認証407、SOCKSメソッド拒否メソッドと資格情報の所在する層
ターゲットの確立CONNECTが2xx以外、SOCKS REP失敗プロキシポリシー、ターゲット解決、出口
内側のTLS名前またはチェーンの失敗実際のターゲットの身元と信頼材料
ビジネスHTTP401、403、5xxターゲットサービスの認証とビジネス処理
UDPパスTCPは成功したがUDPは応答なし関連付け、relay到達可能性、カプセル化とアプリケーションサポート

システムプロキシ、環境変数、アプリケーション内設定は互いに上書きされない可能性があります。プロキシチェーン内でも、異なる認証と解決の位置がある場合があります。ルートとエラーの段階を分けることで、「すべての問題をDNSを変えて解決する」や「すべてのトラフィックがすでに乗っ取られている」といった誤判断を避けることができます。

継続的な参照

  • 透過プロキシとTUN:アプリケーションに明示的なプロキシ設定がない場合に、トラフィックをどのように乗っ取るか。
  • プロトコル比較:転送とセキュリティの境界に基づいてソリューションを比較する。
  • リバースプロキシ:サーバー側のエントリポイントと接続の境界。