# 🛡️ Especificación y Plan Técnico: Módulo de Seguridad GeoIP y Cortafuegos de IPs para Aplicaciones Web PHP / MySQL

**Documento:** Plan de Arquitectura y Guía de Implementación Reutilizable  
**Tipo:** Estándar de Seguridad Aplicativa (WAF / GeoIP Firewall)  
**Compatibilidad:** PHP 7.4+, PHP 8.x, MySQL 5.7+, MariaDB 10.3+, Apache / Nginx / cPanel  

---

## 📑 Tabla de Contenidos
1. [Resumen Ejecutivo y Objetivos](#1-resumen-ejecutivo-y-objetivos)
2. [Arquitectura de Seguridad y Flujo de Intercepción](#2-arquitectura-de-seguridad-y-flujo-de-intercepción)
3. [Modelo de Base de Datos Universal (DDL)](#3-modelo-de-base-de-datos-universal-ddl)
4. [Jerarquía de Políticas: Superadmin (Global) vs Empresa (Tenant)](#4-jerarquía-de-políticas-superadmin-global-vs-empresa-tenant)
5. [Motor de Detección y Geolocalización con Caché Persistente](#5-motor-de-detección-y-geolocalización-con-caché-persistente)
6. [Algoritmo de Validación y Cotejo de Rangos CIDR](#6-algoritmo-de-validación-y-cotejo-de-rangos-cidr)
7. [Interfaz de Usuario y Gestión Visual](#7-interfaz-de-usuario-y-gestión-visual)
8. [Plantilla de Pantalla de Bloqueo (HTTP 403 Forbidden)](#8-plantilla-de-pantalla-de-bloqueo-http-403-forbidden)
9. [Guía Paso a Paso para Integrar en Cualquier Proyecto PHP](#9-guía-paso-a-paso-para-integrar-en-cualquier-proyecto-php)

---

## 1. Resumen Ejecutivo y Objetivos

Este módulo provee un **Cortafuegos a Nivel de Aplicación (Application-Level Firewall)** diseñado para:
1. **Restringir el acceso geográfico (Geo-Fencing):** Permitir conexiones únicamente desde países autorizados (ej. solo Chile `CL` y países vecinos) o bloquear países de origen de ciberataques/botnets recurrentes.
2. **Control por Lista Blanca y Lista Negra de IPs:** Bloqueo o autorización de direcciones IP individuales (`IPv4` / `IPv6`) o subredes completas (`CIDR`).
3. **Alto Rendimiento (<1ms):** Las consultas de geolocalización se almacenan en una tabla de caché local (`ip_geo_cache`), evitando llamadas externas repetidas.
4. **Protección Anti-Bloqueo de Red Local:** Las subredes privadas (`127.0.0.1`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`) están siempre autorizadas por defecto.

---

## 2. Arquitectura de Seguridad y Flujo de Intercepción

```mermaid
flowchart TD
    A[Petición Entrante HTTP/HTTPS] --> B[Obtener IP del Cliente]
    B --> C{¿Es IP Local / Privada?}
    C -- Sí --> D[Permitir Acceso Inmediato]
    C -- No --> E{¿Está en Lista Blanca de IPs?}
    E -- Sí --> D
    E -- No --> F{¿Está en Lista Negra de IPs?}
    F -- Sí --> G[Bloquear HTTP 403 + Registrar Auditoría]
    F -- No --> H{¿Cortafuegos GeoIP Activo?}
    H -- No --> D
    H -- Sí --> I[Consultar País de la IP en ip_geo_cache / API]
    I --> J{¿Evaluar Jerarquía Superadmin vs Empresa?}
    J --> K{¿País Autorizado según Política?}
    K -- Sí --> D
    K -- No --> G
```

---

## 3. Modelo de Base de Datos Universal (DDL)

```sql
-- 1. Tabla de Reglas de Seguridad (Países e IPs)
CREATE TABLE IF NOT EXISTS `geo_ip_rules` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `empresa_id` INT(11) DEFAULT NULL COMMENT 'NULL = Regla Global del Sistema / Superadmin',
  `tipo` ENUM('country_whitelist','country_blacklist','ip_whitelist','ip_blacklist') NOT NULL,
  `valor` VARCHAR(100) NOT NULL COMMENT 'Código ISO país (ej: CL, PE) o IP/CIDR (ej: 190.45.1.0/24)',
  `pais_nombre` VARCHAR(100) DEFAULT NULL COMMENT 'Nombre legible del país',
  `descripcion` VARCHAR(255) DEFAULT NULL,
  `creado_por` VARCHAR(50) DEFAULT NULL,
  `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_rule_empresa` (`empresa_id`),
  KEY `idx_rule_tipo` (`tipo`),
  KEY `idx_rule_valor` (`valor`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- 2. Tabla de Caché de Geolocalización IP (Alto Rendimiento)
CREATE TABLE IF NOT EXISTS `ip_geo_cache` (
  `ip` VARCHAR(45) NOT NULL,
  `country_code` VARCHAR(5) NOT NULL,
  `country_name` VARCHAR(100) NOT NULL,
  `city` VARCHAR(100) DEFAULT NULL,
  `isp` VARCHAR(150) DEFAULT NULL,
  `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`ip`),
  KEY `idx_cache_country` (`country_code`),
  KEY `idx_cache_updated` (`updated_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- 3. Parámetros de Configuración Global (en tabla configuracion o similar)
INSERT INTO `configuracion` (`clave`, `valor`) VALUES 
('geo_blocking_enabled', '1') ON DUPLICATE KEY UPDATE `valor`=`valor`;
INSERT INTO `configuracion` (`clave`, `valor`) VALUES 
('geo_mode', 'whitelist') ON DUPLICATE KEY UPDATE `valor`=`valor`; -- 'whitelist' o 'blacklist'
INSERT INTO `configuracion` (`clave`, `valor`) VALUES 
('geo_allow_company_exceptions', '0') ON DUPLICATE KEY UPDATE `valor`=`valor`; -- '1' = permite a empresas añadir países

-- Regla inicial recomendada: Permitir Chile (CL) por defecto
INSERT IGNORE INTO `geo_ip_rules` (`empresa_id`, `tipo`, `valor`, `pais_nombre`, `descripcion`, `creado_por`) 
VALUES (NULL, 'country_whitelist', 'CL', 'Chile', 'Regla base de instalación inicial', 'sistema');
```

---

## 4. Jerarquía de Políticas: Superadmin (Global) vs Empresa (Tenant)

| Nivel | Rol | Alcance | Comportamiento |
| :--- | :--- | :--- | :--- |
| **Nivel 1** | **Super Administrador** | **Global (Servidor Completo)** | Define si el cortafuegos está activo, el modo de operación (`whitelist`/`blacklist`), la lista de países base del servidor y las IPs globales autorizadas/bloqueadas. |
| **Interruptor de Delegación** | **Super Administrador** | **Parámetro `geo_allow_company_exceptions`** | **`0` (Apagado):** Rige estrictamente la política global del Superadmin para todos.<br>**`1` (Encendido):** Cada empresa puede habilitar países adicionales o restringir los suyos. |
| **Nivel 2** | **Administrador de Empresa** | **Sede / Empresa Específica** | Si la delegación está activa, administra los países permitidos para sus propios operadores y sus listas de IPs locales. No altera las políticas de otras empresas ni del Superadmin. |

---

## 5. Motor de Detección y Geolocalización con Caché Persistente

```php
<?php
// Funciones Helper para db.php / seguridad_core.php

function is_private_ip($ip) {
    if (in_array($ip, ['127.0.0.1', '::1', 'localhost'])) return true;
    return !filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
}

function get_client_ip_real() {
    $headers = [
        'HTTP_CF_CONNECTING_IP', // Cloudflare
        'HTTP_X_REAL_IP',        // Nginx proxy
        'HTTP_X_FORWARDED_FOR',  // Load balancers / proxies
        'REMOTE_ADDR'            // Conexión directa
    ];
    foreach ($headers as $header) {
        if (!empty($_SERVER[$header])) {
            $ip_list = explode(',', $_SERVER[$header]);
            $ip = trim($ip_list[0]);
            if (filter_var($ip, FILTER_VALIDATE_IP)) {
                return $ip;
            }
        }
    }
    return $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
}

function get_ip_geolocation($ip) {
    global $pdo;

    // 1. Detección de Red Local / Privada
    if (is_private_ip($ip)) {
        return [
            'country_code' => 'LOCAL',
            'country_name' => 'Red Local / Privada',
            'city' => 'Localhost',
            'isp' => 'LAN'
        ];
    }

    // 2. Detección rápida por cabecera Cloudflare / cPanel
    if (!empty($_SERVER['HTTP_CF_IPCOUNTRY'])) {
        $country = strtoupper(trim($_SERVER['HTTP_CF_IPCOUNTRY']));
        return [
            'country_code' => $country,
            'country_name' => $country,
            'city' => '',
            'isp' => 'Cloudflare Detected'
        ];
    }

    // 3. Revisar en Caché de Base de Datos
    try {
        $stmt = $pdo->prepare("SELECT country_code, country_name, city, isp FROM ip_geo_cache WHERE ip = ?");
        $stmt->execute([$ip]);
        $cached = $stmt->fetch(PDO::FETCH_ASSOC);
        if ($cached) {
            return $cached;
        }
    } catch (Exception $e) {}

    // 4. Consulta a Servicio GeoIP Ligero (Fallback)
    $geo_data = ['country_code' => 'XX', 'country_name' => 'Desconocido', 'city' => '', 'isp' => ''];
    $apis = [
        "https://ipapi.co/{$ip}/json/",
        "http://ip-api.com/json/{$ip}?fields=status,country,countryCode,city,isp"
    ];

    foreach ($apis as $url) {
        $ctx = stream_context_create(['http' => ['timeout' => 1.5, 'user_agent' => 'GeoIP-Firewall-Client/1.0']]);
        $json = @file_get_contents($url, false, $ctx);
        if ($json) {
            $data = json_decode($json, true);
            if (!empty($data['country_code']) || !empty($data['countryCode'])) {
                $geo_data['country_code'] = strtoupper($data['country_code'] ?? $data['countryCode']);
                $geo_data['country_name'] = $data['country_name'] ?? $data['country'] ?? $geo_data['country_code'];
                $geo_data['city'] = $data['city'] ?? '';
                $geo_data['isp'] = $data['isp'] ?? ($data['org'] ?? '');
                break;
            }
        }
    }

    // 5. Guardar en Caché de Base de Datos
    try {
        $stmt = $pdo->prepare("INSERT INTO ip_geo_cache (ip, country_code, country_name, city, isp) 
                               VALUES (?, ?, ?, ?, ?) 
                               ON DUPLICATE KEY UPDATE country_code=VALUES(country_code), country_name=VALUES(country_name), updated_at=NOW()");
        $stmt->execute([$ip, $geo_data['country_code'], $geo_data['country_name'], $geo_data['city'], $geo_data['isp']]);
    } catch (Exception $e) {}

    return $geo_data;
}
```

---

## 6. Algoritmo de Validación y Cotejo de Rangos CIDR

```php
<?php
function ip_matches_cidr($ip, $cidr) {
    if ($ip === $cidr) return true;
    if (strpos($cidr, '/') === false) return $ip === $cidr;

    list($subnet, $bits) = explode('/', $cidr);
    if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) || !filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
        return false;
    }

    $ip_long = ip2long($ip);
    $subnet_long = ip2long($subnet);
    $mask = -1 << (32 - (int)$bits);
    $subnet_long &= $mask;

    return ($ip_long & $mask) == $subnet_long;
}

function verify_geo_firewall_access($empresa_id = null) {
    global $pdo;

    $enabled = get_config('geo_blocking_enabled', '0');
    if ($enabled !== '1') return true; // Cortafuegos apagado

    $client_ip = get_client_ip_real();

    // 1. Red Local -> Siempre Autorizada
    if (is_private_ip($client_ip)) return true;

    // 2. Comprobar Lista Blanca de IPs (Global o de la Empresa)
    $emp_id_clean = (int)$empresa_id;
    $stmt = $pdo->prepare("SELECT valor FROM geo_ip_rules WHERE tipo = 'ip_whitelist' AND (empresa_id IS NULL OR empresa_id = ?)");
    $stmt->execute([$emp_id_clean]);
    $whitelisted_ips = $stmt->fetchAll(PDO::FETCH_COLUMN);

    foreach ($whitelisted_ips as $rule_ip) {
        if (ip_matches_cidr($client_ip, trim($rule_ip))) {
            return true; // IP explícitamente autorizada
        }
    }

    // 3. Comprobar Lista Negra de IPs
    $stmt = $pdo->prepare("SELECT valor FROM geo_ip_rules WHERE tipo = 'ip_blacklist' AND (empresa_id IS NULL OR empresa_id = ?)");
    $stmt->execute([$emp_id_clean]);
    $blacklisted_ips = $stmt->fetchAll(PDO::FETCH_COLUMN);

    foreach ($blacklisted_ips as $rule_ip) {
        if (ip_matches_cidr($client_ip, trim($rule_ip))) {
            render_blocked_page($client_ip, 'IP en Lista Negra de Seguridad');
            exit;
        }
    }

    // 4. Evaluación de País (GeoIP)
    $geo_info = get_ip_geolocation($client_ip);
    $country_code = $geo_info['country_code'];
    $mode = get_config('geo_mode', 'whitelist'); // 'whitelist' o 'blacklist'
    $allow_company_exceptions = get_config('geo_allow_company_exceptions', '0') === '1';

    // Obtener países autorizados o bloqueados
    $query = "SELECT valor FROM geo_ip_rules WHERE tipo = ? AND (empresa_id IS NULL" . 
             (($allow_company_exceptions && $emp_id_clean > 0) ? " OR empresa_id = $emp_id_clean" : "") . ")";
    
    if ($mode === 'whitelist') {
        $stmt = $pdo->prepare(str_replace('?', "'country_whitelist'", $query));
        $stmt->execute();
        $allowed_countries = $stmt->fetchAll(PDO::FETCH_COLUMN);

        if (!in_array($country_code, $allowed_countries)) {
            if (function_exists('log_action')) {
                log_action('IP_GEO_BLOCKED', "Acceso bloqueado a IP $client_ip desde país no autorizado: {$geo_info['country_name']} ($country_code)", $empresa_id);
            }
            render_blocked_page($client_ip, "Acceso restringido desde su país de origen ({$geo_info['country_name']})");
            exit;
        }
    } else { // Mode: blacklist
        $stmt = $pdo->prepare(str_replace('?', "'country_blacklist'", $query));
        $stmt->execute();
        $blocked_countries = $stmt->fetchAll(PDO::FETCH_COLUMN);

        if (in_array($country_code, $blocked_countries)) {
            if (function_exists('log_action')) {
                log_action('IP_GEO_BLOCKED', "Acceso bloqueado a IP $client_ip desde país en lista negra: {$geo_info['country_name']} ($country_code)", $empresa_id);
            }
            render_blocked_page($client_ip, "Acceso bloqueado por política de país ({$geo_info['country_name']})");
            exit;
        }
    }

    return true;
}
```

---

## 7. Interfaz de Usuario y Gestión Visual

El panel de administración (`seguridad.php`) debe contemplar 4 secciones clave:

1. **Panel de Estado del Cortafuegos:**
   - Interruptor Maestro: **Activado / Desactivado**.
   - Modo de Filtrado: **Solo Países Autorizados (Lista Blanca)** vs **Bloquear Países Específicos (Lista Negra)**.
   - Switch de Delegación (Solo Superadmin): **"Permitir excepciones por empresa"**.

2. **Gestión de Países (GeoIP):**
   - Selector visual con buscador, nombre de país, bandera / código ISO y botón para agregar con 1 clic.
   - Tabla de países activos con indicador de quién creó la regla (Global vs Empresa).

3. **Gestión de Direcciones IP:**
   - **Lista Blanca (IPs de Confianza):** Botón *"Añadir mi IP actual"* para evitar auto-bloqueos.
   - **Lista Negra (IPs Bloqueadas):** Para mitigar intentos de fuerza bruta repetidos.

4. **Probador de Conexión y Simulador de IPs:**
   - Campo para ingresar cualquier dirección IP externa y botón **"Simular Acceso"**.
   - Muestra el país detectado, ciudad, ISP y si el sistema permitiría o bloquearía la conexión bajo las reglas actuales.

---

## 8. Plantilla de Pantalla de Bloqueo (HTTP 403 Forbidden)

```php
<?php
function render_blocked_page($ip, $reason = '') {
    http_response_code(403);
?>
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>403 - Acceso Restringido por Seguridad</title>
    <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;700&display=swap" rel="stylesheet">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.min.css" rel="stylesheet">
    <style>
        body { font-family: 'Outfit', sans-serif; background-color: #0f172a; color: #f8fafc; height: 100vh; display: flex; align-items: center; justify-content: center; margin: 0; }
        .card-block { background: #1e293b; border: 1px solid #334155; border-radius: 20px; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5); max-width: 540px; padding: 40px; text-align: center; }
        .icon-shield { width: 80px; height: 80px; background: rgba(239, 68, 68, 0.15); color: #ef4444; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 40px; margin: 0 auto 24px; border: 2px solid rgba(239, 68, 68, 0.3); }
    </style>
</head>
<body>
    <div class="card-block">
        <div class="icon-shield"><i class="bi bi-shield-x"></i></div>
        <h2 class="fw-bold mb-2">Acceso No Autorizado</h2>
        <p class="text-slate-400 mb-4">La conexión a este servidor ha sido restringida debido a las políticas de seguridad geográfica y cortafuegos de la plataforma.</p>
        
        <div class="p-3 bg-slate-900 rounded-3 text-start mb-4 border border-slate-700" style="font-size: 0.9rem;">
            <div class="mb-1"><strong class="text-slate-300">Dirección IP:</strong> <code class="text-info"><?= htmlspecialchars($ip) ?></code></div>
            <div><strong class="text-slate-300">Motivo:</strong> <span class="text-danger"><?= htmlspecialchars($reason ?: 'Filtro Geográfico Activo') ?></span></div>
        </div>
        
        <p class="small text-slate-500 mb-0">Si considera que esto es un error, por favor contacte al Administrador del Sistema con los detalles de su IP.</p>
    </div>
</body>
</html>
<?php
}
```

---

## 9. Guía Paso a Paso para Integrar en Cualquier Proyecto PHP

1. **Ejecutar el script SQL** con las tablas `geo_ip_rules` e `ip_geo_cache`.
2. **Incluir las funciones core** (`get_client_ip_real`, `get_ip_geolocation`, `verify_geo_firewall_access`) en el archivo central de base de datos o middleware (`db.php` / `init.php`).
3. **Llamar al interceptor** al inicio de cada script protegido:
   ```php
   require_once 'db.php';
   verify_geo_firewall_access(get_empresa_id()); // Valida la IP antes de ejecutar cualquier lógica
   ```
4. **Agregar la interfaz visual** (`seguridad.php`) dentro del panel de administración para que los administradores gestionen las reglas con comodidad.
