わいえむねっと

Contents
Categories
Calendar
2023/02
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28
Monthly Archives
~2000/01
Recent Entries
RSS1.0
Templates
Information
Processed: 0.116 sec
Chashed: -
日記まとめ/SO_EXCLUSIVEADDRUSE
SO_EXCLUSIVEADDRUSE について書き散らかした3年前の日記の内容の再検証およびまとめ。

経緯

2007/08/21

  • 事の発端
  • WindowsとLinuxで使用するソケットラッパーをつくっていた
  • コードは基本的に共通化してあり、どちらも SO_REUSEADDR を指定していた
  • Windowsでのみ hijack が発生
  • これはWindowsにおける SO_REUSEADDR の正常な挙動
  • Windowsでは SO_EXCLUSIVEADDRUSE を指定するよう修正

2007/08/23

  • 監視エージェントからプロセスを再起動された場合に、Windowsでのみ稀にbindに失敗
  • FIN_WAIT_2 のソケットが残っている場合の挙動がLinuxと異なっていた

2007/09/03

  • そもそもオプションを指定しない場合の挙動を確認していなかったので確認
  • 最終的にオプション指定なしで落ち着いた



検証コード

LISTEN および ESTABLISHED、FIN_WAIT2、TIME_WAIT のソケットがある状態での bind の挙動を確認します。
エラーコードを確認したいだけなので、後処理等は省略。

なお、もともとがプロセス再起動のタイミングで発生する問題に対する調査だったため、

  • 同じオプション指定
  • 同じアドレス指定
  • 同じユーザ

での bind が前提となっています。組み合わせが変わると挙動も変わってくるので注意。

サーバソケット

server.cpp
#ifdef WIN32
#include <winsock2.h>
#include <stdio.h>
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <stdio.h>
#include <unistd.h>

#define SOCKET_ERROR (-1)
#define INVALID_SOCKET (SOCKET)(~0)

typedef unsigned int SOCKET;
#endif

int main(int argc, char* argv[])
{
#ifdef WIN32
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);
#endif

    SOCKET s = socket(AF_INET, SOCK_STREAM, 0);

#ifdef _REUSEADDR
    int optval = 1;
    setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char*)&optval, sizeof(optval));
#endif
#ifdef _EXCLUSIVEADDRUSE
    int optval = 1;
    setsockopt(s, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, (char*)&optval, sizeof(optval));
#endif

    struct sockaddr_in sa;
    sa.sin_family = AF_INET;
    sa.sin_port = htons(60000);
#ifdef WIN32
    sa.sin_addr.S_un.S_addr = INADDR_ANY;
#else
    sa.sin_addr.s_addr = INADDR_ANY;
#endif
    if(bind(s, (const sockaddr*)&sa, sizeof(sa)) == SOCKET_ERROR)
    {
#ifdef WIN32
        printf("bind failed: %d\n", WSAGetLastError());
#else
        printf("bind failed: %d\n", errno);
#endif
        return -1;
    }
    else
    {
        printf("bind success\n");
        if(argc == 2)
        {
            return 0;
        }
    }

    if(listen(s, 2) == SOCKET_ERROR)
    {
#ifdef WIN32
        printf("listen failed: %d\n", WSAGetLastError());
#else
        printf("listen failed: %d\n", errno);
#endif
        return -1;
    }
    else
    {
        printf("listen success\n");
    }

    SOCKET t = accept(s, NULL, NULL);
    if(t == INVALID_SOCKET)
    {
#ifdef WIN32
        printf("accept failed: %d\n", WSAGetLastError());
#else
        printf("accept failed: %d\n", errno);
#endif
        return -1;
    }
    else
    {
        printf("accept success\n");
    }

#ifdef WIN32
    Sleep(2000);
    shutdown(t, SD_BOTH);
    closesocket(t);
    shutdown(s, SD_BOTH);
    closesocket(s);
#else
    sleep(2);
    shutdown(t, SHUT_RDWR);
    close(t);
    shutdown(s, SHUT_RDWR);
    close(s);
#endif
    printf("server close\n");

    return 0;
}


クライアントソケット

client.cpp
#ifdef WIN32
#include <winsock2.h>
#include <stdio.h>
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <stdio.h>
#include <unistd.h>

#define SOCKET_ERROR (-1)

typedef unsigned int SOCKET;
#endif

int main(int argc, char* argv[])
{
#ifdef WIN32
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);
#endif

    SOCKET s = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in sa;
    sa.sin_family = AF_INET;
    sa.sin_port = htons(60000);
#ifdef WIN32
    sa.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
#else
    sa.sin_addr.s_addr = inet_addr("127.0.0.1");
#endif
    if(connect(s, (const sockaddr*)&sa, sizeof(sa)) == SOCKET_ERROR)
    {
#ifdef WIN32
        printf("connect failed: %d\n", WSAGetLastError());
#else
        printf("connect failed: %d\n", errno);
#endif
        return -1;
    }
    else
    {
        printf("connect success\n");
    }

#ifdef WIN32
    Sleep(4000);
    shutdown(s, SD_BOTH);
    closesocket(s);
#else
    sleep(4);
    shutdown(s, SHUT_RDWR);
    close(s);
#endif
    printf("client close\n");

    return 0;
}


シナリオ

Windows

test.bat
@echo off
start /b server
sleep 1000

rem LISTEN
netstat -anp tcp|findstr 60000
start /b server --noblock

start /b client
sleep 1000

rem ESTABLISHED
netstat -anp tcp|findstr 60000
start /b server --noblock

sleep 2000

rem FIN_WAIT_2
netstat -anp tcp|findstr 60000
start /b server --noblock

sleep 2000

rem TIME_WAIT
netstat -anp tcp|findstr 60000
start /b server --noblock

  • sleep.exe は適当に用意

Linux

test.sh
./server &
sleep 1

# LISTEN
netstat -an --tcp|grep 60000
./server --noblock &

./client &
sleep 1

# ESTABLISHED
netstat -an --tcp|grep 60000
./server --noblock &

sleep 2

# FIN_WAIT_2
netstat -an --tcp|grep 60000
./server --noblock &

sleep 2

# TIME_WAIT
netstat -an --tcp|grep 60000
./server --noblock &



検証結果

Windows

  • Microsoft Windows XP SP3
  • Microsoft Visual C++ 2008 Express Edition

Default

bind success
listen success
  TCP    0.0.0.0:60000          0.0.0.0:0              LISTENING
bind failed: 10048
accept success
connect success
  TCP    0.0.0.0:60000          0.0.0.0:0              LISTENING
  TCP    127.0.0.1:2592         127.0.0.1:60000        ESTABLISHED
  TCP    127.0.0.1:60000        127.0.0.1:2592         ESTABLISHED
bind failed: 10048
server close
  TCP    127.0.0.1:2592         127.0.0.1:60000        CLOSE_WAIT
  TCP    127.0.0.1:60000        127.0.0.1:2592         FIN_WAIT_2
bind success
client close
  TCP    127.0.0.1:60000        127.0.0.1:2592         TIME_WAIT
bind success

SO_REUSEADDR

bind success
listen success
  TCP    0.0.0.0:60000          0.0.0.0:0              LISTENING
bind success
connect success
accept success
  TCP    0.0.0.0:60000          0.0.0.0:0              LISTENING
  TCP    127.0.0.1:2161         127.0.0.1:60000        ESTABLISHED
  TCP    127.0.0.1:60000        127.0.0.1:2161         ESTABLISHED
bind success
server close
  TCP    127.0.0.1:2161         127.0.0.1:60000        CLOSE_WAIT
  TCP    127.0.0.1:60000        127.0.0.1:2161         FIN_WAIT_2
bind success
client close
  TCP    127.0.0.1:60000        127.0.0.1:2161         TIME_WAIT
bind success

SO_EXCLUSIVEADDRUSE

bind success
listen success
  TCP    0.0.0.0:60000          0.0.0.0:0              LISTENING
bind failed: 10048
accept success
connect success
  TCP    0.0.0.0:60000          0.0.0.0:0              LISTENING
  TCP    127.0.0.1:2142         127.0.0.1:60000        ESTABLISHED
  TCP    127.0.0.1:60000        127.0.0.1:2142         ESTABLISHED
bind failed: 10048
server close
  TCP    127.0.0.1:2142         127.0.0.1:60000        CLOSE_WAIT
  TCP    127.0.0.1:60000        127.0.0.1:2142         FIN_WAIT_2
bind failed: 10048
client close
  TCP    127.0.0.1:60000        127.0.0.1:2142         TIME_WAIT
bind success

  • 前回の調査結果と相違なし
  • なお、当時使用していた開発環境は Microsoft Visual C++ .Net 2003


Linux (Fedora Core)

  • Fedora Core 6
  • Kernel 2.6.18
  • gcc 4.1.1

Default

bind success
listen success
tcp        0      0 0.0.0.0:60000               0.0.0.0:*                   LISTEN
bind failed: 98
accept success
connect success
tcp        0      0 0.0.0.0:60000               0.0.0.0:*                   LISTEN
tcp        0      0 127.0.0.1:60000             127.0.0.1:59817             ESTABLISHED
tcp        0      0 127.0.0.1:59817             127.0.0.1:60000             ESTABLISHED
bind failed: 98
server close
tcp        0      0 127.0.0.1:60000             127.0.0.1:59817             FIN_WAIT2
tcp        1      0 127.0.0.1:59817             127.0.0.1:60000             CLOSE_WAIT
bind failed: 98
client close
tcp        0      0 127.0.0.1:60000             127.0.0.1:59817             TIME_WAIT
bind failed: 98

SO_REUSEADDR

bind success
listen success
tcp        0      0 0.0.0.0:60000               0.0.0.0:*                   LISTEN
bind failed: 98
connect success
accept success
tcp        0      0 0.0.0.0:60000               0.0.0.0:*                   LISTEN
tcp        0      0 127.0.0.1:60356             127.0.0.1:60000             ESTABLISHED
tcp        0      0 127.0.0.1:60000             127.0.0.1:60356             ESTABLISHED
bind failed: 98
server close
tcp        1      0 127.0.0.1:60356             127.0.0.1:60000             CLOSE_WAIT
tcp        0      0 127.0.0.1:60000             127.0.0.1:60356             FIN_WAIT2
bind success
client close
tcp        0      0 127.0.0.1:60000             127.0.0.1:60356             TIME_WAIT
bind success

Linux (Red Hat)

  • Red Hat 8.0
  • Kernel 2.4.20
  • gcc 3.2

Default

bind success
listen success
tcp        0      0 0.0.0.0:60000           0.0.0.0:*               LISTEN
bind failed: 98
accept success
connect success
tcp        0      0 0.0.0.0:60000           0.0.0.0:*               LISTEN
tcp        0      0 127.0.0.1:60000         127.0.0.1:2364          ESTABLISHED
tcp        0      0 127.0.0.1:2364          127.0.0.1:60000         ESTABLISHED
bind failed: 98
server close
tcp        0      0 127.0.0.1:60000         127.0.0.1:2364          FIN_WAIT2
tcp        1      0 127.0.0.1:2364          127.0.0.1:60000         CLOSE_WAIT
bind failed: 98
client close
tcp        0      0 127.0.0.1:60000         127.0.0.1:2364          TIME_WAIT
bind failed: 98

SO_REUSEADDR

bind success
listen success
tcp        0      0 0.0.0.0:60000           0.0.0.0:*               LISTEN
bind failed: 98
accept success
connect success
tcp        0      0 0.0.0.0:60000           0.0.0.0:*               LISTEN
tcp        0      0 127.0.0.1:60000         127.0.0.1:2363          ESTABLISHED
tcp        0      0 127.0.0.1:2363          127.0.0.1:60000         ESTABLISHED
bind failed: 98
server close
tcp        0      0 127.0.0.1:60000         127.0.0.1:2363          FIN_WAIT2
tcp        1      0 127.0.0.1:2363          127.0.0.1:60000         CLOSE_WAIT
bind success
client close
tcp        0      0 127.0.0.1:60000         127.0.0.1:2363          TIME_WAIT
bind success

Linux (Ubuntu)

  • Ubuntu 8.04
  • Kernel 2.6.24
  • gcc 4.2.3

Default

bind success
listen success
tcp        0      0 0.0.0.0:60000           0.0.0.0:*               LISTEN     
bind failed: 98
accept success
connect success
tcp        0      0 0.0.0.0:60000           0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:60000         127.0.0.1:46401         ESTABLISHED
tcp        0      0 127.0.0.1:46401         127.0.0.1:60000         ESTABLISHED
bind failed: 98
server close
tcp        0      0 127.0.0.1:60000         127.0.0.1:46401         FIN_WAIT2  
tcp        1      0 127.0.0.1:46401         127.0.0.1:60000         CLOSE_WAIT 
bind failed: 98
client close
tcp        0      0 127.0.0.1:60000         127.0.0.1:46401         TIME_WAIT  
bind failed: 98

SO_REUSEADDR

bind success
listen success
tcp        0      0 0.0.0.0:60000           0.0.0.0:*               LISTEN     
bind failed: 98
accept success
connect success
tcp        0      0 0.0.0.0:60000           0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:60000         127.0.0.1:46400         ESTABLISHED
tcp        0      0 127.0.0.1:46400         127.0.0.1:60000         ESTABLISHED
bind failed: 98
server close
tcp        0      0 127.0.0.1:60000         127.0.0.1:46400         FIN_WAIT2  
tcp        1      0 127.0.0.1:46400         127.0.0.1:60000         CLOSE_WAIT 
bind success
client close
tcp        0      0 127.0.0.1:60000         127.0.0.1:46400         TIME_WAIT  
bind success

  • 前回の調査結果と相違なし
  • なお、当時使用していたディストリビューションは、Red Hat Enterprise Linux 4
  • FreeBSD 6.2 でも同様の結果
  • netinet/in.h のinclude を追加
  • errno は 48(EADDRINUSE)


結果一覧


LISTEN
-
-
LISTEN
ESTABLISHED
ESTABLISHED
-
CLOSE_WAIT
FIN_WAIT_2
-
-
TIME_WAIT
Windows DefaultWSAEADDRINUSEWSAEADDRINUSESuccessSuccess
Windows SO_REUSEADDRSuccessSuccessSuccessSuccess
Windows SO_EXCLUSIVEADDRUSEWSAEADDRINUSEWSAEADDRINUSEWSAEADDRINUSESuccess
Linux DefaultEADDRINUSEEADDRINUSEEADDRINUSEEADDRINUSE
Linux SO_REUSEADDREADDRINUSEEADDRINUSESuccessSuccess

SO_EXCLUSIVEADDRUSE のこのあたりの挙動は以下に明記されています。

Using SO_REUSEADDR and SO_EXCLUSIVEADDRUSE (Windows)
http://msdn.micro​soft.com/en-us/library/ms740621(VS.85).aspx
There is an important caveat to be aware of when setting SO_EXCLUSIVEADDRUSE: if at least one connection that originated from or was accepted by a port bound with SO_EXCLUSIVEADDRUSE set is still active on that port, then all subsequent bind attempts to that port will fail.

Likewise, an "active" TCP port is a port that is currently in one of the following states: ESTABLISHED, FIN_WAIT, FIN_WAIT_2, or LAST_ACK.

Conversely, a socket with the SO_EXCLUSIVEADDRUSE set cannot necessarily be reused immediately after socket closure. For example, if a listening socket with SO_EXCLUSIVEADDRUSE set accepts a connection and is then subsequently closed, another socket (also with SO_EXCLUSIVEADDRUSE) cannot bind to the same port as the first socket until the original connection becomes inactive.


そもそも SO_EXCLUSIVEADDRUSE は

Using SO_EXCLUSIVEADDRUSE (Windows)
http://msdn.micro​soft.com/en-us/library/ms740618(VS.85).aspx
The SO_EXCLUSIVEADDRUSE option prevents other sockets from being forcibly bound to the same address and port, a practice enabled by the SO_REUSEADDR option; such reuse can be executed by malicious applications to disrupt the application.

とあるように、セキュリティを目的としたオプションです。動作が厳格になります。
SO_REUSEADDR とは真逆の意味を持つオプションなので、ご利用は計画的に。