diff options
Diffstat (limited to 'engine/sv_rcon.cpp')
| -rw-r--r-- | engine/sv_rcon.cpp | 636 |
1 files changed, 636 insertions, 0 deletions
diff --git a/engine/sv_rcon.cpp b/engine/sv_rcon.cpp new file mode 100644 index 0000000..f3317b9 --- /dev/null +++ b/engine/sv_rcon.cpp @@ -0,0 +1,636 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: implementation of the rcon server +// +//===========================================================================// + + +#if defined(_WIN32) +#if !defined(_X360) +#include <winsock.h> +#endif +#undef SetPort // winsock screws with the SetPort string... *sigh* +#define socklen_t int +#define MSG_NOSIGNAL 0 +#elif POSIX +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <errno.h> +#include <sys/ioctl.h> +#define closesocket close +#define WSAGetLastError() errno +#define ioctlsocket ioctl +#ifdef OSX +#define MSG_NOSIGNAL 0 +#endif +#endif +#include <tier0/dbg.h> +#include "utlbuffer.h" +#include "server.h" +#include "sv_rcon.h" +#include "proto_oob.h" // PORT_RCON define +#include "sv_remoteaccess.h" +#include "cl_rcon.h" +#include "sv_filter.h" + +#if defined( _X360 ) +#include "xbox/xbox_win32stubs.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#ifdef ENABLE_RPT +class CRPTServer : public CRConServer +{ + typedef CRConServer BaseClass; + +public: + virtual void OnSocketAccepted( SocketHandle_t hSocket, const netadr_t & netAdr, void** ppData ) + { + BaseClass::OnSocketAccepted( hSocket, netAdr, ppData ); + + // Enable cheats on this client only + Cmd_SetRptActive( true ); + } + + virtual void OnSocketClosed( SocketHandle_t hSocket, const netadr_t & netAdr, void* pData ) + { + Cmd_SetRptActive( false ); + BaseClass::OnSocketClosed( hSocket, netAdr, pData ); + } +}; + + +static CRPTServer g_RPTServer; +CRConServer & RPTServer() +{ + return g_RPTServer; +} +#endif // ENABLE_RPT + +static CRConServer g_RCONServer; +CRConServer & RCONServer() +{ + return g_RCONServer; +} + +static void RconPasswordChanged_f( IConVar *pConVar, const char *pOldString, float flOldValue ) +{ + ConVarRef var( pConVar ); + const char *pPassword = var.GetString(); +#ifndef SWDS + RCONClient().SetPassword( pPassword ); +#endif + RCONServer().SetPassword( pPassword ); + +} +ConVar rcon_password ( "rcon_password", "", FCVAR_SERVER_CANNOT_QUERY|FCVAR_DONTRECORD, "remote console password.", RconPasswordChanged_f ); + +ConVar sv_rcon_banpenalty( "sv_rcon_banpenalty", "0", 0, "Number of minutes to ban users who fail rcon authentication", true, 0, false, 0 ); +ConVar sv_rcon_maxfailures( "sv_rcon_maxfailures", "10", 0, "Max number of times a user can fail rcon authentication before being banned", true, 1, true, 20 ); +ConVar sv_rcon_minfailures( "sv_rcon_minfailures", "5", 0, "Number of times a user can fail rcon authentication in sv_rcon_minfailuretime before being banned", true, 1, true, 20 ); +ConVar sv_rcon_minfailuretime( "sv_rcon_minfailuretime", "30", 0, "Number of seconds to track failed rcon authentications", true, 1, false, 0 ); +ConVar sv_rcon_whitelist_address( "sv_rcon_whitelist_address", "", 0, "When set, rcon failed authentications will never ban this address, e.g. '127.0.0.1'" ); + +ConVar sv_rcon_maxpacketsize( "sv_rcon_maxpacketsize", "1024", 0, "The maximum number of bytes to allow in a command packet", true, 0, false, 0 ); +ConVar sv_rcon_maxpacketbans( "sv_rcon_maxpacketbans", "1", 0, "Ban IPs for sending RCON packets exceeding the value specified in sv_rcon_maxpacketsize", true, 0, true, 1 ); + +//----------------------------------------------------------------------------- +// Purpose: Constructor +//----------------------------------------------------------------------------- +#pragma warning ( disable : 4355 ) + +CRConServer::CRConServer() : m_Socket( this ) +{ +} + +CRConServer::CRConServer( const char *pNetAddress ) : m_Socket( this ) +{ + SetAddress( pNetAddress ); +} + +#pragma warning ( default : 4355 ) + + +//----------------------------------------------------------------------------- +// Purpose: Destructor +//----------------------------------------------------------------------------- +CRConServer::~CRConServer() +{ +} + + +//----------------------------------------------------------------------------- +// Allows a server to request a listening client to connect to it +//----------------------------------------------------------------------------- +bool CRConServer::ConnectToListeningClient( const netadr_t &adr, bool bSingleSocket ) +{ + if ( m_Socket.ConnectSocket( adr, bSingleSocket ) < 0 ) + { + ConWarning( "Unable to connect to remote client (%s)\n", adr.ToString() ); + return false; + } + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: returns true if the listening socket is created and listening +//----------------------------------------------------------------------------- +bool CRConServer::IsConnected() +{ + return m_Socket.IsListening(); +} + +void CRConServer::SetPassword( const char *pPassword ) +{ + m_Socket.CloseAllAcceptedSockets(); + m_Password = pPassword; +} + +bool CRConServer::HasPassword() const +{ + return !m_Password.IsEmpty(); +} + +bool CRConServer::IsPassword( const char *pPassword ) const +{ + // Must have a password set to allow any rconning. + if ( !HasPassword() ) + return false; + + // If the pw does not match, then not authed + return ( Q_strcmp( pPassword, m_Password.Get() ) == 0 ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Set the address to bind to +//----------------------------------------------------------------------------- +void CRConServer::SetAddress( const char *pNetAddress ) +{ + NET_StringToAdr( pNetAddress, &m_Address ); + if ( m_Address.GetPort() == 0 ) + { + m_Address.SetPort( PORT_RCON ); + } +} + +bool CRConServer::CreateSocket() +{ + return m_Socket.CreateListenSocket( m_Address ); +} + + +//----------------------------------------------------------------------------- +// Inherited from ISocketCreatorListener +//----------------------------------------------------------------------------- +bool CRConServer::ShouldAcceptSocket( SocketHandle_t hSocket, const netadr_t & netAdr ) +{ + return !Filter_ShouldDiscard( netAdr ); +} + +void CRConServer::OnSocketAccepted( SocketHandle_t hSocket, const netadr_t &netAdr, void** ppData ) +{ + ConnectedRConSocket_t *pNewSocket = new ConnectedRConSocket_t; + pNewSocket->lastRequestID = 0; + pNewSocket->authed = false; + pNewSocket->listenerID = g_ServerRemoteAccess.GetNextListenerID( true, &netAdr ); + *ppData = pNewSocket; +} + +void CRConServer::OnSocketClosed( SocketHandle_t hSocket, const netadr_t &netAdr, void* pData ) +{ + m_bSocketDeleted = true; + ConnectedRConSocket_t *pOldSocket = (ConnectedRConSocket_t*)( pData ); + delete pOldSocket; +} + + + +//----------------------------------------------------------------------------- +// Purpose: accept new connections and walk open sockets and handle any incoming data +//----------------------------------------------------------------------------- +void CRConServer::RunFrame() +{ + m_Socket.RunFrame(); + m_bSocketDeleted = false; + + // handle incoming data + // NOTE: Have to iterate in reverse since we may be killing sockets + int nCount = m_Socket.GetAcceptedSocketCount(); + for ( int i = nCount - 1; i >= 0; --i ) + { + // process any outgoing data for this socket + ConnectedRConSocket_t *pData = GetSocketData( i ); + SocketHandle_t hSocket = m_Socket.GetAcceptedSocketHandle( i ); + const netadr_t& socketAdr = m_Socket.GetAcceptedSocketAddress( i ); + while ( pData->m_OutstandingSends.Count() > 0 ) + { + CUtlBuffer &packet = pData->m_OutstandingSends[ pData->m_OutstandingSends.Head()]; + bool bSent = SendRCONResponse( i, packet.PeekGet(), packet.TellPut() - packet.TellGet(), true ); + if ( bSent ) // all this packet was sent, remove it + { + pData->m_OutstandingSends.Remove( pData->m_OutstandingSends.Head() ); // delete this entry no matter what, SendRCONResponse() will re-queue if needed + } + else // must have blocked part way through, SendRCONResponse + // fixed up the queued entry + { + break; + } + } + + int sendLen = g_ServerRemoteAccess.GetDataResponseSize( pData->listenerID ); + if ( sendLen > 0 ) + { + char sendBuf[4096]; + char *pBuf = sendBuf; + bool bAllocate = ( sendLen + sizeof(int) > sizeof(sendBuf) ); + if ( bAllocate ) + { + pBuf = new char[sendLen + sizeof(int)]; + } + memcpy( pBuf, &sendLen, sizeof(sendLen) ); // copy the size of the packet in + g_ServerRemoteAccess.ReadDataResponse( pData->listenerID, pBuf + sizeof(int), sendLen ); + SendRCONResponse( i, pBuf, sendLen + sizeof(int) ); + if ( bAllocate ) + { + delete [] pBuf; + } + } + + // check for incoming data + int pendingLen = 0; + unsigned long readLen = 0; + char ch; + pendingLen = recv( hSocket, &ch, sizeof(ch), MSG_PEEK ); + if ( pendingLen == -1 && SocketWouldBlock() ) + continue; + + if ( pendingLen == 0 ) + { + m_Socket.CloseAcceptedSocket( i ); + continue; + } + + if ( pendingLen < 0 ) + { + //DevMsg( "RCON Cmd: peek error %s\n", NET_ErrorString(WSAGetLastError())); + m_Socket.CloseAcceptedSocket( i ); + continue; + } + + // find out how much we have to read + ioctlsocket( hSocket, FIONREAD, &readLen ); + if ( readLen > sizeof(int) ) // we have a command to process + { + CUtlBuffer & response = pData->packetbuffer; + response.EnsureCapacity( response.TellPut() + readLen ); + char *recvBuf = (char *)_alloca( min( 1024ul, readLen ) ); // a buffer used for recv() + unsigned int len = 0; + while ( len < readLen ) + { + int recvLen = recv( hSocket, recvBuf , min(1024ul, readLen - len) , 0 ); + if ( recvLen == 0 ) // socket was closed + { + m_Socket.CloseAcceptedSocket( i ); + break; + } + + if ( recvLen < 0 && !SocketWouldBlock() ) + { + Warning( "RCON Cmd: recv error (%s)\n", NET_ErrorString( WSAGetLastError() ) ); + break; + } + + response.Put( recvBuf, recvLen ); + len += recvLen; + } + + response.SeekGet( CUtlBuffer::SEEK_HEAD, 0 ); + + int size = response.GetInt(); + + if ( sv_rcon_maxpacketsize.GetInt() > 0 && size > sv_rcon_maxpacketsize.GetInt() ) + { + if ( sv_rcon_maxpacketbans.GetBool() ) + { + HandleFailedRconAuth( socketAdr ); + } + + m_Socket.CloseAcceptedSocket( i ); + continue; + } + + while ( size > 0 && size <= response.TellPut() - response.TellGet() ) + { + SV_RedirectStart( RD_SOCKET, &socketAdr ); + g_ServerRemoteAccess.WriteDataRequest( this, pData->listenerID, response.PeekGet(), size ); + SV_RedirectEnd(); + if ( m_bSocketDeleted ) + return; + response.SeekGet( CUtlBuffer::SEEK_CURRENT, size ); // eat up the buffer we just sent + + if ( response.TellPut() - response.TellGet() >= sizeof(int) ) + { + size = response.GetInt(); // read how much is in this packet + } + else + { + size = 0; // finished the packet + } + } + + // Check and see if socket was closed as a result of processing - this can happen if the user has entered too many passwords + int nNewCount = m_Socket.GetAcceptedSocketCount(); + if ( 0 == nNewCount || i > nNewCount || pData != GetSocketData( i ) ) + { + response.Purge(); + break; + } + + if ( size > 0 || (response.TellPut() - response.TellGet() > 0)) + { + // trim the bytes that were just processed + CUtlBuffer tmpBuf; + if ( response.TellPut() - response.TellGet() > 0 ) + { + tmpBuf.Put( response.PeekGet(), response.TellPut() - response.TellGet() ); + } + + response.Purge(); + + if ( size > 0 ) + { + response.Put( &size, sizeof(size)); + } + + if ( tmpBuf.TellPut() > 0 ) + { + response.Put( tmpBuf.Base(), tmpBuf.TellPut() ); + } + } + else + { + response.Purge(); + } + } + } // for each socket +} + + +//----------------------------------------------------------------------------- +// Purpose: flush the response of a network command back to a user +//----------------------------------------------------------------------------- +void CRConServer::FinishRedirect( const char *msg, const netadr_t &adr ) +{ + // NOTE: Has to iterate in reverse; SendRCONResponse can close sockets + int nCount = m_Socket.GetAcceptedSocketCount(); + for ( int i = nCount - 1; i >= 0; --i ) + { + const netadr_t& socketAdr = m_Socket.GetAcceptedSocketAddress( i ); + if ( !adr.CompareAdr( socketAdr ) ) + continue; + + CUtlBuffer response; + + // build the response + ConnectedRConSocket_t *pSocketData = GetSocketData( i ); + response.PutInt(0); // the size, this gets set once we make the packet + response.PutInt(pSocketData->lastRequestID); + response.PutInt(SERVERDATA_RESPONSE_VALUE); + response.PutString(msg); + response.PutString(""); + int size = response.TellPut() - sizeof(int); + response.SeekPut( CUtlBuffer::SEEK_HEAD, 0 ); + response.PutInt(size); // the size + response.SeekPut( CUtlBuffer::SEEK_CURRENT, size ); + + +// OutputDebugString( va("RCON: String is %i long\n", Q_strlen(msg)) ); // can't use DevMsg(), we are potentially inside the RedirectFlush() function +// printf("RCON: String is %i long, packet size %i\n", Q_strlen(msg), size ); + + SendRCONResponse( i, response.Base(), response.TellPut() ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: set the current outstanding request ID for this connection, used by the redirect flush above +//----------------------------------------------------------------------------- +void CRConServer::SetRequestID( ra_listener_id listener, int iRequestID ) +{ + int nCount = m_Socket.GetAcceptedSocketCount(); + for ( int i = 0; i < nCount; i++ ) + { + ConnectedRConSocket_t *pSocketData = GetSocketData( i ); + if ( pSocketData->listenerID == listener) + { + pSocketData->lastRequestID = iRequestID; + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: send a buffer to a particular connection +//----------------------------------------------------------------------------- +bool CRConServer::SendRCONResponse( int nIndex, const void *data, int len, bool fromQueue ) +{ + SocketHandle_t hSocket = m_Socket.GetAcceptedSocketHandle( nIndex ); + if ( hSocket < 0 ) + return false; + + ConnectedRConSocket_t *pSocketData = GetSocketData( nIndex ); + + // if we already have queued data pending then just add this to the end + // of the queue + if ( !fromQueue && pSocketData->m_OutstandingSends.Count() > 0 ) + { + if ( pSocketData->m_OutstandingSends.Count() > RCON_MAX_OUTSTANDING_SENDS ) + { + m_Socket.CloseAcceptedSocket( nIndex ); + return false; + } + + int index = pSocketData->m_OutstandingSends.AddToTail(); + pSocketData->m_OutstandingSends[index].Put( data, len ); + return true; + } + + Assert( !( fromQueue && data != (pSocketData->m_OutstandingSends[pSocketData->m_OutstandingSends.Head()].Base()))); + + int sendLen = 0; + while ( sendLen < len ) + { + int ret = send( hSocket, (const char *)data + sendLen, len - sendLen, MSG_NOSIGNAL ); + if ( ret == -1 ) + { + // can't finish sending this right now, push it back + // on the TOP of the queue to be sent next time around + if ( !SocketWouldBlock() ) + { + m_Socket.CloseAcceptedSocket( nIndex ); + return false; + } + + if ( !fromQueue ) // we don't have an entry for this + // yet, add a new one + { + int index = pSocketData->m_OutstandingSends.AddToHead(); + pSocketData->m_OutstandingSends[index].Put( (void *)((char *)data + sendLen), len - sendLen ); + } + else // update the existing queued item to show we + // sent some of it (we only ever send the head of the list) + { + pSocketData->m_OutstandingSends[pSocketData->m_OutstandingSends.Head()].SeekGet( CUtlBuffer::SEEK_CURRENT, sendLen ); + } + return false; + } + else if ( ret > 0 ) + { + sendLen += ret; + } + } +// printf("RCON: Sending packet %i in len\n", len); +// OutputDebugString( va("RCON: Sending packet %i in len\n", len) ); // can't use DevMsg(), we are potentially inside the RedirectFlush() function + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: compares failed rcons based on most recent failure time +//----------------------------------------------------------------------------- +bool CRConServer::FailedRCon_t::operator<(const struct CRConServer::FailedRCon_t &rhs) const +{ + int myTime = 0; + int rhsTime = 0; + + if ( badPasswordTimes.Count() ) + myTime = badPasswordTimes[ badPasswordTimes.Count() - 1 ]; + + if ( rhs.badPasswordTimes.Count() ) + rhsTime = rhs.badPasswordTimes[ rhs.badPasswordTimes.Count() - 1 ]; + + return myTime < rhsTime; +} + +//----------------------------------------------------------------------------- +// Purpose: tracks failed rcon attempts and bans repeat offenders +//----------------------------------------------------------------------------- +bool CRConServer::HandleFailedRconAuth( const netadr_t & adr ) +{ + if ( sv_rcon_whitelist_address.GetString()[0] ) + { + if ( !V_strcmp( adr.ToString( true ), sv_rcon_whitelist_address.GetString() ) ) + { + ConMsg( "Rcon auth failed from rcon whitelist address %s\n", adr.ToString() ); + return false; + } + } + + int i; + FailedRCon_t *failedRcon = NULL; + int nCount = m_failedRcons.Count(); + for ( i=0; i < nCount; ++i ) + { + if ( adr.CompareAdr( m_failedRcons[i].adr, true ) ) + { + failedRcon = &m_failedRcons[i]; + break; + } + } + + if ( !failedRcon ) + { + // remove an old rcon if necessary + if ( nCount >= 32 ) + { + // look for the one with the oldest failure + int indexToRemove = -1; + for ( i=0; i < nCount; ++i ) + { + if ( indexToRemove < 0 || m_failedRcons[i] < m_failedRcons[indexToRemove] ) + { + indexToRemove = i; + } + } + if ( indexToRemove >= 0 ) + { + m_failedRcons.Remove( indexToRemove ); + } + } + + // add the new rcon + int index = m_failedRcons.AddToTail(); + failedRcon = &m_failedRcons[index]; + failedRcon->adr = adr; + failedRcon->badPasswordCount = 0; + failedRcon->badPasswordTimes.RemoveAll(); + } + + // update this failed rcon + ++failedRcon->badPasswordCount; + failedRcon->badPasswordTimes.AddToTail( sv.GetTime() ); + + // remove old failure times (sv_rcon_maxfailures is limited to 20, so we won't be hurting anything by pruning) + while ( failedRcon->badPasswordTimes.Count() > 20 ) + { + failedRcon->badPasswordTimes.Remove( 0 ); + } + + // sanity-check the rcon banning cvars + if ( sv_rcon_maxfailures.GetInt() < sv_rcon_minfailures.GetInt() ) + { + int temp = sv_rcon_maxfailures.GetInt(); + sv_rcon_maxfailures.SetValue( sv_rcon_minfailures.GetInt() ); + sv_rcon_minfailures.SetValue( temp ); + } + +// ConMsg( "%d of %d bad password times tracked\n", failedRcon->badPasswordTimes.Count(), failedRcon->badPasswordCount ); +// ConMsg( "min=%d, max=%d, time=%.2f\n", sv_rcon_minfailures.GetInt(), sv_rcon_maxfailures.GetInt(), sv_rcon_minfailuretime.GetFloat() ); + + // check if the user should be banned based on total failed attempts + if ( failedRcon->badPasswordCount > sv_rcon_maxfailures.GetInt() ) + { + ConMsg( "Banning %s for rcon hacking attempts\n", failedRcon->adr.ToString( true ) ); + Cbuf_AddText( va( "addip %i %s\n", sv_rcon_banpenalty.GetInt(), failedRcon->adr.ToString( true ) ) ); + Cbuf_Execute(); + return true; + } + + // check if the user should be banned based on recent failed attempts + int recentFailures = 0; + for ( i=failedRcon->badPasswordTimes.Count()-1; i>=0; --i ) + { + if ( failedRcon->badPasswordTimes[i] + sv_rcon_minfailuretime.GetFloat() >= sv.GetTime() ) + { + ++recentFailures; + } + } + if ( recentFailures > sv_rcon_minfailures.GetInt() ) + { + ConMsg( "Banning %s for rcon hacking attempts\n", failedRcon->adr.ToString( true ) ); + Cbuf_AddText( va( "addip %i %s\n", sv_rcon_banpenalty.GetInt(), failedRcon->adr.ToString( true ) ) ); + Cbuf_Execute(); + return true; + } + + return false; +} + +bool CRConServer::BCloseAcceptedSocket( ra_listener_id listener ) +{ + int nCount = m_Socket.GetAcceptedSocketCount(); + for ( int i = 0; i < nCount; i++ ) + { + ConnectedRConSocket_t *pSocketData = GetSocketData( i ); + if ( pSocketData->listenerID == listener ) + { + m_Socket.CloseAcceptedSocket( i ); + return true; + } + } + return false; +} |