/* Copyright (c) 2013, 2014 IOnU Security Inc. All rights reserved. Copyright (c) 2015, 2016 Sequence Logic, Inc. Created August 2013 by Kendrick Webster K2Daemon/main.cpp - Main module for low-latency high-throughput UDP push notification daemon */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "global.h" #include "Version.h" #include "ClientCache.h" #include "MongoDB.h" #include "MongoNotifyQueue.h" #include "MongoDeviceStatus.h" #include "../libeye/eyelog.h" #define INVALID_SOCKET (-1) #define IONU_HOME_ENV_KEY "SEQUENCELOGICHOME" #define IONU_HOME_DEFAULT "/sequencelogic" #define IONU_LOGS_SUBDIR "logs/" #define IONU_LOGFILE_NAME "K2Daemon.log" #define IONU_LOG_MAX_FILES 6 #define IONU_LOG_MAX_FILE_SIZE_KB (64 * 1024) #define DEFAULT_LOG_LEVEL Log::informational #define LOG_SHORT_IDENTIFIER "K2Daemon" #define COMMMAND_LINE_HELP "\ Command-line options (case insensitive):\n\ ?|help|-help|--help - Print this help text and exit\n\ \n\ --daemon - Run in daemon (background) mode\n\ \n\ --dump-clients [] - Log connected clients periodically\n\ If is not specified, it defaults to 60\n\ If this switch is not supplied, clients are not dumped to the log\n\ \n\ --log-echo-stdout - Echo log text to stdout\n\ --log-echo-syslog - Echo log text to syslog\n\ \n\ --log-echo-udp514 - Echo log text to UDP(localhost:514)\n\ \n\ --log-file|--logfile - Write log output to specified file\n\ default = {$SEQUENCELOGICHOME}/logs/K2Daemon.log\n\ If SEQUENCELOGICHOME is not set in the environment, \"/sequencelogic\" is used.\n\ If is relative, {$SEQUENCELOGICHOME}/logs/ is prepended.\n\ If \"syslog\" is specified as then use Linux syslog facility.\n\ \n\ --log-level - Set log filter level:\n\ 0 EMG emergency\n\ 1 ALR alert\n\ 2 CRI critical\n\ 3 ERR error\n\ 4 WRN warning\n\ 5 NTC notice\n\ 6 INF informational\n\ 7 DBG debug\n\ 8 DB2 debug2\n\ 9 DB3 debug3\n\ default = informational\n\ \n\ --log-max-file-size - Maximum log file size in KiB\n\ default = 65536 (64MiB)\n\ \n\ --log-max-files - Maximum log file count\n\ default = 6\n\ \n\ --mongo-host - Use specified MongoDB server(s)\n\ default = 127.0.0.1:27017\n\ single host format: host:port\n\ replica set format: replica_set_name::host1:port1,host2:port2,...\n\ ports are optional, default is 27017\n\ \n\ --poll-interval - Set database poll frequency\n\ is in 100ms (tenths of seconds) units, default = 0 (no polling)\n\ Database reads are normally triggered via UDP messages pushed from\n\ CloudGuard. Polling can be used as a failsafe or for testing.\n\ \n\ --port - Listen on specified UDP port\n\ default = %d\n\ \n" // externs (globals) sc_hash_state_t rng_hash; int sockfd = INVALID_SOCKET; uint8_t tx_buf[K2IPC_MAX_PACKET_SIZE]; uint8_t rx_buf[K2IPC_MAX_PACKET_SIZE]; struct sockaddr_in recvaddr; // module static (local) data namespace { class MongoInitCleanup { public: MongoInitCleanup() { printf("mongoc_init()\n");mongoc_init(); } ~MongoInitCleanup() { printf("mongoc_cleanup()\n");mongoc_cleanup(); } }; Log::config_t logConfig; unsigned int dump_clients_ticks; unsigned int dump_clients_tick_counter; unsigned int poll_database_ticks; unsigned int poll_database_tick_counter; bool poll_database; bool daemonize; uint16_t port = K2IPC_DEFAULT_UDP_PORT; bool using_mongo_replica_set; std::string mongo_replica_set_name; std::string mongo_replica_set_host_list; std::string mongo_host = MongoDB::DEFAULT_HOST; uint16_t mongo_port = MongoDB::DEFAULT_PORT; struct { bool is_open; int fd; } child_status_pipe; } static void finalizeLogging(void) { Log::Finalize(); } static int errorExit(void) { logprintf(Log::error, "K2Daemon (pid = %d) exiting due to an error", getpid()); if (child_status_pipe.is_open) { char buf = 1; if (1 != write(child_status_pipe.fd, &buf, 1)) { logprintf(Log::error, "write(pipe): %s", strerror(errno)); } if (0 != close(child_status_pipe.fd)) { logprintf(Log::error, "close(pipe): %s", strerror(errno)); } child_status_pipe.is_open = false; } // A divider line makes appended log files easier to read logprintf(Log::informational, "################################################################################"); finalizeLogging(); return EXIT_FAILURE; } static void seedRNG(void) { using std::chrono::steady_clock; steady_clock::time_point t = steady_clock::now(); sc_hash_vector(&rng_hash, (uint8_t*)&t, sizeof(t)); } static success_t initSocket(void) { struct sockaddr_in servaddr; struct timeval tv; sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (INVALID_SOCKET == sockfd) { logprintf(Log::error, "socket: %s", strerror(errno)); return FAILURE; } bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(port); logprintf(Log::informational, "listening on UDP port %d", port); if (0 != bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr))) { logprintf(Log::error, "bind: %s", strerror(errno)); return FAILURE; } tv.tv_sec = 0; tv.tv_usec = TIMER_MICROSECONDS / 2; if (0 != setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv))) { logprintf(Log::error, "setsockopt: %s", strerror(errno)); return FAILURE; } return SUCCESS; } #ifdef TEST_CHRONO static void testChrono(void) { using std::chrono::steady_clock; steady_clock::time_point t = steady_clock::now(); logprintf(Log::debug, "steady_clock::time_point, %ld bytes:", sizeof(t)); loghexdump(&t, sizeof(t)); } #endif #ifdef TEST_MONGODB static void testMongoDB(void) { MongoDB::nqueue::doc_t *p, *q; MongoDB::nqueue::AddTestData(); // TEB p = MongoDB::nqueue::GetK2(); bool tryAgain = false; p = MongoDB::nqueue::GetByType(MongoDB::nqueue::TYPE_K2, tryAgain); logprintf(Log::debug, "<<<<<------ MongoDB notification queue test dump -------------------------------------------------------------"); while (p) { logprintf(Log::debug, "id = \"%s\", device = \"%s\", info = \"%s\", type = \"%s\", expires = \"%s\", time = \"%s\"", p->id.c_str(), p->device.c_str(), p->info.c_str(), p->type.c_str(), p->expires.c_str(), p->time.c_str()); q = p; p = p->next; delete q; } logprintf(Log::debug, ">>>>>------ END MongoDB notification queue test dump ---------------------------------------------------------"); } #endif // Early log initialization -- Sets logging to stdout (while parsing command-line) static void initializeLoggingToStdout(void) { static Log::config_t c; c.echoToStdout = true; c.echoToSyslog = false; c.echoToUDP514 = false; c.logToFile = false; c.filterLevel = Log::debug3; Log::Initialize(&c); } // Normal log initialization, use defaults that can be overriden by command-line static void setLogFile(const char* p) { if ('/' == p[0]) { logConfig.file = p; } else { logConfig.file = IONU_HOME_DEFAULT; char* e = getenv(IONU_HOME_ENV_KEY); if (e) { logConfig.file = e; } if ('/' != logConfig.file[logConfig.file.length() - 1]) { logConfig.file += '/'; } logConfig.file += IONU_LOGS_SUBDIR; logConfig.file += p; } logConfig.logToFile = true; } static void setLoggingDefaults(void) { logConfig.echoToStdout = false; logConfig.echoToSyslog = false; logConfig.echoToUDP514 = false; logConfig.filterLevel = DEFAULT_LOG_LEVEL; setLogFile(IONU_LOGFILE_NAME); logConfig.nFilesMax = IONU_LOG_MAX_FILES; logConfig.nBytesPerFileMax = IONU_LOG_MAX_FILE_SIZE_KB * 1024; logConfig.shortIdentifier = LOG_SHORT_IDENTIFIER; } static void initializeLogging(void) { Log::Initialize(&logConfig); } static void sendNotifications(MongoDB::nqueue::doc_t* p) { MongoDB::nqueue::doc_t* q; while (p) { dbg_mongo_nqueue(Log::debug3, "id = \"%s\", device = \"%s\", info = \"%s\", type = \"%s\", expires = \"%s\", time = \"%s\"", p->id.c_str(), p->device.c_str(), p->info.c_str(), p->type.c_str(), p->expires.c_str(), p->time.c_str()); switch (ClientCache::SendMessage(p->id.c_str(), p->device.c_str(), p->info.c_str())) { case ClientCache::SENT_PENDING_ACK: // to do: (to support multiple daemons) add daemon ID (host, port) ? // ... or just use timestamps ? ... // the deamon that enqueued a record could cleanup sooner since the timestamp // would be usable as-is without any offset calculations p->type = MongoDB::nqueue::TYPE_K2ACK; dbg_mongo_nqueue(Log::debug3, "message sent, requeuing as %s", MongoDB::nqueue::TYPE_K2ACK); break; case ClientCache::CLIENT_OFFLINE: // to do: ask other daemons, need another "pending" state for this ... // for now, we assume that this is the one-and-only daemon p->type = MongoDB::nqueue::TYPE_PUSH; dbg_mongo_nqueue(Log::debug3, "client %s offline, requeuing as %s", p->device.c_str(), MongoDB::nqueue::TYPE_PUSH); break; case ClientCache::CLIENT_BUSY: poll_database = true; // poll again on the next timer tick to retry K2DFR entries p->type = MongoDB::nqueue::TYPE_K2DFR; dbg_mongo_nqueue(Log::debug2, "client %s busy, requeuing as %s", p->device.c_str(), MongoDB::nqueue::TYPE_K2DFR); break; } MongoDB::nqueue::Update(*p); q = p; p = p->next; delete q; } } static void pollMongoNotificationQueue(void) { bool try_again = false; MongoDB::nqueue::doc_t* p; /* first retry any K2DFR entries that may have been created on the last poll iteration */ p = MongoDB::nqueue::GetByType(MongoDB::nqueue::TYPE_K2DFR, try_again); sendNotifications(p); /* try new (from CloudGuard) entries, some may be requeued as K2DFR */ p = MongoDB::nqueue::GetByType(MongoDB::nqueue::TYPE_K2, try_again); sendNotifications(p); if (try_again) { poll_database = true; } } // this is called every TIMER_MICROSECONDS (+/- some jitter) static void onTimer(void) { seedRNG(); ClientCache::Timer(); if (poll_database) { poll_database = false; poll_database_tick_counter = poll_database_ticks; pollMongoNotificationQueue(); } else if (0 != poll_database_ticks) { if (0 == --poll_database_tick_counter) { poll_database_tick_counter = poll_database_ticks; pollMongoNotificationQueue(); } } if (0 != dump_clients_ticks) { if (0 == --dump_clients_tick_counter) { dump_clients_tick_counter = dump_clients_ticks; ClientCache::DumpClients(); } } MongoDB::Timer(); MongoDB::devstatus::Timer(); MongoDB::nqueue::Timer(); LogMemstats::Timer(); if (child_status_pipe.is_open) { // Tell the parent process that the child started OK. This is here in onTimer() because the main // loop would have aborted before calling onTimer() if anything was wrong with the socket. logprintf(Log::informational, "Signalling parent process: child has started successfully"); char buf = 0; if (1 != write(child_status_pipe.fd, &buf, 1)) { logprintf(Log::error, "write(pipe): %s", strerror(errno)); } if (0 != close(child_status_pipe.fd)) { logprintf(Log::error, "close(pipe): %s", strerror(errno)); } child_status_pipe.is_open = false; } #if 0 // test code -- verify timer rate { static int n; if (++n >= 10) { logprintf(Log::debug, "tick"); n = 0; } } #endif } static void onSignalExit(int s) { const char* t = "SIG"; switch (s) { case SIGINT: t = "SIGINT"; break; case SIGTERM: t = "SIGTERM"; break; } logprintf(Log::notice, "K2Daemon (pid = %d) caught %s, cleaning up and exiting", getpid(), t); MongoDB::Disconnect(); // A divider line makes appended log files easier to read logprintf(Log::informational, "################################################################################"); finalizeLogging(); exit(1); } static Log::severity_t parseLogLevel(const char* p) { if (isdigit(*p)) { return static_cast(atol(p)); } #define X(level, abbrev) \ if (0 == strcasecmp(p, #abbrev)) {return Log::level;} LOG_TRACE_SEVERITY_LEVELS #undef X #define X(level, abbrev) \ if (0 == strcasecmp(p, #level)) {return Log::level;} LOG_TRACE_SEVERITY_LEVELS #undef X return Log::debug3; } // helper for parseMongoReplicaSetHost static void splitMongoHostPort(const std::string& s, std::string& host, uint16_t& port) { const char* p = s.c_str(); const char* q = strchr(p, ':'); if (q) { host.assign(p, q - p); port = static_cast(atol(q + 1)); } else { host = s; port = MongoDB::DEFAULT_PORT; } } // helper for parseMongoReplicaSetHostList // parses one comma-delimited list item, returns true if an item was parsed static bool parseMongoReplicaSetHost(const char*& p, std::string& host, uint16_t& port) { if (0 == *p) { return false; } std::string s; const char* q = strchr(p, ','); if (q) { s.assign(p, q - p); p = q + 1; } else { s = p; p += s.length(); } splitMongoHostPort(s, host, port); return true; } // mongo replica set host list format: host1:port1,host2:port2,..., ports are optional static void parseMongoReplicaSetHostList(const char* p) { std::string host; uint16_t port; while (parseMongoReplicaSetHost(p, host, port)) { std::stringstream tmpHost; tmpHost << host << ":" << port << ","; mongo_replica_set_host_list += tmpHost.str(); } // Remove the last comma mongo_replica_set_host_list.pop_back(); } // --mongo-host command line parameter may be a replica set or a single host, // replica sets are prefixed with "replica_set_name::" static void parseMongoHost(const char* p) { using_mongo_replica_set = false; const char* q = strstr(p, "::"); if (q) { using_mongo_replica_set = true; parseMongoReplicaSetHostList(q + 2); mongo_replica_set_name.assign(p, q - p); } else { q = strchr(p, ':'); if (q) { mongo_host.assign(p, q - p); mongo_port = static_cast(atol(q + 1)); } else { mongo_host = p; mongo_port = MongoDB::DEFAULT_PORT; } } } static void parseCommandLine(int argc, char *argv[]) { long n; enum { PCL_IDLE, PCL_HELP, PCL_DUMP_CLIENTS, PCL_LOG_FILE, PCL_LOG_LEVEL, PCL_LOG_MAX_FILE_SIZE, PCL_LOG_MAX_FILES, PCL_MONGO_HOST, PCL_POLL_INTERVAL, PCL_PORT } state = PCL_IDLE; ++argv; while (argc-- > 1) { const char *p = *(argv++); if (('-' == p[0]) && ('-' == p[1])) { p += 2; state = PCL_IDLE; if (0 == strcasecmp(p, "help")) { state = PCL_HELP; } else if (0 == strcasecmp(p, "daemon")) { daemonize = true; } else if (0 == strcasecmp(p, "dump-clients")) { state = PCL_DUMP_CLIENTS; } else if (0 == strcasecmp(p, "log-echo-stdout")) { logConfig.echoToStdout = true; } else if (0 == strcasecmp(p, "log-echo-syslog")) { logConfig.echoToSyslog = true; } else if (0 == strcasecmp(p, "log-echo-UDP514")) { logConfig.echoToUDP514 = true; } else if (0 == strcasecmp(p, "log-file")) { state = PCL_LOG_FILE; } else if (0 == strcasecmp(p, "logfile")) { state = PCL_LOG_FILE; } else if (0 == strcasecmp(p, "log-level")) { state = PCL_LOG_LEVEL; } else if (0 == strcasecmp(p, "log-max-file-size")) { state = PCL_LOG_MAX_FILE_SIZE; } else if (0 == strcasecmp(p, "log-max-files")) { state = PCL_LOG_MAX_FILES; } else if (0 == strcasecmp(p, "mongo-host")) { state = PCL_MONGO_HOST; } else if (0 == strcasecmp(p, "poll-interval")) { state = PCL_POLL_INTERVAL; } else if (0 == strcasecmp(p, "port")) { state = PCL_PORT; } } else if ((0 == strcasecmp(p, "?")) || (0 == strcasecmp(p, "help")) || (0 == strcasecmp(p, "-help"))) { state = PCL_HELP; } else // parameter for previous flag { switch (state) { case PCL_DUMP_CLIENTS: n = atol(p); dump_clients_ticks = static_cast(TIMER_TICKS_PER_SECOND * n); dump_clients_tick_counter = dump_clients_ticks; state = PCL_IDLE; break; case PCL_LOG_FILE: setLogFile(p); state = PCL_IDLE; break; case PCL_LOG_LEVEL: logConfig.filterLevel = parseLogLevel(p); state = PCL_IDLE; break; case PCL_LOG_MAX_FILE_SIZE: n = atol(p); logConfig.nBytesPerFileMax = static_cast(n * 1024); state = PCL_IDLE; break; case PCL_LOG_MAX_FILES: n = atol(p); logConfig.nFilesMax = static_cast(n); state = PCL_IDLE; break; case PCL_MONGO_HOST: parseMongoHost(p); state = PCL_IDLE; break; case PCL_POLL_INTERVAL: n = atol(p); poll_database_ticks = static_cast(n); poll_database_tick_counter = poll_database_ticks; state = PCL_IDLE; break; case PCL_PORT: n = atol(p); port = static_cast(n); state = PCL_IDLE; break; default: // ignore break; } } // Handle parameter-less states and defaults for no parameters supplied switch (state) { case PCL_HELP: printf(COMMMAND_LINE_HELP, K2IPC_DEFAULT_UDP_PORT); //printf("about to exit\n"); exit(EXIT_SUCCESS); //printf("after exit\n"); break; case PCL_DUMP_CLIENTS: dump_clients_ticks = 60 * TIMER_TICKS_PER_SECOND; dump_clients_tick_counter = dump_clients_ticks; break; default: // do nothing break; } } } static void logLine(const std::string& line) { if (line.length()) { logprintf(Log::informational, "%s", line.c_str()); } } static void logCommandLine(int argc, char *argv[]) { std::string line; logprintf(Log::informational, "------ Command Line ------"); for (int i = 1; i < argc; ++i) { if ('-' == argv[i][0]) { logLine(line); line = argv[i]; } else { line += ' '; line += argv[i]; } } logLine(line); logprintf(Log::informational, "--------------------------"); } static void logHello(int argc, char *argv[]) { // A divider line makes appended log files easier to read logprintf(Log::informational, "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); Version::Log(); char b[256]; ssize_t n = readlink("/proc/self/exe", b, sizeof(b) - 1); if (0 > n) { logprintf(Log::error, "readlink(/proc/self/exe) : %s", strerror(errno)); } else { b[n] = 0; logprintf(Log::informational, "%s", b); } if (argc > 1) { logCommandLine(argc, argv); } else { logprintf(Log::informational, "no command-line args"); } } void Main::PollDatabase(void) { poll_database = true; } int main(int argc, char *argv[]) { parseCommandLine(argc, argv); // will exit() before logging anything if only showing help //printf("made it here\n"); seedRNG(); initializeLoggingToStdout(); logprintf(Log::debug, "K2 socket notifier daemon (pid %d, ppid %d) starting", getpid(), getppid()); setLoggingDefaults(); parseCommandLine(argc, argv); if (daemonize) { int pipefd[2]; if (0 != pipe(pipefd)) { logprintf(Log::error, "%s: %s", "pipe", strerror(errno)); return errorExit(); } pid_t pid, sid; pid = fork(); // returns 0 for child if (pid < 0) { logprintf(Log::error, "%s: %s", "fork", strerror(errno)); return errorExit(); } if (pid > 0) { // parent process ... wait to see if child started OK, then exit char buf = 1; close(pipefd[1]); // write end if (-1 == read(pipefd[0], &buf, 1)) { logprintf(Log::error, "%s: %s", "read(pipe)", strerror(errno)); return errorExit(); } if (0 == buf) { logprintf(Log::debug, "parent process exiting with SUCCESS status"); return EXIT_SUCCESS; } else { logprintf(Log::error, "parent process exiting with FAILURE status, see log for details"); return EXIT_FAILURE; } } // child process ... close(pipefd[0]); // read end child_status_pipe.is_open = true; child_status_pipe.fd = pipefd[1]; umask(0); initializeLogging(); sid = setsid(); if (sid < 0) { logprintf(Log::error, "%s: %s", "setsid", strerror(errno)); return errorExit(); } if ((chdir("/")) < 0) { logprintf(Log::error, "%s: %s", "chdir(\"/\")", strerror(errno)); return errorExit(); } close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO); logHello(argc, argv); logprintf(Log::informational, "Running as daemon, sid %d, pid %d, ppid %d", sid, getpid(), getppid()); } else { logprintf(Log::informational, "Initializing logging..."); initializeLogging(); logHello(argc, argv); logprintf(Log::informational, "Running in console mode, pid %d, ppid %d", getpid(), getppid()); } #ifdef TEST_CHRONO testChrono(); testChrono(); testChrono(); #endif if (FAILURE == initSocket()) { return errorExit(); } seedRNG(); { struct sigaction sa; sa.sa_handler = onSignalExit; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGINT, &sa, NULL); sigaction(SIGTERM, &sa, NULL); } MongoInitCleanup mongoInit; if (using_mongo_replica_set) { if (SUCCESS != MongoDB::ConnectReplica(mongo_replica_set_name, mongo_replica_set_host_list)) { logprintf(Log::error, "Unable to connect to MongoDB replica set"); return errorExit(); } } else { if (SUCCESS != MongoDB::Connect(mongo_host.c_str(), mongo_port)) { logprintf(Log::error, "Unable to connect to MongoDB host"); return errorExit(); } } #ifdef TEST_MONGODB testMongoDB(); #endif logprintf(Log::debug, "Cleaning MongoDB device status collection"); MongoDB::devstatus::Clean(); logprintf(Log::debug, "Done cleaning MongoDB device status collection"); seedRNG(); CStopWatch stopwatch; for (;;) { socklen_t recvaddr_len = sizeof(recvaddr); int n = recvfrom(sockfd, rx_buf, sizeof(rx_buf), 0, reinterpret_cast(&recvaddr), &recvaddr_len); if (n > 0) { ClientCache::HandlePacket(static_cast(n)); } else if (EAGAIN != errno) { logprintf(Log::debug, "recvfrom() returned %d", n); logprintf(Log::error, "%s: %s", "recvfrom", strerror(errno)); return errorExit(); } if (TIMER_MICROSECONDS <= stopwatch.Microseconds()) { stopwatch.DecreaseMicroseconds(TIMER_MICROSECONDS); onTimer(); } } }