diff options
Diffstat (limited to 'src/master/master.c')
-rw-r--r-- | src/master/master.c | 868 |
1 files changed, 868 insertions, 0 deletions
diff --git a/src/master/master.c b/src/master/master.c new file mode 100644 index 0000000..11cec6a --- /dev/null +++ b/src/master/master.c @@ -0,0 +1,868 @@ +/* + master.c + + A master server for Tremulous + + Copyright (C) 2002-2005 Mathieu Olivier + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + + +#include <stdarg.h> +#include <signal.h> +#include <ctype.h> + +#ifndef WIN32 +# include <pwd.h> +# include <unistd.h> +#endif + +#include "common.h" +#include "messages.h" +#include "servers.h" + + +// ---------- Constants ---------- // + +// Version of dpmaster +#define VERSION "1.6" + +// Default master port +#define DEFAULT_MASTER_PORT 30710 + +// Maximum and minimum sizes for a valid packet +#define MAX_PACKET_SIZE 2048 +#define MIN_PACKET_SIZE 5 + +#ifndef WIN32 +// Default path we use for chroot +# define DEFAULT_JAIL_PATH "/var/empty/" + +// User we use by default for dropping super-user privileges +# define DEFAULT_LOW_PRIV_USER "nobody" +#endif + + +// ---------- Types ---------- // + +#ifdef WIN32 +typedef int socklen_t; +#endif + + +// ---------- Private variables ---------- // + +// The port we use +static unsigned short master_port = DEFAULT_MASTER_PORT; + +// Local address we listen on, if any +static const char* listen_name = NULL; +static struct in_addr listen_addr; + +#ifndef WIN32 +// On UNIX systems, we can run as a daemon +static qboolean daemon_mode = qfalse; + +// Path we use for chroot +static const char* jail_path = DEFAULT_JAIL_PATH; + +// Low privileges user +static const char* low_priv_user = DEFAULT_LOW_PRIV_USER; +#endif + + +// ---------- Public variables ---------- // + +// The master socket +int inSock = -1; +int outSock = -1; + +// The current time (updated every time we receive a packet) +time_t crt_time; + +// Maximum level for a message to be printed +msg_level_t max_msg_level = MSG_NORMAL; + +// Peer address. We rebuild it every time we receive a new packet +char peer_address [128]; + + +// ---------- Private functions ---------- // + +/* +==================== +PrintPacket + +Print the contents of a packet on stdout +==================== +*/ +static void PrintPacket (const char* packet, size_t length) +{ + size_t i; + + // Exceptionally, we use MSG_NOPRINT here because if the function is + // called, the user probably wants this text to be displayed + // whatever the maximum message level is. + MsgPrint (MSG_NOPRINT, "\""); + + for (i = 0; i < length; i++) + { + char c = packet[i]; + if (c == '\\') + MsgPrint (MSG_NOPRINT, "\\\\"); + else if (c == '\"') + MsgPrint (MSG_NOPRINT, "\""); + else if (c >= 32 && (qbyte)c <= 127) + MsgPrint (MSG_NOPRINT, "%c", c); + else + MsgPrint (MSG_NOPRINT, "\\x%02X", c); + } + + MsgPrint (MSG_NOPRINT, "\" (%u bytes)\n", length); +} + + +/* +==================== +SysInit + +System dependent initializations +==================== +*/ +static qboolean SysInit (void) +{ +#ifdef WIN32 + WSADATA winsockdata; + + if (WSAStartup (MAKEWORD (1, 1), &winsockdata)) + { + MsgPrint (MSG_ERROR, "ERROR: can't initialize winsocks\n"); + return qfalse; + } +#endif + + return qtrue; +} + + +/* +==================== +UnsecureInit + +System independent initializations, called BEFORE the security initializations. +We need this intermediate step because DNS requests may not be able to resolve +after the security initializations, due to chroot. +==================== +*/ +static qboolean UnsecureInit (void) +{ + // Resolve the address mapping list + if (! Sv_ResolveAddressMappings ()) + return qfalse; + + // Resolve the listen address if one was specified + if (listen_name != NULL) + { + struct hostent* itf; + + itf = gethostbyname (listen_name); + if (itf == NULL) + { + MsgPrint (MSG_ERROR, "ERROR: can't resolve %s\n", listen_name); + return qfalse; + } + if (itf->h_addrtype != AF_INET) + { + MsgPrint (MSG_ERROR, "ERROR: %s is not an IPv4 address\n", + listen_name); + return qfalse; + } + + memcpy (&listen_addr.s_addr, itf->h_addr, + sizeof (listen_addr.s_addr)); + } + + return qtrue; +} + + +/* +==================== +SecInit + +Security initializations (system dependent) +==================== +*/ +static qboolean SecInit (void) +{ +#ifndef WIN32 + // Should we run as a daemon? + if (daemon_mode && daemon (0, 0)) + { + MsgPrint (MSG_NOPRINT, "ERROR: daemonization failed (%s)\n", + strerror (errno)); + return qfalse; + } + + // UNIX allows us to be completely paranoid, so let's go for it + if (geteuid () == 0) + { + struct passwd* pw; + + MsgPrint (MSG_WARNING, + "WARNING: running with super-user privileges\n"); + + // We must get the account infos before the calls to chroot and chdir + pw = getpwnam (low_priv_user); + if (pw == NULL) + { + MsgPrint (MSG_ERROR, "ERROR: can't get user \"%s\" properties\n", + low_priv_user); + return qfalse; + } + + // Chroot ourself + MsgPrint (MSG_NORMAL, " - chrooting myself to %s... ", jail_path); + if (chroot (jail_path) || chdir ("/")) + { + MsgPrint (MSG_ERROR, "FAILED (%s)\n", strerror (errno)); + return qfalse; + } + MsgPrint (MSG_NORMAL, "succeeded\n"); + + // Switch to lower privileges + MsgPrint (MSG_NORMAL, " - switching to user \"%s\" privileges... ", + low_priv_user); + if (setgid (pw->pw_gid) || setuid (pw->pw_uid)) + { + MsgPrint (MSG_ERROR, "FAILED (%s)\n", strerror (errno)); + return qfalse; + } + MsgPrint (MSG_NORMAL, "succeeded (UID: %u, GID: %u)\n", + pw->pw_uid, pw->pw_gid); + + MsgPrint (MSG_NORMAL, "\n"); + } +#endif + + return qtrue; +} + + +/* +==================== +ParseCommandLine + +Parse the options passed by the command line +==================== +*/ +static qboolean ParseCommandLine (int argc, const char* argv []) +{ + int ind = 1; + unsigned int vlevel = max_msg_level; + qboolean valid_options = qtrue; + + while (ind < argc && valid_options) + { + // If it doesn't even look like an option, why bother? + if (argv[ind][0] != '-') + valid_options = qfalse; + + else switch (argv[ind][1]) + { +#ifndef WIN32 + // Daemon mode + case 'D': + daemon_mode = qtrue; + break; +#endif + + // Help + case 'h': + valid_options = qfalse; + break; + + // Hash size + case 'H': + ind++; + if (ind < argc) + valid_options = Sv_SetHashSize (atoi (argv[ind])); + else + valid_options = qfalse; + break; + +#ifndef WIN32 + // Jail path + case 'j': + ind++; + if (ind < argc) + jail_path = argv[ind]; + else + valid_options = qfalse; + break; +#endif + + // Listen address + case 'l': + ind++; + if (ind >= argc || argv[ind][0] == '\0') + valid_options = qfalse; + else + listen_name = argv[ind]; + break; + + // Address mapping + case 'm': + ind++; + if (ind < argc) + valid_options = Sv_AddAddressMapping (argv[ind]); + else + valid_options = qfalse; + break; + + // Maximum number of servers + case 'n': + ind++; + if (ind < argc) + valid_options = Sv_SetMaxNbServers (atoi (argv[ind])); + else + valid_options = qfalse; + break; + + // Port number + case 'p': + { + unsigned short port_num = 0; + ind++; + if (ind < argc) + port_num = atoi (argv[ind]); + if (!port_num) + valid_options = qfalse; + else + master_port = port_num; + break; + } + +#ifndef WIN32 + // Low privileges user + case 'u': + ind++; + if (ind < argc) + low_priv_user = argv[ind]; + else + valid_options = qfalse; + break; +#endif + + // Verbose level + case 'v': + // If a verbose level has been specified + if (ind + 1 < argc && argv[ind + 1][0] != '-') + { + ind++; + vlevel = atoi (argv[ind]); + if (vlevel > MSG_DEBUG) + valid_options = qfalse; + } + else + vlevel = MSG_DEBUG; + break; + + default: + valid_options = qfalse; + } + + ind++; + } + + // If the command line is OK, we can set the verbose level now + if (valid_options) + { +#ifndef WIN32 + // If we run as a daemon, don't bother printing anything + if (daemon_mode) + max_msg_level = MSG_NOPRINT; + else +#endif + max_msg_level = vlevel; + } + + return valid_options; +} + + +/* +==================== +PrintHelp + +Print the command line syntax and the available options +==================== +*/ +static void PrintHelp (void) +{ + MsgPrint (MSG_ERROR, + "Syntax: dpmaster [options]\n" + "Available options are:\n" +#ifndef WIN32 + " -D : run as a daemon\n" +#endif + " -h : this help\n" + " -H <hash_size> : hash size in bits, up to %u (default: %u)\n" +#ifndef WIN32 + " -j <jail_path> : use <jail_path> as chroot path (default: %s)\n" + " only available when running with super-user privileges\n" +#endif + " -l <address> : listen on local address <address>\n" + " -m <a1>=<a2> : map address <a1> to <a2> when sending it to clients\n" + " addresses can contain a port number (ex: myaddr.net:1234)\n" + " -n <max_servers> : maximum number of servers recorded (default: %u)\n" + " -p <port_num> : use port <port_num> (default: %u)\n" +#ifndef WIN32 + " -u <user> : use <user> privileges (default: %s)\n" + " only available when running with super-user privileges\n" +#endif + " -v [verbose_lvl] : verbose level, up to %u (default: %u; no value means max)\n" + "\n", + MAX_HASH_SIZE, DEFAULT_HASH_SIZE, +#ifndef WIN32 + DEFAULT_JAIL_PATH, +#endif + DEFAULT_MAX_NB_SERVERS, + DEFAULT_MASTER_PORT, +#ifndef WIN32 + DEFAULT_LOW_PRIV_USER, +#endif + MSG_DEBUG, MSG_NORMAL); +} + + +/* +==================== +SecureInit + +System independent initializations, called AFTER the security initializations +==================== +*/ +static qboolean SecureInit (void) +{ + struct sockaddr_in address; + + // Init the time and the random seed + crt_time = time (NULL); + srand (crt_time); + + // Initialize the server list and hash table + if (!Sv_Init ()) + return qfalse; + + // Open the socket + inSock = socket (PF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (inSock < 0) + { + MsgPrint (MSG_ERROR, "ERROR: socket creation failed (%s)\n", + strerror (errno)); + return qfalse; + } + + outSock = socket (PF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (outSock < 0) + { + MsgPrint (MSG_ERROR, "ERROR: socket creation failed (%s)\n", + strerror (errno)); + return qfalse; + } + + // Bind it to the master port + memset (&address, 0, sizeof (address)); + address.sin_family = AF_INET; + if (listen_name != NULL) + { + MsgPrint (MSG_NORMAL, "Listening on address %s (%s)\n", + listen_name, inet_ntoa (listen_addr)); + address.sin_addr.s_addr = listen_addr.s_addr; + } + else + address.sin_addr.s_addr = htonl (INADDR_ANY); + + address.sin_port = htons (master_port); + if (bind (inSock, (struct sockaddr*)&address, sizeof (address)) != 0) + { + MsgPrint (MSG_ERROR, "ERROR: socket binding failed (%s)\n", + strerror (errno)); +#ifdef WIN32 + closesocket (inSock); +#else + close (inSock); +#endif + return qfalse; + } + MsgPrint (MSG_NORMAL, "Listening on UDP port %hu\n", + ntohs (address.sin_port)); + + // Deliberately use a different port for outgoing traffic in order + // to confuse NAT UDP "connection" tracking and thus delist servers + // hidden by NAT + address.sin_port = htons (master_port+1); + if (bind (outSock, (struct sockaddr*)&address, sizeof (address)) != 0) + { + MsgPrint (MSG_ERROR, "ERROR: socket binding failed (%s)\n", + strerror (errno)); +#ifdef WIN32 + closesocket (outSock); +#else + close (outSock); +#endif + return qfalse; + } + + return qtrue; +} + +static qboolean exitNow = qfalse; + +/* +=============== +cleanUp + +Clean up +=============== +*/ +static void cleanUp( int signal ) +{ + MsgPrint( MSG_NORMAL, "Caught signal %d, exiting...\n", signal ); + + exitNow = qtrue; +} + +#define ADDRESS_LENGTH 16 +static const char *ignoreFile = "ignore.txt"; + +typedef struct +{ + char address[ ADDRESS_LENGTH ]; // Dotted quad +} ignoreAddress_t; + +#define PARSE_INTERVAL 60 // seconds + +static time_t lastParseTime = 0; +static int numIgnoreAddresses = 0; +static ignoreAddress_t *ignoreAddresses = NULL; + +/* +==================== +parseIgnoreAddress +==================== +*/ +static qboolean parseIgnoreAddress( void ) +{ + int numAllocIgnoreAddresses = 1; + FILE *f = NULL; + int i; + + // Only reparse periodically + if( crt_time - lastParseTime < PARSE_INTERVAL ) + return qtrue; + + lastParseTime = time( NULL ); + + // Free existing list + if( ignoreAddresses != NULL ) + { + free( ignoreAddresses ); + ignoreAddresses = NULL; + } + + numIgnoreAddresses = 0; + ignoreAddresses = malloc( sizeof( ignoreAddress_t ) * numAllocIgnoreAddresses ); + + // Alloc failed, fail parsing + if( ignoreAddresses == NULL ) + return qfalse; + + f = fopen( ignoreFile, "r" ); + + if( !f ) + { + free( ignoreAddresses ); + ignoreAddresses = NULL; + return qfalse; + } + + while( !feof( f ) ) + { + char c; + char buffer[ ADDRESS_LENGTH ]; + + i = 0; + + // Skip whitespace + do + { + c = fgetc( f ); + } + while( c != EOF && isspace( c ) ); + + if( c != EOF ) + { + do + { + if( i >= ADDRESS_LENGTH ) + { + buffer[ i - 1 ] = '\0'; + break; + } + + buffer[ i ] = c; + + if( isspace( c ) ) + { + buffer[ i ] = '\0'; + break; + } + + i++; + } while( ( c = fgetc( f ) ) != EOF ); + + strcpy( ignoreAddresses[ numIgnoreAddresses ].address, buffer ); + + numIgnoreAddresses++; + + // Make list bigger + if( numIgnoreAddresses >= numAllocIgnoreAddresses ) + { + ignoreAddress_t *new; + + numAllocIgnoreAddresses *= 2; + new = realloc( ignoreAddresses, + sizeof( ignoreAddress_t ) * numAllocIgnoreAddresses ); + + // Alloc failed, fail parsing + if( new == NULL ) + { + fclose( f ); + free( ignoreAddresses ); + ignoreAddresses = NULL; + return qfalse; + } + + ignoreAddresses = new; + } + } + } + + fclose( f ); + + return qtrue; +} + +/* +==================== +ignoreAddress + +Check whether or not to ignore a specific address +==================== +*/ +static qboolean ignoreAddress( const char *address ) +{ + int i; + + if( !parseIgnoreAddress( ) ) + { + // Couldn't parse, allow the address + return qfalse; + } + + for( i = 0; i < numIgnoreAddresses; i++ ) + { + if( strcmp( address, ignoreAddresses[ i ].address ) == 0 ) + break; + } + + // It matched + if( i < numIgnoreAddresses ) + return qtrue; + + return qfalse; +} + +/* +==================== +max + +Maximum of two ints +==================== +*/ +static inline int max( int a, int b ) +{ + return a > b ? a : b; +} + +/* +==================== +main + +Main function +==================== +*/ +int main (int argc, const char* argv []) +{ + struct sockaddr_in address; + socklen_t addrlen; + int nb_bytes; + int sock; + char packet [MAX_PACKET_SIZE + 1]; // "+ 1" because we append a '\0' + qboolean valid_options; + fd_set rfds; + struct timeval tv; + + + signal( SIGINT, cleanUp ); + signal( SIGTERM, cleanUp ); + + // Get the options from the command line + valid_options = ParseCommandLine (argc, argv); + + MsgPrint (MSG_NORMAL, + "tremmaster (version " VERSION " " __DATE__ " " __TIME__ ")\n" ); + + // If there was a mistake in the command line, print the help and exit + if (!valid_options) + { + PrintHelp (); + return EXIT_FAILURE; + } + + // Initializations + if (!SysInit () || !UnsecureInit () || !SecInit () || !SecureInit ()) + return EXIT_FAILURE; + MsgPrint (MSG_NORMAL, "\n"); + + // Until the end of times... + while( !exitNow ) + { + FD_ZERO( &rfds ); + FD_SET( inSock, &rfds ); + FD_SET( outSock, &rfds ); + tv.tv_sec = tv.tv_usec = 0; + + // Check for new data every 100ms + if( select( max( inSock, outSock ) + 1, &rfds, NULL, NULL, &tv ) <= 0 ) + { +#ifdef _WIN32 + Sleep( 100 ); +#else + usleep( 100000 ); +#endif + continue; + } + + if( FD_ISSET( inSock, &rfds ) ) + sock = inSock; + else if( FD_ISSET( outSock, &rfds ) ) + sock = outSock; + else + continue; + + // Get the next valid message + addrlen = sizeof (address); + nb_bytes = recvfrom (sock, packet, sizeof (packet) - 1, 0, + (struct sockaddr*)&address, &addrlen); + if (nb_bytes <= 0) + { + MsgPrint (MSG_WARNING, + "WARNING: \"recvfrom\" returned %d\n", nb_bytes); + continue; + } + + // Ignore abusers + if( ignoreAddress( inet_ntoa( address.sin_addr ) ) ) + continue; + + // If we may have to print something, rebuild the peer address buffer + if (max_msg_level != MSG_NOPRINT) + snprintf (peer_address, sizeof (peer_address), "%s:%hu", + inet_ntoa (address.sin_addr), ntohs (address.sin_port)); + + // We print the packet contents if necessary + // TODO: print the current time here + if (max_msg_level >= MSG_DEBUG) + { + MsgPrint (MSG_DEBUG, "New packet received from %s: ", + peer_address); + PrintPacket (packet, nb_bytes); + } + + // A few sanity checks + if (nb_bytes < MIN_PACKET_SIZE) + { + MsgPrint (MSG_WARNING, + "WARNING: rejected packet from %s (size = %d bytes)\n", + peer_address, nb_bytes); + continue; + } + if (*((unsigned int*)packet) != 0xFFFFFFFF) + { + MsgPrint (MSG_WARNING, + "WARNING: rejected packet from %s (invalid header)\n", + peer_address); + continue; + } + if( ntohs( address.sin_port ) < 1024 ) + { + MsgPrint (MSG_WARNING, + "WARNING: rejected packet from %s (source port = 0)\n", + peer_address); + continue; + } + + // Append a '\0' to make the parsing easier and update the current time + packet[nb_bytes] = '\0'; + crt_time = time (NULL); + + // Call HandleMessage with the remaining contents + HandleMessage (packet + 4, nb_bytes - 4, &address); + } + + return 0; +} + + +// ---------- Public functions ---------- // + +/* +==================== +MsgPrint + +Print a message to screen, depending on its verbose level +==================== +*/ +int MsgPrint (msg_level_t msg_level, const char* format, ...) +{ + va_list args; + int result; + + // If the message level is above the maximum level, don't print it + if (msg_level > max_msg_level) + return 0; + + va_start (args, format); + result = vprintf (format, args); + va_end (args); + + fflush (stdout); + + return result; +} |