From 2b8b3e199386c078f098100116bf1b7266326139 Mon Sep 17 00:00:00 2001 From: Alibek Omarov Date: Thu, 9 Jun 2022 03:07:19 +0300 Subject: [PATCH] engine: server: new IP filter, rewritten with IPv6 in mind --- common/netadr.h | 8 +- engine/common/com_strings.h | 1 + engine/common/host.c | 11 +- engine/common/net_ws.c | 183 +++++++++- engine/common/net_ws.h | 4 +- engine/common/tests.h | 19 +- engine/server/sv_filter.c | 684 ++++++++++++++++++++++++------------ 7 files changed, 674 insertions(+), 236 deletions(-) diff --git a/common/netadr.h b/common/netadr.h index 99eb15c7..c56f517f 100644 --- a/common/netadr.h +++ b/common/netadr.h @@ -50,8 +50,12 @@ typedef struct netadr_s struct { uint32_t type; - uint8_t ip[4]; // or last 4 IPv6 octets - uint8_t ipx[10]; // or first 10 IPv6 octets + union + { + uint8_t ip[4]; + uint32_t ip4; + }; + uint8_t ipx[10]; }; struct { diff --git a/engine/common/com_strings.h b/engine/common/com_strings.h index fcb40d41..bc495b42 100644 --- a/engine/common/com_strings.h +++ b/engine/common/com_strings.h @@ -21,6 +21,7 @@ GNU General Public License for more details. #define S_WARN "^3Warning:^7 " #define S_ERROR "^1Error:^7 " #define S_USAGE "Usage: " +#define S_USAGE_INDENT " " #define S_OPENGL_NOTE "^2OpenGL Note:^7 " #define S_OPENGL_WARN "^3OpenGL Warning:^7 " diff --git a/engine/common/host.c b/engine/common/host.c index d3ca35b8..7b55e802 100644 --- a/engine/common/host.c +++ b/engine/common/host.c @@ -818,18 +818,15 @@ static void Host_RunTests( int stage ) { case 0: // early engine load memset( &tests_stats, 0, sizeof( tests_stats )); - Test_RunLibCommon(); - Test_RunCommon(); - Test_RunCmd(); - Test_RunCvar(); + TEST_LIST_0; #if !XASH_DEDICATED - Test_RunCon(); + TEST_LIST_0_CLIENT; #endif /* XASH_DEDICATED */ break; case 1: // after FS load - Test_RunImagelib(); + TEST_LIST_1; #if !XASH_DEDICATED - Test_RunVOX(); + TEST_LIST_1_CLIENT; #endif Msg( "Done! %d passed, %d failed\n", tests_stats.passed, tests_stats.failed ); Sys_Quit(); diff --git a/engine/common/net_ws.c b/engine/common/net_ws.c index 3aef0752..79dce12e 100644 --- a/engine/common/net_ws.c +++ b/engine/common/net_ws.c @@ -581,6 +581,118 @@ static int NET_StringToSockaddr( const char *s, struct sockaddr_storage *sadr, q return 1; } +/* +==================== +NET_StringToFilterAdr + +==================== +*/ +qboolean NET_StringToFilterAdr( const char *s, netadr_t *adr, uint *prefixlen ) +{ + char copy[128], *temp; + qboolean hasCIDR = false; + byte ip6[16]; + uint len; + + if( !COM_CheckStringEmpty( s )) + return false; + + memset( adr, 0, sizeof( *adr )); + + // copy the string and remove CIDR prefix + Q_strncpy( copy, s, sizeof( copy )); + temp = Q_strrchr( copy, '/' ); + + if( temp ) + { + *temp = 0; + if( Q_isdigit( temp + 1 )) + { + len = Q_atoi( temp + 1 ); + hasCIDR = len != 0; + } + } + + // try to parse as IPv6 first + if( ParseIPv6Addr( copy, ip6, NULL, NULL )) + { + NET_IP6BytesToNetadr( adr, ip6 ); + adr->type6 = NA_IP6; + + if( !hasCIDR ) + *prefixlen = 128; + else + *prefixlen = len; + } + else + { + int num = 0; + int octet = 0; + + // parse as ipv4 but we don't need to allow all forms here + for( temp = copy; *temp; temp++ ) + { + char c = *temp; + + if( c >= '0' && c <= '9' ) + { + num *= 10; + num += c - '0'; + } + else if( c == '.' ) + { + if( num > 255 ) + return false; + + adr->ip[octet++] = num; + num = 0; + + if( octet > 3 ) + return false; + } + else + { + return false; + } + } + + if( num > 255 ) + return false; + + adr->ip[octet++] = num; + + if( !hasCIDR ) + { + int i; + + *prefixlen = 32; + + for( i = 3; i >= 0; i-- ) + { + if( !adr->ip[i] ) + *prefixlen -= 8; + else + break; + } + } + else + { + uint32_t mask; + + len = bound( 0, len, 32 ); + *prefixlen = len; + + // drop unneeded bits + mask = htonl( adr->ip4 ) & ( 0xFFFFFFFF << ( 32 - len )); + adr->ip4 = ntohl( mask ); + } + + adr->type = NA_IP; + } + + return true; +} + /* ==================== NET_AdrToString @@ -635,17 +747,14 @@ Compares without the port */ qboolean NET_CompareBaseAdr( const netadr_t a, const netadr_t b ) { - if( a.type != b.type ) + if( a.type6 != b.type6 ) return false; if( a.type == NA_LOOPBACK ) return true; if( a.type == NA_IP ) - { - if( !memcmp( a.ip, b.ip, 4 )) - return true; - } + return a.ip4 == b.ip4; if( a.type6 == NA_IP6 ) { @@ -663,9 +772,9 @@ NET_CompareClassBAdr Compare local masks ==================== */ -qboolean NET_CompareClassBAdr( netadr_t a, netadr_t b ) +qboolean NET_CompareClassBAdr( const netadr_t a, const netadr_t b ) { - if( a.type != b.type ) + if( a.type6 != b.type6 ) return false; if( a.type == NA_LOOPBACK ) @@ -681,6 +790,60 @@ qboolean NET_CompareClassBAdr( netadr_t a, netadr_t b ) // this check is very dumb and only used for LAN restriction // Actual check is in IsReservedAdr + // for real mask compare use NET_CompareAdrByMask + + return false; +} + +/* +==================== +NET_CompareAdrByMask + +Checks if adr is a part of subnet +==================== +*/ +qboolean NET_CompareAdrByMask( const netadr_t a, const netadr_t b, uint prefixlen ) +{ + if( a.type6 != b.type6 || a.type == NA_LOOPBACK ) + return false; + + if( a.type == NA_IP ) + { + uint32_t ipa = htonl( a.ip4 ); + uint32_t ipb = htonl( b.ip4 ); + + if(( ipa & (( 0xFFFFFFFFU ) << ( 32 - prefixlen ))) == ipb ) + return true; + } + else if( a.type6 == NA_IP6 ) + { + uint16_t a_[8], b_[8]; + size_t check = prefixlen / 16; + size_t remaining = prefixlen % 16; + + // convert to 16-bit pieces first + NET_NetadrToIP6Bytes( (uint8_t*)a_, &a ); + NET_NetadrToIP6Bytes( (uint8_t*)b_, &b ); + + // check complete hextets first, if not equal, then it's different subnets + if( check && memcmp( a_, b_, check * sizeof( uint16_t ))) + return false; + + // check by bits now, similar to v4 check but with 16-bit type + if( remaining ) + { + uint16_t hexa, hexb, mask = 0xFFFFU << ( 16 - remaining ); + + hexa = htons( a_[check] ); + hexb = htons( b_[check] ); + + if(( hexa & mask ) == ( hexb & mask )) + return true; + } + else + return true; + } + return false; } @@ -739,7 +902,7 @@ Compare full address */ qboolean NET_CompareAdr( const netadr_t a, const netadr_t b ) { - if( a.type != b.type ) + if( a.type6 != b.type6 ) return false; if( a.type == NA_LOOPBACK ) @@ -747,7 +910,7 @@ qboolean NET_CompareAdr( const netadr_t a, const netadr_t b ) if( a.type == NA_IP ) { - if( !memcmp( a.ip, b.ip, 4 ) && a.port == b.port ) + if( a.ip4 == b.ip4 && a.port == b.port ) return true; return false; } @@ -1513,7 +1676,7 @@ static int NET_IPSocket( const char *net_iface, int port, int family ) if( family == AF_INET6 ) pfamily = PF_INET6; else if( family == AF_INET ) - pfamily == PF_INET; + pfamily = PF_INET; if( NET_IsSocketError(( net_socket = socket( pfamily, SOCK_DGRAM, IPPROTO_UDP )) ) ) { diff --git a/engine/common/net_ws.h b/engine/common/net_ws.h index 4ad83a1f..f5591a6d 100644 --- a/engine/common/net_ws.h +++ b/engine/common/net_ws.h @@ -56,11 +56,13 @@ qboolean NET_IsLocalAddress( netadr_t adr ); const char *NET_AdrToString( const netadr_t a ); const char *NET_BaseAdrToString( const netadr_t a ); qboolean NET_IsReservedAdr( netadr_t a ); -qboolean NET_CompareClassBAdr( netadr_t a, netadr_t b ); +qboolean NET_CompareClassBAdr( const netadr_t a, const netadr_t b ); qboolean NET_StringToAdr( const char *string, netadr_t *adr ); +qboolean NET_StringToFilterAdr( const char *s, netadr_t *adr, uint *prefixlen ); int NET_StringToAdrNB( const char *string, netadr_t *adr ); qboolean NET_CompareAdr( const netadr_t a, const netadr_t b ); qboolean NET_CompareBaseAdr( const netadr_t a, const netadr_t b ); +qboolean NET_CompareAdrByMask( const netadr_t a, const netadr_t b, uint prefixlen ); qboolean NET_GetPacket( netsrc_t sock, netadr_t *from, byte *data, size_t *length ); qboolean NET_BufferToBufferCompress( byte *dest, uint *destLen, byte *source, uint sourceLen ); qboolean NET_BufferToBufferDecompress( byte *dest, uint *destLen, byte *source, uint sourceLen ); diff --git a/engine/common/tests.h b/engine/common/tests.h index e58f20a5..a88b6b22 100644 --- a/engine/common/tests.h +++ b/engine/common/tests.h @@ -26,7 +26,7 @@ extern struct tests_stats_s tests_stats; #define TASSERT( exp ) \ _TASSERT( !(exp), Msg( S_ERROR "assert failed at %s:%i\n", __FILE__, __LINE__ ) ) #define TASSERT_EQi( val1, val2 ) \ - _TASSERT( ( val1 ) != ( val2 ), Msg( S_ERROR "assert failed at %s:%i, \"%d\" != \"%d\"\n", __FILE__, __LINE__, #val1, #val2 )) + _TASSERT( ( val1 ) != ( val2 ), Msg( S_ERROR "assert failed at %s:%i, \"%d\" != \"%d\"\n", __FILE__, __LINE__, val1, val2 )) #define TASSERT_STR( str1, str2 ) \ _TASSERT( Q_strcmp(( str1 ), ( str2 )), Msg( S_ERROR "assert failed at %s:%i, \"%s\" != \"%s\"\n", __FILE__, __LINE__, ( str1 ), ( str2 ))) @@ -37,6 +37,23 @@ void Test_RunCmd( void ); void Test_RunCvar( void ); void Test_RunCon( void ); void Test_RunVOX( void ); +void Test_RunIPFilter( void ); + +#define TEST_LIST_0 \ + Test_RunLibCommon(); \ + Test_RunCommon(); \ + Test_RunCmd(); \ + Test_RunCvar(); \ + Test_RunIPFilter(); + +#define TEST_LIST_0_CLIENT \ + Test_RunCon(); + +#define TEST_LIST_1 \ + Test_RunImagelib(); + +#define TEST_LIST_1_CLIENT \ + Test_RunVOX(); #endif diff --git a/engine/server/sv_filter.c b/engine/server/sv_filter.c index fa7164a0..d3d1228d 100644 --- a/engine/server/sv_filter.c +++ b/engine/server/sv_filter.c @@ -17,21 +17,13 @@ GNU General Public License for more details. #include "server.h" -typedef struct ipfilter_s -{ - float time; - float endTime; // -1 for permanent ban - struct ipfilter_s *next; - uint mask; - uint ip; -} ipfilter_t; +/* +============================================================================= -static ipfilter_t *ipfilter = NULL; - - -// TODO: Is IP filter really needed? -// TODO: Make it IPv6 compatible, for future expansion +PLAYER ID FILTER +============================================================================= +*/ typedef struct cidfilter_s { float endTime; @@ -67,32 +59,6 @@ static void SV_RemoveID( const char *id ) } } -static void SV_RemoveIP( uint ip, uint mask ) -{ - ipfilter_t *filter, *prevfilter = NULL; - - for( filter = ipfilter; filter; filter = filter->next ) - { - if( filter->ip != ip || mask != filter->mask ) - { - prevfilter = filter; - continue; - } - - if( filter == ipfilter ) - { - ipfilter = ipfilter->next; - Mem_Free( filter ); - return; - } - - if( prevfilter ) - prevfilter->next = filter->next; - Mem_Free( filter ); - return; - } -} - qboolean SV_CheckID( const char *id ) { qboolean ret = false; @@ -122,34 +88,6 @@ qboolean SV_CheckID( const char *id ) return ret; } -qboolean SV_CheckIP( netadr_t *addr ) -{ - uint ip = addr->ip[0] << 24 | addr->ip[1] << 16 | addr->ip[2] << 8 | addr->ip[3]; - qboolean ret = false; - ipfilter_t *filter; - - for( filter = ipfilter; filter; filter = filter->next ) - { - while( filter->endTime && host.realtime > filter->endTime ) - { - uint rip = filter->ip; - uint rmask = filter->mask; - SV_RemoveIP( rip, rmask ); - filter = filter->next; - if( !filter ) - return false; - } - - if( (ip & filter->mask) == (filter->ip & filter->mask) ) - { - ret = true; - break; - } - } - - return ret; -} - static void SV_BanID_f( void ) { float time = Q_atof( Cmd_Argv( 1 ) ); @@ -289,167 +227,25 @@ static void SV_WriteID_f( void ) FS_Close( f ); } -static qboolean StringToIP( const char *str, const char *maskstr, uint *outip, uint *outmask ) -{ - byte ip[4] = {0}; - byte mask[4] = {0}; - int i = 0; - - if( *str > '9' || *str < '0' ) - return false; - - do - { - while( *str <= '9' && *str >= '0' ) - { - ip[i] *=10; - ip[i] += *str - '0'; - str++; - } - mask[i] = 255; - i++; - if( *str != '.' ) break; - str++; - } while( i < 4 ); - - i = 0; - - if( !maskstr || *maskstr > '9' || *maskstr < '0' ) - goto end; - - do - { - byte mask1 = 0; - while( *maskstr <= '9' && *maskstr >= '0' ) - { - mask1 *=10; - mask1 += *maskstr - '0'; - maskstr++; - } - mask[i] &= mask1; - i++; - if( *maskstr != '.' ) break; - maskstr++; - } while( i < 4 ); - -end: - *outip = ip[0] << 24 | ip[1] << 16 | ip[2] << 8 | ip[3]; - if( outmask ) - *outmask = mask[0] << 24 | mask[1] << 16 | mask[2] << 8 | mask[3]; - - return true; -} - -#define IPARGS(ip) (ip >> 24) & 0xFF, (ip >> 16) & 0xFF, (ip >> 8) & 0xFF, ip & 0xFF -static void SV_AddIP_f( void ) -{ - float time = Q_atof( Cmd_Argv( 1 ) ); - const char *ipstr = Cmd_Argv( 2 ); - const char *maskstr = Cmd_Argv( 3 ); - uint ip, mask; - ipfilter_t *filter; - - if( time ) - time = host.realtime + time * 60.0f; - - if( !StringToIP( ipstr, maskstr, &ip, &mask ) ) - { - Con_Reportf( "Usage: addip [mask]\n0 minutes for permanent ban\n"); - return; - } - - SV_RemoveIP( ip, mask ); - - filter = Mem_Malloc( host.mempool, sizeof( ipfilter_t ) ); - filter->endTime = time; - filter->ip = ip; - filter->mask = mask; - filter->next = ipfilter; - - ipfilter = filter; -} - -static void SV_ListIP_f( void ) -{ - ipfilter_t *filter; - - Con_Reportf( "ip ban list\n" ); - Con_Reportf( "-----------\n" ); - - for( filter = ipfilter; filter; filter = filter->next ) - { - if( filter->endTime && host.realtime > filter->endTime ) - continue; // no negative time - - if( filter->endTime ) - Con_Reportf( "%d.%d.%d.%d %d.%d.%d.%d expries in %f minutes\n", IPARGS( filter->ip ), IPARGS( filter->mask ), ( filter->endTime - host.realtime ) / 60.0f ); - else - Con_Reportf( "%d.%d.%d.%d %d.%d.%d.%d permanent\n", IPARGS( filter->ip ), IPARGS( filter->mask ) ); - } -} - -static void SV_RemoveIP_f( void ) -{ - uint ip, mask; - - if( !StringToIP( Cmd_Argv(1), Cmd_Argv(2), &ip, &mask ) ) - { - Con_Reportf( "Usage: removeip [mask]\n" ); - return; - } - - SV_RemoveIP( ip, mask ); -} - -static void SV_WriteIP_f( void ) -{ - file_t *f = FS_Open( Cvar_VariableString( "listipcfgfile" ), "w", false ); - ipfilter_t *filter; - - if( !f ) - { - Con_DPrintf( S_ERROR "Could not write %s\n", Cvar_VariableString( "listipcfgfile" ) ); - 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 IP blacklist\n", Cvar_VariableString( "listipcfgfile" ) ); - FS_Printf( f, "//=======================================================================\n" ); - - for( filter = ipfilter; filter; filter = filter->next ) - if( !filter->endTime ) // only permanent - FS_Printf( f, "addip 0 %d.%d.%d.%d %d.%d.%d.%d\n", IPARGS(filter->ip), IPARGS(filter->mask) ); - - FS_Close( f ); -} - -void SV_InitFilter( void ) +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" ); - 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" ); } -void SV_ShutdownFilter( void ) +static void SV_ShutdownIDFilter( void ) { - ipfilter_t *ipList, *ipNext; cidfilter_t *cidList, *cidNext; // should be called manually because banned.cfg is not executed by engine - //SV_WriteIP_f(); //SV_WriteID_f(); - for( ipList = ipfilter; ipList; ipList = ipNext ) - { - ipNext = ipList->next; - Mem_Free( ipList ); - } + Cmd_RemoveCommand( "banid" ); + Cmd_RemoveCommand( "listid" ); + Cmd_RemoveCommand( "removeid" ); + Cmd_RemoveCommand( "writeid" ); for( cidList = cidfilter; cidList; cidList = cidNext ) { @@ -458,5 +254,463 @@ void SV_ShutdownFilter( void ) } 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