コンテンツにスキップ

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
0000   04 01 00 50 00 00 00 01 00 6e 65 76 65 72 73 73
0010   6c 2e 63 6f 6d 00

このデータは、次のように解釈できます。

フィールド バイト数 この例での値 説明
VN 1 04 バージョン番号。SOCKS 4aでも04を使用する
CD 1 01 コマンド。01CONNECT02BIND
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、ドメイン名を扱える
  • CONNECTBINDに加え、UDP ASSOCIATEでUDP転送を扱える
  • ドメイン名指定を使うことで、サーバー側で名前解決させる構成を取れる

curlで見ると、名前解決を行う場所は次のように変わります。

1
curl -x socks5://p.p.p.p:9580 http://neverssl.com

この場合は、通常クライアント側で名前解決したIPアドレスをSOCKSサーバーへ渡します。

1
curl -x socks5h://p.p.p.p:9580 http://neverssl.com

こちらは、ドメイン名をそのままSOCKSサーバーへ渡し、サーバー側で名前解決させます。

以降では、まずTCP通信の基本形として、認証なし、クライアント側名前解決、CONNECT利用というもっとも単純な流れを確認します。

TCP接続時の全体フロー

クライアントで次のようなコマンドを実行したとします。

1
curl -x socks5://p.p.p.p:9580 http://neverssl.com

この通信をパケット単位で追うと、大きく次の2段階に分かれます。

  1. クライアントとSOCKSサーバーが、利用する認証方式を決める
  2. クライアントが接続したい宛先を指定し、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
05 02 00 01

この意味は次のとおりです。

フィールド バイト数 この例での値 説明
VER 1 05 バージョン番号。SOCKS 5では05
NMETHODS 1 02 これから提示する認証方式の数
METHODS 1から255 00 01 利用可能な認証方式の一覧。00は認証なし、01はGSSAPI

今回の例では、クライアントは「認証なし」と「GSSAPI」の2種類に対応している、と伝えています。

サーバーは、その中から実際に使う方式を1つ選び、次のデータを返します。

1
05 00
フィールド バイト数 この例での値 説明
VER 1 05 バージョン番号
METHOD 1 00 サーバーが選択した認証方式。ここでは認証なし

この応答により、クライアントとサーバーは「今回は認証なしで続行する」と合意したことになります。

なお、ユーザー名・パスワード認証を使う場合や、GSSAPIを使う場合は、この後に追加の認証メッセージが続きます。認証方式の選択と、実際の認証処理は別フェーズです。

SOCKS 5のCONNECTリクエスト

認証方式が決まったら、クライアントは接続先を指定します。今回の例では、次のデータを送信しています。

1
05 01 00 01 22 df 7c 2d 00 50
フィールド バイト数 この例での値 説明
VER 1 05 バージョン番号
CMD 1 01 コマンド。01CONNECT02BIND03UDP 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
05 00 00 01 ac 1f 14 b6 19 c0
フィールド バイト数 この例での値 説明
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.ADDRBND.PORTは、「SOCKSサーバーがターゲットへ接続するために確保したソケット情報」です。

大事なのは、REP00であれば接続に成功しており、この応答以降はクライアントが送ったアプリケーションデータがSOCKSサーバー経由でターゲットへ転送される、という点です。

TCP接続における名前解決の違い

先ほどはクライアント側で名前解決したIPv4アドレスを送りました。そのため、ATYP01でした。

一方、サーバー側で名前解決したい場合は、ATYP03を使い、DST.ADDRへドメイン名を入れます。これが、curlsocks5h://を指定した場合の考え方です。

整理すると、次の違いがあります。

curlでの指定方法 名前解決を行う場所 SOCKSリクエストの宛先形式
socks5:// クライアント側 IPv4またはIPv6アドレス
socks5h:// SOCKSサーバー側 ドメイン名

DNSリークを避けたい場合や、クライアントからDNSへ直接到達できない場合は、サーバー側名前解決を利用する構成が有効です。

SOCKS 5のUDP転送

UDP転送の全体像

SOCKS 5はUDP転送にも対応しています。UDP転送では、TCPのCONNECTとは異なり、次の流れになります。

  1. クライアントはまずTCPでSOCKSサーバーへ接続する
  2. TCP上で認証方式を決める
  3. TCP上でUDP ASSOCIATEを送る
  4. サーバーから、UDPリレー用の送信先アドレスとポートを受け取る
  5. 以降のUDPデータは、そのリレー先へSOCKS 5独自ヘッダー付きで送る

つまり、UDP転送であっても、最初の制御チャネルはTCPです。さらに、このTCP接続は単なる初期化用ではなく、UDPアソシエーションの寿命にも関係します。制御用TCP接続が閉じると、対応するUDP転送も終了します。

検証構成で注意したい点

今回の検証では、SOCKSサーバーとしてdante-serverを想定します。dante-serverはSOCKS 5をサポートしており、もちろんUDP転送に対応していますが、いくつかの制限事項があります。

  1. クラウド環境では注意が必要です。

    AWSでパブリックIPやElastic IPを利用する場合、実際にはNATによってIPアドレス変換が行われます。サーバー自身が認識しているアドレスと、外部から到達すべきアドレスが一致しません。

    しかしdante-serverは、自身が認識しているIPアドレスをクライアントへ応答してしまいます。インターネット上のクライアントがこのIPアドレスを使用して接続しようとしても、接続できません。

    検証を単純化するため、ここではクライアントをSOCKSサーバーと同じVPC内に置き、SOCKSサーバーのプライベートIPへ接続する前提にします。

  2. セキュリティグループなどのファイアウォールの設定に注意が必要です。

    SOCKSサーバーはUDPリレーを行う際に、待受ポートを確保し、クライアントからUDPパケットを受信します。

    デフォルトではランダムのエフェメラルポートを使用するため、ファイアウォールで大きなポート範囲を開けておく必要があります。

    /etc/danted.confudp.portrangeオプションを記述することで、SOCKSクライアントと dante-server 間で使用可能なUDPポート範囲を絞ることも可能ですが、利用可能なポート数が少なすぎると、同時接続するクライアントが多い場合にポート不足へ陥ることがあります。

UDPクライアント側の準備

クライアントには、dante-clientをインストールします。ここではUbuntuを例にします。

!!! note dante-client dante-clientは、Ubuntuの公式リポジトリに含まれています。

1
Ubuntu以外のOSの場合、OSによってソースからビルドする必要があります。

bash sudo apt update sudo apt install dante-client

dante-clientには、任意のアプリケーションの通信をSOCKS経由にするためのsocksifyコマンドが含まれています。

続いて、クライアント側の設定ファイルを作成します。

1
2
3
4
5
6
7
8
9
cat > ~/socks.conf << EOF
route {
    from: 0.0.0.0/0 to: 0.0.0.0/0 via: p.p.p.p port = 9580
    proxyprotocol: socks_v5
    protocol: udp
    command: udpassociate
    method: none
}
EOF

ここで、p.p.p.pはSOCKSサーバーのプライベートIPアドレスです。

この設定は、「UDP通信を、認証なしのSOCKS 5 UDP ASSOCIATEで転送する」という意味になります。

簡易UDP Echoサーバーの構築

転送確認用として、受信したUDPデータに少し情報を付けて送り返すEchoサーバーを用意します。

Amazon EC2などでLinuxサーバーを起動し、socatを使って次のコマンドを実行します。socatが未インストールの場合は事前にインストールしてください。

1
2
socat -v UDP-LISTEN:43333,fork,reuseaddr \
SYSTEM:'read msg; echo "time=$(date -Is) client=$SOCAT_PEERADDR:$SOCAT_PEERPORT msg=$msg"'

この例ではUDP 43333番ポートを使用します。OSファイアウォールやセキュリティグループでも、このポートへの通信を許可しておきます。

SOCKSを経由しない場合の確認

まずは、SOCKSサーバーを経由せず、UDP Echoサーバーへ直接送信します。dante-clientがインストールされているクライアントから、以下のコマンドを実行します。

1
echo 'test echo server' | nc -u -w2 s.s.s.s 43333

ここで、s.s.s.sはUDP EchoサーバーのIPアドレスです。

成功すると、次のような応答が返ります。

1
time=2026-05-09T14:46:53+00:00 client=c.c.c.c:6606 msg=test echo server

client=の値には、Echoサーバーから見た送信元アドレスと送信元ポートが表示されます。インターネット越しの検証であれば、一般にはクライアント側のグローバルIPアドレスが見えます。

SOCKS経由でのUDP転送確認

次に、socksifyを使ってSOCKS経由で送信します。dante-clientがインストールされているクライアントから、以下のコマンドを実行します。

1
echo 'test socks proxy' | SOCKS_CONF=~/socks.conf socksify nc -u -w2 s.s.s.s 43333

成功すると、次のような応答になります。

1
time=2026-05-09T14:48:21+00:00 client=p.p.p.p:37254 msg=test socks proxy

この場合、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 0005 00は、TCP接続時と同じ認証方式ネゴシエーションです。今回の例では、クライアントは「認証なし」の1種類だけを提示し、サーバーもそれを選択しています。

また、UDPの転送なのに、最初は必ずTCPを使用するのもポイントです。ファイアウォールの観点からすると、UDPしか転送しないSOCKSサーバーであっても、TCPポートを開けておく必要があります。

UDP ASSOCIATEリクエスト

ネゴシエーションが終わると、クライアントは引き続き次のデータを送信します。

1
05 03 00 01 ac 1f 23 11 91 86
フィールド バイト数 この例での値 説明
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.ADDRDST.PORTは、最終的なターゲットサーバーの情報ではありません。ここでは、クライアント自身がどのUDPソケットを使う予定かをSOCKSサーバーへ知らせています。

ただし、この情報を厳密に指定できない場合もあります。その場合は、クライアントが未定を示す値を入れ、実際に届いたUDPパケットの送信元情報をもとにサーバーが処理する構成もあります。

UDP ASSOCIATE応答

SOCKSサーバーは、次のような応答を返します。

1
05 00 00 01 ac 1f 14 b6 86 8c
フィールド バイト数 この例での値 説明
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.ADDRBND.PORTは、TCPのCONNECT応答とは意味合いが異なります。UDP転送では、クライアントはこのアドレスとポートに対して、以降のUDPデータを送信します。

つまり、この応答によって「クライアントがどこへUDPパケットを投げればSOCKSサーバーが転送してくれるのか」が確定します。

UDPデータグラムの形式

UDPデータ転送では、クライアントが送る本来のデータの前に、SOCKS 5独自のヘッダーを付けます。

今回の例では、次のようなパケットが送られています。

1
00 00 00 01 12 b7 f2 8d a9 45 + 元データ
フィールド バイト数 この例での値 説明
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.ADDRDST.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.ADDRDST.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点を押さえると、パケットキャプチャ上の動きも追いやすくなります。