========================================================= poenyでのWinny2ファイル共有互換プロトコルの実装メモ (1) 2006.03.21 ========================================================= ・問題や錯誤が含まれている可能性あり ・自己の良心に従って使用のこと ・無保証自己責任 ----- poenyでのWinny2ファイル共有互換プロトコルについて書いていきます。 間違い指摘や、その仕様変じゃない!?というつっこみは歓迎。 「日本語が変」や、「意味が分からん」なども反省のため聞きます。 5回くらいに分けて書こうと思っています。 プロトコルには、通信レベルでの話とアプリケーションレベルでの話があります。 □ 0x01 定義 この文章内での「Winny」とは、Winny2b7.1のファイル共有部分のことを指します。 この文章で紹介されるデータ構造のデータ型には次のものがあります。 ======================================================================== データ型名 | バイト長 | 内容 ======================================================================== uint | 4 | Cのuint32_t型 リトルエンディアン ------------------------------------------------------------------------ ushort | 2 | Cのuint16_t型 リトルエンディアン ------------------------------------------------------------------------ ubyte | 1 | Cのuint8_t型 ------------------------------------------------------------------------ float | 4 | Cのfloat型 ------------------------------------------------------------------------ string | 可変長 | null terminateされていない文字列 ------------------------------------------------------------------------ C string | 可変長 | null terminateされているCの文字列 ------------------------------------------------------------------------ ■0x02.) Winny通信方式の基礎知識 1. 非同期通信  Winnyのコマンド送受信は、各ノード間で非同期に行われます。ここでいう「非同期」とは、要求に対して応答がいつ返ってくるか分からないという意味での非同期です。たとえば、"ポエム"というキーワードで検索クエリを送信したとしても、その結果応答が即座に返るか、2秒後に返るか、10分後に返るか、返ってこないか、もしくは全く関係のない他ノード通知コマンドが返ってくるか分からないのです。よって、Winnyと通信をする場合は、応答待ち状態にならずに要求と応答を並行して別々に処理できる仕組みを作る必要があります。 2. 暗号方式  Winnyの通信暗号は、RC4という対称鍵方式のストリーム暗号で行われます。1バイト単位で順序ある暗号化を行い、暗号化を行うたびに暗号関数の内部状態が変化するため、接続開始時から1バイトでも欠けると復号化できなくなります。パケット受信のタイムアウトで中途半端に受信したパケットを復号化せずに読み捨てるようなことを行うと、次から復号化できなくなるため、ソケットのI/Oにできるだけ近いレベルで暗号化/復号化を行うのがよい方法です。  また、Winnyでは暗号鍵をC言語の文字列として扱っているため、鍵長以内であってもバイナリ0が含まれると、そこまでが鍵となります。鍵の先頭バイトがバイナリ0の場合は、バイナリ0の1バイトが鍵となります。 例: 入力鍵: 303132003334h -> 鍵: 303132h 入力鍵: 003132003334h -> 鍵: 00h 資料: poenyのRC暗号クラス: -------- class RC4 { protected: uint m_x; uint m_y; ubyte m_state[]; public: this(ubyte[] rc4_key) { m_state = new ubyte[256]; key(rc4_key); } this(){ m_state = new ubyte[256]; } void key(ubyte[] key) { uint u, i; uint keyindex; uint stateindex; // Winny用に0までをキーとする for (i = 0; i < key.length; ++i) { if (key[i] == 0) { if (i == 0) { key.length = 1; } else { key.length = i; } break; } } m_x = 0; m_y = 0; for (i = 0; i < m_state.length; ++i) { m_state[i] = i; } if (key.length == 0) { return; } keyindex = 0; stateindex = 0; for (i = 0; i < m_state.length; ++i) { stateindex = (stateindex + key[keyindex] + m_state[i]) & 0xff; u = m_state[stateindex]; m_state[stateindex] = m_state[i]; m_state[i] = u; if (++keyindex >= key.length) { keyindex = 0; } } } ubyte[] crypt(ubyte[] src) { ubyte[] dest = src; for (int i = 0; i < src.length; i++) { uint x; uint y; uint sx, sy; x = (m_x + 1) & 0xff; sx = m_state[x]; y = (sx + m_y) & 0xff; sy = m_state[y]; m_x = x; m_y = y; m_state[y] = sx; m_state[x] = sy; dest[i] = src[i] ^ m_state[(sx + sy) & 0xff]; } return dest; } static ubyte[] crypt(ubyte[] key, ubyte[] src) { auto RC4 rc4 = new RC4(key); return rc4.crypt(src); } } -------- 3. バイトオーダー  Winnyコマンドに含まれる2バイトまたは4バイトの数値は、基本的にリトルエンディアンです。x86マシンなら、C言語のuint32_t型, uint16_t型と同じデータ構造となります。float型に関しても同様です。  IPアドレス情報だけが、ビッグエンディアンです。(数値とは別のデータ構造と考えるの正しい) 4. 日本語文字コード  Shift_JISコードです。 ■0x03.) Winny通信プロトコル 1. プロトコル認証  Winnyは、接続開始時にプロトコル認証を行い、通信暗号鍵、バージョン情報、コネクションの種別、自ノード情報の交換を行います。  その後に、コネクションの種別によって、各コマンドを使って処理を開始します。  まず、通信開始時に行う鍵交換およびバージョン情報交換について説明します。  TCP接続を確立したWinnyは、暗号鍵情報、バージョン限定切断要求、Winnyプロトコルヘッダを順番に(パケット上は1つにして)送信します。バージョン限定切断要求、Winnyプロトコルヘッダは、先頭の暗号鍵情報で既に暗号化されています。暗号鍵はWinnyプロトコルヘッダを送信した後に一度スクランブルされます。  各データ構造は次のようになります。 - 暗号鍵情報 ======================================================================== 列名 | バイト長 | データ型 | 内容 ======================================================================== 乱数(ダミー) | 2 | ubyte[2] | ダミーの乱数。未使用 ------------------------------------------------------------------------ 通信暗号鍵 | 4 | ubyte[4] | 通信(送信)に使うRC4の暗号鍵 ------------------------------------------------------------------------ - コマンド97 バージョン限定切断要求 ======================================================================== 列名 | バイト長 |データ型 | 内容 ======================================================================== コマンド長 | 4 | uint | 以降のバイト数 (1固定) ------------------------------------------------------------------------ コマンドコード | 1 | ubyte | 97 ------------------------------------------------------------------------ 実装メモ:  Winnyは、旧バージョンとの接続を切断するために、Winnyプロトコルヘッダの直前に、コマンド97を送信します。Winny2b7.1ではこれを無視します。poenyも同様に無視します。 - コマンド00 Winnyプロトコルヘッダ ======================================================================== 列名 | バイト長 | データ型 | 内容 ======================================================================== コマンド長 | 4 | uint | 以降のバイト長 (可変) ------------------------------------------------------------------------ コマンドコード | 1 | ubyte | 0 ------------------------------------------------------------------------ バージョン情報 | 4 | uint | 12710 ------------------------------------------------------------------------ プロトコル認証 | 可変長 | string | アプリケーション名 文字列 | | | Winny: "Winny Ver2.0b1 " | | | poeny: "Winny Ver2.0b1 (poeny)" ------------------------------------------------------------------------ 実装メモ:  バージョン情報は、Winny側のバージョン警告の要因になるのと、一定範囲外だと切断対象となるので、12710以外の値は使わないようにします。poenyの場合は、12710以外からの接続は全て拒否します。  プロトコル認証文字列は、先頭の"Winny Ver2.0b1"までは固定とします。これ以外だとWinnyは接続を拒否します。以降に続く文字列はチェックされていないため、poenyでは、poenyを区別できるように"(poeny)"を付加しています。  プロトコル認証文字列は、先頭の暗号鍵情報とは別のプロトコル認証用固定暗号鍵で多重に暗号化されています。暗号の順序は、平文→プロトコル認証用固定暗号→通信暗号となっています。この鍵は、Winnyが実行形式内部に固定で持っています。poenyのソースコード上では、次のように定義されています。 -------- static const ubyte[] CERT_KEY = [0x39, 0x38, 0x37, 0x38, 0x39, 0x61, 0x73, 0x6a]; --------  暗号鍵で上記2つのコマンドを暗号した後に、暗号鍵を一度スクランブルします。スクランブルは、交換した暗号鍵(4バイト: 0〜3)のうち1〜3バイト目に対して0x39でXOR演算を行います。 資料: poenyでの暗号鍵スクランブル関数 -------- ubyte[] toStep2Key(ubyte[] step1_key) { ubyte[] step2_key = step1_key.dup; for (int i = 1; i < step2_key.length; ++i) { step2_key[i] ^= 0x39; } return step2_key; } --------  以降の送信データは、このスクランブルした鍵で暗号化されます。スクランブル後の暗号鍵にバイナリ0が含まれる場合は、前述の鍵ルールが適用されることになります。 注釈:  プロトコル認証は、connect側、accept側、双方から同時に行われます。双方とも、自ノードのデータ送信用暗号鍵(通信相手から見るとデータ受信用暗号鍵)を生成し、互いに交換します。つまり、1つのTCPコネクションに対して、データ送信用、データ受信用の2つの通信暗号鍵が存在します。  プロトコル認証では、続いて、回線速度通知、コネクション種別通知、自ノード情報通知の3コマンドを順に送信します。BBSノードの場合は、さらにこれらに続いて、BBSポート番号通知コマンドを送信します。  各データ構造は次のようになります。 - コマンド02 回線速度の通知 ======================================================================== 列名 | バイト長 | データ型 | 内容 ======================================================================== コマンド長 | 4 | uint | 以降のバイト長 (5固定) ------------------------------------------------------------------------ コマンドコード | 1 | ubyte | 1 ------------------------------------------------------------------------ 回線速度 | 4 | float | 自ノードの回線速度 ------------------------------------------------------------------------ 実装メモ:  poenyでは、回線速度を、15KB/S、50KB/S、120KB/S、500KB/S、1000KB/Sに限定しています。Winnyのデフォルト選択肢では、500KB/Sがありませんが、実際のWinnyネットワークには、200KB/S、300KB/S、400KB/S、500KB/Sなどのノードが多くいたため、poenyでは、120KB/S以上、1000KB/S未満のノードがいることを想定しています。 - コマンド02 コネクションタイプの通知 ======================================================================== 列名 | バイト長 | データ型 | 内容 ======================================================================== コマンド長 | 4 | uint | 以降のバイト長 (5固定) ------------------------------------------------------------------------ コマンドコード | 1 | ubyte | 2 ------------------------------------------------------------------------ リンクの種別 | 1 | ubyte | 0: 検索リンク | | | 1: 転送リンク | | | 2: BBS検索リンク ------------------------------------------------------------------------ Port0フラグ | 1 | ubyte | 0: 非Port0 | | | 1: Port0 ------------------------------------------------------------------------ BadPort0フラグ| 1 | ubyte | 0: 通常接続 | | | 1: BadPort0判定のための逆接続 ------------------------------------------------------------------------ BBSリンクフラグ | 1 | ubyte | 0: ファイル共有リンク | | | 1: BBSリンク ------------------------------------------------------------------------ 注釈:  ファイル共有で使うコネクションには、大きく分けて3種類あります。ファイル検索用コネクション(検索リンク)、ファイル転送用コネクション(転送リンク)、接続テスト用コネクション(BadPort0判定用接続)です。接続テストは、Port0でないノードから検索リンクまたは転送リンクがはられた際に、ファイル転送のポートが開いているか確認するために転送リンクを使って行います。(リンク種別: 1, BadPort0フラグ:1) 実装メモ:  poenyからの接続テストは、Winnyプロトコルヘッダのみ送受信して切断します。これは今後、Winnyに合わせて修正する予定です。(bug) - コマンド03 自ノード情報の通知 ======================================================================== 列名 | バイト長 | データ型 | 内容 ======================================================================== コマンド長 | 4 | uint | 以降のバイト長 (可変) ------------------------------------------------------------------------ コマンドコード | 1 | ubyte | 3 ------------------------------------------------------------------------ IPアドレス | 4 | ubyte[4] | オクテット毎のIPアドレス | | | 192.168.0.11 → C0A8000Bh | | | ------------------------------------------------------------------------ ファイル共有 | 4 | uint | 検索リンク、転送リンク接続 ポート | | | 可能なポート | | | ------------------------------------------------------------------------ DDNS名文字列長 | 1 | ubyte | DDNS名のバイト長 ------------------------------------------------------------------------ クラスタリング | 1 | ubyte | クラスタリング文字列(1)の 文字列長(1) | | | バイト長 ------------------------------------------------------------------------ クラスタリング | 1 | ubyte | クラスタリング文字列(3)の 文字列長(2) | | | バイト長 ------------------------------------------------------------------------ クラスタリング | 1 | ubyte | クラスタリング文字列(4)の 文字列長(3) | | | バイト長 ------------------------------------------------------------------------ DDNS名 | 可変長 | string | DDNS名 ------------------------------------------------------------------------ クラスタリング | 可変長 | string | クラスタリング文字列(1) 文字列(1) | | | ------------------------------------------------------------------------ クラスタリング | 可変長 | string | クラスタリング文字列(2) 文字列(2) | | | ------------------------------------------------------------------------ クラスタリング | 可変長 | string | クラスタリング文字列(3) 文字列(3) | | | ------------------------------------------------------------------------ 注釈:  クラスタリング文字列長は、クラスタリング文字列部の長さです。クラスタリング文字列はShift_JISコードで、null terminateはされていません。 実装メモ:  poenyは、DDNSを使っていません。  LAN内のWinnyの場合、通知されるIPアドレスは、LAN内でのアドレスになっているため(例:192.168.0.1)接続された側で保存したり、他ノードに配る際にはグローバルアドレスへ書き換える必要があります。このNATノード判定は、接続元のIPアドレスと通知されたIPアドレスの比較で行います。違う場合が、NATノードとなります。 - コマンド05 BBSポート番号の通知 ======================================================================== 列名 | バイト長 | データ型 | 内容 ======================================================================== コマンド長 | 4 | uint | 以降のバイト長 (5固定) ------------------------------------------------------------------------ コマンドコード | 1 | ubyte | 5 ------------------------------------------------------------------------ BBSポート | 4 | uint | BBSポート ------------------------------------------------------------------------ 実装メモ:  poenyは、このコマンドを無視します。poenyから、このコマンドが送信されることもありません。 ---------------- 今日はここまで ---------------- 参考文献 : - 「Winnyの技術」 金子勇著 : - 「Winny2_src_noyounamono」製作:算譜職人