/* sv_filter.c - server ID/IP filter Copyright (C) 2017 a1batross 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 3 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. */ #include "common.h" #include "server.h" /* ============================================================================= PLAYER ID FILTER ============================================================================= */ typedef struct cidfilter_s { float endTime; struct cidfilter_s *next; string id; } cidfilter_t; static cidfilter_t *cidfilter = NULL; static void SV_RemoveID( const char *id ) { cidfilter_t *filter, *prevfilter = NULL; for( filter = cidfilter; filter; filter = filter->next ) { if( Q_strcmp( filter->id, id )) { prevfilter = filter; continue; } if( filter == cidfilter ) { cidfilter = cidfilter->next; Mem_Free( filter ); return; } if( prevfilter ) prevfilter->next = filter->next; Mem_Free( filter ); return; } } qboolean SV_CheckID( const char *id ) { qboolean ret = false; cidfilter_t *filter; for( filter = cidfilter; filter; filter = filter->next ) { int len1 = Q_strlen( id ), len2 = Q_strlen( filter->id ); int len = Q_min( len1, len2 ); while( filter->endTime && host.realtime > filter->endTime ) { char *fid = filter->id; filter = filter->next; SV_RemoveID( fid ); if( !filter ) return false; } if( !Q_strncmp( id, filter->id, len )) { ret = true; break; } } return ret; } static void SV_BanID_f( void ) { float time = Q_atof( Cmd_Argv( 1 )); const char *id = Cmd_Argv( 2 ); sv_client_t *cl = NULL; cidfilter_t *filter; if( time ) time = host.realtime + time * 60.0f; if( !id[0] ) { Con_Reportf( S_USAGE "banid <#userid or unique id>\n0 minutes for permanent ban\n" ); return; } if( !svs.clients ) { Con_Reportf( S_ERROR "banid: no players\n" ); return; } if( id[0] == '#' ) { Con_Printf( S_ERROR "banid: not supported\n" ); return; #if 0 int i = Q_atoi( &id[1] ); cl = SV_ClientById( i ); if( !cl ) { Con_Printf( S_ERROR "banid: no such player with userid %d\n", i ); return; } #endif } else { size_t len; int i; if( !Q_strnicmp( id, "STEAM_", 6 ) || !Q_strnicmp( id, "VALVE_", 6 )) id += 6; if( !Q_strnicmp( id, "XASH_", 5 )) id += 5; len = Q_strlen( id ); for( i = 0; i < sv_maxclients.value; i++ ) { if( FBitSet( svs.clients[i].flags, FCL_FAKECLIENT )) continue; if( svs.clients[i].state != cs_spawned ) continue; if( !Q_strncmp( id, Info_ValueForKey( svs.clients[i].useragent, "uuid" ), len )) { cl = &svs.clients[i]; break; } } if( !cl ) { Con_Printf( S_ERROR "banid: no such player with userid %s\n", id ); return; } } id = Info_ValueForKey( cl->useragent, "uuid" ); SV_RemoveID( id ); filter = Mem_Malloc( host.mempool, sizeof( cidfilter_t )); filter->endTime = time; filter->next = cidfilter; Q_strncpy( filter->id, id, sizeof( filter->id )); cidfilter = filter; if( cl && !Q_stricmp( Cmd_Argv( Cmd_Argc() - 1 ), "kick" )) Cbuf_AddTextf( "kick #%d \"Kicked and banned\"\n", cl->userid ); } static void SV_ListID_f( void ) { cidfilter_t *filter; Con_Reportf( "id ban list\n" ); Con_Reportf( "-----------\n" ); for( filter = cidfilter; filter; filter = filter->next ) { if( filter->endTime && host.realtime > filter->endTime ) continue; // no negative time if( filter->endTime ) Con_Reportf( "%s expries in %f minutes\n", filter->id, ( filter->endTime - host.realtime ) / 60.0f ); else Con_Reportf( "%s permanent\n", filter->id ); } } static void SV_RemoveID_f( void ) { const char *id = Cmd_Argv( 1 ); if( id[0] == '#' && svs.clients ) { int num = Q_atoi( id + 1 ); if( num >= sv_maxclients.value || num < 0 ) return; id = Info_ValueForKey( svs.clients[num].useragent, "uuid" ); } if( !id[0] ) { Con_Reportf( S_USAGE "removeid <#slotnumber or uniqueid>\n"); return; } SV_RemoveID( id ); } static void SV_WriteID_f( void ) { file_t *f = FS_Open( Cvar_VariableString( "bannedcfgfile" ), "w", false ); cidfilter_t *filter; if( !f ) { Con_DPrintf( S_ERROR "Could not write %s\n", Cvar_VariableString( "bannedcfgfile" )); return; } FS_Printf( f, "//=======================================================================\n" ); FS_Printf( f, "//\t\tCopyright Flying With Gauss Team %s ©\n", Q_timestamp( TIME_YEAR_ONLY )); FS_Printf( f, "//\t\t %s - archive of id blacklist\n", Cvar_VariableString( "bannedcfgfile" )); FS_Printf( f, "//=======================================================================\n" ); for( filter = cidfilter; filter; filter = filter->next ) if( !filter->endTime ) // only permanent FS_Printf( f, "banid 0 %s\n", filter->id ); FS_Close( f ); } static void SV_InitIDFilter( void ) { Cmd_AddRestrictedCommand( "banid", SV_BanID_f, "ban player by ID" ); Cmd_AddRestrictedCommand( "listid", SV_ListID_f, "list banned players" ); Cmd_AddRestrictedCommand( "removeid", SV_RemoveID_f, "remove player from banned list" ); Cmd_AddRestrictedCommand( "writeid", SV_WriteID_f, "write banned.cfg" ); } static void SV_ShutdownIDFilter( void ) { cidfilter_t *cidList, *cidNext; // should be called manually because banned.cfg is not executed by engine //SV_WriteID_f(); Cmd_RemoveCommand( "banid" ); Cmd_RemoveCommand( "listid" ); Cmd_RemoveCommand( "removeid" ); Cmd_RemoveCommand( "writeid" ); for( cidList = cidfilter; cidList; cidList = cidNext ) { cidNext = cidList->next; Mem_Free( cidList ); } cidfilter = NULL; } /* ============================================================================= CLIENT IP FILTER ============================================================================= */ typedef struct ipfilter_s { float endTime; struct ipfilter_s *next; netadr_t adr; uint prefixlen; } ipfilter_t; static ipfilter_t *ipfilter = NULL; static void SV_CleanExpiredIPFilters( void ) { ipfilter_t *f, **back; back = &ipfilter; while( 1 ) { f = *back; if( !f ) return; if( f->endTime && host.realtime > f->endTime ) { *back = f->next; back = &f->next; Mem_Free( f ); } else back = &f->next; } } static int SV_FilterToString( char *dest, size_t size, qboolean config, ipfilter_t *f ) { const char *strformat; if( config ) { return Q_snprintf( dest, size, "addip 0 %s/%d\n", NET_AdrToString( f->adr ), f->prefixlen ); } else if( f->endTime ) { return Q_snprintf( dest, size, "%s/%d (%f minutes)", NET_AdrToString( f->adr ), f->prefixlen, f->endTime ); } return Q_snprintf( dest, size, "%s/%d (permanent)", NET_AdrToString( f->adr ), f->prefixlen ); } static qboolean SV_IPFilterIncludesIPFilter( ipfilter_t *a, ipfilter_t *b ) { if( a->adr.type6 != b->adr.type6 ) return false; // can't include bigger subnet in small if( a->prefixlen < b->prefixlen ) return false; if( a->prefixlen == b->prefixlen ) return NET_CompareAdr( a->adr, b->adr ); return NET_CompareAdrByMask( a->adr, b->adr, b->prefixlen ); } static void SV_RemoveIPFilter( ipfilter_t *toremove, qboolean removeAll, qboolean verbose ) { ipfilter_t *f, **back; back = &ipfilter; while( 1 ) { f = *back; if( !f ) return; if( SV_IPFilterIncludesIPFilter( toremove, f )) { if( verbose ) { string filterStr; SV_FilterToString( filterStr, sizeof( filterStr ), false, f ); Con_Printf( "%s removed.\n", filterStr ); } *back = f->next; back = &f->next; Mem_Free( f ); if( !removeAll ) break; } else back = &f->next; } } qboolean SV_CheckIP( netadr_t *adr ) { // TODO: ip rate limit ipfilter_t *entry = ipfilter; for( ; entry; entry = entry->next ) { switch( entry->adr.type6 ) { case NA_IP: case NA_IP6: if( NET_CompareAdrByMask( *adr, entry->adr, entry->prefixlen )) return true; break; } } return false; } static void SV_AddIP_PrintUsage( void ) { Con_Printf(S_USAGE "addip \n" S_USAGE_INDENT "addip \n" "Use 0 minutes for permanent\n" "ipaddress A.B.C.D/24 is equivalent to A.B.C.0 and A.B.C\n" "NOTE: IPv6 addresses only support prefix format!\n"); } static void SV_RemoveIP_PrintUsage( void ) { Con_Printf(S_USAGE "removeip [removeAll]\n" S_USAGE_INDENT "removeip [removeAll]\n" "Use removeAll to delete all ip filters which ipaddress or ipaddress/CIDR includes\n"); } static void SV_ListIP_PrintUsage( void ) { Con_Printf(S_USAGE "listip [ipaddress]\n" S_USAGE_INDENT "listip [ipaddress/CIDR]\n"); } static void SV_AddIP_f( void ) { const char *szMinutes = Cmd_Argv( 1 ); const char *adr = Cmd_Argv( 2 ); ipfilter_t filter, *newfilter; float minutes; int i; if( Cmd_Argc() != 3 ) { // a1ba: kudos to rehlds for an idea of using CIDR prefixes // in these commands :) SV_AddIP_PrintUsage(); return; } minutes = Q_atof( szMinutes ); if( minutes < 0.1f ) minutes = 0; if( minutes != 0.0f ) filter.endTime = host.realtime + minutes * 60; else filter.endTime = 0; if( !NET_StringToFilterAdr( adr, &filter.adr, &filter.prefixlen )) { Con_Printf( "Invalid IP address!\n" ); SV_AddIP_PrintUsage(); return; } newfilter = Mem_Malloc( host.mempool, sizeof( *newfilter )); newfilter->endTime = filter.endTime; newfilter->adr = filter.adr; newfilter->prefixlen = filter.prefixlen; newfilter->next = ipfilter; ipfilter = newfilter; for( i = 0; i < svs.maxclients; i++ ) { netadr_t clientadr = svs.clients[i].netchan.remote_address; if( !NET_CompareAdrByMask( clientadr, filter.adr, filter.prefixlen )) continue; SV_ClientPrintf( &svs.clients[i], "The server operator has added you to banned list\n" ); SV_DropClient( &svs.clients[i], false ); } } static void SV_ListIP_f( void ) { qboolean haveFilter = false; ipfilter_t filter, *f; if( Cmd_Argc() > 2 ) { SV_ListIP_PrintUsage(); return; } if( ipfilter == NULL ) { Con_Printf( "IP filter list is empty\n" ); return; } if( Cmd_Argc() == 2 ) { haveFilter = NET_StringToFilterAdr( Cmd_Argv( 1 ), &filter.adr, &filter.prefixlen ); if( !haveFilter ) { Con_Printf( "Invalid IP address!\n" ); SV_ListIP_PrintUsage(); return; } } Con_Printf( "IP filter list:\n" ); for( f = ipfilter; f; f = f->next ) { string filterStr; if( haveFilter && !SV_IPFilterIncludesIPFilter( &filter, f )) continue; SV_FilterToString( filterStr, sizeof( filterStr ), false, f ); Con_Printf( "%s\n", filterStr ); } } static void SV_RemoveIP_f( void ) { const char *adr = Cmd_Argv( 1 ); qboolean removeAll; ipfilter_t filter; int i; if( Cmd_Argc() != 2 && Cmd_Argc() != 3 ) { SV_RemoveIP_PrintUsage(); return; } removeAll = Cmd_Argc() == 3 && !Q_strcmp( Cmd_Argv( 2 ), "removeAll" ); if( !NET_StringToFilterAdr( adr, &filter.adr, &filter.prefixlen )) { Con_Printf( "Invalid IP address!\n" ); SV_RemoveIP_PrintUsage(); return; } SV_RemoveIPFilter( &filter, removeAll, true ); } static void SV_WriteIP_f( void ) { file_t *fd = FS_Open( Cvar_VariableString( "listipcfgfile" ), "w", true ); ipfilter_t *f; if( !fd ) { Con_Printf( "Couldn't open listip.cfg\n" ); return; } for( f = ipfilter; f; f = f->next ) { string filterStr; int size; // do not save temporary bans if( f->endTime ) continue; size = SV_FilterToString( filterStr, sizeof( filterStr ), true, f ); FS_Write( fd, filterStr, size ); } FS_Close( fd ); } static void SV_InitIPFilter( void ) { Cmd_AddRestrictedCommand( "addip", SV_AddIP_f, "add entry to IP filter" ); Cmd_AddRestrictedCommand( "listip", SV_ListIP_f, "list current IP filter" ); Cmd_AddRestrictedCommand( "removeip", SV_RemoveIP_f, "remove IP filter" ); Cmd_AddRestrictedCommand( "writeip", SV_WriteIP_f, "write listip.cfg" ); } static void SV_ShutdownIPFilter( void ) { ipfilter_t *ipList, *ipNext; // should be called manually because banned.cfg is not executed by engine //SV_WriteIP_f(); for( ipList = ipfilter; ipList; ipList = ipNext ) { ipNext = ipList->next; Mem_Free( ipList ); } ipfilter = NULL; } void SV_InitFilter( void ) { SV_InitIPFilter(); SV_InitIDFilter(); } void SV_ShutdownFilter( void ) { SV_ShutdownIPFilter(); SV_ShutdownIDFilter(); } #if XASH_ENGINE_TESTS #include "tests.h" void Test_StringToFilterAdr( void ) { ipfilter_t f1; int i; struct { const char *str; qboolean valid; int prefixlen; int a, b, c, d; } ipv4tests[] = { { "127.0.0.0/8", true, 8, 127, 0, 0, 0 }, { "192.168", true, 16, 192, 168, 0, 0 }, { "192.168/23", true, 23, 192, 168, 0, 0 }, { "192.168./23", true, 23, 192, 168, 0, 0 }, { "192.168../23", true, 23, 192, 168, 0, 0 }, { "..192...168/23", false }, { "", false }, { "abcd", false } }; struct { const char *str; qboolean valid; int prefixlen; uint8_t x[16]; } ipv6tests[] = { { "::1", true, 128, { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 } }, { "fd18:b9d4:65cf:83de::/64", true, 64, { 0xfd, 0x18, 0xb9, 0xd4, 0x65, 0xcf, 0x83, 0xde } }, { "kkljnljkhfjnkj", false }, { "fd8a:63d5:e014:0d62:ffff:ffff:ffff:ffff:ffff", false }, }; for( i = 0; i < ARRAYSIZE( ipv4tests ); i++ ) { qboolean ret = NET_StringToFilterAdr( ipv4tests[i].str, &f1.adr, &f1.prefixlen ); TASSERT_EQi( ret, ipv4tests[i].valid ); if( ret ) { TASSERT_EQi( f1.prefixlen, ipv4tests[i].prefixlen ); TASSERT_EQi( f1.adr.ip[0], ipv4tests[i].a ); TASSERT_EQi( f1.adr.ip[1], ipv4tests[i].b ); TASSERT_EQi( f1.adr.ip[2], ipv4tests[i].c ); TASSERT_EQi( f1.adr.ip[3], ipv4tests[i].d ); } } for( i = 0; i < ARRAYSIZE( ipv6tests ); i++ ) { qboolean ret = NET_StringToFilterAdr( ipv6tests[i].str, &f1.adr, &f1.prefixlen ); uint8_t x[16]; TASSERT_EQi( ret, ipv6tests[i].valid ); if( ret ) { TASSERT_EQi( f1.prefixlen, ipv6tests[i].prefixlen ); NET_NetadrToIP6Bytes( (uint8_t*)x, &f1.adr ); TASSERT( memcmp( x, ipv6tests[i].x, sizeof( x )) == 0 ); } } } void Test_IPFilterIncludesIPFilter( void ) { qboolean ret; const char *adrs[] = { "127.0.0.1/8", // 0 "127.0.0.1", // 1 "192.168/16", // 2 "fe80::/64", // 3 "fe80::96ab:9a49:2944:1808", // 4 "2a00:1370:8190:f9eb::/62", // 5 "2a00:1370:8190:f9eb:3866:6126:330c:b82b" // 6 }; ipfilter_t f[7]; int i; int tests[][3] = { // ipv4 { 0, 0, true }, { 0, 1, false }, { 1, 0, true }, { 0, 2, false }, { 2, 0, false }, // mixed { 0, 3, false }, { 1, 4, false }, // ipv6 { 3, 3, true }, { 3, 4, false }, { 4, 3, true }, { 5, 3, false }, { 3, 5, false }, { 6, 5, true }, }; for( i = 0; i < 7; i++ ) { NET_StringToFilterAdr( adrs[i], &f[i].adr, &f[i].prefixlen ); } for( i = 0; i < ARRAYSIZE( tests ); i++ ) { ret = SV_IPFilterIncludesIPFilter( &f[tests[i][0]], &f[tests[i][1]] ); TASSERT_EQi( ret, tests[i][2] ); } } void Test_RunIPFilter( void ) { Test_StringToFilterAdr(); Test_IPFilterIncludesIPFilter(); } #endif // XASH_ENGINE_TESTS