# Análisis Técnico y Operativo: Asignación y Gestión de Direcciones IP para Clientes

**Proyecto:** Dev-ISP (Sistema de Gestión y Facturación ISP SaaS)  
**Fecha de Análisis:** Octubre 2026  
**Documento:** `ip.md`  
**Estado:** ✅ IMPLEMENTADO Y EN PRODUCCIÓN (Las 4 Fases Ejecutadas y Verificadas)  

---

## 1. Resumen Ejecutivo y Diagnóstico Rápido

| Pregunta Clave | Estado Actual | Detalle Técnico |
| :--- | :---: | :--- |
| **¿Cómo se asigna actualmente la IP a un cliente?** | **Manual / Simulado** | En los formularios ([`Create.jsx`](file:///c:/laragon/www/devisp/resources/js/Pages/App/Clientes/Create.jsx) y modal de [`Index.jsx`](file:///c:/laragon/www/devisp/resources/js/Pages/App/Clientes/Index.jsx)), la IP se ingresa de forma manual. El botón *"Buscar IP disponible"* ejecuta un generador aleatorio en JavaScript (`10.255.4.X`) sin consultar la red ni la base de datos. |
| **¿Existe validación de unicidad (anti-duplicados)?** | **NO** | Ni en base de datos ni en el controlador existe una restricción de unicidad (`unique`). Clientes diferentes pueden tener asignada la misma IP sin que el sistema genere advertencia o bloqueo. |
| **¿Existe validación de formato IPv4 estricto?** | **NO** | En [`ClienteController.php`](file:///c:/laragon/www/devisp/app/Http/Controllers/App/ClienteController.php) el campo se valida únicamente como `'nullable|string|max:255'`. |
| **¿Los clientes creados por Excel reciben IP?** | **Opcional / Nula** | Si el archivo Excel contiene una columna de IP, se guarda; si la columna viene vacía o no existe, el cliente se guarda con `ip = null`. No hay asignación automática desde un pool. |
| **¿Qué ocurre si un cliente no tiene IP asignada?** | **Riesgo en RouterOS** | En [`MikrotikService.php`](file:///c:/laragon/www/devisp/app/Services/MikrotikService.php), los scripts de corte y reconexión usan por defecto `0.0.0.0`, lo que genera reglas de firewall y colas simples inválidas en MikroTik. |
| **¿Están operativos los Pools de IP en Routers/Zonas?** | **Inactivos (Solo BD)** | Los campos `rangos_ip` y el switch `ips_disponibles_sistema` existen en las tablas `routers` y `zonas`, pero no cuentan con lógica en backend para calcular subredes CIDR ni reservar IPs libres. |

---

## 2. Auditoría en Base de Datos Real

Tras examinar la base de datos en producción de la instancia, se obtuvieron las siguientes métricas exactas:

* **Total de clientes registrados:** `448`
* **Clientes con IP asignada:** `10` (~2.2%)
* **Clientes con IP nula / vacía:** `438` (~97.8%)
* **Caso real de colisión / duplicación detectado:**
  * **Cliente ID 1 (Juan):** IP `172.30.6.105` (Router ID 2 - CORPORATIVO)
  * **Cliente ID 2 (María):** IP `172.30.6.105` (Router ID 6 - PPPOE_PLAN_SERVER_1)
  * Ambos clientes comparten la misma IP sin restricción en el sistema.
* **Estado de los Routers MikroTik registrados:**
  * Existen 6 routers registrados (`CCR1009`, `CORPORATIVO`, servidores PPPoE 1 al 4).
  * Todos presentan el campo `rangos_ip = null` y `ips_disponibles_sistema = 0`.

---

## 3. Arquitectura y Flujo Técnico Actual

### 3.1. En el Frontend (Búsqueda Simulada)
En [`resources/js/Pages/App/Clientes/Create.jsx`](file:///c:/laragon/www/devisp/resources/js/Pages/App/Clientes/Create.jsx#L228-L231) y [`Index.jsx`](file:///c:/laragon/www/devisp/resources/js/Pages/App/Clientes/Index.jsx#L445-L447):

```javascript
// Función que responde al botón de lupa "Buscar IP disponible"
const handleFindAvailableIp = () => {
    const nextIp = `10.255.4.${Math.floor(20 + Math.random() * 200)}`;
    setData('ip', nextIp);
};
```
* **Problema:** Genera una dirección aleatoria en el rango `10.255.4.[20-219]`. No toma en cuenta la zona seleccionada, el router asignado, las subredes del ISP ni las IPs que ya están ocupadas por otros abonados.

### 3.2. En el Backend (Almacenamiento y Validación)
En [`app/Http/Controllers/App/ClienteController.php`](file:///c:/laragon/www/devisp/app/Http/Controllers/App/ClienteController.php#L126):

```php
$validated = $request->validate([
    // ...
    'ip' => 'nullable|string|max:255',
    'ipv6' => 'nullable|string|max:255',
    'remote_ipv6_prefix' => 'nullable|string|max:255',
    // ...
]);
```
* **Problema:** Falta la validación de formato `ipv4` y la regla de unicidad por empresa (`unique:clientes,ip,NULL,id,empresa_id,$empresaId`).

### 3.3. En la Carga Masiva desde Excel
En [`app/Services/ClienteExcelImportService.php`](file:///c:/laragon/www/devisp/app/Services/ClienteExcelImportService.php#L641-L643):

```php
if (!empty($ipRaw)) {
    $cliente->ip = trim((string)$ipRaw);
}
```
* Si el Excel no contiene una columna de IP, la propiedad queda nula. No existe un mecanismo que asigne la siguiente IP disponible dentro de la zona del cliente importado.

### 3.4. En la Integración con MikroTik RouterOS
En [`app/Services/MikrotikService.php`](file:///c:/laragon/www/devisp/app/Services/MikrotikService.php#L21-L54):

```php
$ip = trim($cliente->ip ?? '0.0.0.0');
$queueName = $cliente->nombre_simple_queue ?: "Q-{$ip}";

switch ($tipoCorte) {
    case 'Corte de servicio por address list moroso':
        $commands[] = "/ip firewall address-list remove [find list=morosos address=\"{$ip}\"]";
        $commands[] = "/ip firewall address-list add list=morosos address={$ip} ...";
        $commands[] = "/queue simple set [find target=\"{$ip}/32\"] comment=\"CORTADO POR MOROSIDAD\"";
        break;
}
```
* Si el cliente tiene `ip = null`, se envían comandos a RouterOS apuntando a `0.0.0.0/32`. Esto puede bloquear la tabla ARP o anular colas maestras en el router físico.

---

## 4. Diagrama Comparativo: Flujo Actual vs Flujo Óptimo

```mermaid
flowchart TD
    subgraph Flujo_Actual[" Flujo Actual (Vulnerable) "]
        A1[Usuario presiona 'Buscar IP'] --> B1["Math.random() en JS (10.255.4.X)"]
        B1 --> C1[Se coloca IP sin verificar BD ni Router]
        C1 --> D1["Guardar Cliente (Sin validación unique)"]
        D1 --> E1{¿IP vacía?}
        E1 -- SÍ --> F1["MikrotikService usa 0.0.0.0 (Error en RouterOS)"]
        E1 -- NO --> G1["Riesgo de IP duplicada con otro cliente"]
    end

    subgraph Flujo_Propuesto[" Flujo Recomendado con IpPoolService "]
        A2[Usuario presiona 'Buscar IP'] --> B2["Llamada API a /app/clientes/siguiente-ip-disponible"]
        B2 --> C2["IpPoolService analiza rangos_ip del Router/Zona"]
        C2 --> D2["Excluye IPs activas en la empresa"]
        D2 --> E2[Retorna primera IP libre garantizada]
        E2 --> F2[Validación Backend: ipv4 + unique por empresa]
        F2 --> G2["Comandos MikroTik limpios y sin colisiones"]
    end
```

---

## 5. Matriz de Riesgos Operativos Identificados

| Nivel de Riesgo | Componente | Descripción de la Falla | Consecuencia en Red ISP |
| :---: | :--- | :--- | :--- |
| 🔴 **Crítico** | `MikrotikService` | Clientes sin IP ejecutan cortes sobre `target=0.0.0.0/32` | Bloqueo imprevisto de rutas predeterminadas o error de ejecución API en RouterOS. |
| 🔴 **Crítico** | `Base de Datos` | Ausencia de índice único (`unique`) para `(empresa_id, ip)` | Conflicto de IP (ARP spoofing, desconexión intermitente entre dos abonados). |
| 🟡 **Medio** | `Frontend (Create/Edit)` | Búsqueda basada en `Math.random()` | Asignación de IPs ficticias fuera del direccionamiento del nodo o repetidas. |
| 🟡 **Medio** | `Router / Zona` | Segmentos `rangos_ip` sin parseo CIDR | El administrador configura subredes en el router pero el sistema no las gestiona ni sabe cuántas IPs quedan libres. |
| 🟢 **Bajo** | `Trafico / Mapa` | Fallback visual con `$c->id` (`172.30.6.10X`) | Muestra IPs calculadas al vuelo que no coinciden con la ficha del cliente. |

---

## 6. Plan de Implementación de la Solución (Roadmap Ejecutado)

### [x] Fase 1: Creación del Servicio de Gestión de Pools (`IpPoolService`)
**Implementado en:** [`App\Services\IpPoolService.php`](file:///c:/laragon/www/devisp/app/Services/IpPoolService.php)
1. **Parser de Subredes CIDR y Rangos:** Interpreta formatos CIDR (`192.168.10.0/24`, `10.50.0.0/22`), rangos explícitos (`10.0.0.50-10.0.0.100`), rangos abreviados (`172.16.1.200-250`) e IPs estáticas individuales.
2. **Exclusión de IPs Reservadas:** Reserva automáticamente la dirección de red (`.0`), el gateway por defecto (`.1`) y la dirección de broadcast.
3. **Cálculo de Ocupación:** Contrasta en tiempo real contra los clientes de la empresa: total del pool, ocupadas, disponibles y porcentaje de uso.
4. **Asignación Atómica y Determinística:** Método `obtenerSiguienteIpLibre($routerId, $zonaId, $empresaId, $excluirClienteId)`.

### [x] Fase 2: Endpoint API para el Frontend
**Implementado en:** [`routes/web.php`](file:///c:/laragon/www/devisp/routes/web.php) y [`ClienteController.php`](file:///c:/laragon/www/devisp/app/Http/Controllers/App/ClienteController.php)
* **Ruta:** `GET /app/clientes/siguiente-ip-disponible`
* **Parámetros:** `router_id`, `zona_id`, `cliente_id` (opcional al editar)
* **Respuesta JSON:**
  ```json
  {
      "success": true,
      "ip": "10.255.4.2",
      "rango_origen": "10.255.4.0/24",
      "origen": "router",
      "nombre_origen": "Router: PPPOE_PLAN_SERVER_2",
      "disponibles_en_pool": 1012,
      "total_pool": 1012,
      "ocupadas_pool": 0,
      "porcentaje_uso": 0
  }
  ```

### [x] Fase 3: Conexión con la Interfaz de Usuario
**Implementado en:** [`Create.jsx`](file:///c:/laragon/www/devisp/resources/js/Pages/App/Clientes/Create.jsx) e [`Index.jsx`](file:///c:/laragon/www/devisp/resources/js/Pages/App/Clientes/Index.jsx)
* Se eliminó el generador aleatorio `Math.random()`.
* Al presionar la lupa o el botón *"IP Libre"*, se consulta vía Axios la API `/app/clientes/siguiente-ip-disponible` con los parámetros del router y zona actuales.
* Se incorporó spinner de carga (`<Loader2 className="animate-spin" />`) y mensaje interactivo de estado que indica la IP asignada, el origen (Zona/Router) y la cantidad de IPs libres en el pool.

### [x] Fase 4: Blindaje en Backend y MikroTik
1. **Validación en `ClienteController`:**
   En `store()` y `update()` se aplica validación estricta de formato `ipv4` y unicidad scoped por empresa:
   ```php
   'ip' => [
       'nullable',
       'ipv4',
       Rule::unique('clientes', 'ip')
           ->where(fn ($q) => $q->where('empresa_id', $empresaId)->whereNull('deleted_at'))
           ->ignore($cliente?->id),
   ],
   ```
2. **Salvaguarda en `MikrotikService`:**
   En [`MikrotikService.php`](file:///c:/laragon/www/devisp/app/Services/MikrotikService.php):
   - Si `$cliente->ip` es nulo, vacío o `0.0.0.0`, se registra advertencia en `Log::warning(...)` y se omiten los comandos de firewall o queues que apunten a `0.0.0.0/32`.
   - Se retorna el estado `'skipped_no_ip'`, protegiendo la tabla ARP y las colas maestras de RouterOS.
3. **Carga Masiva Excel (`ClienteExcelImportService`):**
   - En [`ClienteExcelImportService.php`](file:///c:/laragon/www/devisp/app/Services/ClienteExcelImportService.php), si el archivo Excel no incluye la columna IP, el sistema reserva y asigna automáticamente la siguiente IP libre del pool del router/zona correspondiente.
4. **Saneamiento de Base de Datos:**
   - Se resolvió la colisión previa entre Cliente ID 1 y Cliente ID 2 (reasignado a `172.30.6.106`).
   - Verificado: **0 IPs duplicadas** en la base de datos de producción.

---

## 7. Conclusión

El plan descrito en este documento ha sido ejecutado e integrado satisfactoriamente en Dev-ISP. El sistema cuenta ahora con asignación automática e inteligente de IPs, prevención total de colisiones y duplicados por empresa, compatibilidad total con RouterOS MikroTik y retroalimentación en tiempo real en los formularios de clientes.
