Files
Virtnet.bond/extensions/Others/AntiVPN/AntiVPN.php
2025-11-30 11:57:22 +01:00

1327 lines
55 KiB
PHP

<?php
namespace Paymenter\Extensions\Others\AntiVPN;
use App\Classes\Extension\Extension;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Request;
use Closure;
class AntiVPN extends Extension
{
public function getName(): string
{
return 'AntiVPN Protection';
}
public function getDescription(): string
{
return 'Blocks detected VPN/Proxy/Suspicious IPs using IPHunter.info, IPGeolocation.io, IsProxyIP.com, or GetIPIntel.net API with multi-provider support. Note: GetIPIntel requires a valid email address for contact information.';
}
public function getVersion(): string
{
return '2.1.0';
}
public function getAuthor(): string
{
return 'QKingsoftware';
}
public function getConfig($values = []): array
{
return [
[
'name' => 'operation_mode',
'type' => 'select',
'label' => 'Operation Mode',
'description' => 'Choose how the extension operates with multiple API providers.',
'required' => true,
'options' => [
'multi' => 'Multi-Mode (Use all available providers)',
'check' => 'Check Mode (IPv4→IPHunter, IPv6→Others)',
'single' => 'Single Provider Mode (Legacy)',
],
'value' => $values['operation_mode'] ?? 'multi',
],
[
'name' => 'api_provider',
'type' => 'select',
'label' => 'Primary API Provider (Single Mode)',
'description' => 'Choose your primary VPN detection API provider for single mode.',
'required' => false,
'options' => [
'iphunter' => 'IPHunter.info',
'ipgeolocation' => 'IPGeolocation.io (Security API - Paid)',
'isproxyip' => 'IsProxyIP.com (Free 1K/day, IPv4+IPv6)',
'getipintel' => 'GetIPIntel.net (Free 500/day, IPv4+IPv6)',
],
'value' => $values['api_provider'] ?? 'iphunter',
],
[
'name' => 'iphunter_api_key',
'type' => 'password',
'label' => 'IPHunter.info API Key',
'description' => 'Enter your IPHunter.info API key (IPv4 only).',
'required' => false,
'value' => $values['iphunter_api_key'] ?? '',
],
[
'name' => 'ipgeolocation_api_key',
'type' => 'password',
'label' => 'IPGeolocation.io API Key',
'description' => 'Enter your IPGeolocation.io Security API key (IPv4+IPv6).',
'required' => false,
'value' => $values['ipgeolocation_api_key'] ?? '',
],
[
'name' => 'isproxyip_api_key',
'type' => 'password',
'label' => 'IsProxyIP.com API Key',
'description' => 'Enter your IsProxyIP.com API key (IPv4+IPv6, Free 1K/day).',
'required' => false,
'value' => $values['isproxyip_api_key'] ?? '',
],
[
'name' => 'getipintel_api_key',
'type' => 'password',
'label' => 'GetIPIntel.net Contact Email',
'description' => 'Enter your valid email address for GetIPIntel.net (Free 500/day, IPv4+IPv6). Must be a real email that can receive messages.',
'required' => false,
'value' => $values['getipintel_api_key'] ?? '',
],
[
'name' => 'provider_api_key',
'type' => 'password',
'label' => 'Legacy API Key (Single Mode)',
'description' => 'Legacy field for single mode operation. Use individual API keys above for multi-mode.',
'required' => false,
'value' => $values['provider_api_key'] ?? '',
],
[
'name' => 'block_on_detect',
'type' => 'checkbox',
'label' => 'Block VPN/Proxy',
'description' => 'Block when VPN/Proxy is detected by any provider.',
'required' => false,
'value' => $values['block_on_detect'] ?? true,
],
[
'name' => 'block_on_suspicious',
'type' => 'checkbox',
'label' => 'Block Suspicious IPs',
'description' => 'Block when IP is flagged as suspicious by any provider.',
'required' => false,
'value' => $values['block_on_suspicious'] ?? true,
],
[
'name' => 'multi_mode_consensus',
'type' => 'select',
'label' => 'Multi-Mode Consensus',
'description' => 'How to handle results when multiple providers are used.',
'required' => false,
'options' => [
'any' => 'Block if ANY provider detects threat',
'majority' => 'Block if MAJORITY of providers detect threat',
'all' => 'Block only if ALL providers detect threat',
],
'value' => $values['multi_mode_consensus'] ?? 'any',
],
[
'name' => 'ipv6_handling',
'type' => 'select',
'label' => 'IPv6 Handling (Check Mode)',
'description' => 'How to handle IPv6 addresses in check mode. Note: IPHunter only supports IPv4.',
'required' => false,
'options' => [
'convert' => 'Try to convert to IPv4 first (recommended for IPHunter)',
'allow' => 'Allow all IPv6',
'block' => 'Block all IPv6',
'native' => 'Use native IPv6 support (IPGeolocation/IsProxyIP/GetIPIntel only)',
],
'value' => $values['ipv6_handling'] ?? 'convert',
],
[
'name' => 'whitelist',
'type' => 'textarea',
'label' => 'Whitelisted IPs',
'description' => 'IPs to never block (one per line). Supports both IPv4 and IPv6.',
'required' => false,
'value' => $values['whitelist'] ?? '',
],
[
'name' => 'test_mode',
'type' => 'checkbox',
'label' => 'Test Mode (Force Block)',
'description' => 'Block ALL IPs for testing (DANGER - can lock you out).',
'required' => false,
'value' => $values['test_mode'] ?? false,
],
[
'name' => 'enable_caching',
'type' => 'checkbox',
'label' => 'Enable IP Caching',
'description' => 'Cache API results to reduce API calls and improve performance.',
'required' => false,
'value' => $values['enable_caching'] ?? true,
],
[
'name' => 'cache_ttl',
'type' => 'number',
'label' => 'Cache TTL (minutes)',
'description' => 'How long to cache IP results in minutes (1-1440).',
'required' => false,
'value' => $values['cache_ttl'] ?? 60,
],
[
'name' => 'graceful_fallback',
'type' => 'checkbox',
'label' => 'Graceful Fallback',
'description' => 'Allow requests through when API returns unexpected responses.',
'required' => false,
'value' => $values['graceful_fallback'] ?? true,
],
[
'name' => 'debug_mode',
'type' => 'checkbox',
'label' => 'Debug Mode',
'description' => 'Enable detailed logging for troubleshooting. Disable in production for better performance.',
'required' => false,
'value' => $values['debug_mode'] ?? false,
],
];
}
public function boot(): void
{
if (app()->runningInConsole()) {
return;
}
// Only log boot once to avoid spam
static $booted = false;
if (!$booted) {
Log::info('AntiVPN Extension: Starting boot');
$booted = true;
}
$router = app('router');
$router->pushMiddlewareToGroup('web', \Paymenter\Extensions\Others\AntiVPN\AntiVPNMiddleware::class);
// Only log registration once to avoid spam
static $registered = false;
if (!$registered) {
Log::info('AntiVPN Extension: Middleware registered');
$registered = true;
}
}
public static function getInstance(): self
{
return app(self::class);
}
}
class AntiVPNMiddleware
{
private $debugMode = false;
public function handle(Request $request, Closure $next)
{
$this->debugLog('=== AntiVPN Middleware START ===');
$this->debugLog('Request URL: ' . $request->fullUrl());
$this->debugLog('Request Method: ' . $request->method());
$this->debugLog('Request Path: ' . $request->path());
// Skip admin routes
if (str_starts_with($request->path(), 'admin')) {
$this->debugLog('Skipping admin route');
return $next($request);
}
// Only check GET requests
if (!$request->isMethod('GET')) {
$this->debugLog('Skipping non-GET request');
return $next($request);
}
try {
$extension = AntiVPN::getInstance();
$this->debugLog('Extension instance loaded');
// Get config
$operationMode = $extension->config('operation_mode', 'multi') ?? 'multi';
$apiProvider = $extension->config('api_provider', 'iphunter') ?? 'iphunter';
$iphunterKey = $extension->config('iphunter_api_key', '') ?? '';
$ipgeolocationKey = $extension->config('ipgeolocation_api_key', '') ?? '';
$isproxyipKey = $extension->config('isproxyip_api_key', '') ?? '';
$getipintelKey = $extension->config('getipintel_api_key', '') ?? '';
$legacyKey = $extension->config('provider_api_key', '') ?? '';
$blockOnDetect = $extension->config('block_on_detect', true) ?? true;
$blockOnSuspicious = $extension->config('block_on_suspicious', true) ?? true;
$multiModeConsensus = $extension->config('multi_mode_consensus', 'any') ?? 'any';
$ipv6Handling = $extension->config('ipv6_handling', 'allow') ?? 'allow';
$testMode = $extension->config('test_mode', false) ?? false;
$enableCaching = $extension->config('enable_caching', true) ?? true;
$cacheTtl = max(1, min(1440, $extension->config('cache_ttl', 60) ?? 60)); // Clamp between 1-1440
$gracefulFallback = $extension->config('graceful_fallback', true) ?? true;
$debugMode = $extension->config('debug_mode', false) ?? false;
$this->debugMode = $debugMode;
$this->debugLog('Config loaded - Operation Mode: ' . $operationMode);
$this->debugLog('Config loaded - IPHunter Key: ' . (!empty($iphunterKey) ? 'SET' : 'EMPTY'));
$this->debugLog('Config loaded - IPGeolocation Key: ' . (!empty($ipgeolocationKey) ? 'SET' : 'EMPTY'));
$this->debugLog('Config loaded - IsProxyIP Key: ' . (!empty($isproxyipKey) ? 'SET' : 'EMPTY'));
$this->debugLog('Config loaded - GetIPIntel Key: ' . (!empty($getipintelKey) ? 'SET' : 'EMPTY'));
$this->debugLog('Config loaded - Legacy Key: ' . (!empty($legacyKey) ? 'SET' : 'EMPTY'));
// Debug: Show actual values (truncated for security)
$this->debugLog('IsProxyIP Key (first 10 chars): ' . substr($isproxyipKey, 0, 10));
$this->debugLog('GetIPIntel Key (first 10 chars): ' . substr($getipintelKey, 0, 10));
// Check if we have any valid API keys
$hasValidKeys = !empty($iphunterKey) || !empty($ipgeolocationKey) || !empty($isproxyipKey) || !empty($getipintelKey) || !empty($legacyKey);
if (!$hasValidKeys && !$testMode) {
$this->debugLog('No API keys and not test mode - allowing');
return $next($request);
}
$ip = $this->getRealIp($request);
$this->debugLog('Real IP detected: ' . $ip);
// Test mode - block everyone (except admin)
if ($testMode) {
Log::warning('AntiVPN: TEST MODE - BLOCKING IP: ' . $ip);
return $this->getBlockResponse($request, $ip, 'TEST');
}
// Check whitelist
if ($this->isWhitelisted($ip, $extension)) {
$this->debugLog('IP is whitelisted - allowing');
return $next($request);
}
// Store original IP for logging
$originalIp = $ip;
// Determine which providers to use based on operation mode
$providers = [];
if ($operationMode === 'multi') {
// Multi-mode: Use all available providers
if (!empty($iphunterKey)) {
$providers['iphunter'] = $iphunterKey;
}
if (!empty($ipgeolocationKey)) {
$providers['ipgeolocation'] = $ipgeolocationKey;
}
if (!empty($isproxyipKey)) {
$providers['isproxyip'] = $isproxyipKey;
}
if (!empty($getipintelKey) && filter_var($getipintelKey, FILTER_VALIDATE_EMAIL)) {
$this->debugLog('Multi mode - Using GetIPIntel key (valid email)');
$providers['getipintel'] = $getipintelKey;
} elseif (!empty($getipintelKey)) {
Log::warning('AntiVPN: GetIPIntel key appears to be encrypted/invalid, skipping GetIPIntel provider');
$this->debugLog('Multi mode - GetIPIntel key value: ' . substr($getipintelKey, 0, 20) . '...');
}
} elseif ($operationMode === 'check') {
// Check mode: IPv4 → IPHunter, IPv6 → Others
if ($this->isIPv6($ip)) {
$this->debugLog('Check mode - IPv6 detected, using IPv6-capable providers');
if (!empty($ipgeolocationKey)) {
$providers['ipgeolocation'] = $ipgeolocationKey;
}
if (!empty($isproxyipKey)) {
$providers['isproxyip'] = $isproxyipKey;
}
if (!empty($getipintelKey) && filter_var($getipintelKey, FILTER_VALIDATE_EMAIL)) {
$this->debugLog('Check mode - Using GetIPIntel key (valid email)');
$providers['getipintel'] = $getipintelKey;
} elseif (!empty($getipintelKey)) {
Log::warning('AntiVPN: GetIPIntel key appears to be encrypted/invalid, skipping GetIPIntel provider');
$this->debugLog('Check mode - GetIPIntel key value: ' . substr($getipintelKey, 0, 20) . '...');
}
// Handle IPv6 conversion if needed
if (empty($providers) && $ipv6Handling === 'convert') {
$convertedIp = $this->convertIPv6ToIPv4($ip);
if ($convertedIp !== null && !empty($iphunterKey)) {
$this->debugLog('Check mode - IPv6 converted to IPv4, using IPHunter');
$providers['iphunter'] = $iphunterKey;
// Update IP for API call
$ip = $convertedIp;
}
}
} else {
$this->debugLog('Check mode - IPv4 detected, using IPHunter');
if (!empty($iphunterKey)) {
$providers['iphunter'] = $iphunterKey;
} elseif (!empty($ipgeolocationKey)) {
$providers['ipgeolocation'] = $ipgeolocationKey;
} elseif (!empty($isproxyipKey)) {
$providers['isproxyip'] = $isproxyipKey;
} elseif (!empty($getipintelKey) && filter_var($getipintelKey, FILTER_VALIDATE_EMAIL)) {
$providers['getipintel'] = $getipintelKey;
} elseif (!empty($getipintelKey)) {
Log::warning('AntiVPN: GetIPIntel key appears to be encrypted/invalid, skipping GetIPIntel provider');
}
}
} else {
// Single mode: Prioritize individual keys over legacy key
$this->debugLog('Single mode - Selected provider: ' . $apiProvider);
// Check if we have individual keys for the selected provider first
$useIndividualKey = false;
if ($apiProvider === 'iphunter' && !empty($iphunterKey)) {
$this->debugLog('Single mode - Using IPHunter individual key');
$providers['iphunter'] = $iphunterKey;
$useIndividualKey = true;
} elseif ($apiProvider === 'ipgeolocation' && !empty($ipgeolocationKey)) {
$this->debugLog('Single mode - Using IPGeolocation individual key');
$providers['ipgeolocation'] = $ipgeolocationKey;
$useIndividualKey = true;
} elseif ($apiProvider === 'isproxyip' && !empty($isproxyipKey)) {
$this->debugLog('Single mode - Using IsProxyIP individual key');
$providers['isproxyip'] = $isproxyipKey;
$useIndividualKey = true;
} elseif ($apiProvider === 'getipintel' && !empty($getipintelKey) && filter_var($getipintelKey, FILTER_VALIDATE_EMAIL)) {
$this->debugLog('Single mode - Using GetIPIntel individual key (valid email)');
$this->debugLog('Single mode - GetIPIntel key value: ' . substr($getipintelKey, 0, 20) . '...');
$providers['getipintel'] = $getipintelKey;
$useIndividualKey = true;
} elseif ($apiProvider === 'getipintel' && !empty($getipintelKey)) {
Log::warning('AntiVPN: GetIPIntel selected but email is invalid, falling back to other providers');
// Try other providers as fallback
if (!empty($iphunterKey)) {
$this->debugLog('Single mode - Fallback to IPHunter');
$providers['iphunter'] = $iphunterKey;
$useIndividualKey = true;
} elseif (!empty($isproxyipKey)) {
$this->debugLog('Single mode - Fallback to IsProxyIP');
$providers['isproxyip'] = $isproxyipKey;
$useIndividualKey = true;
} elseif (!empty($ipgeolocationKey)) {
$this->debugLog('Single mode - Fallback to IPGeolocation');
$providers['ipgeolocation'] = $ipgeolocationKey;
$useIndividualKey = true;
}
}
// Only use legacy key if no individual key was found
if (!$useIndividualKey && !empty($legacyKey)) {
$this->debugLog('Single mode - No individual key found, using legacy key for provider: ' . $apiProvider);
$this->debugLog('Single mode - Legacy key value: ' . substr($legacyKey, 0, 20) . '...');
$providers[$apiProvider] = $legacyKey;
} elseif (!$useIndividualKey) {
Log::warning('AntiVPN: Single mode - No valid key found for selected provider: ' . $apiProvider);
}
}
if (empty($providers)) {
$this->debugLog('No valid providers available - allowing');
return $next($request);
}
$this->debugLog('Using providers: ' . implode(', ', array_keys($providers)));
$this->debugLog('Selected provider in single mode: ' . $apiProvider);
// Skip private IPs (IPv4 only, or if we converted from IPv6)
if (!$this->isIPv6($ip) && !filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
$this->debugLog('Private IPv4 - allowing');
return $next($request);
}
// Check cache first
$cacheKey = 'antivpn_' . md5($ip . '_' . implode('_', array_keys($providers)));
$results = null;
if ($enableCaching) {
$cachedResult = Cache::get($cacheKey);
if ($cachedResult !== null) {
$this->debugLog('Using cached result for IP: ' . $ip);
$results = $cachedResult;
}
}
// Check APIs if not cached
if ($results === null) {
$results = $this->checkMultipleApis($ip, $providers, $gracefulFallback);
$this->debugLog('API results: ' . json_encode($results));
// Cache the results
if ($enableCaching && !empty($results)) {
$cacheMinutes = max(1, min(1440, $cacheTtl)); // Clamp between 1-1440 minutes
Cache::put($cacheKey, $results, now()->addMinutes($cacheMinutes));
$this->debugLog('Cached result for IP: ' . $ip . ' (TTL: ' . $cacheMinutes . ' minutes)');
}
}
// Decision logic based on operation mode
$shouldBlock = false;
$blockReason = null;
$blockProvider = null;
if ($operationMode === 'multi') {
$decision = $this->evaluateMultiModeResults($results, $blockOnDetect, $blockOnSuspicious, $multiModeConsensus);
$shouldBlock = $decision['should_block'];
$blockReason = $decision['reason'];
$blockProvider = $decision['provider'] ?? null;
} else {
// Single mode or check mode - use first available result
$result = !empty($results) ? reset($results) : null;
if ($result !== null) {
if ($result['is_vpn'] && $blockOnDetect) {
Log::warning('AntiVPN: BLOCKING - VPN detected and block_on_detect enabled');
$shouldBlock = true;
$blockReason = 'VPN';
$blockProvider = $result['provider'] ?? null;
} elseif ($result['is_suspicious'] && $blockOnSuspicious) {
Log::warning('AntiVPN: BLOCKING - Suspicious IP detected and block_on_suspicious enabled');
$shouldBlock = true;
$blockReason = 'Suspicious';
$blockProvider = $result['provider'] ?? null;
} else {
$this->debugLog('NOT BLOCKING - VPN: ' . ($result['is_vpn']?'Y':'N') . ', Suspicious: ' . ($result['is_suspicious']?'Y':'N') . ', Block VPN: ' . ($blockOnDetect?'Y':'N') . ', Block Suspicious: ' . ($blockOnSuspicious?'Y':'N'));
}
}
}
if ($shouldBlock) {
return $this->getBlockResponse($request, $originalIp, $blockReason, $blockProvider);
}
} catch (\Exception $e) {
Log::error('AntiVPN: Exception - ' . $e->getMessage());
Log::error('AntiVPN: Stack trace - ' . $e->getTraceAsString());
}
$this->debugLog('Allowing request');
$this->debugLog('=== AntiVPN Middleware END ===');
return $next($request);
}
private function checkMultipleApis(string $ip, array $providers, bool $gracefulFallback = true): array
{
$results = [];
foreach ($providers as $provider => $apiKey) {
try {
$this->debugLog("Checking {$provider} for IP: {$ip}");
$result = $this->checkApi($ip, $apiKey, $provider, $gracefulFallback);
if ($result !== null) {
$results[$provider] = $result;
$this->debugLog("{$provider} result - VPN: " . ($result['is_vpn'] ? 'Y' : 'N') . ", Suspicious: " . ($result['is_suspicious'] ? 'Y' : 'N'));
} else {
Log::warning("AntiVPN: {$provider} returned null result");
if ($gracefulFallback) {
Log::info("AntiVPN: Graceful fallback enabled - allowing request due to {$provider} failure");
return []; // Return empty results to allow request
}
}
} catch (\Exception $e) {
Log::error("AntiVPN: {$provider} API exception - " . $e->getMessage());
if ($gracefulFallback) {
Log::info("AntiVPN: Graceful fallback enabled - allowing request due to {$provider} exception");
return []; // Return empty results to allow request
}
}
}
return $results;
}
private function evaluateMultiModeResults(array $results, bool $blockOnDetect, bool $blockOnSuspicious, string $consensus): array
{
if (empty($results)) {
return ['should_block' => false, 'reason' => null, 'provider' => null];
}
// Filter out empty/null results and fallback results
$validResults = array_filter($results, function($result) {
return $result !== null && !isset($result['fallback']);
});
if (empty($validResults)) {
Log::info('AntiVPN: Multi-mode - No valid results available, allowing request');
return ['should_block' => false, 'reason' => 'no_valid_results', 'provider' => null];
}
$vpnDetections = 0;
$suspiciousDetections = 0;
$totalProviders = count($validResults);
$detectingProviders = [];
foreach ($validResults as $provider => $result) {
if ($result['is_vpn']) {
$vpnDetections++;
$detectingProviders[] = $provider;
}
if ($result['is_suspicious']) {
$suspiciousDetections++;
$detectingProviders[] = $provider;
}
}
$this->debugLog("Multi-mode evaluation - Valid providers: {$totalProviders}, VPN detections: {$vpnDetections}, Suspicious detections: {$suspiciousDetections}, Consensus: {$consensus}");
// If we have fewer valid results than expected, log it
$originalCount = count($results);
if ($totalProviders < $originalCount) {
$skippedCount = $originalCount - $totalProviders;
$this->debugLog("Multi-mode - Skipped {$skippedCount} invalid/fallback results");
}
$shouldBlock = false;
$reason = null;
$provider = null;
// Ensure consensus is valid, default to 'any' if not
if (!in_array($consensus, ['any', 'majority', 'all'])) {
Log::warning("AntiVPN: Invalid consensus value '{$consensus}', defaulting to 'any'");
$consensus = 'any';
}
if ($consensus === 'any') {
if ($vpnDetections > 0 && $blockOnDetect) {
$shouldBlock = true;
$reason = 'VPN (any)';
$provider = implode(', ', array_unique($detectingProviders));
} elseif ($suspiciousDetections > 0 && $blockOnSuspicious) {
$shouldBlock = true;
$reason = 'Suspicious (any)';
$provider = implode(', ', array_unique($detectingProviders));
}
} elseif ($consensus === 'majority') {
$majorityThreshold = ceil($totalProviders / 2);
if ($vpnDetections >= $majorityThreshold && $blockOnDetect) {
$shouldBlock = true;
$reason = 'VPN (majority)';
$provider = implode(', ', array_unique($detectingProviders));
} elseif ($suspiciousDetections >= $majorityThreshold && $blockOnSuspicious) {
$shouldBlock = true;
$reason = 'Suspicious (majority)';
$provider = implode(', ', array_unique($detectingProviders));
}
} elseif ($consensus === 'all') {
if ($vpnDetections === $totalProviders && $blockOnDetect) {
$shouldBlock = true;
$reason = 'VPN (all)';
$provider = implode(', ', array_unique($detectingProviders));
} elseif ($suspiciousDetections === $totalProviders && $blockOnSuspicious) {
$shouldBlock = true;
$reason = 'Suspicious (all)';
$provider = implode(', ', array_unique($detectingProviders));
}
}
if (!$shouldBlock) {
$this->debugLog("Multi-mode - No threats detected by any provider");
}
return ['should_block' => $shouldBlock, 'reason' => $reason, 'provider' => $provider];
}
private function getRealIp(Request $request): string
{
// Try Cloudflare first
if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) {
return trim($_SERVER['HTTP_CF_CONNECTING_IP']);
}
// Try X-Forwarded-For
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
return trim($ips[0]);
}
return $request->ip();
}
private function isIPv6(string $ip): bool
{
return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
}
private function convertIPv6ToIPv4(string $ipv6): ?string
{
try {
// Remove brackets if present
$ipv6 = trim($ipv6, '[]');
// 1. Check for IPv4-mapped IPv6 (::ffff:192.0.2.1)
if (preg_match('/^::ffff:([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})$/i', $ipv6, $matches)) {
$this->debugLog('Found IPv4-mapped IPv6');
return $matches[1];
}
// 2. Check for IPv4-compatible IPv6 (::192.0.2.1)
if (preg_match('/^::([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})$/', $ipv6, $matches)) {
$this->debugLog('Found IPv4-compatible IPv6');
return $matches[1];
}
// 3. Check for embedded IPv4 in last 32 bits (2001:db8::192.0.2.1)
if (preg_match('/^([0-9a-f:]+)::([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})$/i', $ipv6, $matches)) {
$this->debugLog('Found embedded IPv4 in IPv6');
return $matches[2];
}
// 4. Try to extract IPv4 from Teredo tunneling (2001:0000::/32)
if (preg_match('/^2001:0000:([0-9a-f]{4}):([0-9a-f]{4}):([0-9a-f]{4}):([0-9a-f]{4}):([0-9a-f]{4}):([0-9a-f]{4})$/i', $ipv6, $matches)) {
$this->debugLog('Attempting Teredo IPv4 extraction');
// In Teredo, the client IPv4 is in the last 32 bits (XORed with 0xFFFFFFFF)
$clientIPv4Hex = $matches[5] . $matches[6];
$clientIPv4Int = hexdec($clientIPv4Hex) ^ 0xFFFFFFFF;
$ipv4 = long2ip($clientIPv4Int);
if (filter_var($ipv4, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return $ipv4;
}
}
// 5. Check for 6to4 tunneling (2002::/16)
if (preg_match('/^2002:([0-9a-f]{4}):([0-9a-f]{4}):/i', $ipv6, $matches)) {
$this->debugLog('Attempting 6to4 IPv4 extraction');
// In 6to4, the IPv4 address is encoded in the first 32 bits after 2002:
$ipv4Hex = $matches[1] . $matches[2];
$ipv4Int = hexdec($ipv4Hex);
$ipv4 = long2ip($ipv4Int);
if (filter_var($ipv4, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return $ipv4;
}
}
$this->debugLog('No IPv4 conversion method applicable for: ' . $ipv6);
return null;
} catch (\Exception $e) {
Log::error('AntiVPN: IPv6 to IPv4 conversion failed: ' . $e->getMessage());
return null;
}
}
private function isWhitelisted(string $ip, $extension): bool
{
$whitelist = trim($extension->config('whitelist', ''));
if (empty($whitelist)) {
return false;
}
$whitelistIps = array_filter(array_map('trim', explode("\n", $whitelist)));
return in_array($ip, $whitelistIps, true);
}
private function checkApi(string $ip, string $apiKey, string $provider, bool $gracefulFallback = true): ?array
{
try {
$this->debugLog("Making API call to {$provider} for IP: {$ip}");
if ($provider === 'iphunter') {
return $this->checkIPHunterApi($ip, $apiKey, $gracefulFallback);
} elseif ($provider === 'ipgeolocation') {
return $this->checkIPGeolocationApi($ip, $apiKey, $gracefulFallback);
} elseif ($provider === 'isproxyip') {
return $this->checkIsProxyIPApi($ip, $apiKey, $gracefulFallback);
} elseif ($provider === 'getipintel') {
return $this->checkGetIPIntelApi($ip, $apiKey, $gracefulFallback);
}
Log::error('AntiVPN: Unknown API provider: ' . $provider);
return null;
} catch (\Exception $e) {
Log::error('AntiVPN: API exception - ' . $e->getMessage());
return null;
}
}
private function checkIPHunterApi(string $ip, string $apiKey, bool $gracefulFallback = true): ?array
{
try {
$response = Http::timeout(10)
->withHeaders([
'X-Key' => $apiKey,
'Accept' => 'application/json',
'User-Agent' => 'AntiVPN-Extension/2.1.0',
])
->get("https://www.iphunter.info:8082/v1/ip/{$ip}");
$this->debugLog('IPHunter response status: ' . $response->status());
$this->debugLog('IPHunter response body: ' . $response->body());
if (!$response->successful()) {
Log::error('AntiVPN: IPHunter API request failed with status: ' . $response->status());
if ($gracefulFallback) {
Log::info('AntiVPN: Graceful fallback - allowing request due to IPHunter API failure');
return ['is_vpn' => false, 'is_suspicious' => false, 'provider' => 'iphunter', 'fallback' => true];
}
return null;
}
$data = $response->json();
$this->debugLog('IPHunter parsed data: ' . json_encode($data));
if (isset($data['status']) && $data['status'] === 'success' && isset($data['data']['block'])) {
$blockLevel = (int) $data['data']['block'];
return [
'is_vpn' => $blockLevel === 1,
'is_suspicious' => $blockLevel === 2,
'provider' => 'iphunter',
'raw_response' => $data
];
}
Log::warning('AntiVPN: Unexpected IPHunter API response structure');
if ($gracefulFallback) {
Log::info('AntiVPN: Graceful fallback - allowing request due to unexpected IPHunter response');
return ['is_vpn' => false, 'is_suspicious' => false, 'provider' => 'iphunter', 'fallback' => true];
}
return null;
} catch (\Exception $e) {
Log::error('AntiVPN: IPHunter API exception - ' . $e->getMessage());
return null;
}
}
private function checkIPGeolocationApi(string $ip, string $apiKey, bool $gracefulFallback = true): ?array
{
try {
$response = Http::timeout(10)
->withHeaders([
'Accept' => 'application/json',
'User-Agent' => 'AntiVPN-Extension/2.1.0',
])
->get("https://api.ipgeolocation.io/v2/security", [
'apiKey' => $apiKey,
'ip' => $ip
]);
$this->debugLog('IPGeolocation response status: ' . $response->status());
$this->debugLog('IPGeolocation response body: ' . $response->body());
if (!$response->successful()) {
Log::error('AntiVPN: IPGeolocation API request failed with status: ' . $response->status());
if ($gracefulFallback) {
Log::info('AntiVPN: Graceful fallback - allowing request due to IPGeolocation API failure');
return ['is_vpn' => false, 'is_suspicious' => false, 'provider' => 'ipgeolocation', 'fallback' => true];
}
return null;
}
$data = $response->json();
$this->debugLog('IPGeolocation parsed data: ' . json_encode($data));
// Check for error in response
if (isset($data['message'])) {
Log::error('AntiVPN: IPGeolocation API error: ' . $data['message']);
if ($gracefulFallback) {
Log::info('AntiVPN: Graceful fallback - allowing request due to IPGeolocation API error');
return ['is_vpn' => false, 'is_suspicious' => false, 'provider' => 'ipgeolocation', 'fallback' => true];
}
return null;
}
// IPGeolocation.io Security API structure
$isProxy = false;
$isThreat = false;
if (isset($data['security'])) {
$security = $data['security'];
// Check for proxy/VPN
if (isset($security['is_proxy']) && $security['is_proxy']) {
$isProxy = true;
}
// Check for Tor
if (isset($security['is_tor']) && $security['is_tor']) {
$isProxy = true; // Treat Tor as proxy/VPN
}
// Check for various threat indicators
if (isset($security['is_anonymous']) && $security['is_anonymous']) {
$isThreat = true;
}
if (isset($security['is_known_attacker']) && $security['is_known_attacker']) {
$isThreat = true;
}
if (isset($security['is_spam']) && $security['is_spam']) {
$isThreat = true;
}
if (isset($security['is_bot']) && $security['is_bot']) {
$isThreat = true;
}
// High threat score
if (isset($security['threat_score']) && $security['threat_score'] > 50) {
$isThreat = true;
}
$this->debugLog('IPGeolocation security analysis - Proxy: ' . ($isProxy ? 'Y' : 'N') . ', Threat: ' . ($isThreat ? 'Y' : 'N'));
$this->debugLog('IPGeolocation threat details - Threat Score: ' . ($security['threat_score'] ?? 'N/A') . ', Proxy Type: ' . ($security['proxy_type'] ?? 'N/A') . ', Provider: ' . ($security['proxy_provider'] ?? 'N/A'));
}
return [
'is_vpn' => $isProxy,
'is_suspicious' => $isThreat,
'provider' => 'ipgeolocation',
'raw_response' => $data
];
} catch (\Exception $e) {
Log::error('AntiVPN: IPGeolocation API exception - ' . $e->getMessage());
return null;
}
}
private function checkIsProxyIPApi(string $ip, string $apiKey, bool $gracefulFallback = true): ?array
{
try {
$response = Http::timeout(10)
->withHeaders([
'Accept' => 'application/json',
'User-Agent' => 'AntiVPN-Extension/2.1.0',
])
->get("https://api.isproxyip.com/v1/check.php", [
'key' => $apiKey,
'ip' => $ip,
'format' => 'json'
]);
$this->debugLog('IsProxyIP response status: ' . $response->status());
$this->debugLog('IsProxyIP response body: ' . $response->body());
if (!$response->successful()) {
Log::error('AntiVPN: IsProxyIP API request failed with status: ' . $response->status());
if ($gracefulFallback) {
Log::info('AntiVPN: Graceful fallback - allowing request due to IsProxyIP API failure');
return ['is_vpn' => false, 'is_suspicious' => false, 'provider' => 'isproxyip', 'fallback' => true];
}
return null;
}
$data = $response->json();
$this->debugLog('IsProxyIP parsed data: ' . json_encode($data));
// Check for error in response
if (isset($data['status']) && $data['status'] === 'error') {
Log::error('AntiVPN: IsProxyIP API error: ' . ($data['message'] ?? 'Unknown error'));
if ($gracefulFallback) {
Log::info('AntiVPN: Graceful fallback - allowing request due to IsProxyIP API error');
return ['is_vpn' => false, 'is_suspicious' => false, 'provider' => 'isproxyip', 'fallback' => true];
}
return null;
}
// IsProxyIP API structure - simple proxy detection
$isProxy = false;
if (isset($data['status']) && $data['status'] === 'success' && isset($data['proxy'])) {
$isProxy = (bool) $data['proxy']; // 1 = proxy, 0 = not proxy
$this->debugLog('IsProxyIP proxy detection: ' . ($isProxy ? 'PROXY' : 'CLEAN'));
}
return [
'is_vpn' => $isProxy,
'is_suspicious' => false, // IsProxyIP only detects proxy/VPN, not suspicious behavior
'provider' => 'isproxyip',
'raw_response' => $data
];
} catch (\Exception $e) {
Log::error('AntiVPN: IsProxyIP API exception - ' . $e->getMessage());
return null;
}
}
private function getBlockResponse(Request $request, string $ip, $blockLevel, $provider = null)
{
if ($request->expectsJson()) {
return response()->json([
'error' => 'Access blocked by AntiVPN',
'ip' => $ip,
'level' => $blockLevel,
'provider' => $provider,
], 403);
}
$dateandtime = date('Y-m-d H:i:s');
$providerText = $provider ? "Detected by: <strong>{$provider}{$ip}</strong>" : '';
$html = "<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<meta name='description' content='Access via VPN or proxy networks has been restricted on Virtnet.bond due to security policies.' />
<title>Access Restricted | Virtnet.bond</title>
<link rel='preconnect' href='https://fonts.googleapis.com' />
<link rel='preconnect' href='https://fonts.gstatic.com' crossorigin />
<link href='https://fonts.googleapis.com/css?family=Montserrat:400,500,700&display=swap' rel='stylesheet' />
<style>
/* ------------------------------
Global Styles
------------------------------ */
*, *::before, *::after {
box-sizing: border-box;
}
body {
margin: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #eceff1;
color: #263238;
}
main.container {
background: #fff;
width: 90%;
max-width: 520px;
padding: 2rem 1.5rem;
border-radius: 0.75rem;
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.15);
text-align: center;
animation: fadeIn 0.8s ease-out forwards;
}
h1 {
font-size: 1.5rem;
font-weight: 700;
margin: 1rem 0 0.5rem 0;
}
h2 {
font-size: 1.125rem;
font-weight: 500;
color: #455a64;
margin: 0.25rem 0 1rem;
}
p {
color: #37474f;
line-height: 1.6;
margin: 0.5rem 0;
}
a {
color: #1976d2;
text-decoration: none;
font-weight: 500;
}
a:hover {
text-decoration: underline;
}
code {
background: #f5f5f5;
padding: 0.1rem 0.3rem;
border-radius: 0.2rem;
font-size: 0.95em;
}
small {
display: block;
margin-top: 1.5rem;
color: #607d8b;
font-size: 0.85rem;
}
/* ORIGINAL forbidden-sign block (unchanged) */
.forbidden-sign {
margin: auto;
width: 4.6666666667rem;
height: 4.6666666667rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background-color: #ef5350;
-webkit-animation: grow 1s forwards;
animation: grow 1s forwards;
}
.forbidden-sign::before {
position: absolute;
background-color: white;
border-radius: 50%;
content: '';
width: 4rem;
height: 4rem;
transform: scale(0);
animation: grow2 0.5s forwards 0.5s;
}
.forbidden-sign::after {
content: '';
z-index: 2;
position: absolute;
width: 4rem;
height: 0.3333333333rem;
transform: scaley(0) rotateZ(0deg);
background: #ef5350;
animation: grow3 0.5s forwards 1s;
}
@keyframes grow {
from { transform: scale(0); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
@keyframes grow2 {
from { transform: scale(0); }
to { transform: scale(1); }
}
@keyframes grow3 {
from { transform: scaley(0) rotateZ(0deg); }
to { transform: scaley(1) rotateZ(-45deg); }
}
.actions {
margin-top: 1.5rem;
}
.btn {
display: inline-block;
background: #1976d2;
color: white;
padding: 0.6rem 1.2rem;
border-radius: 0.4rem;
font-weight: 600;
text-decoration: none;
transition: background 0.2s ease;
margin: 0.3rem;
}
.btn:hover {
background: #1565c0;
}
.btn-secondary {
background: #90a4ae;
}
.btn-secondary:hover {
background: #78909c;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(1rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 480px) {
main.container {
padding: 1.5rem;
}
h1 {
font-size: 1.25rem;
}
}
/* ------------------------------
Dark Mode
------------------------------ */
@media (prefers-color-scheme: dark) {
body {
background-color: #121212;
color: #e0e0e0;
}
main.container {
background-color: #1e1e1e;
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.6);
}
h1, h2, p, small {
color: #e0e0e0;
}
a {
color: #64b5f6;
}
code {
background: #2c2c2c;
color: #fafafa;
}
.btn {
background: #2196f3;
}
.btn:hover {
background: #1976d2;
}
.btn-secondary {
background: #607d8b;
}
.btn-secondary:hover {
background: #546e7a;
}
.forbidden-sign {
background-color: #ef5350;
}
.forbidden-sign::before {
background-color: #1e1e1e;
}
.forbidden-sign::after {
background: #ef5350;
}
}
</style>
</head>
<body>
<main class='container' role='alert'>
<div class='forbidden-sign' aria-label='Access Restricted'></div>
<h1>Access Restricted</h1>
<h2>VPN or Proxy Connection Detected</h2>
<p>Access from virtual private networks (VPNs) or proxies is strictly prohibited. You must access this page using a standard, non-VPN/proxy internet connection. To maintain network integrity and prevent abuse, Virtnet.bond restricts access from these services.</p>
<p>If you believe this is an error, you may request a review by contacting <a href='mailto:abuse@virtnet.bond'>abuse@virtnet.bond</a> or submitting a support ticket.</p>
<p><strong>Logged IP:</strong> {$ip}</p>
<p><strong>Date/Time:</strong> <time datetime='{$dateandtime}'>{$dateandtime}</time></p>
<p>Your connection details are logged in accordance with our security and data protection policies. For more information, please review our <a href='https://virtnet.bond/tos' target='_blank'>Privacy Policy</a>.</p>
<div class='actions'>
<a href='mailto:abuse@virtnet.bond' class='btn'>Contact Support</a>
<a href='javascript:history.back()' class='btn btn-secondary'>Previous Page</a>
</div>
<small>© 2025 Virtnet.bond. All rights reserved.</small>
</main>
</body>
</html>
";
return response($html, 403)->header('Content-Type', 'text/html');
}
private function checkGetIPIntelApi(string $ip, string $contactEmail, bool $gracefulFallback = true): ?array
{
try {
$this->debugLog('GetIPIntel API called with contact email: ' . substr($contactEmail, 0, 20) . '...');
// Validate email format
if (!filter_var($contactEmail, FILTER_VALIDATE_EMAIL)) {
Log::error('AntiVPN: GetIPIntel contact email is not valid: ' . $contactEmail);
Log::error('AntiVPN: GetIPIntel contact email appears to be encrypted. Please check your configuration.');
return null;
}
$response = Http::timeout(10)
->withHeaders([
'Accept' => 'application/json',
'User-Agent' => 'AntiVPN-Extension/2.1.0',
])
->get("http://check.getipintel.net/check.php", [
'ip' => $ip,
'contact' => $contactEmail,
'format' => 'json',
'flags' => 'm' // Use dynamic ban list only for fastest response
]);
$this->debugLog('GetIPIntel response status: ' . $response->status());
$this->debugLog('GetIPIntel response body: ' . $response->body());
if (!$response->successful()) {
Log::error('AntiVPN: GetIPIntel API request failed with status: ' . $response->status());
if ($gracefulFallback) {
Log::info('AntiVPN: Graceful fallback - allowing request due to GetIPIntel API failure');
return ['is_vpn' => false, 'is_suspicious' => false, 'provider' => 'getipintel', 'fallback' => true];
}
return null;
}
$data = $response->json();
$this->debugLog('GetIPIntel parsed data: ' . json_encode($data));
// Check for error in response
if (isset($data['status']) && $data['status'] === 'error') {
$errorCode = $data['result'] ?? 'unknown';
$errorMessage = $data['message'] ?? 'Unknown error';
// Handle specific error codes
switch ($errorCode) {
case '-6':
Log::error('AntiVPN: GetIPIntel API error -6: Invalid contact email. Please provide a valid email address.');
break;
case '-5':
Log::error('AntiVPN: GetIPIntel API error -5: IP banned or rate limit exceeded.');
break;
case '-4':
Log::error('AntiVPN: GetIPIntel API error -4: Database unavailable.');
break;
case '-3':
Log::error('AntiVPN: GetIPIntel API error -3: Private/unroutable IP address.');
break;
case '-2':
Log::error('AntiVPN: GetIPIntel API error -2: Invalid IP address format.');
break;
case '-1':
Log::error('AntiVPN: GetIPIntel API error -1: No input provided.');
break;
default:
Log::error('AntiVPN: GetIPIntel API error ' . $errorCode . ': ' . $errorMessage);
}
if ($gracefulFallback) {
Log::info('AntiVPN: Graceful fallback - allowing request due to GetIPIntel API error');
return ['is_vpn' => false, 'is_suspicious' => false, 'provider' => 'getipintel', 'fallback' => true];
}
return null;
}
// GetIPIntel returns a score between 0-1
$score = isset($data['result']) ? (float) $data['result'] : 0.0;
// According to GetIPIntel docs:
// Values > 0.95 should be looked at
// Values > 0.99 are most likely proxies
// Values below 0.90 are considered "low risk"
$isProxy = $score >= 0.95;
$isSuspicious = $score >= 0.90 && $score < 0.95;
$this->debugLog('GetIPIntel score: ' . $score . ' - Proxy: ' . ($isProxy ? 'Y' : 'N') . ', Suspicious: ' . ($isSuspicious ? 'Y' : 'N'));
return [
'is_vpn' => $isProxy,
'is_suspicious' => $isSuspicious,
'provider' => 'getipintel',
'raw_response' => $data,
'score' => $score
];
} catch (\Exception $e) {
Log::error('AntiVPN: GetIPIntel API exception - ' . $e->getMessage());
return null;
}
}
/**
* Debug logging helper - only logs when debug mode is enabled
*/
private function debugLog(string $message): void
{
if ($this->debugMode) {
Log::info('AntiVPN DEBUG: ' . $message);
}
}
}