ソケットオプションで SO_EXCLUSIVEADDRUSE を使う

よく問い合わせを受けるネタなので、書いておこうと思います。
Windows Server 2003 以降は、エンハンスト ソケット セキュリティが実装されています。
ソケットオプションに SO_EXCLUSIVEADDRUSE (ソケットを排他アクセスにする)を使う際には
必ず知っておかなければならないことです。

少し具体的な話しをします。
例として、以下の簡単なサンプルコードを考えてみます。(エラー処理は省略)

int _tmain(int argc, _TCHAR* argv[])
{
    SOCKET sdf1, sdf2;
    struct sockaddr_in addrin;
    int rc;

    WSADATA wsa_data;
    WORD wVersionRequested = MAKEWORD(2, 2);
    rc = WSAStartup(wVersionRequested, (WSADATA *)&wsa_data);
    sdf1 = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    //int optval = 1;
    //setsockopt(sdf1, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, (char*)&optval, sizeof(optval));
    addrin.sin_addr.s_addr = inet_addr("127.0.0.1");
    addrin.sin_port = htons(55001);
    addrin.sin_family = AF_INET;
    rc = bind(sdf1, (SOCKADDR*)&addrin, sizeof(addrin));

    sdf2 = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    addrin.sin_addr.s_addr = htonl(INADDR_ANY);
    //addrin.sin_addr.s_addr = inet_addr("127.0.0.1");
    addrin.sin_port = htons(55001);
    addrin.sin_family = AF_INET;
    rc = bind(sdf2, (SOCKADDR*)addrin, sizeof(addrin));

    printf("port no 55001 binded\n");
    printf("netstat -ao execute\n");

    WSACleanup();
    getchar();

    return 0;
}

上記のURL 「Using SO_REUSEADDR and SO_EXCLUSIVEADDRUSE」内で「using SO_EXCLUSIVEADDRUSE」に
ある表を見てください。
エンハンスト ソケット セキュリティ以前の動作、つまり、XP 以前はどうなるかというと
First bind call ですが、sfd1 のソケットオプションは無いので、Default です。
sfd1は、ループバックアドレス inet_addr("127.0.0.1") で bind() しているので、Specific です。

次に、Second bind call ですが、sfd2 のソケットオプションは無いので、Default です。
sfd2 は、ワイルドカードアドレス htonl(INADDR_ANY) で bind() しているので、Wildcard です。

この組み合わせを見ると、INUSE (WSAEADDRINUSE:10048)になります。

次にエンハンスト ソケット セキュリティ以降の動作、2003 以降を見ます。
「Using SO_REUSEADDR and SO_EXCLUSIVEADDRUSE」の「Enhanced Socket Security」にある表を見てください。

同じ見方をすると、default で

First bind call => Specific
Second bind call => Wildcard

の組み合わせは、Success です。つまり、bind() は成功すると言っています。

SO_EXCLUSIVEADDRUSE オプションを使う場合にも、これと同じような見方をしてください。

ソケットオプションで SO_EXCLUSIVEADDRUSE を設定する場合には

  • ソケットオプションの見直し
    排他ソケットとしてTCP シーケンスを見直す
  • 使用する IP アドレスの見直し
    bind() するアドレスは再利用するのか/しないのか、また、bind() するタイミングに問題は無いか

をきちんと行わないと、bind() に際に WSAEADDRINUSE や WSAEACCES が発生します。
SO_EXCLUSIVEADDRUSE の指定は使用中のポートとの重複バインド抑止を目的にするものですが、
その際、注意しないと bind() で WSAEACCES が発生します。
この原因の多くは、TCP のシーケンス上で、まだ生きているポートに対して bind() しているだけです。
つまり、bind() のアドレス指定や TCP シーケンスの終了待ち前に処理をしてしまったことにあります。

ソケットは、closesocket() しても、すぐに完全にソケットクローズするわけではありません。
TCP のシーケンスに従い、FIN_WAIT1、FIN_WAIT2、LAST_ACK を経由したのちにクローズします。

つまり、SO_EXCLUSIVEADDRUSE オプションがついているソケットに対して、コード上で closesocket() した後、
すぐに socket()、bind() しても、まだ、完全に閉じられていない場合があり、そのポートに対して bind() すると
WSAEACCES が発生します。

以下が基本的な考え方です。

  • 同一マシン内の同じポートに対してバインドするときには、バインドアドレスを合わせ、ソケットオプションで動作を決める
    => つまり、異なるバインドアドレスを使わない
  • 対象はあくまでもポートということを忘れない。
    => 同じプロセス内だろうと、別プロセス間だろうと考え方は同じ

この辺を考慮したソケットプログラミングを行うとよいと思います。

なお、Windows Vista 以降では、エフェメラルポート の番号も変更されていますので注意してください。

Windows Vista より前の OS : 1025 ~ 5000
Windows Vista 以降の OS : 49152 ~ 65535

1024 ~ 49151 は IANA に登録が必要になります。

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中

%d人のブロガーが「いいね」をつけました。