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;
}