SO_EXCLUSIVEADDRUSE について書き散らかした3年前の日記の内容の再検証およびまとめ。
経緯
- WindowsとLinuxで使用するソケットラッパーをつくっていた
- コードは基本的に共通化してあり、どちらも SO_REUSEADDR を指定していた
- これはWindowsにおける SO_REUSEADDR の正常な挙動
- Windowsでは SO_EXCLUSIVEADDRUSE を指定するよう修正
- 監視エージェントからプロセスを再起動された場合に、Windowsでのみ稀にbindに失敗
- FIN_WAIT_2 のソケットが残っている場合の挙動がLinuxと異なっていた
- そもそもオプションを指定しない場合の挙動を確認していなかったので確認
検証コード
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
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
- netinet/in.h のinclude を追加
- errno は 48(EADDRINUSE)
結果一覧
| LISTEN - - | LISTEN ESTABLISHED ESTABLISHED | - CLOSE_WAIT FIN_WAIT_2 | - - TIME_WAIT |
Windows Default | WSAEADDRINUSE | WSAEADDRINUSE | Success | Success |
Windows SO_REUSEADDR | Success | Success | Success | Success |
Windows SO_EXCLUSIVEADDRUSE | WSAEADDRINUSE | WSAEADDRINUSE | WSAEADDRINUSE | Success |
Linux Default | EADDRINUSE | EADDRINUSE | EADDRINUSE | EADDRINUSE |
Linux SO_REUSEADDR | EADDRINUSE | EADDRINUSE | Success | Success |
SO_EXCLUSIVEADDRUSE のこのあたりの挙動は以下に明記されています。
Using SO_REUSEADDR and SO_EXCLUSIVEADDRUSE (Windows)
http://msdn.microsoft.com/en-us/library/ms740621(VS.85).aspxThere 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.microsoft.com/en-us/library/ms740618(VS.85).aspxThe 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 とは真逆の意味を持つオプションなので、ご利用は計画的に。