SOCKSプロトコルを完全に理解する(SOCKS 5編)
以前の記事では、SOCKS 4について紹介しました。SOCKS 4は、TCP接続を代理転送するためのシンプルなプロトコルですが、現代的な用途で見るといくつかの制約があります。代表的なのは、次の2点です。
- 宛先をIPv4アドレスで指定する必要があり、ドメイン名をそのまま扱えない
- 認証やUDP転送を標準機能として持たない
この不足を補うために登場したのが、SOCKS 4aとSOCKS 5です。
SOCKS 4aは、SOCKS 4の設計をほぼ維持したまま、サーバー側の名前解決を可能にした拡張です。一方のSOCKS 5は、認証方式のネゴシエーション、IPv6、ドメイン名指定、UDP転送などを含む、より汎用的な設計になっています。
この記事では、まずSOCKS 4aが何を解決したのかを確認し、そのうえでSOCKS 5のTCP転送とUDP転送の仕組みを、パケットの中身を追いながら整理します。
SOCKS 4a
SOCKS 4aが解決した問題
SOCKS 4aは、SOCKS 4の「宛先をIPアドレスでしか指定できない」という制約を緩和するために設計されました。
まず、SOCKS 4のCONNECTリクエストを振り返ります。
| VN | CD | DSTPORT | DSTIP | USERID | NULL |
|---|---|---|---|---|---|
| 04 | 01 | 通信先ポート | 通信先IPv4アドレス | 任意 | 00 |
SOCKS 4では、接続先としてDSTIPにIPv4アドレスを入れる必要があります。したがって、クライアントがexample.comのようなドメイン名へ接続したい場合は、SOCKSサーバーに接続する前に、自分でDNS名前解決を済ませておかなければなりません。
しかし、実際にSOCKSプロキシを使う場面では、クライアントが自由に外部DNSへ到達できないことがあります。特に、アプリケーション通信だけをSOCKSサーバー経由に逃がしたい構成では、DNSだけがローカルネットワークに残ってしまい、名前解決に失敗することがあります。
この問題に対し、SOCKS 4aは「宛先の名前解決をSOCKSサーバー側に委任する」仕組みを追加しました。
SOCKS 4aのリクエスト形式
SOCKS 4aでは、SOCKS 4との互換性を保ちながら、ドメイン名を追加で送信します。以下は、neverssl.com:80へ接続するリクエストの例です。
1 2 | |
このデータは、次のように解釈できます。
| フィールド | バイト数 | この例での値 | 説明 |
|---|---|---|---|
| VN | 1 | 04 |
バージョン番号。SOCKS 4aでも04を使用する |
| CD | 1 | 01 |
コマンド。01はCONNECT、02はBIND |
| DSTPORT | 2 | 00 50 |
接続先ポート。ここでは80番ポート |
| DSTIP | 4 | 00 00 00 01 |
SOCKS 4a特有の指定。先頭3オクテットを00にすることで、後続のドメイン名を解決してほしいことを示す |
| USERID | 可変長 | 空 | SOCKS 4と同様のユーザー名欄。認証用ではなく識別用 |
| NULL | 1 | 00 |
USERIDの終端 |
| DOMAIN | 可変長 | neverssl.comのASCII表記 |
接続先ドメイン名 |
| NULL | 1 | 00 |
DOMAINの終端 |
ここで重要なのは、DSTIPそのものに実際の宛先アドレスを入れていない点です。00 00 00 x(ただし x は非ゼロ)という特別な値を置き、後ろに続くDOMAINを見て名前解決してほしい、とSOCKSサーバーに伝えています。
サーバーはこのリクエストを受け取ると、neverssl.comを自分で名前解決し、その結果得られたIPアドレスに対してTCP接続を確立します。
SOCKS 4aの位置づけ
SOCKS 4aは、SOCKS 4の不足を小さな変更で補った実用的な拡張です。ただし、認証やUDP転送といった課題は解決していません。そのため、現在ではSOCKS 5のほうが広く使われています。
たとえばcurlはSOCKS 4aを扱えますが、利用するサーバー実装によってはSOCKS 4aをサポートしていない場合があります(dante-serverもSOCKS 4a未サポートです)。検証時は、クライアント側とサーバー側の両方の対応状況を確認する必要があります。
SOCKS 5
SOCKS 5で追加された機能
SOCKS 5では、SOCKS 4系と比べて主に次の機能が追加されています。
- 認証方式を通信開始時に選択できる
- 宛先としてIPv4、IPv6、ドメイン名を扱える
CONNECTとBINDに加え、UDP ASSOCIATEでUDP転送を扱える- ドメイン名指定を使うことで、サーバー側で名前解決させる構成を取れる
curlで見ると、名前解決を行う場所は次のように変わります。
1 | |
この場合は、通常クライアント側で名前解決したIPアドレスをSOCKSサーバーへ渡します。
1 | |
こちらは、ドメイン名をそのままSOCKSサーバーへ渡し、サーバー側で名前解決させます。
以降では、まずTCP通信の基本形として、認証なし、クライアント側名前解決、CONNECT利用というもっとも単純な流れを確認します。
TCP接続時の全体フロー
クライアントで次のようなコマンドを実行したとします。
1 | |
この通信をパケット単位で追うと、大きく次の2段階に分かれます。
- クライアントとSOCKSサーバーが、利用する認証方式を決める
- クライアントが接続したい宛先を指定し、SOCKSサーバーが代理でTCP接続する
以下は、その流れを簡略化した図です。ACKなど、ペイロードを持たないTCP制御パケットは省略しています。
sequenceDiagram
participant Client as クライアント
participant Socks as SOCKS 5サーバー
participant Server as ターゲットサーバー
rect rgb(130, 250, 250, 0.3)
Note over Client,Socks: 認証方式のネゴシエーション
Client-->Socks: 3-WAY Handshake
Client->>Socks: 05 02 00 01
Socks->>Client: 05 00
end
rect rgb(150, 130, 230, 0.3)
Note over Client,Socks: CONNECTリクエスト
Client->>Socks: 05 01 00 01 22 df 7c 2d 00 50
Socks-->Server: 3-WAY Handshake
Socks->>Client: 05 00 00 01 ac 1f 14 b6 19 c0
end
Client->>Socks: GET / HTTP/1.1
Socks->>Server: GET / HTTP/1.1
Server->>Socks: HTTP/1.1 200 OK
Socks->>Client: HTTP/1.1 200 OK
SOCKS 5は、HTTPそのものを解釈するプロトコルではありません。HTTPリクエストが流れ始める前段として、「どの認証方式を使うか」と「どこに接続したいか」をSOCKSサーバーへ伝え、その後は確立済みのTCPストリームを中継します。
SOCKS 5の認証方式ネゴシエーション
最初に、クライアントがSOCKSサーバーへ送信するのは次のデータです。
1 | |
この意味は次のとおりです。
| フィールド | バイト数 | この例での値 | 説明 |
|---|---|---|---|
| VER | 1 | 05 |
バージョン番号。SOCKS 5では05 |
| NMETHODS | 1 | 02 |
これから提示する認証方式の数 |
| METHODS | 1から255 | 00 01 |
利用可能な認証方式の一覧。00は認証なし、01はGSSAPI |
今回の例では、クライアントは「認証なし」と「GSSAPI」の2種類に対応している、と伝えています。
サーバーは、その中から実際に使う方式を1つ選び、次のデータを返します。
1 | |
| フィールド | バイト数 | この例での値 | 説明 |
|---|---|---|---|
| VER | 1 | 05 |
バージョン番号 |
| METHOD | 1 | 00 |
サーバーが選択した認証方式。ここでは認証なし |
この応答により、クライアントとサーバーは「今回は認証なしで続行する」と合意したことになります。
なお、ユーザー名・パスワード認証を使う場合や、GSSAPIを使う場合は、この後に追加の認証メッセージが続きます。認証方式の選択と、実際の認証処理は別フェーズです。
SOCKS 5のCONNECTリクエスト
認証方式が決まったら、クライアントは接続先を指定します。今回の例では、次のデータを送信しています。
1 | |
| フィールド | バイト数 | この例での値 | 説明 |
|---|---|---|---|
| VER | 1 | 05 |
バージョン番号 |
| CMD | 1 | 01 |
コマンド。01はCONNECT、02はBIND、03はUDP ASSOCIATE |
| RSV | 1 | 00 |
予約フィールド。00固定 |
| ATYP | 1 | 01 |
宛先アドレスの種類。01はIPv4、03はドメイン名、04はIPv6 |
| DST.ADDR | 可変長 | 22 df 7c 2d |
宛先アドレス。今回はIPv4なので4バイト |
| DST.PORT | 2 | 00 50 |
宛先ポート。ここでは80番ポート |
このリクエストは、「IPv4アドレス22 df 7c 2dの80番ポートへTCP接続してほしい」という意味です。
サーバーは接続処理を実行し、その結果を次の形式で返します。
1 | |
| フィールド | バイト数 | この例での値 | 説明 |
|---|---|---|---|
| VER | 1 | 05 |
バージョン番号 |
| REP | 1 | 00 |
結果コード。00は成功 |
| RSV | 1 | 00 |
予約フィールド。00固定 |
| ATYP | 1 | 01 |
後続アドレスの種類。今回はIPv4 |
| BND.ADDR | 可変長 | ac 1f 14 b6 |
SOCKSサーバーがターゲットへ接続する際に利用するローカル側アドレス |
| BND.PORT | 2 | 19 c0 |
SOCKSサーバーがターゲットへ接続する際に利用するローカル側ポート |
ここで返ってくるBND.ADDRとBND.PORTは、「SOCKSサーバーがターゲットへ接続するために確保したソケット情報」です。
大事なのは、REPが00であれば接続に成功しており、この応答以降はクライアントが送ったアプリケーションデータがSOCKSサーバー経由でターゲットへ転送される、という点です。
TCP接続における名前解決の違い
先ほどはクライアント側で名前解決したIPv4アドレスを送りました。そのため、ATYPは01でした。
一方、サーバー側で名前解決したい場合は、ATYPに03を使い、DST.ADDRへドメイン名を入れます。これが、curlでsocks5h://を指定した場合の考え方です。
整理すると、次の違いがあります。
curlでの指定方法 |
名前解決を行う場所 | SOCKSリクエストの宛先形式 |
|---|---|---|
socks5:// |
クライアント側 | IPv4またはIPv6アドレス |
socks5h:// |
SOCKSサーバー側 | ドメイン名 |
DNSリークを避けたい場合や、クライアントからDNSへ直接到達できない場合は、サーバー側名前解決を利用する構成が有効です。
SOCKS 5のUDP転送
UDP転送の全体像
SOCKS 5はUDP転送にも対応しています。UDP転送では、TCPのCONNECTとは異なり、次の流れになります。
- クライアントはまずTCPでSOCKSサーバーへ接続する
- TCP上で認証方式を決める
- TCP上で
UDP ASSOCIATEを送る - サーバーから、UDPリレー用の送信先アドレスとポートを受け取る
- 以降のUDPデータは、そのリレー先へSOCKS 5独自ヘッダー付きで送る
つまり、UDP転送であっても、最初の制御チャネルはTCPです。さらに、このTCP接続は単なる初期化用ではなく、UDPアソシエーションの寿命にも関係します。制御用TCP接続が閉じると、対応するUDP転送も終了します。
検証構成で注意したい点
今回の検証では、SOCKSサーバーとしてdante-serverを想定します。dante-serverはSOCKS 5をサポートしており、もちろんUDP転送に対応していますが、いくつかの制限事項があります。
-
クラウド環境では注意が必要です。
AWSでパブリックIPやElastic IPを利用する場合、実際にはNATによってIPアドレス変換が行われます。サーバー自身が認識しているアドレスと、外部から到達すべきアドレスが一致しません。
しかし
dante-serverは、自身が認識しているIPアドレスをクライアントへ応答してしまいます。インターネット上のクライアントがこのIPアドレスを使用して接続しようとしても、接続できません。検証を単純化するため、ここではクライアントをSOCKSサーバーと同じVPC内に置き、SOCKSサーバーのプライベートIPへ接続する前提にします。
-
セキュリティグループなどのファイアウォールの設定に注意が必要です。
SOCKSサーバーはUDPリレーを行う際に、待受ポートを確保し、クライアントからUDPパケットを受信します。
デフォルトではランダムのエフェメラルポートを使用するため、ファイアウォールで大きなポート範囲を開けておく必要があります。
/etc/danted.confにudp.portrangeオプションを記述することで、SOCKSクライアントとdante-server間で使用可能なUDPポート範囲を絞ることも可能ですが、利用可能なポート数が少なすぎると、同時接続するクライアントが多い場合にポート不足へ陥ることがあります。
UDPクライアント側の準備
クライアントには、dante-clientをインストールします。ここではUbuntuを例にします。
!!! note dante-client
dante-clientは、Ubuntuの公式リポジトリに含まれています。
1 | |
bash
sudo apt update
sudo apt install dante-client
dante-clientには、任意のアプリケーションの通信をSOCKS経由にするためのsocksifyコマンドが含まれています。
続いて、クライアント側の設定ファイルを作成します。
1 2 3 4 5 6 7 8 9 | |
ここで、p.p.p.pはSOCKSサーバーのプライベートIPアドレスです。
この設定は、「UDP通信を、認証なしのSOCKS 5 UDP ASSOCIATEで転送する」という意味になります。
簡易UDP Echoサーバーの構築
転送確認用として、受信したUDPデータに少し情報を付けて送り返すEchoサーバーを用意します。
Amazon EC2などでLinuxサーバーを起動し、socatを使って次のコマンドを実行します。socatが未インストールの場合は事前にインストールしてください。
1 2 | |
この例ではUDP 43333番ポートを使用します。OSファイアウォールやセキュリティグループでも、このポートへの通信を許可しておきます。
SOCKSを経由しない場合の確認
まずは、SOCKSサーバーを経由せず、UDP Echoサーバーへ直接送信します。dante-clientがインストールされているクライアントから、以下のコマンドを実行します。
1 | |
ここで、s.s.s.sはUDP EchoサーバーのIPアドレスです。
成功すると、次のような応答が返ります。
1 | |
client=の値には、Echoサーバーから見た送信元アドレスと送信元ポートが表示されます。インターネット越しの検証であれば、一般にはクライアント側のグローバルIPアドレスが見えます。
SOCKS経由でのUDP転送確認
次に、socksifyを使ってSOCKS経由で送信します。dante-clientがインストールされているクライアントから、以下のコマンドを実行します。
1 | |
成功すると、次のような応答になります。
1 | |
この場合、Echoサーバーから見える送信元は、実際にターゲットへUDPパケットを送ったSOCKSサーバー側のアドレスになります。インターネット越しの構成であれば、通常はSOCKSサーバー側のグローバルIPアドレスとして観測されます。
パケットキャプチャで見るUDP転送
UDP転送時の通信フロー
ここからは、tcpdumpなどで取得したパケットを元に、UDP転送の実際の流れを確認します。
sequenceDiagram
participant Client as クライアント
participant Socks as SOCKS 5サーバー
participant Server as ターゲットサーバー
rect rgb(130, 250, 250, 0.3)
Note over Client,Socks: 認証方式のネゴシエーション(TCP)
Client-->Socks: 3-WAY Handshake
Client->>Socks: 05 01 00
Socks->>Client: 05 00
end
rect rgb(150, 130, 230, 0.3)
Note over Client,Socks: UDP ASSOCIATE(TCP)
Client->>Socks: 05 03 00 01 ac 1f 23 11 91 86
Socks->>Client: 05 00 00 01 ac 1f 14 b6 86 8c
end
rect rgb(150, 130, 230, 0.3)
Note over Client,Socks: データ転送(UDP)
Client->>Socks: 00 00 00 01 12 b7 f2 8d a9 45 + 元データ
Socks->>Server: 元データ
Server->>Socks: リプライ
Socks->>Client: 00 00 00 01 12 b7 f2 8d a9 45 + リプライ
end
最初の05 01 00と05 00は、TCP接続時と同じ認証方式ネゴシエーションです。今回の例では、クライアントは「認証なし」の1種類だけを提示し、サーバーもそれを選択しています。
また、UDPの転送なのに、最初は必ずTCPを使用するのもポイントです。ファイアウォールの観点からすると、UDPしか転送しないSOCKSサーバーであっても、TCPポートを開けておく必要があります。
UDP ASSOCIATEリクエスト
ネゴシエーションが終わると、クライアントは引き続き次のデータを送信します。
1 | |
| フィールド | バイト数 | この例での値 | 説明 |
|---|---|---|---|
| VER | 1 | 05 |
バージョン番号 |
| CMD | 1 | 03 |
UDP ASSOCIATE |
| RSV | 1 | 00 |
予約フィールド。00固定 |
| ATYP | 1 | 01 |
アドレス種別。01はIPv4 |
| DST.ADDR | 可変長 | ac 1f 23 11 |
クライアントがUDP送信に使う予定のアドレス |
| DST.PORT | 2 | 91 86 |
クライアントがUDP送信に使う予定のポート |
UDP ASSOCIATEにおけるDST.ADDRとDST.PORTは、最終的なターゲットサーバーの情報ではありません。ここでは、クライアント自身がどのUDPソケットを使う予定かをSOCKSサーバーへ知らせています。
ただし、この情報を厳密に指定できない場合もあります。その場合は、クライアントが未定を示す値を入れ、実際に届いたUDPパケットの送信元情報をもとにサーバーが処理する構成もあります。
UDP ASSOCIATE応答
SOCKSサーバーは、次のような応答を返します。
1 | |
| フィールド | バイト数 | この例での値 | 説明 |
|---|---|---|---|
| VER | 1 | 05 |
バージョン番号 |
| REP | 1 | 00 |
結果コード。00は成功 |
| RSV | 1 | 00 |
予約フィールド。00固定 |
| ATYP | 1 | 01 |
アドレス種別。01はIPv4 |
| BND.ADDR | 可変長 | ac 1f 14 b6 |
クライアントがUDPデータを送るべきSOCKSサーバー側のアドレス |
| BND.PORT | 2 | 86 8c |
クライアントがUDPデータを送るべきSOCKSサーバー側のポート |
ここで返されるBND.ADDRとBND.PORTは、TCPのCONNECT応答とは意味合いが異なります。UDP転送では、クライアントはこのアドレスとポートに対して、以降のUDPデータを送信します。
つまり、この応答によって「クライアントがどこへUDPパケットを投げればSOCKSサーバーが転送してくれるのか」が確定します。
UDPデータグラムの形式
UDPデータ転送では、クライアントが送る本来のデータの前に、SOCKS 5独自のヘッダーを付けます。
今回の例では、次のようなパケットが送られています。
1 | |
| フィールド | バイト数 | この例での値 | 説明 |
|---|---|---|---|
| RSV | 2 | 00 00 |
予約フィールド。00 00固定 |
| FRAG | 1 | 00 |
フラグメント番号。今回は分割なし |
| ATYP | 1 | 01 |
アドレス種別。01はIPv4 |
| DST.ADDR | 可変長 | 12 b7 f2 8d |
最終的なターゲットサーバーのIPアドレス |
| DST.PORT | 2 | a9 45 |
最終的なターゲットサーバーのポート。16進表記で43333 |
| DATA | 可変長 | test socks proxyのASCII表記 |
本来送信したいUDPペイロード |
ここがTCP転送との大きな違いです。
TCPのCONNECTでは、SOCKSサーバーとの1本のTCP接続が、原則として1つの宛先接続に対応します。一方、UDP転送では、クライアントが送るUDPデータグラムごとにDST.ADDRとDST.PORTを付けます。したがって、UDPヘッダー上は、データグラム単位で転送先を指定できる構造になっています。
SOCKSサーバーはこのパケットを受け取ると、SOCKS 5ヘッダーを取り除き、DATA部分だけを実際のターゲットサーバーへ送ります。
応答パケットの形式
ターゲットサーバーから返信が返ってくると、SOCKSサーバーは再びSOCKS 5のUDPヘッダーを付けてクライアントへ返します。
| フィールド | バイト数 | この例での値 | 説明 |
|---|---|---|---|
| RSV | 2 | 00 00 |
予約フィールド。00 00固定 |
| FRAG | 1 | 00 |
フラグメント番号。今回は分割なし |
| ATYP | 1 | 01 |
アドレス種別。01はIPv4 |
| DST.ADDR | 可変長 | 12 b7 f2 8d |
実際には返信元となるターゲットサーバーのIPアドレス |
| DST.PORT | 2 | a9 45 |
実際には返信元となるターゲットサーバーのポート |
| DATA | 可変長 | time=...のASCII表記 |
ターゲットサーバーから返ってきたUDPペイロード |
表の項目名はリクエスト形式に合わせてDST.ADDR、DST.PORTと書かれますが、サーバーからクライアントへ戻る向きでは、「どのターゲットから返ってきたデータなのか」を示す情報として読むのが自然です。
まとめ
SOCKS 4aとSOCKS 5の違いを整理すると、次のようになります。
| プロトコル | 主な特徴 |
|---|---|
| SOCKS 4 | TCPの代理接続を扱うシンプルな方式 |
| SOCKS 4a | SOCKS 4にサーバー側名前解決を追加 |
| SOCKS 5 | 認証方式選択、IPv6、ドメイン名指定、UDP転送に対応 |
SOCKS 4aは、「名前解決をサーバー側で行いたい」という実務上の課題を小さな拡張で解決しました。SOCKS 5はそれをさらに進め、現代的な中継プロトコルとして必要な要素をまとめて備えています。
特にSOCKS 5のUDP転送は、次の3点を押さえると理解しやすくなります。
- UDP転送でも最初の制御はTCPで行う
- データグラム単位で転送先を指定可能
- 実データのUDPパケットには、SOCKS 5独自ヘッダーが付くので、TCP転送に比べてややオーバーヘッドがある
この3点を押さえると、パケットキャプチャ上の動きも追いやすくなります。