/************************************************************************
 *
 * "minirsyslogd.c" by <mikael.olsson@clavister.com>
 *
 *   Simplistic, fast and secure (through lack of bloat) remote syslog
 *   receiver suitable for hardened log receiver hosts and/or central log
 *   receivers that receive several gigabytes of logs each day.
 *
 *
 * TERMS OF USE ("no terms")
 *
 *   minirsyslogd is released into the public domain, and, as such, is
 *   provided "AS IS".  Do with it what you wish.  I'll happily consider
 *   adding relevant improvements to it, but be warned: I won't personally
 *   host anything called 'minirsyslogd' that grows to include a full-sized
 *   ruleset, etc...
 *
 *
 * VERSION HISTORY
 *
 * v1.02: 2003-10-30
 *   Initial public release.  Prior to this, it has been in operation 
 *   in various locations for over a year.
 *
 *   There never was a version 1.0. Paul Robertson added his two cents.
 *
 *
 * BASIC IDEA OF OPERATION
 *
 * - Receive inbound UDP syslog packets on a port of the user's choice.
 *   (Do NOT deal with local syslog sockets! This is remote only.)
 *
 * - Store in a structured way according to sender IP, e.g.:
 *   ./192.168.0.123/192.168.0.123-2002102407
 *   (The timestamp is YYYYMMDDHH, or YYYYMMDD with --split day)
 *
 * - Open files always close at the turn of the hour.
 *
 * - Do NOT create directories automatically.
 *   The existance/nonexistance of a destination directory is the
 *   Access Control List of the receiver.
 *
 * - If the open file table is full, a random file will be closed
 *   to allow a new one to be opened. This is better than it sounds;
 *   definitely a LOT better than "closing the oldest one".
 *
 * - Once an hour: Output a list IP addresses denied access.
 *   The size of this list is limited to 10 entries.
 *
 * - Do nothing else. KISS - Keep It Simple Stupid.
 *   Also, keep it fast.  The current implementation was about
 *   20 times faster than syslog-ng last time I measured.
 *
 *
 * FUTURE IMPROVEMENTS
 *
 * - Smarter error reporting for fopen() failures (did it fail because the
 *   destination directory didn't exist? any other reason?)
 *
 ************************************************************************/



/************************************************************************
 *
 * Tweakables..
 *
 ************************************************************************/


#define MAX_DESTS 1000      // Maximum number of concurrently open files
                            // (Further limited by what your OS supports)

#define DEST_HASH_BITS 12   // 1<<bits must be >= MAX_DESTS*3
                            // 1<<12 = 4096 .. 1<<13 = 8192 .. 1<<14 = 16384
                            // 1<<15 = 32768 .. 1<<16 = 65536 .. 1<<17 = 131072

#define MAX_FAILREPORTS 10  // Maximum number of "failed to open" reports
                            // per hour



/************************************************************************
 *
 * Includes and typedefs
 *
 ************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>   // varargs.h instead?
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/socket.h>
#include <errno.h>
#include <netdb.h>
#include <netinet/in.h>
#include <unistd.h>
#include <time.h>
#include <signal.h>
#include <fcntl.h>

#define VERSION_STRING "1.02"

typedef char *LPSTR;
typedef const char *LPCSTR;
typedef unsigned int DWORD;
typedef short unsigned int WORD;
typedef int BOOL;
typedef void * PVOID;
typedef unsigned char BYTE, *PBYTE;
#define __far

#ifndef FALSE
#define FALSE (0)
#define TRUE (!FALSE)
#endif

#define stricmp strcasecmp

#define mymin(a,b) ((a)<(b) ? (a) : (b))

/************************************************************************
 *
 * CONSTANTS
 *
 ************************************************************************/

// Syslog facilities / severities
enum {
  FACILITY_KERNEL = 0<<3,
  FACILITY_USER = 1<<3,
  FACILITY_MAIL = 2<<3,
  FACILITY_SYSDAEMON = 3<<3,
  FACILITY_AUTH1 = 4<<3,
  FACILITY_SYSLOGD = 5<<3,
  FACILITY_LPR = 6<<3,
  FACILITY_NEWS = 7<<3,
  FACILITY_UUCP = 8<<3,
  FACILITY_CLOCK1 = 9<<3,
  FACILITY_AUTH2 = 10<<3,
  FACILITY_FTP = 11<<3,
  FACILITY_NTP = 12<<3,
  FACILITY_LOGAUDIT = 13<<3,
  FACILITY_LOGALERT = 14<<3,
  FACILITY_CLOCK2 = 15<<3,
  FACILITY_LOCAL0 = 16<<3,
  FACILITY_LOCAL1 = 17<<3,
  FACILITY_LOCAL2 = 18<<3,
  FACILITY_LOCAL3 = 19<<3,
  FACILITY_LOCAL4 = 20<<3,
  FACILITY_LOCAL5 = 21<<3,
  FACILITY_LOCAL6 = 22<<3,
  FACILITY_LOCAL7 = 23<<3,

  NUM_FACILITIES = 24,

  SEVERITY_EMERG = 0,
  SEVERITY_ALERT = 1,
  SEVERITY_CRIT = 2,
  SEVERITY_ERROR = 3,
  SEVERITY_WARN = 4,
  SEVERITY_NOTICE = 5,
  SEVERITY_INFO = 6,
  SEVERITY_DEBUG = 7
};


// Receive modes
enum
{
  RECVMODE_TRUNCATE,
  RECVMODE_FLAT,
  RECVMODE_SPLIT,   // Default
  RECVMODE_FORENSIC,
  RECVMODE_FORENSICRAW
};


// Log file splitting modes
enum
{
  SPLITMODE_HOUR,   // Default
  SPLITMODE_DAY
};



/***************************************************************************
 *
 * Type definitions and macros
 *
 ***************************************************************************/


// Basic IPv4 address representation -- struct inaddr never looks the same
typedef union tagIP4ADDR
{
  DWORD dwIP4;
  BYTE achIP4[4];
} IP4ADDR, *PIP4ADDR;


// Description of an (open) log output destination
typedef struct tagDEST
{
  FILE *fp;
  long lFileSize;
  IP4ADDR IPAddr;
  char szIPStr[20];
  char *vbuf;
} DEST, *PDEST;


// Failure report for a single IP address (with occurence counter)
typedef struct tagFAILREPORT
{
  IP4ADDR IPAddr;
  int nOccurences;
} FAILREPORT, *PFAILREPORT;

// Information about current time
typedef struct tagTIMEINFO
{
  struct timeval tv;
  int nUTCOffs;
  char szTimestamp[64];         // Time like "2002-01-05T12:34:56%06u+01:00" or "Jan  5 12:34:56" (if "--oldtimestamp")
  char szNameTimestamp_Day[20]; // Time like "20020105"
  char szNameTimestamp[20];     // Time like "2002010512" or "20020105" (if "--split day")
} TIMEINFO, *PTIMEINFO;
typedef const TIMEINFO *LPCTIMEINFO;



/************************************************************************
 *
 * Global variables
 *
 ************************************************************************/

PDEST g_apDests[MAX_DESTS];
int g_nDests;
PDEST g_apUnusedDests[MAX_DESTS];
int g_nUnusedDests;

PDEST g_apDestHash[1<<DEST_HASH_BITS];


int g_nRecvMode = RECVMODE_SPLIT;

LPCSTR g_pszProgname = "minirsyslogd";

// These two get reset every hour
DWORD g_dwBytesReceived=0;      // Counts up to 1MB
DWORD g_dwMegaBytesReceived=0;  // Counts the rest

FILE *g_fpDaemonLog = NULL;


FAILREPORT g_aFailReports[MAX_FAILREPORTS];
int g_nFailReports=0;
int g_nWildcardFails = 0; // Failures not accounted for in the g_aFailReports array

BOOL g_bExitNow = FALSE;            // Set to TRUE by SIGINT and SIGTERM
BOOL g_bCloseAllFilesNow = FALSE;   // Set to TRUE by SIGHUP

TIMEINFO g_tiNow;                     // Lots of places need the current time



/************************************************************************
 *
 * Global variables -- settings parsed from cmdline
 *
 ************************************************************************/

BOOL g_bVerbose = FALSE;
int g_nBufSize=16384;
int g_nPort=514;
int g_nMaxDests=50;
int g_nMaxOpensPerSec=200;
long g_lMaxFileSize=2000000000; // Don't exceed 2GB file size limit
BOOL g_bDaemon = FALSE;
int g_nSplitMode = SPLITMODE_HOUR;
BOOL g_bOldTimestamps = FALSE;    // Use old-style syslog timestamps rather than RFC3339
mode_t g_nUmask = 0077;
LPCSTR g_pszRootDir = ".";



/************************************************************************
 *
 * Function : mystrlcpy
 *
 * Purpose  : strncpy with guaranteed NUL termination
 *
 * Params   : [O] char *dest
 *            [I] const char *src
 *            [I] size_t n
 *
 * Returns  : -
 * 
 ************************************************************************/

void mystrlcpy(char *dest, const char *src, size_t n)
{
  strncpy(dest, src, n);
  dest[n-1]='\0';
}


/************************************************************************
 *
 * Function : GetTimestamp
 *
 * Purpose  : Return log record timestamp string like
 *            "2002-01-05T12:34:56.456789+01:00" or 
 *            "Jan  5 12:34:56" (if g_bOldTimestamps)
 *
 * Note     : Uses internal static buffer. Next call overwrites.
 *
 * Params   : [I]   LPCTIMEINFO pti
 *
 * Returns  : LPCSTR -- pointer to usable timestamp string
 *                      in static buffer
 * 
 ************************************************************************/

LPCSTR GetTimestamp(LPCTIMEINFO pti)
{
  static char achBuf[64];

  if(g_bOldTimestamps)
    mystrlcpy(achBuf, pti->szTimestamp, sizeof(achBuf));
  else
    // szTimestamp will contain a "%06u" where the microsecond value goes
    snprintf(achBuf, sizeof(achBuf), pti->szTimestamp, pti->tv.tv_usec);

  return achBuf;
}


/************************************************************************
 *
 * Function : dlog
 *
 * Purpose  : Output a message to the local daemon log
 *            Accepts printf-style input
 *
 *            If the daemon log is not open: dump on stderr
 *
 *            If g_bVerbose, messages will always be copied to stdout
 *
 * Params   : [I] int nWhere      -- 0: only daemon log
 *                                   1: daemon log + stdout
 *                                   2: daemon log + stderr
 *            [I] LPCSTR pszFmt   -- printf-style format string
 *            [I] ...             -- args
 *
 * Returns  : -
 *
 ************************************************************************/

void dlog(int nWhere, LPCSTR pszFmt, ...)
{
  va_list args;
  FILE *fp;

  // If the local daemon log file isn't open, dump the message on stderr
  fp=g_fpDaemonLog;
  if(!g_fpDaemonLog)
    fp = stderr;

  fprintf(fp, "%s ", GetTimestamp(&g_tiNow));

  va_start(args, pszFmt);
  vfprintf(fp, pszFmt, args);
  va_end(args);

  fprintf(fp, "\n");

  fflush(fp);

  // If we're verbose, or nWhere>0: output on stdout too
  if( g_bVerbose || nWhere>0 )
  {
    if(nWhere>=2 && fp!=stderr)
      fp=stderr;
    else
      fp=stdout;
    fprintf(fp, "%s ", g_pszProgname);
    va_start(args, pszFmt);
    vfprintf(fp, pszFmt, args);
    va_end(args);
    fprintf(fp,"\n");
    fflush(fp);
  }
}



/************************************************************************
 *
 * Function : bomb / bomberrno
 *
 * Purpose  : - Print error message
 *              (bomberrno: append error description)
 *            - exit(1)
 *
 * Params   : [I] LPCSTR str
 *
 * Returns  : -
 *
 ************************************************************************/

void bomb(LPCSTR pszMsg)
{
  dlog(2, "fatal: %s", pszMsg);
  exit(1);
}

void bomberrno(LPCSTR pszMsg)
{
  dlog(2, "fatal: %s: %s", pszMsg, strerror(errno));
  exit(1);
}






/************************************************************************
 *
 * Function : myrand
 *
 * Purpose  : Low-cost PRNG which behaves "well" for these purposes.
 *            Don't you DARE think about using it for anything
 *            security critical - it is very predictable if the
 *            outputs are actually revealed.
 *
 * Returns  : DWORD -- 32-bit pseudorandom number
 *            The best entropy is returned in the low 16 bits.
 *
 ************************************************************************/

DWORD myrand(void)
{
  static DWORD dwLCPRNG=0xdeadbeef;
  static DWORD dwCntr=0;

  // Simple LCPRNG, period 2^31-1
  dwLCPRNG = dwLCPRNG * 1103515245 + 12345;
  if(dwLCPRNG >= 0x7fffffff)
    (dwLCPRNG) -= 0x7fffffff;
  if(dwLCPRNG >= 0x7fffffff)
    (dwLCPRNG) -= 0x7fffffff;

  // Increment counter by the decimal part of the golden ratio, period 2^32
  dwCntr+=0x9e3779b9;

  // Return wordswap(dwLCPRNG) + dwCntr + g_dwBytesReceived.

  // The total period of dwLCPRNG and dwCntr combined is about 2^63.
  // The addition of the received byte count adds "real" entropy, making
  // the period "indefinitely" long.

  // The high/low words of dwLCPRNG are swapped since the low bits of LCPRNGs
  // exhibit disturbing statistical correlations. This instead puts the "bad
  // entropy" in the middle of the returned DWORD.
  return (dwLCPRNG>>16) + (dwLCPRNG<<16) + dwCntr + g_dwBytesReceived;
}



/************************************************************************
 *
 * Function : GenerateTimeInfo
 *
 * Purpose  : Fill out a TIMEINFO struct:
 *            - tv
 *            - nUTCOffs
 *            - szTimestamp  -- used in log records, either RFC3339 or syslog std
 *            - szNameTimestamp_Day   -- YYYYMMDD 
 *            - szNameTimestamp -- YYYYMMDDHH, or YYYYMMDD (with "--split day")
 *
 *            szTimestamp is special. If RFC3339 timestamps are used,
 *            it will contain a "%06u" format string so that it can
 *            be passed to a printf-style formatter that inserts the
 *            the microsecond value.
 *
 * Note     : Never returns failure; calls bomberrno() on failure
 *
 *            Uses the existing contents of the timeinfo struct
 *            to determine whether or not we have passed into a 
 *            new second. If not, only tv.tv_usec is updated, and
 *            the function returns FALSE.
 *
 * Called by: main loop, at startup and every second
 *
 * Params   : [I/O] PTIMEINFO pti  
 *            [O]   struct tm *ptmOut  -- copy of local time (optional)
 *
 * Returns  : If we have passed into a new second: TRUE, and
 *              will have updated all members of the struct
 *            If we're in the same second: FALSE, and 
 *              only tv.tv_usec updated
 *
 ************************************************************************/

BOOL GenerateTimeInfo(PTIMEINFO pti, struct tm *ptmOut)
{
  static LPCSTR apszMonths[]={"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
  int nUTCOffs_Abs;
  struct tm *ptmTmp, tmLocal, tmUTC;
  struct timeval tv;
  time_t t;

  ////////////////////////////////////////////////////////////
  // Get current time

  if(gettimeofday(&tv, NULL)<0)
    bomberrno("gettimeofday");
  
  if(tv.tv_usec>=1000000)  // Yes, this actually happens on some OSes
  {
    tv.tv_usec-=1000000;
    tv.tv_sec+=1;
  }

  // Are we still in the same second?
  if(tv.tv_sec == pti->tv.tv_sec)
  {
    pti->tv.tv_usec = tv.tv_usec;
    return FALSE;  // "same second"
  }


  ////////////////////////////////////////////////////////////
  // New second: regenerate everything

  pti->tv = tv;

  // Get time components
  t=(time_t)pti->tv.tv_sec;
  if(!( ptmTmp = localtime(&t) ))
    bomberrno("localtime");
  tmLocal = *ptmTmp;

  if(!( ptmTmp = gmtime(&t) ))
    bomberrno("gmtime");
  tmUTC = *ptmTmp;

  // Hand out tmLocal?
  if(ptmOut)
    *ptmOut = tmLocal;

  // Compute UTC offset. (No, there's no other portable way of getting hold of it.)
  pti->nUTCOffs= ( (tmLocal.tm_hour - tmUTC.tm_hour) * 60 ) + tmLocal.tm_min - tmUTC.tm_min;

  if(tmLocal.tm_year > tmUTC.tm_year)
    pti->nUTCOffs += 24*60;
  else if(tmLocal.tm_year < tmUTC.tm_year)
    pti->nUTCOffs -= 24*60;
  else if(tmLocal.tm_yday > tmUTC.tm_yday)
    pti->nUTCOffs += 24*60;
  else if(tmLocal.tm_yday < tmUTC.tm_yday)
    pti->nUTCOffs -= 24*60;

  if((nUTCOffs_Abs = pti->nUTCOffs)<0)
    nUTCOffs_Abs = 0 - nUTCOffs_Abs;


  ////////////////////////////////////////////////////////////
  // Format szTimestamp (used in log records): 

  if(g_bOldTimestamps)
    // as "Jun 02 12:34:56" (syslog standard format)
    snprintf(pti->szTimestamp, sizeof(pti->szTimestamp), "%s %2u %02u:%02u:%02u",
      apszMonths[tmLocal.tm_mon], tmLocal.tm_mday, tmLocal.tm_hour, tmLocal.tm_min, tmLocal.tm_sec
    );
  else
    // or "2002-06-02T12:34:56.%06u+02:00" (RFC3339) -- note the "%06u"
    snprintf(pti->szTimestamp, sizeof(pti->szTimestamp), "%04u-%02u-%02uT%02u:%02u:%02u.%%06u%c%02u:%02u",
      tmLocal.tm_year+1900, tmLocal.tm_mon+1, tmLocal.tm_mday,
      tmLocal.tm_hour, tmLocal.tm_min, tmLocal.tm_sec,
      pti->nUTCOffs>=0 ? '+' : '-',
      nUTCOffs_Abs/60,
      nUTCOffs_Abs%60
    );


  ////////////////////////////////////////////////////////////
  // Format g_szNameTimestamp_Day as "20020602" (used by daemon's own logs)

  snprintf(pti->szNameTimestamp_Day, sizeof(pti->szNameTimestamp_Day), "%04u%02u%02u",
    tmLocal.tm_year+1900, tmLocal.tm_mon+1, tmLocal.tm_mday
  );


  ////////////////////////////////////////////////////////////
  // Format g_szNameTimestamp (log file naming)

  if(g_nSplitMode == SPLITMODE_DAY)
    // as "20020602" (with --split day)
    mystrlcpy(pti->szNameTimestamp, pti->szNameTimestamp_Day, sizeof(pti->szNameTimestamp));
  else
    // as "2002060212" (default)
    snprintf(pti->szNameTimestamp, sizeof(pti->szNameTimestamp), "%04u%02u%02u%02u",
      tmLocal.tm_year+1900, tmLocal.tm_mon+1, tmLocal.tm_mday, tmLocal.tm_hour
    );

  return TRUE;  // "we hit a new second"
}






/************************************************************************
 *
 * Function : OpenDaemonLog
 *
 * Purpose  : Open the local daemon log file g_fpDaemonLog:
 *            "minirsyslogd-YYYYMMDD"
 *
 * Note     : Never returns failure; calls bomberrno() on failure
 *
 * Assumes  : That g_fpDaemonLog is already closed
 *
 * Called by: main loop, at startup and every hour
 *
 * Returns  : -
 *            Will have set g_fpDaemonLog
 *
 ************************************************************************/

void OpenDaemonLog(void)
{
  char szTmp[1024];

  snprintf(szTmp, sizeof(szTmp), "minirsyslogd-%s", g_tiNow.szNameTimestamp_Day);
  g_fpDaemonLog = fopen(szTmp, "at");
  if(!g_fpDaemonLog)
    bomberrno("could not open local daemon log file");
}



/************************************************************************
 *
 * Function : CloseDaemonLog
 *
 * Purpose  : Close the local daemon log file (if it is open)
 *
 * Called by: main loop, at shutdown and every hour
 *
 * Returns  : -
 *            g_fpDaemonLog will be NULL
 * 
 ************************************************************************/

void CloseDaemonLog(void)
{
  if(g_fpDaemonLog)
    fclose(g_fpDaemonLog);
  g_fpDaemonLog=NULL;
}





/************************************************************************
 *
 * Function : DestHash_HashFunc
 *
 * Purpose  : Generate hash value for given IP4ADDR
 *
 * Note     : This hash would be at the mercy of attackers if it wasn't 
 *            for the fact that the administrator gets to decide the IPs, 
 *            not the attacker. Hah.
 *
 * Called by: DestHash_Add, DestHash_Remove
 *
 * Params   : [I] PIP4ADDR pIPAddr
 *
 * Returns  : DWORD 
 * 
 ************************************************************************/

DWORD DestHash_HashFunc(PIP4ADDR pIPAddr)
{

  DWORD dw;

  dw = (pIPAddr->achIP4[0]<<24) |
       (pIPAddr->achIP4[1]<<16) |
       (pIPAddr->achIP4[2]<<8) |
       (pIPAddr->achIP4[3]);

  dw *= 0x9e3779b9;

  dw >>= 32-DEST_HASH_BITS;

  return dw;
}

/************************************************************************
 *
 * Function : DestHash_Add
 *
 * Purpose  : Add given pDest to g_apDestHash
 *
 * Called by: Dest_Init
 *
 * Params   : [I] PDEST pDest
 *
 * Returns  : -
 * 
 ************************************************************************/

void DestHash_Add(PDEST pDest)
{
  DWORD dwHash, dwCnt;

  // This is a simple skip hash. Instead of making arbitrarily deep buckets, 
  // we just try the next bucket if the right one is taken.
  //
  // The hash MUST have at least as many buckets as items, and preferably
  // three times as many for it to work any where near well.

  dwHash = DestHash_HashFunc(&pDest->IPAddr);
  // Find the first empty slot at or after the hash value
  for( dwCnt=1<<DEST_HASH_BITS ; g_apDestHash[dwHash] ; dwCnt--)
  {
    if(!dwCnt)
      bomb("INTERNAL BROKENNESS: DestHash_Add: Hash is FULL?!");
    if(g_apDestHash[dwHash]==pDest)
      bomb("INTERNAL BROKENNESS: DestHash_Add: pDest was already in the hash?!");
    dwHash = (dwHash+1) & ((1<<DEST_HASH_BITS)-1);
  }

  g_apDestHash[dwHash] = pDest;
}


/************************************************************************
 *
 * Function : DestHash_Remove
 *
 * Purpose  : Remove given pDest from g_apDestHash
 *
 * Called by: Dest_Destroy
 *
 * Params   : [I] PDEST pDest
 *
 * Returns  : -
 * 
 ************************************************************************/

void DestHash_Remove(PDEST pDest)
{
  DWORD dwHash, dwCnt;

  dwHash = DestHash_HashFunc(&pDest->IPAddr);
  // Find the given pDest at or after the hash value
  for( dwCnt=1<<DEST_HASH_BITS ; g_apDestHash[dwHash]!=pDest ; dwCnt--)
  {
    if(!dwCnt)
      bomb("INTERNAL BROKENNESS: DestHash_Remove: Couldn't find pDest in the hash?!");
    dwHash = (dwHash+1) & ((1<<DEST_HASH_BITS)-1);
  }

  g_apDestHash[dwHash] = NULL;
}


/************************************************************************
 *
 * Function : Dest_Init
 *
 * Purpose  : Initialize a DEST struct:
 *            - Copy in destination information (IP address)
 *            - Open output file
 *            - Allocate (bigger?) stream buffer
 *
 * Calls    : DestHash_Add()
 *
 * Assumes  : That g_tiNow contains valid time data
 *
 * Params   : [O] PDEST pDest
 *            [I] PIP4ADDR pip
 *
 * Returns  : BOOL - FALSE for failure (failed to open output file)
 *
 ************************************************************************/

BOOL Dest_Init(PDEST pDest, PIP4ADDR pip)
{
  char szTmp[1024];

  memset(pDest, 0, sizeof(DEST));

  // Copy in given parameters
  pDest->IPAddr = *pip;

  // Add to hash
  DestHash_Add(pDest);

  // Set up string representation of IP address (for speed, later on)
  snprintf(pDest->szIPStr, sizeof(pDest->szIPStr), "%u.%u.%u.%u",
    pDest->IPAddr.achIP4[0], pDest->IPAddr.achIP4[1], pDest->IPAddr.achIP4[2], pDest->IPAddr.achIP4[3]
  );

  // Open output file
  snprintf(szTmp, sizeof(szTmp), "%s/%s-%s",
    pDest->szIPStr, pDest->szIPStr, g_tiNow.szNameTimestamp
  );

  if(g_bVerbose) printf("OPEN %s ", szTmp);

  if(!( pDest->fp = fopen(szTmp, "at") ))
  {
    // Open failed -- we keep the destination around with a null FP though, to cache the "failed" indiciation
    // 3TODO: Make this smarter -- WHY did it fail?
    if(g_bVerbose) printf("FAILED!\n");
    return FALSE;
  }

  if(g_bVerbose) printf("OK\n");

  // Get current file length (we need to track it so that we never write
  // files that exceed the 2GB limit -- it causes glibc to terminate the
  // application!)
  if( (pDest->lFileSize = ftell(pDest->fp)) < 0)
  {
    dlog(0, "warning: could not get length of %s: %s", szTmp, strerror(errno));
    fclose(pDest->fp);
    pDest->fp=NULL;
    return FALSE;
  }


  // Set buffer size (do nothing if our malloc fails; the buffer allocated by
  // libc gets used).  I originally had a smarter algorithm that malloced and
  // switched to the larger buffer only if it was actually necessary (lots of
  // writes per second), but many versions of glibc hates that. Duh.
  if((pDest->vbuf = malloc(g_nBufSize)))
    setvbuf(pDest->fp, pDest->vbuf, _IOFBF, g_nBufSize);

  return TRUE;
}



/************************************************************************
 *
 * Function : Dest_Output
 *
 * Purpose  : Format pszFmt+varargs and output to the given pDest
 *
 *            Also keep track of file lengths so that they aren't
 *            exceeded. If this happens, we dlog() the event and output
 *            a "Maximum file length exceeded" line in the output file
 *            and refuse to write more data to the file.
 *
 * Note     : No extra formatting. Data is written exactly as given.
 *            Don't do multiple calls for a single entry; you don't
 *            want the size limit to trigger in the middle of an entry!
 *
 * Called by: main loop
 *
 * Params   : [I] PDEST pDest    - an open DEST struct
 *            [I] LPCSTR pszFmt  - printf style format
 *            [I] ...
 *
 * Returns  : -
 *
 ************************************************************************/

void Dest_Output(PDEST pDest, LPCSTR pszFmt, ...)
{
  va_list args;
  long lWritten;

  // Bail early if the file is already too big
  if(pDest->lFileSize >= g_lMaxFileSize)
    return;

  // Format and output
  va_start(args, pszFmt);
  lWritten=vfprintf(pDest->fp, pszFmt, args);
  va_end(args);

  // Track file size changes
  if(lWritten<0)
    ;  // 4TODO: What to do here? Close the output file? Log an error? Keep ignoring it?
  else if(lWritten>=65600)
    ;  // 5TODO: "Should never happen" ...?
  else
    pDest->lFileSize += lWritten;

  // Emit warnings if this write exceeded the max file size
  if(pDest->lFileSize >= g_lMaxFileSize)
  {
    fprintf(pDest->fp, "%s minirsyslogd <%u>Maximum file length (%u) exceeded for %s\n",
      GetTimestamp(&g_tiNow), FACILITY_SYSLOGD | SEVERITY_WARN, (DWORD)g_lMaxFileSize, pDest->szIPStr);
    dlog(0, "drop: maximum file length (%u) exceeded for %s", (DWORD)g_lMaxFileSize, pDest->szIPStr);
  }
}



/************************************************************************
 *
 * Function : Dest_Destroy
 *
 * Purpose  : Destroy a DEST struct. Free associated resources.
 *
 * Calls    : DestHash_Remove()
 *
 * Params   : PDEST pDest
 *
 * Returns  : -
 *
 ************************************************************************/

void Dest_Destroy(PDEST pDest)
{
  DestHash_Remove(pDest);

  if(pDest->fp)
    fclose(pDest->fp);
  pDest->fp=NULL;

  if(pDest->vbuf)
    free(pDest->vbuf);
  pDest->vbuf=NULL;
}



/************************************************************************
 *
 * Function : Dest_DestroyAll
 *
 * Purpose  : Call Dest_DestroyAll for ALL open dests
 *
 * Calls    : Dest_Destroy()
 *
 * Called by: main
 *
 * Returns  : -
 * 
 ************************************************************************/

void Dest_DestroyAll(void)
{
  while(g_nDests>0)
  {
    g_nDests--;
    if(g_bVerbose) printf("CLOSE %s\n", g_apDests[g_nDests]->szIPStr);
    Dest_Destroy(g_apDests[g_nDests]);
    g_apUnusedDests[g_nUnusedDests++] = g_apDests[g_nDests];
  }
}

/************************************************************************
 *
 * Function : AddFailReport
 *
 * Purpose  : Add a failure report to the g_aFailReports array
 *            or: increment counter of an entry already in the array
 *            or: increment g_nWildcardFails if there's no room
 *
 * Params   : [I] PIP4ADDR pIPAddr
 *
 * Returns  : -
 *
 ************************************************************************/

void AddFailReport(PIP4ADDR pIPAddr)
{
  int n;

  // See if this IP is already in the list of failure reports
  for(n=g_nFailReports-1 ; n>=0 ; n--)
    if(g_aFailReports[n].IPAddr.dwIP4 == pIPAddr->dwIP4)
    {
      if(g_aFailReports[n].nOccurences < 0x7fffffff)
        g_aFailReports[n].nOccurences++;  // Just increment the "occurences" counter
      return;
    }

  // Not in the list of failure reports: add it if there's room
  if(g_nFailReports<MAX_FAILREPORTS)
  {
    if(g_bVerbose) printf("Adding new fail report for %u.%u.%u.%u\n", pIPAddr->achIP4[0], pIPAddr->achIP4[1], pIPAddr->achIP4[2], pIPAddr->achIP4[3]);
    g_aFailReports[g_nFailReports].IPAddr = *pIPAddr;
    g_aFailReports[g_nFailReports].nOccurences = 1;
    g_nFailReports++;
  }
  else if(g_nWildcardFails < 0x7fffffff)
    g_nWildcardFails++;
}



/************************************************************************
 *
 * Function : OutputFailReports
 *
 * Purpose  : Output the list of "failed to open" occurences
 *            (IP addresses denied access)
 *
 *            Clear the status and prepare for another period
 *
 * Assumes  : That g_tiNow contains valid time data
 *
 * Returns  : -
 *
 ************************************************************************/

void OutputFailReports(void)
{
  int n;

  if(g_nFailReports<=0)
    goto justreset;

  // Print detailed list
  for(n=0;n<g_nFailReports;n++)
  {
    IP4ADDR ip = g_aFailReports[n].IPAddr;
    dlog(0, "drop: failed %u.%u.%u.%u %i times",
      ip.achIP4[0], ip.achIP4[1], ip.achIP4[2], ip.achIP4[3],
      g_aFailReports[n].nOccurences
    );
  }

  // Print wildcard occurences (stuff that didn't fit in the list)
  if(g_nWildcardFails)
  {
    dlog(0, "drop: failed * %i times",
      g_nWildcardFails
    );
  }

  // Reset everything
justreset:
  g_nFailReports=0;
  g_nWildcardFails=0;
}



/************************************************************************
 *
 * Function : usage
 *
 * Purpose  : Display program usage guide and, optionally, an error
 *            message.  If an error message is displayed, everything
 *            goes out through stderr, otherwise stdout.
 *
 *            Exits the program with a status of 1 or 0 depending
 *            on if an error message was displayed.
 *
 * Calls    : exit()
 *
 * Params   : [I] LPCSTR pszMsg
 *
 * Returns  : int (ignored; the function never returns)
 *
 ************************************************************************/

int usage(LPCSTR pszMsg)
{
  FILE *fp;

  if(pszMsg && !*pszMsg)
    pszMsg = NULL;

  if(pszMsg)
    fp = stderr;
  else
    fp = stdout;
  
  if(pszMsg)
    fprintf(fp, "%s: %s\n\n", g_pszProgname, pszMsg);

  fprintf(fp,
    "%s " VERSION_STRING " options:\n"
    "  --daemon               run in background, as a daemon\n"
    "  --rootdir <path>       where to store the logs (default: .)\n"
    "  --maxopen <num>        max open files (default: 50, limit: %u)\n"
    "  --port <num>           port number to use (default: 514)\n"
    "  --recvmode <mode>      see 'receive modes' below (default: split)\n"
    "  --split <mode>         see 'splitting modes' below (default: hour)\n"
    "  --oldtimestamp         use old-style syslog timestamps rather than rfc3339\n"
    "  --pidfile <filename>   where to output the pid of minirsyslogd\n"
    "                         (default: /var/run/minirsyslogd[-<port>].pid)\n"
    "  --umask <mask>         umask to use when creating log files (default: 077)\n"
    "  --maxopenspersec <num> max file open attempts/s (default: 200)\n"
    "  --maxfilesize <mbytes> maximum output file size in megabytes (default: 2000)\n"
    "  --bufsize <bytes>      output file buffer size (default: 16384)\n"
    "  --verbose              verbose output (to stdout)\n"
    "\n"
    "Log file splitting modes:\n"
    "  hour        - split log files each hour (default)\n"
    "  day         - split log files each day\n"
    "\n"
    "Receive modes:\n"
    "  truncate    - truncate the event at the first LF\n"
    "  flat        - convert all LFs to spaces\n"
    "  split       - split data on LFs and store as multiple events (default)\n"
    "  forensic    - emit a byte count, followed by data (possibly with LFs)\n"
    "  forensicraw - -\"-, but without stripping control codes\n"
    "\n"
    "  In all modes except forensicraw, ASCII codes 1-31 and 128-159\n"
    "  are converted to spaces (32). NUL characters are always\n"
    "  converted to spaces.\n"
    ,
    g_pszProgname,
    (int)mymin(MAX_DESTS, sysconf(_SC_OPEN_MAX)-5)
  );

  exit(pszMsg ? 1 : 0);
  return 0; // Keep compilers happy
}




/************************************************************************
 *
 * Function : sighandler
 *
 * Purpose  : For SIGTERM and SIGINT: Set g_bExitNow and set these signal
 *                                    back to default behavior.
 *            For SIGHUP: Set g_bCloseAllFilesNow
 *            For others: no-op. None others are expected.
 *
 * Params   : [I] int sig
 *
 * Returns  : -
 *
 ************************************************************************/

void sighandler(int sig)
{
  if(sig==SIGTERM || sig==SIGINT)
  {
    if(g_bVerbose) printf("sighandler: got SIGTERM/SIGINT\n");
    g_bExitNow=TRUE;
    signal(SIGTERM, SIG_DFL);
    signal(SIGINT, SIG_DFL);
  }
  else if(sig==SIGHUP)
  {
    if(g_bVerbose) printf("sighandler: got SIGHUP\n");
    g_bCloseAllFilesNow=TRUE;
  }
  else if(g_bVerbose)
    printf("sighandler: got signal %u ?!?!?!\n", sig);
}




/************************************************************************
 *
 * Function : WritePIDFile
 *
 * Purpose  : Write the current PID (or not) to the given file
 *
 * Called by: main()
 *
 * Params   : [I] BOOL bWritePID  -- TRUE: write the PID
 *                                   FALSE: just wipe the contents of the file
 *
 * Returns  : -
 * 
 ************************************************************************/

void WritePIDFile(BOOL bWritePID, LPCSTR pszPIDFile)
{
  char szPIDFileTmp[1024];
  FILE *fp;
  mode_t nPreviousUmask;
  
  // Change umask: causes pidfile to be -rw-------
  nPreviousUmask = umask(077);       

  // Figure out name of pid file if not given
  if(!pszPIDFile)
  {
    if(g_nPort==514)
      snprintf(szPIDFileTmp, sizeof(szPIDFileTmp), "/var/run/minirsyslogd.pid");
    else
      snprintf(szPIDFileTmp, sizeof(szPIDFileTmp), "/var/run/minirsyslogd-%u.pid", g_nPort);
    pszPIDFile=szPIDFileTmp;
  }

  if(g_bVerbose) 
  {
    if(bWritePID)
      printf("Writing '%u' to pidfile '%s'...\n", (int)getpid(), pszPIDFile);
    else
      printf("Wiping pidfile '%s'...\n", pszPIDFile);
  }

  // Open, write and close
  if(!(fp=fopen(pszPIDFile, "wt")))
    dlog(2, "warning: failed to write to pidfile '%s'", pszPIDFile);
  else
  {
    if(bWritePID)
      fprintf(fp, "%u\n", getpid());
    fclose(fp);
  }

  // Restore umask
  umask(nPreviousUmask);  
}


/************************************************************************
 *
 * Function : main
 *
 * Purpose  : - Parse arguments
 *
 * Params   : [I] int argc
 *            [I] char *argv[]
 *
 * Returns  : int
 *
 ************************************************************************/

int main(int argc, char *argv[])
{
  int sock;
  int n, nArg;
  char szTmp[1024];
  struct sockaddr_in Sin;
  size_t cbSin;
  struct tm tmNow, tmPrev;
  PDEST pDest;
  int nOpensThisSec=0;          // Number of file open attempts this second
  int nOpensThisHour=0;         // Number of file open attempts this hour
  LPCSTR pszPIDFile=NULL;
  int nSecondsSinceCloseAll=0;  // Seconds since the turn of the hour, or since SIGHUP
  BOOL bFirstLoop=TRUE;

  g_pszProgname = argv[0];
  g_bExitNow = FALSE;
  g_bCloseAllFilesNow = FALSE;

  g_tiNow.tv.tv_sec = 0;  // Make sure GenerateTimeInfo() generates EVERYTHING this time
  GenerateTimeInfo(&g_tiNow, &tmNow);  // Set up initial timestamps for startup messages

  signal(SIGTERM, &sighandler);
  signal(SIGINT, &sighandler);
  signal(SIGHUP, &sighandler);


  ////////////////////////////////////////////////////////////
  // Parse arguments

  for(nArg=1;nArg<argc;nArg++)
  {
    if(!stricmp(argv[nArg], "--help"))
      usage(NULL);
    else if(!stricmp(argv[nArg], "--port"))
    {
      if(++nArg >= argc)
        usage("missing argument to '--port'");
      g_nPort = atoi(argv[nArg]);
      if(g_nPort<1 || g_nPort>65535)
        usage("bad argument to '--port': must be 1..65535");
    }
    else if(!stricmp(argv[nArg], "--maxopen"))
    {
      if(++nArg >= argc)
        usage("missing argument to '--maxopen'");
      g_nMaxDests = atoi(argv[nArg]);

      if(g_nMaxDests<5 || g_nMaxDests>MAX_DESTS || g_nMaxDests>sysconf(_SC_OPEN_MAX)-5)
      {
        snprintf(szTmp, sizeof(szTmp), "bad argument to '--maxopen': must be 5..%i", (int)mymin(MAX_DESTS,sysconf(_SC_OPEN_MAX)-5) );
        usage(szTmp);
      }
    }
    else if(!stricmp(argv[nArg], "--maxopenspersec"))
    {
      if(++nArg >= argc)
        usage("missing argument to '--maxopenspersec'");
      g_nMaxOpensPerSec = atoi(argv[nArg]);
      if(g_nMaxOpensPerSec<5 || g_nMaxOpensPerSec>MAX_DESTS*1000)
      {
        snprintf(szTmp, sizeof(szTmp), "bad argument to '--maxopenspersec': must be 5..%i", MAX_DESTS*1000);
        usage(szTmp);
      }
    }
    else if(!stricmp(argv[nArg], "--bufsize"))
    {
      if(++nArg >= argc)
        usage("missing argument to '--bufsize'");
      g_nBufSize = atoi(argv[nArg]);
      if(g_nBufSize<1024 || g_nBufSize>131072)
        usage("bad argument to '--bufsize': must be 1024..131072, preferably a power of 2");
    }
    else if(!stricmp(argv[nArg], "--maxfilesize"))
    {
      int n;
      if(++nArg >= argc)
        usage("missing argument to '--maxfilesize'");
      n = atoi(argv[nArg]);
      if(n<1 || n>2000)
        usage("bad argument to '--maxfilesize': must be 1..2000 (megabytes)");
      g_lMaxFileSize = (long)n * 1000000;
    }
    else if(!stricmp(argv[nArg], "--recvmode"))
    {
      if(++nArg >= argc)
        usage("missing argument to '--recvmode'");
      if(!stricmp(argv[nArg], "truncate"))
        g_nRecvMode = RECVMODE_TRUNCATE;
      else if(!stricmp(argv[nArg], "flat"))
        g_nRecvMode = RECVMODE_FLAT;
      else if(!stricmp(argv[nArg], "split"))
        g_nRecvMode = RECVMODE_SPLIT;
      else if(!stricmp(argv[nArg], "forensic"))
        g_nRecvMode = RECVMODE_FORENSIC;
      else if(!stricmp(argv[nArg], "forensicraw"))
        g_nRecvMode = RECVMODE_FORENSICRAW;
      else
        usage("bad argument to '--recvmode'");
    }
    else if(!stricmp(argv[nArg], "--daemon"))
    {
      g_bDaemon=TRUE;
    }
    else if(!stricmp(argv[nArg], "--splitday"))   // Deprecated
    {
      g_nSplitMode = SPLITMODE_DAY;
      dlog(2, "warning: '--splitday' is deprecated. Use '--split day' instead.");
    }
    else if(!stricmp(argv[nArg], "--split"))
    {
      if(++nArg >= argc)
        usage("missing argument to '--split'");
      if(!stricmp(argv[nArg], "day"))
        g_nSplitMode = SPLITMODE_DAY;
      else if(!stricmp(argv[nArg], "hour"))
        g_nSplitMode = SPLITMODE_HOUR;
      else
        usage("bad argument to '--split'");
    }
    else if(!stricmp(argv[nArg], "--oldtimestamp"))
    {
      g_bOldTimestamps=TRUE;
    }
    else if(!stricmp(argv[nArg], "--umask"))
    {
      DWORD dw=0;
      LPCSTR psz;
      
      if(++nArg >= argc)
        usage("missing argument to '--umask'");

      psz = argv[nArg];
      for(;*psz;psz++)
        if(*psz>='0' && *psz<='7')
          dw=(dw<<3) | (DWORD)( *psz - '0' );
        else
          usage("bad argument to --umask. expected octal number.");

      if(dw>=0100)
        usage("bad argument to --umask. expected octal number between 000 and 077.");

      g_nUmask = (mode_t)dw;

      printf("umask: %08x\n", dw);
    }
    else if(!stricmp(argv[nArg], "--pidfile"))
    {
      if(++nArg >= argc)
        usage("missing argument to '--pidfile'");
      pszPIDFile = argv[nArg];
    }
    else if(!stricmp(argv[nArg], "--rootdir"))
    {
      if(++nArg >= argc)
        usage("missing argument to '--rootdir'");
      g_pszRootDir = argv[nArg];
    }
    else if(!stricmp(argv[nArg], "--verbose"))
    {
      g_bVerbose=TRUE;
    }
    else
    {
      snprintf(szTmp, sizeof(szTmp), "unrecognized option '%s'", argv[nArg]);
      szTmp[sizeof(szTmp)-1]='\0';
      usage(szTmp);
    }
  
  } // END for(nArg=1;nArg<argc;nArg++)


  ////////////////////////////////////////////////////////////
  // Fix umask and current directory

  umask(g_nUmask);

  if(chdir(g_pszRootDir)!=0)
  {
    snprintf(szTmp, sizeof(szTmp), "chdir \"%s\"", g_pszRootDir);
    bomberrno(szTmp);
  }

  
  ////////////////////////////////////////////////////////////
  // Tell people that we're starting up

  OpenDaemonLog();       // Open local daemon log

  dlog(1, "startup: version=\"" VERSION_STRING "\" pid=%u uid=%u gid=%u euid=%u egid=%u", 
    getpid(), getuid(), getgid(), geteuid(), getegid());

  if(!getcwd(szTmp, sizeof(szTmp)))
    bomb("getcwd");
  
  dlog(1, "settings: rootdir=\"%s\" maxopen=%u port=%u maxopenspersec=%u split=%s recvmode=%s", 
    szTmp, g_nMaxDests, g_nPort, g_nMaxOpensPerSec,
    g_nSplitMode == SPLITMODE_DAY ? "day" : 
      "hour",
    g_nRecvMode == RECVMODE_TRUNCATE ? "truncate" : 
      g_nRecvMode == RECVMODE_FLAT ? "flat" : 
      g_nRecvMode == RECVMODE_FORENSIC ? "forensic" : 
      g_nRecvMode == RECVMODE_FORENSICRAW ? "forensicraw" : 
      "split"
  );



  /////////////////////////////////////////////////////////
  // Set up DEST structs

  for(n=0;n<g_nMaxDests;n++)
  {
    if(!(pDest = malloc(sizeof(DEST))))
      bomb("out of memory mallocing destination descriptors");
    g_apUnusedDests[n]=pDest;
  }
  g_nUnusedDests=n;
  g_nDests=0;
  memset(g_apDestHash, 0, sizeof(g_apDestHash));


  ////////////////////////////////////////////////////////////
  // Daemonize if requested

  if(g_bDaemon && !g_bVerbose)
  {
    int i;

    dlog(1, "startup: backgrounding (daemonizing)");
    
    // Fork once so that the new process can call setsid()
    switch(fork())
    {
      case -1:
        bomberrno("fork 1");
      case 0:
        // I'm the child.
        break;
      default:
        exit(0);
    }
    
    // Call setsid() to start a new process group
    if(!setsid())
      bomberrno("setsid");

    // Fork again so the child isn't the process group leader
    switch(fork())
    {
      case -1:
        bomberrno("fork 2");
      case 0:
        // I'm the grandchild.
        break;
      default:
        exit(0);
    }
    
    // Close all open files
    CloseDaemonLog();
    for(i=0;i<sysconf(_SC_OPEN_MAX);i++)
      close(i);

    // Reopen stdin/stderr/stdout to point at "/dev/null"
    if(open("/dev/null", O_RDWR, 666)!=0) // The first opened file _SHOULD_ be 0!
      exit(2);  // And no stderr to report to, either. *sigh*
    dup2(0,1);
    dup2(0,2);

    // Reopen the local daemon log
    OpenDaemonLog();
  }


  ////////////////////////////////////////////////////////
  // Bind listening socket

  if(g_bVerbose) printf("Binding port %u/udp\n", g_nPort);
  sock=socket(AF_INET, SOCK_DGRAM, 0);
  if(sock==-1)
    bomberrno("socket");

  memset(&Sin, 0, sizeof(Sin));
  Sin.sin_port=htons(g_nPort);
  Sin.sin_family=AF_INET;
  if(bind(sock, (struct sockaddr*)&Sin, sizeof(Sin))!=0)
    bomberrno("bind");


  ////////////////////////////////////////////////////////////
  // Main loop

  WritePIDFile(TRUE, pszPIDFile);

  dlog(1, "startup: minirsyslogd initialized. listening on %u/udp", g_nPort);
  bFirstLoop = TRUE;

  while(!g_bExitNow)
  {
    char achBuf[8193];
    int nLen;
    IP4ADDR ip;
    char *pch;
    fd_set fds;
    struct timeval tmo={0,250000};

    ////////////////////////////////////////////////////////////
    // Receive inbound packet

#ifndef SPAMRECEIVE
    cbSin=sizeof(Sin);
    FD_ZERO(&fds);
    FD_SET(sock, &fds);
    if(select(sock+1, &fds, NULL, NULL, &tmo)>0)
    {
      nLen=recvfrom(sock, achBuf, sizeof(achBuf)-1, 0, (struct sockaddr*)&Sin, &cbSin);
      if(nLen<0)
        nLen=0;
      memcpy(&ip, &Sin.sin_addr, sizeof(ip));
    }
    else
      nLen=0;
#else
    if(1)
    {
      static int nSpams=0;
      strcpy(achBuf, "<123>");
      for(nLen=5;nLen<100;nLen++)
        achBuf[nLen]='a';
      ip.achIP4[0]=ip.achIP4[1]=ip.achIP4[2]=1;
      ip.achIP4[3]=(BYTE)rand();
      if(++nSpams>=2000000)
        g_bExitNow = TRUE;
    }
#endif
    g_dwBytesReceived+=(DWORD)nLen;
    while(g_dwBytesReceived>=1000000)
    {
      g_dwBytesReceived-=1000000;
      g_dwMegaBytesReceived++;
    }

    ////////////////////////////////////////////////////////////
    // Per-second processing

    if(GenerateTimeInfo(&g_tiNow, &tmNow) || bFirstLoop )
    {
      // New second!

      // See if we've passed into a new hour (test all variables to catch system date changes!)
      if(tmNow.tm_hour != tmPrev.tm_hour ||
         tmNow.tm_mday != tmPrev.tm_mday ||
         tmNow.tm_mon  != tmPrev.tm_mon  ||
         tmNow.tm_year != tmPrev.tm_year ||
         bFirstLoop )
      {
        // Open new local daemon log file
        CloseDaemonLog();
        OpenDaemonLog();

        // Forcibly close all dests
        Dest_DestroyAll();
        nSecondsSinceCloseAll=0;

        // Output and reset statistics for current hour
        if(!bFirstLoop)
          dlog(0, "statistics: opens=%u recvd=%u%06u", 
            nOpensThisHour, g_dwMegaBytesReceived, g_dwBytesReceived
          );
        nOpensThisHour=0;
        g_dwMegaBytesReceived = g_dwBytesReceived = 0;

        // Report open failures (IPs denied access)
        OutputFailReports();  // Also clears all counters / state
      }

      // Whine if the maxopenspersec count has been exceeded
      if( g_nMaxOpensPerSec<1 )
        ; // noop
      else if( nOpensThisSec <= g_nMaxOpensPerSec )
        ; // ok
      else if(nSecondsSinceCloseAll<2 && nOpensThisSec <= g_nMaxDests)
        ; // allow more open attempts the first 2 seconds after having closed all open files
      else if(nSecondsSinceCloseAll<2 && g_nMaxDests > g_nMaxOpensPerSec )
      {
        dlog(0, "drops: ignored %u file open attempts. maxopen (%u) exceeded during a single second.",
          nOpensThisSec - g_nMaxDests,
          g_nMaxDests
        );
      }
      else
      {
        dlog(0, "drops: ignored %u file open attempts. maxopenspersec (%u) exceeded.",
          nOpensThisSec - g_nMaxOpensPerSec,
          g_nMaxOpensPerSec
        );
      }
      nSecondsSinceCloseAll++;

      // Reset stuff that needs to be reset
      nOpensThisSec = 0;
      tmPrev = tmNow;

    } // END if(GenerateTimeInfo(&g_tiNow, &tmNow))

    bFirstLoop=FALSE;

    ////////////////////////////////////////////////////////////
    // SIGHUP -> g_bCloseAllFilesNow ?

    if(g_bCloseAllFilesNow)
    {
      g_bCloseAllFilesNow = FALSE;

      dlog(0, "signal: got SIGHUP - flushing and closing all open files");

      Dest_DestroyAll();
      nSecondsSinceCloseAll=0;

      CloseDaemonLog();
      OpenDaemonLog();

      dlog(0, "signal: back from SIGHUP - all files including local daemon log were closed");
    }


    ////////////////////////////////////////////////////////////
    // Now, did we actually get a packet above?

    if(nLen<1)
      continue; // Nope. Go back to idling.


    ////////////////////////////////////////////////////////////
    // Get or set up a destination to store the data to

    // Find out if we already have the destination (use hash lookup)
    pDest=NULL;
    if(1)
    {
      DWORD dwHash, dwCnt;
      dwHash = DestHash_HashFunc(&ip);
      for(dwCnt=1<<DEST_HASH_BITS ; dwCnt>0 ; dwCnt--)
      {
        if(!g_apDestHash[dwHash])
          break;
        if(ip.dwIP4 == g_apDestHash[dwHash]->IPAddr.dwIP4)
        {
          pDest = g_apDestHash[dwHash];
          break;
        }
        dwHash = (dwHash+1) & ((1<<DEST_HASH_BITS)-1);
      }

    }

    // WE DO NOT HAVE THIS DESTINATION OPEN: Set up a new destination
    if(!pDest)
    {
      // Apply maxopenspersec limit
      nOpensThisSec++;
      if( g_nMaxOpensPerSec<1 )
        ; // noop
      else if( nOpensThisSec <= g_nMaxOpensPerSec )
        ; // ok
      else if(nSecondsSinceCloseAll<2 && nOpensThisSec <= g_nMaxDests)
        ; // allow more open attempts the first 2 seconds after having closed all open files
      else
        continue; // This error is reported at the turn of the second

      // Statistics...
      if(nOpensThisHour<0x7fffffff)
        nOpensThisHour++;

      // Get unused dest struct or free a random one (which is better than the least recently used one)
      if(g_nUnusedDests<1)
      {
        int i, nDest;
        if(g_nDests<1)
          bomb("Internal brokenness: no used and no unused destinations?!?!?!");
        // We try up to three times to find a dest without an open file.
        for(i=0;i<3;i++)
        {
          nDest = myrand() % g_nDests;
          pDest = g_apDests[nDest];
          if(!pDest->fp)
            break;
        }
        if(g_bVerbose && pDest->fp) printf("FORCECLOSE %s\n", pDest->szIPStr);
        Dest_Destroy(pDest);
      }
      else
      {
        pDest = g_apUnusedDests[--g_nUnusedDests];
        g_apDests[g_nDests++]=pDest;
      }

      // Initialize destination (open output file)
      if(!Dest_Init(pDest, &ip))
      {
        AddFailReport(&ip);
        continue;
      }

    } // END if(!pDest)


    // See if this destination was a cached "couldn't open the file" indicator
    if(!pDest->fp)
    {
      AddFailReport(&pDest->IPAddr);
      // Note that this means that we get one fail report _per_ _packet_, even
      // if we're in "multiple events per packet" mode.
      continue;
    }


    ////////////////////////////////////////////////////////////
    // Actual packet processing...

    // Compute a length that we can actually trust, and terminate buffer
    if(nLen<1)
      continue;
    if(nLen>sizeof(achBuf)-1)
      nLen = sizeof(achBuf)-1;
    achBuf[nLen]='\0';

    // Act according to receive mode
    switch(g_nRecvMode)
    {
      // TRUNCATE -- convert ctl to space, stop at first LF (or EOL)
      case RECVMODE_TRUNCATE:
      {
        char* pchBufEnd = achBuf+nLen;
        for(pch=achBuf ; pch<pchBufEnd && *pch!='\n' ; pch++)
          if( (*pch>=0x00 && *pch<=0x1f) || (*(PBYTE)pch>=0x80 && *(PBYTE)pch<=0x9f) )
            *pch=' ';
        *pch='\0';
        Dest_Output(pDest, "%s %s %s\n", GetTimestamp(&g_tiNow), pDest->szIPStr, achBuf);
        break;
      }
      
      // FLAT -- convert all ctl (incl. LF) to space
      case RECVMODE_FLAT:
      {
        char* pchBufEnd = achBuf+nLen;
        for(pch=achBuf ; pch<pchBufEnd ; pch++)
          if( (*pch>=0x00 && *pch<=0x1f) || (*(PBYTE)pch>=0x80 && *(PBYTE)pch<=0x9f) )
            *pch=' ';
        Dest_Output(pDest, "%s %s %s\n", GetTimestamp(&g_tiNow), pDest->szIPStr, achBuf);
        break;
      }
      
      // FORENSIC -- emit byte count, convert ctl to space, leave LFs alone
      case RECVMODE_FORENSIC:
      {
        char* pchBufEnd = achBuf+nLen;
        for(pch=achBuf ; pch<pchBufEnd ; pch++)
          if( *pch=='\n' )
            ; // leave LF alone
          else if( (*pch>=0x00 && *pch<=0x1f) || (*(PBYTE)pch>=0x80 && *(PBYTE)pch<=0x9f) )
            *pch=' ';
        Dest_Output(pDest, "%s %s %u %s\n", GetTimestamp(&g_tiNow), pDest->szIPStr, nLen, achBuf);
        break;
      }
      
      // FORENSICRAW -- emit byte count, output received data as-is (except convert NUL to space)
      case RECVMODE_FORENSICRAW:
      {
        char* pchBufEnd = achBuf+nLen;
        for(pch=achBuf ; pch<pchBufEnd ; pch++)
          if( *pch=='\0' )
            *pch=' ';
        Dest_Output(pDest, "%s %s %u %s\n", GetTimestamp(&g_tiNow), pDest->szIPStr, nLen, achBuf);
        break;
      }
      
      // SPLIT (default) -- convert ctl to space, split events on LF
      default:
      case RECVMODE_SPLIT:
      {
        char* pchBufEnd = achBuf+nLen;
        char *pchLineBegin = achBuf;
        pch=achBuf;
        
        while(TRUE)
        {
          if(*pch=='\n' || pch>=pchBufEnd)
          {
            *(pch++)='\0';
            Dest_Output(pDest, "%s %s %s\n", GetTimestamp(&g_tiNow), pDest->szIPStr, pchLineBegin);
            pchLineBegin=pch;
            if(pch>=pchBufEnd)
              break;
            continue;
          }
          
          if( (*pch>=0x00 && *pch<=0x1f) || (*(PBYTE)pch>=0x80 && *(PBYTE)pch<=0x9f) )
            *pch=' ';
          pch++;
        }
        break;
      }
    
    } // END switch(g_nRecvMode)


  } // END neverending loop

  
  ////////////////////////////////////////////////////////////
  // Output some final statistics

  dlog(0, "statistics: opens=%u recvd=%u%06u", 
    nOpensThisHour, g_dwMegaBytesReceived, g_dwBytesReceived
  );
  nOpensThisHour=0;
  g_dwMegaBytesReceived = g_dwBytesReceived = 0;

  OutputFailReports();


  ////////////////////////////////////////////////////////////
  // Clean up and exit

  dlog(1, "shutdown: version=\"" VERSION_STRING "\" pid=%u uid=%u gid=%u euid=%u egid=%u", 
    getpid(), getuid(), getgid(), geteuid(), getegid());

  Dest_DestroyAll();

  WritePIDFile(FALSE, pszPIDFile);  // Clear the PID file

  CloseDaemonLog();

  return 0;
}



syntax highlighted by Code2HTML, v. 0.9.1