13.8 Guarding Against Creating Too Many Network Sockets
13.8.1 Problem
You need to limit the number of network
sockets that your program can create.
13.8.2 Solution
Limiting the number of sockets that can be created in an application
is a good way to mitigate potential denial of service attacks by
preventing an attacker from creating too many open sockets for your
program to be able to handle. Imposing a limit on sockets is a simple
matter of maintaining a count of the number of sockets that have been
created so far. To do this, you will need to appropriately wrap three
socket functions. The first two functions that need to be wrapped,
socket( ) and accept( ), are
used to obtain new socket descriptors, and they should be modified to
increment the number of sockets when they're
successful. The third function, close( )
(closesocket( ) on Windows), is used to dispose of
an existing socket descriptor, and it should be modified to decrement
the number of sockets when it's successful.
13.8.3 Discussion
To limit the number of sockets that can be created, the first step is
to call spc_socketpool_init(
) to initialize the socket pool code. On Unix,
this does nothing, but it is required on Windows to initialize two
synchronization objects. Once the socket pool code is initialized,
the next step is to call spc_socketpool_setlimit(
) with the maximum number of sockets to allow.
In our implementation, any limit less than or equal to zero disables
limiting sockets but causes them still to be counted. We have written
the code to be thread-safe and to allow the wrapped functions to
block when no sockets are available. If the limit is adjusted to
allow more sockets when the old limit has already been reached, we
cause all threads waiting for sockets to be awakened by signaling a
condition object using pthread_cond_broadcast(
) on Unix or PulseEvent(
) on
Windows.
#include <errno.h>
#include <sys/types.h>
#ifndef WIN32
#include <sys/socket.h>
#include <pthread.h>
#else
#include <windows.h>
#include <winsock.h>
#endif
#ifndef WIN32
#define SPC_ACQUIRE_MUTEX(mtx) pthread_mutex_lock(&(mtx))
#define SPC_RELEASE_MUTEX(mtx) pthread_mutex_unlock(&(mtx))
#define SPC_CREATE_COND(cond) (!pthread_cond_init(&(cond), 0))
#define SPC_DESTROY_COND(cond) pthread_cond_destroy(&(cond))
#define SPC_SIGNAL_COND(cond) pthread_cond_signal(&(cond))
#define SPC_BROADCAST_COND(cond) pthread_cond_broadcast(&(cond))
#define SPC_WAIT_COND(cond, mtx) pthread_cond_wait(&(cond), &(mtx))
#define SPC_CLEANUP_PUSH(func, arg) pthread_cleanup_push(func, arg)
#define SPC_CLEANUP_POP(exec) pthread_cleanup_pop(exec)
#define closesocket(sock) close((sock))
#define SOCKET_ERROR -1
#else
#define SPC_ACQUIRE_MUTEX(mtx) WaitForSingleObjectEx((mtx), INFINITE, FALSE)
#define SPC_RELEASE_MUTEX(mtx) ReleaseMutex((mtx))
#define SPC_CREATE_COND(cond) ((cond) = CreateEvent(0, TRUE, FALSE, 0))
#define SPC_DESTROY_COND(cond) CloseHandle((cond))
#define SPC_SIGNAL_COND(cond) SetEvent((cond))
#define SPC_BROADCAST_COND(cond) PulseEvent((cond))
#define SPC_WAIT_COND(cond, mtx) spc_win32_wait_cond((cond), (mtx))
#define SPC_CLEANUP_PUSH(func, arg) { void (*_ _spc_func)(void *) = func; \
void *_ _spc_arg = arg;
#define SPC_CLEANUP_POP(exec) if ((exec)) _ _spc_func(_ _spc_arg); } \
do { } while (0)
#endif
static int socketpool_used = 0;
static int socketpool_limit = 0;
#ifndef WIN32
static pthread_cond_t socketpool_cond = PTHREAD_COND_INITIALIZER;
static pthread_mutex_t socketpool_mutex = PTHREAD_MUTEX_INITIALIZER;
#else
static HANDLE socketpool_cond, socketpool_mutex;
#endif
#ifdef WIN32
static void spc_win32_wait_cond(HANDLE cond, HANDLE mutex) {
HANDLE handles[2];
handles[0] = cond;
handles[1] = mutex;
ResetEvent(cond);
ReleaseMutex(mutex);
WaitForMultipleObjectsEx(2, handles, TRUE, INFINITE, FALSE);
}
#endif
int spc_socketpool_init(void) {
#ifdef WIN32
if (!SPC_CREATE_COND(socketpool_cond)) return 0;
if (!(socketpool_mutex = CreateMutex(0, FALSE, 0))) {
CloseHandle(socketpool_cond);
return 0;
}
#endif
return 1;
}
int spc_socketpool_setlimit(int limit) {
SPC_ACQUIRE_MUTEX(socketpool_mutex);
if (socketpool_limit > 0 && socketpool_used >= socketpool_limit) {
if (limit <= 0 || limit > socketpool_limit)
SPC_BROADCAST_COND(socketpool_cond);
}
socketpool_limit = limit;
SPC_RELEASE_MUTEX(socketpool_mutex);
return 1;
}
The wrappers for the accept( ) and
socket( ) calls are very similar, and they really
differ only in the arguments they accept. Our wrappers add an extra
argument that indicates whether the functions should wait for a
socket to become available if one is not immediately available. Any
nonzero value will cause the functions to wait until a socket becomes
available. A value of zero will cause the functions to return
immediately with errno set to
EMFILE if there are no available sockets. Should
the actual wrapped functions return any kind of error, the wrapper
functions will return that error immediately without incrementing the
socket count.
static void socketpool_cleanup(void *arg) {
SPC_RELEASE_MUTEX(socketpool_mutex);
}
int spc_socketpool_accept(int sd, struct sockaddr *addr, int *addrlen, int block) {
int avail = 1, new_sd = -1;
SPC_ACQUIRE_MUTEX(socketpool_mutex);
SPC_CLEANUP_PUSH(socketpool_cleanup, 0);
if (socketpool_limit > 0 && socketpool_used >= socketpool_limit) {
if (!block) {
avail = 0;
errno = EMFILE;
} else {
while (socketpool_limit > 0 && socketpool_used >= socketpool_limit)
SPC_WAIT_COND(socketpool_cond, socketpool_mutex);
}
}
if (avail && (new_sd = accept(sd, addr, addrlen)) != -1)
socketpool_used++;
SPC_CLEANUP_POP(1);
return new_sd;
}
int spc_socketpool_socket(int domain, int type, int protocol, int block) {
int avail = 1, new_sd = -1;
SPC_ACQUIRE_MUTEX(socketpool_mutex);
SPC_CLEANUP_PUSH(socketpool_cleanup, 0);
if (socketpool_limit > 0 && socketpool_used >= socketpool_limit) {
if (!block) {
avail = 0;
errno = EMFILE;
} else {
while (socketpool_limit > 0 && socketpool_used >= socketpool_limit)
SPC_WAIT_COND(socketpool_cond, socketpool_mutex);
}
}
if (avail && (new_sd = socket(domain, type, protocol)) != -1)
socketpool_used++;
SPC_CLEANUP_POP(1);
return new_sd;
}
When a socket that was obtained using spc_socketpool_accept(
) or spc_socketpool_socket( ) is no
longer needed, close it by calling spc_socketpool_close(
). Do not call spc_socketpool_close(
) with file or socket descriptors that were not obtained
from one of the wrapper functions; otherwise, the socket count will
become corrupted. This implementation does not keep a list of the
actual descriptors that have been allocated, so it is the
responsibility of the caller to do so. If a socket being closed makes
room for another socket to be created, the condition that the
accept( ) and socket( ) wrapper
functions wait on will be signaled.
int spc_socketpool_close(int sd) {
if (closesocket(sd) = = SOCKET_ERROR) return -1;
SPC_ACQUIRE_MUTEX(socketpool_mutex);
if (socketpool_limit > 0 && socketpool_used = = socketpool_limit)
SPC_SIGNAL_COND(socketpool_cond);
socketpool_used--;
SPC_RELEASE_MUTEX(socketpool_mutex);
return 0;
}
|